From 4489ce6b135c96dcbffeb681713f2f310870c4c9 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:48:27 -0500 Subject: [PATCH 01/44] add type hinting --- src/innov8/components/decorators.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/innov8/components/decorators.py b/src/innov8/components/decorators.py index e1439f5..5251423 100644 --- a/src/innov8/components/decorators.py +++ b/src/innov8/components/decorators.py @@ -1,11 +1,20 @@ +from functools import wraps +from typing import Any, Callable, TypeVar + +from typing_extensions import Concatenate, ParamSpec + from innov8.app import app from innov8.db_ops import data +P = ParamSpec("P") +R = TypeVar("R") + + +def data_access(func: Callable[Concatenate[Any, P], R]) -> Callable[P, R]: + """Decorator which passes the db_ops.data instance as the first parameter of any function it decorates""" -# Define a decorator which passes the data_instance as the first parameter -# of any function it decorates -def data_access(func): - def inner(*args, **kwargs): + @wraps(func) + def inner(*args: P.args, **kwargs: P.kwargs) -> R: return func(data, *args, **kwargs) return inner From d40547623bd47cec4f94f0c9fe6607fb73b13195 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:52:55 -0500 Subject: [PATCH 02/44] move decorator, adjust and ignore some types --- src/innov8/app.py | 1 + src/innov8/components/charts_52w.py | 4 ++-- src/innov8/components/dropdowns.py | 2 +- src/innov8/components/forcast.py | 2 +- src/innov8/components/intra_sector.py | 2 +- src/innov8/components/main_carousel.py | 2 +- src/innov8/components/price_card.py | 2 +- src/innov8/components/price_chart.py | 2 +- src/innov8/components/update.py | 2 +- src/innov8/db_ops.py | 17 +++++++++++++---- .../decorators.py => decorators/data_access.py} | 0 src/innov8/update_all.py | 2 +- 12 files changed, 24 insertions(+), 14 deletions(-) rename src/innov8/{components/decorators.py => decorators/data_access.py} (100%) diff --git a/src/innov8/app.py b/src/innov8/app.py index d811cb6..4dd9eed 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -18,4 +18,5 @@ title="innov8finance", background_callback_manager=background_callback_manager, suppress_callback_exceptions=True, + serve_locally=False, ) diff --git a/src/innov8/components/charts_52w.py b/src/innov8/components/charts_52w.py index 96a999c..fda9d58 100644 --- a/src/innov8/components/charts_52w.py +++ b/src/innov8/components/charts_52w.py @@ -4,7 +4,7 @@ from dash.dependencies import Input, Output from dash_bootstrap_templates import ThemeChangerAIO, template_from_url -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access # Carousel showing 52-week data @@ -85,7 +85,7 @@ def update_52_week_charts(data, symbol, theme, update): gauge={ "axis": {"range": [df_52_week_low, df_52_week_high]}, # Set bar color to theme's primary color (extracted from previous chart) - "bar": {"color": fig.layout.template.layout.colorway[0]}, + "bar": {"color": fig.layout.template.layout.colorway[0]}, # type: ignore }, domain={"x": [0, 1], "y": [0, 0.9]}, ) diff --git a/src/innov8/components/dropdowns.py b/src/innov8/components/dropdowns.py index aeb0d7c..3b10b1d 100644 --- a/src/innov8/components/dropdowns.py +++ b/src/innov8/components/dropdowns.py @@ -1,7 +1,7 @@ from dash import dcc from dash.dependencies import Input, Output -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access # list all sectors sector_query = """ diff --git a/src/innov8/components/forcast.py b/src/innov8/components/forcast.py index 4bf4f5d..5000674 100644 --- a/src/innov8/components/forcast.py +++ b/src/innov8/components/forcast.py @@ -4,7 +4,7 @@ import dash_bootstrap_components as dbc from dash import Input, Output, State, ctx, no_update -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access def forecast_button() -> dbc.Button: diff --git a/src/innov8/components/intra_sector.py b/src/innov8/components/intra_sector.py index 253a9d4..e47fc20 100644 --- a/src/innov8/components/intra_sector.py +++ b/src/innov8/components/intra_sector.py @@ -4,7 +4,7 @@ from dash import dash_table, dcc, html from dash.dependencies import Input, Output -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access # Store intermediate values diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index 2a61e95..6f586b0 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -2,7 +2,7 @@ from dash import html from dash.dependencies import Input, Output -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access change_query = """ WITH growth AS ( diff --git a/src/innov8/components/price_card.py b/src/innov8/components/price_card.py index 50881b7..d1590e3 100644 --- a/src/innov8/components/price_card.py +++ b/src/innov8/components/price_card.py @@ -1,7 +1,7 @@ from dash import html from dash.dependencies import Input, Output -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access # div with main ticker information diff --git a/src/innov8/components/price_chart.py b/src/innov8/components/price_chart.py index edcad1d..dd28692 100644 --- a/src/innov8/components/price_chart.py +++ b/src/innov8/components/price_chart.py @@ -8,7 +8,7 @@ from dash.exceptions import PreventUpdate from dash_bootstrap_templates import ThemeChangerAIO, template_from_url -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access def hex_to_rgba(hex_color, alpha=1.0) -> str: diff --git a/src/innov8/components/update.py b/src/innov8/components/update.py index d7790d0..67464ef 100644 --- a/src/innov8/components/update.py +++ b/src/innov8/components/update.py @@ -7,7 +7,7 @@ from tqdm import tqdm from innov8 import update_all -from innov8.components.decorators import callback, data_access +from innov8.decorators.data_access import callback, data_access # Button with scope dropdown diff --git a/src/innov8/db_ops.py b/src/innov8/db_ops.py index 1ddb2ee..2cfd6cf 100644 --- a/src/innov8/db_ops.py +++ b/src/innov8/db_ops.py @@ -1,6 +1,6 @@ import numpy as np -np.float_ = np.float64 +np.float_ = np.float64 # type: ignore import logging import os @@ -56,7 +56,7 @@ def __init__(self, script_directory: Path): self.con = sqlite3.connect(self.db_path, check_same_thread=False) self.cur = self.con.cursor() self.ticker_symbols = None - self.main_table: pd.DataFrame = None + self.main_table: pd.DataFrame | None = None # Check if the database is populated by checking if the price table is present if not self.cur.execute( @@ -280,7 +280,10 @@ def fill_ohlc(self): )[["Open", "High", "Low", "Close", "Volume"]] # Convert the date to a unix timestamp (remove timezone holding local time representations) ohlc_data.index = ( - ohlc_data.index.tz_localize(None).astype("int64") / 10**9 + cast(pd.DatetimeIndex, ohlc_data.index) + .tz_localize(None) + .astype("int64") + / 10**9 ) ohlc_data.reset_index(inplace=True) # Convert to a list of dictionaries (records) @@ -335,6 +338,7 @@ def fill_ohlc(self): def generate_forecast(self, symbol: str) -> None: df = self.main_table + assert df is not None predictions = {} periods = 5 @@ -531,7 +535,12 @@ def add_new_ohlc(self, symbol): start=next_entry, raise_errors=True )[["Open", "High", "Low", "Close", "Volume"]] # Convert the date to a unix timestamp (remove timezone holding local time representations) - ohlc_data.index = ohlc_data.index.tz_localize(None).astype("int64") / 10**9 + ohlc_data.index = ( + cast(pd.DatetimeIndex, ohlc_data.index) + .tz_localize(None) + .astype("int64") + / 10**9 + ) ohlc_data.reset_index(inplace=True) # Convert to a list of dictionaries (records) ohlc_data = ohlc_data.to_dict(orient="records") diff --git a/src/innov8/components/decorators.py b/src/innov8/decorators/data_access.py similarity index 100% rename from src/innov8/components/decorators.py rename to src/innov8/decorators/data_access.py diff --git a/src/innov8/update_all.py b/src/innov8/update_all.py index e7bda10..9577665 100644 --- a/src/innov8/update_all.py +++ b/src/innov8/update_all.py @@ -2,7 +2,7 @@ import numpy as np -np.float_ = np.float64 +np.float_ = np.float64 # type: ignore from loguru import logger from tqdm import tqdm From 9b01a87d8f0c27a50386d60cb18f12cdf4498966 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:01:30 -0500 Subject: [PATCH 03/44] fix min width --- src/innov8/components/forcast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/innov8/components/forcast.py b/src/innov8/components/forcast.py index 5000674..2f353b4 100644 --- a/src/innov8/components/forcast.py +++ b/src/innov8/components/forcast.py @@ -13,7 +13,7 @@ def forecast_button() -> dbc.Button: children="Forecast", outline=True, color="success", - style={"height": "37px", "width": "100%"}, + style={"height": "37px", "width": "100%", "min-width": "min-content"}, ) @@ -32,7 +32,7 @@ def forecast_button() -> dbc.Button: ) @data_access def update_price_chart_w_forecast(data, button, series_data, symbol, _) -> Any: - style = {"height": "37px", "width": "100%"} + style = {"height": "37px", "width": "100%", "min-width": "min-content"} # On initial render or ticker switch if ctx.triggered_id != "forecast-button" or ctx.triggered_id == "update-state": From 23f2cf79490c37945a426b95037be40db2306399 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:47:02 -0500 Subject: [PATCH 04/44] create and use default style --- src/innov8/components/forcast.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/innov8/components/forcast.py b/src/innov8/components/forcast.py index 2f353b4..d74f8c5 100644 --- a/src/innov8/components/forcast.py +++ b/src/innov8/components/forcast.py @@ -6,6 +6,15 @@ from innov8.decorators.data_access import callback, data_access +default_style = { + "height": "37px", + "width": "100%", + "min-width": "fit-content", + "display": "flex", + "justifyContent": "center", + "alignItems": "center", +} + def forecast_button() -> dbc.Button: return dbc.Button( @@ -13,7 +22,7 @@ def forecast_button() -> dbc.Button: children="Forecast", outline=True, color="success", - style={"height": "37px", "width": "100%", "min-width": "min-content"}, + style=default_style, ) @@ -32,7 +41,7 @@ def forecast_button() -> dbc.Button: ) @data_access def update_price_chart_w_forecast(data, button, series_data, symbol, _) -> Any: - style = {"height": "37px", "width": "100%", "min-width": "min-content"} + style = default_style.copy() # On initial render or ticker switch if ctx.triggered_id != "forecast-button" or ctx.triggered_id == "update-state": From 51a84eb9abb0772a86bceb61fe2858480e6fee57 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:55:21 -0500 Subject: [PATCH 05/44] refactor callback output and input --- src/innov8/components/forcast.py | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/innov8/components/forcast.py b/src/innov8/components/forcast.py index d74f8c5..4f6551f 100644 --- a/src/innov8/components/forcast.py +++ b/src/innov8/components/forcast.py @@ -28,24 +28,33 @@ def forecast_button() -> dbc.Button: # Add forecast ohlc to main chart on button press @callback( - Output("tv-price-chart", "seriesData", allow_duplicate=True), - Output("forecast-button", "disabled"), - Output("forecast-button", "n_clicks"), - Output("forecast-button", "color"), - Output("forecast-button", "style"), Input("forecast-button", "n_clicks"), - State("tv-price-chart", "seriesData"), - Input("symbol-dropdown", "value"), - Input("update-state", "data"), + Input("tv-price-chart", "seriesData"), + State("symbol-dropdown", "value"), + output={ + "series_data": Output("tv-price-chart", "seriesData", allow_duplicate=True), + "forecast_button_disabled": Output("forecast-button", "disabled"), + "forecast_button_clicks": Output("forecast-button", "n_clicks"), + "forecast_button_color": Output("forecast-button", "color"), + "forecast_button_style": Output("forecast-button", "style"), + }, prevent_initial_call=True, ) @data_access -def update_price_chart_w_forecast(data, button, series_data, symbol, _) -> Any: +def update_price_chart_w_forecast(data, button, series_data, symbol) -> Any: style = default_style.copy() - # On initial render or ticker switch - if ctx.triggered_id != "forecast-button" or ctx.triggered_id == "update-state": - return (no_update, False, None, "success", style) + output = { + "series_data": no_update, + "forecast_button_disabled": False, + "forecast_button_clicks": no_update, + "forecast_button_color": "success", + "forecast_button_style": style, + } + + # Reset the button + if ctx.triggered_id != "forecast-button": + return output | {"forecast_button_clicks": None} # Change the button color and style depending on how many times the forecast button has been pressed match button: @@ -55,6 +64,7 @@ def update_price_chart_w_forecast(data, button, series_data, symbol, _) -> Any: color = "danger" case _: color = no_update + output |= {"forecast_button_color": color} match button: case 2 | 4: style |= {"filter": "hue-rotate(-7deg) contrast(1.05) brightness(0.75)"} @@ -80,6 +90,11 @@ def update_price_chart_w_forecast(data, button, series_data, symbol, _) -> Any: nxt = data.get_forecasts(symbol, forecast[4]) - return series_data, nxt is None, no_update, color, style + return output | { + "series_data": series_data, + "forecast_button_disabled": nxt is None, + } - return (no_update, True, no_update, color, style) + return output | { + "forecast_button_disabled": True, + } From 7b436d527481361548b79a0df309ff035b97f8fb Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:58:37 -0500 Subject: [PATCH 06/44] revert dash-bootstrap-templates to version 1.1.2 (to fix persistence) --- pdm.lock | 8 ++++---- pyproject.toml | 2 +- src/innov8/components/themes.py | 5 ----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pdm.lock b/pdm.lock index e3b9dfa..e68a625 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" -content_hash = "sha256:1488de8559caa6e8958dfd0661c9c48468b8f8ec27ec624c0c0ac6fea97dc676" +content_hash = "sha256:1ca99bab482fe38b3db562e7dc1335fff1897af0fe3fafa7ba0def98e35abe4c" [[package]] name = "beautifulsoup4" @@ -255,7 +255,7 @@ files = [ [[package]] name = "dash-bootstrap-templates" -version = "1.2.0" +version = "1.1.2" summary = "A collection of Plotly figure templates with a Bootstrap theme" groups = ["default"] dependencies = [ @@ -264,8 +264,8 @@ dependencies = [ "numpy", ] files = [ - {file = "dash_bootstrap_templates-1.2.0-py3-none-any.whl", hash = "sha256:450447bd068249fa8d1e9fdbd996de5ba28ff4d19a3073f5327d57f1401fd56f"}, - {file = "dash_bootstrap_templates-1.2.0.tar.gz", hash = "sha256:666dff73f091e4544337004667ed4c486767a6344695d7cfb2974c660f03aa0e"}, + {file = "dash-bootstrap-templates-1.1.2.tar.gz", hash = "sha256:ad09b6d22500b7de3fd6cfe4886389de653ef16320969915f728b4bac5a7ba30"}, + {file = "dash_bootstrap_templates-1.1.2-py3-none-any.whl", hash = "sha256:87bb1e9dd7ac475f07d1237159091b9d154aa80fdae5fe579fb868e87343dfcd"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 94d3a6c..0091152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [ dependencies = [ "dash[diskcache]==2.16.1", "dash-bootstrap-components", - "dash-bootstrap-templates", + "dash-bootstrap-templates==1.1.2", "dash-tvlwc>=0.1.1", "dash-trich-components", "requests", diff --git a/src/innov8/components/themes.py b/src/innov8/components/themes.py index 05781ca..bd666a9 100644 --- a/src/innov8/components/themes.py +++ b/src/innov8/components/themes.py @@ -50,11 +50,6 @@ "value": dbc.themes.JOURNAL, "label_id": "theme-switch-label", }, - { - "label": "Lumen", - "value": dbc.themes.LUMEN, - "label_id": "theme-switch-label", - }, { "label": "Minty", "value": dbc.themes.MINTY, From eb2b081d518a7f5635f3ad976517559a9857e10e Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:40:44 -0500 Subject: [PATCH 07/44] make imports cleaner --- src/innov8/components/__init__.py | 10 ++++++++++ src/innov8/layout.py | 25 ++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 src/innov8/components/__init__.py diff --git a/src/innov8/components/__init__.py b/src/innov8/components/__init__.py new file mode 100644 index 0000000..e74424e --- /dev/null +++ b/src/innov8/components/__init__.py @@ -0,0 +1,10 @@ +from innov8.components.charts_52w import carousel_52_week +from innov8.components.dropdowns import dropdown_1, dropdown_2 +from innov8.components.forcast import forecast_button +from innov8.components.initial_load import initial_load +from innov8.components.intra_sector import intra_sector_data, table_info +from innov8.components.main_carousel import carousel +from innov8.components.price_card import price_card +from innov8.components.price_chart import ema_switch, price_chart, sma_switch +from innov8.components.themes import theme_changer +from innov8.components.update import update_button, update_state diff --git a/src/innov8/layout.py b/src/innov8/layout.py index a629b42..9f2799b 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -2,15 +2,22 @@ from dash import dcc from innov8.app import app -from innov8.components.charts_52w import carousel_52_week -from innov8.components.dropdowns import dropdown_1, dropdown_2 -from innov8.components.forcast import forecast_button -from innov8.components.intra_sector import intra_sector_data, table_info -from innov8.components.main_carousel import carousel -from innov8.components.price_card import price_card -from innov8.components.price_chart import ema_switch, price_chart, sma_switch -from innov8.components.themes import theme_changer -from innov8.components.update import update_button, update_state +from innov8.components import ( + carousel, + carousel_52_week, + dropdown_1, + dropdown_2, + ema_switch, + forecast_button, + intra_sector_data, + price_card, + price_chart, + sma_switch, + table_info, + theme_changer, + update_button, + update_state, +) from innov8.db_ops import data From 4a213c13bb21865079848bb635895edca9f83990 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:45:49 -0500 Subject: [PATCH 08/44] place charts in just divs instead of bootstrap rows and cols --- src/innov8/layout.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/innov8/layout.py b/src/innov8/layout.py index 9f2799b..020e9d5 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -1,5 +1,5 @@ import dash_bootstrap_components as dbc -from dash import dcc +from dash import dcc, html from innov8.app import app from innov8.components import ( @@ -54,13 +54,9 @@ def layout() -> dbc.Container: justify="between", ), # This row contains the main price (candlestick) chart - dbc.Row( - [ - dbc.Col( - [price_chart()], - width=12, - ), - ], + html.Div( + price_chart(), + id="price-chart-container", ), # This row stores the theme changer component and indicators dbc.Row( @@ -98,17 +94,9 @@ def layout() -> dbc.Container: [ dbc.Row([dbc.Col([price_card()], width=12)]), dbc.Row([dbc.Col([table_info()], width=12)]), - dbc.Row( - [ - dbc.Col( - [ - dcc.Loading( - carousel_52_week(), type="circle" - ) - ], - width=12, - ) - ], + html.Div( + carousel_52_week(), + id="52-week-chart-container", ), ], width=3, # -''- From f2655ed66e74fe0985463f590375e4708e3e7aa1 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:48:35 -0500 Subject: [PATCH 09/44] add spinner on initial load --- src/innov8/assets/main.css | 39 +++++++++++++++++++++++++++ src/innov8/components/initial_load.py | 21 +++++++++++++++ src/innov8/layout.py | 2 ++ 3 files changed, 62 insertions(+) create mode 100644 src/innov8/assets/main.css create mode 100644 src/innov8/components/initial_load.py diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css new file mode 100644 index 0000000..06489a0 --- /dev/null +++ b/src/innov8/assets/main.css @@ -0,0 +1,39 @@ +/* Loading indicator */ +._dash-loading { + position: fixed; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + backdrop-filter: blur(3px); + z-index: 1000; + visibility: hidden; + background-color: rgba(0, 0, 0, 0.3); + transform: translate(-50%, -50%); +} + +._dash-loading::after { + content: ""; + width: 40px; + height: 40px; + border: 4px solid #ccc; + border-top-color: #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + /* z-index: 1001; */ + position: absolute; + top: 50%; + left: 50%; + visibility: visible; +} + +/* Spinner animation */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + diff --git a/src/innov8/components/initial_load.py b/src/innov8/components/initial_load.py new file mode 100644 index 0000000..de0aa6e --- /dev/null +++ b/src/innov8/components/initial_load.py @@ -0,0 +1,21 @@ +from typing import Any + +from dash import html +from dash.dependencies import Input, Output + +from innov8.decorators.data_access import callback + + +def initial_load() -> html.Div: + """Initial loading spinner indicator""" + return html.Div(id="initial-load", className="_dash-loading visible") + + +@callback( + Output("initial-load", "className"), + Input("52-week-price-chart", "figure"), + Input("52-week-high-low-indicator", "figure"), +) +def disable_spinner(*args: Any, **kwargs: Any) -> Any: + """Disable spinner when the last loaded charts are ready and enable main chart visibility""" + return "" diff --git a/src/innov8/layout.py b/src/innov8/layout.py index 020e9d5..d7ce15b 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -9,6 +9,7 @@ dropdown_2, ema_switch, forecast_button, + initial_load, intra_sector_data, price_card, price_chart, @@ -27,6 +28,7 @@ def layout() -> dbc.Container: data.load_main_table(force_update=False) return dbc.Container( [ + initial_load(), # A carousel for 10 tickers with the largest absolute change occupying the topmost row dbc.Row([dbc.Col([carousel()], width=12)]), dbc.Row( From 68374a9fb522981b32e2af59bedb795d4c033e8c Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:02:07 -0500 Subject: [PATCH 10/44] hide charts on initial load before rendered --- src/innov8/assets/miscellaneous.css | 7 +++++++ src/innov8/components/charts_52w.py | 3 ++- src/innov8/components/price_chart.py | 2 ++ src/innov8/layout.py | 9 ++++++++- 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/innov8/assets/miscellaneous.css diff --git a/src/innov8/assets/miscellaneous.css b/src/innov8/assets/miscellaneous.css new file mode 100644 index 0000000..9f7eaa1 --- /dev/null +++ b/src/innov8/assets/miscellaneous.css @@ -0,0 +1,7 @@ +.invisible { + visibility: hidden; +} + +.visible { + visibility: visible; +} \ No newline at end of file diff --git a/src/innov8/components/charts_52w.py b/src/innov8/components/charts_52w.py index fda9d58..d5b54d9 100644 --- a/src/innov8/components/charts_52w.py +++ b/src/innov8/components/charts_52w.py @@ -28,6 +28,7 @@ def carousel_52_week(): @callback( Output("52-week-price-chart", "figure"), Output("52-week-high-low-indicator", "figure"), + Output("52-week-chart-container", "hidden"), Input("symbol-dropdown", "value"), Input(ThemeChangerAIO.ids.radio("theme"), "value"), Input("update-state", "data"), @@ -145,4 +146,4 @@ def update_52_week_charts(data, symbol, theme, update): showlegend=False, ) - return fig, fig2 + return (fig, fig2, False) diff --git a/src/innov8/components/price_chart.py b/src/innov8/components/price_chart.py index dd28692..4f062af 100644 --- a/src/innov8/components/price_chart.py +++ b/src/innov8/components/price_chart.py @@ -109,6 +109,7 @@ def sma_switch() -> dbc.InputGroup: Output("tv-price-chart", "seriesData"), Output("tv-price-chart", "seriesOptions"), Output("tv-price-chart", "chartOptions"), + Output("price-chart-container", "className"), Input("symbol-dropdown", "value"), Input("ema", "value"), Input("sma", "value"), @@ -222,6 +223,7 @@ def plot_line(indicator) -> None: }, "timeScale": {"borderColor": grid_color}, }, + "visible", ) diff --git a/src/innov8/layout.py b/src/innov8/layout.py index d7ce15b..9486a62 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -59,6 +59,7 @@ def layout() -> dbc.Container: html.Div( price_chart(), id="price-chart-container", + className="invisible", # hidden on initial load ), # This row stores the theme changer component and indicators dbc.Row( @@ -99,6 +100,7 @@ def layout() -> dbc.Container: html.Div( carousel_52_week(), id="52-week-chart-container", + hidden=True, # hidden on initial load ), ], width=3, # -''- @@ -111,7 +113,12 @@ def layout() -> dbc.Container: ], fluid=True, class_name="dbc", - style={"height": "100%", "width": "100%", "margin": 0, "overflow": "hidden"}, + style={ + "height": "100%", + "width": "100%", + "margin": 0, + "overflow": "hidden", + }, ) From 1fe393acf73688c9a082cf16da3ba1bfa103d77e Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:47:30 -0500 Subject: [PATCH 11/44] styles to classes, interface improvements --- src/innov8/assets/main.css | 72 ++++++++++++++++++++++++++ src/innov8/assets/miscellaneous.css | 7 --- src/innov8/components/dropdowns.py | 4 +- src/innov8/components/forcast.py | 19 ++----- src/innov8/components/main_carousel.py | 11 +--- src/innov8/components/price_card.py | 59 ++++----------------- src/innov8/components/price_chart.py | 46 +++++++--------- src/innov8/components/themes.py | 2 +- src/innov8/components/update.py | 16 ++---- src/innov8/layout.py | 21 -------- 10 files changed, 112 insertions(+), 145 deletions(-) delete mode 100644 src/innov8/assets/miscellaneous.css diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index 06489a0..57f1862 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -37,3 +37,75 @@ } } +.container-fluid { + height: 100%; + width: 100%; + margin: 0; + overflow: hidden; +} + +.flex-center { + display: flex !important; + justify-content: center !important; + align-items: center !important; +} + +.row-option { + height: 37px !important; + width: 100%; + min-width: fit-content; +} + +.width-reset { + width: initial !important; + min-width: initial !important; +} + +#update-dropdown { + min-width: 8em; +} + +#ticker-symbol { + text-align: left; + margin-top: 0; + margin-bottom: -7px; + font-size: 3em; +} + +#ticker-name { + font-size: 1em; + text-align: left; + margin-bottom: -7px; +} + +#ticker-price { + font-size: 2.3em; + text-align: right; + margin-bottom: -7px; +} + +#ticker-change { + font-size: 1.2em; + text-align: right; + margin-bottom: -3px; +} + +#exchange-name { + text-align: left; + font-size: 1.3em; + margin-bottom: -3px; +} + +#economic-sector { + text-align: left; + font-size: 1.3em; + margin-bottom: 0.5em; +} + +.invisible { + visibility: hidden; +} + +.visible { + visibility: visible; +} \ No newline at end of file diff --git a/src/innov8/assets/miscellaneous.css b/src/innov8/assets/miscellaneous.css deleted file mode 100644 index 9f7eaa1..0000000 --- a/src/innov8/assets/miscellaneous.css +++ /dev/null @@ -1,7 +0,0 @@ -.invisible { - visibility: hidden; -} - -.visible { - visibility: visible; -} \ No newline at end of file diff --git a/src/innov8/components/dropdowns.py b/src/innov8/components/dropdowns.py index 3b10b1d..90ff69a 100644 --- a/src/innov8/components/dropdowns.py +++ b/src/innov8/components/dropdowns.py @@ -26,10 +26,10 @@ def dropdown_1(data): options=[sector[0] for sector in data.cur.execute(sector_query)], value="Technology", id="sector-dropdown", - style={"height": "37px"}, # placeholder="Select Economic Sector", clearable=False, persistence=True, # user interaction local persistence + className="row-option", ) @@ -39,10 +39,10 @@ def dropdown_2(): # options will be filled by callback # Default ticker - first ticker of sector, chosen by callback id="symbol-dropdown", - style={"height": "37px"}, # placeholder="Select Ticker Symbol", clearable=False, persistence=True, # user interaction local persistence + className="row-option", ) diff --git a/src/innov8/components/forcast.py b/src/innov8/components/forcast.py index 4f6551f..4fbc52e 100644 --- a/src/innov8/components/forcast.py +++ b/src/innov8/components/forcast.py @@ -6,15 +6,6 @@ from innov8.decorators.data_access import callback, data_access -default_style = { - "height": "37px", - "width": "100%", - "min-width": "fit-content", - "display": "flex", - "justifyContent": "center", - "alignItems": "center", -} - def forecast_button() -> dbc.Button: return dbc.Button( @@ -22,7 +13,7 @@ def forecast_button() -> dbc.Button: children="Forecast", outline=True, color="success", - style=default_style, + className="row-option flex-center", ) @@ -42,14 +33,12 @@ def forecast_button() -> dbc.Button: ) @data_access def update_price_chart_w_forecast(data, button, series_data, symbol) -> Any: - style = default_style.copy() - output = { "series_data": no_update, "forecast_button_disabled": False, "forecast_button_clicks": no_update, "forecast_button_color": "success", - "forecast_button_style": style, + "forecast_button_style": {}, } # Reset the button @@ -67,7 +56,9 @@ def update_price_chart_w_forecast(data, button, series_data, symbol) -> Any: output |= {"forecast_button_color": color} match button: case 2 | 4: - style |= {"filter": "hue-rotate(-7deg) contrast(1.05) brightness(0.75)"} + output["forecast_button_style"] |= { + "filter": "hue-rotate(-7deg) contrast(1.05) brightness(0.75)" + } date = series_data[0][-1]["time"] diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index 6f586b0..3cd2f88 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -82,23 +82,16 @@ def update_main_carousel(data, update): html.Div( [ # Ticker symbol - html.Span( - symbol, - style={ - "marginRight": "1em", - "fontSize": "1.1em", - }, - ), + html.Span(symbol), # Change (colored) html.Span( f"{'+' if change > 0 else ''}{change:.2f}%", style={ + "margin-left": "10px", "color": "green" if change > 0 else "red", - "fontSize": "1.1em", }, ), ], - style={"height": "2em", "display": "flex", "alignItems": "center"}, ) for symbol, change in data.cur.execute(change_query) ] diff --git a/src/innov8/components/price_card.py b/src/innov8/components/price_card.py index d1590e3..fd60033 100644 --- a/src/innov8/components/price_card.py +++ b/src/innov8/components/price_card.py @@ -10,55 +10,17 @@ def price_card(): return html.Div( [ # Symbol - html.P( - id="ticker-symbol", - style={ - "textAlign": "left", - "marginTop": 0, - "marginBottom": -7, - "fontSize": "3em", - }, - ), + html.P(id="ticker-symbol"), # Name - html.P( - id="ticker-name", - style={ - "fontSize": "1em", - "textAlign": "left", - "marginBottom": -7, - }, - ), + html.P(id="ticker-name"), # Price and currency - html.P( - id="ticker-price", - style={ - "fontSize": "2.3em", - "textAlign": "right", - "marginBottom": -7, - }, - ), - # Price change (style is specified in callback) - html.P( - id="ticker-change", - ), + html.P(id="ticker-price"), + # Price change + html.P(id="ticker-change"), # Exchange - html.P( - id="exchange-name", - style={ - "textAlign": "left", - "fontSize": "1.3em", - "marginBottom": -3, - }, - ), + html.P(id="exchange-name"), # Economic sector - html.P( - id="economic-sector", - style={ - "textAlign": "left", - "fontSize": "1.3em", - "marginBottom": "0.5em", - }, - ), + html.P(id="economic-sector"), ], id="ticker-data", ) @@ -77,12 +39,12 @@ def price_card(): Input("update-state", "data"), ) @data_access -def update_symbol_data(data, symbol, update): +def update_symbol_data(data, symbol, _): ticker = data.main_table.loc[ data.main_table.symbol == symbol, ["name", "close", "exchange", "sector", "currency"], ].tail(2) - # Getting the chosen symbol's current price and its change in comparison to its previous value + # Getting the chosen symbols current price and its change in comparison to its previous value current_price = ticker.iat[-1, 1] change = (current_price / ticker.iat[-2, 1]) - 1 return ( @@ -91,9 +53,6 @@ def update_symbol_data(data, symbol, update): f"{current_price:.2f} ({ticker.iat[0, 4]})", # (currency) f"{'+' if change > 0 else ''}{change:.2%}", { - "fontSize": "1.2em", - "textAlign": "right", - "marginBottom": -3, "color": "green" if change > 0 else "red", }, # set style color depending on price change f"Exchange: {ticker.iat[0, 2]}", diff --git a/src/innov8/components/price_chart.py b/src/innov8/components/price_chart.py index 4f062af..76e6cd5 100644 --- a/src/innov8/components/price_chart.py +++ b/src/innov8/components/price_chart.py @@ -39,16 +39,14 @@ def ema_switch() -> dbc.InputGroup: [ dbc.InputGroupText( dbc.Checklist( - ["EMA"], id="ema", value=["EMA"], switch=True, persistence=True + ["EMA"], + id="ema", + value=["EMA"], + switch=True, + persistence=True, + input_class_name="position-absolute", ), - style={ - "height": "37px", - "display": "flex", - "justifyContent": "center", - "alignItems": "center", - # "borderRadius": "0", - }, - class_name="btn btn-outline-secondary", + class_name="row-option width-reset flex-center btn btn-outline-secondary", ), dbc.Input( id="ema-period", @@ -59,10 +57,7 @@ def ema_switch() -> dbc.InputGroup: step=1, value=9, persistence=True, - style={ - "paddingLeft": 10, - # "borderRadius": "0" - }, + class_name="row-option", ), ], ) @@ -74,16 +69,14 @@ def sma_switch() -> dbc.InputGroup: [ dbc.InputGroupText( dbc.Checklist( - ["SMA"], id="sma", value=["SMA"], switch=True, persistence=True + ["SMA"], + id="sma", + value=["SMA"], + switch=True, + persistence=True, + input_class_name="position-absolute", ), - style={ - "height": "37px", - "display": "flex", - "justifyContent": "center", - "alignItems": "center", - # "borderRadius": "0", - }, - class_name="btn btn-outline-secondary", + class_name="row-option width-reset flex-center btn btn-outline-secondary", ), dbc.Input( id="sma-period", @@ -94,10 +87,7 @@ def sma_switch() -> dbc.InputGroup: step=1, value=50, persistence=True, - style={ - "paddingLeft": 10, - # "borderRadius": "0" - }, + class_name="row-option", ), ], ) @@ -163,9 +153,9 @@ def update_price_chart(data, symbol, ema, sma, ema_period, sma_period, theme, up theme_name = template_from_url(theme) template = plotly.io.templates[theme_name] - text_color = template["layout"]["font"]["color"] + text_color = template["layout"]["font"]["color"] # type: ignore # bg_color = template["layout"]["plot_bgcolor"] - grid_color = template["layout"]["scene"]["xaxis"]["gridcolor"] + grid_color = template["layout"]["scene"]["xaxis"]["gridcolor"] # type: ignore def plot_line(indicator) -> None: # Plot indicator line diff --git a/src/innov8/components/themes.py b/src/innov8/components/themes.py index bd666a9..0d73c33 100644 --- a/src/innov8/components/themes.py +++ b/src/innov8/components/themes.py @@ -79,5 +79,5 @@ "value": dbc.themes.CYBORG, "persistence": True, }, - button_props={"style": {"height": "37px"}}, + button_props={"class_name": "row-option flex-center"}, ) diff --git a/src/innov8/components/update.py b/src/innov8/components/update.py index 67464ef..268d955 100644 --- a/src/innov8/components/update.py +++ b/src/innov8/components/update.py @@ -17,14 +17,7 @@ def update_button() -> dbc.ButtonGroup: dbc.Button( id="update-button", outline=True, - style={ - "height": "37px", - "width": "fit-content", - "minWidth": "fit-content", - "display": "flex", - "justifyContent": "center", - "alignItems": "center", - }, + className="row-option flex-center", ), dcc.Dropdown( options=[ @@ -35,17 +28,14 @@ def update_button() -> dbc.ButtonGroup: value="Ticker", id="update-dropdown", style={ - "width": "auto", - "minWidth": "8em", - "height": "37px", "borderTopLeftRadius": 0, # squarify :] "borderBottomLeftRadius": 0, }, clearable=False, + className="row-option", ), ], - # style={"padding-left": "4em", "margin-left": "auto", "margin-right": 0}, - style={"width": "100%"}, + className="w-100", ) diff --git a/src/innov8/layout.py b/src/innov8/layout.py index 9486a62..d23740e 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -65,21 +65,6 @@ def layout() -> dbc.Container: dbc.Row( [ dbc.Col(theme_changer, width=2), - # dbc.Col( - # dbc.DropdownMenu( - # children=[ - # ema_switch(), - # sma_switch(), - # ], - # label="Technical Indicators", - # id="indicators", - # direction="up", - # align_end=True, - # color="transparent", - # style={"height": "37px", "all": "unset"}, - # ), - # width="auto", - # ), dbc.Col( ema_switch(), width={"size": 3, "offset": 3} ), @@ -113,12 +98,6 @@ def layout() -> dbc.Container: ], fluid=True, class_name="dbc", - style={ - "height": "100%", - "width": "100%", - "margin": 0, - "overflow": "hidden", - }, ) From d9c2ca7e9a9d7d104d9c9dd9308751f1dc267a27 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:18:18 -0500 Subject: [PATCH 12/44] replace dash-trich-components carousel with swiper --- pdm.lock | 11 +--- pyproject.toml | 1 - src/innov8/app.py | 12 +++- src/innov8/components/charts_52w.py | 64 +++++++++++++----- src/innov8/components/main_carousel.py | 91 +++++++++++++++----------- src/innov8/decorators/data_access.py | 1 + 6 files changed, 113 insertions(+), 67 deletions(-) diff --git a/pdm.lock b/pdm.lock index e68a625..0e5e641 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" -content_hash = "sha256:1ca99bab482fe38b3db562e7dc1335fff1897af0fe3fafa7ba0def98e35abe4c" +content_hash = "sha256:f9f4df568f0713f0a410c9cb1dc8cb4b42081cf56e91056fdbc2ea37c5ed5f4b" [[package]] name = "beautifulsoup4" @@ -298,15 +298,6 @@ files = [ {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, ] -[[package]] -name = "dash-trich-components" -version = "1.0.0" -summary = "Trich.ai components library for Dash Plotly" -groups = ["default"] -files = [ - {file = "dash_trich_components-1.0.0.tar.gz", hash = "sha256:453b2b7d201190fa250925a05978e2eeec8a9d03b8c12b95867e6e6a4ea11b24"}, -] - [[package]] name = "dash-tvlwc" version = "0.1.1" diff --git a/pyproject.toml b/pyproject.toml index 0091152..87e666b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ dependencies = [ "dash-bootstrap-components", "dash-bootstrap-templates==1.1.2", "dash-tvlwc>=0.1.1", - "dash-trich-components", "requests", "beautifulsoup4", "lxml", diff --git a/src/innov8/app.py b/src/innov8/app.py index 4dd9eed..b92e8ae 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -14,7 +14,17 @@ app = Dash( __name__, - external_stylesheets=[default_theme, dbc_css], + external_scripts=[ + {"src": "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"}, + ], + external_stylesheets=[ + default_theme, + dbc_css, + { + "rel": "stylesheet", + "href": "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css", + }, + ], title="innov8finance", background_callback_manager=background_callback_manager, suppress_callback_exceptions=True, diff --git a/src/innov8/components/charts_52w.py b/src/innov8/components/charts_52w.py index d5b54d9..0af959f 100644 --- a/src/innov8/components/charts_52w.py +++ b/src/innov8/components/charts_52w.py @@ -1,26 +1,31 @@ -import dash_trich_components as dtc import plotly.graph_objects as go -from dash import dcc -from dash.dependencies import Input, Output +from dash import dcc, html +from dash.dependencies import Input, Output, State from dash_bootstrap_templates import ThemeChangerAIO, template_from_url -from innov8.decorators.data_access import callback, data_access +from innov8.decorators.data_access import callback, clientside_callback, data_access # Carousel showing 52-week data -def carousel_52_week(): - return dtc.Carousel( - [ - dcc.Graph(id="52-week-price-chart"), - dcc.Graph(id="52-week-high-low-indicator"), - ], - slides_to_show=1, - autoplay=True, - speed=4000, - style={"height": 300, "width": 370, "paddingBottom": "6px"}, - responsive=[ - {"breakpoint": 9999, "settings": {"arrows": False}}, - ], +def carousel_52_week() -> html.Div: + return html.Div( + html.Div( + className="swiper-wrapper", + children=[ + dcc.Graph( + id="52-week-price-chart", + responsive=True, + className="swiper-slide", + ), + dcc.Graph( + id="52-week-high-low-indicator", + responsive=True, + className="swiper-slide", + ), + ], + ), + id="weekly-charts-carousel", + className="swiper weeklySwiper", ) @@ -34,7 +39,7 @@ def carousel_52_week(): Input("update-state", "data"), ) @data_access -def update_52_week_charts(data, symbol, theme, update): +def update_52_week_charts(data, symbol, theme, _): # Filter data by ticker symbol ticker = data.main_table[data.main_table.symbol == symbol].set_index("date") @@ -147,3 +152,26 @@ def update_52_week_charts(data, symbol, theme, update): ) return (fig, fig2, False) + + +clientside_callback( + """ + function initializeWeeklySwiper(id) { + var swiper = new Swiper(".weeklySwiper", { + slidesPerView: 1, + autoplay: { + delay: 3000, + disableOnInteraction: false, + pauseOnMouseEnter: true, + }, + observer: true, + cssMode: false, + }); + + return window.dash_clientside.no_update; + } + """, + Output("weekly-charts-carousel", "id"), + Input("initial-load", "className"), + State("52-week-chart-container", "hidden"), +) diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index 3cd2f88..95bea66 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -1,8 +1,7 @@ -import dash_trich_components as dtc from dash import html -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State -from innov8.decorators.data_access import callback, data_access +from innov8.decorators.data_access import callback, clientside_callback, data_access change_query = """ WITH growth AS ( @@ -35,39 +34,13 @@ """ -# Accepts a list of elements (list comp of html divs in this case) to "carouse" through -@data_access -def carousel(data): - return dtc.Carousel( - [ - html.Div( - [ - # Ticker symbol - html.Span( - symbol, - style={ - "marginRight": "1em", - "fontSize": "1.1em", - }, - ), - # Change (colored) - html.Span( - f"{'+' if change > 0 else ''}{change:.2f}%", - style={ - "color": "green" if change > 0 else "red", - "fontSize": "1.1em", - }, - ), - ], - style={"height": "2em", "display": "flex", "alignItems": "center"}, - ) - for symbol, change in data.cur.execute(change_query) - ], - id="main-carousel", - autoplay=True, - speed=500, - slides_to_show=5, - responsive=[{"breakpoint": 9999, "settings": {"arrows": False}}], +def carousel() -> html.Div: + return html.Div( + html.Div( + className="swiper-wrapper", + id="main-carousel", + ), + className="swiper mainSwiper", ) @@ -77,7 +50,7 @@ def carousel(data): Input("update-state", "data"), ) @data_access -def update_main_carousel(data, update): +def update_main_carousel(data, _) -> list[html.Div]: return [ html.Div( [ @@ -92,6 +65,50 @@ def update_main_carousel(data, update): }, ), ], + className="swiper-slide", ) for symbol, change in data.cur.execute(change_query) ] + + +clientside_callback( + """ + function initializeMainSwiper(id) { + function initSwiper() { + var swiper = new Swiper(".mainSwiper", { + slidesPerView: 2, + breakpoints: { + // when window width is >= 768px + 768: { + slidesPerView: 5, + spaceBetween: 40 + } + }, + loop: true, + autoplay: { + delay: 2000, + disableOnInteraction: false, + }, + observer: true, + cssMode: true, + }); + } + + // Polling mechanism to check if #main-carousel has children + const checkChildren = setInterval(() => { + const carouselElement = document.getElementById("main-carousel"); + + // Check if the carousel element exists and has children + if (carouselElement && carouselElement.children.length > 0) { + clearInterval(checkChildren); // Stop polling once children are found + initSwiper(); // Initialize Swiper + } + }, 100); // Check every 100 milliseconds + + return window.dash_clientside.no_update; + } + """, + Output("main-carousel", "id"), + Input("initial-load", "className"), + State("main-carousel", "children"), +) diff --git a/src/innov8/decorators/data_access.py b/src/innov8/decorators/data_access.py index 5251423..670eff9 100644 --- a/src/innov8/decorators/data_access.py +++ b/src/innov8/decorators/data_access.py @@ -22,3 +22,4 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> R: # For convenience callback = app.callback +clientside_callback = app.clientside_callback From e4932480b85016cd38a0474df990c323500cec18 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Thu, 17 Oct 2024 13:19:24 -0500 Subject: [PATCH 13/44] improve initial_load callback --- src/innov8/components/initial_load.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/innov8/components/initial_load.py b/src/innov8/components/initial_load.py index de0aa6e..9ddab79 100644 --- a/src/innov8/components/initial_load.py +++ b/src/innov8/components/initial_load.py @@ -1,7 +1,7 @@ from typing import Any from dash import html -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State from innov8.decorators.data_access import callback @@ -13,8 +13,10 @@ def initial_load() -> html.Div: @callback( Output("initial-load", "className"), - Input("52-week-price-chart", "figure"), - Input("52-week-high-low-indicator", "figure"), + State("52-week-price-chart", "figure"), + State("52-week-high-low-indicator", "figure"), + State("main-carousel", "children"), + Input("initial-load", "className"), ) def disable_spinner(*args: Any, **kwargs: Any) -> Any: """Disable spinner when the last loaded charts are ready and enable main chart visibility""" From 7e26d027cb7a62f69a5020c91c8c2c9784c7dfa8 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:45:04 -0500 Subject: [PATCH 14/44] add meta tags --- src/innov8/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/innov8/app.py b/src/innov8/app.py index b92e8ae..06095f4 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -25,6 +25,7 @@ "href": "https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css", }, ], + meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], title="innov8finance", background_callback_manager=background_callback_manager, suppress_callback_exceptions=True, From f1a431d88b36d12771dad97521f4739a75338cfa Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:45:36 -0500 Subject: [PATCH 15/44] restructure theme component --- src/innov8/components/themes.py | 165 +++++++++++++++++--------------- 1 file changed, 86 insertions(+), 79 deletions(-) diff --git a/src/innov8/components/themes.py b/src/innov8/components/themes.py index 0d73c33..8d90478 100644 --- a/src/innov8/components/themes.py +++ b/src/innov8/components/themes.py @@ -1,83 +1,90 @@ import dash_bootstrap_components as dbc +from dash import html from dash_bootstrap_templates import ThemeChangerAIO -theme_changer = ThemeChangerAIO( - aio_id="theme", - radio_props={ - "options": [ - { - "label": "Cyborg", - "value": dbc.themes.CYBORG, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Darkly", - "value": dbc.themes.DARKLY, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Slate", - "value": dbc.themes.SLATE, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Solar", - "value": dbc.themes.SOLAR, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Superhero", - "value": dbc.themes.SUPERHERO, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Vapor", - "value": dbc.themes.VAPOR, - "label_id": "theme-switch-label-dark", - }, - { - "label": "Bootstrap", - "value": dbc.themes.BOOTSTRAP, - "label_id": "theme-switch-label", - }, - { - "label": "Flatly", - "value": dbc.themes.FLATLY, - "label_id": "theme-switch-label", - }, - { - "label": "Journal", - "value": dbc.themes.JOURNAL, - "label_id": "theme-switch-label", - }, - { - "label": "Minty", - "value": dbc.themes.MINTY, - "label_id": "theme-switch-label", - }, - { - "label": "Simplex", - "value": dbc.themes.SIMPLEX, - "label_id": "theme-switch-label", - }, - { - "label": "Spacelab", - "value": dbc.themes.SPACELAB, - "label_id": "theme-switch-label", - }, - { - "label": "United", - "value": dbc.themes.UNITED, - "label_id": "theme-switch-label", - }, - { - "label": "Yeti", - "value": dbc.themes.YETI, - "label_id": "theme-switch-label", - }, - ], - "value": dbc.themes.CYBORG, - "persistence": True, - }, - button_props={"class_name": "row-option flex-center"}, +theme_changer = html.Div( + ThemeChangerAIO( + aio_id="theme", + radio_props={ + "options": [ + { + "label": "Cyborg", + "value": dbc.themes.CYBORG, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Darkly", + "value": dbc.themes.DARKLY, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Slate", + "value": dbc.themes.SLATE, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Solar", + "value": dbc.themes.SOLAR, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Superhero", + "value": dbc.themes.SUPERHERO, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Vapor", + "value": dbc.themes.VAPOR, + "label_id": "theme-switch-label-dark", + }, + { + "label": "Bootstrap", + "value": dbc.themes.BOOTSTRAP, + "label_id": "theme-switch-label", + }, + { + "label": "Flatly", + "value": dbc.themes.FLATLY, + "label_id": "theme-switch-label", + }, + { + "label": "Journal", + "value": dbc.themes.JOURNAL, + "label_id": "theme-switch-label", + }, + { + "label": "Minty", + "value": dbc.themes.MINTY, + "label_id": "theme-switch-label", + }, + { + "label": "Simplex", + "value": dbc.themes.SIMPLEX, + "label_id": "theme-switch-label", + }, + { + "label": "Spacelab", + "value": dbc.themes.SPACELAB, + "label_id": "theme-switch-label", + }, + { + "label": "United", + "value": dbc.themes.UNITED, + "label_id": "theme-switch-label", + }, + { + "label": "Yeti", + "value": dbc.themes.YETI, + "label_id": "theme-switch-label", + }, + ], + "value": dbc.themes.CYBORG, + "persistence": True, + }, + button_props={ + "children": "Theme", + "class_name": "row-option flex-center", + }, + ), + id="theme-container", ) From 69aa131d367a633d565e4ba687b7f4a9209e7e6b Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:45:36 -0500 Subject: [PATCH 16/44] add container id, adjust styles --- src/innov8/components/intra_sector.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/innov8/components/intra_sector.py b/src/innov8/components/intra_sector.py index e47fc20..8ce3704 100644 --- a/src/innov8/components/intra_sector.py +++ b/src/innov8/components/intra_sector.py @@ -52,12 +52,12 @@ def table_info(): return html.Div( [ html.P( - "Intra-sector Data Table", + "Intra-sector Table", style={ "textAlign": "center", "display": "block", "marginBottom": "0.5em", - "fontSize": "1.2em", + "fontSize": "1em", }, ), dash_table.DataTable( @@ -75,16 +75,11 @@ def table_info(): }, {"if": {"column_id": ["price", "90-day corr"]}, "width": "30%"}, ], - style_header={"backgroundColor": "rgba(0,0,0,0)"}, style_data={"backgroundColor": "rgba(0,0,0,0)"}, - style_table={ - # "height": "calc(100vh)", - "height": "calc(100vh - 2em - (10.6em * 1.5) - 1.7em - 300px)", - "overflowY": "auto", - }, style_as_list_view=True, ), - ] + ], + id="intra-sector-container", ) From 3cd3b3dcb93e9b3b0e4d66267e8bc8cd11ad7154 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:46:38 -0500 Subject: [PATCH 17/44] use loaded data and pandas instead of making additional sql query --- src/innov8/components/dropdowns.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/innov8/components/dropdowns.py b/src/innov8/components/dropdowns.py index 90ff69a..d2530b0 100644 --- a/src/innov8/components/dropdowns.py +++ b/src/innov8/components/dropdowns.py @@ -3,27 +3,12 @@ from innov8.decorators.data_access import callback, data_access -# list all sectors -sector_query = """ -SELECT name -FROM sector -ORDER BY name -""" -# list ticker symbols in sector -symbols_query = """ -SELECT symbol -FROM ticker t - JOIN sector s ON s.id = t.sector_id -WHERE s.name = ? -ORDER BY symbol -""" - # The economic sectors dropdown @data_access def dropdown_1(data): return dcc.Dropdown( - options=[sector[0] for sector in data.cur.execute(sector_query)], + options=sorted(data.main_table["sector"].unique().tolist()), value="Technology", id="sector-dropdown", # placeholder="Select Economic Sector", @@ -55,5 +40,9 @@ def dropdown_2(): @data_access def update_symbols_dropdown(data, sector): # Select ticker symbols from selected sector - options = [symbol[0] for symbol in data.cur.execute(symbols_query, (sector,))] + options = sorted( + data.main_table.loc[data.main_table["sector"] == sector, "symbol"] + .unique() + .tolist() + ) return options, options[0] From c3e52db784f5526ef8b22e0a37f408b1969b0829 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:47:41 -0500 Subject: [PATCH 18/44] add ids, andjust styles and classes --- src/innov8/components/charts_52w.py | 12 ++++++++++-- src/innov8/components/main_carousel.py | 2 +- src/innov8/components/price_chart.py | 4 +++- src/innov8/components/update.py | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/innov8/components/charts_52w.py b/src/innov8/components/charts_52w.py index 0af959f..893954c 100644 --- a/src/innov8/components/charts_52w.py +++ b/src/innov8/components/charts_52w.py @@ -23,9 +23,17 @@ def carousel_52_week() -> html.Div: className="swiper-slide", ), ], + style={ + "height": "100%", + "width": "200%", + }, ), id="weekly-charts-carousel", className="swiper weeklySwiper", + style={ + "height": "100%", + "width": "100%", + }, ) @@ -33,7 +41,7 @@ def carousel_52_week() -> html.Div: @callback( Output("52-week-price-chart", "figure"), Output("52-week-high-low-indicator", "figure"), - Output("52-week-chart-container", "hidden"), + Output("weekly-charts-container", "hidden"), Input("symbol-dropdown", "value"), Input(ThemeChangerAIO.ids.radio("theme"), "value"), Input("update-state", "data"), @@ -173,5 +181,5 @@ def update_52_week_charts(data, symbol, theme, _): """, Output("weekly-charts-carousel", "id"), Input("initial-load", "className"), - State("52-week-chart-container", "hidden"), + State("weekly-charts-container", "hidden"), ) diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index 95bea66..2dfc152 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -60,7 +60,7 @@ def update_main_carousel(data, _) -> list[html.Div]: html.Span( f"{'+' if change > 0 else ''}{change:.2f}%", style={ - "margin-left": "10px", + "marginLeft": "10px", "color": "green" if change > 0 else "red", }, ), diff --git a/src/innov8/components/price_chart.py b/src/innov8/components/price_chart.py index 76e6cd5..923b308 100644 --- a/src/innov8/components/price_chart.py +++ b/src/innov8/components/price_chart.py @@ -28,7 +28,7 @@ def hex_to_rgba(hex_color, alpha=1.0) -> str: def price_chart() -> dash_tvlwc.Tvlwc: return dash_tvlwc.Tvlwc( id="tv-price-chart", - height="calc(100vh - 2em - 83px)", + height="100%", width="100%", ) @@ -60,6 +60,7 @@ def ema_switch() -> dbc.InputGroup: class_name="row-option", ), ], + id="ema-input-group", ) @@ -90,6 +91,7 @@ def sma_switch() -> dbc.InputGroup: class_name="row-option", ), ], + id="sma-input-group", ) diff --git a/src/innov8/components/update.py b/src/innov8/components/update.py index 268d955..a02e860 100644 --- a/src/innov8/components/update.py +++ b/src/innov8/components/update.py @@ -17,7 +17,7 @@ def update_button() -> dbc.ButtonGroup: dbc.Button( id="update-button", outline=True, - className="row-option flex-center", + className="btn-sm row-option flex-center", ), dcc.Dropdown( options=[ From 61ca3a355cd663a7946e565bfef3f4c48c23b4a1 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:34:25 -0500 Subject: [PATCH 19/44] restructure layout with css grid instead of bootstrap, multiple improvements to responsiveness --- src/innov8/assets/main.css | 131 +++++++++++++++++++++++++++++++++---- src/innov8/layout.py | 95 +++++++++------------------ 2 files changed, 147 insertions(+), 79 deletions(-) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index 57f1862..56cf481 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -37,11 +37,17 @@ } } +:root { + --row-height: 37px; + --gap: 10px; +} + .container-fluid { height: 100%; width: 100%; margin: 0; - overflow: hidden; + /* overflow: hidden; */ + /* enabling this disables sticky */ } .flex-center { @@ -51,7 +57,7 @@ } .row-option { - height: 37px !important; + height: var(--row-height) !important; width: 100%; min-width: fit-content; } @@ -61,45 +67,42 @@ min-width: initial !important; } -#update-dropdown { - min-width: 8em; -} - #ticker-symbol { text-align: left; margin-top: 0; - margin-bottom: -7px; - font-size: 3em; + margin-bottom: 0px; + font-size: 2em; + line-height: 1em; } #ticker-name { - font-size: 1em; + font-size: 0.8em; text-align: left; margin-bottom: -7px; } #ticker-price { - font-size: 2.3em; + font-size: 1.7em; text-align: right; margin-bottom: -7px; } #ticker-change { - font-size: 1.2em; + font-size: 1em; text-align: right; margin-bottom: -3px; } #exchange-name { text-align: left; - font-size: 1.3em; + font-size: 0.8em; margin-bottom: -3px; } #economic-sector { text-align: left; - font-size: 1.3em; - margin-bottom: 0.5em; + font-size: 0.8em; + margin-bottom: 0px; } .invisible { @@ -108,4 +111,104 @@ .visible { visibility: visible; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + grid-template-rows: calc(var(--row-height) - var(--gap)) var(--row-height) auto 1fr 1fr var(--row-height); + gap: var(--gap); + height: 100vh; + padding-bottom: calc(var(--gap) / 2); + + /* Define named grid areas */ + grid-template-areas: + "swiper swiper swiper swiper swiper swiper swiper swiper swiper swiper swiper swiper" + "sector sector sector ticker ticker update update forecast forecast info info info" + "chart chart chart chart chart chart chart chart chart info info info" + "chart chart chart chart chart chart chart chart chart table table table" + "chart chart chart chart chart chart chart chart chart weekly weekly weekly" + "theme theme . . . ema ema sma sma weekly weekly weekly"; +} + +.mainSwiper { + grid-area: swiper; + position: sticky; + top: 0; + z-index: 999; + + width: 100%; + overflow: hidden; + margin-bottom: calc(var(--gap) * -1); +} + +#sector-dropdown { + grid-area: sector; +} + +#symbol-dropdown { + grid-area: ticker; +} + +#update-button-container { + grid-area: update; +} + +#update-button { + width: auto; +} + +#update-dropdown { + min-width: 13ch; +} + +#forecast-button { + grid-area: forecast; +} + +#price-chart-container { + grid-area: chart; +} + +#theme-container { + grid-area: theme; +} + +#ema-input-group { + grid-area: ema; +} + +#sma-input-group { + grid-area: sma; +} + +#ticker-data { + grid-area: info; +} + +#intra-sector-container { + grid-area: table; + height: 100%; +} + +#intra-sector-container { + grid-area: table; + display: flex; + flex-direction: column; + min-height: 0; +} + +#correlation-table { + flex: 1; + overflow-y: auto; +} + +#weekly-charts-container { + grid-area: weekly; +} + +.blur { + display: none; +} + } \ No newline at end of file diff --git a/src/innov8/layout.py b/src/innov8/layout.py index d23740e..f20db44 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -28,76 +28,41 @@ def layout() -> dbc.Container: data.load_main_table(force_update=False) return dbc.Container( [ - initial_load(), # A carousel for 10 tickers with the largest absolute change occupying the topmost row - dbc.Row([dbc.Col([carousel()], width=12)]), - dbc.Row( - [ - # This first column occupies all available width - 370px (for the second column) - dbc.Col( - [ - # This row holds the dropdowns responsible for sector and ticker selection and the update button - dbc.Row( - [ - dbc.Col([dropdown_1()], width=4), - dbc.Col([dropdown_2()], width=3), - dbc.Col( - [ - dcc.Loading( - [update_button(), update_state()], - type="dot", - ) - ], - width={"size": 3}, - ), - dbc.Col([forecast_button()], width=1), - ], - class_name="mb-1 g-0", - justify="between", - ), - # This row contains the main price (candlestick) chart - html.Div( - price_chart(), - id="price-chart-container", - className="invisible", # hidden on initial load - ), - # This row stores the theme changer component and indicators - dbc.Row( - [ - dbc.Col(theme_changer, width=2), - dbc.Col( - ema_switch(), width={"size": 3, "offset": 3} - ), - dbc.Col(sma_switch(), width=3), - ], - justify="between", - class_name="mb-1 g-0", - ), - ], - width=9, # investigate why this is needed later - style={"width": "calc(100% - 370px)"}, - ), - # This column occupies 370px of the dashboard's width - dbc.Col( - [ - dbc.Row([dbc.Col([price_card()], width=12)]), - dbc.Row([dbc.Col([table_info()], width=12)]), - html.Div( - carousel_52_week(), - id="52-week-chart-container", - hidden=True, # hidden on initial load - ), - ], - width=3, # -''- - style={"width": "370px"}, - ), - ], - # class_name="g-0", # remove gutters + carousel(), + dropdown_1(), + dropdown_2(), + html.Div( + dcc.Loading( + [update_button(), update_state()], + type="dot", + ), + id="update-button-container", + ), + forecast_button(), + html.Div( + price_chart(), + id="price-chart-container", + className="invisible", # hidden on initial load + ), + # This row stores the theme changer component and indicators + theme_changer, + ema_switch(), + sma_switch(), + price_card(), + table_info(), + html.Div( + carousel_52_week(), + id="weekly-charts-container", + hidden=True, # hidden on initial load ), intra_sector_data(), + initial_load(), + html.Div(className="blur top row-option"), + html.Div(className="blur bottom row-option"), ], fluid=True, - class_name="dbc", + className="dbc grid-container", ) From 5d6ea2bef2f24b6d6bbd8569ece6c77c8254c8c8 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:01:00 -0500 Subject: [PATCH 20/44] serve assets locally --- src/innov8/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/innov8/app.py b/src/innov8/app.py index 06095f4..917cdb2 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -29,5 +29,4 @@ title="innov8finance", background_callback_manager=background_callback_manager, suppress_callback_exceptions=True, - serve_locally=False, ) From f4d69e1239c327f527b0561b2f3f951bcfcb2c75 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:09:25 -0500 Subject: [PATCH 21/44] add mobile responsive css --- src/innov8/assets/main.css | 130 +++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index 56cf481..531ce89 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -211,4 +211,134 @@ display: none; } +.Select-value { + display: flex; + align-items: center; +} + +@media (max-width: 1024px) { + html { + font-size: 90%; + } + + body { + /* keeps the background from scrolling */ + background-attachment: fixed; + } + + .grid-container { + padding-left: var(--gap) !important; + padding-right: var(--gap) !important; + padding-bottom: 0; + height: calc(200vh - (var(--row-height) * 2)); + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100vh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); + gap: 10px; + grid-template-areas: + "swiper swiper swiper swiper" + "sector sector ticker forecast" + "info info info info" + "chart chart chart chart" + "ema ema sma sma" + "table table table table" + "weekly weekly weekly weekly" + "theme update update update"; + } + + #forecast-button { + font-size: 0.875rem; + } + + .form-check-label, + .form-control { + font-size: 0.85rem; + } + + #price-chart-container { + display: flex; + height: 100%; + max-height: 100%; + overflow: hidden; + } + + #tv-price-chart { + width: 100%; + height: 100%; + max-height: 100%; + max-width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + .tv-lightweight-charts { + height: 100%; + width: 100%; + max-height: 100%; + max-width: 100%; + position: relative; + } + + canvas { + width: 100% !important; + /* Force the canvas to respect parent size */ + height: 100% !important; + /* Force the canvas to respect parent size */ + max-height: 100%; + /* Constrain max height to 100% of the parent */ + max-width: 100%; + /* Constrain max width to 100% of the parent */ + position: absolute; + top: 0; + left: 0; + } + + .mainSwiper { + font-size: 112%; + position: sticky; + top: 0; + z-index: 10; + margin-bottom: 0; + } + + + #theme-container, + #update-button-container { + position: sticky; + bottom: 0; + z-index: 10; + } + + /* dropup */ + #update-dropdown .Select-menu-outer { + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + border-bottom: none; + -webkit-transform: translateY(calc((var(--row-height) * -1) - 100%)); + transform: translateY(calc((var(--row-height) * -1) - 100%)); + } + + .blur { + backdrop-filter: blur(21px); + left: 0; + right: 0; + position: fixed; + z-index: 7; + display: block !important; + } + + .top { + top: 0; + } + + .bottom { + bottom: 0; + } + + .Select-control, + .Select-menu-outer { + font-size: 0.7rem; + } } \ No newline at end of file From dbe501a24ee40f085e6f3a9ec1514888fd8734a3 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:10:41 -0500 Subject: [PATCH 22/44] bump version and toml formatting --- pyproject.toml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87e666b..03da4d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "innov8" -version = "0.2.0-beta" +version = "0.3.0-beta" description = "an interactive market dashboard" authors = [ - {name = "mayushii21", email = "122178787+mayushii21@users.noreply.github.com"}, + { name = "mayushii21", email = "122178787+mayushii21@users.noreply.github.com" }, ] dependencies = [ "dash[diskcache]==2.16.1", @@ -22,7 +22,7 @@ dependencies = [ ] requires-python = ">=3.10" readme = "README.md" -license = {text = "BSD-3-Clause"} +license = { text = "BSD-3-Clause" } [build-system] requires = ["pdm-backend"] @@ -37,13 +37,9 @@ Repository = "https://github.com/mayushii21/market-dashboard" "Bug Tracker" = "https://github.com/mayushii21/market-dashboard/issues" [tool.pytest.ini_options] -addopts = [ - "--import-mode=importlib", -] +addopts = ["--import-mode=importlib"] [tool.pdm] [tool.pdm.dev-dependencies] -test = [ - "pytest>=8.2.2", -] +test = ["pytest>=8.2.2"] From 0470535fd7cca405026716284768bda80101b0f4 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:54:02 -0500 Subject: [PATCH 23/44] loading span --- src/innov8/components/main_carousel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index 2dfc152..d94db5e 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -1,4 +1,5 @@ -from dash import html +from dash import html, no_update +from dash._callback import NoUpdate from dash.dependencies import Input, Output, State from innov8.decorators.data_access import callback, clientside_callback, data_access @@ -37,6 +38,7 @@ def carousel() -> html.Div: return html.Div( html.Div( + children=[html.Span("Loading...")], className="swiper-wrapper", id="main-carousel", ), From f0d8da000382abeecc9e9114dbb87681e8e7c4f5 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:12:09 -0500 Subject: [PATCH 24/44] add assertion --- src/innov8/update_all.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/innov8/update_all.py b/src/innov8/update_all.py index 9577665..c86b80c 100644 --- a/src/innov8/update_all.py +++ b/src/innov8/update_all.py @@ -13,6 +13,7 @@ def main() -> None: logger.configure(handlers=[{"sink": sys.stderr, "level": "INFO"}]) + assert data.main_table symbols = data.main_table.symbol.unique() logger.info("Updating all...") for symbol in tqdm(symbols): From f63cd66385557b37075060f38b2704a27cf14870 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:12:40 -0500 Subject: [PATCH 25/44] adjust tests to changes --- tests/test_callbacks.py | 88 +++------------------------------------- tests/test_components.py | 49 ++++++++-------------- 2 files changed, 23 insertions(+), 114 deletions(-) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 29bed48..a0f0e33 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -16,7 +16,11 @@ ) from innov8.components.main_carousel import update_main_carousel from innov8.components.price_card import update_symbol_data -from innov8.components.price_chart import update_indicator_period, update_price_chart +from innov8.components.price_chart import ( + hex_to_rgba, + update_indicator_period, + update_price_chart, +) from innov8.components.update import update_button_style, update_ticker_data @@ -83,7 +87,7 @@ def test_update_main_carousel(): assert len(carousel_data) == 10 # Verify proper color assignment for item in carousel_data: - assert isinstance(item.children[0].children, str) + assert item.children and isinstance(item.children[0].children, str) assert ( item.children[1].style["color"] == "green" if item.children[1].children[0] == "+" @@ -168,83 +172,3 @@ def price_chart(): ), ): return update_price_chart("AAPL", True, True, 9, 50, None, None) - - -def test_update_price_chart(price_chart): - - if len(price_chart) != 4: - return False - - # Check the first element - if not isinstance(price_chart[0], list) or len(price_chart[0]) != 4: - return False - - # Check the chart types - chart_types = ["candlestick", "histogram", "line", "line"] - if price_chart[0] != chart_types: - return False - - # Check the second element - if not isinstance(price_chart[1], list) or len(price_chart[1]) != 4: - return False - - # Define validators for each part of the second element - def is_valid_candlestick_element(element: List[Dict[str, Any]]) -> bool: - required_keys = {"open", "high", "low", "close", "time"} - for item in element: - if not isinstance(item, dict) or not required_keys.issubset(item.keys()): - return False - if not ( - isinstance(item["open"], (int, float)) - and isinstance(item["high"], (int, float)) - and isinstance(item["low"], (int, float)) - and isinstance(item["close"], (int, float)) - and isinstance(item["time"], datetime) - ): - return False - return True - - def is_valid_histogram_element(element: List[Dict[str, Any]]) -> bool: - required_keys = {"value", "color", "time"} - for item in element: - if not isinstance(item, dict) or not required_keys.issubset(item.keys()): - return False - if not ( - isinstance(item["value"], int) - and isinstance(item["color"], str) - and isinstance(item["time"], datetime) - ): - return False - return True - - def is_valid_line_element(element: List[Dict[str, Any]]) -> bool: - required_keys = {"value", "time"} - for item in element: - if not isinstance(item, dict) or not required_keys.issubset(item.keys()): - return False - if not ( - isinstance(item["value"], (int, float)) - and isinstance(item["time"], datetime) - ): - return False - return True - - # Verify that the figure is successfully plotted - assert is_valid_candlestick_element(price_chart[1][0]) - assert is_valid_histogram_element(price_chart[1][1]) - assert is_valid_line_element(price_chart[1][2]) - assert is_valid_line_element(price_chart[1][3]) - - -def test_update_indicator_period(price_chart): - # Simulate context - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": ""}]})) - return update_indicator_period(price_chart[2], "AAPL", True, True, 9, 50) - - # Run function in context - ctx = copy_context() - output = ctx.run(run_callback) - - # Verify output in Patch format - assert isinstance(output, Patch) diff --git a/tests/test_components.py b/tests/test_components.py index b1ec5d2..7a59157 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -8,28 +8,14 @@ def test_carousel(): - carousel_data = carousel() - # Check that the settings are correct - assert carousel_data.autoplay is True - assert carousel_data.speed == 500 - assert carousel_data.slides_to_show == 5 - # Ensure proper data types for span children, as well as color - for child in carousel_data.children: - color = ( - isinstance(child.children[0].children, str) # True - and isinstance(child.children[1].children, str) # True - and child.children[1].style["color"] - ) - assert color in {"green", "red"} + assert carousel().children def test_carousel_52_week(): - carousel_data = carousel_52_week() - # Check that the settings are correct - assert carousel_data.autoplay is True - assert carousel_data.speed == 4000 - assert carousel_data.slides_to_show == 1 + carousel_data = carousel_52_week().children + assert carousel_data # Verify that the carousel contains proper components + assert len(carousel_data.children) == 2 assert carousel_data.children[0].id == "52-week-price-chart" assert carousel_data.children[1].id == "52-week-high-low-indicator" @@ -38,16 +24,17 @@ def test_dropdowns(): dd_1 = dropdown_1() dd_2 = dropdown_2() # Check settings - assert dd_1.clearable is False and dd_2.clearable is False + assert not getattr(dd_1, "clearable") and not getattr(dd_2, "clearable") # Verify proper components - assert dd_1.id == "sector-dropdown" - assert dd_2.id == "symbol-dropdown" + assert getattr(dd_1, "id") == "sector-dropdown" + assert getattr(dd_2, "id") == "symbol-dropdown" # Verify options data type - assert {isinstance(option, str) for option in dd_1.options} == {True} + assert {isinstance(option, str) for option in getattr(dd_1, "options")} == {True} def test_update_button(): update_group_data = update_button() + assert update_group_data.children # Check settings assert update_group_data.children[0].outline is True assert {option["value"] for option in update_group_data.children[1].options} == { @@ -66,27 +53,24 @@ def test_stores(): isd = intra_sector_data() us = update_state() # Check storage type - assert isd.storage_type == us.storage_type == "session" + assert getattr(isd, "storage_type") == getattr(us, "storage_type") == "session" # Verify proper components - assert isd.id == "intra_sector_data" - assert us.id == "update-state" + assert getattr(isd, "id") == "intra_sector_data" + assert getattr(us, "id") == "update-state" def test_table(): table_data = table_info() + assert table_data.children # Check settings - assert table_data.children[1].style_table["overflowY"] == "auto" - assert ( - table_data.children[1].style_data["backgroundColor"] - == table_data.children[1].style_header["backgroundColor"] - == "rgba(0,0,0,0)" - ) + assert table_data.children[1].style_data["backgroundColor"] == "rgba(0,0,0,0)" # Verify proper component assert table_data.children[1].id == "correlation-table" def test_price_card(): price_card_data = price_card() + assert price_card_data.children # Verify proper components assert price_card_data.children[0].id == "ticker-symbol" assert price_card_data.children[1].id == "ticker-name" @@ -99,6 +83,7 @@ def test_price_card(): def test_switches(): ema_switch_data = ema_switch() sma_switch_data = sma_switch() + assert ema_switch_data.children and sma_switch_data.children # Check settings assert ema_switch_data.children[0].children.persistence is True assert sma_switch_data.children[0].children.persistence is True @@ -115,4 +100,4 @@ def test_switches(): def test_price_chart(): # Verify proper component - assert price_chart().id == "tv-price-chart" + assert getattr(price_chart(), "id") == "tv-price-chart" From 8fb3231f68c5eab458bea9275b3891a6f500e9fb Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:42:32 -0500 Subject: [PATCH 26/44] disable search (input) on dropdowns --- src/innov8/assets/main.css | 4 ++++ src/innov8/components/dropdowns.py | 4 ++-- src/innov8/components/update.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index 531ce89..59183a6 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -216,6 +216,10 @@ align-items: center; } +.Select-input { + background: transparent !important; +} + @media (max-width: 1024px) { html { font-size: 90%; diff --git a/src/innov8/components/dropdowns.py b/src/innov8/components/dropdowns.py index d2530b0..46ab96c 100644 --- a/src/innov8/components/dropdowns.py +++ b/src/innov8/components/dropdowns.py @@ -11,7 +11,7 @@ def dropdown_1(data): options=sorted(data.main_table["sector"].unique().tolist()), value="Technology", id="sector-dropdown", - # placeholder="Select Economic Sector", + searchable=False, clearable=False, persistence=True, # user interaction local persistence className="row-option", @@ -24,7 +24,7 @@ def dropdown_2(): # options will be filled by callback # Default ticker - first ticker of sector, chosen by callback id="symbol-dropdown", - # placeholder="Select Ticker Symbol", + searchable=False, clearable=False, persistence=True, # user interaction local persistence className="row-option", diff --git a/src/innov8/components/update.py b/src/innov8/components/update.py index a02e860..ee09e84 100644 --- a/src/innov8/components/update.py +++ b/src/innov8/components/update.py @@ -31,6 +31,7 @@ def update_button() -> dbc.ButtonGroup: "borderTopLeftRadius": 0, # squarify :] "borderBottomLeftRadius": 0, }, + searchable=False, clearable=False, className="row-option", ), From 25efc9da9cd872c1e40c770a9fbf79e73c6715ca Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:42:54 -0500 Subject: [PATCH 27/44] use dvh instead of vh --- src/innov8/assets/main.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index 59183a6..cb8f805 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -118,7 +118,7 @@ grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-rows: calc(var(--row-height) - var(--gap)) var(--row-height) auto 1fr 1fr var(--row-height); gap: var(--gap); - height: 100vh; + height: 100dvh; padding-bottom: calc(var(--gap) / 2); /* Define named grid areas */ @@ -234,9 +234,9 @@ padding-left: var(--gap) !important; padding-right: var(--gap) !important; padding-bottom: 0; - height: calc(200vh - (var(--row-height) * 2)); + height: calc(200dvh - (var(--row-height) * 2)); grid-template-columns: 1fr 1fr 1fr 1fr; - grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100vh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); + grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100dvh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); gap: 10px; grid-template-areas: "swiper swiper swiper swiper" From c0d4f9593ff2a7143bf4fb4684a5db4c52fd8722 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:52:31 -0500 Subject: [PATCH 28/44] set vapor as default theme --- src/innov8/app.py | 2 +- src/innov8/components/themes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/innov8/app.py b/src/innov8/app.py index 917cdb2..4741d3f 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -3,7 +3,7 @@ from dash import Dash, DiskcacheManager # Initiate dash app with default theme (visible for a split second before theme from ThemeChangerAIO takes over) -default_theme = dbc.themes.CYBORG +default_theme = dbc.themes.VAPOR # css for styling dcc and html components with dbc themes dbc_css = ( "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.0.8/dbc.min.css" diff --git a/src/innov8/components/themes.py b/src/innov8/components/themes.py index 8d90478..c244455 100644 --- a/src/innov8/components/themes.py +++ b/src/innov8/components/themes.py @@ -78,7 +78,7 @@ "label_id": "theme-switch-label", }, ], - "value": dbc.themes.CYBORG, + "value": dbc.themes.VAPOR, "persistence": True, }, button_props={ From e1b2af3f4d2dccb1c9cd6d5bd60f654d9e7be47b Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sun, 20 Oct 2024 17:32:16 -0500 Subject: [PATCH 29/44] fix spinner positioning and add fallbacks and vendor prefs --- src/innov8/assets/main.css | 46 +++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index cb8f805..a0931a9 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -1,18 +1,18 @@ /* Loading indicator */ ._dash-loading { position: fixed; - top: 50%; - left: 50%; + top: 0; + left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; + -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); z-index: 1000; visibility: hidden; background-color: rgba(0, 0, 0, 0.3); - transform: translate(-50%, -50%); } ._dash-loading::after { @@ -22,17 +22,23 @@ border: 4px solid #ccc; border-top-color: #3498db; border-radius: 50%; + -webkit-animation: spin 1s linear infinite; animation: spin 1s linear infinite; - /* z-index: 1001; */ position: absolute; - top: 50%; - left: 50%; visibility: visible; } /* Spinner animation */ +@-webkit-keyframes spin { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + @keyframes spin { to { + -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @@ -59,7 +65,13 @@ .row-option { height: var(--row-height) !important; width: 100%; - min-width: fit-content; + min-width: auto; +} + +@supports (min-width: fit-content) { + .row-option { + min-width: fit-content; + } } .width-reset { @@ -118,7 +130,7 @@ grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-rows: calc(var(--row-height) - var(--gap)) var(--row-height) auto 1fr 1fr var(--row-height); gap: var(--gap); - height: 100dvh; + height: 100vh; padding-bottom: calc(var(--gap) / 2); /* Define named grid areas */ @@ -131,6 +143,12 @@ "theme theme . . . ema ema sma sma weekly weekly weekly"; } +@supports (height: 100dvh) { + .grid-container { + height: 100dvh; + } +} + .mainSwiper { grid-area: swiper; position: sticky; @@ -234,9 +252,9 @@ padding-left: var(--gap) !important; padding-right: var(--gap) !important; padding-bottom: 0; - height: calc(200dvh - (var(--row-height) * 2)); + height: calc(200vh - (var(--row-height) * 2)); grid-template-columns: 1fr 1fr 1fr 1fr; - grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100dvh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); + grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100vh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); gap: 10px; grid-template-areas: "swiper swiper swiper swiper" @@ -249,6 +267,13 @@ "theme update update update"; } + @supports (height: 100dvh) { + .grid-container { + height: calc(200dvh - (var(--row-height) * 2)); + grid-template-rows: var(--row-height) var(--row-height) auto minmax(0, 1fr) var(--row-height) repeat(2, calc((100dvh - (var(--row-height) * 2) - (var(--gap) * 3)) / 2)) var(--row-height); + } + } + #forecast-button { font-size: 0.875rem; } @@ -325,6 +350,7 @@ } .blur { + -webkit-backdrop-filter: blur(21px); backdrop-filter: blur(21px); left: 0; right: 0; From 2bdf22833ff05ceb9abd40e314605718244d5663 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:44:00 -0500 Subject: [PATCH 30/44] inline styles to css --- src/innov8/assets/main.css | 21 +++++++++++++++++---- src/innov8/components/charts_52w.py | 8 -------- src/innov8/components/intra_sector.py | 7 +------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/innov8/assets/main.css b/src/innov8/assets/main.css index a0931a9..c674716 100644 --- a/src/innov8/assets/main.css +++ b/src/innov8/assets/main.css @@ -207,15 +207,18 @@ #intra-sector-container { grid-area: table; height: 100%; -} - -#intra-sector-container { - grid-area: table; display: flex; flex-direction: column; min-height: 0; } +#intra-sector-title { + text-align: center; + display: block; + margin-bottom: 0.5em; + font-size: 1em; +} + #correlation-table { flex: 1; overflow-y: auto; @@ -225,6 +228,16 @@ grid-area: weekly; } +#weekly-charts-carousel { + height: 100%; + width: 100%; +} + +#weekly-charts-carousel>.swiper-wrapper { + height: 100%; + width: 200%; +} + .blur { display: none; } diff --git a/src/innov8/components/charts_52w.py b/src/innov8/components/charts_52w.py index 893954c..d8b19c3 100644 --- a/src/innov8/components/charts_52w.py +++ b/src/innov8/components/charts_52w.py @@ -23,17 +23,9 @@ def carousel_52_week() -> html.Div: className="swiper-slide", ), ], - style={ - "height": "100%", - "width": "200%", - }, ), id="weekly-charts-carousel", className="swiper weeklySwiper", - style={ - "height": "100%", - "width": "100%", - }, ) diff --git a/src/innov8/components/intra_sector.py b/src/innov8/components/intra_sector.py index 8ce3704..75281c3 100644 --- a/src/innov8/components/intra_sector.py +++ b/src/innov8/components/intra_sector.py @@ -53,12 +53,7 @@ def table_info(): [ html.P( "Intra-sector Table", - style={ - "textAlign": "center", - "display": "block", - "marginBottom": "0.5em", - "fontSize": "1em", - }, + id="intra-sector-title", ), dash_table.DataTable( id="correlation-table", From 6796bc744c7cfe14a2543818ca406096d3dabcf3 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:15:42 -0500 Subject: [PATCH 31/44] set default theme to cyborg again.. (for dark screen on initial load) --- src/innov8/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/innov8/app.py b/src/innov8/app.py index 4741d3f..917cdb2 100644 --- a/src/innov8/app.py +++ b/src/innov8/app.py @@ -3,7 +3,7 @@ from dash import Dash, DiskcacheManager # Initiate dash app with default theme (visible for a split second before theme from ThemeChangerAIO takes over) -default_theme = dbc.themes.VAPOR +default_theme = dbc.themes.CYBORG # css for styling dcc and html components with dbc themes dbc_css = ( "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.0.8/dbc.min.css" From 9b6ffb0d3b824aec4fcb1a0e8a3b4ef1ffbf02da Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:21:15 -0500 Subject: [PATCH 32/44] fix tvlwc scaling by triggering resize event on initial load --- src/innov8/assets/utils.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/innov8/assets/utils.js diff --git a/src/innov8/assets/utils.js b/src/innov8/assets/utils.js new file mode 100644 index 0000000..7c44a12 --- /dev/null +++ b/src/innov8/assets/utils.js @@ -0,0 +1,24 @@ +window.addEventListener("load", function () { + function checkIfInitialLoadComplete() { + const initialLoadElement = document.getElementById("initial-load"); + + // Check if the element exists and has no classes (the loading screen has finished) + if (initialLoadElement && initialLoadElement.classList.length === 0) { + window.dispatchEvent(new Event("resize")); + return true; // Stop the loop + } + + return false; // Continue checking + } + + // Poll until #initial-load has no classes + const intervalId = setInterval(() => { + if (checkIfInitialLoadComplete()) { + clearInterval(intervalId); // Stop checking once the condition is met + } + }, 100); + + setTimeout(() => { + clearInterval(intervalId); + }, 10000); +}); From c615c4b229dff1cb0331323e22e777c4c6b52b71 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:22:16 -0500 Subject: [PATCH 33/44] check for full length before initiating swiper --- src/innov8/components/main_carousel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/innov8/components/main_carousel.py b/src/innov8/components/main_carousel.py index d94db5e..e3fa4a9 100644 --- a/src/innov8/components/main_carousel.py +++ b/src/innov8/components/main_carousel.py @@ -101,7 +101,7 @@ def update_main_carousel(data, _) -> list[html.Div]: const carouselElement = document.getElementById("main-carousel"); // Check if the carousel element exists and has children - if (carouselElement && carouselElement.children.length > 0) { + if (carouselElement && carouselElement.children.length >= 5) { clearInterval(checkChildren); // Stop polling once children are found initSwiper(); // Initialize Swiper } From bcf4db91d9e9213bdc2cac513f60f35b33d5876f Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:24:04 -0500 Subject: [PATCH 34/44] replace dcc store with thread locked module level globals fixes react recursive update error --- src/innov8/components/__init__.py | 2 +- src/innov8/components/intra_sector.py | 95 ++++++++++++++------------- src/innov8/layout.py | 2 - 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/innov8/components/__init__.py b/src/innov8/components/__init__.py index e74424e..e25cc9e 100644 --- a/src/innov8/components/__init__.py +++ b/src/innov8/components/__init__.py @@ -2,7 +2,7 @@ from innov8.components.dropdowns import dropdown_1, dropdown_2 from innov8.components.forcast import forecast_button from innov8.components.initial_load import initial_load -from innov8.components.intra_sector import intra_sector_data, table_info +from innov8.components.intra_sector import table_info from innov8.components.main_carousel import carousel from innov8.components.price_card import price_card from innov8.components.price_chart import ema_switch, price_chart, sma_switch diff --git a/src/innov8/components/intra_sector.py b/src/innov8/components/intra_sector.py index 75281c3..38a8810 100644 --- a/src/innov8/components/intra_sector.py +++ b/src/innov8/components/intra_sector.py @@ -1,50 +1,16 @@ import datetime +import threading import pandas as pd -from dash import dash_table, dcc, html +from dash import callback_context, dash_table, html from dash.dependencies import Input, Output from innov8.decorators.data_access import callback, data_access - # Store intermediate values -# Data with the session option will survive a page refresh but will be forgotten on page close -def intra_sector_data(): - return dcc.Store(id="intra_sector_data", storage_type="session") - - -# Calculate intra-sector data for "correlation-table", update when sector changes or data is updated -@callback( - Output("intra_sector_data", "data"), - Input("sector-dropdown", "value"), - Input("update-state", "data"), -) -@data_access -def calculate_table_data(data, sector, update): - # Filter by sector and select necessary columns - sector_table = data.main_table.loc[ - data.main_table.sector == sector, ["symbol", "date", "close"] - ] - # Convert to string from category - sector_table["symbol"] = sector_table.symbol.astype(str) - # Find the latest date that is shared by all symbols of the sector - end_date = sector_table.groupby("symbol").date.max().min() - # Subtract 90 days - start_date = end_date - datetime.timedelta(90) - # Filter the date - sector_table = sector_table[ - (sector_table.date >= start_date) & (sector_table.date <= end_date) - ] - # Pivot and calculate correlations - intra_sector_corr = ( - sector_table.pivot(columns="symbol", index="date", values="close") - .corr() - .round(3) - ) - # Get prices of tickers in sector - sector_prices = sector_table.drop(columns="date").groupby("symbol").last().round(2) - - return [intra_sector_corr.to_dict(), sector_prices.to_dict()] +corr_table: pd.DataFrame = pd.DataFrame() +price_table: pd.DataFrame = pd.DataFrame() +threadlock = threading.Lock() # This DataTable contains intra-sector ticker prices and 90-day correlations @@ -78,17 +44,55 @@ def table_info(): ) +@data_access +def calculate_table_data(data, sector) -> None: + """Calculate intra-sector data for `correlation-table`""" + global corr_table, price_table + # Filter by sector and select necessary columns + sector_table = data.main_table.loc[ + data.main_table.sector == sector, ["symbol", "date", "close"] + ] + # Convert to string from category + sector_table["symbol"] = sector_table.symbol.astype(str) + # Find the latest date that is shared by all symbols of the sector + end_date = sector_table.groupby("symbol").date.max().min() + # Subtract 90 days + start_date = end_date - datetime.timedelta(90) + # Filter the date + sector_table = sector_table[ + (sector_table.date >= start_date) & (sector_table.date <= end_date) + ] + with threadlock: + # Pivot and calculate correlations + corr_table = ( + sector_table.pivot(columns="symbol", index="date", values="close") + .corr() + .round(3) + ) + # Get prices of tickers in sector + price_table = ( + sector_table.drop(columns="date").groupby("symbol").last().round(2) + ) + + # Update the table @callback( Output("correlation-table", "data"), + Input("sector-dropdown", "value"), Input("symbol-dropdown", "value"), - Input("intra_sector_data", "data"), + Input("update-state", "data"), ) -def update_intra_sector_table(symbol, data): - # Filter intra-sector correlation data - filt_corr = pd.DataFrame(data[0])[symbol].drop(symbol).to_frame() - # Filter intra-sector price data - filt_prices = pd.DataFrame(data[1]).drop(symbol) +def update_intra_sector_table(sector, symbol, _): + # Only recalculate when sector changes or data is updated, not when symbol changes + if callback_context.triggered_prop_ids != { + "symbol-dropdown.value": "symbol-dropdown" + }: + calculate_table_data(sector) + with threadlock: + # Filter intra-sector correlation data + filt_corr = pd.DataFrame(corr_table)[symbol].drop(symbol).to_frame() + # Filter intra-sector price data + filt_prices = pd.DataFrame(price_table).drop(symbol) # Combine into a single table table = ( filt_prices.join(filt_corr) @@ -96,4 +100,5 @@ def update_intra_sector_table(symbol, data): .rename(columns={symbol: "90-day corr", "close": "price", "index": "symbol"}) .sort_values(by="90-day corr", key=abs, ascending=False) ) + return table.to_dict("records") diff --git a/src/innov8/layout.py b/src/innov8/layout.py index f20db44..2b786b4 100644 --- a/src/innov8/layout.py +++ b/src/innov8/layout.py @@ -10,7 +10,6 @@ ema_switch, forecast_button, initial_load, - intra_sector_data, price_card, price_chart, sma_switch, @@ -56,7 +55,6 @@ def layout() -> dbc.Container: id="weekly-charts-container", hidden=True, # hidden on initial load ), - intra_sector_data(), initial_load(), html.Div(className="blur top row-option"), html.Div(className="blur bottom row-option"), From b992599910b27e9934878e9e30280cabe65b6efe Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:28:49 -0500 Subject: [PATCH 35/44] fix ambiguous truth value of pandas df, adjust initial population of db --- src/innov8/db_ops.py | 8 +++++++- src/innov8/update_all.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/innov8/db_ops.py b/src/innov8/db_ops.py index 2cfd6cf..31e7c5e 100644 --- a/src/innov8/db_ops.py +++ b/src/innov8/db_ops.py @@ -74,6 +74,10 @@ def __init__(self, script_directory: Path): self.insert_ticker_info() # Download data and fill date and price tables self.fill_ohlc() + self.load_main_table() + assert self.main_table is not None + for symbol in tqdm(self.main_table.symbol.unique()): + self.generate_forecast(symbol) # If the database is already populated else: self.initiate_tickers_obj(scrape=False) @@ -275,8 +279,10 @@ def fill_ohlc(self): ): try: # Retrieve OHLC data for symbol + start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") + end_date = datetime.now().strftime("%Y-%m-%d") ohlc_data = self.tickers.tickers[symbol[0]].history( - start="2022-07-01", end="2023-07-01" + start=start_date, end=end_date )[["Open", "High", "Low", "Close", "Volume"]] # Convert the date to a unix timestamp (remove timezone holding local time representations) ohlc_data.index = ( diff --git a/src/innov8/update_all.py b/src/innov8/update_all.py index c86b80c..0de4f6e 100644 --- a/src/innov8/update_all.py +++ b/src/innov8/update_all.py @@ -13,7 +13,7 @@ def main() -> None: logger.configure(handlers=[{"sink": sys.stderr, "level": "INFO"}]) - assert data.main_table + assert data.main_table is not None symbols = data.main_table.symbol.unique() logger.info("Updating all...") for symbol in tqdm(symbols): From 9f5e17e25ae60456ded4c5e8c6309e1c4f014c62 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Thu, 24 Oct 2024 23:43:38 -0500 Subject: [PATCH 36/44] thread lock db ops --- src/innov8/db_ops.py | 534 ++++++++++++++++++++++--------------------- 1 file changed, 276 insertions(+), 258 deletions(-) diff --git a/src/innov8/db_ops.py b/src/innov8/db_ops.py index 31e7c5e..0f95a39 100644 --- a/src/innov8/db_ops.py +++ b/src/innov8/db_ops.py @@ -5,8 +5,9 @@ import logging import os import sqlite3 +import threading import time -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import cast @@ -52,21 +53,26 @@ def __init__(self, script_directory: Path): self.script_directory = script_directory # Construct the absolute path to the database file self.db_path = script_directory / "stonks.db" + self.lock = threading.Lock() # Connect to the database using the absolute path - self.con = sqlite3.connect(self.db_path, check_same_thread=False) - self.cur = self.con.cursor() + with self.lock: + self.con = sqlite3.connect(self.db_path, check_same_thread=False) + self.cur = self.con.cursor() + self.ticker_symbols = None self.main_table: pd.DataFrame | None = None # Check if the database is populated by checking if the price table is present - if not self.cur.execute( - """ - SELECT name - FROM sqlite_master - WHERE TYPE = 'table' - AND name = 'price' - """ - ).fetchone(): + with self.lock: + tables_exist = self.cur.execute( + """ + SELECT name + FROM sqlite_master + WHERE TYPE = 'table' + AND name = 'price' + """ + ).fetchone() + if not tables_exist: # Create necessary tables self.create_tables() self.initiate_tickers_obj(scrape=True) @@ -183,100 +189,102 @@ def create_tables(self): DROP TABLE IF EXISTS currency; DROP TABLE IF EXISTS forecast; """ - self.cur.executescript(drop_tables) - self.cur.executescript(create_tables_query) - self.con.commit() + with self.lock: + self.cur.executescript(drop_tables) + self.cur.executescript(create_tables_query) + self.con.commit() def insert_ticker_info(self): logger.info("Populating database with main ticker information...") for symbol in tqdm(self.ticker_symbols): try: - with self.con: - info = self.tickers.tickers[symbol].info - self.con.execute( - """ - INSERT - OR IGNORE INTO currency (iso_code) - VALUES (:currency) - """, - info, - ) - self.con.execute( - """ - INSERT - OR IGNORE INTO exchange (name) - VALUES (:exchange) - """, - info, - ) - self.con.execute( - """ - INSERT - OR IGNORE INTO ticker_type (name) - VALUES (:quoteType) - """, - info, - ) - self.con.execute( - """ - INSERT - OR IGNORE INTO sector (name) - VALUES (:sector) - """, - info, - ) - self.con.execute( - """ - INSERT INTO ticker ( - name, - symbol, - currency_id, - exchange_id, - ticker_type_id, - sector_id - ) - VALUES ( - :shortName, - :symbol, - ( - SELECT id - FROM currency - WHERE iso_code = :currency - ), - ( - SELECT id - FROM exchange - WHERE name = :exchange - ), - ( - SELECT id - FROM ticker_type - WHERE name = :quoteType - ), - ( - SELECT id - FROM sector - WHERE name = :sector + info = self.tickers.tickers[symbol].info + with self.lock: + with self.con: + self.con.execute( + """ + INSERT + OR IGNORE INTO currency (iso_code) + VALUES (:currency) + """, + info, + ) + self.con.execute( + """ + INSERT + OR IGNORE INTO exchange (name) + VALUES (:exchange) + """, + info, + ) + self.con.execute( + """ + INSERT + OR IGNORE INTO ticker_type (name) + VALUES (:quoteType) + """, + info, + ) + self.con.execute( + """ + INSERT + OR IGNORE INTO sector (name) + VALUES (:sector) + """, + info, + ) + self.con.execute( + """ + INSERT INTO ticker ( + name, + symbol, + currency_id, + exchange_id, + ticker_type_id, + sector_id ) - ) - """, - info, - ) - logger.debug("Successfully inserted info for {}", symbol) + VALUES ( + :shortName, + :symbol, + ( + SELECT id + FROM currency + WHERE iso_code = :currency + ), + ( + SELECT id + FROM exchange + WHERE name = :exchange + ), + ( + SELECT id + FROM ticker_type + WHERE name = :quoteType + ), + ( + SELECT id + FROM sector + WHERE name = :sector + ) + ) + """, + info, + ) + logger.debug("Successfully inserted info for {}", symbol) except Exception: logger.error("Failed to insert {}", symbol) def fill_ohlc(self): logger.info("Populating database with OHLC data...") # Per ticker OHLC data retrieval - helps avoid rate limiting - for symbol in tqdm( - self.cur.execute( + with self.lock: + symbols = self.cur.execute( """ SELECT symbol FROM ticker """ ).fetchall() - ): + for symbol in tqdm(symbols): try: # Retrieve OHLC data for symbol start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") @@ -294,56 +302,58 @@ def fill_ohlc(self): ohlc_data.reset_index(inplace=True) # Convert to a list of dictionaries (records) ohlc_data = ohlc_data.to_dict(orient="records") - with self.con: - # Inserting date could be optimized - self.con.executemany( - """ - INSERT - OR IGNORE INTO date (date) - VALUES (:Date) - """, - ohlc_data, - ) + with self.lock: + with self.con: + # Inserting date could be optimized + self.con.executemany( + """ + INSERT + OR IGNORE INTO date (date) + VALUES (:Date) + """, + ohlc_data, + ) - # Using an f-string is an SQL injection vulnerability, - # but given the context it doesn't matter, can be easily fixed if needed - self.con.executemany( - f""" - INSERT INTO price ( - ticker_id, - date_id, - OPEN, - high, - low, - close, - volume - ) - VALUES ( - ( - SELECT id - FROM ticker - WHERE symbol = '{symbol[0]}' - ), - ( - SELECT id - FROM date - WHERE date = :Date - ), - :Open, - :High, - :Low, - :Close, - :Volume - ) - """, - ohlc_data, - ) - logger.debug("Successfully inserted OHLC data for {}", symbol[0]) + # Using an f-string is an SQL injection vulnerability, + # but given the context it doesn't matter, can be easily fixed if needed + self.con.executemany( + f""" + INSERT INTO price ( + ticker_id, + date_id, + OPEN, + high, + low, + close, + volume + ) + VALUES ( + ( + SELECT id + FROM ticker + WHERE symbol = '{symbol[0]}' + ), + ( + SELECT id + FROM date + WHERE date = :Date + ), + :Open, + :High, + :Low, + :Close, + :Volume + ) + """, + ohlc_data, + ) + logger.debug("Successfully inserted OHLC data for {}", symbol[0]) except Exception as e: logger.error("[{}] Exception: {}", symbol[0], e) def generate_forecast(self, symbol: str) -> None: - df = self.main_table + with self.lock: + df = self.main_table assert df is not None predictions = {} @@ -408,57 +418,60 @@ def generate_forecast(self, symbol: str) -> None: # Insert data into the database try: - with self.con: - self.con.execute( - """ - INSERT INTO forecast (ticker_id, date, open, high, low, close) - VALUES ( + with self.lock: + with self.con: + self.con.execute( + """ + INSERT INTO forecast (ticker_id, date, open, high, low, close) + VALUES ( + ( + SELECT id + FROM ticker + WHERE symbol = ? + ), + ?, ?, ?, ?, ? + ) + """, ( - SELECT id - FROM ticker - WHERE symbol = ? + symbol, + pred_date, + o_price, + h_price, + l_price, + c_price, ), - ?, ?, ?, ?, ? ) - """, - ( - symbol, - pred_date, - o_price, - h_price, - l_price, - c_price, - ), - ) except Exception as e: logger.error("[{}] Exception: {}", symbol, e) def clear_forecasts(self): - with self.con: - self.con.execute( - """ - DELETE - FROM forecast; - """ - ) + with self.lock: + with self.con: + self.con.execute( + """ + DELETE + FROM forecast; + """ + ) def get_forecasts(self, symbol: str, date: datetime): - with self.con: - return self.con.execute( - """ - SELECT open, high, low, close, date - FROM forecast - WHERE ticker_id = ( - SELECT id - FROM ticker - WHERE symbol = ? - ) - AND date > ? - ORDER BY date ASC - LIMIT 1 - """, - (symbol, date), - ).fetchone() + with self.lock: + with self.con: + return self.con.execute( + """ + SELECT open, high, low, close, date + FROM forecast + WHERE ticker_id = ( + SELECT id + FROM ticker + WHERE symbol = ? + ) + AND date > ? + ORDER BY date ASC + LIMIT 1 + """, + (symbol, date), + ).fetchone() # Create DataFrame from SQL query def load_main_table(self, force_update=True): @@ -469,39 +482,42 @@ def load_main_table(self, force_update=True): ): logger.info("Loading main table...") if update_signal: - if self.con: - self.con.close() # Close the existing connection if it exists - - self.con = sqlite3.connect(self.db_path, check_same_thread=False) - self.cur = self.con.cursor() - os.remove(self.script_directory / "update_signal") - self.main_table = pd.read_sql_query( - self.main_query, - self.con, - parse_dates=["date"], - dtype={ - "symbol": "category", - "name": "category", - "sector": "category", - "exchange": "category", - "type": "category", - "currency": "category", - }, - ) + with self.lock: + if self.con: + self.con.close() # Close the existing connection if it exists + + self.con = sqlite3.connect(self.db_path, check_same_thread=False) + self.cur = self.con.cursor() + os.remove(self.script_directory / "update_signal") + with self.lock: + self.main_table = pd.read_sql_query( + self.main_query, + self.con, + parse_dates=["date"], + dtype={ + "symbol": "category", + "name": "category", + "sector": "category", + "exchange": "category", + "type": "category", + "currency": "category", + }, + ) def initiate_tickers_obj(self, scrape): if scrape: self.ticker_symbols = self.scrape_symbols() else: - self.ticker_symbols = [ - symbol[0] - for symbol in self.con.execute( - """ - SELECT symbol - FROM ticker - """ - ).fetchall() - ] + with self.lock: + self.ticker_symbols = [ + symbol[0] + for symbol in self.con.execute( + """ + SELECT symbol + FROM ticker + """ + ).fetchall() + ] # Initiate tickers instance self.tickers = yf.Tickers(" ".join(self.ticker_symbols)) @@ -509,17 +525,18 @@ def initiate_tickers_obj(self, scrape): def add_new_ohlc(self, symbol): logger.debug("Updating {}...", symbol) try: - # Get the date for the next entry - next_entry = self.cur.execute( - """ - SELECT DATE(max(date) + 86400, 'unixepoch') - FROM price p - JOIN ticker t ON t.id = p.ticker_id - JOIN date d ON p.date_id = d.id - WHERE t.symbol = ? - """, - (symbol,), - ).fetchone()[0] + with self.lock: + # Get the date for the next entry + next_entry = self.cur.execute( + """ + SELECT DATE(max(date) + 86400, 'unixepoch') + FROM price p + JOIN ticker t ON t.id = p.ticker_id + JOIN date d ON p.date_id = d.id + WHERE t.symbol = ? + """, + (symbol,), + ).fetchone()[0] # Skip when start date is after end date timezone = self.tickers.tickers[symbol]._get_ticker_tz( @@ -550,51 +567,52 @@ def add_new_ohlc(self, symbol): ohlc_data.reset_index(inplace=True) # Convert to a list of dictionaries (records) ohlc_data = ohlc_data.to_dict(orient="records") - with self.con: - # Inserting date could be optimized - self.con.executemany( - """ - INSERT - OR IGNORE INTO date (date) - VALUES (:Date) - """, - ohlc_data, - ) + with self.lock: + with self.con: + # Inserting date could be optimized + self.con.executemany( + """ + INSERT + OR IGNORE INTO date (date) + VALUES (:Date) + """, + ohlc_data, + ) - # Using an f-string is an SQL injection vulnerability, - # but given the context it doesn't matter - self.con.executemany( - f""" - INSERT INTO price ( - ticker_id, - date_id, - OPEN, - high, - low, - close, - volume - ) - VALUES ( - ( - SELECT id - FROM ticker - WHERE symbol = '{symbol}' - ), - ( - SELECT id - FROM date - WHERE date = :Date - ), - :Open, - :High, - :Low, - :Close, - :Volume - ) - """, - ohlc_data, - ) - logger.debug("{} updated \u2713", symbol) + # Using an f-string is an SQL injection vulnerability, + # but given the context it doesn't matter + self.con.executemany( + f""" + INSERT INTO price ( + ticker_id, + date_id, + OPEN, + high, + low, + close, + volume + ) + VALUES ( + ( + SELECT id + FROM ticker + WHERE symbol = '{symbol}' + ), + ( + SELECT id + FROM date + WHERE date = :Date + ), + :Open, + :High, + :Low, + :Close, + :Volume + ) + """, + ohlc_data, + ) + logger.debug("{} updated \u2713", symbol) except Exception as e: logger.error("[{}] Exception: {}", symbol, e) From f9f7631e92105f5661ddf52aaa2489444d34416e Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:09:14 -0500 Subject: [PATCH 37/44] rm old test --- tests/test_components.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/test_components.py b/tests/test_components.py index 7a59157..98f30d6 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -1,10 +1,10 @@ from innov8.components.charts_52w import carousel_52_week from innov8.components.dropdowns import dropdown_1, dropdown_2 -from innov8.components.intra_sector import intra_sector_data, table_info +from innov8.components.intra_sector import table_info from innov8.components.main_carousel import carousel from innov8.components.price_card import price_card from innov8.components.price_chart import ema_switch, price_chart, sma_switch -from innov8.components.update import update_button, update_state +from innov8.components.update import update_button def test_carousel(): @@ -49,16 +49,6 @@ def test_update_button(): assert update_group_data.children[1].id == "update-dropdown" -def test_stores(): - isd = intra_sector_data() - us = update_state() - # Check storage type - assert getattr(isd, "storage_type") == getattr(us, "storage_type") == "session" - # Verify proper components - assert getattr(isd, "id") == "intra_sector_data" - assert getattr(us, "id") == "update-state" - - def test_table(): table_data = table_info() assert table_data.children From 35c4faed73b44cd24dcf6b1d57257f40ad822913 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:01:48 -0500 Subject: [PATCH 38/44] use theme temp fix --- pdm.lock | 76 +++++++++++++++++++++++++++++--------------------- pyproject.toml | 2 +- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/pdm.lock b/pdm.lock index 0e5e641..0bc895a 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,8 +4,11 @@ [metadata] groups = ["default", "test"] strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.2" -content_hash = "sha256:f9f4df568f0713f0a410c9cb1dc8cb4b42081cf56e91056fdbc2ea37c5ed5f4b" +lock_version = "4.5.0" +content_hash = "sha256:3cffddece53ee55e8725f543b512793be580bc0a38b2a9b38d01a9a306b06a8a" + +[[metadata.targets]] +requires_python = ">=3.10" [[package]] name = "beautifulsoup4" @@ -122,6 +125,7 @@ summary = "Composable command line interface toolkit" groups = ["default"] dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, @@ -255,18 +259,19 @@ files = [ [[package]] name = "dash-bootstrap-templates" -version = "1.1.2" +version = "0.1.dev109" +git = "https://github.com/mayushii21/dash-bootstrap-templates.git" +ref = "cdbb4b28c8aa97f44fcc76085639d1a26d3cc247" +revision = "cdbb4b28c8aa97f44fcc76085639d1a26d3cc247" summary = "A collection of Plotly figure templates with a Bootstrap theme" groups = ["default"] dependencies = [ "dash", "dash-bootstrap-components>=1.0.0", + "importlib-metadata>=3.4.0; python_version == \"3.7\"", + "importlib-resources>=5.1.0; python_version < \"3.9\"", "numpy", ] -files = [ - {file = "dash-bootstrap-templates-1.1.2.tar.gz", hash = "sha256:ad09b6d22500b7de3fd6cfe4886389de653ef16320969915f728b4bac5a7ba30"}, - {file = "dash_bootstrap_templates-1.1.2-py3-none-any.whl", hash = "sha256:87bb1e9dd7ac475f07d1237159091b9d154aa80fdae5fe579fb868e87343dfcd"}, -] [[package]] name = "dash-core-components" @@ -328,13 +333,13 @@ files = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" requires_python = ">=3.8" summary = "serialize all of Python" groups = ["default"] files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [[package]] @@ -371,6 +376,7 @@ dependencies = [ "Werkzeug>=3.0.0", "blinker>=1.6.2", "click>=8.1.3", + "importlib-metadata>=3.6.0; python_version < \"3.10\"", "itsdangerous>=2.1.2", ] files = [ @@ -486,6 +492,7 @@ requires_python = ">=3.8" summary = "Read metadata from Python packages" groups = ["default"] dependencies = [ + "typing-extensions>=3.6.4; python_version < \"3.8\"", "zipp>=0.5", ] files = [ @@ -499,6 +506,9 @@ version = "6.4.0" requires_python = ">=3.8" summary = "Read resources from Python packages" groups = ["default"] +dependencies = [ + "zipp>=3.1.0; python_version < \"3.10\"", +] files = [ {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, @@ -546,6 +556,9 @@ version = "1.4.5" requires_python = ">=3.7" summary = "A fast implementation of the Cassowary constraint solver" groups = ["default"] +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, @@ -617,6 +630,7 @@ requires_python = ">=3.5" summary = "Python logging made (stupidly) simple" groups = ["default"] dependencies = [ + "aiocontextvars>=0.2.0; python_version < \"3.7\"", "colorama>=0.3.4; sys_platform == \"win32\"", "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] @@ -803,6 +817,7 @@ dependencies = [ "contourpy>=1.0.1", "cycler>=0.10", "fonttools>=4.22.0", + "importlib-resources>=3.2.0; python_version < \"3.10\"", "kiwisolver>=1.3.1", "numpy>=1.23", "packaging>=20.0", @@ -838,25 +853,22 @@ files = [ [[package]] name = "multiprocess" -version = "0.70.16" +version = "0.70.17" requires_python = ">=3.8" summary = "better multiprocessing and multithreading in Python" groups = ["default"] dependencies = [ - "dill>=0.3.8", + "dill>=0.3.9", ] files = [ - {file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}, - {file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}, - {file = "multiprocess-0.70.16-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a"}, - {file = "multiprocess-0.70.16-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054"}, - {file = "multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41"}, - {file = "multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a"}, - {file = "multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}, - {file = "multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a"}, - {file = "multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e"}, - {file = "multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3"}, - {file = "multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2c82d0375baed8d8dd0d8c38eb87c5ae9c471f8e384ad203a36f095ee860f67"}, + {file = "multiprocess-0.70.17-py310-none-any.whl", hash = "sha256:38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744"}, + {file = "multiprocess-0.70.17-py311-none-any.whl", hash = "sha256:2884701445d0177aec5bd5f6ee0df296773e4fb65b11903b94c613fb46cfb7d1"}, + {file = "multiprocess-0.70.17-py312-none-any.whl", hash = "sha256:2818af14c52446b9617d1b0755fa70ca2f77c28b25ed97bdaa2c69a22c47b46c"}, + {file = "multiprocess-0.70.17-py313-none-any.whl", hash = "sha256:20c28ca19079a6c879258103a6d60b94d4ffe2d9da07dda93fb1c8bc6243f522"}, + {file = "multiprocess-0.70.17.tar.gz", hash = "sha256:4ae2f11a3416809ebc9a48abfc8b14ecce0652a0944731a1493a3c1ba44ff57a"}, ] [[package]] @@ -1131,19 +1143,19 @@ files = [ [[package]] name = "psutil" -version = "6.0.0" +version = "6.1.0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" summary = "Cross-platform lib for process and system monitoring in Python." groups = ["default"] files = [ - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "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"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 03da4d1..4348f64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [ dependencies = [ "dash[diskcache]==2.16.1", "dash-bootstrap-components", - "dash-bootstrap-templates==1.1.2", + "dash-bootstrap-templates @ git+https://github.com/mayushii21/dash-bootstrap-templates.git@cdbb4b28c8aa97f44fcc76085639d1a26d3cc247", "dash-tvlwc>=0.1.1", "requests", "beautifulsoup4", From 9d22d8c0dba7b032e93dce8759cefe81479d2209 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:33:05 -0600 Subject: [PATCH 39/44] add cov dep --- pdm.lock | 144 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 5 +- 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/pdm.lock b/pdm.lock index 0bc895a..863ec7e 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:3cffddece53ee55e8725f543b512793be580bc0a38b2a9b38d01a9a306b06a8a" +content_hash = "sha256:ed52893c498030e96726c49d145e593750e21c987eec946e351c499109f66903" [[metadata.targets]] requires_python = ">=3.10" @@ -207,6 +207,133 @@ files = [ {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, ] +[[package]] +name = "coverage" +version = "7.6.4" +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["test"] +files = [ + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, +] + +[[package]] +name = "coverage" +version = "7.6.4" +extras = ["toml"] +requires_python = ">=3.9" +summary = "Code coverage measurement for Python" +groups = ["test"] +dependencies = [ + "coverage==7.6.4", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1188,6 +1315,21 @@ files = [ {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +requires_python = ">=3.9" +summary = "Pytest plugin for measuring coverage." +groups = ["test"] +dependencies = [ + "coverage[toml]>=7.5", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" diff --git a/pyproject.toml b/pyproject.toml index 4348f64..5ae81ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,4 +42,7 @@ addopts = ["--import-mode=importlib"] [tool.pdm] [tool.pdm.dev-dependencies] -test = ["pytest>=8.2.2"] +test = [ + "pytest>=8.2.2", + "pytest-cov>=6.0.0", +] From 1a81ce32017ad095866364daa3cf1b5796e2dc3d Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:50:10 -0600 Subject: [PATCH 40/44] use dicts for better caching --- src/innov8/components/intra_sector.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/innov8/components/intra_sector.py b/src/innov8/components/intra_sector.py index 38a8810..7102b30 100644 --- a/src/innov8/components/intra_sector.py +++ b/src/innov8/components/intra_sector.py @@ -8,8 +8,8 @@ from innov8.decorators.data_access import callback, data_access # Store intermediate values -corr_table: pd.DataFrame = pd.DataFrame() -price_table: pd.DataFrame = pd.DataFrame() +corrs: dict[str, pd.DataFrame] = {} +prices: dict[str, pd.DataFrame] = {} threadlock = threading.Lock() @@ -47,7 +47,7 @@ def table_info(): @data_access def calculate_table_data(data, sector) -> None: """Calculate intra-sector data for `correlation-table`""" - global corr_table, price_table + global corrs, prices # Filter by sector and select necessary columns sector_table = data.main_table.loc[ data.main_table.sector == sector, ["symbol", "date", "close"] @@ -64,13 +64,13 @@ def calculate_table_data(data, sector) -> None: ] with threadlock: # Pivot and calculate correlations - corr_table = ( + corrs[sector] = ( sector_table.pivot(columns="symbol", index="date", values="close") .corr() .round(3) ) # Get prices of tickers in sector - price_table = ( + prices[sector] = ( sector_table.drop(columns="date").groupby("symbol").last().round(2) ) @@ -90,9 +90,9 @@ def update_intra_sector_table(sector, symbol, _): calculate_table_data(sector) with threadlock: # Filter intra-sector correlation data - filt_corr = pd.DataFrame(corr_table)[symbol].drop(symbol).to_frame() + filt_corr = pd.DataFrame(corrs[sector])[symbol].drop(symbol).to_frame() # Filter intra-sector price data - filt_prices = pd.DataFrame(price_table).drop(symbol) + filt_prices = pd.DataFrame(prices[sector]).drop(symbol) # Combine into a single table table = ( filt_prices.join(filt_corr) From da63b47c78366723d5060e90adcf4ce0c85972d0 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:53:42 -0600 Subject: [PATCH 41/44] update with fixed dependency --- pdm.lock | 11 ++++++----- pyproject.toml | 7 ++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pdm.lock b/pdm.lock index 863ec7e..86c4616 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:ed52893c498030e96726c49d145e593750e21c987eec946e351c499109f66903" +content_hash = "sha256:7d0fb56d3b1ab9890d191400cd0292ca78dbdc71c7a88b86b9534247df9afd03" [[metadata.targets]] requires_python = ">=3.10" @@ -386,10 +386,7 @@ files = [ [[package]] name = "dash-bootstrap-templates" -version = "0.1.dev109" -git = "https://github.com/mayushii21/dash-bootstrap-templates.git" -ref = "cdbb4b28c8aa97f44fcc76085639d1a26d3cc247" -revision = "cdbb4b28c8aa97f44fcc76085639d1a26d3cc247" +version = "1.3.0" summary = "A collection of Plotly figure templates with a Bootstrap theme" groups = ["default"] dependencies = [ @@ -399,6 +396,10 @@ dependencies = [ "importlib-resources>=5.1.0; python_version < \"3.9\"", "numpy", ] +files = [ + {file = "dash_bootstrap_templates-1.3.0-py3-none-any.whl", hash = "sha256:bb49a57d4a4331d0e610c87fcfb3fcbab4d015b5361f2fa58dce0bf1213ffc7c"}, + {file = "dash_bootstrap_templates-1.3.0.tar.gz", hash = "sha256:a704e71b45bb9443e6ba7851ceef33d220b6decac5c344fff41b52d928470877"}, +] [[package]] name = "dash-core-components" diff --git a/pyproject.toml b/pyproject.toml index 5ae81ce..b749e16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [ dependencies = [ "dash[diskcache]==2.16.1", "dash-bootstrap-components", - "dash-bootstrap-templates @ git+https://github.com/mayushii21/dash-bootstrap-templates.git@cdbb4b28c8aa97f44fcc76085639d1a26d3cc247", + "dash-bootstrap-templates>=1.3.0", "dash-tvlwc>=0.1.1", "requests", "beautifulsoup4", @@ -42,7 +42,4 @@ addopts = ["--import-mode=importlib"] [tool.pdm] [tool.pdm.dev-dependencies] -test = [ - "pytest>=8.2.2", - "pytest-cov>=6.0.0", -] +test = ["pytest>=8.2.2", "pytest-cov>=6.0.0"] From e80a3aaa71efd5bad44ccaeb1e7e7d4a5a961b39 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Sat, 21 Dec 2024 15:16:29 -0600 Subject: [PATCH 42/44] update update tests --- tests/test_callbacks.py | 65 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index a0f0e33..0ce90da 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -1,24 +1,17 @@ -import unittest.mock -from contextvars import copy_context -from datetime import datetime -from typing import Any, Dict, List +from unittest.mock import patch import pytest -from dash import Patch -from dash._callback_context import context_value -from dash._utils import AttributeDict from innov8.components.charts_52w import update_52_week_charts from innov8.components.dropdowns import update_symbols_dropdown from innov8.components.intra_sector import ( calculate_table_data, + corrs, update_intra_sector_table, ) from innov8.components.main_carousel import update_main_carousel from innov8.components.price_card import update_symbol_data from innov8.components.price_chart import ( - hex_to_rgba, - update_indicator_period, update_price_chart, ) from innov8.components.update import update_button_style, update_ticker_data @@ -106,26 +99,38 @@ def test_update_symbol_data(): # Fixture with calculated correlation table data @pytest.fixture(scope="module") -def calculated_table_data(): - return calculate_table_data("Technology", None) +def calculated_table_data() -> None: + return calculate_table_data("Technology") -def test_calculate_table_data(calculated_table_data): - # Check that proper sector selected with expected symbols - assert {"AAPL", "ACN", "ADBE", "ADI", "ADSK"}.issubset( - calculated_table_data[1]["close"] - ) - # Verify that correlation is calculated between all symbols of the sector - symbols = calculate_table_data("Technology", None)[0].keys() - for symbol in symbols: - assert symbols == calculated_table_data[0][symbol].keys() - assert calculated_table_data[0][symbol][symbol] == 1 +def test_calculate_table_data(calculated_table_data: None): + df = corrs["Technology"] + # Check that the correlation matrix is symmetric: + # correlation(sym1, sym2) should equal correlation(sym2, sym1) + for sym1 in df.columns: + for sym2 in df.index: + assert df.loc[sym1, sym2] == pytest.approx( + df.loc[sym2, sym1] + ), f"Matrix is not symmetric: {sym1}/{sym2} != {sym2}/{sym1}" -def test_update_intra_sector_table(calculated_table_data): - output = update_intra_sector_table("AAPL", calculated_table_data) - # Proper table columns - assert list(output[0].keys()) == ["symbol", "price", "90-day corr"] + # Check that all expected symbols are present + symbols_to_check = {"AAPL", "AMD", "EPAM", "IBM", "MSFT", "NVDA"} + missing_in_symbols = symbols_to_check - set(df.index) + assert not missing_in_symbols, f"Missing symbols in rows: {missing_in_symbols}" + + +def test_update_intra_sector_table(): + # dash.callback_context.triggered_prop_ids is only available from a callback! + with patch( + "innov8.components.intra_sector.callback_context" + ) as mock_callback_context: + mock_callback_context.triggered_prop_ids = { + "symbol-dropdown.value": "symbol-dropdown" + } + + output = update_intra_sector_table("Technology", "AAPL", None) + assert list(output[0].keys()) == ["symbol", "price", "90-day corr"] # Define the mocked behavior for template_from_url @@ -140,7 +145,7 @@ def mock_template_hex_color(url): def test_update_52_week_charts(): # Use unittest.mock.patch to replace the template_from_url function with the mock - with unittest.mock.patch( + with patch( "innov8.components.charts_52w.template_from_url", side_effect=mock_template_from_url, ): @@ -155,20 +160,20 @@ def test_update_52_week_charts(): def price_chart(): # Use unittest.mock.patch to replace the template_from_url function with the mock with ( - unittest.mock.patch( + patch( "innov8.components.price_chart.template_from_url", side_effect=mock_template_from_url, ), - unittest.mock.patch( + patch( "plotly.io.templates", new={ "plotly": { "layout": { - "scene": {"xaxis": {"gridcolor": "#123456"}}, # Valid hex color + "scene": {"xaxis": {"gridcolor": "#123456"}}, "font": {"color": "#123456"}, } } - }, # Valid hex color + }, ), ): return update_price_chart("AAPL", True, True, 9, 50, None, None) From 019b1ef065ac06fb814ce7e0914b105e168118c1 Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:42:22 -0600 Subject: [PATCH 43/44] delete a few records before adding new ohlc test --- tests/test_db_ops.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/test_db_ops.py b/tests/test_db_ops.py index 40e3218..0084894 100644 --- a/tests/test_db_ops.py +++ b/tests/test_db_ops.py @@ -77,9 +77,23 @@ def test_load_main_table(data_store): def test_add_new_ohlc(data_store): + # Delete a few records to simulate old data + data_store.cur.execute(""" + DELETE FROM price + WHERE (ticker_id, date_id) IN ( + SELECT p.ticker_id, p.date_id + FROM price p + JOIN date d + ON p.date_id = d.id + JOIN ticker t + ON p.ticker_id = t.id + WHERE t.symbol = "AAPL" + ORDER BY d.date DESC + LIMIT 3 + ); + """) + # Store old data count - data_store.cur.execute("SELECT COUNT(*) FROM date") - date_count_1 = data_store.cur.fetchone()[0] data_store.cur.execute("SELECT COUNT(*) FROM price") price_count_1 = data_store.cur.fetchone()[0] @@ -87,13 +101,10 @@ def test_add_new_ohlc(data_store): data_store.add_new_ohlc("AAPL") # Check new data count - data_store.cur.execute("SELECT COUNT(*) FROM date") - date_count_2 = data_store.cur.fetchone()[0] data_store.cur.execute("SELECT COUNT(*) FROM price") price_count_2 = data_store.cur.fetchone()[0] # Check if new OHLC data is added to the database - assert date_count_2 > date_count_1 assert price_count_2 > price_count_1 From 08df4a303753e7ccadabdea130858c4d2f31070e Mon Sep 17 00:00:00 2001 From: mayushii21 <122178787+mayushii21@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:46:28 -0600 Subject: [PATCH 44/44] update pic with markup --- README.md | 2 +- doc/markup.png | Bin 0 -> 906686 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/markup.png diff --git a/README.md b/README.md index 23a6479..c5c698a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
wPtp$WM4vpEqhw{EMETP{L)M6jw~_u|Nc4csHm*`JaNyK-pf4*ZO ziyfX%j=i8t(p7fVbiI3>e!Y49r)lm;&+Nf=C|9 zf!9j&3B9>~y&QCA)*kWiAO7EuFvnC~U45E;y?Xs%=ulpR JsrMileu^;4T1Qg+y3vKffZ9< z{VtH%AJa2ao>uv1J)uU#{NPJuBG=u&UdLTyuD$<%-brzV3A%{0D^d9GQn48SZj=%{ zFz^Z){{OK|yjD7Bfi@2h&)gB;zneE_RPQBqLzXyjj+UN&u677~-t~K#>$4$O3!(pg zLf`}U(0xIt_pV>%<>k@TFwoJJRaYM$-)8=2qxJ;l+7rQXvo~HIPm2Z)`g`5XBUdT( z+YQQgK0C~tJ}l)n r9a(&_8Qu36!H ziR$P66ro;Ss%4x}F(=)6lx8C*Z*$MKWkjUs+8tMGyq4ZkU*=+F)2GxI^B-PZ_{`Ry zdQ>;L+-c&*ZE~JZUpz_ge!ZJ{ZkoJnn%vfCGm{y0>B@QU1Fb&soWxFV80CrItYNe2 zs^9GXOyxXHOicx)8(aO#c}Lu@@bNd>(un=z{7V-av{WbKzAo_`Pk4Cv#foT?&wmfk zo0sIDV--hk&Y>=O_?H3*plH J10Jz`dj(yY!t#A-* 4TOojlTA1e~;d-IsIco)^mp5Iec$InZ8SUw_!(^g@>?J0wWDStG+xe z`;CH8V1C`}F*AG67SQqDYdKVO8}SrbICgjc4+_20t2crUgqju|&Z}IIhi22DJ4O;W z<=>rM?(_=pGP(F?(q80WoFmJwKT4i@9QIGUt!!=(qBB=N7 K*-SmN!Icx%iEV z=jck)2@9roqg`u8Nlm?+WZJRKcTD$Th}CI_T5w*KBK1^+yVUam4qc+gC#;|H+f8Io z?Z2Z?zUZg_Mv%Abf3|Yj?F^S8of%&_ex}yUFxYSg4bP~}9tklFtQ${bR^E9W$c?gE zDfO5wi-Oh0^{vFotu`W!-upAeJH`%WoAI07Fe1lVuHnrRZOT4O!;oSxWR_8h?h0zY zOhzo57*o2MrEcBGFj{njh{H2quAxpZ5)dZ-9xfB2v)3NahWtk0QTyUSL){F#Wd_7^ zelAfJqRZ(u%XLv?15x5TMiK8t_wy~sEVKMG%~$e`&>m$7cwM`i>}k %8}&4+BhGSv&TU#8N7j4yyX(M0_353O zu4&N@qrN;>8ehZk2HvTx-Rh|hRbjjDQeGd@zWq1sWyt=?d44@P=~(qy!K=vo9bP3G zjXU+Rn3)OE71reJba46mX3(Yq{ tby|n2m5CPTxk*>;_V8TVt&-=<9@f# z)|m-RZ?nGgk6J~E@HSd3teH4FyAF($(u~s~EfMqc<)xjm+xe1*Z)OA~c2Ee`sqe|l zhvrIVyLS1MSAMfcJ8n7&`aF29Nz*farPHe@Oi(wxCWdJlc{tmpd9^|~*RY)gX @L&U^G^;x;?Kd#nAokc8H}{hRf@UE#Fhx A*+u_k#1YK};asnOB`x{3PS>hj? zN8nW=m-R*!q{{Fv`VbnUn7SF=1$)!>2(zCuY!`|V^UhLgFfA}G6{ID4aOd{q=Vyj@ zxRU!4`>Z277_(M~8Gb*wtK=G};AZG_@I+5&bl}9!oZyaZ*oI58Ag;UW(*gQyPg}hw z*pSgL??XQMsMZ}D3fHMymI`YfhKb_Z1@kx_2B5M_Z7@UXTF+iViRTD^_Mo*2<@2;Y z4cmRJ%13YVXR1YDPLnR_x<2V}Y8H*z3DJbgK5LpU(0}~XPJ|;#g`5y4p7~K^&XztJ z0jEnAi;c*6TU5ja$y5`WEUT=PuNeDmEWqy}kSRbPw|?Rqt;mERLg!67X%*_=<-UE0 zm!ifhubHT-Td-R&Z1>BTzOCh G@Zu)*JMni*`HfY zjqS-D=o8iZp~pv&wUldLZ=*Y>IQTlZuvjT5osE%`n9h&4aBiaKd6`8(;$w@2Ne&6} z*G`%jR)y;AX|UgJyV-z@7?LHnsx2K)6N@d>;nXR5K^=4ZNU^Pbn(y<-_K)482uupI zGJBE=*lLwLQsm~(-IE6~fgu-1gQ8JAN_`>}<9z!Z3`M^J%Qyy%ik{$;iL!}^s@ ;RUh!F4pr1gzL|kd>ZNEu zh5nRrUae1yc9NEm)WPNPWw+GvpQKf3%I&QWKTmH8P~Cak`j%PwrAe||EwsTDH;?M~ z`Dx>IBqD7b(${2yW-0gCvl2+{b4}73br4P)V3nm3peDj^d&gdhzj*I{znNDG4C8S3 zOG>sNTRAqF#K?uv_mxwjaJ9oRl$7)>sWOiUA)W#_S`hv71 Q&rK0U9;IMdLUH6f` yH7E%k(*ksLi(nr`G$qReCVlFX*(u`>Chbr)q&P~w-j?p+FF9ag)17% z(Rlo;mY$tS; h(u}((RaHN4V7MCvr7n$G4=1N#1DFWv|A?vOF58B$zNS~oKM(54C zRMgfgS4@63=2$iMGOipCm3>k(&2mRB P!9uYH`wxez ziK#gobGC58uv=VQT*alO6)2QVq|FpmLoD`bDH?Nd5Lq#z&6=R>nYFxZ?dj<$CM|6v zkZP6i!~Ro=R^bnqnnLs@`E!N%Wc;=kJ_%kvghl=Jtu;Mm6IafUM$`@YpQ2MG*B;2S zKW(-RWPwB8f8tM|Ln?|2`#yb2qV4~j(4aBXT^i?&=X{Q0jc{@N{fW`k%$Gz@6l@eK zkGFi1W4?v|Kp81kH-BghN;)qE`7=}@TriIuT&AR?`(QoK;zKVl1CU5$IsTMRaRM`b zhvj>Rs@^|;AQE*FcWz^uHhL?U=)w?=QN9goRfef#+75TKRu@q`uNa82Z4|4fw{6P& ztgXW`XXzZOAW14=E#^ IW7Jh8OD%MFoCYbZ+e(JRYo7a$j*&s0;PJj(RgfdE4^t&Ahtj^kj z_oV$x@QN`j80>XPi8BaWm6nbU9JVkr@XUDH7I&<2dKGh9rlzPJ9UUw6bl>LS&1bv4 zeEEd717xK|v3z5miu!sd)-Xbx95EZQj3F~u+VeOSyHv`LxvEt;w^7cPNz|O9Ng+26 z7t=eaP`|$9(#7yh)>q+d7(W<>n#;JNp?e!IXe3Oa#cG&MyAKr`IT2+jm%}OkPJ6cM z>w#LVJi-dlU-`s)NvX$U4;_BdEn>ZLaXma(CS*;(x1h2)V}4R)Xzk%Vu+**G+Ov+2 z8K&Zk3UWB2-u6rfkMtoqS(5<#r;?Hqd*0f~A8gs+RUZ)X)ipJmv2CBmK_Xo)i4s5E z*x&>aIDfK!at }*i#Kl^z-ur0g@h&$QS`*)h1Jy<) 5IYZ2vA0T5Cs#7#JF**5Z) BwE^-_(Ni7L(o5@F)6 zb7S^fm4!70e<7DJSzba^t}~62cG*Q<2}|^VM`ErGtil7A@mDL)pZ?X&GjFak-T$<0 zp(i&;w84D}%n`;Jg4uc{$4UR`PGMr@Z)>?Re`3o68-(s@mb~!!)1o~tDv|>2toJ^P zm$|ySmyw-qOEwI`A|~dp^!b*c+K`&Kv~<2!$-%Fdr`2B721X_@A!^XI-A=KbJU%93 z@ZY<6K6G-nGVMx56#aa8e4HZaye24W69qmAbAFXEVwgMpHya%td4S^7w(t&+5KW=kgSUz z9B&?rpic&KT1zwJ{Bh;B-`=mAar9D_g$?~$MIGLfEhhovqT`d((=cb}q=W=A)2*hD z=*_a)st!z7+?uDKv9U4VgE# ;cX>Do$y*E9`Wi1fpszj{(h9F6(J^4WLY^b>3{mJKwZW> z=vsW<|MKo-VD`ex?%rO4qM{;6@4es#+ d;Yj)+JCPF*MGl&*ZX+u zL5$*SaA=-hUWH?uzj3kD)Yax@KD*ArS5T)9VAxB->B-1RWfNx;C(~8PF?Aua(yqP! z&8ujVq4pH9%Vrl|yF^EYRXcCH!>Iu4L0yzVf5@^{;l6GVJp?GaDd52v>zig*XS^9186RoGZHhXS7=q+KX;?|p3wB2^9D>_G zFX{iJ#1JM#4|jJ5DWzwF%$~l!y0NqLdIg=4l|_wv!Uq6Iy<4M^X8&rznON3Jqyp60 za)hUmR(}EdwMX5Nu+;xY1?DJTQHBp`OP%Ra|B*;pO4cMT4Qr}CA1&OYW|=YhG%13s z@IgyGV*Mje(gQTofHo5A@#J~?ClWg?^G3rRLEclqd}O{GQrJ9#(9qCG`uqva%%oFO zA6oOQOYCKFh=DjdZh~Z<6>u#DI&)Ce08ZYd*>rMs)#glEcZaw`GW%aiUEWIYLvMOa zNjOWEKA}~LR)pPtEv=j*ShEP~;qkFZcF>i0An{9hpe*@=n=2rj{=LG^0e^Rj=pK=n zn3(r+!^~x8t(4bob_)lhP2cLb0v@{Ro3)}Q0t{ZsswJW&++QNn#%}(kR%$VIM8BO8 zuD-pULXDN)OsH|*yEL%A_$R+1%Ve~aE{2*aT*aj3D4ZXSF8ME7A)dSW569EMY;sgr zR@MSy1B8UTpUIyF)A1=44h5<=o%oCqg?iLmJKF~>;S8i*{9}eOltU*K%Au8)PIa>@ z>$vLs1g7K1ejtI&&YI{>anaH-z}?BG?zct8#8AXB$mHlhChZ^z#|;O`7Ca(D&;foy zL4kKxas>RRaw3Rv@JqjY$9B9mXB(TPrs~BoLc^(nk^|MuZmm@sT=y0{>)%hALO(ek zx8OLu8zt3|T`-1pS_l}tq^#J!%K>906{#ed`Rz7nRW>^mt5oXnTbm2@8v!$^hHbxY zEJsT9Q9V)@7Da|UsG;9 R0E_bCyR<$oE5zi47K7CoMWfKsPITU#G0ALjm zfG_!kEISHQ{Z+4d9IL3o>Q0wN)K9lvgPxaZEJg8N^?M7k5 L#1d4v4Ww=IH;i%V-n`SCn}z27q4mhcK+@y8d$AVFc#Q=IrJ z>`VEfDOe028BI^C4`sx*UT8$r`c<`p=yOHB(%7wbg;|KSQ~E@aTFHp|Z1f91oA|a= zh)s^wEHwT{SGzS==W0f=Di!DI!wj{Y;C5?S@8?SjYak(2RSDC}__VqP9vXFG_`$Un zK|1Ibo0l8&@YBZ(8v>+k03sa|Cu6hQf<-SmfjI+B-T66)hp%Itb#-;1+3=mESL#w4 z-ShwmVlv=c*TGMLptC-+oqn_3vELQnHB~ z)qWst7HMV$$Qu8=Cpr28v*vtMnBq3V!%A(z3U_Y&sy7hozA^QjEMY{1kDDB3v(whQ zQZ?Z(CbH<>8dmlXiNu|?s#&Pxy|;H0Z2hi=3O{uW-s^XVhlu9(ZN&=v4hT!Um`zX+ zLl-={`(T(!`fQ1;+RM|stZC@%3?~+&QK&O9Im_?d63&!A!VyTUQHu6CT_XXw*0u4B z6=7r+-Dj_wJLxpM>G2?@^E*;v6}rwY_~xGU#XHNg?x;&j_qy4~>W#t-gUi8&58g}Q zK%E(N4$p&ogg0$03QULdtG0;A{Im-yH$zj_8*-dxLXWVRbXQwliY(g`OR&2TKj>(@ zj6C;$X5F95wmg_*scWuuXxCKh;&06|;mV*jxAestd4-vYTrNrxqc@V(^39H?;=$Xc z%_ARk81pQhuy%Egb3u B}#lB7`qA8e-^Yrv8)GY$%)Of|IS1^_)7P%LY z&F5|>VpLq}s!^!BSD-yOI0z^)z$n GQI4&fhTDY9Xj%$9{X)`gmVp3p6#t;k#DgEH-yY}pFg7vZak2UUmio* zS!SD7ot1YL&!vyER1RRcJX_YZ( _j>_<6&Zui{sj7C{FlJP4x z np0)ylWHb&(;aobqR+m>UXOvc3|XhfC*;_C_?f4J4A$e{L9a2^GU4Vbb|Y z$K5Wx=G9~TBmvlih%T{gj%Old+P}()WT5;B?NMSW%8}iX?CM)Sq?`2bk#?OFoenF$ z8*&Tvl?syTxeU`uKRlpZ9Vj{sp3Xc3#(FIEVoUlJJo5=GgOP%*8<0LVcA&A9ZBhr| z%-$z;Z0tpE^{gkS3okjrs58x^`}9jO=2*r*m-Lj$l)69Gh`acYeDCJbf-H1)xaxnl zY*rkFMJ^fFfa_Q6N@Y9Wbg|6qe|dzCe!~QSeE}O=F|uU;v (je`K)tapN6He$D3MWpLk3!R>S6BzqXI_ zwvtzvs>*(PAF*pN+$p;juO| )MiSEK*Jod|RP%{cd6b)-7Bonh+$Vo_nrwx^4_}yAFK)}vZXbW?q2=*V zNFSe7JvU5FZPVY7Ucu5+v|2Y7w+X4SlwX@%+96$OBKlEri$-F{ouL6jc4WnqR@31F zB#%J5y#l~b#3?om7Gwt~2J!tmT}tV(imwWd!FVGtYgT=>c2bcvjk|}dyp6vmPlm+A z#arOC)B~)IGS=4CNuYNQQM%`Ib`Z>W2S0=u4Of+HeB`Q%nB}{(cWLhO$rkcPEEfNw znIO!jDhkaw@LF`>)^iVs*+<&LYAR_>Y6F6e5FN3CLFgzMKSFwlm$pinXjX(!G2+SH zq*_WSzi{Y_gx8yINDMbZJotI0{5tYVG!vhgQ7Z{GSPkiQjY=gJ`hB77#XHeX?tN3@ z_1K_hVP`Wy5}b<^X(k`l9We +~K!@O|FYp z#H19h{j#LcOm9A_y^AvhKt(DXfMx_h)wf~LTWImuui( A zzGP#4e(S~~f|Of2oW*xu6Ti?CN>` `!PqhM*&nSFX5o>T%UpyeM5|P?w z{il&~`c02~C1|Hx`99C@YO_<=3sHj}0|0S?uv>s97;FM32UsS8FgezIXng3fT|0O+ z?(j{wsH~wO1|x z!i1<^D#pQnT 2B;+3@SlkP;U>f;d!Efx1A81p3FYXl$`( zq<~quwWSXW=#55=7YEP%=W)Afy>xYT|8k0B4w}eB0|S%gh1_s}Z}-pVud@L8jLPIO zHy#HZrK4jJXn%Bd_3)l1)V$GrMy_NRaF2i4#E_Oo#-k+?MRVMMB7js-<(e?r4Xdh_ zXQlZ6?q2!)ltzZ^M})z5_kupP$xi7ycI7Y;U0ha|JZ}843B0aMSl44it&eGu-drMN z^$O6f$5ST)ElZV+0VRV@@85N{|F9oamZnQyssenkIZy+(Jvqpuz!4lQb`Xw_kAMIE z{p0-R&=~Dc{|XKEZ?&~OZA$mf4*Gci7C%9LmM0ZsImVoy$W#mUKhL@#e0>$25aaBT zKWg4ilX0a*=S(b(RO2|O;d13^uvdH&%Ah1^XjS#$OPN?Ukj229X}u* {doOeB0+ z^P{^dbic-?b+mT3yOAI6z^Xmp^RT~oFXxdIF@$C9K*anloTMzdrmEdCB|-m^cxXgoll-Oi64uEq@3u@%6E?&-Oi%X-_|8Fe+FP>ZK) zZ*7ONZffu(r$Sjlj!#sVIZw=62C!y>tAbc4tD~7&%_eU5*!XpeIR#CUB~bq#;!zRd z0yVv&scFcZ@5aY5hcXmOg F^DJtXQ`;i<=(j+Z z+@9oN`ZfU@X%EXwJTrX! p!e;wRh{Sfyre5;+!H%4B3zt`L$ zzd0(!Mwi`6UfD_2qH2CT?C8sCY1~R)rS%A7S=6#Mkb>C!nz?j)(anjqmXNBTmx+x` zGkyZmQH!v%$x-frJ`@+m{NVB(XUsQ0DHR|K_8jZ)P$CXJ38@sW9#30%DPO3=8Vl=P zCN1LPihlDHk{N3+Q`gvea6qcUaclF>+PQQu!N4IuT9YVCui1mvJqMn7Mz1zzm;`WJ z<# kCv@pkBHR#j7yCMGo{nkAM27g@WXQk5Wbm03u zmsFSx-Uu>KL}v2q)is>M1a_2as^G4HK9RXLC*Uue}F#(TUk z*5Ul^D>XQ%w(e1vD)?;^1z!D48?i)jK*@n#7J88I&`_SV1P{IdzKK0a-abdOtzEJ= zHvzjdY$snaX=!bZgZ6p-fzO9TEe+jT#6QU5xS%oyS#iPIeD|e&DFO@Aoy)WP^5jLc zz*@M7DLp4sF?7U@5;KtiF`;<%gTVW%=OW6I^70P<{x{3U-|-@9&Fi0CTTDwM;p^fe zKF$;pz$>EgF|qYf5@hzj>Kc?Pt_$>KS7S|o^e+0EE@XcDT{@ZZ!DkL70f8MRcT 6=PG#!kZ|eQe;;+nuH&ODM?cS34vo4q!L5neJP!|CCcKi=P1 2O6274TlR|Bt0`&%C}u22Siof#_h zv-Z6rPB{N_yvkVNFIjf}=2(B4$V*M0d}#1fvQUpLKFR^tgat9EL)rV-n0gAPwf`On z4fl~HefecN9AV>WY6WfUJ^1QblAnOY$CJ#F+$R=`$mO!4#*9#ZJDVIO1`9>5Hf%84 zm#(9DkgJ+|lE!G3SQuzkHyt;@$~S3gZICTB5UtkwcYFhx_YZ{^+!@0A*WX(o`ZJVv z+O%lcIhN4TAPNZ4&xQ=Dqd1F>&&x|pCQzj)p(?^#{jIq~HlMVHh&q{yxC+!#q6b Ob%Kmk;)F&?yJ_mBDOO0|oHX31nhsIO5G=F*< zByK@loGv)faA|32e~A&@V@Mt60qTML`UZws_S4Hm`}@eM{Q`H|@DS$_Eso?Citm6B z9CN@~X3xJ8a*W7JY(>PRxY>H4D`ToG>0|z~8J=~jwOUySN)(Ba#!qCH!YXzCfceIE zVwxI3Dd9m;>XA1u;;K@`*yYO8KBn~+Ly>lv6~1b-PpAOW=4#{NyMnqV!V0|09PZ{_ zu58Mc<91z@PoQ+{?v|{XRk@ltMtFxY@3cg>RW>vR)TP5yMB*}26FRE6Dm!`rsRCJV zzd;QejkX2il7#+CCq60wA=!aW EFA-Cc#HWv)kt#5d!@hr=JOKW zr<3@}Mi>P0N2!TadNKE^C8EP$z1d05U>g5znuwZk?^_Og0Y^t1n-CF?hF52&9|D<= zg-f=MlKB|3Fd0lX8ygs^6a4#MooEa7wJq2LenzbDSBOONrF$dOoxj4^-Z7Ew4os(H zo){LWL<|8H=vn3fxM6@T0-|D}K4UCsoz+$@(jmEVicEk#QQ@HNvat3D1Dpy7FIi$B zS(T@(eOLR?mzj~}28YX%cPZ+bJWh=;?@FZ{tUsn{ZkHZf@fbhT)pJrS;@_{Z!Du;Q ztj6lqA`JRo=%wSxXy3(R>PDL+1m!#991g}!@rTgOmgjUaF-5mjpo3FS{hdq#i^&W( z7K@B@z3okcb b)2;tN~Ur z_wK3IjW;p-kJyzKm^%j^_Zd-^x^B{$!%7pTKbSQqM#3p5{qv(D4_4JEgNahAs_v#n zY&_Sc9mQh(2;)MzZKbQc`|F{?5xt1vSnl`u_$+%D_flF g$D8I;L zO?_vaEHv2NW6V23t)$DqGl_bpIW+Y?&Ib9xcO+6U*La6kuqYeqw51B2lS!7~jZ|Wp z`yX=S!@rWT6cGQNgtx8SUiDbXmFY0vc}jp2$4>BIN`%Y)eA}e}MmwCCqtCv#)I$bJ z9Z+;iGx?syr-1thqI0AyDiF<>5NXO{sG$CmxeNCAb1EKJd3@xJ6=LeB4-64e`Q z*(apw4nO~N-Ua|R*i!NVV%5nVRqMirNUluncAk_oxM}xfk!3AQB&;wCzS`0Sy)nwi z^}Z^rR=mBRmRd_MOp-u*ZG&U K L%ujx{n{csHuuT$C5}EzK`hm>PR=D zvLbf7xi+AopkKbVTI%?dBHh>2++B~)WUbZ`{90w_M2P(+_d}!a$5>fZt3tjh-dp_X z*o6Y=pLx z(=0$|ujJOrc~p zUwuOTld9b5z+kqrc3VpgY7qAQK`w-#0M?OS_?8r!b>49Qt>3$1mbJSv^!C4#6z@7+ zKr{WdSIa3XHb0jT3?-$utFk$3-(>DL_W3F4+p#qNz2CUU9}yk7jqNs8kCe1Na1T$t z5in^gVg6y0^_eLbUegVsUt&B!u>sWQ)oHlQLCy7s>pxnUM-q@RICcB=A5&DEW>TK_ ztBLU1M8igMB=By5bT;Q$tCWWD!HKiUwclWbLy2#H&k8T1O~hC+`gOr~)YgA?2-2^4 zq~lUT8^4H_ytL6vNKAM nlZ aSjj3#VrCiLu+RjdHq FBCnROXiH{L(LKAs1p zp!N9Nxd=bY-Dgt~5w8Yj80piSHX iqc1IerCo4IbNn^%U>$w<%fx2FG_SCOMigMdgu3j8C@9PnC0WfBVe+msQVH z3+KCIU}k0u1YfOM`GS#8`t1G4_l`|3*vajx4U2JZzHR|*D?3FLsFdHTt2?i*2tiiv zT<%a9?=|8G+pHxgRG_V6gv{!etC7OvXN*hwO24U|a9B)Plx!GCv{r`c4x 0JGh&N%J_C6?XnD$d$F}=roZ8o zwo{xsq|`;-hktz^Lb&+{rYE$SGdRUzvs!4+Bu||-)}5+t1c_k6Ypr`EY^h8ND!i?& ztypYZXXova^+bmfMZi`%JKtSqH{IG5EK#=?qNzaGIX9ZF=4|*%sRxEIWj<8iIujP* zk>Kgo1cCu)Qe>^3zG$`FCqBe!Ew{*Ojd;qf>#z4G$hSfrhqsE*(PE_Yv;}V*#utHa zgE>PR*TZ4vQG$)7v2S%HN!O-h28VXBr;gs%65^&{kEE93Gje`1?YYltCjNena;oGg zTbSp4jOW5T)nYrZ6}y(;)YnEInSoE$NRh+3E&dEkuU4kHBPHchFI{SYkdSS3#n{Ko zG_!LZ{dTMrD#E)`Wxr)SZu3g5Z{^!~rXFQwgcH+0WI&ldUqYw62a+JvVLKe7~M zMr}w2<3;xEes7$QfCfJy<@j}>hHWUxfj9LsX96WxI%R<+RP-c?Oxgi0hRrcAwk}o% zn+q>L35lGQ`PP){{wPs~ET`znfuZc_Bn)OXHJpRq+~2O5w;|+8FQK@{Aoba>aFF|| z_BnBOT${bIsw07Y;5k7d)9yv8z{!GK*^^qW&VAzq%4z zaZks8zPK0DtfU$ptQg~@HecgGX_jC&a$rO${O8G#F~f*sXEg Kz>QOFmV+d+H-155SXVydI2`HB^-{;Qt>NEV=L!=xF;D-`5Q zFT>M=zBfwCjMw_f6^&Wi`gM&z`fGUu-I}d!!Nu}W&_e5Y{HzXpP-b6=iT>yGDX~A* zG4J>T=Fnw>5bIwPsP*o4;ni;bX+;s;q+r!&<79}&e37VGtS}X)0Nr(ixguz0bFI6E zl444ss4$#MMDAGF+3i5PTtRMX%%CR9ijCi8XI1e@>8*6E^Xg6s%}IWgo#R~+#fOGt z`2(lj<60;};f0Q&=l?_VaX Y-Ix^gpSK0hCC|D6j8a^o4p(OcTO_g}ORHB*l>KE=+vMbfzx*XV{k~<8 z45Ant6Ip$IA%1 hjvK*p&EcPt6|H!>h zx3qFHpNIUbc_g9-(~jqxpTd7iWg9u#0;PGB7rSkxz{yw6-Bj*b6A7vii^F)NXIavR zzqYMP=acny4uD~|GwD8j_@HlQ2CR`9MK6Gz2Wai2Zyv?b%cQB2*Rui;#XsdSTYH)s z8Fa(p2$JNMx?pEz^hTmxv%RMWzri&5@{$r*Dy-y~yF4cJ=RfN3XmjWZ>@UZBhKmdE zhzvFgl4&9hU;WW&dBy#wV7vII=PMG{SkuXgsLL~GYQ%f%o#dFhl;gQg8_6@jCnAGr zmTEHcG9L!t4mD^25wWuk*%6XNs_;3EZNDKGFwG<+)NU|~UN6{}#9) `<4Sb?`SdLzB)rg9E+wnU6{| z6T#%586dGfUEX!K*ys;=f{`T--}vlfGQX!EvKK8D$54tg0#U(h?^F-+)pk)fp_!bz z6$o6<{h? 76z8IF$`=Qyw z1TPaw_QNc?TUu@gowhO9jnZ6f91g`GPz=FZ*p1oZxz_N*H{OQq dCx>3VwUbJ5J@Zlqegse7JpFD3 z{lgOb=tPnhSA!PP6vJou35WtnmT&@7k+z@uAB#9qFo+RaPfE7E=r?ruQYu+2A|j`o zfXl~v%;xG;!m)<1iv`|4dLk|@xpXh+6bm4aygU}5I! L zQ<%twE)5x^Jl9UZGP7=_n-v|rYp)qW()nq%bt{*Py1}JAp4^Q{n$E8rdl9#cLsytT z0V|3~x);gn$-iS)y_e6Iz~F11;Zgasvx8kEfmv etH-{FNjQ7tUn`D|NNYOKUUueqmfgT0 z61YSlP_@1hHOFGCT!;zTGQDJtCg^Y91QL#6l}eRvo(|_=m6AIxF52TSPRW~H`L*}1 z$2%G$)((f(OfUaS!ouDB@AU87PuLFy9Bj1Zr<8^(hiQfL`QO$O3tw+p8 I1(f4~^$)*|_@7QTk?Z~ |(%BOrhM=L zCN;feZ@54B?!ADBdRn$ngqr= zFAa-v!|KlB%i=ha6P>DBJ>@%;p sU6l`@Gm-?fOI5NuPU5 2uV{u~i#pp9!RGGB(VYfruKeQN-FN$4@)2AxZ=o0AS(uQgf!V8u8MNz&h zOH{Cp&s_wo{a?w+B<4mHL<8pH29ndJ^i!obr#rS&SK&zMkBq{7RnFYH3gh rfr9jg*BChjES;kioK%l$MQds1~Z>E)*`Yx(C)Gh3B@nI?@WgsuiyG9 z;r``+2Gv}t|1oG**dn<<1aJ$F+yX3t|K%9^;{q@}0grV%JY;o%{BbPi*SYrK(2x)C zk?5HPCDoeTmJQzhYlw9$-L0ixcJpzO!- fWzq`R-mdFSX?IDFuNuqm zE*4%;Wv`nxn&5TDqQA-gOeDTvO3p*|Ii)f5QBk6VNXuJDS1f!*{6}h^m6kw>;{zHC zwpR^-H<@mm-24E274ylUCE8KX zysl4XHs2L zeR^>?1f~so+ OE2R?UHr6(QC) zMHdob-B)`Vsmr&-kJxXBb=MM$$u1 TUbi(RE@mXf^Jn^jk{{7d+0cTVaKrxFoO=sNayvO1YNM{9M ze2y5aGAU^~tboS`9kZ{frI#`Ko WO8*u@my2OzQV%X z$a{1J`p{ARw2^>~H%KtgI-^5_=@w8D#U#bd|GLKqRDrkmuUG7Rw`uj@AfE5eK#pqq z)0$~#V1#@|r2veDH-Lt&o+WMJ>J`Tx3%uBpL02 2iaYT0-`+-g{QQO<*qS_|Rt!geIShCHxMph#sp}E6L7#{aF5}8-lU0%KOE({7D8e zXLGP`K5oQsfPe!4G#}MBRCT1xk%6J%;)uG;a>o_n@yW?5MTF_|t5nGtqgq?{ThOaJ zD-W7(DogR#+83+V?V7syJs`s!fsH_3-%lh1!E)j#&{siNU9DmDVFYG6!c|?TK!Onp z`ts6uaO CPk_g)?23k8EpFxM3}>TCmM4| z0=GZE(UtXfc~X1+OA723E@y+Er9kw)x{?O6;60hEw+ =2lOIs*W+rw`u+AOUXBhsv8A=Wx4`ueE*h}^0~06UDH?Yx z%FA0jS8hv4NEEv|X_V-e!?eTk&>nENlhEH)mClQfs?4jj^jf0NgkRW@!YpT*ZV8he zvqPO|`O`JVI}K!cCy&ch@D1;CVwN;m5G~3kGtH=LA&>Dv-I$nWe&?agS~ch>w0N={ zy$6+mo1jw#6+|?PQ?qFw7 U2*u_jXO5r%{rz2Ke`))htv2Ux)X C^GB z63Y%T&s(C-BdVeHH|dSwF|Q-4vIbtAD+UXSe(2~`LfI8!7aPLQbaJ0ck)=}}L9x>y z$bzWQpB*w0CQp_uW)@r8)jA&EQ==5Y583N`lx%dnaR@kJelLZ#wY>l;y@8PtX6ZL} z9&~^a?-p 6)|Ah0|aznU5~~5uoIf_+Y25jQ6=-M zGXegeGUvnXe;7%?-~Iean$XkUDUV}F_6E!!o(5v|EH^2~0|Of cbP!4o z`B^XCc!+n4hnH7eOw#=N3^6k2;FAiRXKbY8v6w|)dKHfPknmP$?B|d^cSZ@bEfqGF zL|A@im0G!ov7lzb>-0!p7sQUwa*Uu6i{!24w4UYu{YO$S> -(I{WP-?=L1kDrB}f@ad?w;jkZN zM%zdVA3(dPZ&G^Z s-LJ@K%Iu}0*&Rqy0FM|yht zcjR;0E zI zs#@IoTJX|T%qGXeD4~*6Ur sx}@VF+~=WLP1Ea*}g3_G>M`+N5Jo3352V}Qcl zbd}y@0fbaszk2}30$Jk2bf2+_hZqE685EQaJo;&1I8ib4D8}Qjlejl8K4Q8T?`6Jr z_UqSJgKFx);f*c(vJT^#hQ8gu`WCBS-jRC9@-S_KoopoXoqxO)SrN)XVJ-j@oUr`v zKG!oD9FLrrw{Uq@!aouM^;t_zqkzD69@zCg;p#N;xDjX4ojNKX`aRYyXs0h{@C$xr zK-T1GPO=GDrO{7hMiryAYj*i#ve#|FY|Rjb5{GfuThb)A13EKtadFvP1GD*GEp52L z@C EGnK5Th${>ut|*xx zT~WZ8ik&b|#CH@TtojoQy;H@g#CjZPKJCDEk8JM=9eYNsN7?WGD`hU(8dd1n3nPRW zyg#7Xv-PDKUBWrf>$#up9aJhQVfl|%u730^Z`tF%qqlhsv+jH$f*E#>;c*;Ac|4;R zK)92^!9*D1=jtCYp#r9xvIGBc92v%_ uLN+!+dO iDZ|(WhX1yJ2J9EIb>xXvgg4ue%I-HfA9PEc=S)D9G`K$ukm_4 zFBXO7xmG6M crN}BW8*hkr--f1&ZzJJ!&T(zF z%F4)4f#tnTbv`R5NuHf^&d ZI;adUm%%DQc&0cKSJ z+JU8=@4AMQ%;Jrkdo?x${DJaIzdy)4)28Q8=6!B4`pKm%A^F2d$Ea4QCCiIP4--1( z`S|h6ey|M)w{q&2l75pbrB1jypKl%zCn4xK$M$Q8OUYv>$I61a 6jGv|>SuB^ffp7B8!3qzB1Ti>7_DfB#~ z%DP?DHl%X1o`uW-RXVLg%Q3_m88q*#LxZVQ8We+$&U|ESFq@{ilBZ$+Gf!?-J=9c* zUy=92eZ6t#G|;`+g*v?q*9Zn*3XF84gPCB${y{50!6fTE?HpLhMMNd%p)v!1#_6iM zx>;`D @kE(G&CZ; z-8 A5Drtb?LGU{3qdHH4JtVLIDwYTG`W zSo3rSG0yo-9WCU$olq{UyzzouP4E`yW3={<#bIw6Gy3{0b0k;uY{m6wV=S7v9sb>m zk6X2~imYi}c-;#_f-<$UGS3KNIR^}NkIf;G@Ej0#;GqIPRFl0(x5I+~7=cV-B_R}< zm^i-LD&@bQ`3@PWd=;Ky@EGeBDgv@VJ71kWw%} io;lH105 z!{8z2IZlI-@uf 3@{^^ikpe0^v+dU(usp>t*Yu<8+AL9qP t!$$RzIv7PImhsJB3Em7rV z>J$m3pLw==iQIAWODQ&2bo EJ*LLJNgQz;@%Y*_5xYl6L19DRer`Qur|Tw1LLy52*aNHYjY12% MFRgo6Yqd`o_f0}Y28Sg1PJ13VFaoXS zAJLP4rT<@e1vg~puNnGMB~18I)Y#)xI97OWDqN+_Rz^qhWZ7Lb3R2yh4&jC(9$|oh zAoFHMgjALo|6aqv9}Z}>?j4V=K?Aw-SHBOoM;L7FUzEjio73c8n$|QAD@TgM*q`|F z< Pfsn|(rzxyI_V zP^oGk?>V0Kss|t5IX_t#b;R4H4SKaYdImwDSrrV9V}0}a*RfA!BP5>vY9Edbd;Y#V zc%dvz$)}mAr1wgm-nnkXK%z^xQBiy>B@3kXL-2F9^N!n6MjMMz{T>y1ZA3M&; +?X4<*Y|IYGV08$P6b?xMfli1wyGN-3RSbIu7F*u~+L^{ht>Z{^)-ee
u7H;pW0wRmAB)V~)>l2$Tz4m$2|X-`b6Lj4*EX%u5b?W17dB$Qm2|4@KRxHte6YC|&$2Uj?BS_g_=> zn+YVbCe-3*@ebgN{q|al53(A)uM~KnC{JwH!`(H|t7~oB Xy~lZ02eL z3wU3tEssU1!*EVim|C0V%a!(jDG7gTsnnF;rBpM{g`}OS;ew EOW2U1|m`Q1fnyXIus&Xajl^B9PllFIWi)m4OYr}=)d-&s} zA@A6$OTv3Z9~t(Hbq@?%ZvC-#7{x~qcj)$HEbpD~tlE2!^aO^jku_!5hQ6Oa$-#ds z2%{`YEcg88kXFH7oYV kJR!BIdwJOrMLJ ztNTnJ rYnWQ@9H5 z`2F^igUjIB<>D?bD8*X6FMLxM5!7L+G~`8>Oh|X@79+|WzB66v(73*y@Tc~VCJ G10k*O&L!q)zNUTG2%IfnOdX5RP|KdeM$n#hIct#5#rhKESB zQ6v|^HG`E@NH#5jj|+W{HyLl5?7x5`M=4K>ZzsYwj)$nUMw(0x8r9IdNl?v23=dyw zJ3Wl?1|rzrpDyle5%O^2Oi0=F@>d8>ZSy~I`c{6CF~ywtV9e!^?U#)tGlp)MYTnOr zF@7(}<>KcZBIjq*ANie>B@s6q-)Kn)c9q72RhC?ry`Y${qq_EV_3|w AUsmgl$Hgc4$B^pSj-yWwrA{6mZ_uA?w5Iv0pm{NbGxlq+P-(k@J=Nm4a|Kk8GK4rwoQBvLC@z=`+SlWGWTGBF=jZH>_0RDc0<}H)`IS6UW_K znkO+PGzcLbNpvq!U6okyOC`AL%|Wrb5?{1Pwf`$E?ZyR+S-&NChyD>D8kbnmz6$)> z-I`-6CyJy6-w7YfH{iDvYTpVs0VAUkRj=q?tRmC%a$T1~kW;#e`(*!o^oM%HS_s@8 zS|1-l(16#s-~OA0crSCfJ}Mi|R89(L6|)9|Aaz96;Q)GykXeGv!<4V1Wj%yfN^0Nh zc0~%N94Xp3-lnf#j{K?0`Qb~O^5wES+!b1yF|A7a6{O#X5=4^94C)O2P$RyLUqmP$ zrhZr&HJxi&aV)%cai}NbX+wJ@DbHequ)h&bQ#t=6R3eYRW<~@`k<9-0 h%}g{b8pdw#QdpCaDkxcIqpN#>!s>pupRI3yeg2q>rX zt_K#4jtP-#=Iq;ur8*UatldgOZ_?2tf8NmvNO`R6Zb_l^Rzyomi*OQO!*kfc*P~mI}6?Ism}_UuGCX1oO>rnLz! 34g-H+7u)K5=%yWpuH;P>-gl-axJofy${~$` ndF46U`TZ6G7yMo< zIwk)6XlJvPf-Xw5r>> k_pF)^GYAok^7@&qz(g5z6=ZugpH<2 zdUrOSU``B )VfU5xjj!XY_|@(Db7)%XQ`eUs|6&2$ z&5wOP-%DW$Sqpj?{#qFwtE0}ASDTN9j(L*ayjWt<9I!$R#>&YJjmXUPI)3$sv9n!- z0CSGaw;%d^Vb64}ye2Q8wu)a_&ZGp7Y~9=v2oE$8VVS5_s7qZ ;cS`}W&4G>KH=nyWmCFU*dAiU#5u2a$*?LH;C zJN%TnsIKDN6CZJ2epI&>Ng34=WkgqmX#L~RmB!O7XgZd;N78P&SsPr=dE4IfF*P&8 zb^PPpJ@SKTGa_c<*GTbB*W32Zeo;`S?YYP>h!DMS#-l#9!zAU9zEOsS08D6zTe6K& za?%(T>~L~)s`F }6pi9Q=~%o^Dbj=gWzkW;bWH zsa@ZEhk3{TTgD(~sSv)$+b}7 <80@vy(oc!tt`}*ciOj#gWeFI zO|IugWQmB|?JD~=iJ2_AnZ6cub*zeY;Z9g%gT%T&iUl%)q=q>q{DqgYGPVU)b3+0t z&Iv8<$b>r1n^yR0cRdOHy1$iWXeq(?S@cuqPfa|arahpvxvy@z`KVo*jnr`9kiu+3 zps?eh;UUTEQ29(9<(?q6K}UJ@XoA5lQRn02ye>~&jt*41X0(RR?OWr=QM0{^yFbEE zygO%8I7M{k9nsNVwkd=>gTLgcUAF{(eX*tAdLEwa +;11LKV+ibbgZC!WXhaTZ_xu8?Fby?A= zi#8apsQR;)V`2jNTLOX6JTv}ufJw0 -vt2b;vXzYj0W({h~_F_oU#eD*kDpARt_Ux!@A)IZ0yb!8=wNzb$R@@C~ zG$zL?q?DMjp8YCM&E&!E_NKAha;>5abNWaj-+FtaB+njXi1GLBgI}u8vyx(JbXe1* z48S+nHhnYAD^#&nY=5_4e+ >ZAa3P?Fzz=}q#%EMC4ed9Kws zSCc@UWPQYJHI;ek(YMwW&z70(nh?a}!3U0ik{q@lKeRr z@vPr&3R3e#J{o`Vc@CcRhQQQEoZUd z3#;#D&Y*^Zx3YF-m!31R?N=p*uS`_)*VVj0S0vge-a*f-I6DU(!ddL;FlzTvKg; bSKnA6x3tht0;JdZ7uzA{aAZ8s2XV^71-PpS5e`Lo!@gyB^}c;Ek}| zELp2CS?xqlDvtS&o}1nGUcJZ8-WSwn$Uu+r`9QN~EbgmB9|J)v@R0vl1ulE#1R3|> z*g=t7UDf2xVwwm_M@;3_zO_yzyRKKoUFmi+MV@WmPo4}jx mSFanr@ZWj*+LgbrM2e&S)KbMt<#pdQ@2%F- zx{x^)ld8NU%(T00&5#Yc&iy^Av7o=Q_AUZF@OIL! WOemW+QnBOQ@?iU!|(^pa~iRwo3Cu;DuuTjS=|j#9bO3prJFZYfFA ztJAx!DBIvym94*8t>!Q%yvUuLj(0S@nT%=|n;*7$Pj4 {yf=+b`WA6+%Di(0Db2my)!ZP}|K8`uqT2wn*BG6Tou3RD zksQU;L~G@3du^eUr6N3@H72#Wjk-}_l1@@S(CiLw>Dld%FN|)sV&}6_HjQ?YH6m@E z^Tomr pu`YRkh{Tf3xUbOkp9Pz^pEFos{Q8Q zXO+g*;fm+rZMC91lQyV5-Wd s2x ze2S7Ptj< {kC?hxNxn52TNOxs`inbn9MRi^o e>%6y?YX}+TBbG*6Lr7U8VU6Ft-(g-y-sf44^w?k{q4?Aoku)6##sanCxWK1X2 z=cJ#^{<6 |Y~T h!tEE~=0x;KY5<^*9;$P2Dx8|Qv%t|tiwdr5HwI(i^K&e+lC$gKC ze%M8dXY{W1O;j+eGm(NZ-bhiw73lZ=SL7=H&t0}cpjwg2Oadw2q6j4Ze&=_==gWph z9@DnF(&a`sJC84W{{0Xp-`a122;DYJjyi1U_wVVCH=k81cH||7DNg- YI~fd5iZ zWzz4atHpcj2S0~9*Idg?u5n*I+S``~=#QKfNLq9ZHhv5R=)3f+5*DJbcZ}aLGo&v> ze-Lrj4jC;A(eoF(F>pGS{Wm+? ujwgoiiBy}17x@MrZ%=a^y7|B2nAV-bhhef2 z)dbrI9-J=Qt;QN|bXcR?CCFD{5j8fvO+s z&{(wsk)_(Ednwu-SLej*SogD%6**YCb#YP(v5Jy6x0ok}ZtPG+_?2OgIqtsuqa;8~ zOfIkKq!?XKo%qIQm?_v8k*)Tpz1)uCxaNV$yT4FZGPxF{SD0Sne(};vy&pef(`&Le z2uaSpm3`Zim$-Sm5#)_NF03Y{p-YKX8kW>bogJrgzGACy%p3OE)h=CIsn{ArR~57c z)d)?DYn`oMJw)C9&_R!>SzE~|Is86jCkDm#zwc08@qgS8PBJ3o8V3z&Ds68TWO2dg zaTe)aQ0Q_}s4GsnGiQfqEW~(k$#gnFDf`*`?+bRBfj$JvS&;4aNCanyGGt%rb$9DD zss7Zfy3a=0bcN-Zzis1%0^etw1}3_rO qsrASlg&@HEs>bTtWR#%K2z8vSzV8Rr^5hM z$Nz_lEI*L~g2U?K(Uxe;?JpgQ&>O>>3XGh%r-7;b5um^(=<3^1tCb_usrBToo%g<^ z3f*}74m>89S6-aDPEKi~msK)IgH>OC*88eX_b!~O{hr4ZQ~GX@(<#<($pDY6Vyx34 zd+~ZWn5|18<^jG%$_ouf-6S+u ?(urb ?ffHouQd+%WDm z6dR&en=|A+(}{f_T@x3-8I#{xKSilR9kSXRq B>VT5vyy2p zQ%+D|_Rq#iSx$L>Io3-dHsTZDZR7BJ{5t#g0&NniSmB+4L`)9=iH(w57GxVMK5_ff zxjo`c;PO*Yh@3TW?i>Bay{;6L!CJfOeDPE GGInqjbFXq^u1mXZjV*?n>WO7GsdJ^W zpGtgy-y+7yaDPhID0$u{h}DkX#m9siy9vw$?{{8R?&DVDPDQO-V><`fu$xgbT*Oc2 z8q_#HX8U||L~%U!?YX>YBWXfl7rs9kBxAKzyB(}n@M0;UG41 oGs6FPTP2(L)lJUMldV22OZ;SLkAM@% z(=%z#C5bTz#&!TgK{~t3k>DlJ?Cs1XjOXGEi4!B~{U2tk>Bep2phpZk5r9>LbmZ5s zjFIR4A5e=fwID-)nE(hcN2mtPt4E)|J+-v71VZgWYr#CNkDz3>cSlO+4jPHm4O$3* z8SFE*?7M %TLhd1u(x%Qx(Km5?0ai@bm$WGWL zz$dB`g6nv8Hva3Ydl{^qV|V6Kx&9Z`F8_^cX%d#Fx1GltRvo(c(oG&j&i*2wvJ3ga zc6(A%p70%nKq-2PHOr7*CbmThw3uy8-`v_2wnhKnHt3qJAk%)igPINSEflHW7jZAd zo42`hX56KIsUpu>uVH2Two>M*b0GwtY4=r+qqufw%v%%#`Rlsgt&VB_*fB$&28&B# zr;AU`eRRJ~b#LJw88z}5`zNvqiC=^pc8A`q9JGgXuf?a(en-NIH50E%shHJ1%A^ye zy4HRZEKGR>j7?ZXfjXHt-0GL7CuQM9qtS<`lTE{*W^O FvOElIYJm9!Jo<*duW-DX<;;(U#Xw^?li?V4NA}#}5HWET9s--gHDr56E zDi(-hRhP>j|0;_cb*i}zeK2*hXEv0Seb}1SI1C#rvw{pX_Ub?TWs)_lL8liq9V3JX zD9fpXdk7O-vE2$Axbwc3#^x!AN R^PmOA# zT{Mmd^;=iivKk)}QS?D$zG#T_jGV>wH)citc2@ov`FFwiX8ks*r@~9WM;>~t-rCa~ zL2r5dxvet6q=;_waxB0yR3DAXK0B#R{R{U7Wken#LpN_4;K@$P6ci4kiqHBGPg|HQ zN55}hoR hw=xY}iWr*2EE&uDFQZP9yP2>pGVKDMoF z-Nl-TFDUE_-#F8q!!{{}eeXK=qLWWCCO-7SM~Z7?dYgYL^!l8;d`efgPVU-DPENmG z!Zfbd!DfV$#NJ}*PH)x4_Vdm66C8mDm2XU^M6)}H1g~ya=Z 418eJbaW;u_~9 zBzq0jaeQ6mNj=N}Cjiga?vRk4o*oX30rARXoXd!+Vc@`PgRXXu=t-gW$k>>vxq0Bq z_;o}d6XC6J;>+NmK*OL9>qU8o^IQFse=~AvH_Cs43e!G?7<^YS 4jLr6o zg-&(7K^m#1SZBH#D+%6d2oQMEZ>!z1r%&LoaBBw&ugYJ6C=K#G(1h-IT< }AAMs?Sx`6g`{cbXa{H8+(X zM;F_kFrfG@^=h_vVdq*yunlQxvq=nCuL6rqK=gdBX$E?Bt1+*hIE6fKCokr`7T(CB z69^r^incr<0JBfkTt}IAqZ9=(icA2l?8YIr(qI7EK -{t`Uzjsy{|T!61lV|!ivCPh=;`sz2C1FJ*Zfpez=WZpScTT`UD z)6sh$UIAkUfWPME=IVRrM8b8fwb|8yG(r(l1=F`Lreafo44SfdkgoQ=ehi1iG-we2 zvNKj~%a3Z5N*ULq={XN)70{Tb-RBKP*D#<*kI#4aD6(qPWaCdzg{TNp72ue&ws-hP z!;lGpb-;TU_c?^Q3t$;$^0^zQ8;|2on)$JZRP7s9)pCH6g~JM-vaIqS8EUL=TL?h9 zVQpi_n;HtL_&WV~?*E=z7woS5V_vw=7GU!D?c@AMpo<3yL8127E%9j)<_l*JJEHT} zUTKhpv?jdDCP%>HnCQCE4(DRe)W8{$f7n5wmhWtdTHL2(!4^60b*2fASGaXMW6`50 z(TYs)zydRrH5ha^j-z%~fj%jt!!|Ruwuk#MoBKz0Nz2hPd2lM4Q >;Y(Y>0UM6AdN35C;&iJP+OV)^@w(uG|ji=RAbIb&tHg5 zt<}g|2QoK&!|){_r`~0Y)x9mbzpWV@a1+MfBH{M>5oY-vApA1VHLn9 cD^Upi=^Nlb$mw`*YERA~Z5J4w<_-1d68DlZ~J0D_OB{K_tw0t(1 zK~pBIxqz{cwiRo@%2hR%P#M7Q7?Dl01}-{91}Q*x1o|8xAS9l%bt!rLOX9}EgStqr zV5PJYg9fi4v^%_?5>^NdBGQ*wp`%i{-qzDc3i;^RvI9HEhnXCEIQ<7R6kolcP;9KJ zX)W!kU;4Qy=-#bFfgzvY!$(+_;HCs)@$ 4oC{_oFLFaOBPfh^`sCzS;(~*Ne=|zuaZ8 z?aeospPZ%)fi{I0M}GdYwcVhBPP%sf1|TvH7o#1jyW t0m~Ie z#(@}g^>zBbo<=bE;wC2OKyAES>f2?f@ZazZ` ^k)rsLDN0{o50 Xd2v~n z560Cu5bds G~9CYNb{-g3{+YckY(+-8*c2%+WUWQHI+hwOm($PB5(ULGRkiO>M$Ws0OBER4( z_x&e}d1q4jvZLeU?>=0qxW BQlUUhff}
+>CG<@dU>!=bm!xGZvO`6j@!qmam=vtdC|No!5#^Xhpw0loYn N{ z>iMlJNt?7p8=QT9S@5z?%=%QZ!0{<&?@*0_)9r#cn=^fpBD>`3cv0`JP#U*J>z?f$ zmuarEK=P`Y^+vcRvvBA5doEl3jIIn<>1%oCm^j%&yGD_&c82A=@p>)^(|bmz!Tz}lSt}>whHh`$h6 $LmjYbf&&umYOXEiEiW3iXD#g0$J-5PT&kRSqL;^j_zC+XcDij zh}DJD2wbZPsyVU-u=EF>1JM6|J3c-}T+;p%2oK&rznW|B I*1qzY=9d6wq7a2FW zoJ|I&nLy$tP?Pe|I(RsAmYVk!jB0m3<@u7P3Wp+i76x5%wOY~+pw0rUCDGS{b@Xto zvyRfE!l0;lkeD=1;yau}i3rKWYlr-uje`ac$}_Q)t9WqmzEsSPEWhMHi{I|dO0IMY zHb^j6$hnF2qic$6x^&ZZ3#VD}Co!OTf{dn{EmUheA?b#4D;K=f76R~UjxbKSiJS4z zk_7Z~G(Vi-)6AUsK#KR0Et+}$P@U{b>y>O{g1_!p9jl!mM+HyVGt_|h=p@#oJ*1?r zMS^FP`WWG(zR;q4)5g|zbEBP}|Jp- Nyw%&aIre3P}BoQ%*g2Kne^U31QRMXhn>nW~4< z GIIs04$~Qwm#=uCmAZX=W{Gv5Gw-{AfYTNcu%xq_g!A9qR)p z>xKuccgTknYX$0_ooefUG5Gsjl-pCew4riJ{A@&->I0`xZECdnyqe`}AsR!_3VTRI ziWrGdY2>4T7u|wv0U9CzOV%2&;ZT#^1wrDXOU;WbUQl&G(wy#Xi?r$L-!{0~3Kyn- ztxw8Czw*;<=jxBz`S2AvgO)=FwZId|(*`-7NGDD!ks8X+QgG<_ KR za^^Edb`D2tDwG(SHt$RMlwrFmadh4VK5{wsXKk}jP))M;Xrn|exPn{w`khkqm3_-}C(Pf<>UYa5C5ixS z#Y#5p`KjHMZ~o&TFXl5(cdG9yjEUp R+w^>?U{q%XDElqXN#S #HrzxBN?GEFKuYB z)m;>zfx8bOhrdLTolE%~E1Ve+EUvN;NdMm2Y*jk{O4_BDH5im0fI0_{59F&dB@c>G z*@KHXV1| %z|s4dF{3+aCJh zmlYM2Ot5l}YvD!G_K7O7XTy#%C`@2~5Nk}R{nqTP|N0G6+K8YTH`_Tk@S;L52^Io@ z^jpZCx}8^Z^3A=ZFlOMMmtFmA3t9`1bSrf!v1WttBV_BM*Cb^wr8J4Z=lgQd`0`zg z09NtbV4vj7_H1+ rR{1l#H5 z< !z1?p ?p72~t{vcDg+<;3pfOq;=);rKJ%@uxJKTs0!cp!jU! z%8Ct6eK;K=g>>;S3oq|Os9WP5y`%>t0Ut{~3|N4o{?9Up!y6FVM|?N@fbC5AX0cmG z%Ddt8()YTCwu4_{EG#U^X?+y %*o{+c1PmsgGiO|fEY}oO+y|}&7eUfV`VLbdLs_!fn6P~a<=;JT0j$e}7P3$a} z`9?KkyC{=>s+G^5^g(lio2saGsHI-YopbhGu$+z$o6{pvcczTaqQbW@kNAdY;Ktql z+Cv53+s%56VMlOGyk-E+st7Tx6*{%t=+?Bqf34n@dvv!Z>tcBAI2Sks;2DM*5cVLL z0R$#LpBVnAGWC{o&)CRFj&?EVe<5Ceef6sPV8NLxZZYA-fU`c3ohfcJia{>$EQB0t zprWPCq{-u0xDBa3>(S8sg7zA7PB&Rc^;#5TI_!q3dHyFJay%RpdwGN(wunLfvp#hX zN -S+}S1!7JYCu9L>|zj2Dex zqiwH}Omvxii40Fx#v!=utiP{eK>Y}}1)w n!*LHA zqo!&e!bzDhGYf=N*dp|M>YjMt%v?wur0 9nYa&oj!Ya$|BHg z%G1x(PlMRGvQ-fo@^Q4~(Yc(CHwJDB+%@m%4X!Sc$R8ah#}1emZZ|GEs(PF}NUPOA z9XY*=)d+@0S3AA0aw7`e8bW!$;N^VN#97I`^|QAI>=B8Du{z&>T6`*L=$-pXeq_*B z(=|eoO}I<9TvGE{(KA~mPYV{=1ha;Ntu+&l#`1bO&u0NG;jriNQ+<7puwfDwJ+OHd zij(CPiT4v9_^;9AmTc1{HneYhpJ=!@n)GvH9DOI%^SVq5-kg1NMvjVIi-?`keA0Y( z=VPMrbzPdop^r!(C)zi;Jl9#pLGOsBUJnRD5JnU>Pg)UW?E8 zn;T(2UJXldZ~9He8^P%n>3PMLe^fpQYhom+lG*OZkucts8=UAbu(47kee>~?!|9nT zq4$HIKA$+@7I8YPt3t0eUGOURuHd`*6^z!!C=;;+4OTbnwPhU5O&;j)PU2Jf#SPP& zn+~fJ@l4!9BTw=Kw0qA-Mx?;%t*@E56<%PlRLnM}fXWQ^+i8DQR-ojjBs|v{L&%ZO z0ng{}8ZHPk0QUnU4DehWF`u2pvu4!u?#)@hlb3Ac=xzWRFzE7OMFy^; la#WjVy4|bkbNP?y%m^uCSU*x;?hI1@!^31K(t3M|4Sj!R zjjm8*A{nQk<9ayii?O?QDG+?z8ikK6g%Tk#h9(w{js-mYYD-N`b{x$W8fueEywnly zo3jLvJ@5Iz_CKhpzZ>XGp?tT5alr$}dVq-%> V_wU3w4uJfE TRxN66XZ2oHRq%Pa@YN;jmfR71JSP^Lg*uO z*b8Pv&mqK(udBP{mj&m0J;6S *h>NFv77TGdfH{K-A{2-t8suF`d#>?t`l*jK-qSZ~CKXC3z zw#Jpo%X|#2aC9{u?-T=W_L&GZHSdK)mWOHUb|>SFxLuh}Za5b<=&M&&ZiCqt=eh8D zN2- ZkZo9tgc3Nu6B3d?LN zx!Hk~-x&9Gxo{AicfR NI~PuU<3!YcB94H` z#!8`0gW<6x%(O9*H)>CYo(lpfDW9Wl0|~n?oSj?2l8>fD>m)7z=-KDeknfE4I&$Jm zv3GKlg$-a3?Vp!FPhm!VmUGYhwk7t?8?tp*y9gK_MLR5D&gYEEWaJHfgfktlkBfa0 zniD{gn(K;_`s4U79vd=s;g>j#&)!JslTVt8&&-4t6|J(#K-B{PH-a*ayq4oFGy-9O zTf{NcD+EIhC_{QziMv*wMYdEWgv#GCprc)%Gi+$t5{DTCWKS?7J`0NiR7&`RfzUAp zd=+?hc&|mkR4BOh48VpA44C@~kYv07)JN!V9Jx+z?4S(IO|Z+QhU$6Rz>&huL)ZIn zLG=aAbyL`90=7Iqp4sK?=$N06ez^>6jFg(Yr4lRwx @vONG%U=@&<3(t?Zvx!T3H%}Jha3klauH^-(!B=sOlcbC;{qU@EsitD{O8J zkNwJ$T7WL-ywAvz3pC(?kK19;LU*_-pNw}en=xKjZYWPgPOT^4FW`GC(fM@#p6<9olNP-QxRIYv(OA2 z+1W&jq$n# MRy)5Fl6wg4WgCDrFc{VmA34w 7{H_Hs zhC8mEJPPj#gDw}Ws(A}TJ{W#}c$5$8Wntfm3T&YTR1J9h0pH%Y1#$!9pOTlWQ{_qU z{<@MSPd3{Dl2mFAPDa3;fA;*%{WD#|_? -kc>Owob>d)S&p`| z>$! HOd$`jqeGLKJA7gX
x>e1^fjjSJFOLs6~r;Q3aAj9`# z)!0z9o?0UNv{Gy9%jr!b8G1emmjm=h7r}Mk>sj-W@oto_Aqfry_c)-W!*Q*3x9zq@MwO4NQc8g;E@X%$is04p3!mb4C+8Tj5rRJm=lqTN zoBnvE#A7Y8HA+GlE8tYzuF;2^2E9Azcbkf(4=&>NpOc5+2B%+NeJv>3SE|GRe-bYG z|5Q({F+WgKHATn(>*7z4w&xB`joLLn135xhmlE?Jn_iM08SFVUU{(kvecU){4wMfZ zTG>>UGYytFLJXC)>WtwFpMh8aSLJXc!nbUtHn%J`v@3W?4Eh=euO!vUcwwb1O!k(F z8|Kqa_S1lRqk3lbQ)%tJ(*7F;hG Q|~!86_TO5)>BLQl_Ho%~Ixz$;NR zwQCJC^v6q5r>Gd_{t(i8?M1^t#*}Yr`Y;(TWTAnHqvN1RFVSnjy`O>$2WTqj16OiA zxC;8XFs(79rm}8wnMeM@$WBCfIL@l;w!XkX&*}Z;*MV5QPZ~4~sPGuGHH46quhWsb zwgSUrW5$-2mdrT_!QNQ#IYHD>R#qt5Np34s@9||`M4Kk7UsW>=A%zw{xU)M ~`p-3s zN$=khQo5|eW{VU#id|zlxy&+%4xN^KRH-nocauTLSP^kG1*$1zk>Nl{Z=Q(rtsKsR zQHtqXsz;QeGYoRMvfOp=Nt4wvvF>X7(UE#{BYTD-8)^NQdWQ?WTE2GXR&~yp9waM> z8 3)i!8O@SK4a^0R5X6RQzfBkqr?KlYCm)2J;aUys;o_&$kcDi_je?MQA zMmk&1Z!_4;ow7pycWe{ey Po4o zq9WxtPi-yL9~w^9=d)&yJb&x=RHNdPBiTXPuLHr&I)1;17bKk;w34s0hhLtWYE)vC z8j6m6>iCcp!74^q=K2(N0G&ilcbyd9`|)Fe$?d>u*y~jP 2S8CjeO*l=sCK|p((he`z{j93e+OEydmU2f68z$l5;W$vkYg|XVXsKbSG;>D8 zUpih?(Exz}I@CSSY#T8`PHwQWf(5KrDjRTww{j2$(3b)}B L}F{i1nRx97+Tg zO5gVO>PT(A7Slt!0lmE{`ls;<@2);#ih1zIyNs2O^zIFk)3T>t8Xit;b`nCM(5Uve z8BK`iV@ml(64BUZtT 7M+@%_!tQ9Ic0$+XvF&UOpWZj@bMdzEzzgovuWh{8&MGj!dk(O0j1CGSBY5% z5;gKdE!((xGg*yAXw6<_ DmCq!$Vgt}Ux95R+b6R9eglP16l0!C#5Wnyc0aOce|Ei*-Dj!afbD2C> `e`fr& Q4w@*{kgWOYdwS&!+y%liLnCoorj zb;V%e#cNBRteNX1S2ViHKlPkRCyvLvcW_LgD#8?;VO@J;lQ&xWy#x`_AMKC~opfrU zl1DEy)TPr&F`hA^7v#lm-fUJoFZz|)jv`M-WoQ{o7J4=O=&f#1fHt*oBI)9~*&TI* zra-YZW8OD6<3h&6B>K!@k!K$bG&FIik-Qs@b d*_GAS*fT;&wo`{?jtkzJimpcrSq~keam4*II8y1a`V}z>*pBU zG^t#P?q3f;vv%vs=25F#J|}$XS7D5;y} bcw8qium|)s4BTS^@(f=NguW@Ol#5CAz@56c8r9S_XLhU7|um2W)BB7YR>@q zNI91u5F15HW_@8BrK)1;D-IO;^j~>Q1*jeD?eAvmK7) ~<|Ju P+Ki}-8T(%Ed!-_{;>KA_`?4)yJP1l1T<C$vN&I3ep+CL0jeL7nJG3-@$;ZaeX{Zi#bt9TXU D7`i^J&GEFElLoa%69~R<>$kodv&}qVhyWc7;AzySRRWc zWQQBfN5@UTHuo%ky&Cz5#&}eyT=#ZTcVxrUhx%>kEzzhB##+O=LDfm>Xra5=r}=gQ zTG5!@g(uxq(=&fWOD#f+P1?tGL~>^6H3agt3YlX+6A&cG-Fn@0&35+b)2T(#Eequb zoSh*~{x20IQ2)o&bq7-2xBZ4PlkF(7%HD*`vS(Qd*|N&sTgu*L%NCN*vA3*bo;YP> zWhHy>@Ls3;dG6=^>+Tju=ls6ox<2d5H@8>6DvwAei{v9dk9;m?^6n;<%WFyL6rFB{ zf-SAy%g^?FQ)|D=(7yjF(i&wUBO+#hJb1Bbp4T|8AT7wUv`2AXF5-eOWY oN8&-DeydtIBy7{UR&9|Cgln)FmTECrj?LkF!gS+zLf1y0@Sp`!PzzWA zC?5%I53>t;UK5;q{)_wWQ4zKZN1~0~WgN> I$%LH|_pN8*%Hw)!Xy854JKaobnOrR?&XiX%d>f3ywD^-mH48EcWHZ zu}rzYta A zq3&!Eh`P0}{8uf3(faf;RqEL)euJSex(1IS6AN}xFmZ%P&paP{F}c$GpvL6jhIgSb za8*7iOTjm!e+MJMkp$<|$L4C(6TZU^b9yMqkze)9bhRlcD!xB`g<9Z8P8U5Y6&4pi zaXFmz(YVW|1BDn=wgXQqtpwh{k5@c;1Se0c @jjA+oivLe zq)kXa+^q9eEDJ$-k{3}(?AYX_1-w}n$1iEBmT(>-%bl;Hc{~nhE)`f>!c2v1*FcTS zdh#hO7-boRM1ZtblPr&J+-i^~6hKp;AwSk#oAM4eQ2 0q{tHm!ObC&noajwVoaUUN{N#?(L zoNFrhPLKV=*#4s9ivj~S?R2ja)hWrAPfr-nV#7y2Y25KTC8k*?x)>*Cm+(UOJ~its z?Zt=&^-tQD&iSlP%V-AQ3n5* bT3Cwpg!ZV6I-=tl3ZfhGr!4o_A%Rm9S}=m@;S~i;_xzyY#apvD iu`wM5 z)s5rKLiTW};pWlqvW-Td?ZSWvkj*3q_x?m`vNduRWztwb)+!w?cUXj*2xOWrB9Lxo zxqpSkZ8tjiZVFMaiCfO^hUdd)c3B^Ebf|hiWlG0lKfGgPM1^0hx0Rmq(&aaO^pIoI zIM3`3#e+PSbM6G&TstB^J;U{hRW3%-Q8 l?I{Co0uM$Z4G *YgYTjX2 zsMBExDZh8UH{`JE$mp<=V$pApMk+k}Q7i}>E49z!*SGVIhzdAx{Pw{w0a>9LqNuo7 z(S r=R^V`^AQmi)$6ih^G`z0P~m=3Or1uHe)8L)3kdo?`GQ7gCj_c z70cEy0aeMn@6Vw8q><0lfB v1T z)oUYi3+IBMkRVH<(#q;uS#>ir#v&ncC#hEVE&T|b4nS ePV(M}!Gu48tk$mj zxZg#B?MT0!5CBW4`{7-{buuzJnLld7hsgk?>oH>UT5%Bp9sSm>kP+B4K+$2&m%*b1 z)CRACOTJQD8e1N~g& 1<+uyx#rl z@Gj=egAkiTpU*ZTW`IF8pRGaympFB2m;t-<9sS{`AeKB5B`W<}RwvZUa?hfyQ%7$` zN}en}fXWIcz(dff3KJ=4WvWgMxN}idd7pT0w^0A0`Mq{+MY2|H3U8$ ^TD4hZfhIDa>H?n4`;otv?v^S@B*PBPjpD6GP5!9Qo6$?|KoP*my4D-a=Pj@ zw6Bb?84Jvi`Gx9oX;<5Z$d-tLem=-<%BwwFz(K2H6@*WpyKlty?rHbcmJI ;I$j>RM<=FcZLmGrr*0>BUP%6lUc*dW!3cn6ocfJpt#xC)inY8>J6eG z;(YVhD9U%*?1 torx0zwfaD zj6HSMltv(Tx#9=s(_Js>LCZ5p;K2YyQVa8|VgQsH3T5=A*F~Hx_#dH!6%W>cE2E=W zY>#F<4;^={{;!s{5J0OCK}ys@(*QHf*&Iiy6$A2-+r|i*MW~=nEDVMdHYUL|s$fx0 z#ckN|X zsh6X`vhX@Ln(UFhU;u@1ctqq z0E>$nK=b{%`f_4|nJ_hsIdwE}w|3WC=15C3*Znj7!2bzfMl5O!V>hU(N=E(qvs>yv zzwz@qj79^5*RZr4;=AK$GeM96Xhn*sGa^{nIplbM-TAD=H#O>Vkd#T+u7+VSu9Oc= zuLZwhvtN4c9ymh4yaN&*xbHx+xL!l5e~oR-m?CP#J%fbExRBbls3UXfjQrxoF%Dz4 zH;>Ajo#z@_aV@b#&SgCr;z^Jlc!$(Ic#$y9)RraPDgN_|XBg$3-WT+h`Vr|f!|}zJ z?+awxRXQ> 8oFAf?)t` >NxN@|~`zkN_{`vwX= z$Os&m?ZmK92N9htbVe*{5HLm_VMViVZa8Q;?{ue?2hsqycNR6NhqJ#w-3*cZa|7k- zd2)0cl<}b5E5n2{@17kt|4ws-UGwd_YejJ5gaLpjwP^{$vsBF~zvr0ZhNgWaEbZ;N zy)8H?z!CjvZH=qcsM&%C@njj_9YJ2Nk}8N~f5P=DF(s_$ZS-TL72$ns3&!LSwW)v* z{~?!t<0UKY=D`mVL&+?1*x`_A$#TE540`$aPnrZMvLHS!e6@Ekf5fEIB4>Q&lz(FL zh(08 $dt_L^KvAh z?EGV+XH`w#T#Q(}yzy2$J1H-BZ5GvVxRBv9ZM%LXP+dH?Wf{&wLO(w-r~hT4j?25g zK3=I~g#NJQB9sD{=w4&{OqOt{PZx;KO%535&-&jZYDYayCxm&E0J+0ALGnG^pGPME z6DI2wf8OgCjIxOHk-&t6+Cxw-4+TR;jF4ZN+Yg1oArZexW!a3odj|B2J!7j6hlYoF zRpZccn7RcltH5WhcoLafc0+>z=p?X@Ej$Du>aeaJgVR|I#fbAF#!LU--r$7;)nBhc z|EvDc`?A;dI)@;9z{1X;)UatTqSI&t9zJA<6`<)eQD`f{(TNFji8r99YAr%*zOTFn zrO_2Ab^d9ngG^ryIJ84_>NwX+I-MV9GY=|u?pz=f3IF=4f3omt@j%*wc%&3;j#8t> zwBXvzccqMU~PJ6c^3_nX<{Egr$a!rvCQaZXT>j<& {#W9pyGM7MfbdZRCC z@vS`iBo)vSjgw*EyZU;lnN&WxxVg!}3`BtJJZ~hLpD?WszgE_o@uFDVmf)}*ZsVET z*ofr`KiG=U`AyrbFG2}X3gAaIECqT7lwUbK-4fj4td&E7>VX>Jrbrp27N(8?Y@&PK zX?$vaTkMe6?V>Y;pduAuw0R~9P#Vn0_t{Ob2bQ}SbefA@>!k0xeub^;2-8nxHxQ5p zVzm!~jAIc=(1=FwGYnAE0#{Z5%advWUNsN04UKnk^dECY^fa#~$JGJY4^GNnrxV z-1n9BG~s&T=|~=8&0kRrmvUA3s{^uCKHtmCa98OR{rPRI4Y#1Tm3}JuEX6xWl}($} z$a^lnPk?$xhCj`!J51CJEEjcuVk)*p<9kJ#_Re?Q{7w2a^DcJp$^8|JLINf2gr{~v zy27URr~KPitwQr>?ct39g(~^eflcnM0@Ua6RO4|7lhVEH%}$=aUP!V!tX CCW+3<}&JUpRR%)DNJ#wcNe z=?{Hwptdn{g`sYNe21#vvq`$Cr>pf`cDGXJ86LE`j2WjI$Kr4}KjDg-)}KD)p|WD) zXDOCOB3k%A9(~-z(WZT|eeE)K_^2?k07*d#)!D01)(2!Acv{t;xR{S0UdzowDys>Q z#YwmrvFG2yQBgIyG_>|C+_jIuYAOVK>X``2_&~42I=LPD?utYTyEPs;%flAV_UyJk zF`b82{)LBI%~l*&?3mp$b}Yg_p;Oc}7Dw*!Z_z&r(YUAiUb-pEktT??J%njn(H^I{ ztT6nz_F&y*Wo2W7sHd!^+BwFnu?0X=kU4&8p@a}tW_;5Yo2>Qj9e@qMjR0_zgh;+V zTRF1!9tJ&rI$gyXn!j%A3>!<#l { $v9VP?Z)Q!v zk$<+@d?Lg)kC$i4dp&K1sWslOR2~u7;CGVR{AZpUqof+Op}^P432l4n@zGV7`t!vA zLyjkK>Vc)Onlj{dR8A-~8ZnZ8zO $kht2|6u(FLJ^k{sSRWB&v0hGO%56 WmCnsUYMc<;+sVxo}#en8?!$RvWBSVSQ3}9tuRqD`r@1 zQ>B*^KJfeVsvNBehjE!XQM1D}!r(t+MrTp?3wj)c-=Y_J((fnz7(evs2crvQ@fxvE zgkx0l_X}Pwc#a|uo4~lT0D2XcypOh`&dCC*q*L3;eU^}I3eUQHXMb;B?d*gbIxF*; zUfE95zrosK-lqg)E34~aI ~e sG>+t!hxl2*UGG2<851Iz8@X9+0@TDy&%a(dsGdHHD&+8*#6>$O zWEn^ecSh)k%U#mcJ@{cp5t$O`J8l&$$Bp%}u@Q9hB*SMXC*r5`{xBK^s4`NQx8Ts* z9QHq9kL!XMP*TaU-h~FQLp%Y{w&Df(r1GSf^NU#003w{RaLYHo1uAB}XK=LNLu~9f zpQQUWqXwplO%P6AUfqCc^JRP-eA 3TAmJ?+D^07G zEgy|+Y!p{P_cAeF$TWGp<4Mxrvdqpz>|&4{6?aLIWlf;nwSjNPki`Wnib88S^@vbw zn2wV5GKze{GW_&ea?Cw)lR FEv zA=x-rT9ewGa;k#0;ORM?YqqQw1>p;& mk^; tW3L6OnYxi6SNpg|bh)*?s06-9%Z5%mN)YlZ91byntDrH>eG {zlYZf`SLoQZsvZZv4#uMP1-D z-}+mM3^}FnNV9hM2rldvJ<_?uo-w^zlj3pHS63XwQm)=0ywn7T(8D6(5@k?A2!O9g zCG<7b5NOL`$qYc-W%QVlKpKSW811B2YS8lJ)D-M~{u@y#eHO@w@F8kiT1zfk7{M;* zSchID>xq<2ESwzyVrC)JzaNXUe3W3=m5ML~h;D%R0og<3w|EHjQ`7lQb@C8^^9q$A z(Z)mIGlkNR1_&0~mS0M7yOpjVo-lcUL`T3;T&aUU*aU*WH+^V(_{!DF^R1D{rWUx- zTW79yhhjgB=-@!Xj}2JKAc9kh#&VV;8a_b&O!ZA$XNu# y{ zqWZ_Pue;7Vya^INIyu$2k;7xkcVinAq8m%Q&*%t)Qp4|QEEo^F!k`UE ?%0O=M)|mod7)@yvVcJcH z1A-Pz2MP9)Cw!i5C(1`y)6TQ+1#gnG)|d1Q3=9m)B=k_TN@_pedAwC(hQRg>_M?=h zO78A$eb&jfX3en4eI@b*m-lw&?<4u0Mzl?Bp(*w)qxzbbH1)5_D#MX73RGwO31I=R z!Z^Gq)qfonRo)Lw!qZG)A$vub62$1`*O2jQBqK<~_*(RDj}Vr@25CeGVQO&QgxzYy zZbF@Cza>9d{y~)LkpZI^`lv%Sp~3S9`3dd?$u|{0J#SziAG^G^{D1+$Q9U_0E}|10 zE(dFCH=rq1XZW_`3v6=t1Id$iINiZOcX%Ur8b`=8;~69o7d`x-^^)3#HMt-f5aN zz(R`OpI=^WnCTu>ZQ#FnGj4WvHm~Y;?j6^uNsE>Kj~>@p=v>I`C_i&0&*GbQJ9ru} zbQnY2k@bsp0Sk4_Lg~rKOCfF2FK6Lwpf{Fo8H&75Hcg#>QI@Fv_^Nm^HQS3qhC$+0 z?9{#de6{Caf(CGRLc*<$rXRHI)8P>OxO#ic$Xbb r(7`ymEdb^5J7`rEJke_t7Sx@nB-o8Pma@ahP80 z$j&{WLc-8o4vMaYZGbJD&4WM^7yObQdH~w_gMSmAAmQjQ5-Qp;^E01Mi^6Sf=J^{F z1TyrAfa#hrvi=ETriQC&;Z}A^e~^F)5g3a$NcKOwFzuPXD_?X8Uq3u($_)CWtz=IB zseHq|E6e^Tc1kRB-{YlUdGQQ5FM214Lnl~sLjkw8O{!DOpkP(q~-q=S^FJp7{4~ z7E!@r*CdOc$BDApIv6mKyU8PR;Sl&wr#uo-=R_s=R0i)X^IRjet{ejz0h2fbmXVRM zi5`6;coTLP3(svbHN-0(f2^3&A!Fw%iQD6}wpwKgPdti NYq1J}Tg5z2}?x>FT5dJ1N$5+M90_zE26v zZ@%bsCUYERp&LZv@O_oIFVGRAPw^xX(fvv7a-kDFg;@SlkF({MgU6MBbn`O%W%uza z-;RcEZ&@HrFJ4Ud)7P0&2uyKD+P$PR7JOrF8eW0Yxk)bkl!o{+P$^B*u`9xaCS(?* z(;5MOWU?yEqz!K+m5$TD-Q%4ws Dbajb9cSAb(QslegnSyh4QA> z^v=%QiFQXmf#mch;T}JZ8G;wW>w%6pu8@w??-(IGEK~bYH!o;(*qkdre_;3e5#@OF z%!YmVBzu&kMva++CCTKm^Ctn-iXotX2?+^3j*%Zr n{5ICj^_@>E^l^ zSLK+kb}IYawkPB+ocOI^rAM<~3%=Li4HP}b)oEC2NlFKCQB(jjP5+Tte(`aNM{9g| zVH=w9V!ipvMsMmB+9&%2)t(G6q2)i_yu ZJh>8JL1HY6UsuBLK?*+6mQF zn|zGNp94oeogY8Dc5nd-Q@Ke1L487z)wTCIt6owL9~QS`E&6Gg%;o6#6{g=Gs%Hr{ zT2a=ZI1cP+vPN>ep!=fV#MPsDWXeUJ{x&;gx=&(`A$peMj)mb0_N8g(In8QWk^9MS zHYAQT7Jl9$X4KWON$^l&GZo5Jkgpj?do^WF5sh2%;4PzCSTYGevCikQf)}4<2h8hA zcSNPB>|LXkhBY7QsIXSjoqM>@OkmEnx?jduWBc}yu#JawnR|~aV)?CItTpY&1E1I7 zH$vJ>65Wm>6GX0|;?%||kC8i6?6-WGoG@afJvl#ggJ(vyK<&?A95p-sbu>jIYHwXC z?r9WdwWe|pT6Q64UTBz9f %hz>AiaH=fGF=<^=YtyEI$ zyWbb0OF`}7;<&-avo6M^nKG$yRfmgo2)?Z_bSh`+ z9L@Y~h4`zKP0HJLrhFUs4^qy$#M1cinLP%m;|?SqaVt~>#&mXAW>xRX+CO%i*rBW) zxtiZRwcG15VaYbli1e`Twr%NSBz~B>w55G8s2Vv!wKel__wybb1(zZDrcR*@ho_>5 z{fGM4{;{nCiB3}%o)W~ZVJ}Z_^Uv8GNr^p&KF&4mw~k(}OR+)&1i)R676-Bk*|Il8 z(U(9@myu Y<4ZTZBpb>Bw<5*?gDR|HNgjAO9nBG^>KapS}h1YlJ%7r54Rd!nd@ zC5J$he)a_snnx9cGJpv9oU97@Y|c}{qWK)hTk38EFY!^JtG~+j2LO=3jucm%!}uIP z+4SdNmQ;UOwbk3Gw`Ysi|6rE(U^eqqMk3P^xb^SfA42U980V%3$4T|S2iL}%zoFH3 zVvBdD>h-y~`c(|co^#mw^ZN94P&!9&&r;OZtaXSPRH2IYkTreg%Z5VfJzHMi_mmfN zxhA+=lYr>YnOX-sx>coKO&GgH0V(SB18i?*G%g^ly_QI`UPxyUN$09HH{_7V?PJ7g zRh{uYO5EA*$Xs5VEwe2lNz$)V$;1zv{El+$@=BsX&opu2F;9K(Y*_Y~vNX7NFn)N; zc3`afJH8~6fcF`Dd~`?f@WXD{UoJJ89IEHSlR71gm8gKroAnfWe8)Ab;kS;