diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da895e9778a..63776b4db7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: key: ${{ runner.os }}-linting restore-keys: ${{ runner.os }}-linting - - run: pip install bandit black codespell flake8 mypy pyupgrade safety pylint==2.11.1 + - run: pip install bandit black codespell flake8 mypy pyupgrade safety pylint==2.12.2 - run: pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil - run: bandit -x ./tests -r . || true - run: black --check . @@ -249,7 +249,7 @@ jobs: - name: Run linter (Bash) shell: bash -l {0} run: | - pip install pylint==2.11.1 + pip install pylint==2.12.2 pylint discordbot gamestonk_terminal terminal.py tests - name: Run MyPy (Bash) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 0ded8a28204..8aaa300b5a7 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -18,7 +18,7 @@ PAPERMILL_NOTEBOOK_REPORT_PORT = "8888" # 1 - DEBUG for terminal, INFO for libraries # 2 - DEBUG for terminal, DEBUG for libraries -LOGGING_VERBOSITY = 2 +LOGGING_VERBOSITY = 0 if tmp_verbosity := os.getenv("GT_LOGGING_VERBOSITY"): print(f"Setting verbosity to {tmp_verbosity}") diff --git a/gamestonk_terminal/decorators.py b/gamestonk_terminal/decorators.py index 833dcd4f3db..51e51a1331d 100644 --- a/gamestonk_terminal/decorators.py +++ b/gamestonk_terminal/decorators.py @@ -28,3 +28,62 @@ def try_except(f): return [] return inner + + +def log_start_end(func=None, log=None): + """Wrap function to add a log entry at execution start and end. + + Parameters + ---------- + func : optional + Function, by default None + log : optional + Logger, by default None + + Returns + ------- + Wrapped function + """ + assert callable(func) or func is None # nosec + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + + args_passed_in_function = [repr(a) for a in args] + # view files have parameters that are usually small given they are input by the user + if log.name[-5:] == "_view": + kwargs_passed_in_function = [f"{k}={v!r}" for k, v in kwargs.items()] + # other files can have as parameters big variables, therefore adds logic to only add small ones + else: + kwargs_passed_in_function = list() + for k, v in kwargs.items(): + if type(v) in (int, float): + kwargs_passed_in_function.append(f"{k}={v!r}") + elif k == "export": + kwargs_passed_in_function.append(f"{k}={v!r}") + else: + kwargs_passed_in_function.append(f"{k}={type(v)!r}") + + if args_passed_in_function or kwargs_passed_in_function: + formatted_arguments = ", ".join( + args_passed_in_function + kwargs_passed_in_function + ) + log.info( + f"START params: {formatted_arguments}", + extra={"func_name_override": func.__name__}, + ) + else: + log.info("START", extra={"func_name_override": func.__name__}) + + try: + value = func(*args, **kwargs) + log.info("END", extra={"func_name_override": func.__name__}) + return value + except Exception: + log.exception("Exception", extra={"func_name_override": func.__name__}) + return None + + return wrapper + + return decorator(func) if callable(func) else decorator diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index afd1a723c2c..b5cfdb59046 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -1,5 +1,6 @@ """Helper functions""" __docformat__ = "numpy" +# pylint: disable=too-many-lines import argparse import logging from typing import List @@ -426,7 +427,7 @@ def divide_chunks(data, n): """Split into chunks""" # looping till length of data for i in range(0, len(data), n): - yield data[i : i + n] + yield data[i : i + n] # noqa: E203 def get_next_stock_market_days(last_stock_day, n_next_days) -> list: @@ -857,7 +858,7 @@ def get_last_time_market_was_open(dt): def export_data( export_type: str, dir_path: str, func_name: str, df: pd.DataFrame = pd.DataFrame() -): +) -> None: """Export data to a file. Parameters @@ -930,23 +931,13 @@ def get_rf() -> float: class LineAnnotateDrawer: + """Line drawing class.""" + def __init__(self, ax: matplotlib.axes = None): self.ax = ax def draw_lines_and_annotate(self): - - # ymin, _ = self.ax.get_ylim() - # xmin, _ = self.ax.get_xlim() - # self.ax.plot( - # [xmin, xmin], - # [ymin, ymin], - # lw=0, - # color="white", - # label="X - leave interactive mode\nClick twice for annotation", - # ) - # self.ax.legend(handlelength=0, handletextpad=0, fancybox=True, loc=2) - # self.ax.figure.canvas.draw() - + """Draw lines.""" print("Click twice for annotation.\nClose window to keep using terminal.\n") while True: diff --git a/gamestonk_terminal/loggers.py b/gamestonk_terminal/loggers.py index b381392fad7..f48991ce4c4 100644 --- a/gamestonk_terminal/loggers.py +++ b/gamestonk_terminal/loggers.py @@ -38,7 +38,6 @@ def library_loggers(verbosity: int = 0) -> None: def setup_file_logger() -> None: """Setup File Logger""" - file_path = Path(__file__) logger.debug("Parent dir: %s", file_path.parent.parent.absolute()) log_dir = file_path.parent.parent.absolute().joinpath("logs") @@ -114,6 +113,9 @@ class CustomFormatterWithExceptions(logging.Formatter): str Formatted log message """ + if hasattr(record, "func_name_override"): + record.funcName = record.func_name_override # type: ignore + record.lineno = 0 s = super().format(record) if record.levelname: prefix = record.levelname[0] @@ -176,7 +178,7 @@ def setup_logging() -> None: else: logger.debug("Unknown loghandler") - library_loggers(verbosity) + library_loggers(int(cfg.LOGGING_VERBOSITY)) logger.info("Logging configuration finished") logger.info("Logging set to %s", cfg.LOGGING_HANDLERS) diff --git a/gamestonk_terminal/stocks/discovery/yahoofinance_view.py b/gamestonk_terminal/stocks/discovery/yahoofinance_view.py index ad7442a4fd5..6058ce027c9 100644 --- a/gamestonk_terminal/stocks/discovery/yahoofinance_view.py +++ b/gamestonk_terminal/stocks/discovery/yahoofinance_view.py @@ -2,12 +2,17 @@ __docformat__ = "numpy" import os +import logging from tabulate import tabulate +from gamestonk_terminal.decorators import log_start_end from gamestonk_terminal.helper_funcs import export_data from gamestonk_terminal.stocks.discovery import yahoofinance_model +logger = logging.getLogger(__name__) -def display_gainers(num_stocks: int, export: str): + +@log_start_end(log=logger) +def display_gainers(num_stocks: int, export: str) -> None: """Display gainers. [Source: Yahoo Finance] Parameters @@ -17,6 +22,7 @@ def display_gainers(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df_gainers = yahoofinance_model.get_gainers() df_gainers.dropna(how="all", axis=1, inplace=True) df_gainers = df_gainers.replace(float("NaN"), "") @@ -43,7 +49,8 @@ def display_gainers(num_stocks: int, export: str): ) -def display_losers(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_losers(num_stocks: int, export: str) -> None: """Display losers. [Source: Yahoo Finance] Parameters @@ -53,6 +60,7 @@ def display_losers(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df_losers = yahoofinance_model.get_losers() df_losers.dropna(how="all", axis=1, inplace=True) df_losers = df_losers.replace(float("NaN"), "") @@ -79,7 +87,8 @@ def display_losers(num_stocks: int, export: str): ) -def display_ugs(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_ugs(num_stocks: int, export: str) -> None: """Display most undervalued growth stock. [Source: Yahoo Finance] Parameters @@ -89,6 +98,7 @@ def display_ugs(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df = yahoofinance_model.get_ugs() df.dropna(how="all", axis=1, inplace=True) df = df.replace(float("NaN"), "") @@ -115,7 +125,8 @@ def display_ugs(num_stocks: int, export: str): ) -def display_gtech(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_gtech(num_stocks: int, export: str) -> None: """Display growth technology stocks. [Source: Yahoo Finance] Parameters @@ -125,6 +136,7 @@ def display_gtech(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df = yahoofinance_model.get_gtech() df.dropna(how="all", axis=1, inplace=True) df = df.replace(float("NaN"), "") @@ -151,7 +163,8 @@ def display_gtech(num_stocks: int, export: str): ) -def display_active(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_active(num_stocks: int, export: str) -> None: """Display most active stocks. [Source: Yahoo Finance] Parameters @@ -161,6 +174,7 @@ def display_active(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df = yahoofinance_model.get_active() df.dropna(how="all", axis=1, inplace=True) df = df.replace(float("NaN"), "") @@ -187,7 +201,8 @@ def display_active(num_stocks: int, export: str): ) -def display_ulc(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_ulc(num_stocks: int, export: str) -> None: """Display potentially undervalued large cap stocks. [Source: Yahoo Finance] Parameters @@ -197,6 +212,7 @@ def display_ulc(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df = yahoofinance_model.get_ulc() df.dropna(how="all", axis=1, inplace=True) df = df.replace(float("NaN"), "") @@ -223,7 +239,8 @@ def display_ulc(num_stocks: int, export: str): ) -def display_asc(num_stocks: int, export: str): +@log_start_end(log=logger) +def display_asc(num_stocks: int, export: str) -> None: """Display small cap stocks with earnings growth rates better than 25%. [Source: Yahoo Finance] Parameters @@ -233,6 +250,7 @@ def display_asc(num_stocks: int, export: str): export : str Export dataframe data to csv,json,xlsx file """ + df = yahoofinance_model.get_asc() df.dropna(how="all", axis=1, inplace=True) df = df.replace(float("NaN"), "") diff --git a/tests/gamestonk_terminal/cryptocurrency/due_diligence/test_glassnode_view.py b/tests/gamestonk_terminal/cryptocurrency/due_diligence/test_glassnode_view.py index 5c538393484..c9c5176daf4 100644 --- a/tests/gamestonk_terminal/cryptocurrency/due_diligence/test_glassnode_view.py +++ b/tests/gamestonk_terminal/cryptocurrency/due_diligence/test_glassnode_view.py @@ -13,6 +13,7 @@ def vcr_config(): } +@pytest.mark.skip("Hangs on Windows") @pytest.mark.vcr @pytest.mark.record_stdout def test_display_btc_rainbow(): diff --git a/tests/gamestonk_terminal/stocks/screener/test_finviz_view.py b/tests/gamestonk_terminal/stocks/screener/test_finviz_view.py index 7a1bbf3d24b..8d865923ae2 100644 --- a/tests/gamestonk_terminal/stocks/screener/test_finviz_view.py +++ b/tests/gamestonk_terminal/stocks/screener/test_finviz_view.py @@ -69,7 +69,7 @@ def test_screener_no_data(data, mocker): export="", ) - assert result == [] + assert result == [] # pylint: disable=use-implicit-booleaness-not-comparison @pytest.mark.vcr diff --git a/tests/gamestonk_terminal/stocks/technical_analysis/cassettes/test_ta_controller/test_call_load.yaml b/tests/gamestonk_terminal/stocks/technical_analysis/cassettes/test_ta_controller/test_call_load.yaml new file mode 100644 index 00000000000..2003c0bac06 --- /dev/null +++ b/tests/gamestonk_terminal/stocks/technical_analysis/cassettes/test_ta_controller/test_call_load.yaml @@ -0,0 +1,61 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, + like Gecko) Chrome/39.0.2171.95 Safari/537.36 + method: GET + uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?period1=1639695600&period2=1639782000&interval=1d&includePrePost=False&events=div%2Csplits + response: + body: + string: '{"chart":{"result":[{"meta":{"currency":"USD","symbol":"TSLA","exchangeName":"NMS","instrumentType":"EQUITY","firstTradeDate":1277818200,"regularMarketTime":1641330002,"gmtoffset":-18000,"timezone":"EST","exchangeTimezoneName":"America/New_York","regularMarketPrice":1149.59,"chartPreviousClose":926.92,"priceHint":2,"currentTradingPeriod":{"pre":{"timezone":"EST","start":1641373200,"end":1641393000,"gmtoffset":-18000},"regular":{"timezone":"EST","start":1641393000,"end":1641416400,"gmtoffset":-18000},"post":{"timezone":"EST","start":1641416400,"end":1641430800,"gmtoffset":-18000}},"dataGranularity":"1d","range":"","validRanges":["1d","5d","1mo","3mo","6mo","1y","2y","5y","10y","ytd","max"]},"timestamp":[1639665000,1639751400],"indicators":{"quote":[{"volume":[27590500,33479100],"open":[994.5,914.77001953125],"close":[926.9199829101562,932.5700073242188],"high":[994.97998046875,960.6599731445312],"low":[921.8499755859375,909.0399780273438]}],"adjclose":[{"adjclose":[926.9199829101562,932.5700073242188]}]}}],"error":null}}' + headers: + Age: + - '2' + Connection: + - keep-alive + Expect-CT: + - max-age=31536000, report-uri="http://csp.yahoo.com/beacon/csp?src=yahoocom-expect-ct-report-only" + Referrer-Policy: + - no-referrer-when-downgrade + Strict-Transport-Security: + - max-age=15552000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1; mode=block + cache-control: + - public, max-age=10, stale-while-revalidate=20 + content-length: + - '1035' + content-type: + - application/json;charset=utf-8 + date: + - Wed, 05 Jan 2022 11:45:24 GMT + server: + - ATS + vary: + - Origin,Accept-Encoding + x-envoy-decorator-operation: + - finance-chart-api--mtls-production-ir2.finance-k8s.svc.yahoo.local:4080/* + x-envoy-upstream-service-time: + - '3' + x-request-id: + - ccc14cc5-af7c-40ac-ac28-44d081fd5beb + x-yahoo-request-id: + - 0gsv07dgtb16k + y-rid: + - 0gsv07dgtb16k + status: + code: 200 + message: OK +version: 1 diff --git a/tests/gamestonk_terminal/stocks/technical_analysis/txt/test_tradingview_view/test_print_recommendation.txt b/tests/gamestonk_terminal/stocks/technical_analysis/txt/test_tradingview_view/test_print_recommendation.txt new file mode 100644 index 00000000000..66f77c84cc9 --- /dev/null +++ b/tests/gamestonk_terminal/stocks/technical_analysis/txt/test_tradingview_view/test_print_recommendation.txt @@ -0,0 +1,20 @@ +╒════════════╤══════════════════╤═══════╤════════╤═══════════╕ +│ INTERVAL │ RECOMMENDATION │ BUY │ SELL │ NEUTRAL │ +╞════════════╪══════════════════╪═══════╪════════╪═══════════╡ +│ 1 month │ BUY │ 14 │ 5 │ 7 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 1 week │ BUY │ 14 │ 4 │ 8 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 1 day │ NEUTRAL │ 9 │ 7 │ 10 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 4 hours │ SELL │ 6 │ 11 │ 9 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 1 hour │ SELL │ 2 │ 14 │ 10 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 15 min │ STRONG_SELL │ 0 │ 16 │ 10 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 5 min │ STRONG_SELL │ 0 │ 16 │ 10 │ +├────────────┼──────────────────┼───────┼────────┼───────────┤ +│ 1 min │ STRONG_SELL │ 1 │ 16 │ 9 │ +╘════════════╧══════════════════╧═══════╧════════╧═══════════╛ + diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 99a0a108b4c..1b424722863 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,6 +1,17 @@ import functools import sys import io +from typing import Union + + +def calc_change(current: Union[float, int], previous: Union[float, int]): + """Calculates change between two different values""" + if current == previous: + return 0 + try: + return ((current - previous) / previous) * 100.0 + except ZeroDivisionError: + return float("inf") def check_print(assert_in: str = "", length: int = -1):