From 3ba90b695ef4a239a8d4cf56f72b72292b5470ac Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 13:14:16 +0000 Subject: [PATCH 001/230] first version for login prompt --- openbb_terminal/account/login_controller.py | 45 +++++++++ openbb_terminal/account/login_model.py | 104 ++++++++++++++++++++ openbb_terminal/rich_config.py | 9 ++ terminal.py | 6 ++ 4 files changed, 164 insertions(+) create mode 100644 openbb_terminal/account/login_controller.py create mode 100644 openbb_terminal/account/login_model.py diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py new file mode 100644 index 00000000000..3ff45120b34 --- /dev/null +++ b/openbb_terminal/account/login_controller.py @@ -0,0 +1,45 @@ +from openbb_terminal.rich_config import console +from openbb_terminal.account.login_model import ( + get_saved_token_if_valid, + request_token, + launch_terminal, +) + + +def display_welcome_message(): + console.print( + "You need to be connected to use the installer version of OpenBB." + "\nTo register please go on: https://my.openbb.co/register" + ) + + +def login_prompt(): + display_welcome_message() + while True: + console.print("\nPlease enter your credentials:") + email = console.input("> Email: ", style="yellow") + password = console.getpass("> Password: ", style="yellow") + save_str = console.input("> Keep me logged in? [y/n]: ").lower() + + if save_str == "y": + save = True + elif save_str == "n": + save = False + + login_info = request_token(email, password, save) + if login_info: + break + + return launch_terminal(login_info) + + +def main(): + """Main function""" + login_info = get_saved_token_if_valid() + if login_info: + return launch_terminal(login_info) + return login_prompt() + + +if __name__ == "__main__": + main() diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py new file mode 100644 index 00000000000..445c06fa953 --- /dev/null +++ b/openbb_terminal/account/login_model.py @@ -0,0 +1,104 @@ +from dataclasses import dataclass +import os.path +import json +import requests +from openbb_terminal import terminal_controller +from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY +from openbb_terminal.rich_config import console +from openbb_terminal import feature_flags +import jwt + +BASE_URL = "http://127.0.0.1:8000/terminal/" +SUCCESS_MSG = "[green]\nLogin successful[/green]" + + +@dataclass(frozen=True) +class User: + email: str + + +def launch_terminal(login_info: dict): + token = login_info.get("access_token", "") + if token: + decoded_info = jwt.decode(token, options={"verify_signature": False}) + User.email = decoded_info.get("sub", "") + setattr(feature_flags, "USE_FLAIR", User.email) + + terminal_controller.parse_args_and_run() + + +def request_token(email: str, password: str, save: bool) -> dict: + """Request token and save it to file + + Parameters + ---------- + email : str + Email + password : str + Password + + """ + data = { + "email": email, + "password": password, + "remember": True, + } + response = requests.post(BASE_URL + "login", json=data) + code = response.status_code + if code == 200: + console.print(SUCCESS_MSG) + login = response.json() + if save: + with open(SETTINGS_DIRECTORY / "login.json", "w") as outfile: + outfile.write(json.dumps(login)) + return login + if code == 401: + console.print(f"[red]\n{str(response.json()['detail']).capitalize()}[/red]\n") + elif code == 403: + console.print(f"[red]\n{str(response.json()['message']).capitalize()}[/red]\n") + else: + console.print("[red]\nUnknown error.[/red]\n") + return {} + + +def get_saved_token_if_valid() -> dict: + """Try to login with saved credentials + + Returns + ------- + bool + True if login was successful, False otherwise + """ + file_path = SETTINGS_DIRECTORY / "login.json" + if os.path.isfile(file_path): + with open(file_path, "r") as file: + login_info = json.load(file) + if is_token_valid(login_info): + return login_info + return {} + + +def is_token_valid(login_info: dict) -> bool: + """Request login with saved credentials + + Parameters + ---------- + login_info : dict + Login information + + Returns + ------- + bool + True if login was successful, False otherwise + """ + if "access_token" in login_info and "token_type" in login_info: + response = requests.get( + url=BASE_URL + "user", + headers={ + "Authorization": f"{login_info['token_type'].title()} {login_info['access_token']}" + }, + ) + if response.status_code == 200: + console.print(SUCCESS_MSG) + return True + return False diff --git a/openbb_terminal/rich_config.py b/openbb_terminal/rich_config.py index 8e0f152d9f0..2e75fac3b4e 100644 --- a/openbb_terminal/rich_config.py +++ b/openbb_terminal/rich_config.py @@ -1,6 +1,7 @@ """Rich Module""" __docformat__ = "numpy" +import getpass import os import json from pathlib import Path @@ -314,5 +315,13 @@ class ConsoleAndPanel: else: print(*args, **kwargs) + def input(self, *args, **kwargs): + self.print(*args, **kwargs, end="") + return input() + + def getpass(self, *args, **kwargs): + self.print(*args, **kwargs, end="") + return getpass.getpass("") + console = ConsoleAndPanel() diff --git a/terminal.py b/terminal.py index f7f88beed18..5d97f199e74 100644 --- a/terminal.py +++ b/terminal.py @@ -2,11 +2,17 @@ import multiprocessing import sys from openbb_terminal import terminal_controller from openbb_terminal.core.integration_tests import integration_controller +from openbb_terminal.account import login_controller +from openbb_terminal.terminal_helper import is_packaged_application + +IS_PACKAGE = True # is_packaged_application() uncomment to test in installer if __name__ == "__main__": multiprocessing.freeze_support() sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: integration_controller.main() + elif IS_PACKAGE: + login_controller.main() else: terminal_controller.parse_args_and_run() From 29d62ebd2673666a946fd77ab50fbee27bb65486 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 13:42:53 +0000 Subject: [PATCH 002/230] looking a bit better --- openbb_terminal/account/login_controller.py | 21 ++++++++------ openbb_terminal/account/login_model.py | 32 +++++---------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 3ff45120b34..52f4286656b 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,6 +1,7 @@ from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( - get_saved_token_if_valid, + get_login_info, + is_login_valid, request_token, launch_terminal, ) @@ -17,14 +18,18 @@ def login_prompt(): display_welcome_message() while True: console.print("\nPlease enter your credentials:") - email = console.input("> Email: ", style="yellow") - password = console.getpass("> Password: ", style="yellow") - save_str = console.input("> Keep me logged in? [y/n]: ").lower() + email = console.input("> Email: ", style="blue") + password = console.getpass("> Password: ", style="blue") + save_str = "" + while save_str not in ["y", "n"]: + save_str = console.input( + "> Keep me logged in (y/n): ", style="blue" + ).lower() + + save = False if save_str == "y": save = True - elif save_str == "n": - save = False login_info = request_token(email, password, save) if login_info: @@ -35,8 +40,8 @@ def login_prompt(): def main(): """Main function""" - login_info = get_saved_token_if_valid() - if login_info: + login_info = get_login_info() + if is_login_valid(login_info): return launch_terminal(login_info) return login_prompt() diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 445c06fa953..97bff56f8e7 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -15,14 +15,17 @@ SUCCESS_MSG = "[green]\nLogin successful[/green]" @dataclass(frozen=True) class User: email: str + uuid: str def launch_terminal(login_info: dict): token = login_info.get("access_token", "") + User.uuid = login_info.get("uuid", "") if token: decoded_info = jwt.decode(token, options={"verify_signature": False}) User.email = decoded_info.get("sub", "") - setattr(feature_flags, "USE_FLAIR", User.email) + username = User.email[:User.email.find("@")] + setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") terminal_controller.parse_args_and_run() @@ -61,36 +64,15 @@ def request_token(email: str, password: str, save: bool) -> dict: return {} -def get_saved_token_if_valid() -> dict: - """Try to login with saved credentials - - Returns - ------- - bool - True if login was successful, False otherwise - """ +def get_login_info() -> dict: file_path = SETTINGS_DIRECTORY / "login.json" if os.path.isfile(file_path): with open(file_path, "r") as file: - login_info = json.load(file) - if is_token_valid(login_info): - return login_info + return json.load(file) return {} -def is_token_valid(login_info: dict) -> bool: - """Request login with saved credentials - - Parameters - ---------- - login_info : dict - Login information - - Returns - ------- - bool - True if login was successful, False otherwise - """ +def is_login_valid(login_info: dict) -> bool: if "access_token" in login_info and "token_type" in login_info: response = requests.get( url=BASE_URL + "user", From 869925877bc6dc5055410110bc14fe2bce1e5dc4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 14:13:25 +0000 Subject: [PATCH 003/230] easier to read --- openbb_terminal/account/login_controller.py | 31 +++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 52f4286656b..58bb3f57042 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -14,23 +14,26 @@ def display_welcome_message(): ) +def get_user_input(): + console.print("\nPlease enter your credentials:") + email = console.input("> Email: ", style="blue") + password = console.getpass("> Password: ", style="blue") + + save_str = "" + while save_str not in ["y", "n"]: + save_str = console.input("> Keep me logged in (y/n): ", style="blue").lower() + + save = False + if save_str == "y": + save = True + + return email, password, save + + def login_prompt(): display_welcome_message() while True: - console.print("\nPlease enter your credentials:") - email = console.input("> Email: ", style="blue") - password = console.getpass("> Password: ", style="blue") - - save_str = "" - while save_str not in ["y", "n"]: - save_str = console.input( - "> Keep me logged in (y/n): ", style="blue" - ).lower() - - save = False - if save_str == "y": - save = True - + email, password, save = get_user_input() login_info = request_token(email, password, save) if login_info: break From b1a5bde9a55fccc43a575ce7d1eed3d86d72df05 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 14:16:04 +0000 Subject: [PATCH 004/230] rename func --- openbb_terminal/account/login_controller.py | 4 ++-- openbb_terminal/account/login_model.py | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 58bb3f57042..7f0897f930f 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -2,7 +2,7 @@ from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( get_login_info, is_login_valid, - request_token, + request_login_info, launch_terminal, ) @@ -34,7 +34,7 @@ def login_prompt(): display_welcome_message() while True: email, password, save = get_user_input() - login_info = request_token(email, password, save) + login_info = request_login_info(email, password, save) if login_info: break diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 97bff56f8e7..7fd51836b89 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -24,23 +24,13 @@ def launch_terminal(login_info: dict): if token: decoded_info = jwt.decode(token, options={"verify_signature": False}) User.email = decoded_info.get("sub", "") - username = User.email[:User.email.find("@")] + username = User.email[: User.email.find("@")] setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") terminal_controller.parse_args_and_run() -def request_token(email: str, password: str, save: bool) -> dict: - """Request token and save it to file - - Parameters - ---------- - email : str - Email - password : str - Password - - """ +def request_login_info(email: str, password: str, save: bool) -> dict: data = { "email": email, "password": password, From cc8910613deff96f1d0a3f3b6589c3d62c0cdaf0 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 14:16:15 +0000 Subject: [PATCH 005/230] add jwt to poetry --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b7378cbd49b..d1f3ef2c50e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,6 +108,7 @@ docstring-parser = {version = "^0.15", optional = true} grpcio = "^1.51.1" jupyterlab-widgets = "^3.0.3" packaging = ">=22.0" +PyJWT = "^2.6.0" [tool.poetry.dev-dependencies] pytest = "^6.2.2" From d1e30fc4104ecf5981ae4fb0696b24ffd3c31038 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 14:16:32 +0000 Subject: [PATCH 006/230] forgot poetry lock --- poetry.lock | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 7ba611b7d14..6d93ad32863 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3059,6 +3059,20 @@ category = "main" optional = true python-versions = ">=3.7" +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.4.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "pre-commit"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + [[package]] name = "pylint" version = "2.15.2" @@ -4856,7 +4870,7 @@ prediction = ["torch", "pytorch-lightning", "u8darts"] [metadata] lock-version = "1.1" python-versions = "^3.8,<3.11, !=3.9.7" -content-hash = "77d8bec0f896363a577f4cbdcd9345705a9c9ea22395c44df71b7f3799c0618d" +content-hash = "7fd9f4ca140556f2b9b3df3ee1568b09259fd3b83fb2546c321b14b190dff36c" [metadata.files] absl-py = [] @@ -5092,6 +5106,7 @@ pygments = [] pyhdfe = [] pyinstaller = [] pyinstaller-hooks-contrib = [] +pyjwt = [] pylint = [] pyluach = [] pymeeus = [] From 5a0012ddcf0c656566689b9549a8b7bf973a9512 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 18:49:33 +0000 Subject: [PATCH 007/230] handle possible errors --- openbb_terminal/account/login_controller.py | 50 ++++-- openbb_terminal/account/login_model.py | 177 ++++++++++++++++---- 2 files changed, 182 insertions(+), 45 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 7f0897f930f..cdb18b83bf2 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,20 +1,31 @@ from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( + Status, + Error, get_login_info, - is_login_valid, + get_login_status, request_login_info, launch_terminal, ) def display_welcome_message(): - console.print( - "You need to be connected to use the installer version of OpenBB." - "\nTo register please go on: https://my.openbb.co/register" - ) + """Display welcome message""" + console.print("You need to be connected to use the installer version of OpenBB.\n") def get_user_input(): + """Get user input + + Returns + ------- + email : str + User email + password : str + User password + save : bool + Save login info + """ console.print("\nPlease enter your credentials:") email = console.input("> Email: ", style="blue") password = console.getpass("> Password: ", style="blue") @@ -30,23 +41,38 @@ def get_user_input(): return email, password, save -def login_prompt(): - display_welcome_message() +def login_prompt(welcome=True): + """Login prompt + + Parameters + ---------- + welcome : bool, optional + Display welcome message, by default True + """ + if welcome: + display_welcome_message() + while True: email, password, save = get_user_input() login_info = request_login_info(email, password, save) - if login_info: + if isinstance(login_info, dict) and login_info: break - return launch_terminal(login_info) + launch_terminal(login_info) def main(): """Main function""" login_info = get_login_info() - if is_login_valid(login_info): - return launch_terminal(login_info) - return login_prompt() + if login_info == Error.READ_ERROR: + return login_prompt() + + status = get_login_status(login_info=login_info) + if status == Status.VALID_LOGIN: + launch_terminal(login_info=login_info) + + if status == Status.INVALID_LOGIN: + login_prompt(welcome=False) if __name__ == "__main__": diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 7fd51836b89..e0c23786bcf 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -1,6 +1,9 @@ from dataclasses import dataclass +from enum import Enum +from pathlib import Path import os.path import json +from typing import Union import requests from openbb_terminal import terminal_controller from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY @@ -9,7 +12,20 @@ from openbb_terminal import feature_flags import jwt BASE_URL = "http://127.0.0.1:8000/terminal/" -SUCCESS_MSG = "[green]\nLogin successful[/green]" + + +class Status(Enum): + VALID_LOGIN = "[green]\nLogin successful.[/green]" + INVALID_LOGIN = "[red]\nInvalid login.[/red]" + + +class Error(Enum): + CONNECTION_ERROR = "[red]\nConnection error.[/red]" + WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" + UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" + UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" + SAVE_ERROR = "[red]\nError saving login info.[/red]" + READ_ERROR = "[red]\nError reading login info.[/red]" @dataclass(frozen=True) @@ -19,6 +35,13 @@ class User: def launch_terminal(login_info: dict): + """Launch the terminal. + + Parameters + ---------- + login_info : dict + The login info. + """ token = login_info.get("access_token", "") User.uuid = login_info.get("uuid", "") if token: @@ -30,47 +53,135 @@ def launch_terminal(login_info: dict): terminal_controller.parse_args_and_run() -def request_login_info(email: str, password: str, save: bool) -> dict: +def write_to_file(data: dict, file_path: Path): + """Write data to a file. + + Parameters + ---------- + data : dict + The data to write. + file_path : Path + The file path. + """ + try: + with open(file_path, "w") as outfile: + outfile.write(json.dumps(data)) + except Exception: + console.print(Error.SAVE_ERROR.value) + + +def process_response(response: requests.Response, save: bool) -> Union[dict, Error]: + """Process the response from the server. + + Parameters + ---------- + response : requests.Response + The response from the server. + save : bool + Save login info. + + Returns + ------- + Union[dict, Error] + The login info, or an error. + """ + if response.status_code == 200: + console.print(Status.VALID_LOGIN.value) + login = response.json() + if save: + write_to_file(login, SETTINGS_DIRECTORY / "login.json") + return login + + if response.status_code == 401: + console.print(Error.WRONG_CREDENTIALS.value) + return Error.WRONG_CREDENTIALS + elif response.status_code == 403: + console.print(Error.UNVERIFIED_EMAIL.value) + return Error.UNVERIFIED_EMAIL + else: + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR + + +def request_login_info(email: str, password: str, save: bool) -> Union[dict, Error]: + """Request login info from the server. + + Parameters + ---------- + email : str + The user email. + password : str + The user password. + save : bool + Save login info. + + Returns + ------- + Union[dict, Error] + The login info, or an error. + """ data = { "email": email, "password": password, "remember": True, } - response = requests.post(BASE_URL + "login", json=data) - code = response.status_code - if code == 200: - console.print(SUCCESS_MSG) - login = response.json() - if save: - with open(SETTINGS_DIRECTORY / "login.json", "w") as outfile: - outfile.write(json.dumps(login)) - return login - if code == 401: - console.print(f"[red]\n{str(response.json()['detail']).capitalize()}[/red]\n") - elif code == 403: - console.print(f"[red]\n{str(response.json()['message']).capitalize()}[/red]\n") - else: - console.print("[red]\nUnknown error.[/red]\n") - return {} + + try: + response = requests.post(BASE_URL + "login", json=data) + except requests.exceptions.ConnectionError: + console.print(Error.CONNECTION_ERROR.value) + return Error.CONNECTION_ERROR + + return process_response(response, save) -def get_login_info() -> dict: +def get_login_info() -> Union[dict, Error]: + """Get the login info from the file. + + Returns + ------- + dict + The login info. + """ file_path = SETTINGS_DIRECTORY / "login.json" - if os.path.isfile(file_path): - with open(file_path, "r") as file: - return json.load(file) + + try: + if os.path.isfile(file_path): + with open(file_path, "r") as file: + return json.load(file) + except Exception: + console.print(Error.READ_ERROR.value) + return Error.READ_ERROR return {} -def is_login_valid(login_info: dict) -> bool: +def get_login_status(login_info: dict) -> Union[Status, Error]: + """Check if the login info is valid. + + Parameters + ---------- + login_info : dict + The login info to check. + + Returns + ------- + Union[Status, Error] + The status of the login info, or an error. + """ if "access_token" in login_info and "token_type" in login_info: - response = requests.get( - url=BASE_URL + "user", - headers={ - "Authorization": f"{login_info['token_type'].title()} {login_info['access_token']}" - }, - ) - if response.status_code == 200: - console.print(SUCCESS_MSG) - return True - return False + try: + if ( + requests.get( + url=BASE_URL + "user", + headers={ + "Authorization": f"{login_info['token_type'].title()} {login_info['access_token']}" + }, + ).status_code + == 200 + ): + console.print(Status.VALID_LOGIN.value) + return Status.VALID_LOGIN + except requests.exceptions.ConnectionError: + console.print(Error.CONNECTION_ERROR.value) + return Error.CONNECTION_ERROR + return Status.INVALID_LOGIN From 32ee937602d058ddcab6abecffba50df491b9b0b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 18:51:11 +0000 Subject: [PATCH 008/230] requirements update --- requirements-full.txt | 1 + requirements.txt | 123 ++++++++++++++---------------------------- 2 files changed, 40 insertions(+), 84 deletions(-) diff --git a/requirements-full.txt b/requirements-full.txt index 70ea24519aa..2d54e78ae8d 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -219,6 +219,7 @@ pyex==0.5.0 pyflakes==2.3.1; python_full_version >= "3.7.0" pygments==2.13.0; python_version >= "3.6" pyhdfe==0.1.0; python_version >= "3.8" +pyjwt==2.6.0; python_version >= "3.7" pylint==2.15.2; python_full_version >= "3.7.2" pyluach==2.0.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" pymeeus==0.5.11; python_version >= "3.7" and python_version < "4" diff --git a/requirements.txt b/requirements.txt index d9963e85961..a08c8368529 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,27 @@ aiodns==3.0.0; python_version >= "3.5.2" and python_full_version >= "3.7.0" aiohttp==3.8.3; python_version >= "3.6" and python_full_version >= "3.7.0" aiosignal==1.2.0; python_version >= "3.6" and python_full_version >= "3.7.0" -alabaster==0.7.12; python_version >= "3.6" alpha-vantage==2.3.1 altair==4.2.0; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" anyio==3.6.2; python_full_version >= "3.6.2" and python_version >= "3.8" and python_version < "4.0" appdirs==1.4.4 -appnope==0.1.3; sys_platform == "darwin" and python_version >= "3.8" and platform_system == "Darwin" and python_full_version >= "3.7.0" and python_full_version < "4.0.0" +appnope==0.1.3; sys_platform == "darwin" and python_version >= "3.8" and platform_system == "Darwin" +arch==5.3.1; python_version >= "3.7" argon2-cffi-bindings==21.2.0; python_version >= "3.7" argon2-cffi==21.3.0; python_version >= "3.7" ascii-magic==1.6; python_version >= "3.5" astor==0.8.1; python_full_version >= "3.7.1" and python_full_version < "4.0.0" and python_version >= "3.8" -astroid==2.12.12; python_full_version >= "3.7.2" +astropy==5.1.1; python_version >= "3.8" asttokens==2.0.8; python_version >= "3.8" async-timeout==4.0.2; python_version >= "3.6" and python_full_version >= "3.7.0" -atomicwrites==1.4.1; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.7.0" and python_full_version < "4.0.0" -attrs==21.4.0; python_full_version >= "3.7.0" and python_version >= "3.7" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0") and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7" and python_full_version < "4.0.0") +attrs==21.4.0; python_version >= "3.6" and python_full_version >= "3.7.0" and (python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7") babel==2.10.3; python_version >= "3.7" backcall==0.2.0; python_version >= "3.8" backoff==2.2.1; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.7.0" backports.zoneinfo==0.2.1; python_version >= "3.7" and python_full_version < "3.9.7" and python_version < "3.9" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.6.0" or python_full_version > "3.9.7" and python_version >= "3.6") or python_full_version > "3.9.7" and python_version >= "3.6" and python_version < "3.9" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.6.0" or python_full_version > "3.9.7" and python_version >= "3.6") -bandit==1.7.4; python_version >= "3.7" base58==2.1.1; python_version >= "3.5" and python_full_version >= "3.7.0" beartype==0.7.1; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.6.0" beautifulsoup4==4.11.1; python_full_version >= "3.6.0" and python_version >= "3.7" -black==22.12.0; python_version >= "3.7" bleach==5.0.1; python_version >= "3.7" blinker==1.5; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.5.0" or python_full_version > "3.9.7" bs4==0.0.1 @@ -32,39 +29,34 @@ bt==0.2.9; (python_version >= "2.7" and python_full_version < "3.0.0") or (pytho cachetools==5.2.0; python_version >= "3.7" and python_version < "4.0" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") ccxt==2.5.80 certifi==2022.9.24; python_version >= "3.8" and python_version < "4" and python_full_version >= "3.7.1" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") -cffi==1.15.1; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.7.0" and python_full_version < "4.0.0" -cfgv==3.3.1; python_full_version >= "3.6.1" and python_version >= "3.7" +cffi==1.15.1; implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.7.0" charset-normalizer==2.1.1; python_full_version >= "3.6.0" click==8.1.3; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" -codespell==2.2.2; python_version >= "3.7" -colorama==0.4.6; python_full_version >= "3.7.2" and python_version >= "3.8" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.7.0") and (python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.7.0") and (python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") or platform_system == "Windows" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") and python_full_version >= "3.7.0") and (python_version >= "3.8" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.8" and python_full_version >= "3.7.0") and python_full_version < "4.0.0" +colorama==0.4.6; python_full_version >= "3.7.0" and python_version >= "3.8" and sys_platform == "win32" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") commonmark==0.9.1; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") convertdate==2.4.0; python_version >= "3.7" and python_version < "4" -coverage==6.5.0; python_version >= "3.7" cryptography==38.0.2; python_version >= "3.6" cssselect==1.1.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" +cvxpy==1.2.1; python_version >= "3.7" cycler==0.11.0; python_version >= "3.7" cython==0.29.32; python_version >= "3.8" and python_full_version < "3.0.0" and sys_platform == "darwin" or python_full_version >= "3.3.0" and sys_platform == "darwin" and python_version >= "3.8" dateparser==1.1.2; python_version >= "3.5" datetime==4.7; python_version >= "3.5" -debugpy==1.6.3; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" -decorator==5.1.1; python_version >= "3.8" and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5") and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.5") +debugpy==1.6.3; python_version >= "3.7" +decorator==5.1.1; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.8" defusedxml==0.7.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" degiro-connector==2.0.21; python_full_version >= "3.7.1" and python_full_version < "4.0.0" deprecation==2.1.0 detecta==0.0.5; python_version >= "3.6" -dill==0.3.6; python_version >= "3.7" and python_full_version >= "3.7.2" -distlib==0.3.6; python_version >= "3.7" dnspython==2.2.1; python_version >= "3.6" and python_version < "4.0" -docutils==0.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -entrypoints==0.4; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") +ecos==2.0.10; python_version >= "3.7" +entrypoints==0.4; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" et-xmlfile==1.1.0; python_version >= "3.6" exchange-calendars==4.2.3; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" executing==1.1.1; python_version >= "3.8" -fastjsonschema==2.16.2; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" +fastjsonschema==2.16.2; python_full_version >= "3.7.0" and python_version >= "3.7" feedparser==6.0.10; python_version >= "3.6" ffn==0.3.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" -filelock==3.8.0; python_version >= "3.7" financedatabase==1.0.2 finnhub-python==2.4.15 finviz==1.4.4 @@ -87,30 +79,26 @@ holidays==0.14.2; python_version >= "3.7" html5lib==1.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") httpcore==0.15.0; python_version >= "3.8" and python_version < "4.0" httpx==0.23.0; python_version >= "3.8" and python_version < "4.0" -identify==2.5.7; python_version >= "3.7" idna==3.4; python_version >= "3.8" and python_version < "4" and python_full_version >= "3.7.1" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") -imagesize==1.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" importlib-metadata==5.0.0; python_version >= "3.7" and python_full_version < "3.9.7" and python_version < "3.10" or python_full_version > "3.9.7" and python_version >= "3.7" and python_version < "3.10" inflection==0.5.1; python_version >= "3.6" -iniconfig==1.1.1; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" interface-meta==1.3.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.1" and python_full_version < "4.0.0" investiny==0.5.0; python_version >= "3.8" and python_version < "4.0" investpy==1.0.8; python_version >= "3.7" ipyflex==0.2.4; python_version >= "3.6" -ipykernel==6.16.1; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" +ipykernel==6.16.1; python_version >= "3.7" ipympl==0.8.4 ipython-genutils==0.2.0; python_version >= "3.7" ipython==8.5.0; python_version >= "3.8" ipywidgets==8.0.2; python_version >= "3.7" iso8601==0.1.16 -isort==5.10.1; python_full_version >= "3.7.2" and python_version < "4.0" jedi==0.18.1; python_version >= "3.8" jinja2==3.1.2; python_version >= "3.7" joblib==1.2.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8" json5==0.9.10; python_version >= "3.7" jsonschema==3.2.0 -jupyter-client==7.4.1; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" -jupyter-core==4.11.2; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" +jupyter-client==7.4.1; python_full_version >= "3.7.0" and python_version >= "3.7" +jupyter-core==4.11.2; python_full_version >= "3.7.0" and python_version >= "3.7" jupyter-server==1.21.0; python_version >= "3.7" jupyterlab-pygments==0.2.2; python_version >= "3.7" jupyterlab-server==2.16.1; python_version >= "3.7" @@ -118,32 +106,25 @@ jupyterlab-widgets==3.0.3; python_version >= "3.7" jupyterlab==3.5.0; python_version >= "3.7" kiwisolver==1.4.4; python_version >= "3.7" korean-lunar-calendar==0.3.1; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" -lazy-object-proxy==1.7.1; python_version >= "3.6" and python_full_version >= "3.7.2" linearmodels==4.27; python_version >= "3.8" loguru==0.6.0; python_version >= "3.5" and python_full_version >= "3.7.0" lxml==4.9.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" -markdown-it-py==1.1.0; python_version >= "3.6" and python_version < "4.0" markupsafe==2.1.1; python_version >= "3.7" -matplotlib-inline==0.1.6; python_version >= "3.8" and python_full_version >= "3.7.0" and python_full_version < "4.0.0" +matplotlib-inline==0.1.6; python_version >= "3.8" matplotlib==3.5.3; python_version >= "3.7" -mccabe==0.6.1; python_full_version >= "3.7.2" -mdit-py-plugins==0.2.8; python_version >= "3.6" and python_version < "4.0" +mccabe==0.6.1; python_full_version >= "3.7.0" mistune==0.8.4; python_version >= "3.7" -mock==4.0.3; python_version >= "3.6" more-itertools==9.0.0; python_version >= "3.7" mplfinance==0.12.9b1 multidict==6.0.2; python_version >= "3.7" and python_full_version >= "3.7.0" multitasking==0.0.11 mypy-extensions==0.4.3; python_version >= "3.8" -mypy==0.930; python_version >= "3.6" -myst-parser==0.15.2; python_version >= "3.6" nbclassic==0.4.5; python_version >= "3.7" -nbclient==0.5.13; python_full_version >= "3.7.0" and python_version >= "3.7" and python_full_version < "4.0.0" +nbclient==0.5.13; python_full_version >= "3.7.0" and python_version >= "3.7" nbconvert==6.5.4; python_version >= "3.7" -nbformat==5.7.0; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" -nbmake==1.3.0; python_full_version >= "3.7.0" and python_full_version < "4.0.0" -nest-asyncio==1.5.6; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" -nodeenv==1.7.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.7.0" and python_version >= "3.7" +nbformat==5.7.0; python_full_version >= "3.7.0" and python_version >= "3.7" +nest-asyncio==1.5.6; python_full_version >= "3.7.0" and python_version >= "3.7" +networkx==2.8.7; python_version >= "3.8" notebook-shim==0.2.0; python_version >= "3.7" notebook==6.5.1; python_version >= "3.7" numpy==1.23.4; python_version >= "3.8" @@ -151,6 +132,7 @@ oandapyv20==0.6.3 oauthlib==3.2.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" onetimepass==1.0.1; python_full_version >= "3.7.1" and python_full_version < "4.0.0" openpyxl==3.0.10; python_version >= "3.6" +osqp==0.6.2.post5; python_version >= "3.7" packaging==22.0; python_version >= "3.7" pandas-datareader==0.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" pandas-market-calendars==3.2; python_full_version >= "3.7.0" @@ -159,40 +141,35 @@ pandas==1.5.1; python_version >= "3.8" pandocfilters==1.5.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" papermill @ git+https://github.com/nteract/papermill.git@main ; python_version >= "3.7" parso==0.8.3; python_version >= "3.8" -pathspec==0.10.2; python_version >= "3.7" patsy==0.5.3; python_version >= "3.8" -pbr==5.11.0; python_version >= "3.8" pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.8" pickleshare==0.7.5; python_version >= "3.8" pillow==9.2.0; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" -platformdirs==2.5.2; python_version >= "3.7" and python_full_version >= "3.7.2" plotly==5.10.0; python_version >= "3.6" -pluggy==1.0.0; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" praw==7.6.0; python_version >= "3.7" and python_version < "4.0" prawcore==2.3.0; python_version >= "3.7" and python_version < "4.0" -pre-commit==2.20.0; python_version >= "3.7" prometheus-client==0.15.0; python_version >= "3.7" prompt-toolkit==3.0.31; python_full_version >= "3.6.2" property-cached==1.6.4; python_version >= "3.8" protobuf==3.20.1; python_version >= "3.7" psaw==0.0.12; python_version >= "3" -psutil==5.9.3; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" +psutil==5.9.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.8" and os_name != "nt" pure-eval==0.2.2; python_version >= "3.8" -py==1.11.0; python_full_version >= "3.7.0" and python_version >= "3.7" and python_full_version < "4.0.0" and implementation_name == "pypy" +py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" and implementation_name == "pypy" or implementation_name == "pypy" and python_version >= "3.7" and python_full_version >= "3.5.0" pyally==1.1.2 pyarrow==9.0.0; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" pycares==4.2.2; python_version >= "3.5.2" and python_full_version >= "3.7.0" pycodestyle==2.7.0; python_full_version >= "3.7.0" pycoingecko==2.3.0 pycparser==2.21; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -pydantic==1.8.2; python_full_version >= "3.7.0" and python_full_version < "4.0.0" pydeck==0.8.0b4; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" +pyerfa==2.0.0.1; python_version >= "3.8" pyex==0.5.0 pyflakes==2.3.1; python_full_version >= "3.7.0" pygments==2.13.0; python_version >= "3.6" pyhdfe==0.1.0; python_version >= "3.8" -pylint==2.15.2; python_full_version >= "3.7.2" +pyjwt==2.6.0; python_version >= "3.7" pyluach==2.0.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" pymeeus==0.5.11; python_version >= "3.7" and python_version < "4" pympler==1.0.1; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.6" @@ -202,10 +179,6 @@ pyotp==2.7.0; python_version >= "3.6" pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" pyprind==2.11.3; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" pyrsistent==0.18.1; python_version >= "3.7" -pytest-cov==3.0.0; python_version >= "3.6" -pytest-mock==3.10.0; python_version >= "3.7" -pytest-recording==0.12.1; python_version >= "3.5" -pytest==6.2.5; python_version >= "3.6" pythclient==0.1.2; python_full_version >= "3.7.0" python-binance==1.0.16 python-coinmarketcap==0.2 @@ -215,11 +188,11 @@ python-i18n==0.3.9 pytrends==4.8.0 pytz-deprecation-shim==0.1.0.post0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.6.0" or python_full_version > "3.9.7" and python_version >= "3.6" pytz==2022.5; python_full_version >= "3.7.1" and python_full_version < "4.0.0" and python_version >= "3.8" and (python_version >= "3.8" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.8") and python_version < "4.0" -pyupgrade==2.38.4; python_version >= "3.7" pywin32==305; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_version >= "3.7" pywinpty==2.0.8; os_name == "nt" and python_version >= "3.7" -pyyaml==6.0; python_version >= "3.7" -pyzmq==24.0.1; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version >= "3.7" +pyyaml==6.0; python_version >= "3.8" +pyzmq==24.0.1; python_version >= "3.7" +qdldl==0.1.5.post2; python_version >= "3.7" quandl==3.7.0; python_version >= "3.6" rapidfuzz==1.9.1; python_version >= "2.7" regex==2022.3.2; python_version >= "3.6" @@ -227,37 +200,29 @@ requests-oauthlib==1.3.1; python_version >= "2.7" and python_full_version < "3.0 requests==2.28.1; python_version >= "3.7" and python_version < "4" rfc3986==1.5.0; python_version >= "3.8" and python_version < "4.0" rich==12.6.0; python_full_version >= "3.6.3" and python_full_version < "4.0.0" +riskfolio-lib==3.3.0; python_version >= "3.7" robin-stocks==2.1.0; python_version >= "3" ruamel.yaml.clib==0.2.7; platform_python_implementation == "CPython" and python_version < "3.11" and python_version >= "3.5" ruamel.yaml==0.17.21; python_version >= "3" -ruff==0.0.210; python_version >= "3.7" scikit-learn==1.1.2; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8" scipy==1.10.0; python_version >= "3.8" and python_version < "3.12" screeninfo==0.6.7 +scs==3.2.0; python_version >= "3.7" seaborn==0.11.2; python_version >= "3.6" semver==2.13.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.4.0" or python_full_version > "3.9.7" send2trash==1.8.0; python_version >= "3.7" sentiment-investor==2.1.0; python_version >= "3.8" and python_version < "4.0" setuptools-scm==6.4.2; python_version >= "3.8" sgmllib3k==1.0.0; python_version >= "3.6" -six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.8" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.3.0" or python_full_version > "3.9.7") and (python_version >= "3.8" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.8") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7" and python_full_version < "4.0.0") and (python_version >= "3.8" and python_full_version < "3.0.0" or python_version >= "3.8" and python_full_version >= "3.3.0") +six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.8" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.3.0" or python_full_version > "3.9.7") and (python_version >= "3.8" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.8") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7") and (python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.8") smmap==5.0.0; python_version >= "3.7" sniffio==1.3.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.6.2" -snowballstemmer==2.2.0; python_version >= "3.6" socketio-client-nexus==0.7.6 soupsieve==2.3.2.post1; python_version >= "3.6" and python_full_version >= "3.6.0" -sphinx==4.5.0; python_version >= "3.6" -sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" -sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" -sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" -sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" -sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" -sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" squarify==0.4.3 sseclient==0.0.27 stack-data==0.5.1; python_version >= "3.8" statsmodels==0.13.2; python_version >= "3.7" -stevedore==4.1.0; python_version >= "3.8" stocksera==0.1.21; python_version >= "3.6" streamlit==1.13.0; (python_version >= "3.7" and python_full_version < "3.9.7") or (python_full_version > "3.9.7") tabulate==0.9.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" @@ -267,24 +232,15 @@ terminado==0.16.0; python_version >= "3.7" thepassiveinvestor==1.0.11 threadpoolctl==3.1.0; python_version >= "3.8" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.8" tinycss2==1.2.1; python_version >= "3.7" -tokenize-rt==4.2.1; python_full_version >= "3.6.1" and python_version >= "3.7" tokenterminal==1.0.1 -toml==0.10.2; python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7" and python_full_version < "4.0.0" -tomli==2.0.1; python_version < "3.11" and python_full_version >= "3.7.2" and python_version >= "3.8" and python_full_version <= "3.11.0a6" -tomlkit==0.11.5; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.7.2" +toml==0.10.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.3.0" or python_full_version > "3.9.7" +tomli==2.0.1; python_version >= "3.8" toolz==0.12.0; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") -tornado==6.2; python_version >= "3.7" and python_full_version < "3.9.7" and python_full_version >= "3.7.0" or python_full_version > "3.9.7" and python_version >= "3.7" and python_full_version < "4.0.0" +tornado==6.2; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" tqdm==4.64.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" tradingview-ta==3.3.0; python_version >= "3.6" -traitlets==5.5.0; python_version >= "3.8" and python_full_version >= "3.7.0" and python_full_version < "4.0.0" -types-python-dateutil==2.8.19.2 -types-pytz==2021.3.8 -types-pyyaml==6.0.12 -types-requests==2.28.11.2 -types-setuptools==57.4.18 -types-six==1.16.21 -types-urllib3==1.26.25.1 -typing-extensions==4.4.0; python_full_version >= "3.7.2" and python_full_version < "4.0.0" and python_version < "3.9" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") +traitlets==5.5.0; python_full_version >= "3.7.0" and python_version >= "3.8" +typing-extensions==4.4.0; python_full_version >= "3.7.0" and python_full_version < "4.0.0" and python_version < "3.9" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7") and (python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7") tzdata==2022.5; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" or python_version >= "3.7" and python_full_version < "3.9.7" and platform_system == "Windows" and python_full_version >= "3.6.0" or python_full_version > "3.9.7" and python_version >= "3.6" and platform_system == "Windows" tzlocal==4.2; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.6" ujson==5.5.0; python_version >= "3.7" @@ -295,8 +251,6 @@ user-agent==0.1.10 vadersentiment==3.3.2 validators==0.20.0; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.4" valinvest==0.0.2; python_version >= "3.6" -vcrpy==4.2.1; python_version >= "3.7" -virtualenv==20.16.5; python_version >= "3.7" voila==0.4.0; python_version >= "3.7" watchdog==2.1.9; python_version >= "3.6" wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.8" @@ -305,7 +259,8 @@ websocket-client==1.4.1; python_version >= "3.8" and python_version < "4.0" websockets==10.3; python_version >= "3.7" widgetsnbextension==4.0.3; python_version >= "3.7" win32-setctime==1.1.0; python_version >= "3.5" and python_full_version >= "3.7.0" and sys_platform == "win32" -wrapt==1.14.1; python_full_version >= "3.7.2" and python_full_version < "4.0.0" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") and python_version >= "3.8" and python_version < "3.11" +wrapt==1.14.1; python_full_version >= "3.7.1" and python_full_version < "4.0.0" and python_version >= "3.8" +xlsxwriter==3.0.3; python_version >= "3.7" yarl==1.8.1; python_version >= "3.7" and python_full_version >= "3.7.0" yfinance==0.2.4 zipp==3.10.0; python_version >= "3.7" and python_full_version < "3.9.7" and python_version < "3.10" or python_full_version > "3.9.7" and python_version >= "3.7" and python_version < "3.10" From 8071408bfc0f8e63047e0b9c4a6e057b2ab6f208 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 18:55:19 +0000 Subject: [PATCH 009/230] return type --- openbb_terminal/account/login_controller.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index cdb18b83bf2..0da6acd0162 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,3 +1,4 @@ +from typing import Tuple from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( Status, @@ -14,17 +15,13 @@ def display_welcome_message(): console.print("You need to be connected to use the installer version of OpenBB.\n") -def get_user_input(): +def get_user_input() -> Tuple[str, str, bool]: """Get user input Returns ------- - email : str - User email - password : str - User password - save : bool - Save login info + Tuple[str, str, bool] + The user email, password and save login option. """ console.print("\nPlease enter your credentials:") email = console.input("> Email: ", style="blue") From b90bb043db8c08d7ad97a9e56e3d88a128b656bb Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 18:58:31 +0000 Subject: [PATCH 010/230] docstring --- openbb_terminal/account/login_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 0da6acd0162..68d1898272e 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -39,7 +39,7 @@ def get_user_input() -> Tuple[str, str, bool]: def login_prompt(welcome=True): - """Login prompt + """Login prompt and launch terminal if login is successful. Parameters ---------- From fbc7766cad9e79b6333f69f72132d6f61efda092 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 19:00:13 +0000 Subject: [PATCH 011/230] import order --- openbb_terminal/account/login_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index e0c23786bcf..0ae63347e2a 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -4,12 +4,12 @@ from pathlib import Path import os.path import json from typing import Union +import jwt import requests from openbb_terminal import terminal_controller from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags -import jwt BASE_URL = "http://127.0.0.1:8000/terminal/" @@ -28,6 +28,7 @@ class Error(Enum): READ_ERROR = "[red]\nError reading login info.[/red]" +# Will something like this for the logging @dataclass(frozen=True) class User: email: str From be5fc3eda0d8665f63872dc7e4000894d737b6a3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 19:06:08 +0000 Subject: [PATCH 012/230] pylint --- openbb_terminal/account/login_controller.py | 15 +++++++-------- openbb_terminal/account/login_model.py | 8 +++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 68d1898272e..14477d5b9bb 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -62,14 +62,13 @@ def main(): """Main function""" login_info = get_login_info() if login_info == Error.READ_ERROR: - return login_prompt() - - status = get_login_status(login_info=login_info) - if status == Status.VALID_LOGIN: - launch_terminal(login_info=login_info) - - if status == Status.INVALID_LOGIN: - login_prompt(welcome=False) + login_prompt() + else: + status = get_login_status(login_info=login_info) + if status == Status.VALID_LOGIN: + launch_terminal(login_info=login_info) + if status == Status.INVALID_LOGIN: + login_prompt(welcome=False) if __name__ == "__main__": diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 0ae63347e2a..dbaae783afd 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -92,16 +92,14 @@ def process_response(response: requests.Response, save: bool) -> Union[dict, Err if save: write_to_file(login, SETTINGS_DIRECTORY / "login.json") return login - if response.status_code == 401: console.print(Error.WRONG_CREDENTIALS.value) return Error.WRONG_CREDENTIALS - elif response.status_code == 403: + if response.status_code == 403: console.print(Error.UNVERIFIED_EMAIL.value) return Error.UNVERIFIED_EMAIL - else: - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR def request_login_info(email: str, password: str, save: bool) -> Union[dict, Error]: From 2733834f061dfc08a5900a7956d5423bc3b671db Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 19:16:52 +0000 Subject: [PATCH 013/230] no login, go to prompt --- openbb_terminal/account/login_controller.py | 9 ++++++--- openbb_terminal/account/login_model.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 14477d5b9bb..5a6e1af78bc 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -12,7 +12,10 @@ from openbb_terminal.account.login_model import ( def display_welcome_message(): """Display welcome message""" - console.print("You need to be connected to use the installer version of OpenBB.\n") + console.print( + "You need to log in to use the installer version of OpenBB.", + style="info", + ) def get_user_input() -> Tuple[str, str, bool]: @@ -23,7 +26,7 @@ def get_user_input() -> Tuple[str, str, bool]: Tuple[str, str, bool] The user email, password and save login option. """ - console.print("\nPlease enter your credentials:") + console.print("\nPlease enter your credentials:", style="info") email = console.input("> Email: ", style="blue") password = console.getpass("> Password: ", style="blue") @@ -61,7 +64,7 @@ def login_prompt(welcome=True): def main(): """Main function""" login_info = get_login_info() - if login_info == Error.READ_ERROR: + if login_info == Error.READ_ERROR or not login_info: login_prompt() else: status = get_login_status(login_info=login_info) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index dbaae783afd..cc4c32ac157 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -146,7 +146,7 @@ def get_login_info() -> Union[dict, Error]: try: if os.path.isfile(file_path): - with open(file_path, "r") as file: + with open(file_path) as file: return json.load(file) except Exception: console.print(Error.READ_ERROR.value) From 87a51e6fe9d64927145f58a95e75cd8fd0eb74cf Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 20:23:41 +0000 Subject: [PATCH 014/230] add logout and whoami --- openbb_terminal/account/login_controller.py | 2 +- openbb_terminal/account/login_model.py | 62 +++++++++++++++------ openbb_terminal/account/user.py | 10 ++++ openbb_terminal/parent_classes.py | 35 ++++++++++++ 4 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 openbb_terminal/account/user.py diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 5a6e1af78bc..ecd2a1338f1 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -13,7 +13,7 @@ from openbb_terminal.account.login_model import ( def display_welcome_message(): """Display welcome message""" console.print( - "You need to log in to use the installer version of OpenBB.", + "\nYou must log in to use the installer version of OpenBB.", style="info", ) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index cc4c32ac157..1a26310ed00 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from enum import Enum from pathlib import Path import os.path @@ -11,12 +10,15 @@ from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags +from openbb_terminal.account.user import User + BASE_URL = "http://127.0.0.1:8000/terminal/" class Status(Enum): VALID_LOGIN = "[green]\nLogin successful.[/green]" INVALID_LOGIN = "[red]\nInvalid login.[/red]" + LOGGED_OUT = "[green]\nLogged out.[/green]" class Error(Enum): @@ -28,13 +30,6 @@ class Error(Enum): READ_ERROR = "[red]\nError reading login info.[/red]" -# Will something like this for the logging -@dataclass(frozen=True) -class User: - email: str - uuid: str - - def launch_terminal(login_info: dict): """Launch the terminal. @@ -43,13 +38,16 @@ def launch_terminal(login_info: dict): login_info : dict The login info. """ - token = login_info.get("access_token", "") + User.token_type = login_info.get("token_type", "") + User.token = login_info.get("access_token", "") User.uuid = login_info.get("uuid", "") - if token: - decoded_info = jwt.decode(token, options={"verify_signature": False}) + if User.token: + decoded_info = jwt.decode(User.token, options={"verify_signature": False}) User.email = decoded_info.get("sub", "") - username = User.email[: User.email.find("@")] - setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") + + if feature_flags.USE_FLAIR == ":openbb": + username = User.email[: User.email.find("@")] + setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") terminal_controller.parse_args_and_run() @@ -65,8 +63,8 @@ def write_to_file(data: dict, file_path: Path): The file path. """ try: - with open(file_path, "w") as outfile: - outfile.write(json.dumps(data)) + with open(file_path, "w") as file: + file.write(json.dumps(data)) except Exception: console.print(Error.SAVE_ERROR.value) @@ -130,7 +128,9 @@ def request_login_info(email: str, password: str, save: bool) -> Union[dict, Err except requests.exceptions.ConnectionError: console.print(Error.CONNECTION_ERROR.value) return Error.CONNECTION_ERROR - + except Exception: + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR return process_response(response, save) @@ -183,4 +183,34 @@ def get_login_status(login_info: dict) -> Union[Status, Error]: except requests.exceptions.ConnectionError: console.print(Error.CONNECTION_ERROR.value) return Error.CONNECTION_ERROR + except Exception: + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR return Status.INVALID_LOGIN + + +def logout() -> Union[Status, Error]: + """Logout the user. + + Returns + ------- + Union[Status, Error] + The status of the logout, or an error. + """ + try: + # TODO: Uncomment when the server supports logout + # requests.post( + # url=BASE_URL + "logout", + # headers={"Authorization": f"{User.token_type.title()} {User.token}"}, + # ) + file_path = SETTINGS_DIRECTORY / "login.json" + if os.path.isfile(file_path): + os.remove(file_path) + console.print(Status.LOGGED_OUT.value) + return Status.LOGGED_OUT + except requests.exceptions.ConnectionError: + console.print(Error.CONNECTION_ERROR.value) + return Error.CONNECTION_ERROR + except Exception: + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py new file mode 100644 index 00000000000..b874abc053a --- /dev/null +++ b/openbb_terminal/account/user.py @@ -0,0 +1,10 @@ +# Will something like this for the logging +from dataclasses import dataclass + + +@dataclass(frozen=True) +class User: + token_type: str = "" + token: str = "" + email: str = "" + uuid: str = "" diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index c719edccd7f..87c5a16f706 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -18,6 +18,8 @@ import pandas as pd from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown +from openbb_terminal.account.login_model import Status, logout +from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import ( USER_CUSTOM_IMPORTS_DIRECTORY, @@ -47,6 +49,7 @@ from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation from openbb_terminal.cryptocurrency import cryptocurrency_helpers from openbb_terminal.core.completer.choices import build_controller_choice_map +from openbb_terminal.account import login_controller logger = logging.getLogger(__name__) @@ -94,6 +97,8 @@ class BaseController(metaclass=ABCMeta): "record", "stop", "screenshot", + "logout", + "whoami", ] CHOICES_COMMANDS: List[str] = [] @@ -656,6 +661,36 @@ class BaseController(metaclass=ABCMeta): if ns_parser: screenshot() + @log_start_end(log=logger) + def call_logout(self, other_args: List[str]) -> None: + """Process logout command.""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="logout", + description="Logout from OpenBB", + ) + ns_parser = self.parse_simple_args(parser, other_args) + + if ns_parser: + if logout() == Status.LOGGED_OUT: + login_controller.main() + + @log_start_end(log=logger) + def call_whoami(self, other_args: List[str]) -> None: + """Process whoami command.""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="whoami", + description="Show current user", + ) + ns_parser = self.parse_simple_args(parser, other_args) + + if ns_parser: + console.print(f"[info]email:[/info] {User.email}") + console.print(f"[info]uuid:[/info] {User.uuid}\n") + @staticmethod def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]): """Parse list of arguments into the supplied parser. From eb66dc5f0c7ee9dca4bdb8c583d622fd15709452 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 20:32:37 +0000 Subject: [PATCH 015/230] clear before prompt --- openbb_terminal/parent_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 87c5a16f706..6effd388390 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -674,6 +674,7 @@ class BaseController(metaclass=ABCMeta): if ns_parser: if logout() == Status.LOGGED_OUT: + system_clear() login_controller.main() @log_start_end(log=logger) From 0ff64533d03c50662ad023ac5f65083b2224904d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 20:35:33 +0000 Subject: [PATCH 016/230] black --- terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.py b/terminal.py index 5d97f199e74..064e65f1305 100644 --- a/terminal.py +++ b/terminal.py @@ -5,7 +5,7 @@ from openbb_terminal.core.integration_tests import integration_controller from openbb_terminal.account import login_controller from openbb_terminal.terminal_helper import is_packaged_application -IS_PACKAGE = True # is_packaged_application() uncomment to test in installer +IS_PACKAGE = True # is_packaged_application() uncomment to test in installer if __name__ == "__main__": multiprocessing.freeze_support() From 5ea1d8c9486e0e2a87ae3f071525089b9a4057a9 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 21:32:26 +0000 Subject: [PATCH 017/230] these are unecessary --- openbb_terminal/account/login_model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 1a26310ed00..80d75d09797 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -26,8 +26,6 @@ class Error(Enum): WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" - SAVE_ERROR = "[red]\nError saving login info.[/red]" - READ_ERROR = "[red]\nError reading login info.[/red]" def launch_terminal(login_info: dict): @@ -66,7 +64,7 @@ def write_to_file(data: dict, file_path: Path): with open(file_path, "w") as file: file.write(json.dumps(data)) except Exception: - console.print(Error.SAVE_ERROR.value) + console.print(Error.UNKNOWN_ERROR.value) def process_response(response: requests.Response, save: bool) -> Union[dict, Error]: @@ -149,8 +147,8 @@ def get_login_info() -> Union[dict, Error]: with open(file_path) as file: return json.load(file) except Exception: - console.print(Error.READ_ERROR.value) - return Error.READ_ERROR + console.print(Error.UNKNOWN_ERROR.value) + return Error.UNKNOWN_ERROR return {} From 5ce85bfae610bf9c04ad0b87f0bdfa28bc90fd73 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 19 Jan 2023 21:35:32 +0000 Subject: [PATCH 018/230] error --- openbb_terminal/account/login_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index ecd2a1338f1..fffd80ad264 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -64,7 +64,7 @@ def login_prompt(welcome=True): def main(): """Main function""" login_info = get_login_info() - if login_info == Error.READ_ERROR or not login_info: + if login_info == Error.UNKNOWN_ERROR or not login_info: login_prompt() else: status = get_login_status(login_info=login_info) From dffbd61f62851a105f9b599a0cc8b06eef0fccc4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 10:07:30 +0000 Subject: [PATCH 019/230] improve login prompt message --- openbb_terminal/account/banner.txt | 18 ++++++++++++++++++ openbb_terminal/account/login_controller.py | 7 +++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 openbb_terminal/account/banner.txt diff --git a/openbb_terminal/account/banner.txt b/openbb_terminal/account/banner.txt new file mode 100644 index 00000000000..b81416cdb92 --- /dev/null +++ b/openbb_terminal/account/banner.txt @@ -0,0 +1,18 @@ + ___ ____ ____ _____ _ _ + / _ \ _ __ ___ _ __ | __ )| __ )_ _|__ _ __ _ __ ___ (_)_ __ __ _| | +| | | | '_ \ / _ \ '_ \| _ \| _ \ | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | +| |_| | |_) | __/ | | | |_) | |_) || | __/ | | | | | | | | | | | (_| | | + \___/| .__/ \___|_| |_|____/|____/ |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_| + |_| + @@@ + @@@ + @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ + @@@ @@@ @@@ @@@ + @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ + @@@ @@@ + %%%%%%%%%%%%%%%%%@@@ @@@%%%%%%%%%%%%%%%%% + @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ + @@@ @@@ @@@ @@@ + @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ + + Investment research for Everyone, Anywhere. diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index fffd80ad264..c86419891b0 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,4 +1,5 @@ from typing import Tuple +from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( Status, @@ -12,10 +13,8 @@ from openbb_terminal.account.login_model import ( def display_welcome_message(): """Display welcome message""" - console.print( - "\nYou must log in to use the installer version of OpenBB.", - style="info", - ) + with open(PACKAGE_DIRECTORY / "account" / "banner.txt") as f: + console.print(f.read(), style="blue") def get_user_input() -> Tuple[str, str, bool]: From ba80ad82bdb7863d8421d33fec30f3923a866e70 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 10:11:02 +0000 Subject: [PATCH 020/230] login does not follow the controller pattern, so will name it differently --- .../account/{login_controller.py => login_wizard.py} | 0 openbb_terminal/parent_classes.py | 4 ++-- terminal.py | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) rename openbb_terminal/account/{login_controller.py => login_wizard.py} (100%) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_wizard.py similarity index 100% rename from openbb_terminal/account/login_controller.py rename to openbb_terminal/account/login_wizard.py diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 6effd388390..fce97453c23 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -49,7 +49,7 @@ from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation from openbb_terminal.cryptocurrency import cryptocurrency_helpers from openbb_terminal.core.completer.choices import build_controller_choice_map -from openbb_terminal.account import login_controller +from openbb_terminal.account import login_wizard logger = logging.getLogger(__name__) @@ -675,7 +675,7 @@ class BaseController(metaclass=ABCMeta): if ns_parser: if logout() == Status.LOGGED_OUT: system_clear() - login_controller.main() + login_wizard.main() @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/terminal.py b/terminal.py index 064e65f1305..12b115c62a0 100644 --- a/terminal.py +++ b/terminal.py @@ -2,17 +2,18 @@ import multiprocessing import sys from openbb_terminal import terminal_controller from openbb_terminal.core.integration_tests import integration_controller -from openbb_terminal.account import login_controller +from openbb_terminal.account import login_wizard from openbb_terminal.terminal_helper import is_packaged_application -IS_PACKAGE = True # is_packaged_application() uncomment to test in installer if __name__ == "__main__": multiprocessing.freeze_support() sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: integration_controller.main() - elif IS_PACKAGE: - login_controller.main() + elif ( + is_packaged_application() or True + ): # remove True to force login just on installer + login_wizard.main() else: terminal_controller.parse_args_and_run() From 19b35ba4a5dbc0d94999c1ade0b0972f3fd92c75 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 10:14:50 +0000 Subject: [PATCH 021/230] nop --- .../account/{login_wizard.py => login_controller.py} | 0 openbb_terminal/parent_classes.py | 4 ++-- terminal.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename openbb_terminal/account/{login_wizard.py => login_controller.py} (100%) diff --git a/openbb_terminal/account/login_wizard.py b/openbb_terminal/account/login_controller.py similarity index 100% rename from openbb_terminal/account/login_wizard.py rename to openbb_terminal/account/login_controller.py diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index fce97453c23..6effd388390 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -49,7 +49,7 @@ from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation from openbb_terminal.cryptocurrency import cryptocurrency_helpers from openbb_terminal.core.completer.choices import build_controller_choice_map -from openbb_terminal.account import login_wizard +from openbb_terminal.account import login_controller logger = logging.getLogger(__name__) @@ -675,7 +675,7 @@ class BaseController(metaclass=ABCMeta): if ns_parser: if logout() == Status.LOGGED_OUT: system_clear() - login_wizard.main() + login_controller.main() @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/terminal.py b/terminal.py index 12b115c62a0..7c7b1089aaa 100644 --- a/terminal.py +++ b/terminal.py @@ -2,7 +2,7 @@ import multiprocessing import sys from openbb_terminal import terminal_controller from openbb_terminal.core.integration_tests import integration_controller -from openbb_terminal.account import login_wizard +from openbb_terminal.account import login_controller from openbb_terminal.terminal_helper import is_packaged_application @@ -14,6 +14,6 @@ if __name__ == "__main__": elif ( is_packaged_application() or True ): # remove True to force login just on installer - login_wizard.main() + login_controller.main() else: terminal_controller.parse_args_and_run() From dfe58c9c86f5ad72740c554492aa8b8fef3d154e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:01:34 +0000 Subject: [PATCH 022/230] move logout and change to results --- openbb_terminal/account/login_controller.py | 9 +- openbb_terminal/account/login_model.py | 108 ++++++-------------- openbb_terminal/account/logout_model.py | 72 +++++++++++++ openbb_terminal/account/statics.py | 26 +++++ openbb_terminal/parent_classes.py | 7 +- 5 files changed, 137 insertions(+), 85 deletions(-) create mode 100644 openbb_terminal/account/logout_model.py create mode 100644 openbb_terminal/account/statics.py diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index c86419891b0..1b86d7361a1 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -2,13 +2,12 @@ from typing import Tuple from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.account.login_model import ( - Status, - Error, get_login_info, get_login_status, request_login_info, launch_terminal, ) +from openbb_terminal.account.statics import Success, Failure def display_welcome_message(): @@ -63,13 +62,13 @@ def login_prompt(welcome=True): def main(): """Main function""" login_info = get_login_info() - if login_info == Error.UNKNOWN_ERROR or not login_info: + if login_info == Failure.UNKNOWN_ERROR or not login_info: login_prompt() else: status = get_login_status(login_info=login_info) - if status == Status.VALID_LOGIN: + if status == Success.VALID_LOGIN: launch_terminal(login_info=login_info) - if status == Status.INVALID_LOGIN: + if status == Failure.INVALID_LOGIN: login_prompt(welcome=False) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 80d75d09797..a2aff670a63 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -1,4 +1,3 @@ -from enum import Enum from pathlib import Path import os.path import json @@ -9,24 +8,9 @@ from openbb_terminal import terminal_controller from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags - +from openbb_terminal.account.statics import BASE_URL, Success, Failure from openbb_terminal.account.user import User -BASE_URL = "http://127.0.0.1:8000/terminal/" - - -class Status(Enum): - VALID_LOGIN = "[green]\nLogin successful.[/green]" - INVALID_LOGIN = "[red]\nInvalid login.[/red]" - LOGGED_OUT = "[green]\nLogged out.[/green]" - - -class Error(Enum): - CONNECTION_ERROR = "[red]\nConnection error.[/red]" - WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" - UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" - UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" - def launch_terminal(login_info: dict): """Launch the terminal. @@ -50,8 +34,8 @@ def launch_terminal(login_info: dict): terminal_controller.parse_args_and_run() -def write_to_file(data: dict, file_path: Path): - """Write data to a file. +def save_login_info(data: dict, file_path: Path): + """Save the login info to a file. Parameters ---------- @@ -64,10 +48,10 @@ def write_to_file(data: dict, file_path: Path): with open(file_path, "w") as file: file.write(json.dumps(data)) except Exception: - console.print(Error.UNKNOWN_ERROR.value) + console.print("Failed to save login info.", style="red") -def process_response(response: requests.Response, save: bool) -> Union[dict, Error]: +def process_response(response: requests.Response, save: bool) -> Union[dict, Failure]: """Process the response from the server. Parameters @@ -79,26 +63,26 @@ def process_response(response: requests.Response, save: bool) -> Union[dict, Err Returns ------- - Union[dict, Error] + Union[dict, Failure] The login info, or an error. """ if response.status_code == 200: - console.print(Status.VALID_LOGIN.value) + console.print(Success.VALID_LOGIN.value) login = response.json() if save: - write_to_file(login, SETTINGS_DIRECTORY / "login.json") + save_login_info(login, SETTINGS_DIRECTORY / "login.json") return login if response.status_code == 401: - console.print(Error.WRONG_CREDENTIALS.value) - return Error.WRONG_CREDENTIALS + console.print(Failure.WRONG_CREDENTIALS.value) + return Failure.WRONG_CREDENTIALS if response.status_code == 403: - console.print(Error.UNVERIFIED_EMAIL.value) - return Error.UNVERIFIED_EMAIL - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR + console.print(Failure.UNVERIFIED_EMAIL.value) + return Failure.UNVERIFIED_EMAIL + console.print("Failed to login.", style="red") + return Failure.UNKNOWN_ERROR -def request_login_info(email: str, password: str, save: bool) -> Union[dict, Error]: +def request_login_info(email: str, password: str, save: bool) -> Union[dict, Failure]: """Request login info from the server. Parameters @@ -112,7 +96,7 @@ def request_login_info(email: str, password: str, save: bool) -> Union[dict, Err Returns ------- - Union[dict, Error] + Union[dict, Failure] The login info, or an error. """ data = { @@ -124,15 +108,15 @@ def request_login_info(email: str, password: str, save: bool) -> Union[dict, Err try: response = requests.post(BASE_URL + "login", json=data) except requests.exceptions.ConnectionError: - console.print(Error.CONNECTION_ERROR.value) - return Error.CONNECTION_ERROR + console.print(Failure.CONNECTION_ERROR.value) + return Failure.CONNECTION_ERROR except Exception: - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR + console.print("Failed to request login info.", style="red") + return Failure.UNKNOWN_ERROR return process_response(response, save) -def get_login_info() -> Union[dict, Error]: +def get_login_info() -> Union[dict, Failure]: """Get the login info from the file. Returns @@ -147,12 +131,12 @@ def get_login_info() -> Union[dict, Error]: with open(file_path) as file: return json.load(file) except Exception: - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR + console.print("Failed to get login info.", style="red") + return Failure.UNKNOWN_ERROR return {} -def get_login_status(login_info: dict) -> Union[Status, Error]: +def get_login_status(login_info: dict) -> Union[Success, Failure]: """Check if the login info is valid. Parameters @@ -162,53 +146,27 @@ def get_login_status(login_info: dict) -> Union[Status, Error]: Returns ------- - Union[Status, Error] + Union[Success, Failure] The status of the login info, or an error. """ if "access_token" in login_info and "token_type" in login_info: try: if ( requests.get( - url=BASE_URL + "user", + url=BASE_URL + "terminal/user", headers={ "Authorization": f"{login_info['token_type'].title()} {login_info['access_token']}" }, ).status_code == 200 ): - console.print(Status.VALID_LOGIN.value) - return Status.VALID_LOGIN + console.print(Success.VALID_LOGIN.value) + return Success.VALID_LOGIN except requests.exceptions.ConnectionError: - console.print(Error.CONNECTION_ERROR.value) - return Error.CONNECTION_ERROR + console.print(Failure.CONNECTION_ERROR.value) + return Failure.CONNECTION_ERROR except Exception: - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR - return Status.INVALID_LOGIN + console.print("Failed to get login status.", style="red") + return Failure.UNKNOWN_ERROR - -def logout() -> Union[Status, Error]: - """Logout the user. - - Returns - ------- - Union[Status, Error] - The status of the logout, or an error. - """ - try: - # TODO: Uncomment when the server supports logout - # requests.post( - # url=BASE_URL + "logout", - # headers={"Authorization": f"{User.token_type.title()} {User.token}"}, - # ) - file_path = SETTINGS_DIRECTORY / "login.json" - if os.path.isfile(file_path): - os.remove(file_path) - console.print(Status.LOGGED_OUT.value) - return Status.LOGGED_OUT - except requests.exceptions.ConnectionError: - console.print(Error.CONNECTION_ERROR.value) - return Error.CONNECTION_ERROR - except Exception: - console.print(Error.UNKNOWN_ERROR.value) - return Error.UNKNOWN_ERROR + return Failure.INVALID_LOGIN diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py new file mode 100644 index 00000000000..135a9b64169 --- /dev/null +++ b/openbb_terminal/account/logout_model.py @@ -0,0 +1,72 @@ +import os.path +from typing import Union +import requests +from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY +from openbb_terminal.helper_funcs import system_clear +from openbb_terminal.rich_config import console +from openbb_terminal.account.statics import BASE_URL, Success, Failure +from openbb_terminal.account.user import User +from openbb_terminal.account import login_controller + + +def remote_logout() -> Union[Success, Failure]: + """Request a remote logout. + + Returns + ------- + Union[Success, Failure] + The status of the token invalidation, or an error. + """ + try: + if ( + requests.get( + url=BASE_URL + "logout-everywhere", + headers={"Authorization": f"{User.token_type.title()} {User.token}"}, + ).status_code + == 200 + ): + console.print(Success.REMOTE_LOGOUT.value) + return Success.REMOTE_LOGOUT + console.print("Failed to logout remotely.", style="red") + return Failure.UNKNOWN_ERROR + except requests.exceptions.ConnectionError: + console.print(Failure.CONNECTION_ERROR.value) + return Failure.CONNECTION_ERROR + except Exception: + console.print("Failed to logout remotely.", style="red") + return Failure.UNKNOWN_ERROR + + +def remove_login_file() -> Union[Success, Failure]: + """Remove the login file. + + Returns + ------- + Success + The status of the local logout. + """ + + try: + file_path = SETTINGS_DIRECTORY / "login.json" + if os.path.isfile(file_path): + os.remove(file_path) + console.print(Success.LOCAL_LOGOUT.value) + return Success.LOCAL_LOGOUT + except Exception: + console.print("Failed to remove login file.", style="red") + return Failure.UNKNOWN_ERROR + + +def logout(): + """Logout the user. + + Returns + ------- + Union[Success, Failure] + The status of the logout, or an error. + """ + system_clear() + remove_login_file() + remote_logout() + console.print("") + login_controller.login_prompt(welcome=True) diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py new file mode 100644 index 00000000000..5aefa17c481 --- /dev/null +++ b/openbb_terminal/account/statics.py @@ -0,0 +1,26 @@ +from enum import Enum +from dataclasses import dataclass + +BASE_URL = "http://127.0.0.1:8000/" + + +class Success(Enum): + VALID_LOGIN = "[green]\nLogin successful.[/green]" + REMOTE_LOGOUT = "[green]\nLogged out remotely.[/green]" + LOCAL_LOGOUT = "[green]\nRemoved login info.[/green]" + + +class Failure(Enum): + INVALID_LOGIN = "[red]\nInvalid login.[/red]" + CONNECTION_ERROR = "[red]\nConnection error.[/red]" + WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" + UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" + UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" + + +@dataclass(frozen=True) +class User: + token_type: str = "" + token: str = "" + email: str = "" + uuid: str = "" diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 6effd388390..286057a90a6 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -18,7 +18,7 @@ import pandas as pd from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.account.login_model import Status, logout +from openbb_terminal.account.logout_model import logout from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import ( @@ -49,7 +49,6 @@ from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation from openbb_terminal.cryptocurrency import cryptocurrency_helpers from openbb_terminal.core.completer.choices import build_controller_choice_map -from openbb_terminal.account import login_controller logger = logging.getLogger(__name__) @@ -673,9 +672,7 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - if logout() == Status.LOGGED_OUT: - system_clear() - login_controller.main() + logout() @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: From e8404ae6dbc9f6f9c60bcdb1902186aeaf483bdb Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:24:58 +0000 Subject: [PATCH 023/230] change return obj --- openbb_terminal/account/login_controller.py | 6 ++-- openbb_terminal/account/login_model.py | 33 ++++++++------------- openbb_terminal/account/logout_model.py | 18 ++++------- openbb_terminal/account/statics.py | 33 ++++++++++++++------- 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 1b86d7361a1..55cc8cd9ddb 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -62,13 +62,13 @@ def login_prompt(welcome=True): def main(): """Main function""" login_info = get_login_info() - if login_info == Failure.UNKNOWN_ERROR or not login_info: + if not login_info: login_prompt() else: status = get_login_status(login_info=login_info) - if status == Success.VALID_LOGIN: + if isinstance(status, Success): launch_terminal(login_info=login_info) - if status == Failure.INVALID_LOGIN: + if isinstance(status, Failure): login_prompt(welcome=False) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index a2aff670a63..94a46328cef 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -67,19 +67,16 @@ def process_response(response: requests.Response, save: bool) -> Union[dict, Fai The login info, or an error. """ if response.status_code == 200: - console.print(Success.VALID_LOGIN.value) + console.print("\nLogin successful.", style="green") login = response.json() if save: save_login_info(login, SETTINGS_DIRECTORY / "login.json") return login if response.status_code == 401: - console.print(Failure.WRONG_CREDENTIALS.value) - return Failure.WRONG_CREDENTIALS + return Failure("[red]\nWrong credentials.[/red]") if response.status_code == 403: - console.print(Failure.UNVERIFIED_EMAIL.value) - return Failure.UNVERIFIED_EMAIL - console.print("Failed to login.", style="red") - return Failure.UNKNOWN_ERROR + return Failure("[red]\nUnverified email.[/red]") + return Failure(("[red]\nFailed to login.[/red]")) def request_login_info(email: str, password: str, save: bool) -> Union[dict, Failure]: @@ -108,15 +105,13 @@ def request_login_info(email: str, password: str, save: bool) -> Union[dict, Fai try: response = requests.post(BASE_URL + "login", json=data) except requests.exceptions.ConnectionError: - console.print(Failure.CONNECTION_ERROR.value) - return Failure.CONNECTION_ERROR + return Failure("[red]\nConnection error.[/red]") except Exception: - console.print("Failed to request login info.", style="red") - return Failure.UNKNOWN_ERROR + return Failure("[red]\nFailed to request login info.[/red]") return process_response(response, save) -def get_login_info() -> Union[dict, Failure]: +def get_login_info() -> dict: """Get the login info from the file. Returns @@ -131,8 +126,7 @@ def get_login_info() -> Union[dict, Failure]: with open(file_path) as file: return json.load(file) except Exception: - console.print("Failed to get login info.", style="red") - return Failure.UNKNOWN_ERROR + console.print("[red]\nFailed to get login info.[/red]") return {} @@ -160,13 +154,10 @@ def get_login_status(login_info: dict) -> Union[Success, Failure]: ).status_code == 200 ): - console.print(Success.VALID_LOGIN.value) - return Success.VALID_LOGIN + return Success("[green]\nLogin successful.[/green]") except requests.exceptions.ConnectionError: - console.print(Failure.CONNECTION_ERROR.value) - return Failure.CONNECTION_ERROR + return Failure("[red]\nConnection error.[/red]") except Exception: - console.print("Failed to get login status.", style="red") - return Failure.UNKNOWN_ERROR + return Failure("[red]\nFailed to get login status.[/red]") - return Failure.INVALID_LOGIN + return Failure("[red]\nInvalid login.[/red]") diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 135a9b64169..e55f170d3e6 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -25,16 +25,12 @@ def remote_logout() -> Union[Success, Failure]: ).status_code == 200 ): - console.print(Success.REMOTE_LOGOUT.value) - return Success.REMOTE_LOGOUT - console.print("Failed to logout remotely.", style="red") - return Failure.UNKNOWN_ERROR + return Success("[green]\nLogged out remotely.[/green]") + return Failure("[red]\nFailed to logout remotely.[/red]") except requests.exceptions.ConnectionError: - console.print(Failure.CONNECTION_ERROR.value) - return Failure.CONNECTION_ERROR + return Failure("[red]\nConnection error.[/red]") except Exception: - console.print("Failed to logout remotely.", style="red") - return Failure.UNKNOWN_ERROR + return Failure("[red]Failed to logout remotely.[/red]") def remove_login_file() -> Union[Success, Failure]: @@ -50,11 +46,9 @@ def remove_login_file() -> Union[Success, Failure]: file_path = SETTINGS_DIRECTORY / "login.json" if os.path.isfile(file_path): os.remove(file_path) - console.print(Success.LOCAL_LOGOUT.value) - return Success.LOCAL_LOGOUT + return Success("[green]\nRemoved login info.[/green]") except Exception: - console.print("Failed to remove login file.", style="red") - return Failure.UNKNOWN_ERROR + return Failure("[red]Failed to remove login file.[/red]") def logout(): diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py index 5aefa17c481..fc7b4f3ba51 100644 --- a/openbb_terminal/account/statics.py +++ b/openbb_terminal/account/statics.py @@ -1,21 +1,32 @@ from enum import Enum from dataclasses import dataclass +from openbb_terminal.rich_config import console BASE_URL = "http://127.0.0.1:8000/" - -class Success(Enum): - VALID_LOGIN = "[green]\nLogin successful.[/green]" - REMOTE_LOGOUT = "[green]\nLogged out remotely.[/green]" - LOCAL_LOGOUT = "[green]\nRemoved login info.[/green]" +# This might be simpler if use a class with msg instead of enum -class Failure(Enum): - INVALID_LOGIN = "[red]\nInvalid login.[/red]" - CONNECTION_ERROR = "[red]\nConnection error.[/red]" - WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" - UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" - UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" +class Success: + def __init__(self, msg): + self.msg = msg + console.print(msg) + + # VALID_LOGIN = "[green]\nLogin successful.[/green]" + # REMOTE_LOGOUT = "[green]\nLogged out remotely.[/green]" + # LOCAL_LOGOUT = "[green]\nRemoved login info.[/green]" + + +class Failure: + def __init__(self, msg): + self.msg = msg + console.print(msg) + + # INVALID_LOGIN = "[red]\nInvalid login.[/red]" + # CONNECTION_ERROR = "[red]\nConnection error.[/red]" + # WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" + # UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" + # UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" @dataclass(frozen=True) From 0731aa0f626c0fc2f4119c37b6f45736ffe33f13 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:28:06 +0000 Subject: [PATCH 024/230] ruff --- openbb_terminal/account/statics.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py index fc7b4f3ba51..282fbf13031 100644 --- a/openbb_terminal/account/statics.py +++ b/openbb_terminal/account/statics.py @@ -1,33 +1,20 @@ -from enum import Enum from dataclasses import dataclass from openbb_terminal.rich_config import console BASE_URL = "http://127.0.0.1:8000/" -# This might be simpler if use a class with msg instead of enum - class Success: def __init__(self, msg): self.msg = msg console.print(msg) - # VALID_LOGIN = "[green]\nLogin successful.[/green]" - # REMOTE_LOGOUT = "[green]\nLogged out remotely.[/green]" - # LOCAL_LOGOUT = "[green]\nRemoved login info.[/green]" - class Failure: def __init__(self, msg): self.msg = msg console.print(msg) - # INVALID_LOGIN = "[red]\nInvalid login.[/red]" - # CONNECTION_ERROR = "[red]\nConnection error.[/red]" - # WRONG_CREDENTIALS = "[red]\nWrong credentials.[/red]" - # UNVERIFIED_EMAIL = "[red]\nUnverified email.[/red]" - # UNKNOWN_ERROR = "[red]\nUnknown error.[/red]" - @dataclass(frozen=True) class User: From b1eacd0450e043f92e7c4552368d938e5425f304 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:33:46 +0000 Subject: [PATCH 025/230] pyupgrade --- openbb_terminal/account/login_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index 94a46328cef..f253633e04f 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -76,7 +76,7 @@ def process_response(response: requests.Response, save: bool) -> Union[dict, Fai return Failure("[red]\nWrong credentials.[/red]") if response.status_code == 403: return Failure("[red]\nUnverified email.[/red]") - return Failure(("[red]\nFailed to login.[/red]")) + return Failure("[red]\nFailed to login.[/red]") def request_login_info(email: str, password: str, save: bool) -> Union[dict, Failure]: From e1828411613d0c9420e04c2d69bf29f66a10a8a4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:38:48 +0000 Subject: [PATCH 026/230] remove user from statics --- openbb_terminal/account/statics.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py index 282fbf13031..9eba64bd2bd 100644 --- a/openbb_terminal/account/statics.py +++ b/openbb_terminal/account/statics.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from openbb_terminal.rich_config import console BASE_URL = "http://127.0.0.1:8000/" @@ -14,11 +13,3 @@ class Failure: def __init__(self, msg): self.msg = msg console.print(msg) - - -@dataclass(frozen=True) -class User: - token_type: str = "" - token: str = "" - email: str = "" - uuid: str = "" From 88afbd8d8d5e6145b9aeb7c3296195d604086826 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 12:39:46 +0000 Subject: [PATCH 027/230] message --- openbb_terminal/account/logout_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index e55f170d3e6..1fd12c73ed1 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -30,7 +30,7 @@ def remote_logout() -> Union[Success, Failure]: except requests.exceptions.ConnectionError: return Failure("[red]\nConnection error.[/red]") except Exception: - return Failure("[red]Failed to logout remotely.[/red]") + return Failure("[red]\nFailed to logout remotely.[/red]") def remove_login_file() -> Union[Success, Failure]: @@ -48,7 +48,7 @@ def remove_login_file() -> Union[Success, Failure]: os.remove(file_path) return Success("[green]\nRemoved login info.[/green]") except Exception: - return Failure("[red]Failed to remove login file.[/red]") + return Failure("[red]\nFailed to remove login file.[/red]") def logout(): From 0869d452e211fec6b20feba4b6ca1056589f1386 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 13:41:30 +0000 Subject: [PATCH 028/230] fetch configs at start --- openbb_terminal/account/login_controller.py | 11 +++-- openbb_terminal/account/login_model.py | 47 ++++++++++++++------- openbb_terminal/account/statics.py | 4 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 55cc8cd9ddb..2271c49588d 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,11 +1,13 @@ from typing import Tuple from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console +from openbb_terminal import terminal_controller from openbb_terminal.account.login_model import ( + fetch_user_configs, get_login_info, get_login_status, + load_user_info, request_login_info, - launch_terminal, ) from openbb_terminal.account.statics import Success, Failure @@ -56,7 +58,9 @@ def login_prompt(welcome=True): if isinstance(login_info, dict) and login_info: break - launch_terminal(login_info) + load_user_info(login_info) + fetch_user_configs() + terminal_controller.parse_args_and_run() def main(): @@ -67,7 +71,8 @@ def main(): else: status = get_login_status(login_info=login_info) if isinstance(status, Success): - launch_terminal(login_info=login_info) + load_user_info(login_info) + terminal_controller.parse_args_and_run() if isinstance(status, Failure): login_prompt(welcome=False) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index f253633e04f..bc01f2dc39f 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -4,16 +4,41 @@ import json from typing import Union import jwt import requests -from openbb_terminal import terminal_controller from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags from openbb_terminal.account.statics import BASE_URL, Success, Failure from openbb_terminal.account.user import User +from openbb_terminal import config_terminal as cfg -def launch_terminal(login_info: dict): - """Launch the terminal. +def fetch_user_configs() -> Union[Success, Failure]: + try: + response = requests.get( + url=BASE_URL + "terminal/user", + headers={"Authorization": f"{User.token_type.title()} {User.token}"}, + ) + if response.status_code == 200: + configs = json.loads(response.content) + settings = configs.get("features_settings", {}) + keys = configs.get("features_keys", {}) + + for key, value in settings.items(): + setattr(cfg, key, value) + + for key, value in keys.items(): + setattr(cfg, key, value) + + return Success("[green]\nApplied user configurations.[/green]") + return Failure("[red]\nFailed to fetch configurations.[/red]") + except requests.exceptions.ConnectionError: + return Failure("[red]\nConnection error.[/red]") + except Exception: + return Failure("[red]\nFailed to apply configurations.[/red]") + + +def load_user_info(login_info: dict): + """Load user info from login info. Parameters ---------- @@ -23,6 +48,7 @@ def launch_terminal(login_info: dict): User.token_type = login_info.get("token_type", "") User.token = login_info.get("access_token", "") User.uuid = login_info.get("uuid", "") + if User.token: decoded_info = jwt.decode(User.token, options={"verify_signature": False}) User.email = decoded_info.get("sub", "") @@ -31,8 +57,6 @@ def launch_terminal(login_info: dict): username = User.email[: User.email.find("@")] setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") - terminal_controller.parse_args_and_run() - def save_login_info(data: dict, file_path: Path): """Save the login info to a file. @@ -145,16 +169,9 @@ def get_login_status(login_info: dict) -> Union[Success, Failure]: """ if "access_token" in login_info and "token_type" in login_info: try: - if ( - requests.get( - url=BASE_URL + "terminal/user", - headers={ - "Authorization": f"{login_info['token_type'].title()} {login_info['access_token']}" - }, - ).status_code - == 200 - ): - return Success("[green]\nLogin successful.[/green]") + load_user_info(login_info) + if isinstance(fetch_user_configs(), Success): + return Success() except requests.exceptions.ConnectionError: return Failure("[red]\nConnection error.[/red]") except Exception: diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py index 9eba64bd2bd..983f8686c1e 100644 --- a/openbb_terminal/account/statics.py +++ b/openbb_terminal/account/statics.py @@ -4,12 +4,12 @@ BASE_URL = "http://127.0.0.1:8000/" class Success: - def __init__(self, msg): + def __init__(self, msg=""): self.msg = msg console.print(msg) class Failure: - def __init__(self, msg): + def __init__(self, msg=""): self.msg = msg console.print(msg) From 405265845f50cad69ac26e0e21930be1a9d01e28 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 14:25:21 +0000 Subject: [PATCH 029/230] remove unecessary func --- openbb_terminal/account/login_controller.py | 9 ++-- openbb_terminal/account/login_model.py | 57 ++++++--------------- openbb_terminal/account/user.py | 2 + 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 2271c49588d..f53b5ba6d69 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -3,9 +3,9 @@ from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller from openbb_terminal.account.login_model import ( + apply_configs, fetch_user_configs, get_login_info, - get_login_status, load_user_info, request_login_info, ) @@ -58,8 +58,10 @@ def login_prompt(welcome=True): if isinstance(login_info, dict) and login_info: break + # Login successful load_user_info(login_info) fetch_user_configs() + apply_configs() terminal_controller.parse_args_and_run() @@ -69,9 +71,10 @@ def main(): if not login_info: login_prompt() else: - status = get_login_status(login_info=login_info) + load_user_info(login_info) + status = fetch_user_configs() if isinstance(status, Success): - load_user_info(login_info) + apply_configs() terminal_controller.parse_args_and_run() if isinstance(status, Failure): login_prompt(welcome=False) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index bc01f2dc39f..a5159f5943f 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -6,7 +6,7 @@ import jwt import requests from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags +from openbb_terminal import feature_flags as obbff from openbb_terminal.account.statics import BASE_URL, Success, Failure from openbb_terminal.account.user import User from openbb_terminal import config_terminal as cfg @@ -19,17 +19,8 @@ def fetch_user_configs() -> Union[Success, Failure]: headers={"Authorization": f"{User.token_type.title()} {User.token}"}, ) if response.status_code == 200: - configs = json.loads(response.content) - settings = configs.get("features_settings", {}) - keys = configs.get("features_keys", {}) - - for key, value in settings.items(): - setattr(cfg, key, value) - - for key, value in keys.items(): - setattr(cfg, key, value) - - return Success("[green]\nApplied user configurations.[/green]") + User.configs = json.loads(response.content) + return Success("[green]\nFetched user configurations.[/green]") return Failure("[red]\nFailed to fetch configurations.[/red]") except requests.exceptions.ConnectionError: return Failure("[red]\nConnection error.[/red]") @@ -37,6 +28,18 @@ def fetch_user_configs() -> Union[Success, Failure]: return Failure("[red]\nFailed to apply configurations.[/red]") +def apply_configs(): + """Apply configurations.""" + if User.configs: + settings = User.configs.get("features_settings", {}) + for k, v in settings.items(): + setattr(obbff, k, v) + + keys = User.configs.get("features_keys", {}) + for k, v in keys.items(): + setattr(cfg, k, v) + + def load_user_info(login_info: dict): """Load user info from login info. @@ -53,9 +56,9 @@ def load_user_info(login_info: dict): decoded_info = jwt.decode(User.token, options={"verify_signature": False}) User.email = decoded_info.get("sub", "") - if feature_flags.USE_FLAIR == ":openbb": + if obbff.USE_FLAIR == ":openbb": username = User.email[: User.email.find("@")] - setattr(feature_flags, "USE_FLAIR", "[" + username + "] 🦋") + setattr(obbff, "USE_FLAIR", "[" + username + "] 🦋") def save_login_info(data: dict, file_path: Path): @@ -152,29 +155,3 @@ def get_login_info() -> dict: except Exception: console.print("[red]\nFailed to get login info.[/red]") return {} - - -def get_login_status(login_info: dict) -> Union[Success, Failure]: - """Check if the login info is valid. - - Parameters - ---------- - login_info : dict - The login info to check. - - Returns - ------- - Union[Success, Failure] - The status of the login info, or an error. - """ - if "access_token" in login_info and "token_type" in login_info: - try: - load_user_info(login_info) - if isinstance(fetch_user_configs(), Success): - return Success() - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to get login status.[/red]") - - return Failure("[red]\nInvalid login.[/red]") diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index b874abc053a..892e01ff35f 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -1,5 +1,6 @@ # Will something like this for the logging from dataclasses import dataclass +from typing import Optional @dataclass(frozen=True) @@ -8,3 +9,4 @@ class User: token: str = "" email: str = "" uuid: str = "" + configs: Optional[dict] = None From 673db3b2cae16233b00a2fa5da3731edd5912dc2 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 15:33:44 +0000 Subject: [PATCH 030/230] unecessary dataclass --- openbb_terminal/account/user.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 892e01ff35f..24111a511b8 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -1,9 +1,6 @@ -# Will something like this for the logging -from dataclasses import dataclass from typing import Optional -@dataclass(frozen=True) class User: token_type: str = "" token: str = "" From e9d8ee56e69be8ea4666fe45382c5bdea50c2e3c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 15:51:41 +0000 Subject: [PATCH 031/230] change user vars --- openbb_terminal/account/login_model.py | 26 ++++++++++++------------- openbb_terminal/account/logout_model.py | 5 ++--- openbb_terminal/account/user.py | 12 +++++------- openbb_terminal/parent_classes.py | 6 +++--- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index a5159f5943f..b0bbe157803 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -8,7 +8,7 @@ from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal.account.statics import BASE_URL, Success, Failure -from openbb_terminal.account.user import User +from openbb_terminal.account import user from openbb_terminal import config_terminal as cfg @@ -16,10 +16,10 @@ def fetch_user_configs() -> Union[Success, Failure]: try: response = requests.get( url=BASE_URL + "terminal/user", - headers={"Authorization": f"{User.token_type.title()} {User.token}"}, + headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, ) if response.status_code == 200: - User.configs = json.loads(response.content) + user.CONFIGS = json.loads(response.content) return Success("[green]\nFetched user configurations.[/green]") return Failure("[red]\nFailed to fetch configurations.[/red]") except requests.exceptions.ConnectionError: @@ -30,12 +30,12 @@ def fetch_user_configs() -> Union[Success, Failure]: def apply_configs(): """Apply configurations.""" - if User.configs: - settings = User.configs.get("features_settings", {}) + if user.CONFIGS: + settings = user.CONFIGS.get("features_settings", {}) for k, v in settings.items(): setattr(obbff, k, v) - keys = User.configs.get("features_keys", {}) + keys = user.CONFIGS.get("features_keys", {}) for k, v in keys.items(): setattr(cfg, k, v) @@ -48,16 +48,16 @@ def load_user_info(login_info: dict): login_info : dict The login info. """ - User.token_type = login_info.get("token_type", "") - User.token = login_info.get("access_token", "") - User.uuid = login_info.get("uuid", "") + user.TOKEN_TYPE = login_info.get("token_type", "") + user.TOKEN = login_info.get("access_token", "") + user.UUID = login_info.get("uuid", "") - if User.token: - decoded_info = jwt.decode(User.token, options={"verify_signature": False}) - User.email = decoded_info.get("sub", "") + if user.TOKEN: + decoded_info = jwt.decode(user.TOKEN, options={"verify_signature": False}) + user.EMAIL = decoded_info.get("sub", "") if obbff.USE_FLAIR == ":openbb": - username = User.email[: User.email.find("@")] + username = user.EMAIL[: user.EMAIL.find("@")] setattr(obbff, "USE_FLAIR", "[" + username + "] 🦋") diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 1fd12c73ed1..86ad47f199a 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -5,8 +5,7 @@ from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console from openbb_terminal.account.statics import BASE_URL, Success, Failure -from openbb_terminal.account.user import User -from openbb_terminal.account import login_controller +from openbb_terminal.account import login_controller, user def remote_logout() -> Union[Success, Failure]: @@ -21,7 +20,7 @@ def remote_logout() -> Union[Success, Failure]: if ( requests.get( url=BASE_URL + "logout-everywhere", - headers={"Authorization": f"{User.token_type.title()} {User.token}"}, + headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, ).status_code == 200 ): diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 24111a511b8..ca735c3f361 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -1,9 +1,7 @@ from typing import Optional - -class User: - token_type: str = "" - token: str = "" - email: str = "" - uuid: str = "" - configs: Optional[dict] = None +TOKEN_TYPE: str = "" +TOKEN: str = "" +EMAIL: str = "" +UUID: str = "" +CONFIGS: Optional[dict] = None diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 286057a90a6..7ebe15b0d50 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -19,7 +19,7 @@ from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown from openbb_terminal.account.logout_model import logout -from openbb_terminal.account.user import User +from openbb_terminal.account import user from openbb_terminal.core.config.paths import ( USER_CUSTOM_IMPORTS_DIRECTORY, @@ -686,8 +686,8 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - console.print(f"[info]email:[/info] {User.email}") - console.print(f"[info]uuid:[/info] {User.uuid}\n") + console.print(f"[info]email:[/info] {user.EMAIL}") + console.print(f"[info]uuid:[/info] {user.UUID}\n") @staticmethod def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]): From 00a11f03545f3cda2594bf55e7110d0fd64fdad7 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 16:55:11 +0000 Subject: [PATCH 032/230] change login flow --- openbb_terminal/account/login_controller.py | 7 +++++-- openbb_terminal/account/login_model.py | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index f53b5ba6d69..a73073ffa2c 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -73,10 +73,13 @@ def main(): else: load_user_info(login_info) status = fetch_user_configs() - if isinstance(status, Success): + if status == 200: apply_configs() terminal_controller.parse_args_and_run() - if isinstance(status, Failure): + elif isinstance(status, Failure): + login_prompt(welcome=False) + else: + console.print("\n[red]Invalid login.[/red]") login_prompt(welcome=False) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index b0bbe157803..c60e08f9b16 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -4,15 +4,16 @@ import json from typing import Union import jwt import requests +from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff -from openbb_terminal.account.statics import BASE_URL, Success, Failure +from openbb_terminal.account.statics import BASE_URL, Failure from openbb_terminal.account import user from openbb_terminal import config_terminal as cfg -def fetch_user_configs() -> Union[Success, Failure]: +def fetch_user_configs() -> Union[int, Failure]: try: response = requests.get( url=BASE_URL + "terminal/user", @@ -20,20 +21,28 @@ def fetch_user_configs() -> Union[Success, Failure]: ) if response.status_code == 200: user.CONFIGS = json.loads(response.content) - return Success("[green]\nFetched user configurations.[/green]") - return Failure("[red]\nFailed to fetch configurations.[/red]") + console.print("[green]\nFetched user configurations.[/green]") + return response.status_code + console.print("[red]\nFailed to fetch configurations.[/red]") + return response.status_code except requests.exceptions.ConnectionError: return Failure("[red]\nConnection error.[/red]") except Exception: - return Failure("[red]\nFailed to apply configurations.[/red]") + return Failure("[red]\nFailed to fetch configurations.[/red]") def apply_configs(): """Apply configurations.""" + console.print(user.CONFIGS, style="red") if user.CONFIGS: settings = user.CONFIGS.get("features_settings", {}) for k, v in settings.items(): - setattr(obbff, k, v) + if hasattr(obbff, k): + if isinstance(getattr(obbff, k), int): + setattr(obbff, k, strtobool(v)) + else: + console.print("string", k, type(getattr(obbff, k))) + setattr(obbff, k, v) keys = user.CONFIGS.get("features_keys", {}) for k, v in keys.items(): From e411a4794212f1c7310b21b6ecddba0d4ba6c388 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 16:56:19 +0000 Subject: [PATCH 033/230] remote logout should not be used --- openbb_terminal/account/logout_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 86ad47f199a..5f76fd4cbcc 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -60,6 +60,5 @@ def logout(): """ system_clear() remove_login_file() - remote_logout() console.print("") login_controller.login_prompt(welcome=True) From 58026157349f1489ae47c4a7040fe44ca5d5610d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 17:05:28 +0000 Subject: [PATCH 034/230] simplify --- openbb_terminal/account/login_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index a73073ffa2c..48c6d6d1b42 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -9,7 +9,6 @@ from openbb_terminal.account.login_model import ( load_user_info, request_login_info, ) -from openbb_terminal.account.statics import Success, Failure def display_welcome_message(): @@ -76,10 +75,7 @@ def main(): if status == 200: apply_configs() terminal_controller.parse_args_and_run() - elif isinstance(status, Failure): - login_prompt(welcome=False) else: - console.print("\n[red]Invalid login.[/red]") login_prompt(welcome=False) From 61c6bf94b06d31e615fe58b8a87f24e994b90475 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 17:35:15 +0000 Subject: [PATCH 035/230] fix double exit logout bug --- openbb_terminal/account/logout_model.py | 2 -- openbb_terminal/parent_classes.py | 9 +++++++++ openbb_terminal/terminal_controller.py | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 5f76fd4cbcc..0f7efa976e2 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -60,5 +60,3 @@ def logout(): """ system_clear() remove_login_file() - console.print("") - login_controller.login_prompt(welcome=True) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 7ebe15b0d50..a7a7745a833 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -11,10 +11,13 @@ import os import re from abc import ABCMeta, abstractmethod from datetime import datetime, timedelta +import subprocess +import sys from typing import Any, Dict, List, Union import numpy as np import pandas as pd +import matplotlib.pyplot as plt from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown @@ -673,6 +676,12 @@ class BaseController(metaclass=ABCMeta): if ns_parser: logout() + plt.close("all") + out = subprocess.run( # nosec + f"{sys.executable} terminal.py", shell=True, check=False + ) + if out.returncode != 0: + console.print("Unfortunately, resetting wasn't possible!\n") @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 75e15dc479f..15e94ce79e3 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -901,6 +901,9 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): try: # Process the input command t_controller.queue = t_controller.switch(an_input) + if an_input == "logout": + break + if an_input in ("q", "quit", "..", "exit", "e"): print_goodbye() break From 427dac1c5c44b27162530893dc570cd21f045163 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 17:37:27 +0000 Subject: [PATCH 036/230] wrong exception --- openbb_terminal/parent_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index a7a7745a833..66f19919e41 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -681,7 +681,7 @@ class BaseController(metaclass=ABCMeta): f"{sys.executable} terminal.py", shell=True, check=False ) if out.returncode != 0: - console.print("Unfortunately, resetting wasn't possible!\n") + raise Exception("Error while logging out.") @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: From 9f0028777ddcd924b886df55648eaa92e1acd3d1 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 18:05:03 +0000 Subject: [PATCH 037/230] apply keys --- openbb_terminal/account/login_model.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index c60e08f9b16..d23f23a1545 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -41,12 +41,15 @@ def apply_configs(): if isinstance(getattr(obbff, k), int): setattr(obbff, k, strtobool(v)) else: - console.print("string", k, type(getattr(obbff, k))) setattr(obbff, k, v) keys = user.CONFIGS.get("features_keys", {}) for k, v in keys.items(): - setattr(cfg, k, v) + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) def load_user_info(login_info: dict): From 8ce990e39caeb89c47f1452aa13019c8fb9ea24a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 19:21:35 +0000 Subject: [PATCH 038/230] change controller --- openbb_terminal/account/login_controller.py | 33 +++++---- openbb_terminal/account/login_model.py | 41 ----------- openbb_terminal/account/logout_model.py | 5 +- openbb_terminal/account/sync_model.py | 77 +++++++++++++++++++++ openbb_terminal/account/user.py | 3 +- 5 files changed, 100 insertions(+), 59 deletions(-) create mode 100644 openbb_terminal/account/sync_model.py diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 48c6d6d1b42..8d14ac8ffb9 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,14 +1,14 @@ from typing import Tuple from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console +import json from openbb_terminal import terminal_controller from openbb_terminal.account.login_model import ( - apply_configs, - fetch_user_configs, get_login_info, load_user_info, request_login_info, ) +from openbb_terminal.account.sync_model import fetch_user_configs, apply_configs def display_welcome_message(): @@ -57,11 +57,24 @@ def login_prompt(welcome=True): if isinstance(login_info, dict) and login_info: break - # Login successful + login(login_info) + + +def login(login_info: dict): + """Login and launch terminal. + + Parameters + ---------- + login_info : dict + The login information. + """ load_user_info(login_info) - fetch_user_configs() - apply_configs() - terminal_controller.parse_args_and_run() + response = fetch_user_configs(login_info) + if response.status_code == 200: + apply_configs(configs=json.loads(response.content)) + terminal_controller.parse_args_and_run() + else: + login_prompt(welcome=False) def main(): @@ -70,13 +83,7 @@ def main(): if not login_info: login_prompt() else: - load_user_info(login_info) - status = fetch_user_configs() - if status == 200: - apply_configs() - terminal_controller.parse_args_and_run() - else: - login_prompt(welcome=False) + login(login_info) if __name__ == "__main__": diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py index d23f23a1545..074c20b616e 100644 --- a/openbb_terminal/account/login_model.py +++ b/openbb_terminal/account/login_model.py @@ -4,52 +4,11 @@ import json from typing import Union import jwt import requests -from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal.account.statics import BASE_URL, Failure from openbb_terminal.account import user -from openbb_terminal import config_terminal as cfg - - -def fetch_user_configs() -> Union[int, Failure]: - try: - response = requests.get( - url=BASE_URL + "terminal/user", - headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, - ) - if response.status_code == 200: - user.CONFIGS = json.loads(response.content) - console.print("[green]\nFetched user configurations.[/green]") - return response.status_code - console.print("[red]\nFailed to fetch configurations.[/red]") - return response.status_code - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to fetch configurations.[/red]") - - -def apply_configs(): - """Apply configurations.""" - console.print(user.CONFIGS, style="red") - if user.CONFIGS: - settings = user.CONFIGS.get("features_settings", {}) - for k, v in settings.items(): - if hasattr(obbff, k): - if isinstance(getattr(obbff, k), int): - setattr(obbff, k, strtobool(v)) - else: - setattr(obbff, k, v) - - keys = user.CONFIGS.get("features_keys", {}) - for k, v in keys.items(): - if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) def load_user_info(login_info: dict): diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 0f7efa976e2..4a4649bb23d 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -3,9 +3,8 @@ from typing import Union import requests from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.helper_funcs import system_clear -from openbb_terminal.rich_config import console from openbb_terminal.account.statics import BASE_URL, Success, Failure -from openbb_terminal.account import login_controller, user +from openbb_terminal.account import user def remote_logout() -> Union[Success, Failure]: @@ -45,7 +44,7 @@ def remove_login_file() -> Union[Success, Failure]: file_path = SETTINGS_DIRECTORY / "login.json" if os.path.isfile(file_path): os.remove(file_path) - return Success("[green]\nRemoved login info.[/green]") + return Success("[green]\nRemoved login info.[/green]") except Exception: return Failure("[red]\nFailed to remove login file.[/red]") diff --git a/openbb_terminal/account/sync_model.py b/openbb_terminal/account/sync_model.py new file mode 100644 index 00000000000..f0603efefdf --- /dev/null +++ b/openbb_terminal/account/sync_model.py @@ -0,0 +1,77 @@ +from typing import Union +import requests +from openbb_terminal.base_helpers import strtobool +from openbb_terminal.rich_config import console +from openbb_terminal import feature_flags as obbff +from openbb_terminal.account.statics import BASE_URL, Failure +from openbb_terminal.account import user +from openbb_terminal import config_terminal as cfg + + +def fetch_user_configs(login_info: dict) -> Union[requests.Response, Failure]: + """Fetch user configurations.""" + + token_type = login_info.get("token_type", "") + token = login_info.get("access_token", "") + + try: + response = requests.get( + url=BASE_URL + "terminal/user", + headers={"Authorization": f"{token_type.title()} {token}"}, + ) + if response.status_code == 200: + console.print("[green]\nFetched user configurations.[/green]") + else: + console.print("[red]\nFailed to fetch configurations.[/red]") + return response + except requests.exceptions.ConnectionError: + return Failure("[red]\nConnection error.[/red]") + except Exception: + return Failure("[red]\nFailed to fetch configurations.[/red]") + + +def apply_configs(configs: dict): + """Apply configurations.""" + console.print(configs, style="red") + if configs: + settings = configs.get("features_settings", {}) + for k, v in settings.items(): + if hasattr(obbff, k): + if isinstance(getattr(obbff, k), int): + setattr(obbff, k, strtobool(v)) + else: + setattr(obbff, k, v) + + keys = configs.get("features_keys", {}) + for k, v in keys.items(): + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) + + +def patch_user_configs(data: dict) -> Union[requests.Response, Failure]: + """Patch user configurations.""" + # When available on the server just need to send the changed configs, not all of them. + # Adapt this to Colin implementation. + pass + + +def put_user_configs(data: dict) -> Union[int, Failure]: + """Push user configurations to the server.""" + try: + response = requests.put( + url=BASE_URL + "terminal/user", + headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, + json=data, + ) + if response.status_code == 200: + console.print("[green]\nPushed user configurations.[/green]") + else: + console.print("[red]\nFailed to push configurations.[/red]") + return response.status_code + except requests.exceptions.ConnectionError: + return Failure("[red]\nConnection error.[/red]") + except Exception: + return Failure("[red]\nFailed to push configurations.[/red]") diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index ca735c3f361..3ca7f8fa4b5 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -1,7 +1,6 @@ -from typing import Optional +# User information TOKEN_TYPE: str = "" TOKEN: str = "" EMAIL: str = "" UUID: str = "" -CONFIGS: Optional[dict] = None From 90f135cd5335305062f642755fdb5b2fa7b77c3f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 19:35:58 +0000 Subject: [PATCH 039/230] mypy --- openbb_terminal/account/logout_model.py | 5 +++-- openbb_terminal/keys_model.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 4a4649bb23d..92bb2bbb4ac 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -36,8 +36,8 @@ def remove_login_file() -> Union[Success, Failure]: Returns ------- - Success - The status of the local logout. + Union[Success, Failure] + The status of the file removal, or an error. """ try: @@ -45,6 +45,7 @@ def remove_login_file() -> Union[Success, Failure]: if os.path.isfile(file_path): os.remove(file_path) return Success("[green]\nRemoved login info.[/green]") + return Success() except Exception: return Failure("[red]\nFailed to remove login file.[/red]") diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 3f49acb8cd0..40dd79e11a3 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -35,6 +35,7 @@ from openbb_terminal.rich_config import console from openbb_terminal.terminal_helper import suppress_stdout from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel +from openbb_terminal.account.sync_model import put_user_configs logger = logging.getLogger(__name__) @@ -237,6 +238,14 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non # Set cfg.env_var_name = env_var_value setattr(cfg, env_var_name, env_var_value) + # Just for testing, replace when patch is available, + # this overwrites the whole config dict + data = { + "features_settings": {}, + "features_keys": {env_var_name: env_var_value}, + } + put_user_configs(data) + def get_keys(show: bool = False) -> pd.DataFrame: """Get currently set API keys. From 3f69ddb55fd21a075794f618aaa5aa3624016c92 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 19:44:42 +0000 Subject: [PATCH 040/230] save if no internet --- openbb_terminal/account/login_controller.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 8d14ac8ffb9..3897b8df2cf 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -2,6 +2,7 @@ from typing import Tuple from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console import json +import requests from openbb_terminal import terminal_controller from openbb_terminal.account.login_model import ( get_login_info, @@ -70,11 +71,14 @@ def login(login_info: dict): """ load_user_info(login_info) response = fetch_user_configs(login_info) - if response.status_code == 200: - apply_configs(configs=json.loads(response.content)) - terminal_controller.parse_args_and_run() + if isinstance(response, requests.Response): + if response.status_code == 200: + apply_configs(configs=json.loads(response.content)) + terminal_controller.parse_args_and_run() + else: + login_prompt(welcome=False) else: - login_prompt(welcome=False) + login_prompt(welcome=True) def main(): From 57d7a42359f5f526d6d7334dae6d98648043b7bf Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 20 Jan 2023 19:50:06 +0000 Subject: [PATCH 041/230] pylint --- openbb_terminal/account/login_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/login_controller.py index 3897b8df2cf..3bd3743452e 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/login_controller.py @@ -1,8 +1,8 @@ from typing import Tuple -from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY -from openbb_terminal.rich_config import console import json import requests +from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY +from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller from openbb_terminal.account.login_model import ( get_login_info, From 9255b92710f312539277e7dd42dbeb3f72589d73 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 23 Jan 2023 14:03:22 +0000 Subject: [PATCH 042/230] logout everywhere --- openbb_terminal/account/logout_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py index 92bb2bbb4ac..5157e4a46f1 100644 --- a/openbb_terminal/account/logout_model.py +++ b/openbb_terminal/account/logout_model.py @@ -7,7 +7,7 @@ from openbb_terminal.account.statics import BASE_URL, Success, Failure from openbb_terminal.account import user -def remote_logout() -> Union[Success, Failure]: +def logout_everywhere() -> Union[Success, Failure]: """Request a remote logout. Returns From 1c72c89468f63fa8a179b655dcc25b08d56751cf Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 24 Jan 2023 14:30:13 +0000 Subject: [PATCH 043/230] breaking changes: move to local/hub models --- openbb_terminal/account/hub_model.py | 175 ++++++++++++++++++ openbb_terminal/account/local_model.py | 94 ++++++++++ openbb_terminal/account/login_model.py | 128 ------------- openbb_terminal/account/logout_model.py | 62 ------- ...in_controller.py => session_controller.py} | 44 +++-- openbb_terminal/account/statics.py | 15 -- openbb_terminal/account/sync_model.py | 77 -------- openbb_terminal/account/user.py | 42 ++++- openbb_terminal/keys_model.py | 2 +- openbb_terminal/parent_classes.py | 12 +- openbb_terminal/terminal_controller.py | 6 +- terminal.py | 4 +- 12 files changed, 344 insertions(+), 317 deletions(-) create mode 100644 openbb_terminal/account/hub_model.py create mode 100644 openbb_terminal/account/local_model.py delete mode 100644 openbb_terminal/account/login_model.py delete mode 100644 openbb_terminal/account/logout_model.py rename openbb_terminal/account/{login_controller.py => session_controller.py} (67%) delete mode 100644 openbb_terminal/account/statics.py delete mode 100644 openbb_terminal/account/sync_model.py diff --git a/openbb_terminal/account/hub_model.py b/openbb_terminal/account/hub_model.py new file mode 100644 index 00000000000..5765982568e --- /dev/null +++ b/openbb_terminal/account/hub_model.py @@ -0,0 +1,175 @@ +from typing import Optional +import requests +from openbb_terminal.account.user import User + +from openbb_terminal.rich_config import console + +BASE_URL = "http://127.0.0.1:8000/" + + +def create_session( + email: str, password: str, base_url=BASE_URL +) -> Optional[requests.Response]: + """Create a session. + + Parameters + ---------- + email : str + The email. + password : str + The password. + base_url : str, optional + The base url, by default BASE_URL + + Returns + ------- + Optional[requests.Response] + The response from the login request. + """ + try: + data = { + "email": email, + "password": password, + "remember": True, + } + return requests.post(base_url + "login", json=data) + except requests.exceptions.ConnectionError: + console.print("[red]\nConnection error.[/red]") + return None + except Exception: + console.print("[red]\nFailed to request login info.[/red]") + return None + + +def process_session_response(response: Optional[requests.Response]) -> dict: + """Process the response from the login request. + + Parameters + ---------- + response : Optional[requests.Response] + The response from the login request. + + Returns + ------- + dict + The login info. + """ + if response is None: + return {} + + if response.status_code == 200: + console.print("\nLogin successful.", style="green") + login = response.json() + return login + if response.status_code == 401: + console.print("[red]\nWrong credentials.[/red]") + return {} + if response.status_code == 403: + console.print("[red]\nUnverified email.[/red]") + return {} + console.print("[red]\nFailed to login.[/red]") + return {} + + +def get_session(email: str, password: str) -> dict: + """Get the session info. + + Parameters + ---------- + email : str + The email. + password : str + The password. + + Returns + ------- + dict + The session info. + """ + response = create_session(email, password) + return process_session_response(response) + + +def fetch_user_configs(session: dict) -> Optional[requests.Response]: + """Fetch user configurations.""" + + token_type = session.get("token_type", "") + token = session.get("access_token", "") + + try: + response = requests.get( + url=BASE_URL + "terminal/user", + headers={"Authorization": f"{token_type.title()} {token}"}, + ) + if response.status_code == 200: + console.print("[green]\nFetched user configurations.[/green]") + else: + console.print("[red]\nFailed to fetch configurations.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print("[red]\nConnection error.[/red]") + return None + except Exception: + console.print("[red]\nFailed to fetch configurations.[/red]") + return None + + +def patch_user_configs(data: dict) -> Optional[requests.Response]: + """Patch user configurations.""" + # When available on the server just need to send the changed configs, not all of them. + # Adapt this to Colin implementation. + pass + + +def put_user_configs(data: dict) -> bool: + """Push user configurations to the server.""" + try: + response = requests.put( + url=BASE_URL + "terminal/user", + headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, + json=data, + ) + if response.status_code == 200: + console.print("[green]\nPushed user configurations.[/green]") + else: + console.print("[red]\nFailed to push configurations.[/red]") + return True + except requests.exceptions.ConnectionError: + console.print("[red]\nConnection error.[/red]") + return False + except Exception: + console.print("[red]\nFailed to push configurations.[/red]") + return False + + +def logout_everywhere() -> bool: + """Request a remote logout. + + Returns + ------- + bool + The status of the logout. + """ + try: + if ( + requests.get( + url=BASE_URL + "logout-everywhere", + headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, + ).status_code + == 200 + ): + console.print("[green]\nLogged out remotely.[/green]") + return True + console.print("[red]\nFailed to logout remotely.[/red]") + return False + except requests.exceptions.ConnectionError: + console.print("[red]\nConnection error.[/red]") + return False + except Exception: + console.print("[red]\nFailed to logout remotely.[/red]") + return False + + +def delete_session(): + """Delete the session.""" + pass diff --git a/openbb_terminal/account/local_model.py b/openbb_terminal/account/local_model.py new file mode 100644 index 00000000000..56a4cdfda9b --- /dev/null +++ b/openbb_terminal/account/local_model.py @@ -0,0 +1,94 @@ +import os +from pathlib import Path +import json +from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY +from openbb_terminal.rich_config import console +from openbb_terminal import feature_flags as obbff +from openbb_terminal import config_terminal as cfg +from openbb_terminal.base_helpers import strtobool + +SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" + + +def save_session(data: dict, file_path: Path = SESSION_FILE_PATH): + """Save the login info to a file. + + Parameters + ---------- + data : dict + The data to write. + file_path : Path + The file path. + """ + try: + with open(file_path, "w") as file: + file.write(json.dumps(data)) + except Exception: + console.print("Failed to save session info.", style="red") + + +def get_session(file_path: Path = SESSION_FILE_PATH) -> dict: + """Get the session info from the file. + + Parameters + ---------- + file_path : Path + The file path. + + Returns + ------- + dict + The session info. + """ + try: + if os.path.isfile(file_path): + with open(file_path) as file: + return json.load(file) + except Exception: + console.print("[red]\nFailed to get login info.[/red]") + return {} + + +def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: + """Remove the session file. + + Parameters + ---------- + file_path : Path + The file path. + + Returns + ------- + bool + The status of the removal. + """ + + try: + if os.path.isfile(file_path): + os.remove(file_path) + return True + return True + except Exception: + console.print("[red]\nFailed to remove login file.[/red]") + return False + + +def apply_configs(configs: dict): + """Apply configurations.""" + console.print(configs, style="red") + if configs: + settings = configs.get("features_settings", {}) + for k, v in settings.items(): + if hasattr(obbff, k): + if isinstance(getattr(obbff, k), int): + setattr(obbff, k, strtobool(v)) + else: + setattr(obbff, k, v) + + keys = configs.get("features_keys", {}) + for k, v in keys.items(): + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) diff --git a/openbb_terminal/account/login_model.py b/openbb_terminal/account/login_model.py deleted file mode 100644 index 074c20b616e..00000000000 --- a/openbb_terminal/account/login_model.py +++ /dev/null @@ -1,128 +0,0 @@ -from pathlib import Path -import os.path -import json -from typing import Union -import jwt -import requests -from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY -from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags as obbff -from openbb_terminal.account.statics import BASE_URL, Failure -from openbb_terminal.account import user - - -def load_user_info(login_info: dict): - """Load user info from login info. - - Parameters - ---------- - login_info : dict - The login info. - """ - user.TOKEN_TYPE = login_info.get("token_type", "") - user.TOKEN = login_info.get("access_token", "") - user.UUID = login_info.get("uuid", "") - - if user.TOKEN: - decoded_info = jwt.decode(user.TOKEN, options={"verify_signature": False}) - user.EMAIL = decoded_info.get("sub", "") - - if obbff.USE_FLAIR == ":openbb": - username = user.EMAIL[: user.EMAIL.find("@")] - setattr(obbff, "USE_FLAIR", "[" + username + "] 🦋") - - -def save_login_info(data: dict, file_path: Path): - """Save the login info to a file. - - Parameters - ---------- - data : dict - The data to write. - file_path : Path - The file path. - """ - try: - with open(file_path, "w") as file: - file.write(json.dumps(data)) - except Exception: - console.print("Failed to save login info.", style="red") - - -def process_response(response: requests.Response, save: bool) -> Union[dict, Failure]: - """Process the response from the server. - - Parameters - ---------- - response : requests.Response - The response from the server. - save : bool - Save login info. - - Returns - ------- - Union[dict, Failure] - The login info, or an error. - """ - if response.status_code == 200: - console.print("\nLogin successful.", style="green") - login = response.json() - if save: - save_login_info(login, SETTINGS_DIRECTORY / "login.json") - return login - if response.status_code == 401: - return Failure("[red]\nWrong credentials.[/red]") - if response.status_code == 403: - return Failure("[red]\nUnverified email.[/red]") - return Failure("[red]\nFailed to login.[/red]") - - -def request_login_info(email: str, password: str, save: bool) -> Union[dict, Failure]: - """Request login info from the server. - - Parameters - ---------- - email : str - The user email. - password : str - The user password. - save : bool - Save login info. - - Returns - ------- - Union[dict, Failure] - The login info, or an error. - """ - data = { - "email": email, - "password": password, - "remember": True, - } - - try: - response = requests.post(BASE_URL + "login", json=data) - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to request login info.[/red]") - return process_response(response, save) - - -def get_login_info() -> dict: - """Get the login info from the file. - - Returns - ------- - dict - The login info. - """ - file_path = SETTINGS_DIRECTORY / "login.json" - - try: - if os.path.isfile(file_path): - with open(file_path) as file: - return json.load(file) - except Exception: - console.print("[red]\nFailed to get login info.[/red]") - return {} diff --git a/openbb_terminal/account/logout_model.py b/openbb_terminal/account/logout_model.py deleted file mode 100644 index 5157e4a46f1..00000000000 --- a/openbb_terminal/account/logout_model.py +++ /dev/null @@ -1,62 +0,0 @@ -import os.path -from typing import Union -import requests -from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY -from openbb_terminal.helper_funcs import system_clear -from openbb_terminal.account.statics import BASE_URL, Success, Failure -from openbb_terminal.account import user - - -def logout_everywhere() -> Union[Success, Failure]: - """Request a remote logout. - - Returns - ------- - Union[Success, Failure] - The status of the token invalidation, or an error. - """ - try: - if ( - requests.get( - url=BASE_URL + "logout-everywhere", - headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, - ).status_code - == 200 - ): - return Success("[green]\nLogged out remotely.[/green]") - return Failure("[red]\nFailed to logout remotely.[/red]") - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to logout remotely.[/red]") - - -def remove_login_file() -> Union[Success, Failure]: - """Remove the login file. - - Returns - ------- - Union[Success, Failure] - The status of the file removal, or an error. - """ - - try: - file_path = SETTINGS_DIRECTORY / "login.json" - if os.path.isfile(file_path): - os.remove(file_path) - return Success("[green]\nRemoved login info.[/green]") - return Success() - except Exception: - return Failure("[red]\nFailed to remove login file.[/red]") - - -def logout(): - """Logout the user. - - Returns - ------- - Union[Success, Failure] - The status of the logout, or an error. - """ - system_clear() - remove_login_file() diff --git a/openbb_terminal/account/login_controller.py b/openbb_terminal/account/session_controller.py similarity index 67% rename from openbb_terminal/account/login_controller.py rename to openbb_terminal/account/session_controller.py index 3bd3743452e..c92a5224b16 100644 --- a/openbb_terminal/account/login_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -1,15 +1,12 @@ from typing import Tuple import json import requests +import openbb_terminal.account.local_model as Local +import openbb_terminal.account.hub_model as Hub +from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller -from openbb_terminal.account.login_model import ( - get_login_info, - load_user_info, - request_login_info, -) -from openbb_terminal.account.sync_model import fetch_user_configs, apply_configs def display_welcome_message(): @@ -41,6 +38,15 @@ def get_user_input() -> Tuple[str, str, bool]: return email, password, save +def create_session(email: str, password: str, save: bool) -> dict: + """Create a session.""" + + session = Hub.get_session(email, password) + if session and save: + Local.save_session(session) + return session + + def login_prompt(welcome=True): """Login prompt and launch terminal if login is successful. @@ -54,26 +60,26 @@ def login_prompt(welcome=True): while True: email, password, save = get_user_input() - login_info = request_login_info(email, password, save) - if isinstance(login_info, dict) and login_info: + session = create_session(email, password, save) + if isinstance(session, dict) and session: break - login(login_info) + login(session=session) -def login(login_info: dict): +def login(session: dict): """Login and launch terminal. Parameters ---------- - login_info : dict - The login information. + session : dict + The session info. """ - load_user_info(login_info) - response = fetch_user_configs(login_info) - if isinstance(response, requests.Response): + User.load_user_info(session) + response = Hub.fetch_user_configs(session) + if response: if response.status_code == 200: - apply_configs(configs=json.loads(response.content)) + Local.apply_configs(configs=json.loads(response.content)) terminal_controller.parse_args_and_run() else: login_prompt(welcome=False) @@ -83,11 +89,11 @@ def login(login_info: dict): def main(): """Main function""" - login_info = get_login_info() - if not login_info: + local_session = Local.get_session() + if not local_session: login_prompt() else: - login(login_info) + login(session=local_session) if __name__ == "__main__": diff --git a/openbb_terminal/account/statics.py b/openbb_terminal/account/statics.py deleted file mode 100644 index 983f8686c1e..00000000000 --- a/openbb_terminal/account/statics.py +++ /dev/null @@ -1,15 +0,0 @@ -from openbb_terminal.rich_config import console - -BASE_URL = "http://127.0.0.1:8000/" - - -class Success: - def __init__(self, msg=""): - self.msg = msg - console.print(msg) - - -class Failure: - def __init__(self, msg=""): - self.msg = msg - console.print(msg) diff --git a/openbb_terminal/account/sync_model.py b/openbb_terminal/account/sync_model.py deleted file mode 100644 index f0603efefdf..00000000000 --- a/openbb_terminal/account/sync_model.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Union -import requests -from openbb_terminal.base_helpers import strtobool -from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags as obbff -from openbb_terminal.account.statics import BASE_URL, Failure -from openbb_terminal.account import user -from openbb_terminal import config_terminal as cfg - - -def fetch_user_configs(login_info: dict) -> Union[requests.Response, Failure]: - """Fetch user configurations.""" - - token_type = login_info.get("token_type", "") - token = login_info.get("access_token", "") - - try: - response = requests.get( - url=BASE_URL + "terminal/user", - headers={"Authorization": f"{token_type.title()} {token}"}, - ) - if response.status_code == 200: - console.print("[green]\nFetched user configurations.[/green]") - else: - console.print("[red]\nFailed to fetch configurations.[/red]") - return response - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to fetch configurations.[/red]") - - -def apply_configs(configs: dict): - """Apply configurations.""" - console.print(configs, style="red") - if configs: - settings = configs.get("features_settings", {}) - for k, v in settings.items(): - if hasattr(obbff, k): - if isinstance(getattr(obbff, k), int): - setattr(obbff, k, strtobool(v)) - else: - setattr(obbff, k, v) - - keys = configs.get("features_keys", {}) - for k, v in keys.items(): - if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) - - -def patch_user_configs(data: dict) -> Union[requests.Response, Failure]: - """Patch user configurations.""" - # When available on the server just need to send the changed configs, not all of them. - # Adapt this to Colin implementation. - pass - - -def put_user_configs(data: dict) -> Union[int, Failure]: - """Push user configurations to the server.""" - try: - response = requests.put( - url=BASE_URL + "terminal/user", - headers={"Authorization": f"{user.TOKEN_TYPE.title()} {user.TOKEN}"}, - json=data, - ) - if response.status_code == 200: - console.print("[green]\nPushed user configurations.[/green]") - else: - console.print("[red]\nFailed to push configurations.[/red]") - return response.status_code - except requests.exceptions.ConnectionError: - return Failure("[red]\nConnection error.[/red]") - except Exception: - return Failure("[red]\nFailed to push configurations.[/red]") diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 3ca7f8fa4b5..2ff0ef9dc93 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -1,6 +1,38 @@ -# User information +from dataclasses import dataclass +import jwt +import openbb_terminal.feature_flags as obbff +from openbb_terminal.rich_config import console -TOKEN_TYPE: str = "" -TOKEN: str = "" -EMAIL: str = "" -UUID: str = "" + +@dataclass +class User: + TOKEN_TYPE: str = "" + TOKEN: str = "" + EMAIL: str = "" + UUID: str = "" + + @classmethod + def load_user_info(cls, session: dict): + """Load user info from login info. + + Parameters + ---------- + session : dict + The login info. + """ + User.TOKEN_TYPE = session.get("token_type", "") + User.TOKEN = session.get("access_token", "") + User.UUID = session.get("uuid", "") + + if User.TOKEN: + decoded_info = jwt.decode(User.TOKEN, options={"verify_signature": False}) + User.EMAIL = decoded_info.get("sub", "") + + if obbff.USE_FLAIR == ":openbb": + username = User.EMAIL[: User.EMAIL.find("@")] + setattr(obbff, "USE_FLAIR", "[" + username + "] 🦋") + + @classmethod + def whoami(cls): + console.print(f"[info]email:[/info] {User.EMAIL}") + console.print(f"[info]uuid:[/info] {User.UUID}\n") diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 40dd79e11a3..e3d29befdd0 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -35,7 +35,7 @@ from openbb_terminal.rich_config import console from openbb_terminal.terminal_helper import suppress_stdout from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel -from openbb_terminal.account.sync_model import put_user_configs +from openbb_terminal.account.hub_model import put_user_configs logger = logging.getLogger(__name__) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index a3b61a8143b..6e92e58fb70 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -21,8 +21,9 @@ import matplotlib.pyplot as plt from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.account.logout_model import logout -from openbb_terminal.account import user +import openbb_terminal.account.local_model as Local +import openbb_terminal.account.hub_model as Hub +from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import ( USER_CUSTOM_IMPORTS_DIRECTORY, @@ -675,7 +676,9 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - logout() + system_clear() + Hub.delete_session() + Local.remove_session_file() plt.close("all") out = subprocess.run( # nosec f"{sys.executable} terminal.py", shell=True, check=False @@ -695,8 +698,7 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - console.print(f"[info]email:[/info] {user.EMAIL}") - console.print(f"[info]uuid:[/info] {user.UUID}\n") + User.whoami() @staticmethod def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]): diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 15e94ce79e3..57d54adb53e 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -901,15 +901,15 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): try: # Process the input command t_controller.queue = t_controller.switch(an_input) - if an_input == "logout": - break if an_input in ("q", "quit", "..", "exit", "e"): print_goodbye() break # Check if the user wants to reset application - if an_input in ("r", "reset") or t_controller.update_success: + # TODO: exit cmd after logout and reentering the terminal is not working + # HELP: login -> logout -> login -> exit should leave the terminal, but it doesn't + if an_input in ("r", "reset", "logout") or t_controller.update_success: ret_code = reset(t_controller.queue if t_controller.queue else []) if ret_code != 0: print_goodbye() diff --git a/terminal.py b/terminal.py index 7c7b1089aaa..984ccb2a759 100644 --- a/terminal.py +++ b/terminal.py @@ -2,7 +2,7 @@ import multiprocessing import sys from openbb_terminal import terminal_controller from openbb_terminal.core.integration_tests import integration_controller -from openbb_terminal.account import login_controller +from openbb_terminal.account import session_controller from openbb_terminal.terminal_helper import is_packaged_application @@ -14,6 +14,6 @@ if __name__ == "__main__": elif ( is_packaged_application() or True ): # remove True to force login just on installer - login_controller.main() + session_controller.main() else: terminal_controller.parse_args_and_run() From 4bd07544d09b1391c319de5d07e4a353f3b38b22 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 24 Jan 2023 14:37:20 +0000 Subject: [PATCH 044/230] unused import --- openbb_terminal/account/session_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index c92a5224b16..30b4853442e 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -1,6 +1,5 @@ from typing import Tuple import json -import requests import openbb_terminal.account.local_model as Local import openbb_terminal.account.hub_model as Hub from openbb_terminal.account.user import User From 74991425845e92f312f3de91a5dbd519b117ce05 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 24 Jan 2023 18:30:04 +0000 Subject: [PATCH 045/230] fix logout bug --- openbb_terminal/account/session_controller.py | 23 +++++++++++++------ openbb_terminal/parent_classes.py | 19 +++++---------- openbb_terminal/terminal_controller.py | 9 +++++++- terminal.py | 7 +----- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index 30b4853442e..c70ae6ecce6 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -4,6 +4,8 @@ import openbb_terminal.account.local_model as Local import openbb_terminal.account.hub_model as Hub from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY +from openbb_terminal.helper_funcs import system_clear +import matplotlib.pyplot as plt from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller @@ -26,10 +28,7 @@ def get_user_input() -> Tuple[str, str, bool]: email = console.input("> Email: ", style="blue") password = console.getpass("> Password: ", style="blue") - save_str = "" - while save_str not in ["y", "n"]: - save_str = console.input("> Keep me logged in (y/n): ", style="blue").lower() - + save_str = console.input("> Keep me logged in (y/n): ", style="blue").lower() save = False if save_str == "y": save = True @@ -46,7 +45,7 @@ def create_session(email: str, password: str, save: bool) -> dict: return session -def login_prompt(welcome=True): +def login_prompt(welcome=True, guest_allowed=True): """Login prompt and launch terminal if login is successful. Parameters @@ -59,6 +58,8 @@ def login_prompt(welcome=True): while True: email, password, save = get_user_input() + if not email and not password and guest_allowed: + return terminal_controller.parse_args_and_run() session = create_session(email, password, save) if isinstance(session, dict) and session: break @@ -86,11 +87,19 @@ def login(session: dict): login_prompt(welcome=True) -def main(): +def logout(): + """Logout and clear session.""" + system_clear() + Hub.delete_session() + Local.remove_session_file() + plt.close("all") + + +def main(guest_allowed: bool = True): """Main function""" local_session = Local.get_session() if not local_session: - login_prompt() + login_prompt(guest_allowed) else: login(session=local_session) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 6e92e58fb70..9bbef27478a 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -11,8 +11,6 @@ import os import re from abc import ABCMeta, abstractmethod from datetime import datetime, timedelta -import subprocess -import sys from typing import Any, Dict, List, Union import numpy as np @@ -21,8 +19,7 @@ import matplotlib.pyplot as plt from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -import openbb_terminal.account.local_model as Local -import openbb_terminal.account.hub_model as Hub +from openbb_terminal.account.session_controller import logout from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import ( @@ -676,15 +673,11 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - system_clear() - Hub.delete_session() - Local.remove_session_file() - plt.close("all") - out = subprocess.run( # nosec - f"{sys.executable} terminal.py", shell=True, check=False - ) - if out.returncode != 0: - raise Exception("Error while logging out.") + i = console.input("Are you sure you want to logout? (y/n) ", style="info") + console.print("") + if i.lower() == "y": + self.queue.insert(0, "y") + logout() @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 57d54adb53e..b53294cfb49 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -24,6 +24,7 @@ from prompt_toolkit.formatted_text import HTML import pandas as pd from openbb_terminal import feature_flags as obbff from openbb_terminal.terminal_helper import is_packaged_application +from openbb_terminal.account import session_controller from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, @@ -909,12 +910,15 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): # Check if the user wants to reset application # TODO: exit cmd after logout and reentering the terminal is not working # HELP: login -> logout -> login -> exit should leave the terminal, but it doesn't - if an_input in ("r", "reset", "logout") or t_controller.update_success: + if an_input in ("r", "reset") or t_controller.update_success: ret_code = reset(t_controller.queue if t_controller.queue else []) if ret_code != 0: print_goodbye() break + if an_input == "logout" and "y" in t_controller.queue: + break + except SystemExit: logger.exception( "The command '%s' doesn't exist on the / menu.", @@ -946,6 +950,9 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): console.print(f"[green]Replacing by '{an_input}'.[/green]") t_controller.queue.insert(0, an_input) + if an_input == "logout": + return session_controller.main(guest_allowed=not is_packaged_application()) + def insert_start_slash(cmds: List[str]) -> List[str]: """Insert a slash at the beginning of a command sequence.""" diff --git a/terminal.py b/terminal.py index 984ccb2a759..bfb9e3c31a0 100644 --- a/terminal.py +++ b/terminal.py @@ -1,6 +1,5 @@ import multiprocessing import sys -from openbb_terminal import terminal_controller from openbb_terminal.core.integration_tests import integration_controller from openbb_terminal.account import session_controller from openbb_terminal.terminal_helper import is_packaged_application @@ -11,9 +10,5 @@ if __name__ == "__main__": sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: integration_controller.main() - elif ( - is_packaged_application() or True - ): # remove True to force login just on installer - session_controller.main() else: - terminal_controller.parse_args_and_run() + session_controller.main(guest_allowed=not is_packaged_application()) From df12a4b592bdf31949b2c5261703933c77d1dba1 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 24 Jan 2023 20:19:44 +0000 Subject: [PATCH 046/230] whoami in guest mode --- openbb_terminal/account/user.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 2ff0ef9dc93..0a1c3d78628 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -34,5 +34,9 @@ class User: @classmethod def whoami(cls): - console.print(f"[info]email:[/info] {User.EMAIL}") - console.print(f"[info]uuid:[/info] {User.UUID}\n") + """Display user info.""" + if User.UUID: + console.print(f"[info]email:[/info] {User.EMAIL}") + console.print(f"[info]uuid:[/info] {User.UUID}\n") + else: + console.print("[info]Only you know...[/info]\n") From 99f1bd8a6d005b6c38d34de8da0ecec1dcf7f229 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 24 Jan 2023 21:00:27 +0000 Subject: [PATCH 047/230] add todo --- openbb_terminal/parent_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 9bbef27478a..3c1dc013c04 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -677,6 +677,7 @@ class BaseController(metaclass=ABCMeta): console.print("") if i.lower() == "y": self.queue.insert(0, "y") + # TODO: this command is not working properly inside menus logout() @log_start_end(log=logger) From 6a699230a0ed0664ef32b05662c7807b2f956de3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 09:58:25 +0000 Subject: [PATCH 048/230] small bug guest allowed --- openbb_terminal/account/session_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index c70ae6ecce6..06cc0e60973 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -99,7 +99,7 @@ def main(guest_allowed: bool = True): """Main function""" local_session = Local.get_session() if not local_session: - login_prompt(guest_allowed) + login_prompt(guest_allowed=guest_allowed) else: login(session=local_session) From f3c8ca14563a99b6160d7e8ecc3899b507b2cc9c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 10:05:42 +0000 Subject: [PATCH 049/230] limit flair length --- openbb_terminal/account/session_controller.py | 2 +- openbb_terminal/account/user.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index 06cc0e60973..153f27ab0a5 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -1,11 +1,11 @@ from typing import Tuple import json +import matplotlib.pyplot as plt import openbb_terminal.account.local_model as Local import openbb_terminal.account.hub_model as Hub from openbb_terminal.account.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.helper_funcs import system_clear -import matplotlib.pyplot as plt from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 0a1c3d78628..9b641cce988 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -28,9 +28,11 @@ class User: decoded_info = jwt.decode(User.TOKEN, options={"verify_signature": False}) User.EMAIL = decoded_info.get("sub", "") + MAX_FLAIR_LEN = 20 + if obbff.USE_FLAIR == ":openbb": username = User.EMAIL[: User.EMAIL.find("@")] - setattr(obbff, "USE_FLAIR", "[" + username + "] 🦋") + setattr(obbff, "USE_FLAIR", "[" + username[:MAX_FLAIR_LEN] + "] 🦋") @classmethod def whoami(cls): From 9f2b3a52b85a9870a4a17733d76659e7cbf4b243 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 10:21:56 +0000 Subject: [PATCH 050/230] fix bug on user pull --- openbb_terminal/account/local_model.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/openbb_terminal/account/local_model.py b/openbb_terminal/account/local_model.py index 56a4cdfda9b..c47d9bc0644 100644 --- a/openbb_terminal/account/local_model.py +++ b/openbb_terminal/account/local_model.py @@ -78,17 +78,19 @@ def apply_configs(configs: dict): console.print(configs, style="red") if configs: settings = configs.get("features_settings", {}) - for k, v in settings.items(): - if hasattr(obbff, k): - if isinstance(getattr(obbff, k), int): - setattr(obbff, k, strtobool(v)) - else: - setattr(obbff, k, v) + if settings: + for k, v in settings.items(): + if hasattr(obbff, k): + if isinstance(getattr(obbff, k), int): + setattr(obbff, k, strtobool(v)) + else: + setattr(obbff, k, v) keys = configs.get("features_keys", {}) - for k, v in keys.items(): - if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) + if keys: + for k, v in keys.items(): + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) From e86163d2d8fd01113e1decb9937223365b91d94c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 10:50:36 +0000 Subject: [PATCH 051/230] logout working --- openbb_terminal/parent_classes.py | 26 +++++++++++++------------- openbb_terminal/terminal_controller.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 4ad8cb0faa9..f4f5b2da319 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -274,13 +274,14 @@ class BaseController(metaclass=ABCMeta): def log_queue(self) -> None: """Log command queue.""" - joined_queue = self.COMMAND_SEPARATOR.join(self.queue) - if self.queue and not self.contains_keys(joined_queue): - logger.info( - "QUEUE: {'path': '%s', 'queue': '%s'}", - self.PATH, - joined_queue, - ) + if self.queue: + joined_queue = self.COMMAND_SEPARATOR.join(self.queue) + if not self.contains_keys(joined_queue): + logger.info( + "QUEUE: {'path': '%s', 'queue': '%s'}", + self.PATH, + joined_queue, + ) def log_cmd_and_queue( self, known_cmd: str, other_args_str: str, the_input: str @@ -673,12 +674,7 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - i = console.input("Are you sure you want to logout? (y/n) ", style="info") - console.print("") - if i.lower() == "y": - self.queue.insert(0, "y") - # TODO: this command is not working properly inside menus - logout() + logout() @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: @@ -942,6 +938,10 @@ class BaseController(metaclass=ABCMeta): # Process the input command self.queue = self.switch(an_input) + + if an_input == "logout": + return ["logout"] + if not self.queue or ( self.queue and self.queue[0] not in ("quit", "help") ): diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index b53294cfb49..c0c630232b5 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -916,7 +916,7 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): print_goodbye() break - if an_input == "logout" and "y" in t_controller.queue: + if an_input == "logout": break except SystemExit: From 56bc55e1207190f71f526e8a956456ae89fbe9b8 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 11:18:13 +0000 Subject: [PATCH 052/230] handle no connection --- openbb_terminal/account/hub_model.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openbb_terminal/account/hub_model.py b/openbb_terminal/account/hub_model.py index 5765982568e..7b75fecd7d8 100644 --- a/openbb_terminal/account/hub_model.py +++ b/openbb_terminal/account/hub_model.py @@ -41,12 +41,12 @@ def create_session( return None -def process_session_response(response: Optional[requests.Response]) -> dict: +def process_session_response(response: requests.Response) -> dict: """Process the response from the login request. Parameters ---------- - response : Optional[requests.Response] + response : requests.Response The response from the login request. Returns @@ -54,9 +54,6 @@ def process_session_response(response: Optional[requests.Response]) -> dict: dict The login info. """ - if response is None: - return {} - if response.status_code == 200: console.print("\nLogin successful.", style="green") login = response.json() @@ -87,6 +84,8 @@ def get_session(email: str, password: str) -> dict: The session info. """ response = create_session(email, password) + if response is None: + return {} return process_session_response(response) From 853820773be7f433b66ce056274e90e9670c611e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 11:18:42 +0000 Subject: [PATCH 053/230] logout user when logout --- openbb_terminal/account/session_controller.py | 1 + openbb_terminal/account/user.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index 153f27ab0a5..fcdc4c30e37 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -90,6 +90,7 @@ def login(session: dict): def logout(): """Logout and clear session.""" system_clear() + User.logout() Hub.delete_session() Local.remove_session_file() plt.close("all") diff --git a/openbb_terminal/account/user.py b/openbb_terminal/account/user.py index 9b641cce988..61bddabbaa3 100644 --- a/openbb_terminal/account/user.py +++ b/openbb_terminal/account/user.py @@ -42,3 +42,12 @@ class User: console.print(f"[info]uuid:[/info] {User.UUID}\n") else: console.print("[info]Only you know...[/info]\n") + + @classmethod + def logout(cls): + """Logout.""" + User.TOKEN_TYPE = "" + User.TOKEN = "" + User.EMAIL = "" + User.UUID = "" + obbff.USE_FLAIR = ":openbb" From f08de5df6cbff788ae613d8da9a506f112673e34 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:17:59 +0000 Subject: [PATCH 054/230] remove keys push --- openbb_terminal/keys_model.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index e3d29befdd0..c5a7a35fcf0 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -238,14 +238,6 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non # Set cfg.env_var_name = env_var_value setattr(cfg, env_var_name, env_var_value) - # Just for testing, replace when patch is available, - # this overwrites the whole config dict - data = { - "features_settings": {}, - "features_keys": {env_var_name: env_var_value}, - } - put_user_configs(data) - def get_keys(show: bool = False) -> pd.DataFrame: """Get currently set API keys. From 0161a09eb29e279aacb7c14fc3a519fe0c30a088 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:18:16 +0000 Subject: [PATCH 055/230] split logger funcs --- openbb_terminal/account/session_controller.py | 1 - .../core/log/generation/settings_logger.py | 33 ++++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/account/session_controller.py index fcdc4c30e37..3c50bc2d63f 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/account/session_controller.py @@ -27,7 +27,6 @@ def get_user_input() -> Tuple[str, str, bool]: console.print("\nPlease enter your credentials:", style="info") email = console.input("> Email: ", style="blue") password = console.getpass("> Password: ", style="blue") - save_str = console.input("> Keep me logged in (y/n): ", style="blue").lower() save = False if save_str == "y": diff --git a/openbb_terminal/core/log/generation/settings_logger.py b/openbb_terminal/core/log/generation/settings_logger.py index 46221e64114..5cb18918023 100644 --- a/openbb_terminal/core/log/generation/settings_logger.py +++ b/openbb_terminal/core/log/generation/settings_logger.py @@ -65,9 +65,7 @@ def log_settings() -> None: logger.info("SETTINGS: %s ", json.dumps(settings_dict)) -def log_config_terminal() -> None: - """Log config_terminal""" - +def collect_config_terminal() -> dict: config_terminal_dict = {} for item in dir(cfg): @@ -80,12 +78,17 @@ def log_config_terminal() -> None: ): config_terminal_dict[item] = str(prop) + return config_terminal_dict + + +def log_config_terminal() -> None: + """Log config_terminal""" + + config_terminal_dict = collect_config_terminal() logger.info("CONFIG_TERMINAL: %s ", json.dumps(config_terminal_dict)) -def log_feature_flags() -> None: - """Log feature flags""" - +def collect_feature_flags() -> dict: feature_flags_dict = {} for item in dir(obbff): @@ -97,12 +100,17 @@ def log_feature_flags() -> None: ): feature_flags_dict[item] = str(prop) + return feature_flags_dict + + +def log_feature_flags() -> None: + """Log feature flags""" + + feature_flags_dict = collect_feature_flags() logger.info("FEATURE_FLAGS: %s ", json.dumps(feature_flags_dict)) -def log_keys() -> None: - """Log keys""" - +def collect_keys() -> dict: var_list = [v for v in dir(cfg) if v.startswith("API_")] current_keys = {} @@ -116,6 +124,13 @@ def log_keys() -> None: else: current_keys[cfg_var_name] = "not_defined" + return current_keys + + +def log_keys() -> None: + """Log keys""" + + current_keys = collect_keys() logger.info("KEYS: %s ", json.dumps(current_keys)) From f697e6d9748f77c8bd0f5c2f148bc4a2d4ba2843 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:26:28 +0000 Subject: [PATCH 056/230] commant apply configs for now --- openbb_terminal/account/local_model.py | 37 +++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/openbb_terminal/account/local_model.py b/openbb_terminal/account/local_model.py index c47d9bc0644..8cac30e1192 100644 --- a/openbb_terminal/account/local_model.py +++ b/openbb_terminal/account/local_model.py @@ -75,22 +75,23 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: def apply_configs(configs: dict): """Apply configurations.""" - console.print(configs, style="red") - if configs: - settings = configs.get("features_settings", {}) - if settings: - for k, v in settings.items(): - if hasattr(obbff, k): - if isinstance(getattr(obbff, k), int): - setattr(obbff, k, strtobool(v)) - else: - setattr(obbff, k, v) + console.print(f"Will apply this: {configs}", style="red") + # TODO: Apply configs when uploading is implemented + # if configs: + # settings = configs.get("features_settings", {}) + # if settings: + # for k, v in settings.items(): + # if hasattr(obbff, k): + # if isinstance(getattr(obbff, k), int): + # setattr(obbff, k, strtobool(v)) + # else: + # setattr(obbff, k, v) - keys = configs.get("features_keys", {}) - if keys: - for k, v in keys.items(): - if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) + # keys = configs.get("features_keys", {}) + # if keys: + # for k, v in keys.items(): + # if hasattr(cfg, k): + # if isinstance(getattr(cfg, k), int): + # setattr(cfg, k, strtobool(v)) + # else: + # setattr(cfg, k, v) From 8dae59477a01e6a7a30b6240a6ee267b19fb970b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:32:55 +0000 Subject: [PATCH 057/230] account to session --- openbb_terminal/keys_model.py | 1 - openbb_terminal/parent_classes.py | 4 ++-- openbb_terminal/{account => session}/banner.txt | 0 openbb_terminal/{account => session}/hub_model.py | 2 +- openbb_terminal/{account => session}/local_model.py | 7 ++++--- .../{account => session}/session_controller.py | 8 ++++---- openbb_terminal/{account => session}/user.py | 0 openbb_terminal/terminal_controller.py | 2 +- terminal.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) rename openbb_terminal/{account => session}/banner.txt (100%) rename openbb_terminal/{account => session}/hub_model.py (99%) rename openbb_terminal/{account => session}/local_model.py (93%) rename openbb_terminal/{account => session}/session_controller.py (92%) rename openbb_terminal/{account => session}/user.py (100%) diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index c5a7a35fcf0..3f49acb8cd0 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -35,7 +35,6 @@ from openbb_terminal.rich_config import console from openbb_terminal.terminal_helper import suppress_stdout from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel -from openbb_terminal.account.hub_model import put_user_configs logger = logging.getLogger(__name__) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index f4f5b2da319..4883158662f 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -19,8 +19,8 @@ import matplotlib.pyplot as plt from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.account.session_controller import logout -from openbb_terminal.account.user import User +from openbb_terminal.session.session_controller import logout +from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import ( USER_CUSTOM_IMPORTS_DIRECTORY, diff --git a/openbb_terminal/account/banner.txt b/openbb_terminal/session/banner.txt similarity index 100% rename from openbb_terminal/account/banner.txt rename to openbb_terminal/session/banner.txt diff --git a/openbb_terminal/account/hub_model.py b/openbb_terminal/session/hub_model.py similarity index 99% rename from openbb_terminal/account/hub_model.py rename to openbb_terminal/session/hub_model.py index 7b75fecd7d8..b7d3a489f78 100644 --- a/openbb_terminal/account/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -1,6 +1,6 @@ from typing import Optional import requests -from openbb_terminal.account.user import User +from openbb_terminal.session.user import User from openbb_terminal.rich_config import console diff --git a/openbb_terminal/account/local_model.py b/openbb_terminal/session/local_model.py similarity index 93% rename from openbb_terminal/account/local_model.py rename to openbb_terminal/session/local_model.py index 8cac30e1192..1771515d15b 100644 --- a/openbb_terminal/account/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -3,9 +3,10 @@ from pathlib import Path import json from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags as obbff -from openbb_terminal import config_terminal as cfg -from openbb_terminal.base_helpers import strtobool + +# from openbb_terminal import feature_flags as obbff +# from openbb_terminal import config_terminal as cfg +# from openbb_terminal.base_helpers import strtobool SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" diff --git a/openbb_terminal/account/session_controller.py b/openbb_terminal/session/session_controller.py similarity index 92% rename from openbb_terminal/account/session_controller.py rename to openbb_terminal/session/session_controller.py index 3c50bc2d63f..923e78cda18 100644 --- a/openbb_terminal/account/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,9 +1,9 @@ from typing import Tuple import json import matplotlib.pyplot as plt -import openbb_terminal.account.local_model as Local -import openbb_terminal.account.hub_model as Hub -from openbb_terminal.account.user import User +import openbb_terminal.session.local_model as Local +import openbb_terminal.session.hub_model as Hub +from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console @@ -12,7 +12,7 @@ from openbb_terminal import terminal_controller def display_welcome_message(): """Display welcome message""" - with open(PACKAGE_DIRECTORY / "account" / "banner.txt") as f: + with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: console.print(f.read(), style="blue") diff --git a/openbb_terminal/account/user.py b/openbb_terminal/session/user.py similarity index 100% rename from openbb_terminal/account/user.py rename to openbb_terminal/session/user.py diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index c0c630232b5..b6d9f1312fe 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -24,7 +24,7 @@ from prompt_toolkit.formatted_text import HTML import pandas as pd from openbb_terminal import feature_flags as obbff from openbb_terminal.terminal_helper import is_packaged_application -from openbb_terminal.account import session_controller +from openbb_terminal.session import session_controller from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, diff --git a/terminal.py b/terminal.py index bfb9e3c31a0..234c7089ae3 100644 --- a/terminal.py +++ b/terminal.py @@ -1,7 +1,7 @@ import multiprocessing import sys from openbb_terminal.core.integration_tests import integration_controller -from openbb_terminal.account import session_controller +from openbb_terminal.session import session_controller from openbb_terminal.terminal_helper import is_packaged_application From ce9780e4afc6ac649f39ff2f0b73c46b8940decc Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:35:23 +0000 Subject: [PATCH 058/230] rename user --- openbb_terminal/session/session_controller.py | 2 +- openbb_terminal/session/user.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 923e78cda18..b7b5eacff70 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -89,7 +89,7 @@ def login(session: dict): def logout(): """Logout and clear session.""" system_clear() - User.logout() + User.clean() Hub.delete_session() Local.remove_session_file() plt.close("all") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 61bddabbaa3..b53e55fa463 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -44,8 +44,8 @@ class User: console.print("[info]Only you know...[/info]\n") @classmethod - def logout(cls): - """Logout.""" + def clean(cls): + """Clean user info.""" User.TOKEN_TYPE = "" User.TOKEN = "" User.EMAIL = "" From c07e827ee2068e01d8884fa9cd12dc74c1872e41 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 12:39:40 +0000 Subject: [PATCH 059/230] clear sounds better --- openbb_terminal/session/session_controller.py | 2 +- openbb_terminal/session/user.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index b7b5eacff70..00baf0b4d3a 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -89,7 +89,7 @@ def login(session: dict): def logout(): """Logout and clear session.""" system_clear() - User.clean() + User.clear() Hub.delete_session() Local.remove_session_file() plt.close("all") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index b53e55fa463..ab28729386c 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -44,8 +44,8 @@ class User: console.print("[info]Only you know...[/info]\n") @classmethod - def clean(cls): - """Clean user info.""" + def clear(cls): + """Clear user info.""" User.TOKEN_TYPE = "" User.TOKEN = "" User.EMAIL = "" From de47d49015202dcad197d3c2de137d5e0dc95ff6 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 16:49:34 +0000 Subject: [PATCH 060/230] breaking changes: splitting guest from logged users --- openbb_terminal/config_plot.py | 7 ----- openbb_terminal/config_terminal.py | 14 --------- openbb_terminal/core/config/paths.py | 1 - openbb_terminal/feature_flags.py | 3 -- openbb_terminal/keys_controller.py | 3 -- openbb_terminal/keys_model.py | 3 +- openbb_terminal/parent_classes.py | 3 +- openbb_terminal/session/session_controller.py | 11 ------- openbb_terminal/session/session_model.py | 29 +++++++++++++++++++ openbb_terminal/session/user.py | 5 ++++ openbb_terminal/terminal_controller.py | 18 ++++++++---- terminal.py | 9 +++--- 12 files changed, 54 insertions(+), 52 deletions(-) create mode 100644 openbb_terminal/session/session_model.py diff --git a/openbb_terminal/config_plot.py b/openbb_terminal/config_plot.py index fb0394bc3d6..e46de27c2b7 100644 --- a/openbb_terminal/config_plot.py +++ b/openbb_terminal/config_plot.py @@ -1,13 +1,6 @@ import os - -import dotenv - -from openbb_terminal.core.config.paths import USER_ENV_FILE, REPOSITORY_ENV_FILE from openbb_terminal.base_helpers import load_env_vars -dotenv.load_dotenv(USER_ENV_FILE) -dotenv.load_dotenv(REPOSITORY_ENV_FILE, override=True) - PLOT_DPI = load_env_vars("OPENBB_PLOT_DPI", int, 100, "settings") # Backend to use for plotting diff --git a/openbb_terminal/config_terminal.py b/openbb_terminal/config_terminal.py index 3fb20facf17..1edaa6da370 100644 --- a/openbb_terminal/config_terminal.py +++ b/openbb_terminal/config_terminal.py @@ -1,24 +1,10 @@ # IMPORTATION STANDARD import os - -# IMPORTATION THIRDPARTY -import dotenv - # IMPORTATION INTERNAL - -from openbb_terminal.core.config.paths import ( - PACKAGE_ENV_FILE, - USER_ENV_FILE, - REPOSITORY_ENV_FILE, -) from openbb_terminal.base_helpers import load_env_vars, strtobool from .helper_classes import TerminalStyle as _TerminalStyle -dotenv.load_dotenv(USER_ENV_FILE) -dotenv.load_dotenv(REPOSITORY_ENV_FILE, override=True) -dotenv.load_dotenv(PACKAGE_ENV_FILE, override=True) - # Terminal UX section MPL_STYLE = os.getenv("OPENBB_MPLSTYLE") or "dark" PMF_STYLE = os.getenv("OPENBB_PMFSTYLE") or "dark" diff --git a/openbb_terminal/core/config/paths.py b/openbb_terminal/core/config/paths.py index a710d05e476..0ee760a4ae6 100644 --- a/openbb_terminal/core/config/paths.py +++ b/openbb_terminal/core/config/paths.py @@ -20,7 +20,6 @@ def load_dotenv_with_priority(): def get_user_data_directory(): """Gets user data path from .env file or returns default path""" - load_dotenv_with_priority() if os.getenv("OPENBB_USER_DATA_DIRECTORY"): user_data_directory = Path(os.getenv("OPENBB_USER_DATA_DIRECTORY")) else: diff --git a/openbb_terminal/feature_flags.py b/openbb_terminal/feature_flags.py index 86da3e1787f..fde5db81791 100644 --- a/openbb_terminal/feature_flags.py +++ b/openbb_terminal/feature_flags.py @@ -9,7 +9,6 @@ import i18n from openbb_terminal.core.config.paths import ( MISCELLANEOUS_DIRECTORY, USER_DATA_SOURCES_DEFAULT_FILE, - load_dotenv_with_priority, ) from openbb_terminal.core.config import paths_helper from openbb_terminal.base_helpers import ( @@ -21,8 +20,6 @@ paths_helper.init_userdata() # pylint: disable=no-member,c-extension-no-member -load_dotenv_with_priority() - try: __import__("git") except ImportError: diff --git a/openbb_terminal/keys_controller.py b/openbb_terminal/keys_controller.py index 1337590ec73..9984de93f8e 100644 --- a/openbb_terminal/keys_controller.py +++ b/openbb_terminal/keys_controller.py @@ -11,7 +11,6 @@ from tqdm import tqdm from openbb_terminal import feature_flags as obbff, keys_view from openbb_terminal import keys_model -from openbb_terminal.core.config.paths import USER_ENV_FILE from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import EXPORT_ONLY_RAW_DATA_ALLOWED @@ -35,11 +34,9 @@ class KeysController(BaseController): # pylint: disable=too-many-public-methods self, queue: List[str] = None, menu_usage: bool = True, - env_file: str = str(USER_ENV_FILE), ): """Constructor""" super().__init__(queue) - self.env_file = env_file if menu_usage: if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 3f49acb8cd0..ec31f7e4929 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -32,6 +32,7 @@ from openbb_terminal.cryptocurrency.coinbase_helpers import ( from openbb_terminal import config_terminal as cfg from openbb_terminal.core.config.paths import USER_ENV_FILE from openbb_terminal.rich_config import console +from openbb_terminal.session.user import User from openbb_terminal.terminal_helper import suppress_stdout from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel @@ -226,7 +227,7 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non If True, api key change will be global, i.e. it will affect terminal environment variables. By default, False. """ - if persist: + if persist and User.is_guest(): os.environ[env_var_name] = env_var_value dotenv.set_key(str(USER_ENV_FILE), env_var_name, env_var_value) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 4883158662f..4cbbb007c99 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -15,11 +15,10 @@ from typing import Any, Dict, List, Union import numpy as np import pandas as pd -import matplotlib.pyplot as plt from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.session.session_controller import logout +from openbb_terminal.session.session_model import logout from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import ( diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 00baf0b4d3a..f9f5e4deb45 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,11 +1,9 @@ from typing import Tuple import json -import matplotlib.pyplot as plt import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY -from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller @@ -86,15 +84,6 @@ def login(session: dict): login_prompt(welcome=True) -def logout(): - """Logout and clear session.""" - system_clear() - User.clear() - Hub.delete_session() - Local.remove_session_file() - plt.close("all") - - def main(guest_allowed: bool = True): """Main function""" local_session = Local.get_session() diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py new file mode 100644 index 00000000000..457fbe88442 --- /dev/null +++ b/openbb_terminal/session/session_model.py @@ -0,0 +1,29 @@ +import os +import sys +import importlib +import matplotlib.pyplot as plt +import openbb_terminal.session.local_model as Local +import openbb_terminal.session.hub_model as Hub +from openbb_terminal.session.user import User +from openbb_terminal.helper_funcs import system_clear + + +def logout(): + """Logout and clear session.""" + system_clear() + User.clear() + + # Clear openbb environment variables + for v in os.environ: + if v.startswith("OPENBB"): + os.environ.pop(v) + + # Reload all openbb modules + modules = sys.modules.copy() + for module in modules: + if module.startswith("openbb"): + importlib.reload(sys.modules[module]) + + Hub.delete_session() + Local.remove_session_file() + plt.close("all") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index ab28729386c..9457b11d11c 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -51,3 +51,8 @@ class User: User.EMAIL = "" User.UUID = "" obbff.USE_FLAIR = ":openbb" + + @classmethod + def is_guest(cls): + """Check if user is guest.""" + return not bool(User.TOKEN) diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index b6d9f1312fe..b7ad89ead2c 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -13,6 +13,7 @@ import sys import webbrowser from typing import List, Dict, Optional import contextlib +import importlib import certifi from rich import panel @@ -23,6 +24,7 @@ from prompt_toolkit.styles import Style from prompt_toolkit.formatted_text import HTML import pandas as pd from openbb_terminal import feature_flags as obbff +from openbb_terminal.session.user import User from openbb_terminal.terminal_helper import is_packaged_application from openbb_terminal.session import session_controller @@ -356,13 +358,13 @@ class TerminalController(BaseController): """Process keys command.""" from openbb_terminal.keys_controller import KeysController - self.queue = self.load_class(KeysController, self.queue, env_file) + self.queue = self.load_class(KeysController, self.queue) def call_settings(self, _): """Process settings command.""" from openbb_terminal.settings_controller import SettingsController - self.queue = self.load_class(SettingsController, self.queue, env_file) + self.queue = self.load_class(SettingsController, self.queue) def call_featflags(self, _): """Process feature flags command.""" @@ -789,6 +791,14 @@ class TerminalController(BaseController): # pylint: disable=global-statement def terminal(jobs_cmds: List[str] = None, test_mode=False): """Terminal Menu.""" + + if User.is_guest(): + load_dotenv_with_priority() + modules = sys.modules.copy() + for module in modules: + if module.startswith("openbb"): + importlib.reload(sys.modules[module]) + if not test_mode: setup_logging() logger.info("START") @@ -838,8 +848,6 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): t_controller.print_help() check_for_updates() - load_dotenv_with_priority() - while ret_code: if obbff.ENABLE_QUICK_EXIT: console.print("Quick exit enabled") @@ -908,8 +916,6 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): break # Check if the user wants to reset application - # TODO: exit cmd after logout and reentering the terminal is not working - # HELP: login -> logout -> login -> exit should leave the terminal, but it doesn't if an_input in ("r", "reset") or t_controller.update_success: ret_code = reset(t_controller.queue if t_controller.queue else []) if ret_code != 0: diff --git a/terminal.py b/terminal.py index 234c7089ae3..ffae390625f 100644 --- a/terminal.py +++ b/terminal.py @@ -1,14 +1,15 @@ import multiprocessing import sys -from openbb_terminal.core.integration_tests import integration_controller -from openbb_terminal.session import session_controller -from openbb_terminal.terminal_helper import is_packaged_application - if __name__ == "__main__": multiprocessing.freeze_support() sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: + from openbb_terminal.core.integration_tests import integration_controller + integration_controller.main() else: + from openbb_terminal.session import session_controller + from openbb_terminal.terminal_helper import is_packaged_application + session_controller.main(guest_allowed=not is_packaged_application()) From 106ee5a37325e9bf6a982c0a61c43aa0cfd992ae Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 17:16:25 +0000 Subject: [PATCH 061/230] pull imports up --- terminal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terminal.py b/terminal.py index ffae390625f..f9ff629347c 100644 --- a/terminal.py +++ b/terminal.py @@ -1,15 +1,15 @@ import multiprocessing import sys +from openbb_terminal.core.integration_tests import integration_controller +from openbb_terminal.session import session_controller +from openbb_terminal.terminal_helper import is_packaged_application if __name__ == "__main__": multiprocessing.freeze_support() sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: - from openbb_terminal.core.integration_tests import integration_controller integration_controller.main() else: - from openbb_terminal.session import session_controller - from openbb_terminal.terminal_helper import is_packaged_application session_controller.main(guest_allowed=not is_packaged_application()) From f55d32b0591b44bf9637f7d11e47769781c6fbb2 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 17:30:54 +0000 Subject: [PATCH 062/230] delete session model --- openbb_terminal/parent_classes.py | 2 +- openbb_terminal/session/session_controller.py | 26 +++++++++++++++++ openbb_terminal/session/session_model.py | 29 ------------------- 3 files changed, 27 insertions(+), 30 deletions(-) delete mode 100644 openbb_terminal/session/session_model.py diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 4cbbb007c99..4a7bd18679b 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -18,7 +18,7 @@ import pandas as pd from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.session.session_model import logout +from openbb_terminal.session.session_controller import logout from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import ( diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index f9f5e4deb45..0d3b7503fbc 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,11 +1,16 @@ +import importlib +import os +import sys from typing import Tuple import json +import matplotlib.pyplot as plt import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller +from openbb_terminal.helper_funcs import system_clear def display_welcome_message(): @@ -93,5 +98,26 @@ def main(guest_allowed: bool = True): login(session=local_session) +def logout(): + """Logout and clear session.""" + system_clear() + User.clear() + + # Clear openbb environment variables + for v in os.environ: + if v.startswith("OPENBB"): + os.environ.pop(v) + + # Reload all openbb modules + modules = sys.modules.copy() + for module in modules: + if module.startswith("openbb"): + importlib.reload(sys.modules[module]) + + Hub.delete_session() + Local.remove_session_file() + plt.close("all") + + if __name__ == "__main__": main() diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py deleted file mode 100644 index 457fbe88442..00000000000 --- a/openbb_terminal/session/session_model.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import sys -import importlib -import matplotlib.pyplot as plt -import openbb_terminal.session.local_model as Local -import openbb_terminal.session.hub_model as Hub -from openbb_terminal.session.user import User -from openbb_terminal.helper_funcs import system_clear - - -def logout(): - """Logout and clear session.""" - system_clear() - User.clear() - - # Clear openbb environment variables - for v in os.environ: - if v.startswith("OPENBB"): - os.environ.pop(v) - - # Reload all openbb modules - modules = sys.modules.copy() - for module in modules: - if module.startswith("openbb"): - importlib.reload(sys.modules[module]) - - Hub.delete_session() - Local.remove_session_file() - plt.close("all") From 5210adf82982f820ac5ebae8ba47973208668dcd Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 17:58:48 +0000 Subject: [PATCH 063/230] move logout func --- openbb_terminal/session/session_controller.py | 18 +++++++++--------- terminal.py | 2 -- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 0d3b7503fbc..11b8262f54f 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -89,15 +89,6 @@ def login(session: dict): login_prompt(welcome=True) -def main(guest_allowed: bool = True): - """Main function""" - local_session = Local.get_session() - if not local_session: - login_prompt(guest_allowed=guest_allowed) - else: - login(session=local_session) - - def logout(): """Logout and clear session.""" system_clear() @@ -119,5 +110,14 @@ def logout(): plt.close("all") +def main(guest_allowed: bool = True): + """Main function""" + local_session = Local.get_session() + if not local_session: + login_prompt(guest_allowed=guest_allowed) + else: + login(session=local_session) + + if __name__ == "__main__": main() diff --git a/terminal.py b/terminal.py index f9ff629347c..3850e0ddce0 100644 --- a/terminal.py +++ b/terminal.py @@ -8,8 +8,6 @@ if __name__ == "__main__": multiprocessing.freeze_support() sent_args = sys.argv[1:] if "-t" in sent_args or "--test" in sent_args: - integration_controller.main() else: - session_controller.main(guest_allowed=not is_packaged_application()) From f9988d24e841c1c6900beb1a1369251a8cb5744f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 19:16:49 +0000 Subject: [PATCH 064/230] add ability to save and apply keys from remote --- openbb_terminal/keys_model.py | 5 ++ openbb_terminal/session/hub_model.py | 84 +++++++++++-------- openbb_terminal/session/local_model.py | 23 ++--- openbb_terminal/session/session_controller.py | 2 +- 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index ec31f7e4929..6835a821124 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -32,6 +32,7 @@ from openbb_terminal.cryptocurrency.coinbase_helpers import ( from openbb_terminal import config_terminal as cfg from openbb_terminal.core.config.paths import USER_ENV_FILE from openbb_terminal.rich_config import console +from openbb_terminal.session.hub_model import patch_user_configs from openbb_terminal.session.user import User from openbb_terminal.terminal_helper import suppress_stdout @@ -238,6 +239,10 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non # Set cfg.env_var_name = env_var_value setattr(cfg, env_var_name, env_var_value) + # Send api key to server + if not User.is_guest(): + patch_user_configs(key=env_var_name, value=env_var_value, type_="keys") + def get_keys(show: bool = False) -> pd.DataFrame: """Get currently set API keys. diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index b7d3a489f78..02add4d6c68 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -113,35 +113,60 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: return None -def patch_user_configs(data: dict) -> Optional[requests.Response]: - """Patch user configurations.""" - # When available on the server just need to send the changed configs, not all of them. - # Adapt this to Colin implementation. - pass +def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Response]: + """Patch user configurations to the server. + Parameters + ---------- + key : str + The key to patch. + value : str + The value to patch. + type_ : str + The type of the patch, either "keys" or "settings". + + Returns + ------- + bool + The status of the patch. + """ + + if type_ not in ["keys", "settings"]: + console.print("[red]\nInvalid patch type.[/red]") + return None + + data = {"key": f"features_{type_}.{key}", "value": value} -def put_user_configs(data: dict) -> bool: - """Push user configurations to the server.""" try: - response = requests.put( - url=BASE_URL + "terminal/user", + response = requests.patch( + url=BASE_URL + "terminal/user-json", headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, json=data, ) if response.status_code == 200: - console.print("[green]\nPushed user configurations.[/green]") + console.print("[green]Saved remotely.[/green]") else: - console.print("[red]\nFailed to push configurations.[/red]") - return True + console.print("[red]Failed to save remotely.[/red]") + return response except requests.exceptions.ConnectionError: - console.print("[red]\nConnection error.[/red]") - return False + console.print("[red]Connection error.[/red]") + return None except Exception: - console.print("[red]\nFailed to push configurations.[/red]") - return False + console.print("[red]Failed to save remotely.[/red]") + return None -def logout_everywhere() -> bool: +def put_user_configs() -> Optional[requests.Response]: + """Push user configurations to the server.""" + pass + + +def delete_session(): + """Delete the session.""" + pass + + +def logout_everywhere() -> Optional[requests.Response]: """Request a remote logout. Returns @@ -150,25 +175,18 @@ def logout_everywhere() -> bool: The status of the logout. """ try: - if ( - requests.get( - url=BASE_URL + "logout-everywhere", - headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, - ).status_code - == 200 - ): + response = requests.get( + url=BASE_URL + "logout-everywhere", + headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, + ) + if response.status_code == 200: console.print("[green]\nLogged out remotely.[/green]") - return True + return response console.print("[red]\nFailed to logout remotely.[/red]") - return False + return None except requests.exceptions.ConnectionError: console.print("[red]\nConnection error.[/red]") - return False + return None except Exception: console.print("[red]\nFailed to logout remotely.[/red]") - return False - - -def delete_session(): - """Delete the session.""" - pass + return None diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 1771515d15b..8ccc957e081 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -5,8 +5,8 @@ from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console # from openbb_terminal import feature_flags as obbff -# from openbb_terminal import config_terminal as cfg -# from openbb_terminal.base_helpers import strtobool +from openbb_terminal import config_terminal as cfg +from openbb_terminal.base_helpers import strtobool SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" @@ -77,6 +77,16 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: def apply_configs(configs: dict): """Apply configurations.""" console.print(f"Will apply this: {configs}", style="red") + + keys = configs.get("features_keys", {}) + if keys: + for k, v in keys.items(): + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) + # TODO: Apply configs when uploading is implemented # if configs: # settings = configs.get("features_settings", {}) @@ -87,12 +97,3 @@ def apply_configs(configs: dict): # setattr(obbff, k, strtobool(v)) # else: # setattr(obbff, k, v) - - # keys = configs.get("features_keys", {}) - # if keys: - # for k, v in keys.items(): - # if hasattr(cfg, k): - # if isinstance(getattr(cfg, k), int): - # setattr(cfg, k, strtobool(v)) - # else: - # setattr(cfg, k, v) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 11b8262f54f..71485ee7456 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -99,7 +99,7 @@ def logout(): if v.startswith("OPENBB"): os.environ.pop(v) - # Reload all openbb modules + # Reload all openbb modules to clear memorized variables modules = sys.modules.copy() for module in modules: if module.startswith("openbb"): From 856b8a29ce58cf24d4a488e866237af3f842ea62 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 19:32:45 +0000 Subject: [PATCH 065/230] no sensitive keys --- openbb_terminal/config_terminal.py | 11 +++++++++++ openbb_terminal/keys_model.py | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/config_terminal.py b/openbb_terminal/config_terminal.py index 1edaa6da370..5be8539f469 100644 --- a/openbb_terminal/config_terminal.py +++ b/openbb_terminal/config_terminal.py @@ -5,6 +5,17 @@ import os from openbb_terminal.base_helpers import load_env_vars, strtobool from .helper_classes import TerminalStyle as _TerminalStyle +SENSITIVE_KEYS = [ + "RH_USERNAME", + "RH_PASSWORD", + "DG_USERNAME", + "DG_PASSWORD", + "DG_TOTP_SECRET", + "OANDA_ACCOUNT_TYPE", + "OANDA_ACCOUNT", + "OANDA_TOKEN", +] + # Terminal UX section MPL_STYLE = os.getenv("OPENBB_MPLSTYLE") or "dark" PMF_STYLE = os.getenv("OPENBB_PMFSTYLE") or "dark" diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 6835a821124..7846d64e983 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -240,7 +240,11 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non setattr(cfg, env_var_name, env_var_value) # Send api key to server - if not User.is_guest(): + if ( + not User.is_guest() + and env_var_name not in cfg.SENSITIVE_KEYS + and env_var_name.startswith("API_") + ): patch_user_configs(key=env_var_name, value=env_var_value, type_="keys") From 6d8678b86f3815a9c99f05131109e83b912d8a88 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 20:31:05 +0000 Subject: [PATCH 066/230] add support for feature flags --- openbb_terminal/featflags_controller.py | 118 ++++++++++-------------- openbb_terminal/session/hub_model.py | 4 +- openbb_terminal/session/local_model.py | 46 ++++----- 3 files changed, 76 insertions(+), 92 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index b99a7040d83..fb5d7268a8f 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -16,6 +16,8 @@ from openbb_terminal.decorators import log_start_end from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText +from openbb_terminal.session.hub_model import patch_user_configs +from openbb_terminal.session.user import User # pylint: disable=too-many-lines,no-member,too-many-public-methods,C0302 # pylint:disable=import-outside-toplevel @@ -78,130 +80,115 @@ class FeatureFlagsController(BaseController): console.print(text=mt.menu_text, menu="Feature Flags") + def set_feature_flag(self, name: str, value: bool): + """Set feature flag + + Parameters + ---------- + name : str + Environment variable name + value : str + Environment variable value + persist : bool, optional + Persist feature flag, by default False + """ + value = not value + + if User.is_guest(): + print(f"Setting {name} to {str(value)}") + set_key(str(USER_ENV_FILE), name, str(value)) + + # Remove "OPENBB_" prefix from env_var + if name.startswith("OPENBB_"): + name = name[7:] + + # Set obbff.env_var_name = not env_var_value + setattr(obbff, name, value) + + # Send feature flag to server + if not User.is_guest(): + patch_user_configs(key=name, value=str(value), type_="settings") + def call_overwrite(self, _): """Process overwrite command""" - obbff.FILE_OVERWITE = not obbff.FILE_OVERWITE - set_key(USER_ENV_FILE, "OPENBB_FILE_OVERWITE", str(obbff.FILE_OVERWITE)) + self.set_feature_flag("OPENBB_FILE_OVERWITE", obbff.FILE_OVERWITE) def call_retryload(self, _): """Process retryload command""" - obbff.RETRY_WITH_LOAD = not obbff.RETRY_WITH_LOAD - set_key(USER_ENV_FILE, "OPENBB_RETRY_WITH_LOAD", str(obbff.RETRY_WITH_LOAD)) + self.set_feature_flag("OPENBB_RETRY_WITH_LOAD", obbff.RETRY_WITH_LOAD) @log_start_end(log=logger) def call_tab(self, _): """Process tab command""" - obbff.USE_TABULATE_DF = not obbff.USE_TABULATE_DF - set_key(USER_ENV_FILE, "OPENBB_USE_TABULATE_DF", str(obbff.USE_TABULATE_DF)) + self.set_feature_flag("OPENBB_USE_TABULATE_DF", obbff.USE_TABULATE_DF) @log_start_end(log=logger) def call_cls(self, _): """Process cls command""" - obbff.USE_CLEAR_AFTER_CMD = not obbff.USE_CLEAR_AFTER_CMD - set_key( - USER_ENV_FILE, - "OPENBB_USE_CLEAR_AFTER_CMD", - str(obbff.USE_CLEAR_AFTER_CMD), - ) + self.set_feature_flag("OPENBB_USE_CLEAR_AFTER_CMD", obbff.USE_CLEAR_AFTER_CMD) @log_start_end(log=logger) def call_color(self, _): """Process color command""" - obbff.USE_COLOR = not obbff.USE_COLOR - set_key(USER_ENV_FILE, "OPENBB_USE_COLOR", str(obbff.USE_COLOR)) + self.set_feature_flag("OPENBB_USE_COLOR", obbff.USE_COLOR) @log_start_end(log=logger) def call_promptkit(self, _): """Process promptkit command""" - obbff.USE_PROMPT_TOOLKIT = not obbff.USE_PROMPT_TOOLKIT - set_key( - USER_ENV_FILE, - "OPENBB_USE_PROMPT_TOOLKIT", - str(obbff.USE_PROMPT_TOOLKIT), - ) + self.set_feature_flag("OPENBB_USE_PROMPT_TOOLKIT", obbff.USE_PROMPT_TOOLKIT) @log_start_end(log=logger) def call_thoughts(self, _): """Process thoughts command""" - obbff.ENABLE_THOUGHTS_DAY = not obbff.ENABLE_THOUGHTS_DAY - set_key( - USER_ENV_FILE, - "OPENBB_ENABLE_THOUGHTS_DAY", - str(obbff.ENABLE_THOUGHTS_DAY), - ) + self.set_feature_flag("OPENBB_ENABLE_THOUGHTS_DAY", obbff.ENABLE_THOUGHTS_DAY) @log_start_end(log=logger) def call_reporthtml(self, _): """Process reporthtml command""" - obbff.OPEN_REPORT_AS_HTML = not obbff.OPEN_REPORT_AS_HTML - set_key( - USER_ENV_FILE, - "OPENBB_OPEN_REPORT_AS_HTML", - str(obbff.OPEN_REPORT_AS_HTML), - ) + self.set_feature_flag("OPENBB_OPEN_REPORT_AS_HTML", obbff.OPEN_REPORT_AS_HTML) @log_start_end(log=logger) def call_exithelp(self, _): """Process exithelp command""" - obbff.ENABLE_EXIT_AUTO_HELP = not obbff.ENABLE_EXIT_AUTO_HELP - set_key( - USER_ENV_FILE, - "OPENBB_ENABLE_EXIT_AUTO_HELP", - str(obbff.ENABLE_EXIT_AUTO_HELP), + self.set_feature_flag( + "OPENBB_ENABLE_EXIT_AUTO_HELP", obbff.ENABLE_EXIT_AUTO_HELP ) @log_start_end(log=logger) def call_rcontext(self, _): """Process rcontext command""" - obbff.REMEMBER_CONTEXTS = not obbff.REMEMBER_CONTEXTS - set_key( - USER_ENV_FILE, - "OPENBB_REMEMBER_CONTEXTS", - str(obbff.REMEMBER_CONTEXTS), - ) + self.set_feature_flag("OPENBB_REMEMBER_CONTEXTS", obbff.REMEMBER_CONTEXTS) @log_start_end(log=logger) def call_dt(self, _): """Process dt command""" - obbff.USE_DATETIME = not obbff.USE_DATETIME - set_key(USER_ENV_FILE, "OPENBB_USE_DATETIME", str(obbff.USE_DATETIME)) + self.set_feature_flag("OPENBB_USE_DATETIME", obbff.USE_DATETIME) @log_start_end(log=logger) def call_rich(self, _): """Process rich command""" - obbff.ENABLE_RICH = not obbff.ENABLE_RICH - set_key(USER_ENV_FILE, "OPENBB_ENABLE_RICH", str(obbff.ENABLE_RICH)) + self.set_feature_flag("OPENBB_ENABLE_RICH", obbff.ENABLE_RICH) @log_start_end(log=logger) def call_richpanel(self, _): """Process richpanel command""" - obbff.ENABLE_RICH_PANEL = not obbff.ENABLE_RICH_PANEL - set_key( - USER_ENV_FILE, - "OPENBB_ENABLE_RICH_PANEL", - str(obbff.ENABLE_RICH_PANEL), - ) + self.set_feature_flag("OPENBB_ENABLE_RICH_PANEL", obbff.ENABLE_RICH_PANEL) @log_start_end(log=logger) def call_ion(self, _): """Process ion command""" - obbff.USE_ION = not obbff.USE_ION - set_key(USER_ENV_FILE, "OPENBB_USE_ION", str(obbff.USE_ION)) + self.set_feature_flag("OPENBB_USE_ION", obbff.USE_ION) @log_start_end(log=logger) def call_watermark(self, _): """Process watermark command""" - obbff.USE_WATERMARK = not obbff.USE_WATERMARK - set_key(USER_ENV_FILE, "OPENBB_USE_WATERMARK", str(obbff.USE_WATERMARK)) + self.set_feature_flag("OPENBB_USE_WATERMARK", obbff.USE_WATERMARK) @log_start_end(log=logger) def call_cmdloc(self, _): """Process cmdloc command""" - obbff.USE_CMD_LOCATION_FIGURE = not obbff.USE_CMD_LOCATION_FIGURE - set_key( - USER_ENV_FILE, - "OPENBB_USE_CMD_LOCATION_FIGURE", - str(obbff.USE_CMD_LOCATION_FIGURE), + self.set_feature_flag( + "OPENBB_USE_CMD_LOCATION_FIGURE", obbff.USE_CMD_LOCATION_FIGURE ) @log_start_end(log=logger) @@ -209,9 +196,4 @@ class FeatureFlagsController(BaseController): """Process tbhint command""" if obbff.TOOLBAR_HINT: console.print("Will take effect when running terminal next.") - obbff.TOOLBAR_HINT = not obbff.TOOLBAR_HINT - set_key( - USER_ENV_FILE, - "OPENBB_TOOLBAR_HINT", - str(obbff.TOOLBAR_HINT), - ) + self.set_feature_flag("OPENBB_TOOLBAR_HINT", obbff.TOOLBAR_HINT) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 02add4d6c68..e08761caaee 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -100,9 +100,7 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: url=BASE_URL + "terminal/user", headers={"Authorization": f"{token_type.title()} {token}"}, ) - if response.status_code == 200: - console.print("[green]\nFetched user configurations.[/green]") - else: + if response.status_code != 200: console.print("[red]\nFailed to fetch configurations.[/red]") return response except requests.exceptions.ConnectionError: diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 8ccc957e081..cf8e3796887 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -4,7 +4,7 @@ import json from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY from openbb_terminal.rich_config import console -# from openbb_terminal import feature_flags as obbff +from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg from openbb_terminal.base_helpers import strtobool @@ -75,25 +75,29 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: def apply_configs(configs: dict): - """Apply configurations.""" - console.print(f"Will apply this: {configs}", style="red") + """Apply configurations. - keys = configs.get("features_keys", {}) - if keys: - for k, v in keys.items(): - if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) + Parameters + ---------- + configs : dict + The configurations. + """ - # TODO: Apply configs when uploading is implemented - # if configs: - # settings = configs.get("features_settings", {}) - # if settings: - # for k, v in settings.items(): - # if hasattr(obbff, k): - # if isinstance(getattr(obbff, k), int): - # setattr(obbff, k, strtobool(v)) - # else: - # setattr(obbff, k, v) + if configs: + keys = configs.get("features_keys", {}) + if keys: + for k, v in keys.items(): + if hasattr(cfg, k): + if isinstance(getattr(cfg, k), int): + setattr(cfg, k, strtobool(v)) + else: + setattr(cfg, k, v) + + settings = configs.get("features_settings", {}) + if settings: + for k, v in settings.items(): + if hasattr(obbff, k): + if isinstance(getattr(obbff, k), int): + setattr(obbff, k, strtobool(v)) + else: + setattr(obbff, k, v) From 8af8f9af694d4cefc3d3dae45a2ae664f3bb1f7e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 20:36:03 +0000 Subject: [PATCH 067/230] unecessary print --- openbb_terminal/session/hub_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index e08761caaee..e630d7575b6 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -55,7 +55,6 @@ def process_session_response(response: requests.Response) -> dict: The login info. """ if response.status_code == 200: - console.print("\nLogin successful.", style="green") login = response.json() return login if response.status_code == 401: From e7930f0f03b462abd850b547065960d66ee0d8f1 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 21:31:09 +0000 Subject: [PATCH 068/230] allow settings from differnt modules --- openbb_terminal/featflags_controller.py | 7 +++++-- openbb_terminal/session/local_model.py | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index fb5d7268a8f..855435affb2 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -95,7 +95,6 @@ class FeatureFlagsController(BaseController): value = not value if User.is_guest(): - print(f"Setting {name} to {str(value)}") set_key(str(USER_ENV_FILE), name, str(value)) # Remove "OPENBB_" prefix from env_var @@ -107,7 +106,11 @@ class FeatureFlagsController(BaseController): # Send feature flag to server if not User.is_guest(): - patch_user_configs(key=name, value=str(value), type_="settings") + patch_user_configs( + key=name, + value="openbb_terminal.feature_flags:" + str(value), + type_="settings", + ) def call_overwrite(self, _): """Process overwrite command""" diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index cf8e3796887..f1b1ec61b30 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -1,3 +1,4 @@ +import importlib import os from pathlib import Path import json @@ -82,7 +83,6 @@ def apply_configs(configs: dict): configs : dict The configurations. """ - if configs: keys = configs.get("features_keys", {}) if keys: @@ -93,11 +93,16 @@ def apply_configs(configs: dict): else: setattr(cfg, k, v) + # Since settings come from different files we use the format {var_name: "path:value"} settings = configs.get("features_settings", {}) if settings: - for k, v in settings.items(): - if hasattr(obbff, k): - if isinstance(getattr(obbff, k), int): - setattr(obbff, k, strtobool(v)) - else: - setattr(obbff, k, v) + for var_name, v in settings.items(): + if v.find(":") > 0: + path, value = v.split(":") + + module = importlib.import_module(path) + if hasattr(module, var_name): + if isinstance(getattr(module, var_name), int): + setattr(module, var_name, strtobool(value)) + else: + setattr(module, var_name, value) From b644c81f3cb6954d612af9f0a2637dabb872b2c6 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 21:44:51 +0000 Subject: [PATCH 069/230] change setff to static --- openbb_terminal/featflags_controller.py | 61 +++++++++++++++++-------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 855435affb2..0e667cf0c3c 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -80,7 +80,8 @@ class FeatureFlagsController(BaseController): console.print(text=mt.menu_text, menu="Feature Flags") - def set_feature_flag(self, name: str, value: bool): + @staticmethod + def set_feature_flag(name: str, value: bool): """Set feature flag Parameters @@ -114,83 +115,105 @@ class FeatureFlagsController(BaseController): def call_overwrite(self, _): """Process overwrite command""" - self.set_feature_flag("OPENBB_FILE_OVERWITE", obbff.FILE_OVERWITE) + FeatureFlagsController.set_feature_flag( + "OPENBB_FILE_OVERWITE", obbff.FILE_OVERWITE + ) def call_retryload(self, _): """Process retryload command""" - self.set_feature_flag("OPENBB_RETRY_WITH_LOAD", obbff.RETRY_WITH_LOAD) + FeatureFlagsController.set_feature_flag( + "OPENBB_RETRY_WITH_LOAD", obbff.RETRY_WITH_LOAD + ) @log_start_end(log=logger) def call_tab(self, _): """Process tab command""" - self.set_feature_flag("OPENBB_USE_TABULATE_DF", obbff.USE_TABULATE_DF) + FeatureFlagsController.set_feature_flag( + "OPENBB_USE_TABULATE_DF", obbff.USE_TABULATE_DF + ) @log_start_end(log=logger) def call_cls(self, _): """Process cls command""" - self.set_feature_flag("OPENBB_USE_CLEAR_AFTER_CMD", obbff.USE_CLEAR_AFTER_CMD) + FeatureFlagsController.set_feature_flag( + "OPENBB_USE_CLEAR_AFTER_CMD", obbff.USE_CLEAR_AFTER_CMD + ) @log_start_end(log=logger) def call_color(self, _): """Process color command""" - self.set_feature_flag("OPENBB_USE_COLOR", obbff.USE_COLOR) + FeatureFlagsController.set_feature_flag("OPENBB_USE_COLOR", obbff.USE_COLOR) @log_start_end(log=logger) def call_promptkit(self, _): """Process promptkit command""" - self.set_feature_flag("OPENBB_USE_PROMPT_TOOLKIT", obbff.USE_PROMPT_TOOLKIT) + FeatureFlagsController.set_feature_flag( + "OPENBB_USE_PROMPT_TOOLKIT", obbff.USE_PROMPT_TOOLKIT + ) @log_start_end(log=logger) def call_thoughts(self, _): """Process thoughts command""" - self.set_feature_flag("OPENBB_ENABLE_THOUGHTS_DAY", obbff.ENABLE_THOUGHTS_DAY) + FeatureFlagsController.set_feature_flag( + "OPENBB_ENABLE_THOUGHTS_DAY", obbff.ENABLE_THOUGHTS_DAY + ) @log_start_end(log=logger) def call_reporthtml(self, _): """Process reporthtml command""" - self.set_feature_flag("OPENBB_OPEN_REPORT_AS_HTML", obbff.OPEN_REPORT_AS_HTML) + FeatureFlagsController.set_feature_flag( + "OPENBB_OPEN_REPORT_AS_HTML", obbff.OPEN_REPORT_AS_HTML + ) @log_start_end(log=logger) def call_exithelp(self, _): """Process exithelp command""" - self.set_feature_flag( + FeatureFlagsController.set_feature_flag( "OPENBB_ENABLE_EXIT_AUTO_HELP", obbff.ENABLE_EXIT_AUTO_HELP ) @log_start_end(log=logger) def call_rcontext(self, _): """Process rcontext command""" - self.set_feature_flag("OPENBB_REMEMBER_CONTEXTS", obbff.REMEMBER_CONTEXTS) + FeatureFlagsController.set_feature_flag( + "OPENBB_REMEMBER_CONTEXTS", obbff.REMEMBER_CONTEXTS + ) @log_start_end(log=logger) def call_dt(self, _): """Process dt command""" - self.set_feature_flag("OPENBB_USE_DATETIME", obbff.USE_DATETIME) + FeatureFlagsController.set_feature_flag( + "OPENBB_USE_DATETIME", obbff.USE_DATETIME + ) @log_start_end(log=logger) def call_rich(self, _): """Process rich command""" - self.set_feature_flag("OPENBB_ENABLE_RICH", obbff.ENABLE_RICH) + FeatureFlagsController.set_feature_flag("OPENBB_ENABLE_RICH", obbff.ENABLE_RICH) @log_start_end(log=logger) def call_richpanel(self, _): """Process richpanel command""" - self.set_feature_flag("OPENBB_ENABLE_RICH_PANEL", obbff.ENABLE_RICH_PANEL) + FeatureFlagsController.set_feature_flag( + "OPENBB_ENABLE_RICH_PANEL", obbff.ENABLE_RICH_PANEL + ) @log_start_end(log=logger) def call_ion(self, _): """Process ion command""" - self.set_feature_flag("OPENBB_USE_ION", obbff.USE_ION) + FeatureFlagsController.set_feature_flag("OPENBB_USE_ION", obbff.USE_ION) @log_start_end(log=logger) def call_watermark(self, _): """Process watermark command""" - self.set_feature_flag("OPENBB_USE_WATERMARK", obbff.USE_WATERMARK) + FeatureFlagsController.set_feature_flag( + "OPENBB_USE_WATERMARK", obbff.USE_WATERMARK + ) @log_start_end(log=logger) def call_cmdloc(self, _): """Process cmdloc command""" - self.set_feature_flag( + FeatureFlagsController.set_feature_flag( "OPENBB_USE_CMD_LOCATION_FIGURE", obbff.USE_CMD_LOCATION_FIGURE ) @@ -199,4 +222,6 @@ class FeatureFlagsController(BaseController): """Process tbhint command""" if obbff.TOOLBAR_HINT: console.print("Will take effect when running terminal next.") - self.set_feature_flag("OPENBB_TOOLBAR_HINT", obbff.TOOLBAR_HINT) + FeatureFlagsController.set_feature_flag( + "OPENBB_TOOLBAR_HINT", obbff.TOOLBAR_HINT + ) From b218079ab182938d8126314f8199bf9f88bb9e5d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 21:50:50 +0000 Subject: [PATCH 070/230] change way to set ff --- openbb_terminal/featflags_controller.py | 37 +++++++++++++------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 0e667cf0c3c..7d5ff0d90fd 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -93,7 +93,6 @@ class FeatureFlagsController(BaseController): persist : bool, optional Persist feature flag, by default False """ - value = not value if User.is_guest(): set_key(str(USER_ENV_FILE), name, str(value)) @@ -116,105 +115,107 @@ class FeatureFlagsController(BaseController): def call_overwrite(self, _): """Process overwrite command""" FeatureFlagsController.set_feature_flag( - "OPENBB_FILE_OVERWITE", obbff.FILE_OVERWITE + "OPENBB_FILE_OVERWITE", not obbff.FILE_OVERWITE ) def call_retryload(self, _): """Process retryload command""" FeatureFlagsController.set_feature_flag( - "OPENBB_RETRY_WITH_LOAD", obbff.RETRY_WITH_LOAD + "OPENBB_RETRY_WITH_LOAD", not obbff.RETRY_WITH_LOAD ) @log_start_end(log=logger) def call_tab(self, _): """Process tab command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_TABULATE_DF", obbff.USE_TABULATE_DF + "OPENBB_USE_TABULATE_DF", not obbff.USE_TABULATE_DF ) @log_start_end(log=logger) def call_cls(self, _): """Process cls command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_CLEAR_AFTER_CMD", obbff.USE_CLEAR_AFTER_CMD + "OPENBB_USE_CLEAR_AFTER_CMD", not obbff.USE_CLEAR_AFTER_CMD ) @log_start_end(log=logger) def call_color(self, _): """Process color command""" - FeatureFlagsController.set_feature_flag("OPENBB_USE_COLOR", obbff.USE_COLOR) + FeatureFlagsController.set_feature_flag("OPENBB_USE_COLOR", not obbff.USE_COLOR) @log_start_end(log=logger) def call_promptkit(self, _): """Process promptkit command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_PROMPT_TOOLKIT", obbff.USE_PROMPT_TOOLKIT + "OPENBB_USE_PROMPT_TOOLKIT", not obbff.USE_PROMPT_TOOLKIT ) @log_start_end(log=logger) def call_thoughts(self, _): """Process thoughts command""" FeatureFlagsController.set_feature_flag( - "OPENBB_ENABLE_THOUGHTS_DAY", obbff.ENABLE_THOUGHTS_DAY + "OPENBB_ENABLE_THOUGHTS_DAY", not obbff.ENABLE_THOUGHTS_DAY ) @log_start_end(log=logger) def call_reporthtml(self, _): """Process reporthtml command""" FeatureFlagsController.set_feature_flag( - "OPENBB_OPEN_REPORT_AS_HTML", obbff.OPEN_REPORT_AS_HTML + "OPENBB_OPEN_REPORT_AS_HTML", not obbff.OPEN_REPORT_AS_HTML ) @log_start_end(log=logger) def call_exithelp(self, _): """Process exithelp command""" FeatureFlagsController.set_feature_flag( - "OPENBB_ENABLE_EXIT_AUTO_HELP", obbff.ENABLE_EXIT_AUTO_HELP + "OPENBB_ENABLE_EXIT_AUTO_HELP", not obbff.ENABLE_EXIT_AUTO_HELP ) @log_start_end(log=logger) def call_rcontext(self, _): """Process rcontext command""" FeatureFlagsController.set_feature_flag( - "OPENBB_REMEMBER_CONTEXTS", obbff.REMEMBER_CONTEXTS + "OPENBB_REMEMBER_CONTEXTS", not obbff.REMEMBER_CONTEXTS ) @log_start_end(log=logger) def call_dt(self, _): """Process dt command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_DATETIME", obbff.USE_DATETIME + "OPENBB_USE_DATETIME", not obbff.USE_DATETIME ) @log_start_end(log=logger) def call_rich(self, _): """Process rich command""" - FeatureFlagsController.set_feature_flag("OPENBB_ENABLE_RICH", obbff.ENABLE_RICH) + FeatureFlagsController.set_feature_flag( + "OPENBB_ENABLE_RICH", not obbff.ENABLE_RICH + ) @log_start_end(log=logger) def call_richpanel(self, _): """Process richpanel command""" FeatureFlagsController.set_feature_flag( - "OPENBB_ENABLE_RICH_PANEL", obbff.ENABLE_RICH_PANEL + "OPENBB_ENABLE_RICH_PANEL", not obbff.ENABLE_RICH_PANEL ) @log_start_end(log=logger) def call_ion(self, _): """Process ion command""" - FeatureFlagsController.set_feature_flag("OPENBB_USE_ION", obbff.USE_ION) + FeatureFlagsController.set_feature_flag("OPENBB_USE_ION", not obbff.USE_ION) @log_start_end(log=logger) def call_watermark(self, _): """Process watermark command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_WATERMARK", obbff.USE_WATERMARK + "OPENBB_USE_WATERMARK", not obbff.USE_WATERMARK ) @log_start_end(log=logger) def call_cmdloc(self, _): """Process cmdloc command""" FeatureFlagsController.set_feature_flag( - "OPENBB_USE_CMD_LOCATION_FIGURE", obbff.USE_CMD_LOCATION_FIGURE + "OPENBB_USE_CMD_LOCATION_FIGURE", not obbff.USE_CMD_LOCATION_FIGURE ) @log_start_end(log=logger) @@ -223,5 +224,5 @@ class FeatureFlagsController(BaseController): if obbff.TOOLBAR_HINT: console.print("Will take effect when running terminal next.") FeatureFlagsController.set_feature_flag( - "OPENBB_TOOLBAR_HINT", obbff.TOOLBAR_HINT + "OPENBB_TOOLBAR_HINT", not obbff.TOOLBAR_HINT ) From d3d87cbb95f619b3ca2382039054cd6200df624e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 25 Jan 2023 22:28:54 +0000 Subject: [PATCH 071/230] fix bug in applying configs --- openbb_terminal/session/local_model.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index f1b1ec61b30..908ec27b470 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -83,15 +83,13 @@ def apply_configs(configs: dict): configs : dict The configurations. """ + console.print(configs, style="red") if configs: keys = configs.get("features_keys", {}) if keys: for k, v in keys.items(): if hasattr(cfg, k): - if isinstance(getattr(cfg, k), int): - setattr(cfg, k, strtobool(v)) - else: - setattr(cfg, k, v) + setattr(cfg, k, v) # Since settings come from different files we use the format {var_name: "path:value"} settings = configs.get("features_settings", {}) @@ -102,7 +100,9 @@ def apply_configs(configs: dict): module = importlib.import_module(path) if hasattr(module, var_name): - if isinstance(getattr(module, var_name), int): + if value.lower() in ["true", "false"]: setattr(module, var_name, strtobool(value)) + elif isinstance(getattr(module, var_name), int): + setattr(module, var_name, int(value)) else: setattr(module, var_name, value) From da202490fb9b36178aada498f4fc89b1b7938868 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 09:36:10 +0000 Subject: [PATCH 072/230] simplify obbff application --- openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/session/local_model.py | 39 +++++++++++++++++-------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 7d5ff0d90fd..9dab4e20bac 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -108,7 +108,7 @@ class FeatureFlagsController(BaseController): if not User.is_guest(): patch_user_configs( key=name, - value="openbb_terminal.feature_flags:" + str(value), + value=str(value), type_="settings", ) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 908ec27b470..f42ad7f5d39 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -1,4 +1,3 @@ -import importlib import os from pathlib import Path import json @@ -7,6 +6,7 @@ from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg +from openbb_terminal import config_plot as cfgp from openbb_terminal.base_helpers import strtobool SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" @@ -94,15 +94,30 @@ def apply_configs(configs: dict): # Since settings come from different files we use the format {var_name: "path:value"} settings = configs.get("features_settings", {}) if settings: - for var_name, v in settings.items(): - if v.find(":") > 0: - path, value = v.split(":") + for k, v in settings.items(): + if hasattr(obbff, k): + cast_set_attr(obbff, k, v) + elif hasattr(cfg, k): + cast_set_attr(cfg, k, v) + elif hasattr(cfgp, k): + cast_set_attr(cfgp, k, v) - module = importlib.import_module(path) - if hasattr(module, var_name): - if value.lower() in ["true", "false"]: - setattr(module, var_name, strtobool(value)) - elif isinstance(getattr(module, var_name), int): - setattr(module, var_name, int(value)) - else: - setattr(module, var_name, value) + +def cast_set_attr(obj, name, value): + """Set attribute in object. + + Parameters + ---------- + obj : object + The object. + name : str + The attribute name. + value : str + The attribute value. + """ + if value.lower() in ["true", "false"]: + setattr(obj, name, strtobool(value)) + elif isinstance(getattr(obj, value), int): + setattr(obj, name, int(value)) + else: + setattr(obj, name, value) From 5a60992c5b0e5424ca52acc1c603a5556857f9cd Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 09:51:29 +0000 Subject: [PATCH 073/230] bug on setting configs --- openbb_terminal/session/local_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index f42ad7f5d39..40e0179ec45 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -6,7 +6,7 @@ from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg -from openbb_terminal import config_plot as cfgp +from openbb_terminal import config_plot as cfg_plot from openbb_terminal.base_helpers import strtobool SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" @@ -99,8 +99,8 @@ def apply_configs(configs: dict): cast_set_attr(obbff, k, v) elif hasattr(cfg, k): cast_set_attr(cfg, k, v) - elif hasattr(cfgp, k): - cast_set_attr(cfgp, k, v) + elif hasattr(cfg_plot, k): + cast_set_attr(cfg_plot, k, v) def cast_set_attr(obj, name, value): @@ -117,7 +117,7 @@ def cast_set_attr(obj, name, value): """ if value.lower() in ["true", "false"]: setattr(obj, name, strtobool(value)) - elif isinstance(getattr(obj, value), int): + elif isinstance(getattr(obj, name), int): setattr(obj, name, int(value)) else: setattr(obj, name, value) From 9427d323e0785b6fdec599658f87d1bbd089c1f2 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 09:51:44 +0000 Subject: [PATCH 074/230] allow text ff --- openbb_terminal/featflags_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 9dab4e20bac..5d955b6582e 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -3,7 +3,7 @@ __docformat__ = "numpy" # IMPORTATION STANDARD import logging -from typing import List +from typing import List, Union # IMPORTATION THIRDPARTY from dotenv import set_key @@ -81,7 +81,7 @@ class FeatureFlagsController(BaseController): console.print(text=mt.menu_text, menu="Feature Flags") @staticmethod - def set_feature_flag(name: str, value: bool): + def set_feature_flag(name: str, value: Union[bool, str]): """Set feature flag Parameters From f56351a7c4283017c2d72ac652b5bda9ce7fd25a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 09:54:55 +0000 Subject: [PATCH 075/230] obbff in settings --- openbb_terminal/settings_controller.py | 33 +++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 1d1b460f278..89e4dbebeaa 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -16,6 +16,7 @@ from dotenv import set_key # IMPORTATION INTERNAL from openbb_terminal import config_plot as cfg_plot from openbb_terminal import feature_flags as obbff +from openbb_terminal import featflags_controller as obbff_ctrl from openbb_terminal.core.config.paths import USER_ENV_FILE, USER_DATA_DIRECTORY from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end @@ -152,8 +153,9 @@ class SettingsController(BaseController): @log_start_end(log=logger) def call_dt(self, _): """Process dt command""" - obbff.USE_DATETIME = not obbff.USE_DATETIME - set_key(USER_ENV_FILE, "OPENBB_USE_DATETIME", str(obbff.USE_DATETIME)) + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_DATETIME", not obbff.USE_DATETIME + ) @log_start_end(log=logger) def call_source(self, other_args: List[str]): @@ -187,21 +189,17 @@ class SettingsController(BaseController): except Exception as e: console.print("Couldn't open the sources file!") console.print(e) - obbff.PREFERRED_DATA_SOURCE_FILE = ns_parser.value - set_key( - USER_ENV_FILE, - "OPENBB_PREFERRED_DATA_SOURCE_FILE", - str(ns_parser.value), + + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.value ) + obbff.PREFERRED_DATA_SOURCE_FILE = ns_parser.value @log_start_end(log=logger) def call_autoscaling(self, _): """Process autoscaling command""" - obbff.USE_PLOT_AUTOSCALING = not obbff.USE_PLOT_AUTOSCALING - set_key( - USER_ENV_FILE, - "OPENBB_USE_PLOT_AUTOSCALING", - str(obbff.USE_PLOT_AUTOSCALING), + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_PLOT_AUTOSCALING", not obbff.USE_PLOT_AUTOSCALING ) @log_start_end(log=logger) @@ -402,8 +400,9 @@ class SettingsController(BaseController): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: if ns_parser.value: - set_key(USER_ENV_FILE, "OPENBB_USE_LANGUAGE", str(ns_parser.value)) - obbff.USE_LANGUAGE = ns_parser.value + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_LANGUAGE", ns_parser.value + ) else: console.print( f"Languages available: {', '.join(self.languages_available)}" @@ -460,8 +459,10 @@ class SettingsController(BaseController): ns_parser.emoji = "" else: ns_parser.emoji = " ".join(ns_parser.emoji) - set_key(USER_ENV_FILE, "OPENBB_USE_FLAIR", str(ns_parser.emoji)) - obbff.USE_FLAIR = ns_parser.emoji + + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_FLAIR", ns_parser.emoji + ) @log_start_end(log=logger) def call_userdata(self, other_args: List[str]): From 550345cf56c88aa106624d685ef40792a54c0fec Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 10:06:16 +0000 Subject: [PATCH 076/230] add upload to cfg_plot 1 --- openbb_terminal/settings_controller.py | 63 ++++++++++++++++++-------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 89e4dbebeaa..fcd655390e8 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -7,7 +7,7 @@ import os.path import argparse import logging from pathlib import Path -from typing import List +from typing import List, Union import pytz # IMPORTATION THIRDPARTY @@ -29,6 +29,8 @@ from openbb_terminal.helper_funcs import ( from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText +from openbb_terminal.session.hub_model import patch_user_configs +from openbb_terminal.session.user import User # pylint: disable=too-many-lines,no-member,too-many-public-methods,C0302 # pylint: disable=import-outside-toplevel @@ -135,6 +137,38 @@ class SettingsController(BaseController): mt.add_raw("\n") console.print(text=mt.menu_text, menu="Settings") + @staticmethod + def set_cfg_plot(name: str, value: Union[bool, str]): + """Set cfg_plot attribute + + Parameters + ---------- + name : str + Environment variable name + value : str + Environment variable value + persist : bool, optional + Persist feature flag, by default False + """ + + if User.is_guest(): + set_key(str(USER_ENV_FILE), name, str(value)) + + # Remove "OPENBB_" prefix from env_var + if name.startswith("OPENBB_"): + name = name[7:] + + # Set obbff.env_var_name = not env_var_value + setattr(cfg_plot, name, value) + + # Send feature flag to server + if not User.is_guest(): + patch_user_configs( + key=name, + value=str(value), + type_="settings", + ) + @log_start_end(log=logger) def call_colors(self, _): """Process colors command""" @@ -193,7 +227,6 @@ class SettingsController(BaseController): obbff_ctrl.FeatureFlagsController.set_feature_flag( "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.value ) - obbff.PREFERRED_DATA_SOURCE_FILE = ns_parser.value @log_start_end(log=logger) def call_autoscaling(self, _): @@ -223,8 +256,7 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser and ns_parser.value: - set_key(USER_ENV_FILE, "OPENBB_PLOT_DPI", str(ns_parser.value)) - cfg_plot.PLOT_DPI = ns_parser.value + SettingsController.set_cfg_plot("OPENBB_PLOT_DPI", ns_parser.value) @log_start_end(log=logger) def call_height(self, other_args: List[str]): @@ -247,8 +279,7 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key(USER_ENV_FILE, "OPENBB_PLOT_HEIGHT", str(ns_parser.value)) - cfg_plot.PLOT_HEIGHT = ns_parser.value + SettingsController.set_cfg_plot("OPENBB_PLOT_HEIGHT", ns_parser.value) @log_start_end(log=logger) def call_width(self, other_args: List[str]): @@ -271,8 +302,7 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key(USER_ENV_FILE, "OPENBB_PLOT_WIDTH", str(ns_parser.value)) - cfg_plot.PLOT_WIDTH = ns_parser.value + SettingsController.set_cfg_plot("OPENBB_PLOT_WIDTH", ns_parser.value) @log_start_end(log=logger) def call_pheight(self, other_args: List[str]): @@ -294,12 +324,9 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key( - USER_ENV_FILE, - "OPENBB_PLOT_HEIGHT_PERCENTAGE", - str(ns_parser.value), + SettingsController.set_cfg_plot( + "OPENBB_PLOT_HEIGHT_PERCENTAGE", ns_parser.value ) - cfg_plot.PLOT_HEIGHT_PERCENTAGE = ns_parser.value @log_start_end(log=logger) def call_pwidth(self, other_args: List[str]): @@ -321,12 +348,9 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key( - USER_ENV_FILE, - "OPENBB_PLOT_WIDTH_PERCENTAGE", - str(ns_parser.value), + SettingsController.set_cfg_plot( + "OPENBB_PLOT_WIDTH_PERCENTAGE", ns_parser.value ) - cfg_plot.PLOT_WIDTH_PERCENTAGE = ns_parser.value @log_start_end(log=logger) def call_monitor(self, other_args: List[str]): @@ -348,8 +372,7 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key(USER_ENV_FILE, "OPENBB_MONITOR", str(ns_parser.value)) - cfg_plot.MONITOR = ns_parser.value + SettingsController.set_cfg_plot("OPENBB_MONITOR", ns_parser.value) @log_start_end(log=logger) def call_backend(self, other_args: List[str]): From 0029e1aa3b70100678ecbbfdc6e8f02328a919c3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 10:21:30 +0000 Subject: [PATCH 077/230] data sources + backend --- openbb_terminal/settings_controller.py | 28 ++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index fcd655390e8..0c81f67ae85 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -7,7 +7,7 @@ import os.path import argparse import logging from pathlib import Path -from typing import List, Union +from typing import List, Optional, Union import pytz # IMPORTATION THIRDPARTY @@ -138,8 +138,8 @@ class SettingsController(BaseController): console.print(text=mt.menu_text, menu="Settings") @staticmethod - def set_cfg_plot(name: str, value: Union[bool, str]): - """Set cfg_plot attribute + def set_cfg_plot(name: str, value: Optional[Union[bool, str]]): + """Set plot config attribute Parameters ---------- @@ -204,7 +204,7 @@ class SettingsController(BaseController): "-v", "--value", type=str, - default=os.getcwd() + os.path.sep + "sources.json.default", + default=str(USER_DATA_DIRECTORY / "data_sources_default.json"), dest="value", help="value", ) @@ -214,20 +214,20 @@ class SettingsController(BaseController): if ns_parser: try: - the_path = os.getcwd() + os.path.sep + ns_parser.value - console.print("Loading sources from " + the_path) + the_path = ns_parser.value + console.print("Loading sources from " + str(the_path)) with open(the_path): # Try to open the file to get an exception if the file doesn't exist pass + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.value + ) + except Exception as e: console.print("Couldn't open the sources file!") console.print(e) - obbff_ctrl.FeatureFlagsController.set_feature_flag( - "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.value - ) - @log_start_end(log=logger) def call_autoscaling(self, _): """Process autoscaling command""" @@ -394,11 +394,9 @@ class SettingsController(BaseController): other_args.insert(0, "-v") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - set_key(USER_ENV_FILE, "OPENBB_BACKEND", str(ns_parser.value)) - if ns_parser.value == "None": - cfg_plot.BACKEND = None # type: ignore - else: - cfg_plot.BACKEND = ns_parser.value + SettingsController.set_cfg_plot( + "OPENBB_BACKEND", None if ns_parser.value == "None" else ns_parser.value + ) @log_start_end(log=logger) def call_lang(self, other_args: List[str]): From d3508e03853faedd2080290446626ad6ebd264aa Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 10:35:30 +0000 Subject: [PATCH 078/230] this solves circular import? --- openbb_terminal/parent_classes.py | 2 +- openbb_terminal/session/session_controller.py | 26 ----------------- openbb_terminal/session/session_model.py | 29 +++++++++++++++++++ 3 files changed, 30 insertions(+), 27 deletions(-) create mode 100644 openbb_terminal/session/session_model.py diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 4a7bd18679b..4cbbb007c99 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -18,7 +18,7 @@ import pandas as pd from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.session.session_controller import logout +from openbb_terminal.session.session_model import logout from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import ( diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 71485ee7456..f9f5e4deb45 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,16 +1,11 @@ -import importlib -import os -import sys from typing import Tuple import json -import matplotlib.pyplot as plt import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller -from openbb_terminal.helper_funcs import system_clear def display_welcome_message(): @@ -89,27 +84,6 @@ def login(session: dict): login_prompt(welcome=True) -def logout(): - """Logout and clear session.""" - system_clear() - User.clear() - - # Clear openbb environment variables - for v in os.environ: - if v.startswith("OPENBB"): - os.environ.pop(v) - - # Reload all openbb modules to clear memorized variables - modules = sys.modules.copy() - for module in modules: - if module.startswith("openbb"): - importlib.reload(sys.modules[module]) - - Hub.delete_session() - Local.remove_session_file() - plt.close("all") - - def main(guest_allowed: bool = True): """Main function""" local_session = Local.get_session() diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py new file mode 100644 index 00000000000..6f3f87017a0 --- /dev/null +++ b/openbb_terminal/session/session_model.py @@ -0,0 +1,29 @@ +import importlib +import os +import sys +import matplotlib.pyplot as plt +import openbb_terminal.session.local_model as Local +import openbb_terminal.session.hub_model as Hub +from openbb_terminal.session.user import User +from openbb_terminal.helper_funcs import system_clear + + +def logout(): + """Logout and clear session.""" + system_clear() + User.clear() + + # Clear openbb environment variables + for v in os.environ: + if v.startswith("OPENBB"): + os.environ.pop(v) + + # Reload all openbb modules to clear memorized variables + modules = sys.modules.copy() + for module in modules: + if module.startswith("openbb"): + importlib.reload(sys.modules[module]) + + Hub.delete_session() + Local.remove_session_file() + plt.close("all") From 9d39fd108bd82042021f98043e0dd05f5c0d2396 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 10:55:59 +0000 Subject: [PATCH 079/230] move create_session to model --- openbb_terminal/session/session_controller.py | 11 ++--------- openbb_terminal/session/session_model.py | 9 +++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index f9f5e4deb45..e03df6aa5cc 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -2,6 +2,7 @@ from typing import Tuple import json import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub +from openbb_terminal.session.session_model import create_session from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console @@ -33,15 +34,6 @@ def get_user_input() -> Tuple[str, str, bool]: return email, password, save -def create_session(email: str, password: str, save: bool) -> dict: - """Create a session.""" - - session = Hub.get_session(email, password) - if session and save: - Local.save_session(session) - return session - - def login_prompt(welcome=True, guest_allowed=True): """Login prompt and launch terminal if login is successful. @@ -64,6 +56,7 @@ def login_prompt(welcome=True, guest_allowed=True): login(session=session) +# TODO: Move some login inside this function to the session_model.py def login(session: dict): """Login and launch terminal. diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 6f3f87017a0..a0a0b9303ca 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -8,6 +8,15 @@ from openbb_terminal.session.user import User from openbb_terminal.helper_funcs import system_clear +def create_session(email: str, password: str, save: bool) -> dict: + """Create a session.""" + + session = Hub.get_session(email, password) + if session and save: + Local.save_session(session) + return session + + def logout(): """Logout and clear session.""" system_clear() From c5830211137eb3b7aa6dd65642d134a26a49d957 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 11:02:00 +0000 Subject: [PATCH 080/230] rever logs changes, not needed for now --- .../core/log/generation/settings_logger.py | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/openbb_terminal/core/log/generation/settings_logger.py b/openbb_terminal/core/log/generation/settings_logger.py index 5cb18918023..46221e64114 100644 --- a/openbb_terminal/core/log/generation/settings_logger.py +++ b/openbb_terminal/core/log/generation/settings_logger.py @@ -65,7 +65,9 @@ def log_settings() -> None: logger.info("SETTINGS: %s ", json.dumps(settings_dict)) -def collect_config_terminal() -> dict: +def log_config_terminal() -> None: + """Log config_terminal""" + config_terminal_dict = {} for item in dir(cfg): @@ -78,17 +80,12 @@ def collect_config_terminal() -> dict: ): config_terminal_dict[item] = str(prop) - return config_terminal_dict - - -def log_config_terminal() -> None: - """Log config_terminal""" - - config_terminal_dict = collect_config_terminal() logger.info("CONFIG_TERMINAL: %s ", json.dumps(config_terminal_dict)) -def collect_feature_flags() -> dict: +def log_feature_flags() -> None: + """Log feature flags""" + feature_flags_dict = {} for item in dir(obbff): @@ -100,17 +97,12 @@ def collect_feature_flags() -> dict: ): feature_flags_dict[item] = str(prop) - return feature_flags_dict - - -def log_feature_flags() -> None: - """Log feature flags""" - - feature_flags_dict = collect_feature_flags() logger.info("FEATURE_FLAGS: %s ", json.dumps(feature_flags_dict)) -def collect_keys() -> dict: +def log_keys() -> None: + """Log keys""" + var_list = [v for v in dir(cfg) if v.startswith("API_")] current_keys = {} @@ -124,13 +116,6 @@ def collect_keys() -> dict: else: current_keys[cfg_var_name] = "not_defined" - return current_keys - - -def log_keys() -> None: - """Log keys""" - - current_keys = collect_keys() logger.info("KEYS: %s ", json.dumps(current_keys)) From 7bc106d22c089b3b226f86bbbd0356ed475e0f61 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 12:34:49 +0000 Subject: [PATCH 081/230] sync optionally --- openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/keys_model.py | 2 +- openbb_terminal/session/hub_model.py | 4 +- openbb_terminal/session/user.py | 65 +++++++++++++++++-------- openbb_terminal/settings_controller.py | 2 +- 5 files changed, 50 insertions(+), 25 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 5d955b6582e..4dcc3cc8a49 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -105,7 +105,7 @@ class FeatureFlagsController(BaseController): setattr(obbff, name, value) # Send feature flag to server - if not User.is_guest(): + if User.is_sync_enabled(): patch_user_configs( key=name, value=str(value), diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 7846d64e983..b80aace3a9d 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -241,7 +241,7 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non # Send api key to server if ( - not User.is_guest() + User.is_sync_enabled() and env_var_name not in cfg.SENSITIVE_KEYS and env_var_name.startswith("API_") ): diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index e630d7575b6..fdec40e512e 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -137,7 +137,7 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re try: response = requests.patch( url=BASE_URL + "terminal/user-json", - headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, + headers={"Authorization": User.get_token()}, json=data, ) if response.status_code == 200: @@ -174,7 +174,7 @@ def logout_everywhere() -> Optional[requests.Response]: try: response = requests.get( url=BASE_URL + "logout-everywhere", - headers={"Authorization": f"{User.TOKEN_TYPE.title()} {User.TOKEN}"}, + headers={"Authorization": User.get_token()}, ) if response.status_code == 200: console.print("[green]\nLogged out remotely.[/green]") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 9457b11d11c..405276fc3af 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -6,10 +6,11 @@ from openbb_terminal.rich_config import console @dataclass class User: - TOKEN_TYPE: str = "" - TOKEN: str = "" - EMAIL: str = "" - UUID: str = "" + _TOKEN_TYPE: str = "" + _TOKEN: str = "" + _EMAIL: str = "" + _UUID: str = "" + _SYNC_ENABLED: bool = True @classmethod def load_user_info(cls, session: dict): @@ -20,39 +21,63 @@ class User: session : dict The login info. """ - User.TOKEN_TYPE = session.get("token_type", "") - User.TOKEN = session.get("access_token", "") - User.UUID = session.get("uuid", "") + User._TOKEN_TYPE = session.get("token_type", "") + User._TOKEN = session.get("access_token", "") + User._UUID = session.get("uuid", "") - if User.TOKEN: - decoded_info = jwt.decode(User.TOKEN, options={"verify_signature": False}) - User.EMAIL = decoded_info.get("sub", "") + if User._TOKEN: + decoded_info = jwt.decode(User._TOKEN, options={"verify_signature": False}) + User._EMAIL = decoded_info.get("sub", "") MAX_FLAIR_LEN = 20 if obbff.USE_FLAIR == ":openbb": - username = User.EMAIL[: User.EMAIL.find("@")] - setattr(obbff, "USE_FLAIR", "[" + username[:MAX_FLAIR_LEN] + "] 🦋") + username = User._EMAIL[: User._EMAIL.find("@")] + if User._SYNC_ENABLED: + flair = "[" + username[:MAX_FLAIR_LEN] + "] 🟢" + else: + flair = "[" + username[:MAX_FLAIR_LEN] + "] 🔴" + setattr(obbff, "USE_FLAIR", flair) @classmethod def whoami(cls): """Display user info.""" - if User.UUID: - console.print(f"[info]email:[/info] {User.EMAIL}") - console.print(f"[info]uuid:[/info] {User.UUID}\n") + if User._UUID: + console.print(f"[info]email:[/info] {User._EMAIL}") + console.print(f"[info]uuid:[/info] {User._UUID}") + if User._SYNC_ENABLED: + sync = "enabled" + else: + sync = "disabled" + console.print(f"[info]sync:[/info] {sync}\n") else: console.print("[info]Only you know...[/info]\n") @classmethod def clear(cls): """Clear user info.""" - User.TOKEN_TYPE = "" - User.TOKEN = "" - User.EMAIL = "" - User.UUID = "" + User._TOKEN_TYPE = "" + User._TOKEN = "" + User._EMAIL = "" + User._UUID = "" obbff.USE_FLAIR = ":openbb" @classmethod def is_guest(cls): """Check if user is guest.""" - return not bool(User.TOKEN) + return not bool(User._TOKEN) + + @classmethod + def is_sync_enabled(cls): + """Check if sync is enabled.""" + return User._SYNC_ENABLED + + @classmethod + def toggle_sync(cls): + """Toggle sync.""" + User._SYNC_ENABLED = not User._SYNC_ENABLED + + @classmethod + def get_token(cls): + """Get token.""" + return f"{User._TOKEN_TYPE.title()} {User._TOKEN}" diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 0c81f67ae85..93d69c2fdcc 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -162,7 +162,7 @@ class SettingsController(BaseController): setattr(cfg_plot, name, value) # Send feature flag to server - if not User.is_guest(): + if User.is_sync_enabled(): patch_user_configs( key=name, value=str(value), From ff979e361562a8685464a92cc7c22bec33c7a22b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 13:33:53 +0000 Subject: [PATCH 082/230] outline account controller interface --- openbb_terminal/account/account_controller.py | 88 +++++++++++++++++++ openbb_terminal/featflags_controller.py | 8 +- openbb_terminal/feature_flags.py | 2 + openbb_terminal/miscellaneous/i18n/en.yml | 8 +- openbb_terminal/session/local_model.py | 53 +++++++---- openbb_terminal/session/user.py | 29 +++--- openbb_terminal/terminal_controller.py | 8 ++ 7 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 openbb_terminal/account/account_controller.py diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py new file mode 100644 index 00000000000..060cf5ae610 --- /dev/null +++ b/openbb_terminal/account/account_controller.py @@ -0,0 +1,88 @@ +import logging +from typing import List, Dict +from pathlib import Path + +from prompt_toolkit.completion import NestedCompleter +from openbb_terminal.featflags_controller import FeatureFlagsController + +from openbb_terminal.parent_classes import BaseController +from openbb_terminal.rich_config import console, MenuText +from openbb_terminal.decorators import log_start_end +from openbb_terminal.core.config.paths import ( + USER_ROUTINES_DIRECTORY, +) +from openbb_terminal import feature_flags as obbff +from openbb_terminal.menu import session +from openbb_terminal.session.user import User + +logger = logging.getLogger(__name__) + + +class AccountController(BaseController): + """Account Controller Class""" + + CHOICES_COMMANDS = [ + "sync", + "force", + "upload", + "download", + ] + + PATH = "/account/" + + def __init__(self, queue: List[str] = None): + super().__init__(queue) + self.ROUTINE_FILES: Dict[str, Path] = {} + self.ROUTINE_CHOICES: Dict[str, None] = {} + self.update_runtime_choices() + + def update_runtime_choices(self): + """Update runtime choices""" + self.ROUTINE_FILES = { + filepath.name: filepath + for filepath in USER_ROUTINES_DIRECTORY.rglob("*.openbb") + } + self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} + if session and obbff.USE_PROMPT_TOOLKIT: + choices: dict = {c: {} for c in self.controller_choices} + choices["send"] = self.ROUTINE_CHOICES + + self.completer = NestedCompleter.from_nested_dict(choices) + + def print_help(self): + """Print help""" + + mt = MenuText("account/", 100) + mt.add_info("_info_") + mt.add_cmd("sync") + mt.add_cmd("force") + mt.add_raw("\n") + mt.add_info("_routines_") + mt.add_cmd("upload") + mt.add_cmd("download") + console.print(text=mt.menu_text, menu="Account") + + @log_start_end(log=logger) + def call_sync(self, _): + """Sync""" + FeatureFlagsController.set_feature_flag( + "OPENBB_SYNC_ENABLED", not obbff.SYNC_ENABLED, force=True + ) + User.update_flair() + + def call_force(self, _): + """Force sync""" + console.print("Pull, Diff, Push") + pass + + @log_start_end(log=logger) + def call_upload(self, _): + """Upload""" + console.print("Upload routine") + pass + + @log_start_end(log=logger) + def call_download(self, _): + """Download""" + console.print("Download routine") + pass diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 4dcc3cc8a49..9bc44e4833b 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -81,7 +81,7 @@ class FeatureFlagsController(BaseController): console.print(text=mt.menu_text, menu="Feature Flags") @staticmethod - def set_feature_flag(name: str, value: Union[bool, str]): + def set_feature_flag(name: str, value: Union[bool, str], force: bool = False): """Set feature flag Parameters @@ -90,8 +90,8 @@ class FeatureFlagsController(BaseController): Environment variable name value : str Environment variable value - persist : bool, optional - Persist feature flag, by default False + force : bool, optional + Force set feature flag, by default False """ if User.is_guest(): @@ -105,7 +105,7 @@ class FeatureFlagsController(BaseController): setattr(obbff, name, value) # Send feature flag to server - if User.is_sync_enabled(): + if User.is_sync_enabled() or force: patch_user_configs( key=name, value=str(value), diff --git a/openbb_terminal/feature_flags.py b/openbb_terminal/feature_flags.py index fde5db81791..22337629c84 100644 --- a/openbb_terminal/feature_flags.py +++ b/openbb_terminal/feature_flags.py @@ -27,6 +27,8 @@ except ImportError: else: WITH_GIT = True +# Sync with OpenBB server +SYNC_ENABLED = load_env_vars("OPENBB_SYNC_ENABLED", strtobool, True, "featflags") # Always overwrite exported files FILE_OVERWITE = load_env_vars("OPENBB_FILE_OVERWRITE", strtobool, False, "featflags") diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index ee231f93bc1..85d54aef9d6 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -16,6 +16,12 @@ en: intro: introduction on the OpenBB Terminal _configure_: Configure your own terminal _main_menu_: Main menu + account/_info_: Synchronization + account/sync: Enable/disable sync + account/force: Force synchronization + account/_routines_: Routines + account/upload: Upload routine + account/download: Download routine keys/_keys_: This menu allows you to set your own API keys. Type 'about' for more information keys/mykeys: get your defined keys keys/_status_: Status @@ -88,7 +94,7 @@ en: forecast: timeseries forecasting with machine learning portfolio: perform portfolio optimization and look at portfolio performance and attribution dashboards: interactive dashboards using voila and jupyter notebooks - stocks/filings: the most-recent form submissions to the SEC + stocks/filings: the most-recent form submissions to the SEC stocks/search: search a specific stock ticker for analysis stocks/load: load a specific stock ticker and additional info for analysis stocks/_ticker: Stock diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 40e0179ec45..43ee703f4bd 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -8,6 +8,7 @@ from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg from openbb_terminal import config_plot as cfg_plot from openbb_terminal.base_helpers import strtobool +from openbb_terminal.session.user import User SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" @@ -84,23 +85,45 @@ def apply_configs(configs: dict): The configurations. """ console.print(configs, style="red") - if configs: - keys = configs.get("features_keys", {}) - if keys: - for k, v in keys.items(): - if hasattr(cfg, k): - setattr(cfg, k, v) - # Since settings come from different files we use the format {var_name: "path:value"} + if configs: settings = configs.get("features_settings", {}) - if settings: - for k, v in settings.items(): - if hasattr(obbff, k): - cast_set_attr(obbff, k, v) - elif hasattr(cfg, k): - cast_set_attr(cfg, k, v) - elif hasattr(cfg_plot, k): - cast_set_attr(cfg_plot, k, v) + obbff.SYNC_ENABLED = is_sync_enabled(settings) + User.update_flair() + if obbff.SYNC_ENABLED: + console.print("Applying...", style="red") + if settings: + for k, v in settings.items(): + if hasattr(obbff, k): + cast_set_attr(obbff, k, v) + elif hasattr(cfg, k): + cast_set_attr(cfg, k, v) + elif hasattr(cfg_plot, k): + cast_set_attr(cfg_plot, k, v) + + keys = configs.get("features_keys", {}) + if keys: + for k, v in keys.items(): + if hasattr(cfg, k): + setattr(cfg, k, v) + + +def is_sync_enabled(settings: dict) -> bool: + """Check if sync is enabled. + + Parameters + ---------- + settings : dict + The settings. + + Returns + ------- + bool + The status of sync. + """ + if settings.get("SYNC_ENABLED", "").lower() == "false": + return False + return True def cast_set_attr(obj, name, value): diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 405276fc3af..f5fef75a425 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -10,7 +10,6 @@ class User: _TOKEN: str = "" _EMAIL: str = "" _UUID: str = "" - _SYNC_ENABLED: bool = True @classmethod def load_user_info(cls, session: dict): @@ -29,15 +28,21 @@ class User: decoded_info = jwt.decode(User._TOKEN, options={"verify_signature": False}) User._EMAIL = decoded_info.get("sub", "") - MAX_FLAIR_LEN = 20 + User.update_flair() - if obbff.USE_FLAIR == ":openbb": - username = User._EMAIL[: User._EMAIL.find("@")] - if User._SYNC_ENABLED: - flair = "[" + username[:MAX_FLAIR_LEN] + "] 🟢" - else: - flair = "[" + username[:MAX_FLAIR_LEN] + "] 🔴" - setattr(obbff, "USE_FLAIR", flair) + @staticmethod + def update_flair(): + MAX_FLAIR_LEN = 20 + username = User._EMAIL[: User._EMAIL.find("@")] + username = "[" + username[:MAX_FLAIR_LEN] + "]" + + if obbff.USE_FLAIR == ":openbb" or username in obbff.USE_FLAIR: + + if obbff.SYNC_ENABLED: + flair = username + " 🟢" + else: + flair = username + " 🔴" + setattr(obbff, "USE_FLAIR", flair) @classmethod def whoami(cls): @@ -45,7 +50,7 @@ class User: if User._UUID: console.print(f"[info]email:[/info] {User._EMAIL}") console.print(f"[info]uuid:[/info] {User._UUID}") - if User._SYNC_ENABLED: + if obbff.SYNC_ENABLED: sync = "enabled" else: sync = "disabled" @@ -70,12 +75,12 @@ class User: @classmethod def is_sync_enabled(cls): """Check if sync is enabled.""" - return User._SYNC_ENABLED + return obbff.SYNC_ENABLED @classmethod def toggle_sync(cls): """Toggle sync.""" - User._SYNC_ENABLED = not User._SYNC_ENABLED + obbff.SYNC_ENABLED = not obbff.SYNC_ENABLED @classmethod def get_token(cls): diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index b7ad89ead2c..86a852a0d0b 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -82,6 +82,7 @@ class TerminalController(BaseController): """Terminal Controller class.""" CHOICES_COMMANDS = [ + "account", "keys", "settings", "survey", @@ -166,6 +167,7 @@ class TerminalController(BaseController): mt.add_cmd("stop") mt.add_raw("\n") mt.add_info("_configure_") + mt.add_menu("account") mt.add_menu("keys") mt.add_menu("featflags") mt.add_menu("sources") @@ -354,6 +356,12 @@ class TerminalController(BaseController): "https://openbb.co/products/terminal#get-started\n" ) + def call_account(self, _): + """Process account command.""" + from openbb_terminal.account.account_controller import AccountController + + self.queue = self.load_class(AccountController, self.queue) + def call_keys(self, _): """Process keys command.""" from openbb_terminal.keys_controller import KeysController From 94894bb977accb26a89e87e0fb9223500b0f0bb9 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 14:22:30 +0000 Subject: [PATCH 083/230] don't sync when guest --- openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/keys_model.py | 3 ++- openbb_terminal/settings_controller.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 9bc44e4833b..b5e7113ff0b 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -105,7 +105,7 @@ class FeatureFlagsController(BaseController): setattr(obbff, name, value) # Send feature flag to server - if User.is_sync_enabled() or force: + if not not User.is_guest() and User.is_sync_enabled() or force: patch_user_configs( key=name, value=str(value), diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index b80aace3a9d..e997c85bd91 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -241,7 +241,8 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non # Send api key to server if ( - User.is_sync_enabled() + not User.is_guest() + and User.is_sync_enabled() and env_var_name not in cfg.SENSITIVE_KEYS and env_var_name.startswith("API_") ): diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 93d69c2fdcc..cf6bb1b1d77 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -162,7 +162,7 @@ class SettingsController(BaseController): setattr(cfg_plot, name, value) # Send feature flag to server - if User.is_sync_enabled(): + if not not User.is_guest() and User.is_sync_enabled(): patch_user_configs( key=name, value=str(value), From b8651cad1b250d93e38828c11eb7a211455ba345 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 14:40:10 +0000 Subject: [PATCH 084/230] block guest on installer --- openbb_terminal/session/session_controller.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index e03df6aa5cc..74e4bd34129 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -47,8 +47,11 @@ def login_prompt(welcome=True, guest_allowed=True): while True: email, password, save = get_user_input() - if not email and not password and guest_allowed: - return terminal_controller.parse_args_and_run() + if not email and not password: + if guest_allowed: + return terminal_controller.parse_args_and_run() + continue + session = create_session(email, password, save) if isinstance(session, dict) and session: break From 279502214094a6100e7069d07c4727e4c0878b46 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 14:43:11 +0000 Subject: [PATCH 085/230] or not and --- openbb_terminal/session/session_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 74e4bd34129..39e2b0eb8ba 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -47,7 +47,7 @@ def login_prompt(welcome=True, guest_allowed=True): while True: email, password, save = get_user_input() - if not email and not password: + if not email or not password: if guest_allowed: return terminal_controller.parse_args_and_run() continue From 271311532e5cc93f424c2908d61ca13e4d2200d9 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 15:35:51 +0000 Subject: [PATCH 086/230] start routines --- openbb_terminal/account/account_controller.py | 49 ++++++++++++---- openbb_terminal/session/hub_model.py | 56 +++++++++++++++++++ openbb_terminal/session/local_model.py | 27 ++++++++- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 060cf5ae610..4e59b62c0b6 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,19 +1,19 @@ +import argparse import logging from typing import List, Dict from pathlib import Path from prompt_toolkit.completion import NestedCompleter from openbb_terminal.featflags_controller import FeatureFlagsController - from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import ( - USER_ROUTINES_DIRECTORY, -) +from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session from openbb_terminal.session.user import User +from openbb_terminal.session import hub_model as Hub +from openbb_terminal.session import local_model as Local logger = logging.getLogger(__name__) @@ -45,7 +45,8 @@ class AccountController(BaseController): self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} - choices["send"] = self.ROUTINE_CHOICES + choices["upload"]["--file"] = self.ROUTINE_CHOICES + choices["upload"]["-f"] = choices["upload"]["--file"] self.completer = NestedCompleter.from_nested_dict(choices) @@ -76,13 +77,41 @@ class AccountController(BaseController): pass @log_start_end(log=logger) - def call_upload(self, _): + def call_upload(self, other_args: List[str]): """Upload""" - console.print("Upload routine") - pass + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="load", + description="Load your portfolio transactions.", + ) + parser.add_argument( + "-f", + "--file", + type=str, + dest="file", + required="-h" not in other_args, + help="The file to be loaded", + choices={c: {} for c in self.ROUTINE_FILES}, + metavar="FILE", + ) + parser.add_argument( + "-n", + "--name", + type=str, + dest="name", + required="-h" not in other_args, + help="The name of the routine", + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-f") + ns_parser = self.parse_known_args_and_warn(parser, other_args) + if ns_parser: + routine = Local.get_routine(ns_parser.file) + if routine: + Hub.upload_routine(ns_parser.name, routine) @log_start_end(log=logger) def call_download(self, _): """Download""" - console.print("Download routine") - pass + Hub.download_routine() diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index fdec40e512e..759d7248357 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -187,3 +187,59 @@ def logout_everywhere() -> Optional[requests.Response]: except Exception: console.print("[red]\nFailed to logout remotely.[/red]") return None + + +def upload_routine( + name: str = "", + routine: str = "", +) -> Optional[requests.Response]: + """Send a routine to the server.""" + + data = {"name": name, "script": routine} + + try: + response = requests.post( + headers={"Authorization": User.get_token()}, + url=BASE_URL + "terminal/script", + json=data, + ) + if response.status_code == 200: + console.print("[green]Successfully uploaded your routine.[/green]") + else: + console.print("[red]Error uploading your routine.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print("[red]Connection error.[/red]") + return None + except Exception: + console.print("[red]Failed to upload your routine.[/red]") + return None + + +def download_routine( + name: str = "", +) -> Optional[requests.Response]: + """Download a routine from the server.""" + console.print("[red]Not implemented yet.[/red]") + + # TODO: Implement when endpoint is ready. + + # data = {"name": name} + + # try: + # response = requests.get( + # headers={"Authorization": User.get_token()}, + # url=BASE_URL + "terminal/script", + # json=data, + # ) + # if response.status_code == 200: + # console.print("Successfully downloaded your routine.") + # else: + # console.print("[red]Error downloading your routine.[/red]") + # return response + # except requests.exceptions.ConnectionError: + # console.print("[red]Connection error.[/red]") + # return None + # except Exception: + # console.print("[red]Failed to download your routine.[/red]") + # return None diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 43ee703f4bd..152c5add020 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -1,7 +1,11 @@ import os from pathlib import Path import json -from openbb_terminal.core.config.paths import SETTINGS_DIRECTORY +from typing import Optional +from openbb_terminal.core.config.paths import ( + SETTINGS_DIRECTORY, + USER_ROUTINES_DIRECTORY, +) from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff @@ -88,10 +92,12 @@ def apply_configs(configs: dict): if configs: settings = configs.get("features_settings", {}) + + # TODO: Find a cleaner way to check if user has sync enabled. obbff.SYNC_ENABLED = is_sync_enabled(settings) User.update_flair() + if obbff.SYNC_ENABLED: - console.print("Applying...", style="red") if settings: for k, v in settings.items(): if hasattr(obbff, k): @@ -144,3 +150,20 @@ def cast_set_attr(obj, name, value): setattr(obj, name, int(value)) else: setattr(obj, name, value) + + +def get_routine(name: str) -> Optional[str]: + """Get the routine. + + Returns + ------- + str + The routine. + """ + try: + with open(USER_ROUTINES_DIRECTORY / name) as f: + routine = "".join(f.readlines()) + return routine + except Exception: + console.print("[red]\nFailed to find routine.[/red]") + return None From 129270d55443a2590cf57ff017f1548850888c75 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:02:27 +0000 Subject: [PATCH 087/230] fix bug on ff, settings --- openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/session/local_model.py | 5 +++-- openbb_terminal/settings_controller.py | 2 +- openbb_terminal/terminal_helper.py | 8 ++++++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index b5e7113ff0b..d3cd3609581 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -105,7 +105,7 @@ class FeatureFlagsController(BaseController): setattr(obbff, name, value) # Send feature flag to server - if not not User.is_guest() and User.is_sync_enabled() or force: + if not User.is_guest() and User.is_sync_enabled() or force: patch_user_configs( key=name, value=str(value), diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 152c5add020..b43cb8a2d7d 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -127,8 +127,9 @@ def is_sync_enabled(settings: dict) -> bool: bool The status of sync. """ - if settings.get("SYNC_ENABLED", "").lower() == "false": - return False + if settings: + if settings.get("SYNC_ENABLED", "").lower() == "false": + return False return True diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index cf6bb1b1d77..d61fbf3aa29 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -162,7 +162,7 @@ class SettingsController(BaseController): setattr(cfg_plot, name, value) # Send feature flag to server - if not not User.is_guest() and User.is_sync_enabled(): + if not User.is_guest() and User.is_sync_enabled(): patch_user_configs( key=name, value=str(value), diff --git a/openbb_terminal/terminal_helper.py b/openbb_terminal/terminal_helper.py index d7dfa82cd2a..11cdb2ef90e 100644 --- a/openbb_terminal/terminal_helper.py +++ b/openbb_terminal/terminal_helper.py @@ -212,6 +212,14 @@ def is_packaged_application() -> bool: return LOGGING_APP_NAME == "gst_packaged" +def is_installer() -> bool: + """Tell whether or not it is a packaged version.""" + # TODO: Merge this with is_packaged_application + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return True + return False + + def bootup(): if sys.platform == "win32": # Enable VT100 Escape Sequence for WINDOWS 10 Ver. 1607 From 658d387940362ffc670b1f794492fe9ef83f456c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:02:43 +0000 Subject: [PATCH 088/230] is installer --- terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal.py b/terminal.py index 3850e0ddce0..9fbd3770ac0 100644 --- a/terminal.py +++ b/terminal.py @@ -2,7 +2,7 @@ import multiprocessing import sys from openbb_terminal.core.integration_tests import integration_controller from openbb_terminal.session import session_controller -from openbb_terminal.terminal_helper import is_packaged_application +from openbb_terminal.terminal_helper import is_installer if __name__ == "__main__": multiprocessing.freeze_support() @@ -10,4 +10,4 @@ if __name__ == "__main__": if "-t" in sent_args or "--test" in sent_args: integration_controller.main() else: - session_controller.main(guest_allowed=not is_packaged_application()) + session_controller.main(guest_allowed=not is_installer()) From 5577d25d85cc8df99bdc72a8df2caf2c6b2fd325 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:23:14 +0000 Subject: [PATCH 089/230] implementing chavi suggestions --- openbb_terminal/account/account_controller.py | 114 +++++++++--------- openbb_terminal/miscellaneous/i18n/en.yml | 5 +- openbb_terminal/session/local_model.py | 1 - openbb_terminal/session/user.py | 11 +- 4 files changed, 66 insertions(+), 65 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 4e59b62c0b6..07c8e2433c8 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -12,8 +12,9 @@ from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session from openbb_terminal.session.user import User -from openbb_terminal.session import hub_model as Hub -from openbb_terminal.session import local_model as Local + +# from openbb_terminal.session import hub_model as Hub +# from openbb_terminal.session import local_model as Local logger = logging.getLogger(__name__) @@ -23,9 +24,10 @@ class AccountController(BaseController): CHOICES_COMMANDS = [ "sync", - "force", - "upload", - "download", + "pull", + "defaults", + # "upload", + # "download", ] PATH = "/account/" @@ -45,8 +47,8 @@ class AccountController(BaseController): self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} - choices["upload"]["--file"] = self.ROUTINE_CHOICES - choices["upload"]["-f"] = choices["upload"]["--file"] + # choices["upload"]["--file"] = self.ROUTINE_CHOICES + # choices["upload"]["-f"] = choices["upload"]["--file"] self.completer = NestedCompleter.from_nested_dict(choices) @@ -56,11 +58,11 @@ class AccountController(BaseController): mt = MenuText("account/", 100) mt.add_info("_info_") mt.add_cmd("sync") - mt.add_cmd("force") - mt.add_raw("\n") - mt.add_info("_routines_") - mt.add_cmd("upload") - mt.add_cmd("download") + mt.add_cmd("pull") + mt.add_cmd("defaults") + # mt.add_info("_routines_") + # mt.add_cmd("upload") + # mt.add_cmd("download") console.print(text=mt.menu_text, menu="Account") @log_start_end(log=logger) @@ -69,49 +71,53 @@ class AccountController(BaseController): FeatureFlagsController.set_feature_flag( "OPENBB_SYNC_ENABLED", not obbff.SYNC_ENABLED, force=True ) - User.update_flair() - def call_force(self, _): - """Force sync""" - console.print("Pull, Diff, Push") + def call_pull(self, _): + """Pull data""" + console.print("Pull, Diff and Merge.") pass - @log_start_end(log=logger) - def call_upload(self, other_args: List[str]): - """Upload""" - parser = argparse.ArgumentParser( - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="load", - description="Load your portfolio transactions.", - ) - parser.add_argument( - "-f", - "--file", - type=str, - dest="file", - required="-h" not in other_args, - help="The file to be loaded", - choices={c: {} for c in self.ROUTINE_FILES}, - metavar="FILE", - ) - parser.add_argument( - "-n", - "--name", - type=str, - dest="name", - required="-h" not in other_args, - help="The name of the routine", - ) - if other_args and "-" not in other_args[0][0]: - other_args.insert(0, "-f") - ns_parser = self.parse_known_args_and_warn(parser, other_args) - if ns_parser: - routine = Local.get_routine(ns_parser.file) - if routine: - Hub.upload_routine(ns_parser.name, routine) + def call_defaults(self, _): + """Reset to defaults""" + console.print("No implemented yet.") + pass - @log_start_end(log=logger) - def call_download(self, _): - """Download""" - Hub.download_routine() + # @log_start_end(log=logger) + # def call_upload(self, other_args: List[str]): + # """Upload""" + # parser = argparse.ArgumentParser( + # add_help=False, + # formatter_class=argparse.ArgumentDefaultsHelpFormatter, + # prog="load", + # description="Load your portfolio transactions.", + # ) + # parser.add_argument( + # "-f", + # "--file", + # type=str, + # dest="file", + # required="-h" not in other_args, + # help="The file to be loaded", + # choices={c: {} for c in self.ROUTINE_FILES}, + # metavar="FILE", + # ) + # parser.add_argument( + # "-n", + # "--name", + # type=str, + # dest="name", + # required="-h" not in other_args, + # help="The name of the routine", + # ) + # if other_args and "-" not in other_args[0][0]: + # other_args.insert(0, "-f") + # ns_parser = self.parse_known_args_and_warn(parser, other_args) + # if ns_parser: + # routine = Local.get_routine(ns_parser.file) + # if routine: + # Hub.upload_routine(ns_parser.name, routine) + + # @log_start_end(log=logger) + # def call_download(self, _): + # """Download""" + # Hub.download_routine() diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 85d54aef9d6..4816bb7c2cf 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -17,8 +17,9 @@ en: _configure_: Configure your own terminal _main_menu_: Main menu account/_info_: Synchronization - account/sync: Enable/disable sync - account/force: Force synchronization + account/sync: Send keys, settings and features flags on changes + account/pull: Pull keys, settings and feature flags from server + account/defaults: Reset to defaults account/_routines_: Routines account/upload: Upload routine account/download: Download routine diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index b43cb8a2d7d..04a1ca26b75 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -95,7 +95,6 @@ def apply_configs(configs: dict): # TODO: Find a cleaner way to check if user has sync enabled. obbff.SYNC_ENABLED = is_sync_enabled(settings) - User.update_flair() if obbff.SYNC_ENABLED: if settings: diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index f5fef75a425..238f38969c1 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -37,12 +37,7 @@ class User: username = "[" + username[:MAX_FLAIR_LEN] + "]" if obbff.USE_FLAIR == ":openbb" or username in obbff.USE_FLAIR: - - if obbff.SYNC_ENABLED: - flair = username + " 🟢" - else: - flair = username + " 🔴" - setattr(obbff, "USE_FLAIR", flair) + setattr(obbff, "USE_FLAIR", username + " 🦋") @classmethod def whoami(cls): @@ -51,9 +46,9 @@ class User: console.print(f"[info]email:[/info] {User._EMAIL}") console.print(f"[info]uuid:[/info] {User._UUID}") if obbff.SYNC_ENABLED: - sync = "enabled" + sync = "ON" else: - sync = "disabled" + sync = "OFF" console.print(f"[info]sync:[/info] {sync}\n") else: console.print("[info]Only you know...[/info]\n") From da8031640f9f01c2015e77c31369f80134ac34d3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:32:44 +0000 Subject: [PATCH 090/230] fix colors on installer prompt --- openbb_terminal/session/session_controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 39e2b0eb8ba..6141f741f65 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -12,7 +12,7 @@ from openbb_terminal import terminal_controller def display_welcome_message(): """Display welcome message""" with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: - console.print(f.read(), style="blue") + console.print(f.read(), style="menu") def get_user_input() -> Tuple[str, str, bool]: @@ -24,9 +24,9 @@ def get_user_input() -> Tuple[str, str, bool]: The user email, password and save login option. """ console.print("\nPlease enter your credentials:", style="info") - email = console.input("> Email: ", style="blue") - password = console.getpass("> Password: ", style="blue") - save_str = console.input("> Keep me logged in (y/n): ", style="blue").lower() + email = console.input("> Email: ", style="menu") + password = console.getpass("> Password: ", style="menu") + save_str = console.input("> Remember me? (y/n): ", style="menu").lower() save = False if save_str == "y": save = True From ea04594e96d18024b5bf5c8f1ab871ec2c676deb Mon Sep 17 00:00:00 2001 From: Henrique Joaquim Date: Thu, 26 Jan 2023 17:52:03 +0000 Subject: [PATCH 091/230] Logging login (#4046) * basic logging for the login * removing unused import * hacking the logs custom filter to not filter the email * black * cooping with the new changes * removing redundant method --- openbb_terminal/core/log/generation/common.py | 18 +++++++++++++ .../core/log/generation/custom_logger.py | 25 +++++++++++++++++ .../generation/formatter_with_exceptions.py | 7 +++++ .../core/log/generation/settings_logger.py | 17 +++--------- .../core/log/generation/user_logger.py | 27 +++++++++++++++++++ openbb_terminal/session/user.py | 10 +++++++ openbb_terminal/terminal_controller.py | 8 ++---- 7 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 openbb_terminal/core/log/generation/common.py create mode 100644 openbb_terminal/core/log/generation/custom_logger.py create mode 100644 openbb_terminal/core/log/generation/user_logger.py diff --git a/openbb_terminal/core/log/generation/common.py b/openbb_terminal/core/log/generation/common.py new file mode 100644 index 00000000000..f2a10557370 --- /dev/null +++ b/openbb_terminal/core/log/generation/common.py @@ -0,0 +1,18 @@ +# IMPORTATION STANDARD +import logging + +# IMPORTATION THIRDPARTY + + +# IMPORTATION INTERNAL +from openbb_terminal.core.log.generation.path_tracking_file_handler import ( + PathTrackingFileHandler, +) + + +def do_rollover(): + """RollOver the log file.""" + + for handler in logging.getLogger().handlers: + if isinstance(handler, PathTrackingFileHandler): + handler.doRollover() diff --git a/openbb_terminal/core/log/generation/custom_logger.py b/openbb_terminal/core/log/generation/custom_logger.py new file mode 100644 index 00000000000..42c7682a2a0 --- /dev/null +++ b/openbb_terminal/core/log/generation/custom_logger.py @@ -0,0 +1,25 @@ +# IMPORTATION STANDARD +import logging + +# IMPORTATION THIRDPARTY + + +# IMPORTATION INTERNAL +from openbb_terminal.loggers import setup_logging +from openbb_terminal.core.log.generation import settings_logger +from openbb_terminal.core.log.generation import user_logger +from openbb_terminal.core.log.generation.common import do_rollover + +logger = logging.getLogger(__name__) + + +def log_terminal(test_mode: bool): + """Logs for the terminal""" + + if not test_mode: + setup_logging() + + logger.info("START") + settings_logger.log_all_settings(with_rollover=False) + user_logger.log_user(with_rollover=False) + do_rollover() diff --git a/openbb_terminal/core/log/generation/formatter_with_exceptions.py b/openbb_terminal/core/log/generation/formatter_with_exceptions.py index 58a39f1639b..28c85a4e5a0 100644 --- a/openbb_terminal/core/log/generation/formatter_with_exceptions.py +++ b/openbb_terminal/core/log/generation/formatter_with_exceptions.py @@ -5,9 +5,13 @@ import re # IMPORTATION THIRDPARTY # IMPORTATION INTERNAL +from openbb_terminal.core.log.generation.user_logger import __name__ as user_logger_name from openbb_terminal.core.config.paths import HOME_DIRECTORY from openbb_terminal.core.log.generation.settings import AppSettings +# considering starting at 0 and after the logging prefix i.e., just the text to be filtered +MODULE_POSITION = 1 + class FormatterWithExceptions(logging.Formatter): """Logging Formatter that includes formatting of Exceptions""" @@ -54,6 +58,9 @@ class FormatterWithExceptions(logging.Formatter): @staticmethod def mock_email(text: str) -> str: + if text.split("|")[MODULE_POSITION] == user_logger_name: + return text + pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" replacement = " FILTERED_EMAIL " text_mocked = re.sub(pattern, replacement, text) diff --git a/openbb_terminal/core/log/generation/settings_logger.py b/openbb_terminal/core/log/generation/settings_logger.py index 46221e64114..3cb5980b95c 100644 --- a/openbb_terminal/core/log/generation/settings_logger.py +++ b/openbb_terminal/core/log/generation/settings_logger.py @@ -11,9 +11,7 @@ from types import FunctionType, ModuleType import openbb_terminal.feature_flags as obbff from openbb_terminal import config_terminal as cfg from openbb_terminal.terminal_helper import is_packaged_application -from openbb_terminal.core.log.generation.path_tracking_file_handler import ( - PathTrackingFileHandler, -) +from openbb_terminal.core.log.generation.common import do_rollover SENSITIVE_WORDS = [ @@ -31,14 +29,15 @@ SENSITIVE_WORDS = [ logger = logging.getLogger(__name__) -def log_all_settings() -> None: +def log_all_settings(with_rollover: bool = True) -> None: """Log all settings""" log_settings() log_config_terminal() log_feature_flags() log_keys() - do_rollover() + if with_rollover: + do_rollover() def log_settings() -> None: @@ -117,11 +116,3 @@ def log_keys() -> None: current_keys[cfg_var_name] = "not_defined" logger.info("KEYS: %s ", json.dumps(current_keys)) - - -def do_rollover(): - """RollOver the log file.""" - - for handler in logging.getLogger().handlers: - if isinstance(handler, PathTrackingFileHandler): - handler.doRollover() diff --git a/openbb_terminal/core/log/generation/user_logger.py b/openbb_terminal/core/log/generation/user_logger.py new file mode 100644 index 00000000000..2c117b2377c --- /dev/null +++ b/openbb_terminal/core/log/generation/user_logger.py @@ -0,0 +1,27 @@ +# IMPORTATION STANDARD +import json +import logging + +# IMPORTATION THIRDPARTY + + +# IMPORTATION INTERNAL +from openbb_terminal.session.user import User +from openbb_terminal.core.log.generation.common import do_rollover + +logger = logging.getLogger(__name__) + + +def log_user(with_rollover: bool = True): + """Log user""" + if not User.is_guest(): + log_user_info() + + if with_rollover: + do_rollover() + + +def log_user_info(): + """Log user info""" + user_info = {"user_uuid": User.get_uuid(), "user_email": User.get_email()} + logger.info("USER: %s ", json.dumps(user_info)) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 238f38969c1..4c181dadcfa 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -39,6 +39,16 @@ class User: if obbff.USE_FLAIR == ":openbb" or username in obbff.USE_FLAIR: setattr(obbff, "USE_FLAIR", username + " 🦋") + @classmethod + def get_uuid(cls): + """Get uuid.""" + return User._UUID + + @classmethod + def get_email(cls): + """Get email.""" + return User._EMAIL + @classmethod def whoami(cls): """Display user info.""" diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 86a852a0d0b..a2f6dc352c7 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -43,8 +43,6 @@ from openbb_terminal.helper_funcs import ( get_flair, EXPORT_ONLY_RAW_DATA_ALLOWED, ) -from openbb_terminal.loggers import setup_logging -from openbb_terminal.core.log.generation.settings_logger import log_all_settings from openbb_terminal.menu import session, is_papermill from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText @@ -62,6 +60,7 @@ from openbb_terminal.helper_funcs import parse_and_split_input from openbb_terminal.keys_model import first_time_user from openbb_terminal.common import feedparser_view from openbb_terminal.reports.reports_model import ipykernel_launcher +from openbb_terminal.core.log.generation.custom_logger import log_terminal # pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args # pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements @@ -807,10 +806,7 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): if module.startswith("openbb"): importlib.reload(sys.modules[module]) - if not test_mode: - setup_logging() - logger.info("START") - log_all_settings() + log_terminal(test_mode=test_mode) if jobs_cmds is not None and jobs_cmds: logger.info("INPUT: %s", "/".join(jobs_cmds)) From e2de1b4283a3d035394035eb57902f55a9c1cc8d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:56:27 +0000 Subject: [PATCH 092/230] ruff --- openbb_terminal/account/account_controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 07c8e2433c8..b9ee5bdff3d 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,4 +1,4 @@ -import argparse +# import argparse import logging from typing import List, Dict from pathlib import Path @@ -11,7 +11,8 @@ from openbb_terminal.decorators import log_start_end from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session -from openbb_terminal.session.user import User + +# from openbb_terminal.session.user import User # from openbb_terminal.session import hub_model as Hub # from openbb_terminal.session import local_model as Local From 7f8da32498ab65ced3dd8ce84d3e3699eacddcbe Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 17:56:44 +0000 Subject: [PATCH 093/230] ruff --- openbb_terminal/session/local_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 04a1ca26b75..0cf22bc04c6 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -12,7 +12,6 @@ from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg from openbb_terminal import config_plot as cfg_plot from openbb_terminal.base_helpers import strtobool -from openbb_terminal.session.user import User SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" From 7f49f139445aee5527904acb0bbdcf190eeb64f2 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 18:01:30 +0000 Subject: [PATCH 094/230] mypy --- openbb_terminal/session/hub_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 759d7248357..c9fe6e2c6ec 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -221,6 +221,7 @@ def download_routine( ) -> Optional[requests.Response]: """Download a routine from the server.""" console.print("[red]Not implemented yet.[/red]") + return None # TODO: Implement when endpoint is ready. From e4ebc1b3807f7c007c44da4a74d767677abb2608 Mon Sep 17 00:00:00 2001 From: Henrique Joaquim Date: Thu, 26 Jan 2023 18:02:45 +0000 Subject: [PATCH 095/230] SDK login (#4051) * sdk login * adding to the trail map * minor fix * improve sdk expirience * adapting to new changes --- .../miscellaneous/library/trail_map.csv | 2 ++ openbb_terminal/parent_classes.py | 2 +- openbb_terminal/session/sdk_session.py | 30 +++++++++++++++++++ openbb_terminal/session/session_model.py | 7 +++-- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 openbb_terminal/session/sdk_session.py diff --git a/openbb_terminal/miscellaneous/library/trail_map.csv b/openbb_terminal/miscellaneous/library/trail_map.csv index 055ebfec3c1..b1f2cafb412 100644 --- a/openbb_terminal/miscellaneous/library/trail_map.csv +++ b/openbb_terminal/miscellaneous/library/trail_map.csv @@ -549,3 +549,5 @@ futures.historical,openbb_terminal.futures.yfinance_model.get_historical_futures futures.curve,openbb_terminal.futures.yfinance_model.get_curve_futures,openbb_terminal.futures.yfinance_view.display_curve economy.get_groups,openbb_terminal.economy.finviz_model.get_groups, stocks.filings,openbb_terminal.stocks.fundamental_analysis.fmp_model.get_filings,openbb_terminal.stocks.fundamental_analysis.fmp_view.display_filings +login,openbb_terminal.session.sdk_session.login, +logout,openbb_terminal.session.session_model.logout, \ No newline at end of file diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 85e4823b995..23dd7c257df 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -673,7 +673,7 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - logout() + logout(cls=True) @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py new file mode 100644 index 00000000000..662b68546aa --- /dev/null +++ b/openbb_terminal/session/sdk_session.py @@ -0,0 +1,30 @@ +import json + +from openbb_terminal.session import local_model as Local +from openbb_terminal.session import hub_model as Hub +from openbb_terminal.session.session_model import create_session +from openbb_terminal.session.user import User +from openbb_terminal.rich_config import console + + +def get_session(email: str, password: str, save: bool): + session = create_session(email, password, save) + if not (isinstance(session, dict) and session): + raise Exception("Failed to create session") + return session + + +def login(email: str = "", password: str = "", save: bool = False): + session = Local.get_session() + + if not session: + session = get_session(email, password, save) + + User.load_user_info(session) + response = Hub.fetch_user_configs(session) + + if not (response and response.status_code == 200): + raise Exception("Failed to fetch user configs") + + Local.apply_configs(configs=json.loads(response.content)) + console.print("[green]Login successful[/green]") diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index a0a0b9303ca..7a9b1050fe5 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -6,6 +6,7 @@ import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.helper_funcs import system_clear +from openbb_terminal.rich_config import console def create_session(email: str, password: str, save: bool) -> dict: @@ -17,9 +18,10 @@ def create_session(email: str, password: str, save: bool) -> dict: return session -def logout(): +def logout(cls: bool = False): """Logout and clear session.""" - system_clear() + if cls: + system_clear() User.clear() # Clear openbb environment variables @@ -36,3 +38,4 @@ def logout(): Hub.delete_session() Local.remove_session_file() plt.close("all") + console.print("[green]\nLogout successful[/green]") From d4c1f8546bf76edfa2eacd3090ddeb8b2109d8b3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 18:14:12 +0000 Subject: [PATCH 096/230] add docstrings --- openbb_terminal/session/session_model.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 7a9b1050fe5..4ff4244cd26 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -10,7 +10,17 @@ from openbb_terminal.rich_config import console def create_session(email: str, password: str, save: bool) -> dict: - """Create a session.""" + """Create a session. + + Parameters + ---------- + email : str + The email. + password : str + The password. + save : bool + Save the session. + """ session = Hub.get_session(email, password) if session and save: @@ -19,7 +29,13 @@ def create_session(email: str, password: str, save: bool) -> dict: def logout(cls: bool = False): - """Logout and clear session.""" + """Logout and clear session. + + Parameters + ---------- + cls : bool + Clear the screen. + """ if cls: system_clear() User.clear() @@ -38,4 +54,4 @@ def logout(cls: bool = False): Hub.delete_session() Local.remove_session_file() plt.close("all") - console.print("[green]\nLogout successful[/green]") + console.print("[green]\nLogout successful.[/green]") From 249b109d99c4ad7702ced0b6a2b8c13a74d3812f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 18:22:44 +0000 Subject: [PATCH 097/230] block guest from account --- openbb_terminal/terminal_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index d1f996f4b2d..4ed7594387f 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -360,6 +360,13 @@ class TerminalController(BaseController): """Process account command.""" from openbb_terminal.account.account_controller import AccountController + if User.is_guest(): + console.print( + "[info]You need to be logged in to use this menu.\n" + "[info]If you don't have an account, " + "please register here https://my.openbb.co/register.[/info]\n" + ) + return self.queue = self.load_class(AccountController, self.queue) def call_keys(self, _): From 909390043802af3bbec18cfef36e32303e6b3843 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 18:31:52 +0000 Subject: [PATCH 098/230] change guest msg --- openbb_terminal/session/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 4c181dadcfa..cfb703d3a66 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -61,7 +61,7 @@ class User: sync = "OFF" console.print(f"[info]sync:[/info] {sync}\n") else: - console.print("[info]Only you know...[/info]\n") + console.print("[info]You are currently logged as a guest.[/info]\n") @classmethod def clear(cls): From 5568dca8fcd114c3947e39f229dabfb77b21474c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 18:38:03 +0000 Subject: [PATCH 099/230] change guest messages --- openbb_terminal/session/user.py | 5 ++++- openbb_terminal/terminal_controller.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index cfb703d3a66..e37e38b1b5b 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -61,7 +61,10 @@ class User: sync = "OFF" console.print(f"[info]sync:[/info] {sync}\n") else: - console.print("[info]You are currently logged as a guest.[/info]\n") + console.print( + "[info]You are currently logged as a guest.\n" + "Create an account here https://my.openbb.co/register.[/info]\n" + ) @classmethod def clear(cls): diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 4ed7594387f..55d256e5052 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -363,8 +363,7 @@ class TerminalController(BaseController): if User.is_guest(): console.print( "[info]You need to be logged in to use this menu.\n" - "[info]If you don't have an account, " - "please register here https://my.openbb.co/register.[/info]\n" + "Create an account here https://my.openbb.co/register.[/info]\n" ) return self.queue = self.load_class(AccountController, self.queue) From 7aa57dc54fd07dc371770fddad67808e0b3e8052 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 19:36:02 +0000 Subject: [PATCH 100/230] add sync on/off --- openbb_terminal/account/account_controller.py | 37 +++++++++++++++++-- openbb_terminal/session/user.py | 6 +-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index b9ee5bdff3d..c3283dac9aa 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,4 +1,4 @@ -# import argparse +import argparse import logging from typing import List, Dict from pathlib import Path @@ -48,6 +48,7 @@ class AccountController(BaseController): self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} + choices["sync"] = {"--on": {}, "--off": {}} # choices["upload"]["--file"] = self.ROUTINE_CHOICES # choices["upload"]["-f"] = choices["upload"]["--file"] @@ -67,11 +68,39 @@ class AccountController(BaseController): console.print(text=mt.menu_text, menu="Account") @log_start_end(log=logger) - def call_sync(self, _): + def call_sync(self, other_args: List[str]): """Sync""" - FeatureFlagsController.set_feature_flag( - "OPENBB_SYNC_ENABLED", not obbff.SYNC_ENABLED, force=True + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="load", + description="Load your portfolio transactions.", ) + parser.add_argument( + "--on", + dest="on", + help="Turn on sync", + action="store_true", + default=False, + ) + parser.add_argument( + "--off", + dest="off", + help="Turn on sync", + action="store_true", + default=False, + ) + ns_parser = self.parse_simple_args(parser, other_args) + if ns_parser: + if ns_parser.on: + if not obbff.SYNC_ENABLED: + FeatureFlagsController.set_feature_flag( + "OPENBB_SYNC_ENABLED", True, force=True + ) + elif ns_parser.off: + FeatureFlagsController.set_feature_flag( + "OPENBB_SYNC_ENABLED", False, force=True + ) def call_pull(self, _): """Pull data""" diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index e37e38b1b5b..823b2b1a86d 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -28,6 +28,7 @@ class User: decoded_info = jwt.decode(User._TOKEN, options={"verify_signature": False}) User._EMAIL = decoded_info.get("sub", "") + # Add username to flair User.update_flair() @staticmethod @@ -85,11 +86,6 @@ class User: """Check if sync is enabled.""" return obbff.SYNC_ENABLED - @classmethod - def toggle_sync(cls): - """Toggle sync.""" - obbff.SYNC_ENABLED = not obbff.SYNC_ENABLED - @classmethod def get_token(cls): """Get token.""" From 0d1db6728edfbd7dd1d3592ab4765b614a868804 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 19:47:56 +0000 Subject: [PATCH 101/230] display onoff --- openbb_terminal/account/account_controller.py | 6 ++++++ openbb_terminal/session/user.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index c3283dac9aa..a79f0ace24a 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -102,6 +102,12 @@ class AccountController(BaseController): "OPENBB_SYNC_ENABLED", False, force=True ) + if obbff.SYNC_ENABLED: + sync = "ON" + else: + sync = "OFF" + console.print(f"[info]sync:[/info] {sync}") + def call_pull(self, _): """Pull data""" console.print("Pull, Diff and Merge.") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 823b2b1a86d..eb659ee75ae 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -60,7 +60,7 @@ class User: sync = "ON" else: sync = "OFF" - console.print(f"[info]sync:[/info] {sync}\n") + console.print(f"[info]sync:[/info] {sync}") else: console.print( "[info]You are currently logged as a guest.\n" From bdd81f4ea56d2bb7c3f976aea1c6f18d2e452692 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 20:15:28 +0000 Subject: [PATCH 102/230] missing paragraph --- openbb_terminal/session/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index eb659ee75ae..823b2b1a86d 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -60,7 +60,7 @@ class User: sync = "ON" else: sync = "OFF" - console.print(f"[info]sync:[/info] {sync}") + console.print(f"[info]sync:[/info] {sync}\n") else: console.print( "[info]You are currently logged as a guest.\n" From 1f42fe33fffb05c58237a34b2b6e7a8011cd85b6 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 20:40:56 +0000 Subject: [PATCH 103/230] fix glitch on logout --- openbb_terminal/terminal_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 55d256e5052..6beefded465 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -25,7 +25,7 @@ from prompt_toolkit.formatted_text import HTML import pandas as pd from openbb_terminal import feature_flags as obbff from openbb_terminal.session.user import User -from openbb_terminal.terminal_helper import is_packaged_application +from openbb_terminal.terminal_helper import is_installer, is_packaged_application from openbb_terminal.session import session_controller from openbb_terminal.core.config.paths import ( @@ -968,7 +968,7 @@ def terminal(jobs_cmds: List[str] = None, test_mode=False): t_controller.queue.insert(0, an_input) if an_input == "logout": - return session_controller.main(guest_allowed=not is_packaged_application()) + return session_controller.main(guest_allowed=not is_installer()) def insert_start_slash(cmds: List[str]) -> List[str]: From dbc7c09ee04d51b38e5e03364e11f5c4b753e93e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 26 Jan 2023 21:05:56 +0000 Subject: [PATCH 104/230] add sys clear --- openbb_terminal/session/session_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 6141f741f65..618600a9190 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -7,10 +7,12 @@ from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller +from openbb_terminal.helper_funcs import system_clear def display_welcome_message(): """Display welcome message""" + system_clear() with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: console.print(f.read(), style="menu") From 46854d2676da226e0e3dd926775eed57bde6dd10 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 27 Jan 2023 10:49:35 +0000 Subject: [PATCH 105/230] filtering password --- .../core/log/generation/formatter_with_exceptions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openbb_terminal/core/log/generation/formatter_with_exceptions.py b/openbb_terminal/core/log/generation/formatter_with_exceptions.py index 28c85a4e5a0..7202d8ee777 100644 --- a/openbb_terminal/core/log/generation/formatter_with_exceptions.py +++ b/openbb_terminal/core/log/generation/formatter_with_exceptions.py @@ -67,6 +67,13 @@ class FormatterWithExceptions(logging.Formatter): return text_mocked + @staticmethod + def mock_password(text: str) -> str: + pattern = r'("password": ")[^"]+' + replacement = r"\1 FILTERED_PASSWORD " + text_mocked = re.sub(pattern, replacement, text) + return text_mocked + @staticmethod def mock_home_directory(text: str) -> str: user_home_directory = str(HOME_DIRECTORY) @@ -85,6 +92,7 @@ class FormatterWithExceptions(logging.Formatter): def filter_piis(cls, text: str) -> str: text_filtered = cls.mock_ipv4(text=text) text_filtered = cls.mock_email(text=text_filtered) + text_filtered = cls.mock_password(text=text_filtered) text_filtered = cls.mock_home_directory(text=text_filtered) return text_filtered From f6fd627761a542b0679b4a60be01fc34306b0ea3 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 27 Jan 2023 10:51:35 +0000 Subject: [PATCH 106/230] filtering the email from user --- .../core/log/generation/formatter_with_exceptions.py | 7 ------- openbb_terminal/core/log/generation/user_logger.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/openbb_terminal/core/log/generation/formatter_with_exceptions.py b/openbb_terminal/core/log/generation/formatter_with_exceptions.py index 7202d8ee777..51ca71f0f63 100644 --- a/openbb_terminal/core/log/generation/formatter_with_exceptions.py +++ b/openbb_terminal/core/log/generation/formatter_with_exceptions.py @@ -5,13 +5,9 @@ import re # IMPORTATION THIRDPARTY # IMPORTATION INTERNAL -from openbb_terminal.core.log.generation.user_logger import __name__ as user_logger_name from openbb_terminal.core.config.paths import HOME_DIRECTORY from openbb_terminal.core.log.generation.settings import AppSettings -# considering starting at 0 and after the logging prefix i.e., just the text to be filtered -MODULE_POSITION = 1 - class FormatterWithExceptions(logging.Formatter): """Logging Formatter that includes formatting of Exceptions""" @@ -58,9 +54,6 @@ class FormatterWithExceptions(logging.Formatter): @staticmethod def mock_email(text: str) -> str: - if text.split("|")[MODULE_POSITION] == user_logger_name: - return text - pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" replacement = " FILTERED_EMAIL " text_mocked = re.sub(pattern, replacement, text) diff --git a/openbb_terminal/core/log/generation/user_logger.py b/openbb_terminal/core/log/generation/user_logger.py index 2c117b2377c..59a07275afd 100644 --- a/openbb_terminal/core/log/generation/user_logger.py +++ b/openbb_terminal/core/log/generation/user_logger.py @@ -23,5 +23,5 @@ def log_user(with_rollover: bool = True): def log_user_info(): """Log user info""" - user_info = {"user_uuid": User.get_uuid(), "user_email": User.get_email()} + user_info = {"user_uuid": User.get_uuid()} logger.info("USER: %s ", json.dumps(user_info)) From 5e78254e45ca35d939ac5df8a222254c7b9c887b Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 27 Jan 2023 11:01:33 +0000 Subject: [PATCH 107/230] avoid writing to multiple log files if a user logs out --- openbb_terminal/session/session_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 4ff4244cd26..15fb349c925 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -1,4 +1,5 @@ import importlib +import logging import os import sys import matplotlib.pyplot as plt @@ -45,6 +46,10 @@ def logout(cls: bool = False): if v.startswith("OPENBB"): os.environ.pop(v) + # Remove the log handlers - needs to be done before reloading modules + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + # Reload all openbb modules to clear memorized variables modules = sys.modules.copy() for module in modules: From a2bdcd7f7b5fddf302cb3724de623c25f82d017d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 11:28:29 +0000 Subject: [PATCH 108/230] fix bug, guest allowed in installer without connection --- openbb_terminal/session/session_controller.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 618600a9190..cbab08d4655 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -7,12 +7,10 @@ from openbb_terminal.session.user import User from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal import terminal_controller -from openbb_terminal.helper_funcs import system_clear def display_welcome_message(): """Display welcome message""" - system_clear() with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: console.print(f.read(), style="menu") @@ -36,7 +34,7 @@ def get_user_input() -> Tuple[str, str, bool]: return email, password, save -def login_prompt(welcome=True, guest_allowed=True): +def login_prompt(welcome=True, guest_allowed=False): """Login prompt and launch terminal if login is successful. Parameters @@ -62,24 +60,26 @@ def login_prompt(welcome=True, guest_allowed=True): # TODO: Move some login inside this function to the session_model.py -def login(session: dict): +def login(session: dict, guest_allowed: bool = True): """Login and launch terminal. Parameters ---------- session : dict The session info. + guest_allowed : bool, optional + Allow guest login, by default True """ - User.load_user_info(session) response = Hub.fetch_user_configs(session) if response: if response.status_code == 200: + User.load_user_info(session) Local.apply_configs(configs=json.loads(response.content)) terminal_controller.parse_args_and_run() else: - login_prompt(welcome=False) + login_prompt(welcome=False, guest_allowed=guest_allowed) else: - login_prompt(welcome=True) + login_prompt(welcome=True, guest_allowed=guest_allowed) def main(guest_allowed: bool = True): @@ -88,7 +88,7 @@ def main(guest_allowed: bool = True): if not local_session: login_prompt(guest_allowed=guest_allowed) else: - login(session=local_session) + login(session=local_session, guest_allowed=guest_allowed) if __name__ == "__main__": From e683ca778ba7a3bf3b3c640771f1d2b7be53a9db Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 27 Jan 2023 11:51:48 +0000 Subject: [PATCH 109/230] log user on the sdk --- openbb_terminal/core/library/operation.py | 18 ++++++++++++++++-- .../core/log/generation/user_logger.py | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/core/library/operation.py b/openbb_terminal/core/library/operation.py index 89e80903810..93cba207bd2 100644 --- a/openbb_terminal/core/library/operation.py +++ b/openbb_terminal/core/library/operation.py @@ -6,9 +6,9 @@ from typing import Any, Callable, Dict, List, Optional from inspect import signature import openbb_terminal.config_terminal as cfg - from openbb_terminal.core.library.metadata import Metadata from openbb_terminal.core.library.trail_map import TrailMap +from openbb_terminal.session.sdk_session import login # pylint: disable=import-outside-toplevel @@ -232,7 +232,7 @@ class OperationLogger: if func_module == "openbb_terminal.keys_model": - from openbb_terminal.core.log.generation.settings_logger import ( # pylint: disable=C0415 + from openbb_terminal.core.log.generation.settings_logger import ( # pylint: disable=import-outside-toplevel log_keys, ) @@ -252,6 +252,9 @@ class OperationLogger: method_result=method_result, method_chosen=self.__method_chosen, ) + self.__log_if_login( + method_chosen=self.__method_chosen, + ) self.__log_end( logger=logger, method_chosen=self.__method_chosen, @@ -275,6 +278,17 @@ class OperationLogger: extra={"func_name_override": method_chosen.__name__}, ) + @staticmethod + def __log_if_login( + method_chosen: Callable, + ): + if method_chosen.__name__ == login.__name__: + from openbb_terminal.core.log.generation.user_logger import ( # pylint: disable=import-outside-toplevel + log_user, + ) + + log_user(with_rollover=False) + @staticmethod def __log_end(logger: Logger, method_chosen: Callable): logger.info( diff --git a/openbb_terminal/core/log/generation/user_logger.py b/openbb_terminal/core/log/generation/user_logger.py index 59a07275afd..acda5a0e8b1 100644 --- a/openbb_terminal/core/log/generation/user_logger.py +++ b/openbb_terminal/core/log/generation/user_logger.py @@ -15,13 +15,13 @@ logger = logging.getLogger(__name__) def log_user(with_rollover: bool = True): """Log user""" if not User.is_guest(): - log_user_info() + _log_user_info() if with_rollover: do_rollover() -def log_user_info(): +def _log_user_info(): """Log user info""" user_info = {"user_uuid": User.get_uuid()} logger.info("USER: %s ", json.dumps(user_info)) From fe94902a92223f010793119da3a4f0ccf337d415 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 12:00:38 +0000 Subject: [PATCH 110/230] split login from lauch --- openbb_terminal/session/session_controller.py | 56 ++++++++++--------- openbb_terminal/session/session_model.py | 26 +++++++++ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index cbab08d4655..288c4b78289 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,12 +1,13 @@ from typing import Tuple -import json +from openbb_terminal import terminal_controller import openbb_terminal.session.local_model as Local -import openbb_terminal.session.hub_model as Hub -from openbb_terminal.session.session_model import create_session -from openbb_terminal.session.user import User +from openbb_terminal.session.session_model import ( + LoginStatus, + create_session, + login, +) from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console -from openbb_terminal import terminal_controller def display_welcome_message(): @@ -34,8 +35,8 @@ def get_user_input() -> Tuple[str, str, bool]: return email, password, save -def login_prompt(welcome=True, guest_allowed=False): - """Login prompt and launch terminal if login is successful. +def prompt(welcome=True, guest_allowed=False): + """Prompt and launch terminal if login is successful. Parameters ---------- @@ -49,18 +50,20 @@ def login_prompt(welcome=True, guest_allowed=False): email, password, save = get_user_input() if not email or not password: if guest_allowed: - return terminal_controller.parse_args_and_run() + return launch_terminal() continue session = create_session(email, password, save) if isinstance(session, dict) and session: - break - - login(session=session) + return login_and_launch(session=session, guest_allowed=guest_allowed) -# TODO: Move some login inside this function to the session_model.py -def login(session: dict, guest_allowed: bool = True): +def launch_terminal(): + """Launch terminal""" + terminal_controller.parse_args_and_run() + + +def login_and_launch(session: dict, guest_allowed: bool = True): """Login and launch terminal. Parameters @@ -70,25 +73,28 @@ def login(session: dict, guest_allowed: bool = True): guest_allowed : bool, optional Allow guest login, by default True """ - response = Hub.fetch_user_configs(session) - if response: - if response.status_code == 200: - User.load_user_info(session) - Local.apply_configs(configs=json.loads(response.content)) - terminal_controller.parse_args_and_run() - else: - login_prompt(welcome=False, guest_allowed=guest_allowed) + status = login(session) + if status == LoginStatus.SUCCESS: + launch_terminal() + elif status == LoginStatus.FAILED: + prompt(welcome=False, guest_allowed=guest_allowed) else: - login_prompt(welcome=True, guest_allowed=guest_allowed) + prompt(welcome=True, guest_allowed=guest_allowed) def main(guest_allowed: bool = True): - """Main function""" + """Main function + + Parameters + ---------- + guest_allowed : bool, optional + Allow guest login, by default True + """ local_session = Local.get_session() if not local_session: - login_prompt(guest_allowed=guest_allowed) + prompt(guest_allowed=guest_allowed) else: - login(session=local_session, guest_allowed=guest_allowed) + login_and_launch(session=local_session, guest_allowed=guest_allowed) if __name__ == "__main__": diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 4ff4244cd26..c639c820a2f 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -1,7 +1,9 @@ import importlib import os import sys +import json import matplotlib.pyplot as plt +from enum import Enum import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User @@ -9,6 +11,12 @@ from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console +class LoginStatus(Enum): + SUCCESS = 1 + FAILED = 2 + NO_RESPONSE = 3 + + def create_session(email: str, password: str, save: bool) -> dict: """Create a session. @@ -28,6 +36,24 @@ def create_session(email: str, password: str, save: bool) -> dict: return session +def login(session: dict) -> LoginStatus: + """Login and load user info. + + Parameters + ---------- + session : dict + The session info. + """ + response = Hub.fetch_user_configs(session) + if response: + if response.status_code == 200: + User.load_user_info(session) + Local.apply_configs(configs=json.loads(response.content)) + return LoginStatus.SUCCESS + return LoginStatus.FAILED + return LoginStatus.NO_RESPONSE + + def logout(cls: bool = False): """Logout and clear session. From 920330c31885421dfc84d637aff2ccd517b63130 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 12:43:49 +0000 Subject: [PATCH 111/230] add clear and change messages on account --- openbb_terminal/account/account_controller.py | 26 ++++++++++++++----- openbb_terminal/miscellaneous/i18n/en.yml | 8 +++--- openbb_terminal/session/hub_model.py | 26 ++++++++++++++++--- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index a79f0ace24a..b62d1040a21 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -11,6 +11,7 @@ from openbb_terminal.decorators import log_start_end from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session +from openbb_terminal.session import hub_model as Hub # from openbb_terminal.session.user import User @@ -26,7 +27,7 @@ class AccountController(BaseController): CHOICES_COMMANDS = [ "sync", "pull", - "defaults", + "clear", # "upload", # "download", ] @@ -61,7 +62,7 @@ class AccountController(BaseController): mt.add_info("_info_") mt.add_cmd("sync") mt.add_cmd("pull") - mt.add_cmd("defaults") + mt.add_cmd("clear") # mt.add_info("_routines_") # mt.add_cmd("upload") # mt.add_cmd("download") @@ -106,17 +107,28 @@ class AccountController(BaseController): sync = "ON" else: sync = "OFF" - console.print(f"[info]sync:[/info] {sync}") + + if ns_parser.on or ns_parser.off: + console.print(f"[info]sync:[/info] {sync}") + else: + console.print(f"sync is {sync}, use --on or --off to change.") def call_pull(self, _): """Pull data""" console.print("Pull, Diff and Merge.") pass - def call_defaults(self, _): - """Reset to defaults""" - console.print("No implemented yet.") - pass + def call_clear(self, _): + """Clear data""" + i = console.input( + "[red]This action is irreversible!\n" + "Are you sure you want to permanently delete your data? (y/n): [/red]" + ) + if i.lower() in ["y", "yes"]: + console.print("") + Hub.clear_user_configs() + else: + console.print("\nAborted.", style="info") # @log_start_end(log=logger) # def call_upload(self, other_args: List[str]): diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 4816bb7c2cf..b874f249f3e 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -16,10 +16,10 @@ en: intro: introduction on the OpenBB Terminal _configure_: Configure your own terminal _main_menu_: Main menu - account/_info_: Synchronization - account/sync: Send keys, settings and features flags on changes - account/pull: Pull keys, settings and feature flags from server - account/defaults: Reset to defaults + account/_info_: Remote storage of keys, settings and feature flags + account/sync: Automatically send to remote on changes + account/pull: Pull from remote + account/clear: Clear from remote account/_routines_: Routines account/upload: Upload routine account/download: Download routine diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index c9fe6e2c6ec..6b25616ebce 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, Optional import requests from openbb_terminal.session.user import User @@ -153,9 +153,27 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re return None -def put_user_configs() -> Optional[requests.Response]: - """Push user configurations to the server.""" - pass +def clear_user_configs() -> Optional[requests.Response]: + """Clear user configurations to the server.""" + data: Dict[str, dict] = {"features_keys": {}, "features_settings": {}} + + try: + response = requests.put( + url=BASE_URL + "terminal/user", + headers={"Authorization": User.get_token()}, + json=data, + ) + if response.status_code == 200: + console.print("[green]Cleared configurations.[/green]") + else: + console.print("[red]Failed to clear configurations.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print("[red]Connection error.[/red]") + return None + except Exception: + console.print("[red]Failed to clear configurations.[/red]") + return None def delete_session(): From 6e1c068e24930f4c08956ef609b2fc412833679c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 12:54:13 +0000 Subject: [PATCH 112/230] docstrings --- openbb_terminal/account/account_controller.py | 1 - openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/settings_controller.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index b62d1040a21..35da9aa01e5 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -116,7 +116,6 @@ class AccountController(BaseController): def call_pull(self, _): """Pull data""" console.print("Pull, Diff and Merge.") - pass def call_clear(self, _): """Clear data""" diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index d3cd3609581..e85d2c5e193 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -91,7 +91,7 @@ class FeatureFlagsController(BaseController): value : str Environment variable value force : bool, optional - Force set feature flag, by default False + Force remote storage of feature flag, by default False """ if User.is_guest(): diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index d61fbf3aa29..9068c6e1309 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -147,8 +147,6 @@ class SettingsController(BaseController): Environment variable name value : str Environment variable value - persist : bool, optional - Persist feature flag, by default False """ if User.is_guest(): From 95f4febbf75a54a8498d35160208e1f8be7db288 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 14:54:23 +0000 Subject: [PATCH 113/230] start pull cmd --- openbb_terminal/account/account_controller.py | 15 ++++++++++++++- openbb_terminal/session/user.py | 9 +++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 35da9aa01e5..ae38e5122c4 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,4 +1,5 @@ import argparse +import json import logging from typing import List, Dict from pathlib import Path @@ -12,6 +13,7 @@ from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session from openbb_terminal.session import hub_model as Hub +from openbb_terminal.session.user import User # from openbb_terminal.session.user import User @@ -115,7 +117,18 @@ class AccountController(BaseController): def call_pull(self, _): """Pull data""" - console.print("Pull, Diff and Merge.") + response = Hub.fetch_user_configs(User.get_session()) + if response: + console.print("Remote data:", style="info") + console.print(json.loads(response.content)) + i = console.input( + "\nDo you want to overwrite your local configurations " + "with the remote? (y/n): " + ) + if i.lower() in ["y", "yes"]: + console.print("[green]\nApplied remote configs.[/green]") + else: + console.print("\nAborted.", style="info") def call_clear(self, _): """Clear data""" diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 823b2b1a86d..3e0208e8df0 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -31,6 +31,15 @@ class User: # Add username to flair User.update_flair() + @classmethod + def get_session(cls): + """Get session info.""" + return { + "token_type": User._TOKEN_TYPE, + "access_token": User._TOKEN, + "uuid": User._UUID, + } + @staticmethod def update_flair(): MAX_FLAIR_LEN = 20 From c55e7ba765c70cdb224a9e5c2f3b15b24e08e7dc Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 14:54:36 +0000 Subject: [PATCH 114/230] change account menu description --- openbb_terminal/miscellaneous/i18n/en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index b874f249f3e..849d9ef36a3 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -9,6 +9,7 @@ en: settings: tune settings (export folder, timezone, language, plot size) featflags: enable and disable feature flags sources: select your preferred data sources + account: manage your OpenBB account keys: set API keys and check their validity wiki: search for an expression in Wikipedia (https://www.wikipedia.org/) record: record session to be converted into a .openbb routine From d0c7dceb21f07c2318e292b3cc9086999aff8186 Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:59:37 +0000 Subject: [PATCH 115/230] Fix login bug and decouple some logic (#4056) * fix bug, guest allowed in installer without connection * split login from lauch * improvements on the sdk login --------- Co-authored-by: hjoaquim --- openbb_terminal/session/sdk_session.py | 19 +++--- openbb_terminal/session/session_controller.py | 60 ++++++++++--------- openbb_terminal/session/session_model.py | 26 ++++++++ 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py index 662b68546aa..47b57b9c826 100644 --- a/openbb_terminal/session/sdk_session.py +++ b/openbb_terminal/session/sdk_session.py @@ -1,14 +1,10 @@ -import json - from openbb_terminal.session import local_model as Local -from openbb_terminal.session import hub_model as Hub -from openbb_terminal.session.session_model import create_session -from openbb_terminal.session.user import User +from openbb_terminal.session import session_model from openbb_terminal.rich_config import console def get_session(email: str, password: str, save: bool): - session = create_session(email, password, save) + session = session_model.create_session(email, password, save) if not (isinstance(session, dict) and session): raise Exception("Failed to create session") return session @@ -20,11 +16,10 @@ def login(email: str = "", password: str = "", save: bool = False): if not session: session = get_session(email, password, save) - User.load_user_info(session) - response = Hub.fetch_user_configs(session) + if session_model.login(session) in [ + session_model.LoginStatus.FAILED, + session_model.LoginStatus.NO_RESPONSE, + ]: + raise Exception("Login failed") - if not (response and response.status_code == 200): - raise Exception("Failed to fetch user configs") - - Local.apply_configs(configs=json.loads(response.content)) console.print("[green]Login successful[/green]") diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 618600a9190..288c4b78289 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,18 +1,17 @@ from typing import Tuple -import json +from openbb_terminal import terminal_controller import openbb_terminal.session.local_model as Local -import openbb_terminal.session.hub_model as Hub -from openbb_terminal.session.session_model import create_session -from openbb_terminal.session.user import User +from openbb_terminal.session.session_model import ( + LoginStatus, + create_session, + login, +) from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console -from openbb_terminal import terminal_controller -from openbb_terminal.helper_funcs import system_clear def display_welcome_message(): """Display welcome message""" - system_clear() with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: console.print(f.read(), style="menu") @@ -36,8 +35,8 @@ def get_user_input() -> Tuple[str, str, bool]: return email, password, save -def login_prompt(welcome=True, guest_allowed=True): - """Login prompt and launch terminal if login is successful. +def prompt(welcome=True, guest_allowed=False): + """Prompt and launch terminal if login is successful. Parameters ---------- @@ -51,44 +50,51 @@ def login_prompt(welcome=True, guest_allowed=True): email, password, save = get_user_input() if not email or not password: if guest_allowed: - return terminal_controller.parse_args_and_run() + return launch_terminal() continue session = create_session(email, password, save) if isinstance(session, dict) and session: - break - - login(session=session) + return login_and_launch(session=session, guest_allowed=guest_allowed) -# TODO: Move some login inside this function to the session_model.py -def login(session: dict): +def launch_terminal(): + """Launch terminal""" + terminal_controller.parse_args_and_run() + + +def login_and_launch(session: dict, guest_allowed: bool = True): """Login and launch terminal. Parameters ---------- session : dict The session info. + guest_allowed : bool, optional + Allow guest login, by default True """ - User.load_user_info(session) - response = Hub.fetch_user_configs(session) - if response: - if response.status_code == 200: - Local.apply_configs(configs=json.loads(response.content)) - terminal_controller.parse_args_and_run() - else: - login_prompt(welcome=False) + status = login(session) + if status == LoginStatus.SUCCESS: + launch_terminal() + elif status == LoginStatus.FAILED: + prompt(welcome=False, guest_allowed=guest_allowed) else: - login_prompt(welcome=True) + prompt(welcome=True, guest_allowed=guest_allowed) def main(guest_allowed: bool = True): - """Main function""" + """Main function + + Parameters + ---------- + guest_allowed : bool, optional + Allow guest login, by default True + """ local_session = Local.get_session() if not local_session: - login_prompt(guest_allowed=guest_allowed) + prompt(guest_allowed=guest_allowed) else: - login(session=local_session) + login_and_launch(session=local_session, guest_allowed=guest_allowed) if __name__ == "__main__": diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 15fb349c925..1f0a393f661 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -2,6 +2,8 @@ import importlib import logging import os import sys +import json +from enum import Enum import matplotlib.pyplot as plt import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub @@ -10,6 +12,12 @@ from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console +class LoginStatus(Enum): + SUCCESS = "success" + FAILED = "failed" + NO_RESPONSE = "no_response" + + def create_session(email: str, password: str, save: bool) -> dict: """Create a session. @@ -29,6 +37,24 @@ def create_session(email: str, password: str, save: bool) -> dict: return session +def login(session: dict) -> LoginStatus: + """Login and load user info. + + Parameters + ---------- + session : dict + The session info. + """ + response = Hub.fetch_user_configs(session) + if response: + if response.status_code == 200: + User.load_user_info(session) + Local.apply_configs(configs=json.loads(response.content)) + return LoginStatus.SUCCESS + return LoginStatus.FAILED + return LoginStatus.NO_RESPONSE + + def logout(cls: bool = False): """Logout and clear session. From bf92b92f54124be3255104289c4eb0173c540af7 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 15:14:53 +0000 Subject: [PATCH 116/230] sync fix --- openbb_terminal/account/account_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index ae38e5122c4..cb22b773b97 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -101,9 +101,10 @@ class AccountController(BaseController): "OPENBB_SYNC_ENABLED", True, force=True ) elif ns_parser.off: - FeatureFlagsController.set_feature_flag( - "OPENBB_SYNC_ENABLED", False, force=True - ) + if obbff.SYNC_ENABLED: + FeatureFlagsController.set_feature_flag( + "OPENBB_SYNC_ENABLED", False, force=True + ) if obbff.SYNC_ENABLED: sync = "ON" From c13de33cbfa7030a90fdd45db30aed231b711a0a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 15:21:56 +0000 Subject: [PATCH 117/230] change account controller description --- openbb_terminal/miscellaneous/i18n/en.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 849d9ef36a3..54fc824ae78 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -17,10 +17,10 @@ en: intro: introduction on the OpenBB Terminal _configure_: Configure your own terminal _main_menu_: Main menu - account/_info_: Remote storage of keys, settings and feature flags - account/sync: Automatically send to remote on changes - account/pull: Pull from remote - account/clear: Clear from remote + account/_info_: Cloud storage of keys, settings and feature flags + account/sync: Turns the cloud synchronization on/off + account/pull: Pull from cloud + account/clear: Clear data from remote cloud account/_routines_: Routines account/upload: Upload routine account/download: Download routine From c8e53f034893266f9c9b1275f9c2005dafec6752 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 15:29:09 +0000 Subject: [PATCH 118/230] ruff --- openbb_terminal/session/session_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 7a211b21da6..1f0a393f661 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -5,7 +5,6 @@ import sys import json from enum import Enum import matplotlib.pyplot as plt -from enum import Enum import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User From 42a322a32fbd29f1f93f570dfa87c2882e6e8b41 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 15:51:21 +0000 Subject: [PATCH 119/230] add help on account cmds --- openbb_terminal/account/account_controller.py | 60 ++++++++++++------- openbb_terminal/terminal_controller.py | 12 ++-- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index cb22b773b97..8d71b1ee9a0 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -76,8 +76,8 @@ class AccountController(BaseController): parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="load", - description="Load your portfolio transactions.", + prog="sync", + description="Turn on/off the automatic sending of configurations when changed.", ) parser.add_argument( "--on", @@ -118,30 +118,46 @@ class AccountController(BaseController): def call_pull(self, _): """Pull data""" - response = Hub.fetch_user_configs(User.get_session()) - if response: - console.print("Remote data:", style="info") - console.print(json.loads(response.content)) - i = console.input( - "\nDo you want to overwrite your local configurations " - "with the remote? (y/n): " - ) - if i.lower() in ["y", "yes"]: - console.print("[green]\nApplied remote configs.[/green]") - else: - console.print("\nAborted.", style="info") + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="pull", + description="Pull and apply stored configurations from the cloud.", + ) + ns_parser = self.parse_simple_args(parser, _) + if ns_parser: + response = Hub.fetch_user_configs(User.get_session()) + if response: + console.print("Remote data:", style="info") + console.print(json.loads(response.content)) + i = console.input( + "\nDo you want to overwrite your local configurations " + "with the remote? (y/n): " + ) + if i.lower() in ["y", "yes"]: + console.print("[green]\nApplied remote configs.[/green]") + else: + console.print("\nAborted.", style="info") def call_clear(self, _): """Clear data""" - i = console.input( - "[red]This action is irreversible!\n" - "Are you sure you want to permanently delete your data? (y/n): [/red]" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="clear", + description="Clear stored configurations from the cloud.", ) - if i.lower() in ["y", "yes"]: - console.print("") - Hub.clear_user_configs() - else: - console.print("\nAborted.", style="info") + ns_parser = self.parse_simple_args(parser, _) + if ns_parser: + i = console.input( + "[red]This action is irreversible!\n" + "Are you sure you want to permanently delete your data? (y/n): [/red]" + ) + if i.lower() in ["y", "yes"]: + console.print("") + Hub.clear_user_configs() + else: + console.print("\nAborted.", style="info") # @log_start_end(log=logger) # def call_upload(self, other_args: List[str]): diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 6beefded465..3d8d85ca0f8 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -360,12 +360,12 @@ class TerminalController(BaseController): """Process account command.""" from openbb_terminal.account.account_controller import AccountController - if User.is_guest(): - console.print( - "[info]You need to be logged in to use this menu.\n" - "Create an account here https://my.openbb.co/register.[/info]\n" - ) - return + # if User.is_guest(): + # console.print( + # "[info]You need to be logged in to use this menu.\n" + # "Create an account here https://my.openbb.co/register.[/info]\n" + # ) + # return self.queue = self.load_class(AccountController, self.queue) def call_keys(self, _): From 9ae5b9d58984203ab65c2a7df29f80e5db6660ae Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 15:56:31 +0000 Subject: [PATCH 120/230] update clear prompt --- openbb_terminal/account/account_controller.py | 4 ++-- openbb_terminal/terminal_controller.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 8d71b1ee9a0..2cf6424407f 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -150,8 +150,8 @@ class AccountController(BaseController): ns_parser = self.parse_simple_args(parser, _) if ns_parser: i = console.input( - "[red]This action is irreversible!\n" - "Are you sure you want to permanently delete your data? (y/n): [/red]" + "[red]This action is irreversible![/red]\n" + "> Are you sure you want to permanently delete your data? (y/n): " ) if i.lower() in ["y", "yes"]: console.print("") diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 3d8d85ca0f8..6beefded465 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -360,12 +360,12 @@ class TerminalController(BaseController): """Process account command.""" from openbb_terminal.account.account_controller import AccountController - # if User.is_guest(): - # console.print( - # "[info]You need to be logged in to use this menu.\n" - # "Create an account here https://my.openbb.co/register.[/info]\n" - # ) - # return + if User.is_guest(): + console.print( + "[info]You need to be logged in to use this menu.\n" + "Create an account here https://my.openbb.co/register.[/info]\n" + ) + return self.queue = self.load_class(AccountController, self.queue) def call_keys(self, _): From 9dc486a2b6fc365033d24fd7bc43a44f62e341c0 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 16:51:43 +0000 Subject: [PATCH 121/230] fix email not in jwt --- openbb_terminal/session/session_model.py | 6 ++++-- openbb_terminal/session/user.py | 11 +++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 1f0a393f661..dc1eaeb3db9 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -48,8 +48,10 @@ def login(session: dict) -> LoginStatus: response = Hub.fetch_user_configs(session) if response: if response.status_code == 200: - User.load_user_info(session) - Local.apply_configs(configs=json.loads(response.content)) + configs = json.loads(response.content) + email = configs.get("email", "") + User.load_user_info(session, email) + Local.apply_configs(configs=configs) return LoginStatus.SUCCESS return LoginStatus.FAILED return LoginStatus.NO_RESPONSE diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 3e0208e8df0..cf6aff072f5 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -import jwt import openbb_terminal.feature_flags as obbff from openbb_terminal.rich_config import console @@ -12,7 +11,7 @@ class User: _UUID: str = "" @classmethod - def load_user_info(cls, session: dict): + def load_user_info(cls, session: dict, email: str): """Load user info from login info. Parameters @@ -23,12 +22,8 @@ class User: User._TOKEN_TYPE = session.get("token_type", "") User._TOKEN = session.get("access_token", "") User._UUID = session.get("uuid", "") - - if User._TOKEN: - decoded_info = jwt.decode(User._TOKEN, options={"verify_signature": False}) - User._EMAIL = decoded_info.get("sub", "") - - # Add username to flair + User._EMAIL = email + if User._EMAIL: User.update_flair() @classmethod From 04179f942868cc78819a3d90b93835a64d688790 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 16:58:47 +0000 Subject: [PATCH 122/230] remove jwt from requiremnts --- poetry.lock | 19 ++----------------- pyproject.toml | 1 - requirements-full.txt | 3 +-- requirements.txt | 1 - 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index dc7b3e3f035..8a33a4141a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1103,7 +1103,7 @@ python-versions = ">=3.7" [[package]] name = "fsspec" -version = "2022.11.0" +version = "2023.1.0" description = "File-system specification" category = "main" optional = true @@ -3059,20 +3059,6 @@ category = "main" optional = true python-versions = ">=3.7" -[[package]] -name = "pyjwt" -version = "2.6.0" -description = "JSON Web Token implementation in Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.4.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "pre-commit"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] - [[package]] name = "pylint" version = "2.15.2" @@ -4870,7 +4856,7 @@ prediction = ["torch", "pytorch-lightning", "u8darts"] [metadata] lock-version = "1.1" python-versions = "^3.8,<3.11, !=3.9.7" -content-hash = "7fd9f4ca140556f2b9b3df3ee1568b09259fd3b83fb2546c321b14b190dff36c" +content-hash = "77d8bec0f896363a577f4cbdcd9345705a9c9ea22395c44df71b7f3799c0618d" [metadata.files] absl-py = [] @@ -5106,7 +5092,6 @@ pygments = [] pyhdfe = [] pyinstaller = [] pyinstaller-hooks-contrib = [] -pyjwt = [] pylint = [] pyluach = [] pymeeus = [] diff --git a/pyproject.toml b/pyproject.toml index cf68ab12c38..5190380a170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,6 @@ docstring-parser = {version = "^0.15", optional = true} grpcio = "^1.51.1" jupyterlab-widgets = "^3.0.3" packaging = ">=22.0" -PyJWT = "^2.6.0" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/requirements-full.txt b/requirements-full.txt index 2d54e78ae8d..8fe70db54bc 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -85,7 +85,7 @@ fred==3.1 fredapi==0.4.3 frozendict==2.3.4; python_version >= "3.6" frozenlist==1.3.1; python_version >= "3.7" and python_full_version >= "3.7.0" -fsspec==2022.11.0; python_version >= "3.7" +fsspec==2023.1.0; python_version >= "3.7" fundamentalanalysis==0.2.14 future==0.18.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" gitdb==4.0.9; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.7" @@ -219,7 +219,6 @@ pyex==0.5.0 pyflakes==2.3.1; python_full_version >= "3.7.0" pygments==2.13.0; python_version >= "3.6" pyhdfe==0.1.0; python_version >= "3.8" -pyjwt==2.6.0; python_version >= "3.7" pylint==2.15.2; python_full_version >= "3.7.2" pyluach==2.0.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" pymeeus==0.5.11; python_version >= "3.7" and python_version < "4" diff --git a/requirements.txt b/requirements.txt index a08c8368529..607ee0a1136 100644 --- a/requirements.txt +++ b/requirements.txt @@ -169,7 +169,6 @@ pyex==0.5.0 pyflakes==2.3.1; python_full_version >= "3.7.0" pygments==2.13.0; python_version >= "3.6" pyhdfe==0.1.0; python_version >= "3.8" -pyjwt==2.6.0; python_version >= "3.7" pyluach==2.0.2; python_version >= "3.8" and python_version < "4.0" and python_full_version >= "3.7.0" pymeeus==0.5.11; python_version >= "3.7" and python_version < "4" pympler==1.0.1; python_version >= "3.7" and python_full_version < "3.9.7" or python_full_version > "3.9.7" and python_version >= "3.6" From af46d2eacf3a574428e091f08c746ab74fb7e9d0 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 17:56:09 +0000 Subject: [PATCH 123/230] pull command output --- openbb_terminal/account/account_controller.py | 12 ++++++--- openbb_terminal/account/account_model.py | 27 +++++++++++++++++++ openbb_terminal/miscellaneous/i18n/en.yml | 4 +-- openbb_terminal/session/local_model.py | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 openbb_terminal/account/account_model.py diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 2cf6424407f..d8d3b50be35 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -13,7 +13,9 @@ from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal import feature_flags as obbff from openbb_terminal.menu import session from openbb_terminal.session import hub_model as Hub +from openbb_terminal.session import local_model as Local from openbb_terminal.session.user import User +from openbb_terminal.account.account_model import show_diff # from openbb_terminal.session.user import User @@ -128,14 +130,16 @@ class AccountController(BaseController): if ns_parser: response = Hub.fetch_user_configs(User.get_session()) if response: - console.print("Remote data:", style="info") - console.print(json.loads(response.content)) + configs = json.loads(response.content) + show_diff(configs=configs) + i = console.input( "\nDo you want to overwrite your local configurations " - "with the remote? (y/n): " + "with the above? (y/n): " ) if i.lower() in ["y", "yes"]: - console.print("[green]\nApplied remote configs.[/green]") + Local.apply_configs(configs=configs) + console.print("\nDone.", style="info") else: console.print("\nAborted.", style="info") diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py new file mode 100644 index 00000000000..c276775fbef --- /dev/null +++ b/openbb_terminal/account/account_model.py @@ -0,0 +1,27 @@ +from openbb_terminal.rich_config import console + + +def show_diff(configs: dict): + """Show the diff between the local and remote configs. + + Parameters + ---------- + configs : dict + The configs. + """ + if configs: + settings = configs.get("features_settings", {}) + keys = configs.get("features_keys", {}) + + if settings: + console.print("Settings:", style="info") + for k, v in sorted(settings.items()): + console.print(f"[menu]{k}[/menu]: {v}") + + if keys: + if settings: + console.print("") + + console.print("Keys:", style="info") + for k, v in sorted(keys.items()): + console.print(f"[menu]{k}[/menu]: {v}") diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 54fc824ae78..9c26988fff0 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -19,8 +19,8 @@ en: _main_menu_: Main menu account/_info_: Cloud storage of keys, settings and feature flags account/sync: Turns the cloud synchronization on/off - account/pull: Pull from cloud - account/clear: Clear data from remote cloud + account/pull: Pull data from cloud + account/clear: Clear data from cloud account/_routines_: Routines account/upload: Upload routine account/download: Download routine diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 0cf22bc04c6..1c8270c2526 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -87,7 +87,7 @@ def apply_configs(configs: dict): configs : dict The configurations. """ - console.print(configs, style="red") + # console.print(configs, style="red") if configs: settings = configs.get("features_settings", {}) From e889e7a6220332aefab140d0e8d55d567499a11e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 17:59:21 +0000 Subject: [PATCH 124/230] remove character --- openbb_terminal/account/account_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index d8d3b50be35..e8b2825fdc9 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -155,7 +155,7 @@ class AccountController(BaseController): if ns_parser: i = console.input( "[red]This action is irreversible![/red]\n" - "> Are you sure you want to permanently delete your data? (y/n): " + "Are you sure you want to permanently delete your data? (y/n): " ) if i.lower() in ["y", "yes"]: console.print("") From f54c72160d71e6db927fc5ed575ba5467b970da0 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 18:23:35 +0000 Subject: [PATCH 125/230] pull cmd when no data --- openbb_terminal/account/account_controller.py | 22 +++++++++---------- openbb_terminal/account/account_model.py | 19 +++++++++++----- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index e8b2825fdc9..09cc80c9e45 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -131,17 +131,17 @@ class AccountController(BaseController): response = Hub.fetch_user_configs(User.get_session()) if response: configs = json.loads(response.content) - show_diff(configs=configs) - - i = console.input( - "\nDo you want to overwrite your local configurations " - "with the above? (y/n): " - ) - if i.lower() in ["y", "yes"]: - Local.apply_configs(configs=configs) - console.print("\nDone.", style="info") - else: - console.print("\nAborted.", style="info") + configs.pop("email", None) + if show_diff(configs=configs): + i = console.input( + "\nDo you want to overwrite your local configurations " + "with the above? (y/n): " + ) + if i.lower() in ["y", "yes"]: + Local.apply_configs(configs=configs) + console.print("\nDone.", style="info") + else: + console.print("\nAborted.", style="info") def call_clear(self, _): """Clear data""" diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index c276775fbef..1cd22aafbf2 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,18 +1,23 @@ from openbb_terminal.rich_config import console -def show_diff(configs: dict): +def show_diff(configs: dict) -> bool: """Show the diff between the local and remote configs. Parameters ---------- configs : dict The configs. - """ - if configs: - settings = configs.get("features_settings", {}) - keys = configs.get("features_keys", {}) + Returns + ------- + bool + True if configs are found, False otherwise. + """ + settings = configs.get("features_settings", {}) + keys = configs.get("features_keys", {}) + + if settings or keys: if settings: console.print("Settings:", style="info") for k, v in sorted(settings.items()): @@ -25,3 +30,7 @@ def show_diff(configs: dict): console.print("Keys:", style="info") for k, v in sorted(keys.items()): console.print(f"[menu]{k}[/menu]: {v}") + + return True + console.print("No configurations found.", style="info") + return False From 5befc2f9f793d3ebc81b786ae1c43a3ab672de57 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 27 Jan 2023 18:59:16 +0000 Subject: [PATCH 126/230] add space below whoami --- openbb_terminal/parent_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 23dd7c257df..2e50eb6a761 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -688,6 +688,7 @@ class BaseController(metaclass=ABCMeta): if ns_parser: User.whoami() + console.print("") @staticmethod def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]): From 5a591e2e13985968e52e5e32feff9248093c1591 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 29 Jan 2023 17:57:44 +0000 Subject: [PATCH 127/230] fix log filenotfound exception --- openbb_terminal/core/log/collection/log_sender.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/core/log/collection/log_sender.py b/openbb_terminal/core/log/collection/log_sender.py index a42a53f5e8a..5849cdb6627 100644 --- a/openbb_terminal/core/log/collection/log_sender.py +++ b/openbb_terminal/core/log/collection/log_sender.py @@ -130,7 +130,8 @@ class LogSender(Thread): def max_size_exceeded(self, file: Path) -> bool: """Check if the log file is bigger than 2MB.""" - + if not file.exists(): + return False return file.stat().st_size > 2 * 1024 * 1024 def send_path(self, path: Path, last: bool = False): From 1d178ecd2611e5cc7291abeb7a001b9545d775c6 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Tue, 31 Jan 2023 09:49:59 +0000 Subject: [PATCH 128/230] changed arg name --- openbb_terminal/session/sdk_session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py index 47b57b9c826..769f470a910 100644 --- a/openbb_terminal/session/sdk_session.py +++ b/openbb_terminal/session/sdk_session.py @@ -10,11 +10,11 @@ def get_session(email: str, password: str, save: bool): return session -def login(email: str = "", password: str = "", save: bool = False): +def login(email: str = "", password: str = "", keep_session: bool = False): session = Local.get_session() if not session: - session = get_session(email, password, save) + session = get_session(email, password, keep_session) if session_model.login(session) in [ session_model.LoginStatus.FAILED, From 309e52e80e94a5a0ef0f072cd65c33dfc727283b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 10:50:14 +0000 Subject: [PATCH 129/230] delete .hist on logout --- openbb_terminal/core/config/paths.py | 1 + openbb_terminal/session/local_model.py | 25 ++++++++++++++++++++++++ openbb_terminal/session/session_model.py | 1 + 3 files changed, 27 insertions(+) diff --git a/openbb_terminal/core/config/paths.py b/openbb_terminal/core/config/paths.py index 0ee760a4ae6..0eee6312585 100644 --- a/openbb_terminal/core/config/paths.py +++ b/openbb_terminal/core/config/paths.py @@ -35,6 +35,7 @@ REPOSITORY_ENV_FILE = REPOSITORY_DIRECTORY / ".env" PACKAGE_ENV_FILE = PACKAGE_DIRECTORY / ".env" SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_terminal" +HIST_FILE_PATH = os.path.join(os.path.expanduser("~"), ".openbb_terminal.his") USER_ENV_FILE = SETTINGS_DIRECTORY / ".env" USER_DATA_DIRECTORY = get_user_data_directory() USER_EXPORTS_DIRECTORY = USER_DATA_DIRECTORY / "exports" diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 1c8270c2526..e6582051c80 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -3,6 +3,7 @@ from pathlib import Path import json from typing import Optional from openbb_terminal.core.config.paths import ( + HIST_FILE_PATH, SETTINGS_DIRECTORY, USER_ROUTINES_DIRECTORY, ) @@ -79,6 +80,30 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: return False +def remove_cli_history_file(file_path: Path = Path(HIST_FILE_PATH)) -> bool: + """Remove the cli history file. + + Parameters + ---------- + file_path : Path + The file path. + + Returns + ------- + bool + The status of the removal. + """ + + try: + if os.path.isfile(file_path): + os.remove(file_path) + return True + return True + except Exception: + console.print("[red]\nFailed to remove terminal history file.[/red]") + return False + + def apply_configs(configs: dict): """Apply configurations. diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index dc1eaeb3db9..fe82b99fa72 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -86,5 +86,6 @@ def logout(cls: bool = False): Hub.delete_session() Local.remove_session_file() + Local.remove_cli_history_file() plt.close("all") console.print("[green]\nLogout successful.[/green]") From 873477991bf79f03b3a4fae4f5760ca2a243b5c8 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 13:50:11 +0000 Subject: [PATCH 130/230] add asteriscs to password --- openbb_terminal/menu.py | 7 ++-- openbb_terminal/rich_config.py | 11 ++---- openbb_terminal/session/session_controller.py | 23 +++++++++-- openbb_terminal/session/session_model.py | 38 +++++++++++++++++++ 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/openbb_terminal/menu.py b/openbb_terminal/menu.py index 9b4909ac894..413540c177e 100644 --- a/openbb_terminal/menu.py +++ b/openbb_terminal/menu.py @@ -1,13 +1,14 @@ import sys from typing import Optional import logging -import os from matplotlib import pyplot from prompt_toolkit import PromptSession from prompt_toolkit.eventloop.inputhook import set_eventloop_with_inputhook from prompt_toolkit.history import FileHistory +from openbb_terminal.core.config.paths import HIST_FILE_PATH + logger = logging.getLogger(__name__) @@ -47,12 +48,10 @@ def inputhook(inputhook_context): return False -history_file = os.path.join(os.path.expanduser("~"), ".openbb_terminal.his") - try: if not is_jupyter() and not is_papermill(): session: Optional[PromptSession] = PromptSession( - history=FileHistory(history_file) + history=FileHistory(HIST_FILE_PATH) ) set_eventloop_with_inputhook(inputhook) else: diff --git a/openbb_terminal/rich_config.py b/openbb_terminal/rich_config.py index 2e75fac3b4e..3a01ea849e7 100644 --- a/openbb_terminal/rich_config.py +++ b/openbb_terminal/rich_config.py @@ -1,7 +1,6 @@ """Rich Module""" __docformat__ = "numpy" -import getpass import os import json from pathlib import Path @@ -315,13 +314,9 @@ class ConsoleAndPanel: else: print(*args, **kwargs) - def input(self, *args, **kwargs): - self.print(*args, **kwargs, end="") - return input() - - def getpass(self, *args, **kwargs): - self.print(*args, **kwargs, end="") - return getpass.getpass("") + # def input(self, *args, **kwargs): + # self.print(*args, **kwargs, end="") + # return input() console = ConsoleAndPanel() diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 288c4b78289..c70ef770aa6 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,14 +1,20 @@ from typing import Tuple +from prompt_toolkit import PromptSession +from prompt_toolkit.output.color_depth import ColorDepth from openbb_terminal import terminal_controller import openbb_terminal.session.local_model as Local from openbb_terminal.session.session_model import ( LoginStatus, + color_message, create_session, + get_color, login, ) from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console +# pylint: disable=consider-using-f-string + def display_welcome_message(): """Display welcome message""" @@ -25,9 +31,20 @@ def get_user_input() -> Tuple[str, str, bool]: The user email, password and save login option. """ console.print("\nPlease enter your credentials:", style="info") - email = console.input("> Email: ", style="menu") - password = console.getpass("> Password: ", style="menu") - save_str = console.input("> Remember me? (y/n): ", style="menu").lower() + + s: PromptSession = PromptSession(color_depth=ColorDepth.TRUE_COLOR) + color = get_color() + + email = s.prompt( + message=color_message("> Email: ", color), + ) + password = s.prompt( + message=color_message("> Password: ", color), + is_password=True, + ) + save_str = s.prompt( + message=color_message("> Remember me? (y/n): ", color), is_password=False + ).lower() save = False if save_str == "y": save = True diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index fe82b99fa72..7789b69d996 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -5,11 +5,13 @@ import sys import json from enum import Enum import matplotlib.pyplot as plt +from prompt_toolkit import HTML import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console +from openbb_terminal.rich_config import CUSTOM_THEME class LoginStatus(Enum): @@ -89,3 +91,39 @@ def logout(cls: bool = False): Local.remove_cli_history_file() plt.close("all") console.print("[green]\nLogout successful.[/green]") + + +def get_color() -> str: + """Get prompt session + + Returns + ------- + str + The hex color. + """ + hex_color = "#ffffff" + c = CUSTOM_THEME.styles["menu"].color + if c: + rgb = c.triplet + if rgb: + hex_color = "#%02x%02x%02x" % (rgb.red, rgb.green, rgb.blue) + + return hex_color + + +def color_message(msg: str, color: str) -> HTML: + """Get colorized message + + Parameters + ---------- + msg : str + The message. + color : str + The hex color. + + Returns + ------- + HTML + The HTML message. + """ + return HTML(f'') From 0d22dc84a6b705c497be4e5c2e2c21969362ba87 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 13:51:59 +0000 Subject: [PATCH 131/230] pylint --- openbb_terminal/session/session_controller.py | 2 -- openbb_terminal/session/session_model.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index c70ef770aa6..a19ec2391bb 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -13,8 +13,6 @@ from openbb_terminal.session.session_model import ( from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console -# pylint: disable=consider-using-f-string - def display_welcome_message(): """Display welcome message""" diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 7789b69d996..7b8c3807806 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -14,6 +14,9 @@ from openbb_terminal.rich_config import console from openbb_terminal.rich_config import CUSTOM_THEME +# pylint: disable=consider-using-f-string + + class LoginStatus(Enum): SUCCESS = "success" FAILED = "failed" From c588bb9870444af8e984736b05fd09452540da8e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 13:54:39 +0000 Subject: [PATCH 132/230] uncomment rich config --- openbb_terminal/rich_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/rich_config.py b/openbb_terminal/rich_config.py index 3a01ea849e7..4854318697c 100644 --- a/openbb_terminal/rich_config.py +++ b/openbb_terminal/rich_config.py @@ -314,9 +314,9 @@ class ConsoleAndPanel: else: print(*args, **kwargs) - # def input(self, *args, **kwargs): - # self.print(*args, **kwargs, end="") - # return input() + def input(self, *args, **kwargs): + self.print(*args, **kwargs, end="") + return input() console = ConsoleAndPanel() From 59d45fa9267459b3b027efc7e7aaacd4482063c8 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 14:32:10 +0000 Subject: [PATCH 133/230] show only diffs on pull --- openbb_terminal/account/account_controller.py | 2 + openbb_terminal/account/account_model.py | 115 +++++++++++++++--- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 09cc80c9e45..4a531d93359 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -132,6 +132,8 @@ class AccountController(BaseController): if response: configs = json.loads(response.content) configs.pop("email", None) + + if show_diff(configs=configs): i = console.input( "\nDo you want to overwrite your local configurations " diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 1cd22aafbf2..e837d218880 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,4 +1,8 @@ from openbb_terminal.rich_config import console +from openbb_terminal import feature_flags as obbff +from openbb_terminal import config_terminal as cfg +from openbb_terminal import config_plot as cfg_plot +from openbb_terminal.base_helpers import strtobool def show_diff(configs: dict) -> bool: @@ -12,25 +16,106 @@ def show_diff(configs: dict) -> bool: Returns ------- bool - True if configs are found, False otherwise. + True if there is a diff. + """ + diff_settings = get_diff_settings(configs) + if diff_settings: + console.print("Settings:", style="info") + console.print("") + for k, v in diff_settings.items(): + console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") + + diff_keys = get_diff_keys(configs) + if diff_keys: + console.print("Keys:", style="info") + for k, v in diff_keys.items(): + console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") + + if diff_settings or diff_keys: + return True + return False + + +def get_diff_settings(configs: dict) -> dict: + """Get the diff between the local and remote settings. + + Parameters + ---------- + configs : dict + The configs. + + Returns + ------- + dict + The diff. """ settings = configs.get("features_settings", {}) - keys = configs.get("features_keys", {}) - - if settings or keys: + diff = {} + if settings: if settings: - console.print("Settings:", style="info") for k, v in sorted(settings.items()): - console.print(f"[menu]{k}[/menu]: {v}") + if hasattr(obbff, k): + new, old = get_diff(obbff, k, v) + if new is not None: + diff[k] = (new, old) + elif hasattr(cfg, k): + new, old = get_diff(cfg, k, v) + if new is not None: + diff[k] = (new, old) + elif hasattr(cfg_plot, k): + new, old = get_diff(cfg_plot, k, v) + if new is not None: + diff[k] = (new, old) - if keys: - if settings: - console.print("") + return diff - console.print("Keys:", style="info") - for k, v in sorted(keys.items()): - console.print(f"[menu]{k}[/menu]: {v}") - return True - console.print("No configurations found.", style="info") - return False +def get_diff_keys(configs: dict) -> dict: + """Get the diff between the local and remote keys. + + Parameters + ---------- + configs : dict + The configs. + + Returns + ------- + dict + The diff. + """ + keys = configs.get("features_keys", {}) + diff = {} + if keys: + for k, v in sorted(keys.items()): + new, old = get_diff(cfg, k, v) + if new is not None: + diff[k] = (new, old) + + return diff + + +def get_diff(obj, name, value): + """Set attribute in object. + + Parameters + ---------- + obj : object + The object. + name : str + The attribute name. + value : str + The attribute value. + """ + current_value = getattr(obj, name) + + if value.lower() in ["true", "false"]: + cast_value = strtobool(value) + elif isinstance(getattr(obj, name), int): + cast_value = int(value) + else: + cast_value = value + + if current_value != cast_value: + return current_value, cast_value + + return None, None From b8d82de15d74863065f5ce28c937fff2f81bd891 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 14:32:22 +0000 Subject: [PATCH 134/230] black# --- openbb_terminal/account/account_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 4a531d93359..28c8e9a4ab6 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -133,7 +133,6 @@ class AccountController(BaseController): configs = json.loads(response.content) configs.pop("email", None) - if show_diff(configs=configs): i = console.input( "\nDo you want to overwrite your local configurations " From 653412e4cd8d6f561d85dcecf5d60ddd0a2e0889 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 14:34:04 +0000 Subject: [PATCH 135/230] warn when no changes# --- openbb_terminal/account/account_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 28c8e9a4ab6..a91195f1e85 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -143,6 +143,8 @@ class AccountController(BaseController): console.print("\nDone.", style="info") else: console.print("\nAborted.", style="info") + else: + console.print("No changes to apply.", style="info") def call_clear(self, _): """Clear data""" From 58a31ce6ca927e1744f7ba8a219054b213621d78 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 14:40:43 +0000 Subject: [PATCH 136/230] pull msg was switched --- openbb_terminal/account/account_model.py | 31 ++++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index e837d218880..c684ae1e484 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -52,20 +52,19 @@ def get_diff_settings(configs: dict) -> dict: settings = configs.get("features_settings", {}) diff = {} if settings: - if settings: - for k, v in sorted(settings.items()): - if hasattr(obbff, k): - new, old = get_diff(obbff, k, v) - if new is not None: - diff[k] = (new, old) - elif hasattr(cfg, k): - new, old = get_diff(cfg, k, v) - if new is not None: - diff[k] = (new, old) - elif hasattr(cfg_plot, k): - new, old = get_diff(cfg_plot, k, v) - if new is not None: - diff[k] = (new, old) + for k, v in sorted(settings.items()): + if hasattr(obbff, k): + old, new = get_diff(obbff, k, v) + if new is not None: + diff[k] = (old, new) + elif hasattr(cfg, k): + old, new = get_diff(cfg, k, v) + if new is not None: + diff[k] = (old, new) + elif hasattr(cfg_plot, k): + old, new = get_diff(cfg_plot, k, v) + if new is not None: + diff[k] = (old, new) return diff @@ -87,9 +86,9 @@ def get_diff_keys(configs: dict) -> dict: diff = {} if keys: for k, v in sorted(keys.items()): - new, old = get_diff(cfg, k, v) + old, new = get_diff(cfg, k, v) if new is not None: - diff[k] = (new, old) + diff[k] = (old, new) return diff From d289188df9306390bca8999dc35b9c35fe5f0551 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 15:57:03 +0000 Subject: [PATCH 137/230] apply diff only --- openbb_terminal/account/account_controller.py | 7 ++-- openbb_terminal/account/account_model.py | 42 +++++++++++-------- openbb_terminal/session/local_model.py | 3 +- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index a91195f1e85..4309d655015 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -15,7 +15,7 @@ from openbb_terminal.menu import session from openbb_terminal.session import hub_model as Hub from openbb_terminal.session import local_model as Local from openbb_terminal.session.user import User -from openbb_terminal.account.account_model import show_diff +from openbb_terminal.account.account_model import get_diff # from openbb_terminal.session.user import User @@ -133,13 +133,14 @@ class AccountController(BaseController): configs = json.loads(response.content) configs.pop("email", None) - if show_diff(configs=configs): + configs_diff = get_diff(configs=configs) + if configs_diff: i = console.input( "\nDo you want to overwrite your local configurations " "with the above? (y/n): " ) if i.lower() in ["y", "yes"]: - Local.apply_configs(configs=configs) + Local.apply_configs(configs=configs_diff) console.print("\nDone.", style="info") else: console.print("\nAborted.", style="info") diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index c684ae1e484..e4ef36a46db 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,3 +1,4 @@ +from typing import Dict, Any from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg @@ -5,7 +6,7 @@ from openbb_terminal import config_plot as cfg_plot from openbb_terminal.base_helpers import strtobool -def show_diff(configs: dict) -> bool: +def get_diff(configs: dict) -> dict: """Show the diff between the local and remote configs. Parameters @@ -18,25 +19,34 @@ def show_diff(configs: dict) -> bool: bool True if there is a diff. """ - diff_settings = get_diff_settings(configs) + SETTINGS = "features_settings" + KEYS = "features_keys" + configs_diff: Dict[str, Dict[str, Any]] = {SETTINGS: {}, KEYS: {}} + + diff_settings = get_diff_settings(configs.get(SETTINGS, {})) if diff_settings: console.print("Settings:", style="info") - console.print("") for k, v in diff_settings.items(): + configs_diff[SETTINGS][k] = v[1] console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") - diff_keys = get_diff_keys(configs) + diff_keys = get_diff_keys(configs.get(KEYS, {})) if diff_keys: console.print("Keys:", style="info") for k, v in diff_keys.items(): + configs_diff[KEYS][k] = v[1] console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") - if diff_settings or diff_keys: - return True - return False + if not configs_diff[SETTINGS]: + configs_diff.pop(SETTINGS) + + if not configs_diff[KEYS]: + configs_diff.pop(KEYS) + + return configs_diff -def get_diff_settings(configs: dict) -> dict: +def get_diff_settings(settings: dict) -> dict: """Get the diff between the local and remote settings. Parameters @@ -49,27 +59,26 @@ def get_diff_settings(configs: dict) -> dict: dict The diff. """ - settings = configs.get("features_settings", {}) diff = {} if settings: for k, v in sorted(settings.items()): if hasattr(obbff, k): - old, new = get_diff(obbff, k, v) + old, new = get_var_diff(obbff, k, v) if new is not None: diff[k] = (old, new) elif hasattr(cfg, k): - old, new = get_diff(cfg, k, v) + old, new = get_var_diff(cfg, k, v) if new is not None: diff[k] = (old, new) elif hasattr(cfg_plot, k): - old, new = get_diff(cfg_plot, k, v) + old, new = get_var_diff(cfg_plot, k, v) if new is not None: diff[k] = (old, new) return diff -def get_diff_keys(configs: dict) -> dict: +def get_diff_keys(keys: dict) -> dict: """Get the diff between the local and remote keys. Parameters @@ -82,18 +91,17 @@ def get_diff_keys(configs: dict) -> dict: dict The diff. """ - keys = configs.get("features_keys", {}) diff = {} if keys: for k, v in sorted(keys.items()): - old, new = get_diff(cfg, k, v) + old, new = get_var_diff(cfg, k, v) if new is not None: diff[k] = (old, new) return diff -def get_diff(obj, name, value): +def get_var_diff(obj, name, value): """Set attribute in object. Parameters @@ -107,7 +115,7 @@ def get_diff(obj, name, value): """ current_value = getattr(obj, name) - if value.lower() in ["true", "false"]: + if str(value).lower() in ["true", "false"]: cast_value = strtobool(value) elif isinstance(getattr(obj, name), int): cast_value = int(value) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index e6582051c80..2e60e9aa556 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -168,7 +168,8 @@ def cast_set_attr(obj, name, value): value : str The attribute value. """ - if value.lower() in ["true", "false"]: + + if str(value).lower() in ["true", "false"]: setattr(obj, name, strtobool(value)) elif isinstance(getattr(obj, name), int): setattr(obj, name, int(value)) From f2951161f9f4fb709c555776727f15014d394a60 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 16:04:10 +0000 Subject: [PATCH 138/230] breaking?: strbool change --- openbb_terminal/base_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/base_helpers.py b/openbb_terminal/base_helpers.py index b58ada0383f..7f0bce781c4 100644 --- a/openbb_terminal/base_helpers.py +++ b/openbb_terminal/base_helpers.py @@ -78,9 +78,9 @@ def strtobool(val): """ val = str(val).lower() if val in ("y", "yes", "t", "true", "on", "1"): - output = 1 + output = True elif val in ("n", "no", "f", "false", "off", "0"): - output = 0 + output = False else: raise ValueError(f"invalid truth value {val}") From e5854bc2da74e69ee8d440cd212355f185b05364 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 17:26:40 +0000 Subject: [PATCH 139/230] reduce some code --- openbb_terminal/account/account_controller.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 4309d655015..337ff7a6513 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -130,10 +130,7 @@ class AccountController(BaseController): if ns_parser: response = Hub.fetch_user_configs(User.get_session()) if response: - configs = json.loads(response.content) - configs.pop("email", None) - - configs_diff = get_diff(configs=configs) + configs_diff = get_diff(configs=json.loads(response.content)) if configs_diff: i = console.input( "\nDo you want to overwrite your local configurations " From f12c15b6582be929510f6a6f4fccbbe936955b56 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 17:48:00 +0000 Subject: [PATCH 140/230] add timeout to hub requests --- openbb_terminal/session/hub_model.py | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 6b25616ebce..72d6f321b17 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -5,6 +5,7 @@ from openbb_terminal.session.user import User from openbb_terminal.rich_config import console BASE_URL = "http://127.0.0.1:8000/" +TIMEOUT = 5 def create_session( @@ -32,10 +33,13 @@ def create_session( "password": password, "remember": True, } - return requests.post(base_url + "login", json=data) + return requests.post(url=base_url + "login", json=data, timeout=TIMEOUT) except requests.exceptions.ConnectionError: console.print("[red]\nConnection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]\nFailed to request login info.[/red]") return None @@ -98,6 +102,7 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: response = requests.get( url=BASE_URL + "terminal/user", headers={"Authorization": f"{token_type.title()} {token}"}, + timeout=TIMEOUT, ) if response.status_code != 200: console.print("[red]\nFailed to fetch configurations.[/red]") @@ -105,6 +110,9 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: except requests.exceptions.ConnectionError: console.print("[red]\nConnection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]\nFailed to fetch configurations.[/red]") return None @@ -139,6 +147,7 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re url=BASE_URL + "terminal/user-json", headers={"Authorization": User.get_token()}, json=data, + timeout=TIMEOUT, ) if response.status_code == 200: console.print("[green]Saved remotely.[/green]") @@ -148,6 +157,9 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re except requests.exceptions.ConnectionError: console.print("[red]Connection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]Failed to save remotely.[/red]") return None @@ -162,6 +174,7 @@ def clear_user_configs() -> Optional[requests.Response]: url=BASE_URL + "terminal/user", headers={"Authorization": User.get_token()}, json=data, + timeout=TIMEOUT, ) if response.status_code == 200: console.print("[green]Cleared configurations.[/green]") @@ -171,6 +184,9 @@ def clear_user_configs() -> Optional[requests.Response]: except requests.exceptions.ConnectionError: console.print("[red]Connection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]Failed to clear configurations.[/red]") return None @@ -193,6 +209,7 @@ def logout_everywhere() -> Optional[requests.Response]: response = requests.get( url=BASE_URL + "logout-everywhere", headers={"Authorization": User.get_token()}, + timeout=TIMEOUT, ) if response.status_code == 200: console.print("[green]\nLogged out remotely.[/green]") @@ -202,6 +219,9 @@ def logout_everywhere() -> Optional[requests.Response]: except requests.exceptions.ConnectionError: console.print("[red]\nConnection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]\nFailed to logout remotely.[/red]") return None @@ -220,6 +240,7 @@ def upload_routine( headers={"Authorization": User.get_token()}, url=BASE_URL + "terminal/script", json=data, + timeout=TIMEOUT, ) if response.status_code == 200: console.print("[green]Successfully uploaded your routine.[/green]") @@ -229,6 +250,9 @@ def upload_routine( except requests.exceptions.ConnectionError: console.print("[red]Connection error.[/red]") return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None except Exception: console.print("[red]Failed to upload your routine.[/red]") return None @@ -250,6 +274,7 @@ def download_routine( # headers={"Authorization": User.get_token()}, # url=BASE_URL + "terminal/script", # json=data, + # timeout=TIMEOUT, # ) # if response.status_code == 200: # console.print("Successfully downloaded your routine.") @@ -259,6 +284,9 @@ def download_routine( # except requests.exceptions.ConnectionError: # console.print("[red]Connection error.[/red]") # return None + # except requests.exceptions.Timeout: + # console.print("[red]\nConnection timeout.[/red]") + # return None # except Exception: # console.print("[red]Failed to download your routine.[/red]") # return None From 5ee313f01a77c0763ee04b6a7fefb6cffc1e1229 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 17:48:24 +0000 Subject: [PATCH 141/230] black --- openbb_terminal/portfolio/portfolio_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index aa9cba7b253..77829244746 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -435,7 +435,9 @@ class PortfolioController(BaseController): "[red]Please first load transactions file using 'load'[/red]" ) else: - if self.portfolio.set_benchmark(ns_parser.benchmark, ns_parser.full_shares): + if self.portfolio.set_benchmark( + ns_parser.benchmark, ns_parser.full_shares + ): self.benchmark_name = statics.BENCHMARK_CHOICES.get( ns_parser.benchmark, ns_parser.benchmark ) From 9181117fdef0f0c9a7afb24d264dbec98a61856b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 18:44:33 +0000 Subject: [PATCH 142/230] add remote logout --- openbb_terminal/session/hub_model.py | 87 +++++++++++------------- openbb_terminal/session/session_model.py | 4 +- openbb_terminal/session/user.py | 7 +- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 72d6f321b17..729578c23c9 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -5,7 +5,7 @@ from openbb_terminal.session.user import User from openbb_terminal.rich_config import console BASE_URL = "http://127.0.0.1:8000/" -TIMEOUT = 5 +TIMEOUT = 15 def create_session( @@ -45,6 +45,31 @@ def create_session( return None +def delete_session(base_url=BASE_URL): + """Delete the session.""" + try: + response = requests.post( + url=base_url + "logout", + headers={"Authorization": User.get_auth_header()}, + json={"token": User.get_token()}, + timeout=TIMEOUT, + ) + if response.status_code == 200: + console.print("[green]Server session deleted.[/green]") + else: + console.print("[red]Failed to delete server session.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print("[red]Connection error.[/red]") + return None + except requests.exceptions.Timeout: + console.print("[red]\nConnection timeout.[/red]") + return None + except Exception: + console.print("[red]Failed to delete server session.[/red]") + return None + + def process_session_response(response: requests.Response) -> dict: """Process the response from the login request. @@ -92,7 +117,7 @@ def get_session(email: str, password: str) -> dict: return process_session_response(response) -def fetch_user_configs(session: dict) -> Optional[requests.Response]: +def fetch_user_configs(session: dict, base_url=BASE_URL) -> Optional[requests.Response]: """Fetch user configurations.""" token_type = session.get("token_type", "") @@ -100,7 +125,7 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: try: response = requests.get( - url=BASE_URL + "terminal/user", + url=base_url + "terminal/user", headers={"Authorization": f"{token_type.title()} {token}"}, timeout=TIMEOUT, ) @@ -118,7 +143,9 @@ def fetch_user_configs(session: dict) -> Optional[requests.Response]: return None -def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Response]: +def patch_user_configs( + key: str, value: str, type_: str, base_url=BASE_URL +) -> Optional[requests.Response]: """Patch user configurations to the server. Parameters @@ -144,8 +171,8 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re try: response = requests.patch( - url=BASE_URL + "terminal/user-json", - headers={"Authorization": User.get_token()}, + url=base_url + "terminal/user-json", + headers={"Authorization": User.get_auth_header()}, json=data, timeout=TIMEOUT, ) @@ -165,14 +192,14 @@ def patch_user_configs(key: str, value: str, type_: str) -> Optional[requests.Re return None -def clear_user_configs() -> Optional[requests.Response]: +def clear_user_configs(base_url=BASE_URL) -> Optional[requests.Response]: """Clear user configurations to the server.""" data: Dict[str, dict] = {"features_keys": {}, "features_settings": {}} try: response = requests.put( - url=BASE_URL + "terminal/user", - headers={"Authorization": User.get_token()}, + url=base_url + "terminal/user", + headers={"Authorization": User.get_auth_header()}, json=data, timeout=TIMEOUT, ) @@ -192,44 +219,10 @@ def clear_user_configs() -> Optional[requests.Response]: return None -def delete_session(): - """Delete the session.""" - pass - - -def logout_everywhere() -> Optional[requests.Response]: - """Request a remote logout. - - Returns - ------- - bool - The status of the logout. - """ - try: - response = requests.get( - url=BASE_URL + "logout-everywhere", - headers={"Authorization": User.get_token()}, - timeout=TIMEOUT, - ) - if response.status_code == 200: - console.print("[green]\nLogged out remotely.[/green]") - return response - console.print("[red]\nFailed to logout remotely.[/red]") - return None - except requests.exceptions.ConnectionError: - console.print("[red]\nConnection error.[/red]") - return None - except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") - return None - except Exception: - console.print("[red]\nFailed to logout remotely.[/red]") - return None - - def upload_routine( name: str = "", routine: str = "", + base_url=BASE_URL, ) -> Optional[requests.Response]: """Send a routine to the server.""" @@ -237,8 +230,8 @@ def upload_routine( try: response = requests.post( - headers={"Authorization": User.get_token()}, - url=BASE_URL + "terminal/script", + headers={"Authorization": User.get_auth_header()}, + url=base_url + "terminal/script", json=data, timeout=TIMEOUT, ) @@ -271,7 +264,7 @@ def download_routine( # try: # response = requests.get( - # headers={"Authorization": User.get_token()}, + # headers={"Authorization": User.get_auth_header()}, # url=BASE_URL + "terminal/script", # json=data, # timeout=TIMEOUT, diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 7b8c3807806..54032b144b8 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -72,6 +72,9 @@ def logout(cls: bool = False): """ if cls: system_clear() + + if not User.is_guest(): + Hub.delete_session() User.clear() # Clear openbb environment variables @@ -89,7 +92,6 @@ def logout(cls: bool = False): if module.startswith("openbb"): importlib.reload(sys.modules[module]) - Hub.delete_session() Local.remove_session_file() Local.remove_cli_history_file() plt.close("all") diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index cf6aff072f5..5a78b021084 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -91,6 +91,11 @@ class User: return obbff.SYNC_ENABLED @classmethod - def get_token(cls): + def get_auth_header(cls): """Get token.""" return f"{User._TOKEN_TYPE.title()} {User._TOKEN}" + + @classmethod + def get_token(cls): + """Get token.""" + return User._TOKEN From b64a6703e3ecb2108bd3a4f50769130896da8f18 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Tue, 31 Jan 2023 18:53:05 +0000 Subject: [PATCH 143/230] success msg only if success --- openbb_terminal/session/hub_model.py | 4 +--- openbb_terminal/session/session_model.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 729578c23c9..2a3daf37c3f 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -54,9 +54,7 @@ def delete_session(base_url=BASE_URL): json={"token": User.get_token()}, timeout=TIMEOUT, ) - if response.status_code == 200: - console.print("[green]Server session deleted.[/green]") - else: + if response.status_code != 200: console.print("[red]Failed to delete server session.[/red]") return response except requests.exceptions.ConnectionError: diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 54032b144b8..6aeaa398f58 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -73,8 +73,11 @@ def logout(cls: bool = False): if cls: system_clear() + success = True if not User.is_guest(): - Hub.delete_session() + r = Hub.delete_session() + if r and r.status_code != 200: + success = False User.clear() # Clear openbb environment variables @@ -92,10 +95,15 @@ def logout(cls: bool = False): if module.startswith("openbb"): importlib.reload(sys.modules[module]) - Local.remove_session_file() - Local.remove_cli_history_file() + if not Local.remove_session_file(): + success = False + + if not Local.remove_cli_history_file(): + success = False plt.close("all") - console.print("[green]\nLogout successful.[/green]") + + if success: + console.print("[green]\nLogout successful.[/green]") def get_color() -> str: From 103133cbd441fbb4f3ab23413ebc14d57c82ce3a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 09:59:15 +0000 Subject: [PATCH 144/230] remove colors in prompt --- openbb_terminal/session/session_controller.py | 14 ++----- openbb_terminal/session/session_model.py | 39 ------------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index a19ec2391bb..24ee76eef17 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,13 +1,10 @@ from typing import Tuple from prompt_toolkit import PromptSession -from prompt_toolkit.output.color_depth import ColorDepth from openbb_terminal import terminal_controller import openbb_terminal.session.local_model as Local from openbb_terminal.session.session_model import ( LoginStatus, - color_message, create_session, - get_color, login, ) from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY @@ -30,19 +27,16 @@ def get_user_input() -> Tuple[str, str, bool]: """ console.print("\nPlease enter your credentials:", style="info") - s: PromptSession = PromptSession(color_depth=ColorDepth.TRUE_COLOR) - color = get_color() + s: PromptSession = PromptSession() email = s.prompt( - message=color_message("> Email: ", color), + message="> Email: ", ) password = s.prompt( - message=color_message("> Password: ", color), + message="> Password: ", is_password=True, ) - save_str = s.prompt( - message=color_message("> Remember me? (y/n): ", color), is_password=False - ).lower() + save_str = s.prompt(message="> Remember me? (y/n): ", is_password=False).lower() save = False if save_str == "y": save = True diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 6aeaa398f58..c1b911e81d2 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -5,14 +5,11 @@ import sys import json from enum import Enum import matplotlib.pyplot as plt -from prompt_toolkit import HTML import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub from openbb_terminal.session.user import User from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console -from openbb_terminal.rich_config import CUSTOM_THEME - # pylint: disable=consider-using-f-string @@ -104,39 +101,3 @@ def logout(cls: bool = False): if success: console.print("[green]\nLogout successful.[/green]") - - -def get_color() -> str: - """Get prompt session - - Returns - ------- - str - The hex color. - """ - hex_color = "#ffffff" - c = CUSTOM_THEME.styles["menu"].color - if c: - rgb = c.triplet - if rgb: - hex_color = "#%02x%02x%02x" % (rgb.red, rgb.green, rgb.blue) - - return hex_color - - -def color_message(msg: str, color: str) -> HTML: - """Get colorized message - - Parameters - ---------- - msg : str - The message. - color : str - The hex color. - - Returns - ------- - HTML - The HTML message. - """ - return HTML(f'') From 42337a80980cf3d214cc50ecaa081cd84f47cb47 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:10:07 +0000 Subject: [PATCH 145/230] update docstrings --- openbb_terminal/session/hub_model.py | 49 ++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 2a3daf37c3f..245ead1081d 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -45,8 +45,19 @@ def create_session( return None -def delete_session(base_url=BASE_URL): - """Delete the session.""" +def delete_session(base_url=BASE_URL) -> Optional[requests.Response]: + """Delete the session. + + Parameters + ---------- + base_url : str, optional + The base url, by default BASE_URL + + Returns + ------- + Optional[requests.Response] + The response from the logout request. + """ try: response = requests.post( url=base_url + "logout", @@ -116,7 +127,20 @@ def get_session(email: str, password: str) -> dict: def fetch_user_configs(session: dict, base_url=BASE_URL) -> Optional[requests.Response]: - """Fetch user configurations.""" + """Fetch user configurations. + + Parameters + ---------- + session : dict + The session info. + base_url : str, optional + The base url, by default BASE_URL + + Returns + ------- + Optional[requests.Response] + The response from the get request. + """ token_type = session.get("token_type", "") token = session.get("access_token", "") @@ -154,11 +178,13 @@ def patch_user_configs( The value to patch. type_ : str The type of the patch, either "keys" or "settings". + base_url : str, optional + The base url, by default BASE_URL Returns ------- - bool - The status of the patch. + Optional[requests.Response] + The response from the patch request. """ if type_ not in ["keys", "settings"]: @@ -191,7 +217,18 @@ def patch_user_configs( def clear_user_configs(base_url=BASE_URL) -> Optional[requests.Response]: - """Clear user configurations to the server.""" + """Clear user configurations to the server. + + Parameters + ---------- + base_url : str, optional + The base url, by default BASE_URL + + Returns + ------- + Optional[requests.Response] + The response from the put request. + """ data: Dict[str, dict] = {"features_keys": {}, "features_settings": {}} try: From 2bce7b5da52e3d06ef28deddedd0dfaeda1e1a58 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:25:07 +0000 Subject: [PATCH 146/230] keep timeout outside funcs --- openbb_terminal/account/account_controller.py | 2 +- openbb_terminal/session/hub_model.py | 53 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 337ff7a6513..7b3a71d385b 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -160,7 +160,7 @@ class AccountController(BaseController): ) if i.lower() in ["y", "yes"]: console.print("") - Hub.clear_user_configs() + Hub.clear_user_configs(auth_header=User.get_auth_header()) else: console.print("\nAborted.", style="info") diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 245ead1081d..3eae067f918 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -9,7 +9,7 @@ TIMEOUT = 15 def create_session( - email: str, password: str, base_url=BASE_URL + email: str, password: str, base_url: str = BASE_URL, timeout: int = TIMEOUT ) -> Optional[requests.Response]: """Create a session. @@ -19,8 +19,10 @@ def create_session( The email. password : str The password. - base_url : str, optional + base_url : str The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT Returns ------- @@ -33,7 +35,7 @@ def create_session( "password": password, "remember": True, } - return requests.post(url=base_url + "login", json=data, timeout=TIMEOUT) + return requests.post(url=base_url + "login", json=data, timeout=timeout) except requests.exceptions.ConnectionError: console.print("[red]\nConnection error.[/red]") return None @@ -45,13 +47,17 @@ def create_session( return None -def delete_session(base_url=BASE_URL) -> Optional[requests.Response]: +def delete_session( + base_url: str = BASE_URL, timeout: int = TIMEOUT +) -> Optional[requests.Response]: """Delete the session. Parameters ---------- - base_url : str, optional + base_url : str The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT Returns ------- @@ -63,7 +69,7 @@ def delete_session(base_url=BASE_URL) -> Optional[requests.Response]: url=base_url + "logout", headers={"Authorization": User.get_auth_header()}, json={"token": User.get_token()}, - timeout=TIMEOUT, + timeout=timeout, ) if response.status_code != 200: console.print("[red]Failed to delete server session.[/red]") @@ -126,15 +132,19 @@ def get_session(email: str, password: str) -> dict: return process_session_response(response) -def fetch_user_configs(session: dict, base_url=BASE_URL) -> Optional[requests.Response]: +def fetch_user_configs( + session: dict, base_url: str = BASE_URL, timeout: int = TIMEOUT +) -> Optional[requests.Response]: """Fetch user configurations. Parameters ---------- session : dict The session info. - base_url : str, optional + base_url : str The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT Returns ------- @@ -149,7 +159,7 @@ def fetch_user_configs(session: dict, base_url=BASE_URL) -> Optional[requests.Re response = requests.get( url=base_url + "terminal/user", headers={"Authorization": f"{token_type.title()} {token}"}, - timeout=TIMEOUT, + timeout=timeout, ) if response.status_code != 200: console.print("[red]\nFailed to fetch configurations.[/red]") @@ -166,7 +176,7 @@ def fetch_user_configs(session: dict, base_url=BASE_URL) -> Optional[requests.Re def patch_user_configs( - key: str, value: str, type_: str, base_url=BASE_URL + key: str, value: str, type_: str, base_url: str = BASE_URL, timeout: int = TIMEOUT ) -> Optional[requests.Response]: """Patch user configurations to the server. @@ -178,8 +188,10 @@ def patch_user_configs( The value to patch. type_ : str The type of the patch, either "keys" or "settings". - base_url : str, optional + base_url : str The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT Returns ------- @@ -198,7 +210,7 @@ def patch_user_configs( url=base_url + "terminal/user-json", headers={"Authorization": User.get_auth_header()}, json=data, - timeout=TIMEOUT, + timeout=timeout, ) if response.status_code == 200: console.print("[green]Saved remotely.[/green]") @@ -216,13 +228,19 @@ def patch_user_configs( return None -def clear_user_configs(base_url=BASE_URL) -> Optional[requests.Response]: +def clear_user_configs( + auth_header: str, base_url: str = BASE_URL, timeout: int = TIMEOUT +) -> Optional[requests.Response]: """Clear user configurations to the server. Parameters ---------- - base_url : str, optional + auth_header : str + The authorization header. + base_url : str The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT Returns ------- @@ -234,9 +252,9 @@ def clear_user_configs(base_url=BASE_URL) -> Optional[requests.Response]: try: response = requests.put( url=base_url + "terminal/user", - headers={"Authorization": User.get_auth_header()}, + headers={"Authorization": auth_header}, json=data, - timeout=TIMEOUT, + timeout=timeout, ) if response.status_code == 200: console.print("[green]Cleared configurations.[/green]") @@ -258,6 +276,7 @@ def upload_routine( name: str = "", routine: str = "", base_url=BASE_URL, + timeout: int = TIMEOUT, ) -> Optional[requests.Response]: """Send a routine to the server.""" @@ -268,7 +287,7 @@ def upload_routine( headers={"Authorization": User.get_auth_header()}, url=base_url + "terminal/script", json=data, - timeout=TIMEOUT, + timeout=timeout, ) if response.status_code == 200: console.print("[green]Successfully uploaded your routine.[/green]") From 61abf795c36b2df5cf84f3c5afab55cc893b7df4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:32:05 +0000 Subject: [PATCH 147/230] update clear --- openbb_terminal/account/account_controller.py | 2 +- openbb_terminal/session/hub_model.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 7b3a71d385b..337ff7a6513 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -160,7 +160,7 @@ class AccountController(BaseController): ) if i.lower() in ["y", "yes"]: console.print("") - Hub.clear_user_configs(auth_header=User.get_auth_header()) + Hub.clear_user_configs() else: console.print("\nAborted.", style="info") diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 3eae067f918..77d0ed30519 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -176,7 +176,12 @@ def fetch_user_configs( def patch_user_configs( - key: str, value: str, type_: str, base_url: str = BASE_URL, timeout: int = TIMEOUT + key: str, + value: str, + type_: str, + auth_header: str, + base_url: str = BASE_URL, + timeout: int = TIMEOUT, ) -> Optional[requests.Response]: """Patch user configurations to the server. @@ -188,6 +193,8 @@ def patch_user_configs( The value to patch. type_ : str The type of the patch, either "keys" or "settings". + auth_header : str + The authorization header. base_url : str The base url, by default BASE_URL timeout : int @@ -208,7 +215,7 @@ def patch_user_configs( try: response = requests.patch( url=base_url + "terminal/user-json", - headers={"Authorization": User.get_auth_header()}, + headers={"Authorization": auth_header}, json=data, timeout=timeout, ) @@ -229,16 +236,16 @@ def patch_user_configs( def clear_user_configs( - auth_header: str, base_url: str = BASE_URL, timeout: int = TIMEOUT + base_url: str = BASE_URL, auth_header: Optional[str] = None, timeout: int = TIMEOUT ) -> Optional[requests.Response]: """Clear user configurations to the server. Parameters ---------- - auth_header : str - The authorization header. base_url : str The base url, by default BASE_URL + auth_header : Optional[str] + The authorization header, by default None timeout : int The timeout, by default TIMEOUT @@ -247,6 +254,8 @@ def clear_user_configs( Optional[requests.Response] The response from the put request. """ + auth_header = User.get_auth_header() if auth_header is None else auth_header + data: Dict[str, dict] = {"features_keys": {}, "features_settings": {}} try: From aaf0ea25ed9494158a48ea34d556571c2032e743 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:41:00 +0000 Subject: [PATCH 148/230] clear and patch --- openbb_terminal/account/account_controller.py | 2 +- openbb_terminal/featflags_controller.py | 1 + openbb_terminal/keys_model.py | 7 ++++++- openbb_terminal/session/hub_model.py | 10 ++++------ openbb_terminal/settings_controller.py | 1 + 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 337ff7a6513..7b3a71d385b 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -160,7 +160,7 @@ class AccountController(BaseController): ) if i.lower() in ["y", "yes"]: console.print("") - Hub.clear_user_configs() + Hub.clear_user_configs(auth_header=User.get_auth_header()) else: console.print("\nAborted.", style="info") diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index e85d2c5e193..3f0f4b7711b 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -110,6 +110,7 @@ class FeatureFlagsController(BaseController): key=name, value=str(value), type_="settings", + auth_header=User.get_auth_header(), ) def call_overwrite(self, _): diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index e630e8e1bae..1f12884cfcf 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -247,7 +247,12 @@ def set_key(env_var_name: str, env_var_value: str, persist: bool = False) -> Non and env_var_name not in cfg.SENSITIVE_KEYS and env_var_name.startswith("API_") ): - patch_user_configs(key=env_var_name, value=env_var_value, type_="keys") + patch_user_configs( + key=env_var_name, + value=env_var_value, + type_="keys", + auth_header=User.get_auth_header(), + ) def get_keys(show: bool = False) -> pd.DataFrame: diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 77d0ed30519..a73b67b28cd 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -194,7 +194,7 @@ def patch_user_configs( type_ : str The type of the patch, either "keys" or "settings". auth_header : str - The authorization header. + The authorization header, e.g. "Bearer ". base_url : str The base url, by default BASE_URL timeout : int @@ -236,16 +236,16 @@ def patch_user_configs( def clear_user_configs( - base_url: str = BASE_URL, auth_header: Optional[str] = None, timeout: int = TIMEOUT + auth_header: str, base_url: str = BASE_URL, timeout: int = TIMEOUT ) -> Optional[requests.Response]: """Clear user configurations to the server. Parameters ---------- + auth_header : str + The authorization header, e.g. "Bearer ". base_url : str The base url, by default BASE_URL - auth_header : Optional[str] - The authorization header, by default None timeout : int The timeout, by default TIMEOUT @@ -254,8 +254,6 @@ def clear_user_configs( Optional[requests.Response] The response from the put request. """ - auth_header = User.get_auth_header() if auth_header is None else auth_header - data: Dict[str, dict] = {"features_keys": {}, "features_settings": {}} try: diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 9068c6e1309..bd1719fdb4e 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -165,6 +165,7 @@ class SettingsController(BaseController): key=name, value=str(value), type_="settings", + auth_header=User.get_auth_header(), ) @log_start_end(log=logger) From 01f7aa0d18353d25956b46ba89be393eb3ea1747 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:47:44 +0000 Subject: [PATCH 149/230] delete session --- openbb_terminal/session/hub_model.py | 11 ++++++++--- openbb_terminal/session/session_model.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index a73b67b28cd..4561e4ba9e7 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -48,12 +48,17 @@ def create_session( def delete_session( - base_url: str = BASE_URL, timeout: int = TIMEOUT + auth_header: str, token: str, base_url: str = BASE_URL, timeout: int = TIMEOUT ) -> Optional[requests.Response]: """Delete the session. Parameters ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + token : str + The token to delete. + In the terminal we want to delete the current session, so we use the user own token. base_url : str The base url, by default BASE_URL timeout : int @@ -67,8 +72,8 @@ def delete_session( try: response = requests.post( url=base_url + "logout", - headers={"Authorization": User.get_auth_header()}, - json={"token": User.get_token()}, + headers={"Authorization": auth_header}, + json={"token": token}, timeout=timeout, ) if response.status_code != 200: diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index c1b911e81d2..e03d6e7fc5a 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -72,7 +72,7 @@ def logout(cls: bool = False): success = True if not User.is_guest(): - r = Hub.delete_session() + r = Hub.delete_session(auth_header=User.get_auth_header(), token=User.get_token()) if r and r.status_code != 200: success = False User.clear() From 4240d4e077e72c2f4842ee0eabedc22fa862be08 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 10:50:09 +0000 Subject: [PATCH 150/230] routine upload --- openbb_terminal/session/hub_model.py | 50 ++++++++++------------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 4561e4ba9e7..23cd7a4f7fb 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -1,6 +1,5 @@ from typing import Dict, Optional import requests -from openbb_terminal.session.user import User from openbb_terminal.rich_config import console @@ -285,18 +284,33 @@ def clear_user_configs( def upload_routine( + auth_header: str, name: str = "", routine: str = "", base_url=BASE_URL, timeout: int = TIMEOUT, ) -> Optional[requests.Response]: - """Send a routine to the server.""" + """Send a routine to the server. + + Parameters + ---------- + auth_header : str + + name : str + The name of the routine. + routine : str + The routine. + base_url : str + The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT + """ data = {"name": name, "script": routine} try: response = requests.post( - headers={"Authorization": User.get_auth_header()}, + headers={"Authorization": auth_header}, url=base_url + "terminal/script", json=data, timeout=timeout, @@ -317,35 +331,7 @@ def upload_routine( return None -def download_routine( - name: str = "", -) -> Optional[requests.Response]: +def download_routine() -> Optional[requests.Response]: """Download a routine from the server.""" console.print("[red]Not implemented yet.[/red]") return None - - # TODO: Implement when endpoint is ready. - - # data = {"name": name} - - # try: - # response = requests.get( - # headers={"Authorization": User.get_auth_header()}, - # url=BASE_URL + "terminal/script", - # json=data, - # timeout=TIMEOUT, - # ) - # if response.status_code == 200: - # console.print("Successfully downloaded your routine.") - # else: - # console.print("[red]Error downloading your routine.[/red]") - # return response - # except requests.exceptions.ConnectionError: - # console.print("[red]Connection error.[/red]") - # return None - # except requests.exceptions.Timeout: - # console.print("[red]\nConnection timeout.[/red]") - # return None - # except Exception: - # console.print("[red]Failed to download your routine.[/red]") - # return None From 2bb47c6e0317f30ed1dedc135a848a720e6370d3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 11:02:33 +0000 Subject: [PATCH 151/230] move .his file to .openbb folder --- openbb_terminal/core/config/paths.py | 2 +- openbb_terminal/session/local_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/core/config/paths.py b/openbb_terminal/core/config/paths.py index 0eee6312585..268b5c0dded 100644 --- a/openbb_terminal/core/config/paths.py +++ b/openbb_terminal/core/config/paths.py @@ -35,7 +35,7 @@ REPOSITORY_ENV_FILE = REPOSITORY_DIRECTORY / ".env" PACKAGE_ENV_FILE = PACKAGE_DIRECTORY / ".env" SETTINGS_DIRECTORY = HOME_DIRECTORY / ".openbb_terminal" -HIST_FILE_PATH = os.path.join(os.path.expanduser("~"), ".openbb_terminal.his") +HIST_FILE_PATH = SETTINGS_DIRECTORY / ".openbb_terminal.his" USER_ENV_FILE = SETTINGS_DIRECTORY / ".env" USER_DATA_DIRECTORY = get_user_data_directory() USER_EXPORTS_DIRECTORY = USER_DATA_DIRECTORY / "exports" diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 2e60e9aa556..0217871f08e 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -80,7 +80,7 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: return False -def remove_cli_history_file(file_path: Path = Path(HIST_FILE_PATH)) -> bool: +def remove_cli_history_file(file_path: Path = HIST_FILE_PATH) -> bool: """Remove the cli history file. Parameters From cef3057e2beb3f8b815b52da91dfb472b58bd96b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 11:11:55 +0000 Subject: [PATCH 152/230] change apply configs --- openbb_terminal/session/local_model.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 0217871f08e..ea8aa7ff1e7 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -112,15 +112,10 @@ def apply_configs(configs: dict): configs : dict The configurations. """ - # console.print(configs, style="red") - if configs: settings = configs.get("features_settings", {}) - - # TODO: Find a cleaner way to check if user has sync enabled. - obbff.SYNC_ENABLED = is_sync_enabled(settings) - - if obbff.SYNC_ENABLED: + sync = update_sync_flag(settings) + if sync: if settings: for k, v in settings.items(): if hasattr(obbff, k): @@ -137,8 +132,8 @@ def apply_configs(configs: dict): setattr(cfg, k, v) -def is_sync_enabled(settings: dict) -> bool: - """Check if sync is enabled. +def update_sync_flag(settings: dict) -> bool: + """Update the sync flag. Parameters ---------- @@ -148,11 +143,13 @@ def is_sync_enabled(settings: dict) -> bool: Returns ------- bool - The status of sync. + The sync flag. """ if settings: if settings.get("SYNC_ENABLED", "").lower() == "false": + obbff.SYNC_ENABLED = False return False + obbff.SYNC_ENABLED = True return True @@ -177,16 +174,20 @@ def cast_set_attr(obj, name, value): setattr(obj, name, value) -def get_routine(name: str) -> Optional[str]: +def get_routine( + name: str, routines_folder: Path = USER_ROUTINES_DIRECTORY +) -> Optional[str]: """Get the routine. Returns ------- str The routine. + routines_folder : Path + The routines folder. """ try: - with open(USER_ROUTINES_DIRECTORY / name) as f: + with open(routines_folder / name) as f: routine = "".join(f.readlines()) return routine except Exception: From d265a126a5d71ce4aded740f839f132d6fbc1eea Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 11:26:23 +0000 Subject: [PATCH 153/230] refactor logout --- .../miscellaneous/library/trail_map.csv | 2 +- openbb_terminal/parent_classes.py | 7 +++++- openbb_terminal/session/hub_model.py | 1 - openbb_terminal/session/sdk_session.py | 9 ++++++++ openbb_terminal/session/session_model.py | 22 ++++++++++++++++--- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/openbb_terminal/miscellaneous/library/trail_map.csv b/openbb_terminal/miscellaneous/library/trail_map.csv index b1f2cafb412..0e156a57b1a 100644 --- a/openbb_terminal/miscellaneous/library/trail_map.csv +++ b/openbb_terminal/miscellaneous/library/trail_map.csv @@ -550,4 +550,4 @@ futures.curve,openbb_terminal.futures.yfinance_model.get_curve_futures,openbb_te economy.get_groups,openbb_terminal.economy.finviz_model.get_groups, stocks.filings,openbb_terminal.stocks.fundamental_analysis.fmp_model.get_filings,openbb_terminal.stocks.fundamental_analysis.fmp_view.display_filings login,openbb_terminal.session.sdk_session.login, -logout,openbb_terminal.session.session_model.logout, \ No newline at end of file +logout,openbb_terminal.session.sdk_session.logout, diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 2e50eb6a761..bc7eca3bbf9 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -673,7 +673,12 @@ class BaseController(metaclass=ABCMeta): ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - logout(cls=True) + logout( + auth_header=User.get_auth_header(), + token=User.get_token(), + guest=User.is_guest(), + cls=True, + ) @log_start_end(log=logger) def call_whoami(self, other_args: List[str]) -> None: diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 23cd7a4f7fb..438224512e9 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -57,7 +57,6 @@ def delete_session( The authorization header, e.g. "Bearer ". token : str The token to delete. - In the terminal we want to delete the current session, so we use the user own token. base_url : str The base url, by default BASE_URL timeout : int diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py index 769f470a910..e138cd44afd 100644 --- a/openbb_terminal/session/sdk_session.py +++ b/openbb_terminal/session/sdk_session.py @@ -1,6 +1,7 @@ from openbb_terminal.session import local_model as Local from openbb_terminal.session import session_model from openbb_terminal.rich_config import console +from openbb_terminal.session.user import User def get_session(email: str, password: str, save: bool): @@ -23,3 +24,11 @@ def login(email: str = "", password: str = "", keep_session: bool = False): raise Exception("Login failed") console.print("[green]Login successful[/green]") + + +def logout(): + session_model.logout( + auth_header=User.get_auth_header(), + token=User.get_token(), + guest=User.is_guest(), + ) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index e03d6e7fc5a..5f3b62bb9b2 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -4,6 +4,7 @@ import os import sys import json from enum import Enum +from typing import Optional import matplotlib.pyplot as plt import openbb_terminal.session.local_model as Local import openbb_terminal.session.hub_model as Hub @@ -59,11 +60,23 @@ def login(session: dict) -> LoginStatus: return LoginStatus.NO_RESPONSE -def logout(cls: bool = False): +def logout( + auth_header: Optional[str] = None, + token: Optional[str] = None, + guest: bool = True, + cls: bool = False, +): """Logout and clear session. Parameters ---------- + auth_header : str, optional + The authorization header, e.g. "Bearer ". + token : str, optional + The token to delete. + In the terminal we want to delete the current session, so we use the user own token. + guest : bool + True if the user is guest, False otherwise. cls : bool Clear the screen. """ @@ -71,8 +84,11 @@ def logout(cls: bool = False): system_clear() success = True - if not User.is_guest(): - r = Hub.delete_session(auth_header=User.get_auth_header(), token=User.get_token()) + if not guest: + if not auth_header or not token: + return + + r = Hub.delete_session(auth_header=auth_header, token=token) if r and r.status_code != 200: success = False User.clear() From 4dd80e2b54c1d8442e5cdcd339a67d3154768d97 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 11:47:59 +0000 Subject: [PATCH 154/230] remove mixed portfolio changes --- .../portfolio/attribution_model.py | 7 +-- .../portfolio/portfolio_controller.py | 54 +++++++++---------- openbb_terminal/portfolio/portfolio_engine.py | 46 ++++------------ openbb_terminal/portfolio/portfolio_model.py | 18 ++----- 4 files changed, 38 insertions(+), 87 deletions(-) diff --git a/openbb_terminal/portfolio/attribution_model.py b/openbb_terminal/portfolio/attribution_model.py index 334b2f18652..dfa815b99de 100644 --- a/openbb_terminal/portfolio/attribution_model.py +++ b/openbb_terminal/portfolio/attribution_model.py @@ -9,7 +9,6 @@ from typing import Dict import yfinance as yf import pandas as pd from openbb_terminal.decorators import log_start_end -from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) @@ -69,11 +68,7 @@ def get_spy_sector_contributions( # Load in info sp500_tickers_data = get_daily_sector_prices(start_date, end_date) - try: - weight_data = yf.Ticker(sectors_ticker).info["sectorWeightings"] - except Exception as _: # noqa - console.print("[red]Could not get info for S&P 500.") - return pd.DataFrame() + weight_data = yf.Ticker(sectors_ticker).info["sectorWeightings"] # reformat Data weights: Dict[str, dict] = {"SPY": {}} diff --git a/openbb_terminal/portfolio/portfolio_controller.py b/openbb_terminal/portfolio/portfolio_controller.py index 77829244746..938908fd481 100644 --- a/openbb_terminal/portfolio/portfolio_controller.py +++ b/openbb_terminal/portfolio/portfolio_controller.py @@ -82,7 +82,6 @@ class PortfolioController(BaseController): CHOICES_MENUS = [ "bro", "po", - "pa", ] VALID_DISTRIBUTIONS = ["laplace", "student_t", "logistic", "normal"] AGGREGATION_METRICS = ["assets", "sectors", "countries", "regions"] @@ -121,15 +120,6 @@ class PortfolioController(BaseController): for file_type in self.file_types for filepath in self.DEFAULT_HOLDINGS_PATH.rglob(f"*.{file_type}") } - self.DATA_HOLDINGS_FILES.update( - { - filepath.name: filepath - for file_type in self.file_types - for filepath in ( - MISCELLANEOUS_DIRECTORY / "portfolio_examples" / "holdings" - ).rglob(f"*.{file_type}") - } - ) self.portfolio_df = pd.DataFrame( columns=[ @@ -160,10 +150,6 @@ class PortfolioController(BaseController): self.update_choices() choices: dict = self.choices_default self.choices = choices - self.choices["bench"] = { - "--benchmark": {c: None for c in statics.BENCHMARK_CHOICES}, - "-b": "--benchmark", - } self.completer = NestedCompleter.from_nested_dict(choices) def update_choices(self): @@ -228,7 +214,7 @@ class PortfolioController(BaseController): > bro brokers holdings, \t\t supports: robinhood, ally, degiro, coinbase > po portfolio optimization, \t optimize your portfolio weights efficiently[/menu] [cmds] - load load data into the portfolio + load load transactions into the portfolio (use load --example for an example) show show existing transactions bench define the benchmark [/cmds] @@ -318,7 +304,6 @@ class PortfolioController(BaseController): "--file", type=str, dest="file", - required="-h" not in other_args, help="The file to be loaded", choices={c: {} for c in self.DATA_HOLDINGS_FILES}, metavar="FILE", @@ -338,12 +323,28 @@ class PortfolioController(BaseController): dest="risk_free_rate", help="Set the risk free rate.", ) + parser.add_argument( + "-e", + "--example", + help="Run an example holdings file to understand how the portfolio menu can be used.", + dest="example", + action="store_true", + default=False, + ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-f") ns_parser = self.parse_known_args_and_warn(parser, other_args) - if ns_parser and ns_parser.file: - if ns_parser.file in self.DATA_HOLDINGS_FILES: + if ns_parser and ns_parser.file or ns_parser and ns_parser.example: + if ns_parser.example: + file_location = ( + MISCELLANEOUS_DIRECTORY / "portfolio" / "holdings_example.xlsx" + ) + console.print( + "[green]Loading an example, please type `about` " + "to learn how to create your own Portfolio Excel sheet.[/green]\n" + ) + elif ns_parser.file in self.DATA_HOLDINGS_FILES: file_location = self.DATA_HOLDINGS_FILES[ns_parser.file] else: file_location = ns_parser.file # type: ignore @@ -356,6 +357,8 @@ class PortfolioController(BaseController): if ns_parser.name: self.portfolio_name = ns_parser.name + elif ns_parser.example: + self.portfolio_name = "OpenBB Example Portfolio" else: self.portfolio_name = ns_parser.file console.print( @@ -415,6 +418,7 @@ class PortfolioController(BaseController): dest="benchmark", required="-h" not in other_args, help="Set the benchmark for the portfolio. By default, this is SPDR S&P 500 ETF Trust (SPY).", + choices={c: {} for c in statics.BENCHMARK_CHOICES}, metavar="BENCHMARK", ) parser.add_argument( @@ -435,13 +439,9 @@ class PortfolioController(BaseController): "[red]Please first load transactions file using 'load'[/red]" ) else: - if self.portfolio.set_benchmark( - ns_parser.benchmark, ns_parser.full_shares - ): - self.benchmark_name = statics.BENCHMARK_CHOICES.get( - ns_parser.benchmark, ns_parser.benchmark - ) - self.original_benchmark_ticker = ns_parser.benchmark + self.benchmark_name = statics.BENCHMARK_CHOICES.get(ns_parser.benchmark) + self.original_benchmark_ticker = ns_parser.benchmark + self.portfolio.set_benchmark(ns_parser.benchmark, ns_parser.full_shares) @log_start_end(log=logger) def call_alloc(self, other_args: List[str]): @@ -580,13 +580,9 @@ class PortfolioController(BaseController): bench_result = attribution_model.get_spy_sector_contributions( start_date, end_date ) - if bench_result.empty: - return portfolio_result = attribution_model.get_portfolio_sector_contributions( start_date, self.portfolio.portfolio_trades ) - if portfolio_result.empty: - return # relative results - the proportions of return attribution if ns_parser.type == "relative": diff --git a/openbb_terminal/portfolio/portfolio_engine.py b/openbb_terminal/portfolio/portfolio_engine.py index c845f6584ae..96b7b628d0a 100644 --- a/openbb_terminal/portfolio/portfolio_engine.py +++ b/openbb_terminal/portfolio/portfolio_engine.py @@ -21,8 +21,7 @@ from openbb_terminal.portfolio.allocation_model import get_allocation from openbb_terminal.terminal_helper import suppress_stdout # pylint: disable=E1136,W0201,R0902,C0302 -# pylint: disable=unsupported-assignment-operation,redefined-outer-name -# pylint: too-many-public-methods, consider-using-f-string,disable=raise-missing-from +# pylint: disable=unsupported-assignment-operation,redefined-outer-name,too-many-public-methods, consider-using-f-string logger = logging.getLogger(__name__) @@ -496,7 +495,7 @@ class PortfolioEngine: ] = info_list @log_start_end(log=logger) - def set_benchmark(self, symbol: str = "SPY", full_shares: bool = False) -> bool: + def set_benchmark(self, symbol: str = "SPY", full_shares: bool = False): """Load benchmark into portfolio. Parameters @@ -506,15 +505,12 @@ class PortfolioEngine: full_shares: bool Whether to mimic the portfolio trades exactly (partial shares) or round down the quantity to the nearest number - - Returns - ------- - bool - True if successful, False otherwise """ p_bar = tqdm(range(4), desc=" Loading benchmark") + self.benchmark_ticker = symbol + self.benchmark_historical_prices = yf.download( symbol, start=self.inception_date - datetime.timedelta(days=1), @@ -523,15 +519,6 @@ class PortfolioEngine: ignore_tz=True, )["Adj Close"] - if self.benchmark_historical_prices.empty: - console.print( - f"\n[red]Could not download benchmark data for {symbol}." - " Choose another symbol.\n[/red]" - ) - return False - - self.benchmark_ticker = symbol - p_bar.n += 1 p_bar.refresh() @@ -551,6 +538,11 @@ class PortfolioEngine: p_bar.refresh() self.benchmark_returns = self.benchmark_historical_prices.pct_change().dropna() + self.benchmark_info = yf.Ticker(symbol).info + + p_bar.n += 1 + p_bar.refresh() + ( self.portfolio_returns, self.benchmark_returns, @@ -559,20 +551,6 @@ class PortfolioEngine: p_bar.n += 1 p_bar.refresh() - try: - self.benchmark_info = yf.Ticker(symbol).info - except Exception as _: # noqa - console.print( - f"[red]\n\nCould not get info for {symbol}." - " This affects 'alloc' command.[/red]\n" - ) - return False - - p_bar.n += 1 - p_bar.refresh() - - return True - @log_start_end(log=logger) def __mimic_trades_for_benchmark(self, full_shares: bool = False): """Mimic trades from the transactions based on chosen benchmark assuming partial shares @@ -928,12 +906,6 @@ class PortfolioEngine: Flag to force recalculate allocation if already exists """ - if not self.benchmark_info: - return - - if self.portfolio_trades.empty: - return - if category == "Asset": if ( self.benchmark_assets_allocation.empty diff --git a/openbb_terminal/portfolio/portfolio_model.py b/openbb_terminal/portfolio/portfolio_model.py index c9e8155f08e..6cd87b810b9 100644 --- a/openbb_terminal/portfolio/portfolio_model.py +++ b/openbb_terminal/portfolio/portfolio_model.py @@ -58,8 +58,8 @@ def generate_portfolio( transactions = PortfolioEngine.read_transactions(transactions_file_path) portfolio_engine = PortfolioEngine(transactions) portfolio_engine.generate_portfolio_data() - portfolio_engine.set_risk_free_rate(risk_free_rate) portfolio_engine.set_benchmark(symbol=benchmark_symbol, full_shares=full_shares) + portfolio_engine.set_risk_free_rate(risk_free_rate) return portfolio_engine @@ -92,7 +92,7 @@ def get_transactions(portfolio_engine: PortfolioEngine) -> pd.DataFrame: @log_start_end(log=logger) def set_benchmark( portfolio_engine: PortfolioEngine, symbol: str, full_shares: bool = False -) -> bool: +): """Load benchmark into portfolio Parameters @@ -106,11 +106,6 @@ def set_benchmark( Whether to mimic the portfolio trades exactly (partial shares) or round down the quantity to the nearest number - Returns - ------- - bool - True if successful, False otherwise - Examples -------- >>> from openbb_terminal.sdk import openbb @@ -118,7 +113,7 @@ def set_benchmark( >>> output = openbb.portfolio.bench(p, symbol="SPY") """ - return portfolio_engine.set_benchmark(symbol=symbol, full_shares=full_shares) + portfolio_engine.set_benchmark(symbol=symbol, full_shares=full_shares) @log_start_end(log=logger) @@ -435,13 +430,6 @@ def join_allocation( pd.DataFrame DataFrame with portfolio and benchmark allocations """ - - if portfolio.empty: - portfolio = pd.DataFrame(columns=[column, "Portfolio"]) - - if benchmark.empty: - benchmark = pd.DataFrame(columns=[column, "Benchmark"]) - combined = pd.merge(portfolio, benchmark, on=column, how="left") combined["Difference"] = combined["Portfolio"] - combined["Benchmark"] combined = combined.replace(np.nan, "-") From 104f929b45f80f3b060a8fd4829fff5817b92cd8 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 12:22:38 +0000 Subject: [PATCH 155/230] fix bug in lock file --- poetry.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 1829bafc7b9..83bd53c5f00 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1127,7 +1127,6 @@ python-versions = ">=3.7" [[package]] name = "fsspec" version = "2023.1.0" -version = "2023.1.0" description = "File-system specification" category = "main" optional = true From 20bc8ae2bf3f1de72fdcecf5f51347284e35cc75 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 13:01:00 +0000 Subject: [PATCH 156/230] fix bug pwidth, pheight are floats --- openbb_terminal/account/account_model.py | 2 ++ openbb_terminal/session/local_model.py | 2 ++ openbb_terminal/settings_controller.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index e4ef36a46db..9e78b45d639 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -119,6 +119,8 @@ def get_var_diff(obj, name, value): cast_value = strtobool(value) elif isinstance(getattr(obj, name), int): cast_value = int(value) + elif isinstance(getattr(obj, name), float): + cast_value = float(value) else: cast_value = value diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index ea8aa7ff1e7..405e747a807 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -170,6 +170,8 @@ def cast_set_attr(obj, name, value): setattr(obj, name, strtobool(value)) elif isinstance(getattr(obj, name), int): setattr(obj, name, int(value)) + elif isinstance(getattr(obj, name), float): + setattr(obj, name, float(value)) else: setattr(obj, name, value) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index bd1719fdb4e..8b5ad8c2107 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -315,7 +315,7 @@ class SettingsController(BaseController): parser.add_argument( "-v", "--value", - type=int, + type=float, dest="value", help="value", ) From 52711a1d2ada40cede36c1f64c1110e1b29c0bce Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 13:29:19 +0000 Subject: [PATCH 157/230] fix sources command & add choices --- openbb_terminal/settings_controller.py | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 8b5ad8c2107..580714df4bb 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -17,7 +17,11 @@ from dotenv import set_key from openbb_terminal import config_plot as cfg_plot from openbb_terminal import feature_flags as obbff from openbb_terminal import featflags_controller as obbff_ctrl -from openbb_terminal.core.config.paths import USER_ENV_FILE, USER_DATA_DIRECTORY +from openbb_terminal.core.config.paths import ( + USER_ENV_FILE, + USER_DATA_DIRECTORY, + USER_DATA_SOURCES_DEFAULT_FILE, +) from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import ( @@ -59,6 +63,7 @@ class SettingsController(BaseController): "colors", ] PATH = "/settings/" + CHOICES_GENERATION = True all_timezones = [tz.replace("/", "_") for tz in pytz.all_timezones] @@ -74,9 +79,10 @@ class SettingsController(BaseController): self.env_file = env_file if session and obbff.USE_PROMPT_TOOLKIT: - choices: dict = {c: {} for c in self.controller_choices} + choices: dict = self.choices_default choices["tz"] = {c: None for c in self.all_timezones} choices["lang"] = {c: None for c in self.languages_available} + self.choices = choices self.completer = NestedCompleter.from_nested_dict(choices) def print_help(self): @@ -200,27 +206,25 @@ class SettingsController(BaseController): description="Preferred data source file.", ) parser.add_argument( - "-v", - "--value", + "-f", + "--file", type=str, - default=str(USER_DATA_DIRECTORY / "data_sources_default.json"), - dest="value", - help="value", + default=str(USER_DATA_SOURCES_DEFAULT_FILE), + dest="file", + help="file", ) if other_args and "-" not in other_args[0][0]: - other_args.insert(0, "-v") + other_args.insert(0, "-f") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: try: - - the_path = ns_parser.value - console.print("Loading sources from " + str(the_path)) - with open(the_path): + console.print("Loading sources from: " + str(ns_parser.file)) + with open(ns_parser.file): # Try to open the file to get an exception if the file doesn't exist pass obbff_ctrl.FeatureFlagsController.set_feature_flag( - "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.value + "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.file ) except Exception as e: From e62bfb118f10cca5ee0f6cbdeee2f36ffe061c23 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 19:09:47 +0000 Subject: [PATCH 158/230] refactor timezone setting --- openbb_terminal/feature_flags.py | 9 ++++-- openbb_terminal/helper_funcs.py | 32 +--------------------- openbb_terminal/settings_controller.py | 38 ++++++++++++++++++++------ 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/openbb_terminal/feature_flags.py b/openbb_terminal/feature_flags.py index 22337629c84..4b78b32a78f 100644 --- a/openbb_terminal/feature_flags.py +++ b/openbb_terminal/feature_flags.py @@ -47,9 +47,6 @@ USE_CLEAR_AFTER_CMD = load_env_vars( # Use coloring features USE_COLOR = load_env_vars("OPENBB_USE_COLOR", strtobool, True, "featflags") -# Select console flair (choose from config_terminal.py list) -USE_FLAIR = str(os.getenv("OPENBB_USE_FLAIR", ":openbb")) - # Add date and time to command line USE_DATETIME = load_env_vars("OPENBB_USE_DATETIME", strtobool, True, "featflags") @@ -132,6 +129,12 @@ PREFERRED_DATA_SOURCE_FILE = str( ) ) +# Timezone +TIMEZONE = str(os.getenv("OPENBB_TIMEZONE", "America/New_York")) + +# Select console flair (choose from config_terminal.py list) +USE_FLAIR = str(os.getenv("OPENBB_USE_FLAIR", ":openbb")) + # Guess file GUESS_EASTER_EGG_FILE = str( os.getenv( diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index aac76938bb6..d21ae26e2dd 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -44,9 +44,7 @@ from openbb_terminal import config_terminal as cfg from openbb_terminal import config_plot as cfgPlot from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, - USER_ENV_FILE, USER_EXPORTS_DIRECTORY, - load_dotenv_with_priority, ) from openbb_terminal.core.config import paths @@ -1104,7 +1102,6 @@ def get_flair() -> str: else str(obbff.USE_FLAIR) ) - set_default_timezone() if obbff.USE_DATETIME and get_user_timezone_or_invalid() != "INVALID": dtime = datetime.now(pytz.timezone(get_user_timezone())).strftime( "%Y %b %d, %H:%M" @@ -1119,14 +1116,6 @@ def get_flair() -> str: return flair -def set_default_timezone() -> None: - """Set a default (America/New_York) timezone if one doesn't exist.""" - load_dotenv_with_priority() - user_tz = os.getenv("OPENBB_TIMEZONE") - if not user_tz: - dotenv.set_key(USER_ENV_FILE, "OPENBB_TIMEZONE", "America/New_York") - - def is_timezone_valid(user_tz: str) -> bool: """Check whether user timezone is valid. @@ -1151,11 +1140,7 @@ def get_user_timezone() -> str: str user timezone based on .env file """ - load_dotenv_with_priority() - user_tz = os.getenv("OPENBB_TIMEZONE") - if user_tz: - return user_tz - return "" + return obbff.TIMEZONE def get_user_timezone_or_invalid() -> str: @@ -1172,21 +1157,6 @@ def get_user_timezone_or_invalid() -> str: return "INVALID" -def replace_user_timezone(user_tz: str) -> None: - """Replace user timezone. - - Parameters - ---------- - user_tz: str - User timezone to set - """ - if is_timezone_valid(user_tz): - dotenv.set_key(USER_ENV_FILE, "OPENBB_TIMEZONE", user_tz) - console.print("Timezone successfully updated", "\n") - else: - console.print("Timezone selected is not valid", "\n") - - def str_to_bool(value) -> bool: """Match a string to a boolean value.""" if isinstance(value, bool): diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 580714df4bb..d8c1459aa83 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -27,7 +27,7 @@ from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import ( get_flair, get_user_timezone_or_invalid, - replace_user_timezone, + parse_and_split_input, set_user_data_folder, ) from openbb_terminal.menu import session @@ -65,8 +65,6 @@ class SettingsController(BaseController): PATH = "/settings/" CHOICES_GENERATION = True - all_timezones = [tz.replace("/", "_") for tz in pytz.all_timezones] - languages_available = [ lang.strip(".yml") for lang in os.listdir(obbff.i18n_dict_location) @@ -80,11 +78,33 @@ class SettingsController(BaseController): if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = self.choices_default - choices["tz"] = {c: None for c in self.all_timezones} + choices["tz"] = {c: None for c in pytz.all_timezones} choices["lang"] = {c: None for c in self.languages_available} self.choices = choices self.completer = NestedCompleter.from_nested_dict(choices) + self.sort_filter = r"((tz\ -t |tz ).*?(" + for tz in pytz.all_timezones: + tz = tz.replace("/", r"\/") + self.sort_filter += f"{tz}|" + self.sort_filter += ")*)" + + def parse_input(self, an_input: str) -> List: + """Parse controller input + + Overrides the parent class function to handle github org/repo path convention. + See `BaseController.parse_input()` for details. + """ + # Filtering out + sort_filter = self.sort_filter + + custom_filters = [sort_filter] + + commands = parse_and_split_input( + an_input=an_input, custom_filters=custom_filters + ) + return commands + def print_help(self): """Print help""" mt = MenuText("settings/") @@ -447,16 +467,18 @@ class SettingsController(BaseController): dest="timezone", help="Choose timezone", required="-h" not in other_args, - choices=self.all_timezones, + metavar="TIMEZONE", + choices=pytz.all_timezones, ) if other_args and "-t" not in other_args[0]: other_args.insert(0, "-t") ns_parser = self.parse_simple_args(parser, other_args) - if ns_parser: - if ns_parser.timezone: - replace_user_timezone(ns_parser.timezone.replace("_", "/", 1)) + if ns_parser and ns_parser.timezone: + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_TIMEZONE", ns_parser.timezone + ) @log_start_end(log=logger) def call_flair(self, other_args: List[str]): From 41d01ac68cb165dcc47e287040a210ec35af5e0e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 20:19:37 +0000 Subject: [PATCH 159/230] userdata --- openbb_terminal/account/account_model.py | 5 +++ openbb_terminal/helper_funcs.py | 15 ------- openbb_terminal/session/local_model.py | 8 ++++ openbb_terminal/settings_controller.py | 52 ++++++++++++++++++------ 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 9e78b45d639..53e14d68865 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,4 +1,5 @@ from typing import Dict, Any +from openbb_terminal.core.config import paths from openbb_terminal.rich_config import console from openbb_terminal import feature_flags as obbff from openbb_terminal import config_terminal as cfg @@ -74,6 +75,10 @@ def get_diff_settings(settings: dict) -> dict: old, new = get_var_diff(cfg_plot, k, v) if new is not None: diff[k] = (old, new) + elif hasattr(paths, k): + old, new = get_var_diff(paths, k, v) + if new is not None: + diff[k] = (old, new) return diff diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index d21ae26e2dd..1cd79887428 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -82,21 +82,6 @@ def set_command_location(cmd_loc: str): command_location = cmd_loc -# pylint: disable=global-statement -def set_user_data_folder(env_file: str = ".env", path_folder: str = ""): - """Set user data folder location. - - Parameters - ---------- - env_file : str - Env file to be updated - path_folder: str - Path folder location - """ - dotenv.set_key(env_file, "OPENBB_USER_DATA_DIRECTORY", path_folder) - paths.USER_DATA_DIRECTORY = Path(path_folder) - - def check_path(path: str) -> str: """Check that path file exists. diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 405e747a807..5f5a2d64c78 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -2,6 +2,7 @@ import os from pathlib import Path import json from typing import Optional +from openbb_terminal.core.config import paths from openbb_terminal.core.config.paths import ( HIST_FILE_PATH, SETTINGS_DIRECTORY, @@ -112,6 +113,11 @@ def apply_configs(configs: dict): configs : dict The configurations. """ + + # TODO: Here I'm assuming that obbff, cfg, cfg_plot and paths don't have variables + # with the same name. If they do, then this assignment will hit the first variable + # that matches the name. + if configs: settings = configs.get("features_settings", {}) sync = update_sync_flag(settings) @@ -124,6 +130,8 @@ def apply_configs(configs: dict): cast_set_attr(cfg, k, v) elif hasattr(cfg_plot, k): cast_set_attr(cfg_plot, k, v) + elif hasattr(paths, k): + cast_set_attr(paths, k, v) keys = configs.get("features_keys", {}) if keys: diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index d8c1459aa83..f485db56842 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -16,10 +16,10 @@ from dotenv import set_key # IMPORTATION INTERNAL from openbb_terminal import config_plot as cfg_plot from openbb_terminal import feature_flags as obbff +from openbb_terminal.core.config import paths from openbb_terminal import featflags_controller as obbff_ctrl from openbb_terminal.core.config.paths import ( USER_ENV_FILE, - USER_DATA_DIRECTORY, USER_DATA_SOURCES_DEFAULT_FILE, ) from openbb_terminal.custom_prompt_toolkit import NestedCompleter @@ -28,8 +28,7 @@ from openbb_terminal.helper_funcs import ( get_flair, get_user_timezone_or_invalid, parse_and_split_input, - set_user_data_folder, -) + ) from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText @@ -124,7 +123,7 @@ class SettingsController(BaseController): mt.add_raw("\n") mt.add_param( "_user_data_folder", - USER_DATA_DIRECTORY, + paths.USER_DATA_DIRECTORY, ) mt.add_raw("\n") mt.add_cmd("tz") @@ -194,6 +193,37 @@ class SettingsController(BaseController): auth_header=User.get_auth_header(), ) + @staticmethod + def set_path_config(name: str, value: Optional[Union[Path, str]]): + """Set path config attribute + + Parameters + ---------- + name : str + Environment variable name + value : str + Environment variable value + """ + + if User.is_guest(): + set_key(str(USER_ENV_FILE), name, str(value)) + + # Remove "OPENBB_" prefix from env_var + if name.startswith("OPENBB_"): + name = name[7:] + + # Set obbff.env_var_name = not env_var_value + setattr(paths, name, value) + + # Send feature flag to server + if not User.is_guest() and User.is_sync_enabled(): + patch_user_configs( + key=name, + value=str(value), + type_="settings", + auth_header=User.get_auth_header(), + ) + @log_start_end(log=logger) def call_colors(self, _): """Process colors command""" @@ -551,9 +581,7 @@ class SettingsController(BaseController): console.print( f"User data to be saved in the default folder: '{default_path}'" ) - set_user_data_folder( - self.env_file, path_folder=str(default_path) - ) + self.set_path_config("OPENBB_USER_DATA_DIRECTORY", default_path) success_userdata = True else: # If the path selected does not start from the user root, give relative location from root @@ -567,8 +595,8 @@ class SettingsController(BaseController): console.print( f"User data to be saved in the selected folder: '{userdata_path}'" ) - set_user_data_folder( - self.env_file, path_folder=userdata_path + self.set_path_config( + "OPENBB_USER_DATA_DIRECTORY", userdata_path ) success_userdata = True else: @@ -586,14 +614,14 @@ class SettingsController(BaseController): console.print( f"[green]Folder '{userdata_path}' successfully created.[/green]" ) - set_user_data_folder( - self.env_file, path_folder=userdata_path + self.set_path_config( + "OPENBB_USER_DATA_DIRECTORY", userdata_path ) else: # Do not update userdata_folder path since we will keep the same as before console.print( "[yellow]User data to keep being saved in " - + f"the selected folder: {str(USER_DATA_DIRECTORY)}[/yellow]" + + f"the selected folder: {str(paths.USER_DATA_DIRECTORY)}[/yellow]" ) success_userdata = True From 6c146c62a907318e35c959522f9877079217635a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 23:13:42 +0000 Subject: [PATCH 160/230] fix flair --- openbb_terminal/session/session_model.py | 1 + openbb_terminal/session/user.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 5f3b62bb9b2..9dd1e9d74e0 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -55,6 +55,7 @@ def login(session: dict) -> LoginStatus: email = configs.get("email", "") User.load_user_info(session, email) Local.apply_configs(configs=configs) + User.update_flair() return LoginStatus.SUCCESS return LoginStatus.FAILED return LoginStatus.NO_RESPONSE diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 5a78b021084..86685b7d074 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -23,8 +23,6 @@ class User: User._TOKEN = session.get("access_token", "") User._UUID = session.get("uuid", "") User._EMAIL = email - if User._EMAIL: - User.update_flair() @classmethod def get_session(cls): @@ -41,7 +39,7 @@ class User: username = User._EMAIL[: User._EMAIL.find("@")] username = "[" + username[:MAX_FLAIR_LEN] + "]" - if obbff.USE_FLAIR == ":openbb" or username in obbff.USE_FLAIR: + if obbff.USE_FLAIR == ":openbb": setattr(obbff, "USE_FLAIR", username + " 🦋") @classmethod From 81aceeb8da479cc558886cf22eda8f4140da891f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 23:29:24 +0000 Subject: [PATCH 161/230] need to cast paths --- openbb_terminal/session/local_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 5f5a2d64c78..ce63d09c8fe 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -180,6 +180,8 @@ def cast_set_attr(obj, name, value): setattr(obj, name, int(value)) elif isinstance(getattr(obj, name), float): setattr(obj, name, float(value)) + elif isinstance(getattr(obj, name), Path): + setattr(obj, name, Path(value)) else: setattr(obj, name, value) From c5caa873c779d50ed44d55faefb4112c56e20c0c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 23:37:10 +0000 Subject: [PATCH 162/230] cast on diff --- openbb_terminal/account/account_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 53e14d68865..33c612ab30b 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Dict, Any from openbb_terminal.core.config import paths from openbb_terminal.rich_config import console @@ -126,6 +127,8 @@ def get_var_diff(obj, name, value): cast_value = int(value) elif isinstance(getattr(obj, name), float): cast_value = float(value) + elif isinstance(getattr(obj, name), Path): + cast_value = Path(value) else: cast_value = value From e9f381de52fd13d7c18060b30a496b8f384c4173 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 1 Feb 2023 23:54:03 +0000 Subject: [PATCH 163/230] fix flair default --- openbb_terminal/session/session_model.py | 4 +++- openbb_terminal/session/user.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 9dd1e9d74e0..6c45b13bc7c 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -55,7 +55,9 @@ def login(session: dict) -> LoginStatus: email = configs.get("email", "") User.load_user_info(session, email) Local.apply_configs(configs=configs) - User.update_flair() + User.update_flair( + flair=configs.get("features_settings", {}).get("USE_FLAIR", None) + ) return LoginStatus.SUCCESS return LoginStatus.FAILED return LoginStatus.NO_RESPONSE diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 86685b7d074..cc9c092f550 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -34,12 +34,12 @@ class User: } @staticmethod - def update_flair(): - MAX_FLAIR_LEN = 20 - username = User._EMAIL[: User._EMAIL.find("@")] - username = "[" + username[:MAX_FLAIR_LEN] + "]" - - if obbff.USE_FLAIR == ":openbb": + def update_flair(flair: str): + """Update flair if user has not changed it.""" + if flair is None: + MAX_FLAIR_LEN = 20 + username = User._EMAIL[: User._EMAIL.find("@")] + username = "[" + username[:MAX_FLAIR_LEN] + "]" setattr(obbff, "USE_FLAIR", username + " 🦋") @classmethod From 656e10ed387cfff8b6ffdbd574ab55daf9440e1b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 11:59:34 +0000 Subject: [PATCH 164/230] save session test --- .../session/test_local_model.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/openbb_terminal/session/test_local_model.py diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py new file mode 100644 index 00000000000..dae9b15f928 --- /dev/null +++ b/tests/openbb_terminal/session/test_local_model.py @@ -0,0 +1,22 @@ +# IMPORTATION STANDARD + +# IMPORTATION THIRDPARTY +import json +from unittest.mock import patch, mock_open + +# IMPORTATION INTERNAL +from openbb_terminal.session import local_model + + +def test_save_good_session(): + open_mock = mock_open() + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + data = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", + } + local_model.save_session(data=data) + + open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") + open_mock.return_value.write.assert_called_once_with(json.dumps(data)) From 43512687a43d88c657c552265edac7b489ed9c74 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:04:41 +0000 Subject: [PATCH 165/230] get session --- openbb_terminal/helper_funcs.py | 2 -- tests/openbb_terminal/session/test_local_model.py | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index 1cd79887428..2da0dbcbf93 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -24,7 +24,6 @@ import pytz import pandas as pd from rich.table import Table import iso8601 -import dotenv import matplotlib import matplotlib.pyplot as plt from holidays import US as us_holidays @@ -46,7 +45,6 @@ from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, USER_EXPORTS_DIRECTORY, ) -from openbb_terminal.core.config import paths logger = logging.getLogger(__name__) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index dae9b15f928..6294064be68 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -20,3 +20,12 @@ def test_save_good_session(): open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") open_mock.return_value.write.assert_called_once_with(json.dumps(data)) + + +def test_get_session(): + open_mock = mock_open(read_data=json.dumps({"test": "test"})) + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + assert local_model.get_session() == {"test": "test"} + + open_mock.assert_called_with(local_model.SESSION_FILE_PATH) + open_mock.return_value.read.assert_called_once() From 57b4ba8bd1a05b652f0c651e062ef7187463ff8d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:06:23 +0000 Subject: [PATCH 166/230] data --- tests/openbb_terminal/session/test_local_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 6294064be68..526bd09215d 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -23,9 +23,14 @@ def test_save_good_session(): def test_get_session(): - open_mock = mock_open(read_data=json.dumps({"test": "test"})) + data = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", + } + open_mock = mock_open(read_data=json.dumps(data)) with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - assert local_model.get_session() == {"test": "test"} + assert local_model.get_session() == data open_mock.assert_called_with(local_model.SESSION_FILE_PATH) open_mock.return_value.read.assert_called_once() From d5dd7f44a0c8b011a9959ede93a018ec37a2f22f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:45:01 +0000 Subject: [PATCH 167/230] test remove --- tests/openbb_terminal/session/test_local_model.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 526bd09215d..a9c3fad1db9 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -29,8 +29,17 @@ def test_get_session(): "uuid": "test_uuid", } open_mock = mock_open(read_data=json.dumps(data)) - with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - assert local_model.get_session() == data + with patch("openbb_terminal.session.local_model.os.path") as path_mock: + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + assert local_model.get_session() == data + path_mock.isfile.assert_called_with(local_model.SESSION_FILE_PATH) open_mock.assert_called_with(local_model.SESSION_FILE_PATH) open_mock.return_value.read.assert_called_once() + + +def test_remove_session_file(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + local_model.remove_session_file() + + os_mock.remove.assert_called_with(local_model.SESSION_FILE_PATH) From 54e92f560f559c50aa56a9db69576800c7502c8b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:45:47 +0000 Subject: [PATCH 168/230] test remove with assert --- tests/openbb_terminal/session/test_local_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index a9c3fad1db9..198edd76c1e 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -40,6 +40,7 @@ def test_get_session(): def test_remove_session_file(): with patch("openbb_terminal.session.local_model.os") as os_mock: - local_model.remove_session_file() + assert local_model.remove_session_file() is True + os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) os_mock.remove.assert_called_with(local_model.SESSION_FILE_PATH) From 9e89ce0353b0b2f8c0e018a0efbba75ddd918175 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:50:44 +0000 Subject: [PATCH 169/230] add bad test cases --- openbb_terminal/session/local_model.py | 2 +- .../session/test_local_model.py | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index ce63d09c8fe..6debd4a3018 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -53,7 +53,7 @@ def get_session(file_path: Path = SESSION_FILE_PATH) -> dict: with open(file_path) as file: return json.load(file) except Exception: - console.print("[red]\nFailed to get login info.[/red]") + console.print("\n[red]Failed to get login info.[/red]") return {} diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 198edd76c1e..d9c66b708a5 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -8,7 +8,7 @@ from unittest.mock import patch, mock_open from openbb_terminal.session import local_model -def test_save_good_session(): +def test_save_session(): open_mock = mock_open() with patch("openbb_terminal.session.local_model.open", open_mock, create=True): data = { @@ -38,9 +38,30 @@ def test_get_session(): open_mock.return_value.read.assert_called_once() +def test_get_session_not_exist(): + open_mock = mock_open() + with patch("openbb_terminal.session.local_model.os.path") as path_mock: + path_mock.isfile.return_value = False + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + assert local_model.get_session() == {} + + path_mock.isfile.assert_called_with(local_model.SESSION_FILE_PATH) + open_mock.assert_not_called() + open_mock.return_value.read.assert_not_called() + + def test_remove_session_file(): with patch("openbb_terminal.session.local_model.os") as os_mock: assert local_model.remove_session_file() is True os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) os_mock.remove.assert_called_with(local_model.SESSION_FILE_PATH) + + +def test_remove_session_file_not_exist(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + os_mock.path.isfile.return_value = False + assert local_model.remove_session_file() is True + + os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) + os_mock.remove.assert_not_called() From 5c96937e4c8cdeed437c8dec2d17d81194be287f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 12:58:48 +0000 Subject: [PATCH 170/230] add exception cases to unittests --- openbb_terminal/session/local_model.py | 6 +- .../session/test_local_model.py | 59 ++++++++++++++----- .../test_get_session_exception.txt | 2 + .../test_remove_session_file_exception.txt | 2 + .../test_save_session_exception.txt | 1 + 5 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 tests/openbb_terminal/session/txt/test_local_model/test_get_session_exception.txt create mode 100644 tests/openbb_terminal/session/txt/test_local_model/test_remove_session_file_exception.txt create mode 100644 tests/openbb_terminal/session/txt/test_local_model/test_save_session_exception.txt diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 6debd4a3018..a269479dd2f 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -32,7 +32,7 @@ def save_session(data: dict, file_path: Path = SESSION_FILE_PATH): with open(file_path, "w") as file: file.write(json.dumps(data)) except Exception: - console.print("Failed to save session info.", style="red") + console.print("[red]Failed to save session info.[/red]") def get_session(file_path: Path = SESSION_FILE_PATH) -> dict: @@ -77,7 +77,7 @@ def remove_session_file(file_path: Path = SESSION_FILE_PATH) -> bool: return True return True except Exception: - console.print("[red]\nFailed to remove login file.[/red]") + console.print("\n[red]Failed to remove login file.[/red]") return False @@ -101,7 +101,7 @@ def remove_cli_history_file(file_path: Path = HIST_FILE_PATH) -> bool: return True return True except Exception: - console.print("[red]\nFailed to remove terminal history file.[/red]") + console.print("\n[red]Failed to remove terminal history file.[/red]") return False diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index d9c66b708a5..1344885bc67 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -1,37 +1,45 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY +import pytest import json from unittest.mock import patch, mock_open # IMPORTATION INTERNAL from openbb_terminal.session import local_model +TEST_DATA = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + def test_save_session(): open_mock = mock_open() with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - data = { - "access_token": "test_token", - "token_type": "bearer", - "uuid": "test_uuid", - } - local_model.save_session(data=data) + local_model.save_session(data=TEST_DATA) open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") - open_mock.return_value.write.assert_called_once_with(json.dumps(data)) + open_mock.return_value.write.assert_called_once_with(json.dumps(TEST_DATA)) + + +@pytest.mark.record_stdout +def test_save_session_exception(): + open_mock = mock_open() + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + open_mock.side_effect = Exception + local_model.save_session(data=TEST_DATA) + + open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") + open_mock.return_value.write.assert_not_called() def test_get_session(): - data = { - "access_token": "test_token", - "token_type": "bearer", - "uuid": "test_uuid", - } - open_mock = mock_open(read_data=json.dumps(data)) + open_mock = mock_open(read_data=json.dumps(TEST_DATA)) with patch("openbb_terminal.session.local_model.os.path") as path_mock: with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - assert local_model.get_session() == data + assert local_model.get_session() == TEST_DATA path_mock.isfile.assert_called_with(local_model.SESSION_FILE_PATH) open_mock.assert_called_with(local_model.SESSION_FILE_PATH) @@ -50,6 +58,19 @@ def test_get_session_not_exist(): open_mock.return_value.read.assert_not_called() +@pytest.mark.record_stdout +def test_get_session_exception(): + open_mock = mock_open() + with patch("openbb_terminal.session.local_model.os.path") as path_mock: + with patch("openbb_terminal.session.local_model.open", open_mock, create=True): + open_mock.side_effect = Exception + assert local_model.get_session() == {} + + path_mock.isfile.assert_called_with(local_model.SESSION_FILE_PATH) + open_mock.assert_called_with(local_model.SESSION_FILE_PATH) + open_mock.return_value.read.assert_not_called() + + def test_remove_session_file(): with patch("openbb_terminal.session.local_model.os") as os_mock: assert local_model.remove_session_file() is True @@ -65,3 +86,13 @@ def test_remove_session_file_not_exist(): os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) os_mock.remove.assert_not_called() + + +@pytest.mark.record_stdout +def test_remove_session_file_exception(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + os_mock.remove.side_effect = Exception + assert local_model.remove_session_file() is False + + os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) + os_mock.remove.assert_called_with(local_model.SESSION_FILE_PATH) diff --git a/tests/openbb_terminal/session/txt/test_local_model/test_get_session_exception.txt b/tests/openbb_terminal/session/txt/test_local_model/test_get_session_exception.txt new file mode 100644 index 00000000000..2b94fe3f7f5 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_local_model/test_get_session_exception.txt @@ -0,0 +1,2 @@ + +[red]Failed to get login info.[/red] diff --git a/tests/openbb_terminal/session/txt/test_local_model/test_remove_session_file_exception.txt b/tests/openbb_terminal/session/txt/test_local_model/test_remove_session_file_exception.txt new file mode 100644 index 00000000000..9f4edce22ff --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_local_model/test_remove_session_file_exception.txt @@ -0,0 +1,2 @@ + +[red]Failed to remove login file.[/red] diff --git a/tests/openbb_terminal/session/txt/test_local_model/test_save_session_exception.txt b/tests/openbb_terminal/session/txt/test_local_model/test_save_session_exception.txt new file mode 100644 index 00000000000..60eb2f1ea30 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_local_model/test_save_session_exception.txt @@ -0,0 +1 @@ +[red]Failed to save session info.[/red] From 42acf57cc2ae887b3b3474473e3330fd635f8ced Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 13:02:37 +0000 Subject: [PATCH 171/230] test remove cli hist file --- .../session/test_local_model.py | 27 +++++++++++++++++++ ...test_remove_cli_history_file_exception.txt | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 tests/openbb_terminal/session/txt/test_local_model/test_remove_cli_history_file_exception.txt diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 1344885bc67..449b06089a8 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -96,3 +96,30 @@ def test_remove_session_file_exception(): os_mock.path.isfile.assert_called_with(local_model.SESSION_FILE_PATH) os_mock.remove.assert_called_with(local_model.SESSION_FILE_PATH) + + +def test_remove_cli_history_file(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + assert local_model.remove_cli_history_file() is True + + os_mock.path.isfile.assert_called_with(local_model.HIST_FILE_PATH) + os_mock.remove.assert_called_with(local_model.HIST_FILE_PATH) + + +def test_remove_cli_history_file_not_exist(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + os_mock.path.isfile.return_value = False + assert local_model.remove_cli_history_file() is True + + os_mock.path.isfile.assert_called_with(local_model.HIST_FILE_PATH) + os_mock.remove.assert_not_called() + + +@pytest.mark.record_stdout +def test_remove_cli_history_file_exception(): + with patch("openbb_terminal.session.local_model.os") as os_mock: + os_mock.remove.side_effect = Exception + assert local_model.remove_cli_history_file() is False + + os_mock.path.isfile.assert_called_with(local_model.HIST_FILE_PATH) + os_mock.remove.assert_called_with(local_model.HIST_FILE_PATH) diff --git a/tests/openbb_terminal/session/txt/test_local_model/test_remove_cli_history_file_exception.txt b/tests/openbb_terminal/session/txt/test_local_model/test_remove_cli_history_file_exception.txt new file mode 100644 index 00000000000..436ae6e20dd --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_local_model/test_remove_cli_history_file_exception.txt @@ -0,0 +1,2 @@ + +[red]Failed to remove terminal history file.[/red] From 90c333dc253bbd7c427edc7588ef89e7772fd20f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 13:03:58 +0000 Subject: [PATCH 172/230] black --- openbb_terminal/settings_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index f485db56842..53785fe069c 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -28,7 +28,7 @@ from openbb_terminal.helper_funcs import ( get_flair, get_user_timezone_or_invalid, parse_and_split_input, - ) +) from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import console, MenuText From cf4024af47cbeb8caed863f32f765bb729b9755e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 14:45:31 +0000 Subject: [PATCH 173/230] test apply configs --- .../session/test_local_model.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 449b06089a8..b62387f08b5 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -1,6 +1,7 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY +from pathlib import Path import pytest import json from unittest.mock import patch, mock_open @@ -123,3 +124,55 @@ def test_remove_cli_history_file_exception(): os_mock.path.isfile.assert_called_with(local_model.HIST_FILE_PATH) os_mock.remove.assert_called_with(local_model.HIST_FILE_PATH) + + +# Patch the config classes +class obbff: + USE_WATERMARK = True + TIMEZONE = "America/New_York" + + +class cfg_plot: + PLOT_DPI = 100 + PLOT_HEIGHT_PERCENTAGE = 50.0 + + +class paths: + USER_DATA_DIRECTORY = Path("user_home/OpenBBUserData") + + +class cfg: + API_KEY_ALPHAVANTAGE = "REPLACE_ME" + API_FRED_KEY = "REPLACE_ME" + + +# Configs to apply +CONFIGS = { + "features_settings": { + "USE_WATERMARK": "False", + "TIMEZONE": "Europe/London", + "PLOT_DPI": "95", + "PLOT_HEIGHT_PERCENTAGE": "50.5", + "USER_DATA_DIRECTORY": "some/path/to/user/data", + }, + "features_keys": { + "API_KEY_ALPHAVANTAGE": "test_av", + "API_FRED_KEY": "test_fred", + }, +} + + +@patch("openbb_terminal.session.local_model.obbff", obbff) +@patch("openbb_terminal.session.local_model.cfg_plot", cfg_plot) +@patch("openbb_terminal.session.local_model.paths", paths) +@patch("openbb_terminal.session.local_model.cfg", cfg) +def test_apply_configs(): + local_model.apply_configs(CONFIGS) + + assert obbff.USE_WATERMARK is False + assert obbff.TIMEZONE == "Europe/London" + assert cfg_plot.PLOT_DPI == 95 + assert cfg_plot.PLOT_HEIGHT_PERCENTAGE == 50.5 + assert paths.USER_DATA_DIRECTORY == Path("some/path/to/user/data") + assert cfg.API_KEY_ALPHAVANTAGE == "test_av" + assert cfg.API_FRED_KEY == "test_fred" From 7bbacc884a7c3d39ad4364420b9bfec181ecf8ae Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 15:01:37 +0000 Subject: [PATCH 174/230] test apply with sync flag --- .../session/test_local_model.py | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index b62387f08b5..18694943c72 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -128,6 +128,7 @@ def test_remove_cli_history_file_exception(): # Patch the config classes class obbff: + SYNC_ENABLED = True USE_WATERMARK = True TIMEZONE = "America/New_York" @@ -162,17 +163,36 @@ CONFIGS = { } +@pytest.mark.parametrize( + "sync", + [ + "False", + "True", + ], +) @patch("openbb_terminal.session.local_model.obbff", obbff) @patch("openbb_terminal.session.local_model.cfg_plot", cfg_plot) @patch("openbb_terminal.session.local_model.paths", paths) @patch("openbb_terminal.session.local_model.cfg", cfg) -def test_apply_configs(): +def test_apply_configs_sync(sync: str): + CONFIGS["features_settings"]["SYNC_ENABLED"] = sync local_model.apply_configs(CONFIGS) - assert obbff.USE_WATERMARK is False - assert obbff.TIMEZONE == "Europe/London" - assert cfg_plot.PLOT_DPI == 95 - assert cfg_plot.PLOT_HEIGHT_PERCENTAGE == 50.5 - assert paths.USER_DATA_DIRECTORY == Path("some/path/to/user/data") - assert cfg.API_KEY_ALPHAVANTAGE == "test_av" - assert cfg.API_FRED_KEY == "test_fred" + if sync == "False": + assert obbff.SYNC_ENABLED is False + assert obbff.USE_WATERMARK is True + assert obbff.TIMEZONE == "America/New_York" + assert cfg_plot.PLOT_DPI == 100 + assert cfg_plot.PLOT_HEIGHT_PERCENTAGE == 50.0 + assert paths.USER_DATA_DIRECTORY == Path("user_home/OpenBBUserData") + assert cfg.API_KEY_ALPHAVANTAGE == "REPLACE_ME" + assert cfg.API_FRED_KEY == "REPLACE_ME" + else: + assert obbff.SYNC_ENABLED is True + assert obbff.USE_WATERMARK is False + assert obbff.TIMEZONE == "Europe/London" + assert cfg_plot.PLOT_DPI == 95 + assert cfg_plot.PLOT_HEIGHT_PERCENTAGE == 50.5 + assert paths.USER_DATA_DIRECTORY == Path("some/path/to/user/data") + assert cfg.API_KEY_ALPHAVANTAGE == "test_av" + assert cfg.API_FRED_KEY == "test_fred" From ac00d522cf4a6f5758492d69d0f1fc2e0b8465d6 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 16:17:08 +0000 Subject: [PATCH 175/230] test create session, session model --- openbb_terminal/session/hub_model.py | 12 ++--- .../session/test_local_model.py | 12 ++--- .../session/test_session_model.py | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 tests/openbb_terminal/session/test_session_model.py diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 438224512e9..83291149eed 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -36,13 +36,13 @@ def create_session( } return requests.post(url=base_url + "login", json=data, timeout=timeout) except requests.exceptions.ConnectionError: - console.print("[red]\nConnection error.[/red]") + console.print("\n[red]Connection error.[/red]") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print("\n[red]Connection timeout.[/red]") return None except Exception: - console.print("[red]\nFailed to request login info.[/red]") + console.print("\n[red]Failed to request login info.[/red]") return None @@ -105,12 +105,12 @@ def process_session_response(response: requests.Response) -> dict: login = response.json() return login if response.status_code == 401: - console.print("[red]\nWrong credentials.[/red]") + console.print("\n[red]Wrong credentials.[/red]") return {} if response.status_code == 403: - console.print("[red]\nUnverified email.[/red]") + console.print("\n[red]Unverified email.[/red]") return {} - console.print("[red]\nFailed to login.[/red]") + console.print("\n[red]Failed to login.[/red]") return {} diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 18694943c72..408bc5f2137 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -9,7 +9,7 @@ from unittest.mock import patch, mock_open # IMPORTATION INTERNAL from openbb_terminal.session import local_model -TEST_DATA = { +TEST_SESSION = { "access_token": "test_token", "token_type": "bearer", "uuid": "test_uuid", @@ -19,10 +19,10 @@ TEST_DATA = { def test_save_session(): open_mock = mock_open() with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - local_model.save_session(data=TEST_DATA) + local_model.save_session(data=TEST_SESSION) open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") - open_mock.return_value.write.assert_called_once_with(json.dumps(TEST_DATA)) + open_mock.return_value.write.assert_called_once_with(json.dumps(TEST_SESSION)) @pytest.mark.record_stdout @@ -30,17 +30,17 @@ def test_save_session_exception(): open_mock = mock_open() with patch("openbb_terminal.session.local_model.open", open_mock, create=True): open_mock.side_effect = Exception - local_model.save_session(data=TEST_DATA) + local_model.save_session(data=TEST_SESSION) open_mock.assert_called_with(local_model.SESSION_FILE_PATH, "w") open_mock.return_value.write.assert_not_called() def test_get_session(): - open_mock = mock_open(read_data=json.dumps(TEST_DATA)) + open_mock = mock_open(read_data=json.dumps(TEST_SESSION)) with patch("openbb_terminal.session.local_model.os.path") as path_mock: with patch("openbb_terminal.session.local_model.open", open_mock, create=True): - assert local_model.get_session() == TEST_DATA + assert local_model.get_session() == TEST_SESSION path_mock.isfile.assert_called_with(local_model.SESSION_FILE_PATH) open_mock.assert_called_with(local_model.SESSION_FILE_PATH) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py new file mode 100644 index 00000000000..4d3068f8fbb --- /dev/null +++ b/tests/openbb_terminal/session/test_session_model.py @@ -0,0 +1,45 @@ +# IMPORTATION STANDARD + +# IMPORTATION THIRDPARTY +from unittest.mock import patch +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.session import session_model + +TEST_SESSION = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + + +@pytest.mark.parametrize( + "email, password, save", + [ + ( + "test_email", + "test_pass", + False, + ), + ( + "test_email", + "test_pass", + True, + ), + ], +) +def test_create_session(email, password, save): + with patch( + "openbb_terminal.session.session_model.Hub.get_session" + ) as mock_get_session: + mock_get_session.return_value = TEST_SESSION + with patch( + "openbb_terminal.session.session_model.Local.save_session" + ) as mock_save_session: + session = session_model.create_session(email, password, save) + + assert session == TEST_SESSION + mock_get_session.assert_called_once_with(email, password) + if save and session: + mock_save_session.assert_called_once_with(session) From 9df58b633a43fd79f1bcc4c7a9a91c149961ff4c Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 17:15:17 +0000 Subject: [PATCH 176/230] login tests, session model --- openbb_terminal/session/session_model.py | 2 +- .../session/test_session_model.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 6c45b13bc7c..00d4ac69efc 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -49,7 +49,7 @@ def login(session: dict) -> LoginStatus: The session info. """ response = Hub.fetch_user_configs(session) - if response: + if response is not None: if response.status_code == 200: configs = json.loads(response.content) email = configs.get("email", "") diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index 4d3068f8fbb..2203efe2e1a 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -1,8 +1,10 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY +import json from unittest.mock import patch import pytest +from requests import Response # IMPORTATION INTERNAL from openbb_terminal.session import session_model @@ -43,3 +45,59 @@ def test_create_session(email, password, save): mock_get_session.assert_called_once_with(email, password) if save and session: mock_save_session.assert_called_once_with(session) + + +@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") +@patch("openbb_terminal.session.session_model.Local.apply_configs") +@patch("openbb_terminal.session.session_model.User.load_user_info") +@patch("openbb_terminal.session.session_model.User.update_flair") +def test_login_no_response( + mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs +): + + mock_fetch_user_configs.return_value = None + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.NO_RESPONSE + + mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) + mock_apply_configs.assert_not_called() + mock_load_user_info.assert_not_called() + mock_update_flair.assert_not_called() + + +@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") +@patch("openbb_terminal.session.session_model.Local.apply_configs") +@patch("openbb_terminal.session.session_model.User.load_user_info") +@patch("openbb_terminal.session.session_model.User.update_flair") +def test_login_fail_response( + mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs +): + + response = Response() + response.status_code = 400 + mock_fetch_user_configs.return_value = response + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.FAILED + + mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) + mock_apply_configs.assert_not_called() + mock_load_user_info.assert_not_called() + mock_update_flair.assert_not_called() + + +@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") +@patch("openbb_terminal.session.session_model.Local.apply_configs") +@patch("openbb_terminal.session.session_model.User.load_user_info") +@patch("openbb_terminal.session.session_model.User.update_flair") +def test_login_success_response( + mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs +): + + response = Response() + response.status_code = 200 + response._content = json.dumps(TEST_SESSION) # pylint: disable=protected-access + mock_fetch_user_configs.return_value = response + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.SUCCESS + + mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) + mock_apply_configs.assert_called_once() + mock_load_user_info.assert_called_once() + mock_update_flair.assert_called_once() From 6c52ee9883ae7d9f5b03354cde90f2f1acd30a71 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Thu, 2 Feb 2023 18:01:38 +0000 Subject: [PATCH 177/230] some tests for the hub_model --- .../openbb_terminal/session/test_hub_model.py | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/openbb_terminal/session/test_hub_model.py diff --git a/tests/openbb_terminal/session/test_hub_model.py b/tests/openbb_terminal/session/test_hub_model.py new file mode 100644 index 00000000000..7dd872b429f --- /dev/null +++ b/tests/openbb_terminal/session/test_hub_model.py @@ -0,0 +1,185 @@ +from unittest.mock import patch, MagicMock +import requests +import pytest + +from openbb_terminal.session import hub_model + +TEST_RESPONSE = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + +TEST_EMAIL_PASSWORD = [ + ( + "test_email", + "test_pass", + ), +] + +TEST_HEADER_TOKEN = [ + ("Bearer test_token", "test_token"), +] + + +@pytest.mark.parametrize("email, password", TEST_EMAIL_PASSWORD) +def test_create_session_success(email, password): + with patch("requests.post") as mock_post: + mock_post.return_value.json.return_value = TEST_RESPONSE + response = hub_model.create_session(email, password) + assert response.json() == TEST_RESPONSE + + mock_post.assert_called_once_with( + url=hub_model.BASE_URL + "login", + json={"email": email, "password": password, "remember": True}, + timeout=hub_model.TIMEOUT, + ) + + +@pytest.mark.parametrize("email, password", TEST_EMAIL_PASSWORD) +def test_create_session_connError(email, password): + with patch("requests.post") as mock_post: + mock_post.side_effect = requests.exceptions.ConnectionError + response = hub_model.create_session(email, password) + assert response is None + + +@pytest.mark.parametrize("email, password", TEST_EMAIL_PASSWORD) +def test_create_session_timeout(email, password): + with patch("requests.post") as mock_post: + mock_post.side_effect = requests.exceptions.Timeout + response = hub_model.create_session(email, password) + assert response is None + + +@pytest.mark.parametrize("email, password", TEST_EMAIL_PASSWORD) +def test_create_session_exception(email, password): + with patch("requests.post") as mock_post: + mock_post.side_effect = Exception + response = hub_model.create_session(email, password) + assert response is None + + +@pytest.mark.parametrize("auth_header, token", TEST_HEADER_TOKEN) +def test_delete_session_success(auth_header, token): + with patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + response = hub_model.delete_session(auth_header, token) + assert response.status_code == 200 + + mock_post.assert_called_once_with( + url=hub_model.BASE_URL + "logout", + headers={"Authorization": "Bearer test_token"}, + json={"token": "test_token"}, + timeout=hub_model.TIMEOUT, + ) + + +@pytest.mark.parametrize("auth_header, token", TEST_HEADER_TOKEN) +def test_delete_session_connError(auth_header, token): + with patch("requests.post") as mock_post: + mock_post.side_effect = requests.exceptions.ConnectionError + response = hub_model.delete_session(auth_header, token) + assert response is None + + +@pytest.mark.parametrize("auth_header, token", TEST_HEADER_TOKEN) +def test_delete_session_timeout(auth_header, token): + with patch("requests.post") as mock_post: + mock_post.side_effect = requests.exceptions.Timeout + response = hub_model.delete_session(auth_header, token) + assert response is None + + +@pytest.mark.parametrize("auth_header, token", TEST_HEADER_TOKEN) +def test_delete_session_exception(auth_header, token): + with patch("requests.post") as mock_post: + mock_post.side_effect = Exception + response = hub_model.delete_session(auth_header, token) + assert response is None + + +def test_process_session_response_success(): + response = requests.Response() + response.status_code = 200 + response.json = lambda: { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", + } + login = hub_model.process_session_response(response) + assert login == { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", + } + + +def test_process_session_response_401(): + response = requests.Response() + response.status_code = 401 + login = hub_model.process_session_response(response) + assert not login + assert isinstance(login, dict) + + +def test_process_session_response_403(): + response = requests.Response() + response.status_code = 403 + login = hub_model.process_session_response(response) + assert not login + assert isinstance(login, dict) + + +def test_process_session_response_fail(): + response = requests.Response() + response.status_code = 400 + login = hub_model.process_session_response(response) + assert not login + assert isinstance(login, dict) + + +def test_get_session_success(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"session": "info"} + + with patch( + "openbb_terminal.session.hub_model.create_session", return_value=mock_response + ) as create_session_mock: + result = hub_model.get_session("email", "password") + assert result == {"session": "info"} + create_session_mock.assert_called_once_with("email", "password") + + +def test_get_session_401(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 401 + + with patch( + "openbb_terminal.session.hub_model.create_session", return_value=mock_response + ) as create_session_mock: + result = hub_model.get_session("email", "password") + assert result == {} + create_session_mock.assert_called_once_with("email", "password") + + +def test_get_session_403(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 403 + + with patch( + "openbb_terminal.session.hub_model.create_session", return_value=mock_response + ) as create_session_mock: + result = hub_model.get_session("email", "password") + assert result == {} + create_session_mock.assert_called_once_with("email", "password") + + +def test_get_session_failed_to_request(): + with patch( + "openbb_terminal.session.hub_model.create_session", return_value=None + ) as create_session_mock: + result = hub_model.get_session("email", "password") + assert result == {} + create_session_mock.assert_called_once_with("email", "password") From 9f796bcb11bc8a0da72fff3f62b415f2872d7a7f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 18:39:01 +0000 Subject: [PATCH 178/230] test logout user --- openbb_terminal/session/session_model.py | 40 ++++--- .../session/test_session_model.py | 101 ++++++++++++------ 2 files changed, 94 insertions(+), 47 deletions(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 00d4ac69efc..6d9cb350908 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -91,25 +91,14 @@ def logout( if not auth_header or not token: return - r = Hub.delete_session(auth_header=auth_header, token=token) + r = Hub.delete_session(auth_header, token) if r and r.status_code != 200: success = False User.clear() - # Clear openbb environment variables - for v in os.environ: - if v.startswith("OPENBB"): - os.environ.pop(v) - - # Remove the log handlers - needs to be done before reloading modules - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) - - # Reload all openbb modules to clear memorized variables - modules = sys.modules.copy() - for module in modules: - if module.startswith("openbb"): - importlib.reload(sys.modules[module]) + clear_openbb_env_vars() + remove_log_handlers() + reload_openbb_modules() if not Local.remove_session_file(): success = False @@ -120,3 +109,24 @@ def logout( if success: console.print("[green]\nLogout successful.[/green]") + + +def clear_openbb_env_vars(): + """Clear openbb environment variables.""" + for v in os.environ: + if v.startswith("OPENBB"): + os.environ.pop(v) + + +def remove_log_handlers(): + """Remove the log handlers - needs to be done before reloading modules.""" + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + + +def reload_openbb_modules(): + """Reload all openbb modules to clear memorized variables.""" + modules = sys.modules.copy() + for module in modules: + if module.startswith("openbb"): + importlib.reload(sys.modules[module]) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index 2203efe2e1a..e8db405dc10 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -42,21 +42,23 @@ def test_create_session(email, password, save): session = session_model.create_session(email, password, save) assert session == TEST_SESSION + mock_get_session.assert_called_once_with(email, password) if save and session: mock_save_session.assert_called_once_with(session) -@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") -@patch("openbb_terminal.session.session_model.Local.apply_configs") -@patch("openbb_terminal.session.session_model.User.load_user_info") -@patch("openbb_terminal.session.session_model.User.update_flair") -def test_login_no_response( - mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs -): +def test_login_no_response(): + path = "openbb_terminal.session.session_model." + with ( + patch(path + "Hub.fetch_user_configs") as mock_fetch_user_configs, + patch(path + "Local.apply_configs") as mock_apply_configs, + patch(path + "User.load_user_info") as mock_load_user_info, + patch(path + "User.update_flair") as mock_update_flair, + ): + mock_fetch_user_configs.return_value = None - mock_fetch_user_configs.return_value = None - assert session_model.login(TEST_SESSION) == session_model.LoginStatus.NO_RESPONSE + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.NO_RESPONSE mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) mock_apply_configs.assert_not_called() @@ -64,18 +66,19 @@ def test_login_no_response( mock_update_flair.assert_not_called() -@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") -@patch("openbb_terminal.session.session_model.Local.apply_configs") -@patch("openbb_terminal.session.session_model.User.load_user_info") -@patch("openbb_terminal.session.session_model.User.update_flair") -def test_login_fail_response( - mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs -): +def test_login_fail_response(): + path = "openbb_terminal.session.session_model." + with ( + patch(path + "Hub.fetch_user_configs") as mock_fetch_user_configs, + patch(path + "Local.apply_configs") as mock_apply_configs, + patch(path + "User.load_user_info") as mock_load_user_info, + patch(path + "User.update_flair") as mock_update_flair, + ): + response = Response() + response.status_code = 400 + mock_fetch_user_configs.return_value = response - response = Response() - response.status_code = 400 - mock_fetch_user_configs.return_value = response - assert session_model.login(TEST_SESSION) == session_model.LoginStatus.FAILED + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.FAILED mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) mock_apply_configs.assert_not_called() @@ -83,21 +86,55 @@ def test_login_fail_response( mock_update_flair.assert_not_called() -@patch("openbb_terminal.session.session_model.Hub.fetch_user_configs") -@patch("openbb_terminal.session.session_model.Local.apply_configs") -@patch("openbb_terminal.session.session_model.User.load_user_info") -@patch("openbb_terminal.session.session_model.User.update_flair") -def test_login_success_response( - mock_update_flair, mock_load_user_info, mock_apply_configs, mock_fetch_user_configs -): +def test_login_success_response(): + path = "openbb_terminal.session.session_model." + with ( + patch(path + "Hub.fetch_user_configs") as mock_fetch_user_configs, + patch(path + "Local.apply_configs") as mock_apply_configs, + patch(path + "User.load_user_info") as mock_load_user_info, + patch(path + "User.update_flair") as mock_update_flair, + ): + response = Response() + response.status_code = 200 + response._content = json.dumps(TEST_SESSION) # pylint: disable=protected-access + mock_fetch_user_configs.return_value = response - response = Response() - response.status_code = 200 - response._content = json.dumps(TEST_SESSION) # pylint: disable=protected-access - mock_fetch_user_configs.return_value = response - assert session_model.login(TEST_SESSION) == session_model.LoginStatus.SUCCESS + assert session_model.login(TEST_SESSION) == session_model.LoginStatus.SUCCESS mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) mock_apply_configs.assert_called_once() mock_load_user_info.assert_called_once() mock_update_flair.assert_called_once() + + +@pytest.mark.parametrize( + "guest", + [ + False, + True, + ], +) +def test_logout_user(guest): + path = "openbb_terminal.session.session_model." + with ( + patch(path + "Hub.delete_session") as mock_delete_session, + patch(path + "Local.remove_session_file") as mock_remove_session_file, + patch(path + "Local.remove_cli_history_file") as mock_remove_cli_history_file, + patch(path + "reload_openbb_modules") as mock_reload_openbb_modules, + patch(path + "clear_openbb_env_vars") as mock_clear_openbb_env_vars, + patch(path + "User.clear") as mock_clear_user, + patch(path + "plt.close") as mock_plt_close, + ): + + auth_header = "Bearer test_token" + token = "test_token" + session_model.logout(auth_header, token, guest) + + if not guest: + mock_delete_session.assert_called_once_with(auth_header, token) + mock_clear_user.assert_called_once() + mock_clear_openbb_env_vars.assert_called_once() + mock_reload_openbb_modules.assert_called_once() + mock_remove_session_file.assert_called_once() + mock_remove_cli_history_file.assert_called_once() + mock_plt_close.assert_called_once() From edb465b6f61c4966ea747effecb52dc0438492c5 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 18:39:26 +0000 Subject: [PATCH 179/230] black --- tests/openbb_terminal/session/test_session_model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index e8db405dc10..2cb0a01e42e 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -58,7 +58,9 @@ def test_login_no_response(): ): mock_fetch_user_configs.return_value = None - assert session_model.login(TEST_SESSION) == session_model.LoginStatus.NO_RESPONSE + assert ( + session_model.login(TEST_SESSION) == session_model.LoginStatus.NO_RESPONSE + ) mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) mock_apply_configs.assert_not_called() From 594d28712c3ab2bdc51437304153d6f767056986 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 19:11:56 +0000 Subject: [PATCH 180/230] test clear vars, reload --- .../session/test_session_model.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index 2cb0a01e42e..cd3db89c95e 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -1,6 +1,7 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY +import os import json from unittest.mock import patch import pytest @@ -140,3 +141,20 @@ def test_logout_user(guest): mock_remove_session_file.assert_called_once() mock_remove_cli_history_file.assert_called_once() mock_plt_close.assert_called_once() + + +def test_clear_openbb_env_vars(): + mock_env = {"OPENBB_TEST": "test", "OPENBB_TEST2": "test2", "TEST": "test"} + with patch.dict("openbb_terminal.session.session_model.os.environ", mock_env): + session_model.clear_openbb_env_vars() + + assert "OPENBB_TEST" not in os.environ + assert "OPENBB_TEST2" not in os.environ + assert "TEST" in os.environ + + +def test_reload_openbb_modules(): + with patch("openbb_terminal.session.session_model.importlib.reload") as mock_reload: + session_model.reload_openbb_modules() + + mock_reload.assert_called() From 9b90f1171b339d1f48605737da787fcd8607b542 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 19:14:21 +0000 Subject: [PATCH 181/230] test create session --- tests/openbb_terminal/session/test_session_model.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index cd3db89c95e..bda3bec6ff9 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -33,14 +33,13 @@ TEST_SESSION = { ], ) def test_create_session(email, password, save): - with patch( - "openbb_terminal.session.session_model.Hub.get_session" - ) as mock_get_session: + path = "openbb_terminal.session.session_model." + with ( + patch(path + "Hub.get_session") as mock_get_session, + patch(path + "Local.save_session") as mock_save_session, + ): mock_get_session.return_value = TEST_SESSION - with patch( - "openbb_terminal.session.session_model.Local.save_session" - ) as mock_save_session: - session = session_model.create_session(email, password, save) + session = session_model.create_session(email, password, save) assert session == TEST_SESSION From 354031e258f1f0c407cbf188eb376ef90ccd2f6a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 22:09:48 +0000 Subject: [PATCH 182/230] update user-json to user --- openbb_terminal/session/hub_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 83291149eed..6c9b46779a7 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -217,7 +217,7 @@ def patch_user_configs( try: response = requests.patch( - url=base_url + "terminal/user-json", + url=base_url + "terminal/user", headers={"Authorization": auth_header}, json=data, timeout=timeout, From 851abfc000b68b1b6f8b06bba6c4481bb273268a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 22:09:56 +0000 Subject: [PATCH 183/230] update user class --- openbb_terminal/session/user.py | 55 ++++++++++++++------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index cc9c092f550..b9dc660729c 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -1,14 +1,12 @@ -from dataclasses import dataclass import openbb_terminal.feature_flags as obbff from openbb_terminal.rich_config import console -@dataclass class User: - _TOKEN_TYPE: str = "" - _TOKEN: str = "" - _EMAIL: str = "" - _UUID: str = "" + token_type: str = "" + token: str = "" + uuid: str = "" + email: str = "" @classmethod def load_user_info(cls, session: dict, email: str): @@ -19,18 +17,18 @@ class User: session : dict The login info. """ - User._TOKEN_TYPE = session.get("token_type", "") - User._TOKEN = session.get("access_token", "") - User._UUID = session.get("uuid", "") - User._EMAIL = email + User.token_type = session.get("token_type", "") + User.token = session.get("access_token", "") + User.uuid = session.get("uuid", "") + User.email = email @classmethod def get_session(cls): """Get session info.""" return { - "token_type": User._TOKEN_TYPE, - "access_token": User._TOKEN, - "uuid": User._UUID, + "token_type": User.token_type, + "access_token": User.token, + "uuid": User.uuid, } @staticmethod @@ -38,31 +36,26 @@ class User: """Update flair if user has not changed it.""" if flair is None: MAX_FLAIR_LEN = 20 - username = User._EMAIL[: User._EMAIL.find("@")] + username = User.email[: User.email.find("@")] username = "[" + username[:MAX_FLAIR_LEN] + "]" setattr(obbff, "USE_FLAIR", username + " 🦋") @classmethod def get_uuid(cls): """Get uuid.""" - return User._UUID - - @classmethod - def get_email(cls): - """Get email.""" - return User._EMAIL + return User.uuid @classmethod def whoami(cls): """Display user info.""" - if User._UUID: - console.print(f"[info]email:[/info] {User._EMAIL}") - console.print(f"[info]uuid:[/info] {User._UUID}") + if User.uuid: + console.print(f"[info]email:[/info] {User.email}") + console.print(f"[info]uuid:[/info] {User.uuid}") if obbff.SYNC_ENABLED: sync = "ON" else: sync = "OFF" - console.print(f"[info]sync:[/info] {sync}\n") + console.print(f"[info]sync:[/info] {sync}") else: console.print( "[info]You are currently logged as a guest.\n" @@ -72,16 +65,16 @@ class User: @classmethod def clear(cls): """Clear user info.""" - User._TOKEN_TYPE = "" - User._TOKEN = "" - User._EMAIL = "" - User._UUID = "" + User.token_type = "" + User.token = "" + User.email = "" + User.uuid = "" obbff.USE_FLAIR = ":openbb" @classmethod def is_guest(cls): """Check if user is guest.""" - return not bool(User._TOKEN) + return not bool(User.token) @classmethod def is_sync_enabled(cls): @@ -91,9 +84,9 @@ class User: @classmethod def get_auth_header(cls): """Get token.""" - return f"{User._TOKEN_TYPE.title()} {User._TOKEN}" + return f"{User.token_type.title()} {User.token}" @classmethod def get_token(cls): """Get token.""" - return User._TOKEN + return User.token From 1f7baeac4fbd69adeb7eda1dea5cdf0ac4ddf7ba Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 2 Feb 2023 23:42:22 +0000 Subject: [PATCH 184/230] add user tests --- openbb_terminal/session/user.py | 50 +++++----- tests/openbb_terminal/session/test_user.py | 92 +++++++++++++++++++ .../test_user/test_whoami[False-False].txt | 3 + .../txt/test_user/test_whoami[True-False].txt | 3 + .../txt/test_user/test_whoami[True-True].txt | 2 + 5 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 tests/openbb_terminal/session/test_user.py create mode 100644 tests/openbb_terminal/session/txt/test_user/test_whoami[False-False].txt create mode 100644 tests/openbb_terminal/session/txt/test_user/test_whoami[True-False].txt create mode 100644 tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index b9dc660729c..401070db547 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -3,10 +3,10 @@ from openbb_terminal.rich_config import console class User: - token_type: str = "" - token: str = "" - uuid: str = "" - email: str = "" + _token_type: str = "" + _token: str = "" + _uuid: str = "" + _email: str = "" @classmethod def load_user_info(cls, session: dict, email: str): @@ -17,18 +17,18 @@ class User: session : dict The login info. """ - User.token_type = session.get("token_type", "") - User.token = session.get("access_token", "") - User.uuid = session.get("uuid", "") - User.email = email + cls._token_type = session.get("token_type", "") + cls._token = session.get("access_token", "") + cls._uuid = session.get("uuid", "") + cls._email = email @classmethod def get_session(cls): """Get session info.""" return { - "token_type": User.token_type, - "access_token": User.token, - "uuid": User.uuid, + "token_type": cls._token_type, + "access_token": cls._token, + "uuid": cls._uuid, } @staticmethod @@ -36,22 +36,22 @@ class User: """Update flair if user has not changed it.""" if flair is None: MAX_FLAIR_LEN = 20 - username = User.email[: User.email.find("@")] + username = User._email[: User._email.find("@")] username = "[" + username[:MAX_FLAIR_LEN] + "]" setattr(obbff, "USE_FLAIR", username + " 🦋") @classmethod def get_uuid(cls): """Get uuid.""" - return User.uuid + return cls._uuid @classmethod def whoami(cls): """Display user info.""" - if User.uuid: - console.print(f"[info]email:[/info] {User.email}") - console.print(f"[info]uuid:[/info] {User.uuid}") - if obbff.SYNC_ENABLED: + if not User.is_guest(): + console.print(f"[info]email:[/info] {cls._email}") + console.print(f"[info]uuid:[/info] {cls._uuid}") + if obbff.SYNC_ENABLED is True: sync = "ON" else: sync = "OFF" @@ -59,22 +59,22 @@ class User: else: console.print( "[info]You are currently logged as a guest.\n" - "Create an account here https://my.openbb.co/register.[/info]\n" + "Create an account here https://my.openbb.co/register.[/info]" ) @classmethod def clear(cls): """Clear user info.""" - User.token_type = "" - User.token = "" - User.email = "" - User.uuid = "" + cls._token_type = "" + cls._token = "" + cls._email = "" + cls._uuid = "" obbff.USE_FLAIR = ":openbb" @classmethod def is_guest(cls): """Check if user is guest.""" - return not bool(User.token) + return not bool(cls._token) @classmethod def is_sync_enabled(cls): @@ -84,9 +84,9 @@ class User: @classmethod def get_auth_header(cls): """Get token.""" - return f"{User.token_type.title()} {User.token}" + return f"{cls._token_type.title()} {cls._token}" @classmethod def get_token(cls): """Get token.""" - return User.token + return cls._token diff --git a/tests/openbb_terminal/session/test_user.py b/tests/openbb_terminal/session/test_user.py new file mode 100644 index 00000000000..cbe055069a2 --- /dev/null +++ b/tests/openbb_terminal/session/test_user.py @@ -0,0 +1,92 @@ +# IMPORTATION STANDARD +from unittest.mock import patch + +# IMPORTATION THIRDPARTY +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.session import user + + +# pylint: disable=protected-access, redefined-outer-name + +TEST_SESSION = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + + +@pytest.fixture +def User(): + """User fixture.""" + user.User.load_user_info(TEST_SESSION, "test@email.com") + yield user.User + + +class obbff: + """Mock obbff.""" + + USE_FLAIR = "" + SYNC_ENABLED = True + + +def test_load_user_info(User): + """Test load user info.""" + assert User._token == "test_token" + assert User._token_type == "bearer" + assert User._uuid == "test_uuid" + assert User._email == "test@email.com" + + +@patch("openbb_terminal.session.user.obbff", obbff) +def test_update_flair(User): + """Test update flair.""" + User.update_flair(flair=None) + assert obbff.USE_FLAIR == "[test] 🦋" + + +def test_get_session(User): + """Test get session.""" + assert User.get_session() == TEST_SESSION + + +def test_get_uuid(User): + """Test get uuid.""" + assert User.get_uuid() == "test_uuid" + + +@pytest.mark.record_stdout +@pytest.mark.parametrize( + "sync, guest", + [ + (False, False), + (True, False), + (True, True), + ], +) +def test_whoami(User, sync, guest): + """Test whoami.""" + path = "openbb_terminal.session.user." + with ( + patch(path + "obbff.SYNC_ENABLED", sync), + patch(path + "User.is_guest", return_value=guest), + ): + User.whoami() + + +def test_clear(User): + """Test clear.""" + User.clear() + assert User._token == "" + assert User._token_type == "" + assert User._uuid == "" + assert User._email == "" + + +def test_get_auth_header(User): + assert User.get_auth_header() == "Bearer test_token" + + +def test_get_token(User): + assert User.get_token() == "test_token" diff --git a/tests/openbb_terminal/session/txt/test_user/test_whoami[False-False].txt b/tests/openbb_terminal/session/txt/test_user/test_whoami[False-False].txt new file mode 100644 index 00000000000..700960c5405 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_user/test_whoami[False-False].txt @@ -0,0 +1,3 @@ +[info]email:[/info] test@email.com +[info]uuid:[/info] test_uuid +[info]sync:[/info] OFF diff --git a/tests/openbb_terminal/session/txt/test_user/test_whoami[True-False].txt b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-False].txt new file mode 100644 index 00000000000..49e66764b03 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-False].txt @@ -0,0 +1,3 @@ +[info]email:[/info] test@email.com +[info]uuid:[/info] test_uuid +[info]sync:[/info] ON diff --git a/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt new file mode 100644 index 00000000000..f9e689b7db0 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt @@ -0,0 +1,2 @@ +[info]You are currently logged as a guest. +Create an account here https://my.openbb.co/register.[/info] From 695fa217621486d1b98fbe09b1990ccf01b94395 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 11:18:10 +0000 Subject: [PATCH 185/230] tests sdk session --- .../session/test_sdk_session.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/openbb_terminal/session/test_sdk_session.py diff --git a/tests/openbb_terminal/session/test_sdk_session.py b/tests/openbb_terminal/session/test_sdk_session.py new file mode 100644 index 00000000000..fab412dc5f2 --- /dev/null +++ b/tests/openbb_terminal/session/test_sdk_session.py @@ -0,0 +1,139 @@ +# IMPORTATION STANDARD + +# IMPORTATION THIRDPARTY +from unittest.mock import patch +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.session import sdk_session +from openbb_terminal.session.session_model import LoginStatus +from openbb_terminal.session.user import User + +TEST_SESSION = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + + +@pytest.mark.parametrize( + "email, password, save", + [ + ( + "test_email", + "test_pass", + True, + ), + ], +) +def test_get_session_exception(email: str, password: str, save: bool): + path = "openbb_terminal.session.sdk_session." + with (patch(path + "session_model.create_session") as mock_create_session): + mock_create_session.return_value = {} + + with pytest.raises(Exception): + sdk_session.get_session(email=email, password=password, save=save) + + mock_create_session.assert_called_once_with(email, password, save) + + +@pytest.mark.parametrize( + "email, password, save", + [ + ( + "test_email", + "test_pass", + True, + ), + ], +) +def test_get_session(email: str, password: str, save: bool): + path = "openbb_terminal.session.sdk_session." + with (patch(path + "session_model.create_session") as mock_create_session): + mock_create_session.return_value = TEST_SESSION + + sdk_session.get_session(email=email, password=password, save=save) + mock_create_session.assert_called_once_with(email, password, save) + + +@pytest.mark.parametrize( + "email, password, keep_session, has_session, status", + [ + ( + "test_email", + "test_pass", + True, + False, + LoginStatus.SUCCESS, + ), + ( + "test_email", + "test_pass", + True, + True, + LoginStatus.SUCCESS, + ), + ( + "test_email", + "test_pass", + True, + False, + LoginStatus.FAILED, + ), + ( + "test_email", + "test_pass", + True, + False, + LoginStatus.NO_RESPONSE, + ), + ], +) +def test_login( + email: str, + password: str, + keep_session: bool, + has_session: bool, + status: LoginStatus, +): + path = "openbb_terminal.session.sdk_session." + with ( + patch(path + "session_model.login") as mock_login, + patch(path + "Local.get_session") as mock_local_get_session, + patch(path + "get_session") as mock_hub_get_session, + ): + + if has_session: + mock_local_get_session.return_value = TEST_SESSION + else: + mock_local_get_session.return_value = None + + mock_hub_get_session.return_value = TEST_SESSION + mock_login.return_value = status + + if status in [LoginStatus.FAILED, LoginStatus.NO_RESPONSE]: + with pytest.raises(Exception): + sdk_session.login( + email=email, password=password, keep_session=keep_session + ) + else: + sdk_session.login(email=email, password=password, keep_session=keep_session) + + mock_local_get_session.assert_called_once() + if has_session: + mock_hub_get_session.assert_not_called() + else: + mock_hub_get_session.assert_called_once_with(email, password, keep_session) + mock_login.assert_called_once_with(TEST_SESSION) + + +def test_logout(): + User.load_user_info(TEST_SESSION, "test@email.com") + path = "openbb_terminal.session.sdk_session." + with (patch(path + "session_model.logout") as mock_logout,): + sdk_session.logout() + mock_logout.assert_called_once_with( + auth_header=User.get_auth_header(), + token=User.get_token(), + guest=User.is_guest(), + ) From 10aafd139dbbcd8a3433d4d4ed0920f6ffa192c0 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 3 Feb 2023 12:15:17 +0000 Subject: [PATCH 186/230] tests for hub_model --- openbb_terminal/session/hub_model.py | 3 + .../openbb_terminal/session/test_hub_model.py | 315 +++++++++++++++++- 2 files changed, 316 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 6c9b46779a7..8c991173aa4 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -332,5 +332,8 @@ def upload_routine( def download_routine() -> Optional[requests.Response]: """Download a routine from the server.""" + + # TODO: Implement test on `test_hub_model.py` when this is implemented. + console.print("[red]Not implemented yet.[/red]") return None diff --git a/tests/openbb_terminal/session/test_hub_model.py b/tests/openbb_terminal/session/test_hub_model.py index 7dd872b429f..ca5b7e46edf 100644 --- a/tests/openbb_terminal/session/test_hub_model.py +++ b/tests/openbb_terminal/session/test_hub_model.py @@ -37,7 +37,7 @@ def test_create_session_success(email, password): @pytest.mark.parametrize("email, password", TEST_EMAIL_PASSWORD) -def test_create_session_connError(email, password): +def test_create_session_connection_error(email, password): with patch("requests.post") as mock_post: mock_post.side_effect = requests.exceptions.ConnectionError response = hub_model.create_session(email, password) @@ -76,7 +76,7 @@ def test_delete_session_success(auth_header, token): @pytest.mark.parametrize("auth_header, token", TEST_HEADER_TOKEN) -def test_delete_session_connError(auth_header, token): +def test_delete_session_connection_error(auth_header, token): with patch("requests.post") as mock_post: mock_post.side_effect = requests.exceptions.ConnectionError response = hub_model.delete_session(auth_header, token) @@ -183,3 +183,314 @@ def test_get_session_failed_to_request(): result = hub_model.get_session("email", "password") assert result == {} create_session_mock.assert_called_once_with("email", "password") + + +@pytest.mark.parametrize("token_type, access_token", [("TokenType", "AccessToken")]) +def test_fetch_user_configs_success(token_type, access_token): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"configs": "info"} + + with patch( + "openbb_terminal.session.hub_model.requests.get", return_value=mock_response + ) as requests_get_mock: + session = {"token_type": token_type, "access_token": access_token} + result = hub_model.fetch_user_configs(session) + assert result == mock_response + requests_get_mock.assert_called_once() + _, kwargs = requests_get_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == { + "Authorization": f"{token_type.title()} {access_token}" + } + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_fetch_user_configs_failure(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 404 + + with patch("requests.get", return_value=mock_response) as get_mock: + session = {"token_type": "Bearer", "access_token": "abc123"} + result = hub_model.fetch_user_configs(session) + assert isinstance(result, requests.Response) + get_mock.assert_called_once_with( + url=hub_model.BASE_URL + "terminal/user", + headers={"Authorization": "Bearer abc123"}, + timeout=hub_model.TIMEOUT, + ) + + +def test_fetch_user_configs_connection_error(): + with patch( + "requests.get", side_effect=requests.exceptions.ConnectionError + ) as get_mock: + session = {"token_type": "Bearer", "access_token": "abc123"} + result = hub_model.fetch_user_configs(session) + assert result is None + get_mock.assert_called_once_with( + url=hub_model.BASE_URL + "terminal/user", + headers={"Authorization": "Bearer abc123"}, + timeout=hub_model.TIMEOUT, + ) + + +def test_fetch_user_configs_timeout(): + with patch("requests.get", side_effect=requests.exceptions.Timeout) as get_mock: + session = {"token_type": "Bearer", "access_token": "abc123"} + result = hub_model.fetch_user_configs(session) + assert result is None + get_mock.assert_called_once_with( + url=hub_model.BASE_URL + "terminal/user", + headers={"Authorization": "Bearer abc123"}, + timeout=hub_model.TIMEOUT, + ) + + +def test_fetch_user_configs_exception(): + with patch("requests.get", side_effect=Exception) as get_mock: + session = {"token_type": "Bearer", "access_token": "abc123"} + result = hub_model.fetch_user_configs(session) + assert result is None + get_mock.assert_called_once_with( + url=hub_model.BASE_URL + "terminal/user", + headers={"Authorization": "Bearer abc123"}, + timeout=hub_model.TIMEOUT, + ) + + +@pytest.mark.parametrize( + "key, value, type_, auth_header", + [ + ("key", "value", "keys", "auth_header"), + ("key", "value", "settings", "auth_header"), + ], +) +def test_patch_user_configs_success(key, value, type_, auth_header): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 200 + + with patch( + "openbb_terminal.session.hub_model.requests.patch", return_value=mock_response + ) as requests_patch_mock: + result = hub_model.patch_user_configs(key, value, type_, auth_header) + + assert result.status_code == mock_response.status_code + requests_patch_mock.assert_called_once() + _, kwargs = requests_patch_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": f"{auth_header}"} + assert kwargs["json"] == {"key": f"features_{type_}.{key}", "value": value} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_patch_user_configs_failure(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 400 + + with patch( + "openbb_terminal.session.hub_model.requests.patch", return_value=mock_response + ) as requests_patch_mock: + result = hub_model.patch_user_configs("key", "value", "keys", "auth_header") + + assert result.status_code == mock_response.status_code + requests_patch_mock.assert_called_once() + _, kwargs = requests_patch_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"key": "features_keys.key", "value": "value"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_patch_user_configs_connection_error(): + with patch( + "openbb_terminal.session.hub_model.requests.patch" + ) as requests_patch_mock: + requests_patch_mock.side_effect = requests.exceptions.ConnectionError() + + result = hub_model.patch_user_configs("key", "value", "keys", "auth_header") + + assert result is None + requests_patch_mock.assert_called_once() + _, kwargs = requests_patch_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"key": "features_keys.key", "value": "value"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_patch_user_configs_timeout(): + with patch( + "openbb_terminal.session.hub_model.requests.patch" + ) as requests_patch_mock: + requests_patch_mock.side_effect = requests.exceptions.Timeout() + + result = hub_model.patch_user_configs("key", "value", "keys", "auth_header") + + assert result is None + requests_patch_mock.assert_called_once() + _, kwargs = requests_patch_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"key": "features_keys.key", "value": "value"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_patch_user_configs_exception(): + with patch( + "openbb_terminal.session.hub_model.requests.patch" + ) as requests_patch_mock: + requests_patch_mock.side_effect = Exception() + + result = hub_model.patch_user_configs("key", "value", "keys", "auth_header") + + assert result is None + requests_patch_mock.assert_called_once() + _, kwargs = requests_patch_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"key": "features_keys.key", "value": "value"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_clear_user_configs_success(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 200 + + with patch( + "openbb_terminal.session.hub_model.requests.put", return_value=mock_response + ) as requests_put_mock: + result = hub_model.clear_user_configs("auth_header") + + assert result.status_code == mock_response.status_code + requests_put_mock.assert_called_once() + _, kwargs = requests_put_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"features_keys": {}, "features_settings": {}} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_clear_user_configs_failure(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 400 + + with patch( + "openbb_terminal.session.hub_model.requests.put", return_value=mock_response + ) as requests_put_mock: + result = hub_model.clear_user_configs("auth_header") + + assert result.status_code == mock_response.status_code + requests_put_mock.assert_called_once() + _, kwargs = requests_put_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"features_keys": {}, "features_settings": {}} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_clear_user_configs_timeout(): + with patch( + "openbb_terminal.session.hub_model.requests.put", + side_effect=requests.exceptions.Timeout, + ) as requests_put_mock: + result = hub_model.clear_user_configs("auth_header") + + assert result is None + requests_put_mock.assert_called_once() + _, kwargs = requests_put_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"features_keys": {}, "features_settings": {}} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_clear_user_configs_connection_error(): + with patch("openbb_terminal.session.hub_model.requests.put") as requests_put_mock: + requests_put_mock.side_effect = requests.exceptions.ConnectionError() + result = hub_model.clear_user_configs("auth_header") + + assert result is None + requests_put_mock.assert_called_once() + _, kwargs = requests_put_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"features_keys": {}, "features_settings": {}} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_clear_user_configs_exception(): + with patch( + "openbb_terminal.session.hub_model.requests.put", side_effect=Exception + ) as requests_put_mock: + result = hub_model.clear_user_configs("auth_header") + + assert result is None + requests_put_mock.assert_called_once() + _, kwargs = requests_put_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/user" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"features_keys": {}, "features_settings": {}} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_upload_routine_success(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 200 + + with patch( + "openbb_terminal.session.hub_model.requests.post", return_value=mock_response + ) as requests_post_mock: + result = hub_model.upload_routine("auth_header", "name", "routine") + + assert result.status_code == mock_response.status_code + requests_post_mock.assert_called_once() + _, kwargs = requests_post_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/script" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"name": "name", "script": "routine"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_upload_routine_failure(): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = 400 + + with patch( + "openbb_terminal.session.hub_model.requests.post", return_value=mock_response + ) as requests_post_mock: + result = hub_model.upload_routine("auth_header", "name", "routine") + + assert result.status_code == mock_response.status_code + requests_post_mock.assert_called_once() + _, kwargs = requests_post_mock.call_args + assert kwargs["url"] == hub_model.BASE_URL + "terminal/script" + assert kwargs["headers"] == {"Authorization": "auth_header"} + assert kwargs["json"] == {"name": "name", "script": "routine"} + assert kwargs["timeout"] == hub_model.TIMEOUT + + +def test_upload_routine_connection_error(): + with patch( + "openbb_terminal.session.hub_model.requests.post", + side_effect=requests.exceptions.ConnectionError, + ): + result = hub_model.upload_routine("auth_header", "name", "routine") + assert result is None + + +def test_upload_routine_timeout(): + with patch( + "openbb_terminal.session.hub_model.requests.post", + side_effect=requests.exceptions.Timeout, + ): + result = hub_model.upload_routine("auth_header", "name", "routine") + assert result is None + + +def test_upload_routine_exception(): + with patch( + "openbb_terminal.session.hub_model.requests.post", side_effect=Exception + ): + result = hub_model.upload_routine("auth_header", "name", "routine") + assert result is None From 6ea8448254fce9065e4c8a19bbf0d69ba99d64a3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 12:34:30 +0000 Subject: [PATCH 187/230] start account controller tsts --- openbb_terminal/account/account_controller.py | 8 +- .../account/test_account_controller.py | 299 ++++++++++++++++++ .../test_call_sync[other_args0-False].txt | 1 + .../test_call_sync[other_args1-True].txt | 1 + .../test_call_sync[other_args2-True].txt | 1 + .../test_call_sync[other_args3-False].txt | 1 + .../test_call_sync[other_args4-True].txt | 1 + .../test_print_help.txt | 5 + 8 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 tests/openbb_terminal/account/test_account_controller.py create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 7b3a71d385b..e83546f4e0b 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -118,7 +118,7 @@ class AccountController(BaseController): else: console.print(f"sync is {sync}, use --on or --off to change.") - def call_pull(self, _): + def call_pull(self, other_args: List[str]): """Pull data""" parser = argparse.ArgumentParser( add_help=False, @@ -126,7 +126,7 @@ class AccountController(BaseController): prog="pull", description="Pull and apply stored configurations from the cloud.", ) - ns_parser = self.parse_simple_args(parser, _) + ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: response = Hub.fetch_user_configs(User.get_session()) if response: @@ -144,7 +144,7 @@ class AccountController(BaseController): else: console.print("No changes to apply.", style="info") - def call_clear(self, _): + def call_clear(self, other_args: List[str]): """Clear data""" parser = argparse.ArgumentParser( add_help=False, @@ -152,7 +152,7 @@ class AccountController(BaseController): prog="clear", description="Clear stored configurations from the cloud.", ) - ns_parser = self.parse_simple_args(parser, _) + ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: i = console.input( "[red]This action is irreversible![/red]\n" diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py new file mode 100644 index 00000000000..4b644adebdf --- /dev/null +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -0,0 +1,299 @@ +# IMPORTATION STANDARD +import os +from unittest.mock import patch + +# IMPORTATION THIRDPARTY +import pytest + +# IMPORTATION INTERNAL +from openbb_terminal.account import account_controller + +# pylint: disable=E1101 +# pylint: disable=W0603 +# pylint: disable=E1111 + + +@pytest.fixture(scope="module") +def vcr_config(): + return { + "filter_headers": [("User-Agent", None)], + "filter_query_parameters": [ + ("period1", "MOCK_PERIOD_1"), + ("period2", "MOCK_PERIOD_2"), + ("date", "MOCK_DATE"), + ("apiKey", "MOCK_API_KEY"), + ], + } + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.parametrize( + "queue, expected", + [ + (["sync", "help"], ["help"]), + (["quit", "help"], ["help"]), + ], +) +def test_menu_with_queue(expected, mocker, queue): + path_controller = "openbb_terminal.account.account_controller" + + # MOCK SWITCH + mocker.patch( + target=f"{path_controller}.AccountController.switch", + return_value=["quit"], + ) + result_menu = account_controller.AccountController(queue=queue).menu() + + assert result_menu == expected + + +@pytest.mark.vcr(record_mode="none") +def test_menu_without_queue_completion(mocker): + path_controller = "openbb_terminal.account.account_controller" + + # ENABLE AUTO-COMPLETION : HELPER_FUNCS.MENU + mocker.patch( + target="openbb_terminal.feature_flags.USE_PROMPT_TOOLKIT", + new=True, + ) + mocker.patch( + target="openbb_terminal.parent_classes.session", + ) + mocker.patch( + target="openbb_terminal.parent_classes.session.prompt", + return_value="quit", + ) + + # DISABLE AUTO-COMPLETION : CONTROLLER.COMPLETER + mocker.patch.object( + target=account_controller.obbff, + attribute="USE_PROMPT_TOOLKIT", + new=True, + ) + mocker.patch( + target=f"{path_controller}.session", + ) + mocker.patch( + target=f"{path_controller}.session.prompt", + return_value="quit", + ) + + result_menu = account_controller.AccountController(queue=None).menu() + + assert result_menu == ["help"] + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.parametrize( + "mock_input", + ["help", "homee help", "home help", "mock"], +) +def test_menu_without_queue_sys_exit(mock_input, mocker): + path_controller = "openbb_terminal.account.account_controller" + + # DISABLE AUTO-COMPLETION + mocker.patch.object( + target=account_controller.obbff, + attribute="USE_PROMPT_TOOLKIT", + new=False, + ) + mocker.patch( + target=f"{path_controller}.session", + return_value=None, + ) + + # MOCK USER INPUT + mocker.patch("builtins.input", return_value=mock_input) + + # MOCK SWITCH + class SystemExitSideEffect: + def __init__(self): + self.first_call = True + + def __call__(self, *args, **kwargs): + if self.first_call: + self.first_call = False + raise SystemExit() + return ["quit"] + + mock_switch = mocker.Mock(side_effect=SystemExitSideEffect()) + mocker.patch( + target=f"{path_controller}.AccountController.switch", + new=mock_switch, + ) + + result_menu = account_controller.AccountController(queue=None).menu() + + assert result_menu == ["help"] + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.record_stdout +def test_print_help(): + controller = account_controller.AccountController(queue=None) + controller.print_help() + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.parametrize( + "an_input, expected_queue", + [ + ("", []), + ("/help", ["home", "help"]), + ("help/help", ["help", "help"]), + ("q", ["quit"]), + ("h", []), + ( + "r", + [ + "quit", + "reset", + "account", + ], + ), + ], +) +def test_switch(an_input, expected_queue): + controller = account_controller.AccountController(queue=None) + queue = controller.switch(an_input=an_input) + + assert queue == expected_queue + + +@pytest.mark.vcr(record_mode="none") +def test_call_cls(mocker): + mocker.patch("os.system") + controller = account_controller.AccountController(queue=None) + controller.call_cls([]) + + assert controller.queue == [] + os.system.assert_called_once_with("cls||clear") + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.parametrize( + "func, queue, expected_queue", + [ + ( + "call_exit", + [], + ["quit", "quit"], + ), + ("call_exit", ["help"], ["quit", "quit", "help"]), + ("call_home", [], ["quit"]), + ("call_help", [], []), + ("call_quit", [], ["quit"]), + ("call_quit", ["help"], ["quit", "help"]), + ( + "call_reset", + [], + [ + "quit", + "reset", + "account", + ], + ), + ( + "call_reset", + ["help"], + [ + "quit", + "reset", + "account", + "help", + ], + ), + ], +) +def test_call_func_expect_queue(expected_queue, func, queue): + controller = account_controller.AccountController(queue=queue) + result = getattr(controller, func)([]) + + assert result is None + assert controller.queue == expected_queue + + +@pytest.mark.vcr(record_mode="none") +@pytest.mark.parametrize( + "func", + [ + "call_sync", + "call_pull", + "call_clear", + ], +) +def test_call_func_no_parser(func, mocker): + # MOCK PARSE_KNOWN_ARGS_AND_WARN + mocker.patch( + target="openbb_terminal.account.account_controller.AccountController.parse_simple_args", + return_value=None, + ) + controller = account_controller.AccountController(queue=None) + + func_result = getattr(controller, func)(other_args=list()) + assert func_result is None + assert controller.queue == [] + controller.parse_simple_args.assert_called_once() + + +@pytest.mark.vcr +@pytest.mark.record_stdout +@pytest.mark.parametrize( + "other_args, sync", + [ + ( + [ + "--on", + ], + False, + ), + ( + [ + "--off", + ], + True, + ), + ( + [ + "--on", + ], + True, + ), + ( + [ + "--off", + ], + False, + ), + ( + [], + True, + ), + ], +) +def test_call_sync(mocker, other_args, sync): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + # MOCK SWITCH + mock_set_obbff = mocker.patch( + target=f"{path_controller}.FeatureFlagsController.set_feature_flag" + ) + mocker.patch( + target=f"{path_controller}.obbff.SYNC_ENABLED", + new=sync, + ) + controller.call_sync(other_args=other_args) + + if not other_args: + mock_set_obbff.assert_not_called() + elif other_args[0] == "--on" and sync: + mock_set_obbff.assert_not_called() + elif other_args[0] == "--off" and not sync: + mock_set_obbff.assert_not_called() + elif other_args[0] in ["--on", "--off"]: + mock_set_obbff.assert_called_once_with( + "OPENBB_SYNC_ENABLED", not sync, force=True + ) + + assert controller.queue == [] diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt new file mode 100644 index 00000000000..4edd4cdac07 --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args0-False].txt @@ -0,0 +1 @@ +[info]sync:[/info] OFF diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt new file mode 100644 index 00000000000..2be0167f17b --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args1-True].txt @@ -0,0 +1 @@ +[info]sync:[/info] ON diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt new file mode 100644 index 00000000000..2be0167f17b --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args2-True].txt @@ -0,0 +1 @@ +[info]sync:[/info] ON diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt new file mode 100644 index 00000000000..4edd4cdac07 --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args3-False].txt @@ -0,0 +1 @@ +[info]sync:[/info] OFF diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt new file mode 100644 index 00000000000..e7dcaab864b --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_sync[other_args4-True].txt @@ -0,0 +1 @@ +sync is ON, use --on or --off to change. diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt b/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt new file mode 100644 index 00000000000..7ccbf387561 --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt @@ -0,0 +1,5 @@ +Cloud storage of keys, settings and feature flags: + sync Turns the cloud synchronization on/off + pull Pull data from cloud + clear Clear data from cloud + From c1bef1fad694a355c3e71076142d9600d4d56f9d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 12:35:25 +0000 Subject: [PATCH 188/230] unused import --- tests/openbb_terminal/account/test_account_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index 4b644adebdf..828d65103e5 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -1,6 +1,5 @@ # IMPORTATION STANDARD import os -from unittest.mock import patch # IMPORTATION THIRDPARTY import pytest From bbe50ee1617682c14df3765a019fde90e0544de9 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 13:08:43 +0000 Subject: [PATCH 189/230] tests account controller: clear, pull --- openbb_terminal/account/account_controller.py | 8 +- .../account/test_account_controller.py | 102 +++++++++++++++++- .../session/test_session_model.py | 16 ++- 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index e83546f4e0b..bda3237b090 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -138,11 +138,11 @@ class AccountController(BaseController): ) if i.lower() in ["y", "yes"]: Local.apply_configs(configs=configs_diff) - console.print("\nDone.", style="info") + console.print("\n[info]Done.[/info]") else: - console.print("\nAborted.", style="info") + console.print("\n[info]Aborted.[/info]") else: - console.print("No changes to apply.", style="info") + console.print("\n[info]No changes to apply.[/info]") def call_clear(self, other_args: List[str]): """Clear data""" @@ -162,7 +162,7 @@ class AccountController(BaseController): console.print("") Hub.clear_user_configs(auth_header=User.get_auth_header()) else: - console.print("\nAborted.", style="info") + console.print("\n[info]Aborted.[/info]") # @log_start_end(log=logger) # def call_upload(self, other_args: List[str]): diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index 828d65103e5..939ad0f581c 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -1,8 +1,10 @@ # IMPORTATION STANDARD import os +import json # IMPORTATION THIRDPARTY import pytest +from requests import Response # IMPORTATION INTERNAL from openbb_terminal.account import account_controller @@ -11,6 +13,26 @@ from openbb_terminal.account import account_controller # pylint: disable=W0603 # pylint: disable=E1111 +TEST_SESSION = { + "access_token": "test_token", + "token_type": "bearer", + "uuid": "test_uuid", +} + +CONFIGS = { + "features_settings": { + "USE_WATERMARK": "False", + "TIMEZONE": "Europe/London", + "PLOT_DPI": "95", + "PLOT_HEIGHT_PERCENTAGE": "50.5", + "USER_DATA_DIRECTORY": "some/path/to/user/data", + }, + "features_keys": { + "API_KEY_ALPHAVANTAGE": "test_av", + "API_FRED_KEY": "test_fred", + }, +} + @pytest.fixture(scope="module") def vcr_config(): @@ -274,7 +296,6 @@ def test_call_sync(mocker, other_args, sync): controller = account_controller.AccountController(queue=None) path_controller = "openbb_terminal.account.account_controller" - # MOCK SWITCH mock_set_obbff = mocker.patch( target=f"{path_controller}.FeatureFlagsController.set_feature_flag" ) @@ -296,3 +317,82 @@ def test_call_sync(mocker, other_args, sync): ) assert controller.queue == [] + + +@pytest.mark.parametrize( + "input_value", + [ + "y", + "n", + ], +) +def test_call_pull(mocker, input_value): + + DIFF = {"TIMEZONE": "Europe/London"} + + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + response = Response() + response.status_code = 200 + response._content = json.dumps(CONFIGS) # pylint: disable=protected-access + + mock_fetch_user_configs = mocker.patch( + target=f"{path_controller}.Hub.fetch_user_configs", + return_value=response, + ) + mock_get_session = mocker.patch( + target=f"{path_controller}.User.get_session", + return_value=TEST_SESSION, + ) + mock_get_diff = mocker.patch( + target=f"{path_controller}.get_diff", + return_value=DIFF, + ) + mock_input = mocker.patch( + target=f"{path_controller}.console.input", + return_value=input_value, + ) + mock_apply_configs = mocker.patch( + target=f"{path_controller}.Local.apply_configs", + ) + controller.call_pull(other_args=list()) + + mock_fetch_user_configs.assert_called_once_with(TEST_SESSION) + mock_get_session.assert_called_once() + mock_get_diff.assert_called_once_with(configs=CONFIGS) + mock_input.assert_called_once() + if input_value == "y": + mock_apply_configs.assert_called_once_with(configs=DIFF) + else: + mock_apply_configs.assert_not_called() + assert controller.queue == [] + + +@pytest.mark.parametrize( + "input_value", + [ + "y", + "n", + ], +) +def test_call_clear(mocker, input_value): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + mock_input = mocker.patch( + target=f"{path_controller}.console.input", + return_value=input_value, + ) + mock_clear_user_configs = mocker.patch( + target=f"{path_controller}.Hub.clear_user_configs", + ) + controller.call_clear(other_args=list()) + + mock_input.assert_called_once() + + if input_value == "y": + mock_clear_user_configs.assert_called_once() + else: + mock_clear_user_configs.assert_not_called() + assert controller.queue == [] diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index bda3bec6ff9..891f94ca8b1 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -16,6 +16,20 @@ TEST_SESSION = { "uuid": "test_uuid", } +CONFIGS = { + "features_settings": { + "USE_WATERMARK": "False", + "TIMEZONE": "Europe/London", + "PLOT_DPI": "95", + "PLOT_HEIGHT_PERCENTAGE": "50.5", + "USER_DATA_DIRECTORY": "some/path/to/user/data", + }, + "features_keys": { + "API_KEY_ALPHAVANTAGE": "test_av", + "API_FRED_KEY": "test_fred", + }, +} + @pytest.mark.parametrize( "email, password, save", @@ -98,7 +112,7 @@ def test_login_success_response(): ): response = Response() response.status_code = 200 - response._content = json.dumps(TEST_SESSION) # pylint: disable=protected-access + response._content = json.dumps(CONFIGS) # pylint: disable=protected-access mock_fetch_user_configs.return_value = response assert session_model.login(TEST_SESSION) == session_model.LoginStatus.SUCCESS From 4622e30ce831d87a3dce711127eff2eb0d6b3ed7 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 3 Feb 2023 13:14:55 +0000 Subject: [PATCH 190/230] tests for session_controller --- openbb_terminal/session/session_controller.py | 2 +- .../session/test_session_controller.py | 8 ++++++++ .../test_display_welcome_message.txt | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/openbb_terminal/session/test_session_controller.py create mode 100644 tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 24ee76eef17..66a670374a7 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -14,7 +14,7 @@ from openbb_terminal.rich_config import console def display_welcome_message(): """Display welcome message""" with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: - console.print(f.read(), style="menu") + console.print(f"[menu]{f.read()}[/menu]") def get_user_input() -> Tuple[str, str, bool]: diff --git a/tests/openbb_terminal/session/test_session_controller.py b/tests/openbb_terminal/session/test_session_controller.py new file mode 100644 index 00000000000..395379f5918 --- /dev/null +++ b/tests/openbb_terminal/session/test_session_controller.py @@ -0,0 +1,8 @@ +import pytest + +from openbb_terminal.session import session_controller + + +@pytest.mark.record_stdout +def test_display_welcome_message(): + session_controller.display_welcome_message() diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt new file mode 100644 index 00000000000..42f658c8f03 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt @@ -0,0 +1,19 @@ +[menu] ___ ____ ____ _____ _ _ + / _ \ _ __ ___ _ __ | __ )| __ )_ _|__ _ __ _ __ ___ (_)_ __ __ _| | +| | | | '_ \ / _ \ '_ \| _ \| _ \ | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | | +| |_| | |_) | __/ | | | |_) | |_) || | __/ | | | | | | | | | | | (_| | | + \___/| .__/ \___|_| |_|____/|____/ |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_| + |_| + @@@ + @@@ + @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ + @@@ @@@ @@@ @@@ + @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ + @@@ @@@ + %%%%%%%%%%%%%%%%%@@@ @@@%%%%%%%%%%%%%%%%% + @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ + @@@ @@@ @@@ @@@ + @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ + + Investment research for Everyone, Anywhere. +[/menu] From 66450d143ac09c9636e11efda84ddc3beecad40a Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 13:31:04 +0000 Subject: [PATCH 191/230] logout success bugfix --- openbb_terminal/session/session_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index 6d9cb350908..a4e356cabe5 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -92,7 +92,7 @@ def logout( return r = Hub.delete_session(auth_header, token) - if r and r.status_code != 200: + if not r or r.status_code != 200: success = False User.clear() From 4bd6a53865ff1a0f945517c9a6020b1468c05a18 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 13:34:45 +0000 Subject: [PATCH 192/230] add integration test for account --- .../core/integration_tests/integration_controller.py | 3 +-- .../integration_tests_scripts/account/test_account.openbb | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 openbb_terminal/miscellaneous/integration_tests_scripts/account/test_account.openbb diff --git a/openbb_terminal/core/integration_tests/integration_controller.py b/openbb_terminal/core/integration_tests/integration_controller.py index e0b2619d7f3..6ae2da57d1b 100644 --- a/openbb_terminal/core/integration_tests/integration_controller.py +++ b/openbb_terminal/core/integration_tests/integration_controller.py @@ -370,7 +370,6 @@ def run_test_files( ------- Tuple[int, int, Dict[str, Dict[str, Any]], float] """ - os.environ["DEBUG_MODE"] = "true" n_successes = 0 n_failures = 0 fails: Dict[str, Dict[str, Any]] = {} @@ -689,10 +688,10 @@ def main(): if "--test" in sys.argv: sys.argv.remove("--test") - os.environ["OPENBB_ENABLE_QUICK_EXIT"] = "True" os.environ["OPENBB_LOG_COLLECT"] = "False" os.environ["OPENBB_USE_ION"] = "False" os.environ["OPENBB_USE_PROMPT_TOOLKIT"] = "False" + os.environ["DEBUG_MODE"] = "true" parse_args_and_run() diff --git a/openbb_terminal/miscellaneous/integration_tests_scripts/account/test_account.openbb b/openbb_terminal/miscellaneous/integration_tests_scripts/account/test_account.openbb new file mode 100644 index 00000000000..c7b2dcef337 --- /dev/null +++ b/openbb_terminal/miscellaneous/integration_tests_scripts/account/test_account.openbb @@ -0,0 +1,7 @@ +# GUEST + +whoami +account + +# AUTHENTICATED USER +# Not implemented From c10635f4d527b9c8ae307335b3ad031b5dc5e39d Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 3 Feb 2023 15:02:59 +0000 Subject: [PATCH 193/230] black --- openbb_terminal/core/library/operation.py | 1 - tests/openbb_terminal/account/test_account_controller.py | 1 - tests/openbb_terminal/session/test_sdk_session.py | 5 ++--- tests/openbb_terminal/session/test_session_model.py | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openbb_terminal/core/library/operation.py b/openbb_terminal/core/library/operation.py index c6e2e3747b2..991403e6c63 100644 --- a/openbb_terminal/core/library/operation.py +++ b/openbb_terminal/core/library/operation.py @@ -230,7 +230,6 @@ class OperationLogger: """ if func_module == "openbb_terminal.keys_model": - from openbb_terminal.core.log.generation.settings_logger import ( # pylint: disable=import-outside-toplevel log_keys, ) diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index 939ad0f581c..9832a4ba8ba 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -327,7 +327,6 @@ def test_call_sync(mocker, other_args, sync): ], ) def test_call_pull(mocker, input_value): - DIFF = {"TIMEZONE": "Europe/London"} controller = account_controller.AccountController(queue=None) diff --git a/tests/openbb_terminal/session/test_sdk_session.py b/tests/openbb_terminal/session/test_sdk_session.py index fab412dc5f2..52f96ae63a0 100644 --- a/tests/openbb_terminal/session/test_sdk_session.py +++ b/tests/openbb_terminal/session/test_sdk_session.py @@ -28,7 +28,7 @@ TEST_SESSION = { ) def test_get_session_exception(email: str, password: str, save: bool): path = "openbb_terminal.session.sdk_session." - with (patch(path + "session_model.create_session") as mock_create_session): + with patch(path + "session_model.create_session") as mock_create_session: mock_create_session.return_value = {} with pytest.raises(Exception): @@ -49,7 +49,7 @@ def test_get_session_exception(email: str, password: str, save: bool): ) def test_get_session(email: str, password: str, save: bool): path = "openbb_terminal.session.sdk_session." - with (patch(path + "session_model.create_session") as mock_create_session): + with patch(path + "session_model.create_session") as mock_create_session: mock_create_session.return_value = TEST_SESSION sdk_session.get_session(email=email, password=password, save=save) @@ -102,7 +102,6 @@ def test_login( patch(path + "Local.get_session") as mock_local_get_session, patch(path + "get_session") as mock_hub_get_session, ): - if has_session: mock_local_get_session.return_value = TEST_SESSION else: diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index 891f94ca8b1..1fb4a4b402f 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -141,7 +141,6 @@ def test_logout_user(guest): patch(path + "User.clear") as mock_clear_user, patch(path + "plt.close") as mock_plt_close, ): - auth_header = "Bearer test_token" token = "test_token" session_model.logout(auth_header, token, guest) From 7b18aee739eb66571666d62af9157fe9bcb4cc78 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Fri, 3 Feb 2023 16:14:55 +0000 Subject: [PATCH 194/230] move routine related commands to other PR --- openbb_terminal/account/account_controller.py | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index bda3237b090..224eee9c469 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -17,10 +17,6 @@ from openbb_terminal.session import local_model as Local from openbb_terminal.session.user import User from openbb_terminal.account.account_model import get_diff -# from openbb_terminal.session.user import User - -# from openbb_terminal.session import hub_model as Hub -# from openbb_terminal.session import local_model as Local logger = logging.getLogger(__name__) @@ -32,8 +28,6 @@ class AccountController(BaseController): "sync", "pull", "clear", - # "upload", - # "download", ] PATH = "/account/" @@ -54,9 +48,6 @@ class AccountController(BaseController): if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} choices["sync"] = {"--on": {}, "--off": {}} - # choices["upload"]["--file"] = self.ROUTINE_CHOICES - # choices["upload"]["-f"] = choices["upload"]["--file"] - self.completer = NestedCompleter.from_nested_dict(choices) def print_help(self): @@ -67,9 +58,6 @@ class AccountController(BaseController): mt.add_cmd("sync") mt.add_cmd("pull") mt.add_cmd("clear") - # mt.add_info("_routines_") - # mt.add_cmd("upload") - # mt.add_cmd("download") console.print(text=mt.menu_text, menu="Account") @log_start_end(log=logger) @@ -163,43 +151,3 @@ class AccountController(BaseController): Hub.clear_user_configs(auth_header=User.get_auth_header()) else: console.print("\n[info]Aborted.[/info]") - - # @log_start_end(log=logger) - # def call_upload(self, other_args: List[str]): - # """Upload""" - # parser = argparse.ArgumentParser( - # add_help=False, - # formatter_class=argparse.ArgumentDefaultsHelpFormatter, - # prog="load", - # description="Load your portfolio transactions.", - # ) - # parser.add_argument( - # "-f", - # "--file", - # type=str, - # dest="file", - # required="-h" not in other_args, - # help="The file to be loaded", - # choices={c: {} for c in self.ROUTINE_FILES}, - # metavar="FILE", - # ) - # parser.add_argument( - # "-n", - # "--name", - # type=str, - # dest="name", - # required="-h" not in other_args, - # help="The name of the routine", - # ) - # if other_args and "-" not in other_args[0][0]: - # other_args.insert(0, "-f") - # ns_parser = self.parse_known_args_and_warn(parser, other_args) - # if ns_parser: - # routine = Local.get_routine(ns_parser.file) - # if routine: - # Hub.upload_routine(ns_parser.name, routine) - - # @log_start_end(log=logger) - # def call_download(self, _): - # """Download""" - # Hub.download_routine() From 2b00f5647c89b1843f2200703e2a3a9ed4ad0df0 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 3 Feb 2023 17:28:33 +0000 Subject: [PATCH 195/230] test for the user input --- openbb_terminal/session/session_controller.py | 2 +- .../session/test_session_controller.py | 27 +++++++++++++++++++ .../test_get_user_input.txt | 2 ++ .../test_get_user_input_with_save.txt | 2 ++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt create mode 100644 tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 66a670374a7..6c71963d339 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -25,7 +25,7 @@ def get_user_input() -> Tuple[str, str, bool]: Tuple[str, str, bool] The user email, password and save login option. """ - console.print("\nPlease enter your credentials:", style="info") + console.print("[info]\nPlease enter your credentials:[/info]") s: PromptSession = PromptSession() diff --git a/tests/openbb_terminal/session/test_session_controller.py b/tests/openbb_terminal/session/test_session_controller.py index 395379f5918..4f0598fa8f1 100644 --- a/tests/openbb_terminal/session/test_session_controller.py +++ b/tests/openbb_terminal/session/test_session_controller.py @@ -1,3 +1,4 @@ +from unittest.mock import patch import pytest from openbb_terminal.session import session_controller @@ -6,3 +7,29 @@ from openbb_terminal.session import session_controller @pytest.mark.record_stdout def test_display_welcome_message(): session_controller.display_welcome_message() + + +@pytest.mark.record_stdout +@patch("openbb_terminal.session.session_controller.PromptSession") +def test_get_user_input(mock_prompt_session): + # Arrange + mock_prompt_session().prompt.side_effect = ["test@example.com", "password", "n"] + + # Act + result = session_controller.get_user_input() + + # Assert + assert result == ("test@example.com", "password", False) + + +@pytest.mark.record_stdout +@patch("openbb_terminal.session.session_controller.PromptSession") +def test_get_user_input_with_save(mock_prompt_session): + # Arrange + mock_prompt_session().prompt.side_effect = ["test@example.com", "password", "y"] + + # Act + result = session_controller.get_user_input() + + # Assert + assert result == ("test@example.com", "password", True) diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt new file mode 100644 index 00000000000..5b74d9dff68 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt @@ -0,0 +1,2 @@ +[info] +Please enter your credentials:[/info] diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt new file mode 100644 index 00000000000..5b74d9dff68 --- /dev/null +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt @@ -0,0 +1,2 @@ +[info] +Please enter your credentials:[/info] From f90bf7ea35a3a9c36842ccc44fe1146085b3cd6d Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Fri, 3 Feb 2023 18:14:59 +0000 Subject: [PATCH 196/230] completed tests for session controller --- .../session/test_session_controller.py | 107 +++++++++++++++++- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/tests/openbb_terminal/session/test_session_controller.py b/tests/openbb_terminal/session/test_session_controller.py index 4f0598fa8f1..15cf50898c3 100644 --- a/tests/openbb_terminal/session/test_session_controller.py +++ b/tests/openbb_terminal/session/test_session_controller.py @@ -12,24 +12,119 @@ def test_display_welcome_message(): @pytest.mark.record_stdout @patch("openbb_terminal.session.session_controller.PromptSession") def test_get_user_input(mock_prompt_session): - # Arrange mock_prompt_session().prompt.side_effect = ["test@example.com", "password", "n"] - # Act result = session_controller.get_user_input() - # Assert assert result == ("test@example.com", "password", False) @pytest.mark.record_stdout @patch("openbb_terminal.session.session_controller.PromptSession") def test_get_user_input_with_save(mock_prompt_session): - # Arrange mock_prompt_session().prompt.side_effect = ["test@example.com", "password", "y"] - # Act result = session_controller.get_user_input() - # Assert assert result == ("test@example.com", "password", True) + + +def test_prompt(): + + with patch( + "openbb_terminal.session.session_controller.get_user_input", + return_value=("email", "password", False), + ) as get_user_input_mock, patch( + "openbb_terminal.session.session_controller.create_session", + return_value={"session": "session"}, + ) as create_session_mock, patch( + "openbb_terminal.session.session_controller.login_and_launch", + return_value=True, + ): + + session_controller.prompt() + assert get_user_input_mock.call_count == 1 + create_session_mock.assert_called_once_with("email", "password", False) + assert create_session_mock.call_count == 1 + + +def test_prompt_guest_allowed(): + + with patch( + "openbb_terminal.session.session_controller.get_user_input", + return_value=(None, None, False), + ) as get_user_input_mock, patch( + "openbb_terminal.session.session_controller.launch_terminal", return_value=True + ) as launch_terminal_mock: + session_controller.prompt(guest_allowed=True) + assert get_user_input_mock.call_count == 1 + assert launch_terminal_mock.call_count == 1 + + +def test_launch_terminal(): + with patch( + "openbb_terminal.session.session_controller.terminal_controller.parse_args_and_run", + return_value=True, + ) as parse_args_mock: + session_controller.launch_terminal() + assert parse_args_mock.call_count == 1 + + +def test_login_and_launch(): + with patch( + "openbb_terminal.session.session_controller.login", + return_value=session_controller.LoginStatus.SUCCESS, + ) as login_mock, patch( + "openbb_terminal.session.session_controller.launch_terminal", return_value=True + ) as launch_terminal_mock: + session_controller.login_and_launch(session={}) + assert login_mock.call_count == 1 + assert launch_terminal_mock.call_count == 1 + + +def test_login_and_launch_failed(): + with patch( + "openbb_terminal.session.session_controller.login", + return_value=session_controller.LoginStatus.FAILED, + ) as login_mock, patch( + "openbb_terminal.session.session_controller.prompt", return_value=True + ) as prompt_mock: + session_controller.login_and_launch(session={}) + assert login_mock.call_count == 1 + assert prompt_mock.call_count == 1 + + +def test_login_and_launch_no_response(): + with patch( + "openbb_terminal.session.session_controller.login", + return_value=session_controller.LoginStatus.NO_RESPONSE, + ) as login_mock, patch( + "openbb_terminal.session.session_controller.prompt", return_value=True + ) as prompt_mock: + session_controller.login_and_launch(session={}) + assert login_mock.call_count == 1 + assert prompt_mock.call_count == 1 + + +def test_main_local_session(): + with patch( + "openbb_terminal.session.session_controller.prompt", return_value=True + ) as prompt_mock, patch( + "openbb_terminal.session.session_controller.Local.get_session", + return_value=False, + ) as get_session_mock: + session_controller.main() + assert prompt_mock.call_count == 1 + assert get_session_mock.call_count == 1 + + +def test_main_no_local_session(): + with patch( + "openbb_terminal.session.session_controller.login_and_launch", return_value=True + ) as login_and_launch_mock, patch( + "openbb_terminal.session.session_controller.Local.get_session", + return_value=True, + ) as get_session_mock: + session_controller.main() + assert login_and_launch_mock.call_count == 1 + assert get_session_mock.call_count == 1 From 144af332ce6c6743bb8cab2fea458207fac01568 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 19:14:44 +0000 Subject: [PATCH 197/230] convert to hist to str) --- openbb_terminal/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/menu.py b/openbb_terminal/menu.py index 413540c177e..1d97b9184e4 100644 --- a/openbb_terminal/menu.py +++ b/openbb_terminal/menu.py @@ -51,7 +51,7 @@ def inputhook(inputhook_context): try: if not is_jupyter() and not is_papermill(): session: Optional[PromptSession] = PromptSession( - history=FileHistory(HIST_FILE_PATH) + history=FileHistory(str(HIST_FILE_PATH)) ) set_eventloop_with_inputhook(inputhook) else: From 33f00cac6b23632f5e3f9c199e2c215069615974 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 19:28:42 +0000 Subject: [PATCH 198/230] fix import errors --- openbb_terminal/helper_funcs.py | 5 +---- openbb_terminal/keys_model.py | 3 --- openbb_terminal/terminal_controller.py | 14 +++++--------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index ff3edf9ca32..09e8fef2957 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -20,8 +20,6 @@ from functools import lru_cache from pathlib import Path from typing import Dict, List, Optional, Tuple, Union -import pytz -import pandas as pd from rich.table import Table import iso8601 import matplotlib @@ -36,13 +34,12 @@ from holidays import US as us_holidays from pandas._config.config import get_option from pandas.plotting import register_matplotlib_converters from PIL import Image, ImageDraw -from rich.table import Table from screeninfo import get_monitors from openbb_terminal import config_plot as cfgPlot from openbb_terminal import config_terminal as cfg from openbb_terminal import feature_flags as obbff -from openbb_terminal.core.config import paths +from openbb_terminal.rich_config import console from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, USER_EXPORTS_DIRECTORY, diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 544101ecae1..5b67cbbb62b 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -33,14 +33,11 @@ from openbb_terminal.cryptocurrency.coinbase_helpers import ( CoinbaseProAuth, make_coinbase_request, ) -from openbb_terminal import config_terminal as cfg -from openbb_terminal.core.config.paths import USER_ENV_FILE from openbb_terminal.rich_config import console from openbb_terminal.session.hub_model import patch_user_configs from openbb_terminal.session.user import User from openbb_terminal.helper_funcs import request from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel -from openbb_terminal.rich_config import console from openbb_terminal.terminal_helper import suppress_stdout logger = logging.getLogger(__name__) diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 0768fe469d8..15f36c2f8c9 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -21,13 +21,12 @@ import pandas as pd from prompt_toolkit import PromptSession from prompt_toolkit.completion import NestedCompleter from prompt_toolkit.formatted_text import HTML -import pandas as pd -from openbb_terminal import feature_flags as obbff -from openbb_terminal.session.user import User -from openbb_terminal.terminal_helper import is_installer, is_packaged_application -from openbb_terminal.session import session_controller +from prompt_toolkit.styles import Style +from rich import panel from openbb_terminal import feature_flags as obbff +from openbb_terminal.session.user import User +from openbb_terminal.session import session_controller from openbb_terminal.common import feedparser_view from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, @@ -38,7 +37,6 @@ from openbb_terminal.core.config.paths import ( USER_ROUTINES_DIRECTORY, load_dotenv_with_priority, ) -from openbb_terminal.core.log.generation.settings_logger import log_all_settings from openbb_terminal.helper_funcs import ( EXPORT_ONLY_RAW_DATA_ALLOWED, check_positive, @@ -59,11 +57,9 @@ from openbb_terminal.terminal_helper import ( suppress_stdout, update_terminal, welcome_message, + is_installer, ) -from openbb_terminal.helper_funcs import parse_and_split_input from openbb_terminal.keys_model import first_time_user -from openbb_terminal.common import feedparser_view -from openbb_terminal.reports.reports_model import ipykernel_launcher from openbb_terminal.core.log.generation.custom_logger import log_terminal # pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args From c7342cd43d2594406af409eb5d7a6e74b2daf7c7 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 19:29:48 +0000 Subject: [PATCH 199/230] ruff --- openbb_terminal/account/account_controller.py | 16 ++++++++-------- openbb_terminal/account/account_model.py | 11 ++++++----- openbb_terminal/core/log/generation/common.py | 2 -- .../core/log/generation/custom_logger.py | 8 +++----- .../core/log/generation/settings_logger.py | 3 +-- .../core/log/generation/user_logger.py | 5 ++--- openbb_terminal/featflags_controller.py | 2 +- openbb_terminal/helper_funcs.py | 4 ++-- openbb_terminal/keys_controller.py | 4 ++-- openbb_terminal/keys_model.py | 4 ++-- openbb_terminal/menu.py | 2 +- openbb_terminal/parent_classes.py | 4 ++-- openbb_terminal/session/hub_model.py | 1 + openbb_terminal/session/local_model.py | 12 ++++++------ openbb_terminal/session/sdk_session.py | 2 +- openbb_terminal/session/session_controller.py | 8 +++++--- openbb_terminal/session/session_model.py | 8 +++++--- openbb_terminal/settings_controller.py | 7 ++++--- openbb_terminal/terminal_controller.py | 14 +++++++------- 19 files changed, 59 insertions(+), 58 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 224eee9c469..836a5e636ae 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,22 +1,22 @@ import argparse import json import logging -from typing import List, Dict from pathlib import Path +from typing import Dict, List from prompt_toolkit.completion import NestedCompleter -from openbb_terminal.featflags_controller import FeatureFlagsController -from openbb_terminal.parent_classes import BaseController -from openbb_terminal.rich_config import console, MenuText -from openbb_terminal.decorators import log_start_end -from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY + from openbb_terminal import feature_flags as obbff +from openbb_terminal.account.account_model import get_diff +from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY +from openbb_terminal.decorators import log_start_end +from openbb_terminal.featflags_controller import FeatureFlagsController from openbb_terminal.menu import session +from openbb_terminal.parent_classes import BaseController +from openbb_terminal.rich_config import MenuText, console from openbb_terminal.session import hub_model as Hub from openbb_terminal.session import local_model as Local from openbb_terminal.session.user import User -from openbb_terminal.account.account_model import get_diff - logger = logging.getLogger(__name__) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 33c612ab30b..645313b788a 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,11 +1,12 @@ from pathlib import Path -from typing import Dict, Any +from typing import Any, Dict + +from openbb_terminal import config_plot as cfg_plot +from openbb_terminal import config_terminal as cfg +from openbb_terminal import feature_flags as obbff +from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config import paths from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags as obbff -from openbb_terminal import config_terminal as cfg -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal.base_helpers import strtobool def get_diff(configs: dict) -> dict: diff --git a/openbb_terminal/core/log/generation/common.py b/openbb_terminal/core/log/generation/common.py index f2a10557370..aff2c1f6fe6 100644 --- a/openbb_terminal/core/log/generation/common.py +++ b/openbb_terminal/core/log/generation/common.py @@ -2,8 +2,6 @@ import logging # IMPORTATION THIRDPARTY - - # IMPORTATION INTERNAL from openbb_terminal.core.log.generation.path_tracking_file_handler import ( PathTrackingFileHandler, diff --git a/openbb_terminal/core/log/generation/custom_logger.py b/openbb_terminal/core/log/generation/custom_logger.py index 42c7682a2a0..e27a3dd7741 100644 --- a/openbb_terminal/core/log/generation/custom_logger.py +++ b/openbb_terminal/core/log/generation/custom_logger.py @@ -1,14 +1,12 @@ # IMPORTATION STANDARD import logging +from openbb_terminal.core.log.generation import settings_logger, user_logger +from openbb_terminal.core.log.generation.common import do_rollover + # IMPORTATION THIRDPARTY - - # IMPORTATION INTERNAL from openbb_terminal.loggers import setup_logging -from openbb_terminal.core.log.generation import settings_logger -from openbb_terminal.core.log.generation import user_logger -from openbb_terminal.core.log.generation.common import do_rollover logger = logging.getLogger(__name__) diff --git a/openbb_terminal/core/log/generation/settings_logger.py b/openbb_terminal/core/log/generation/settings_logger.py index fdfa79460ba..7b6f97fbeb4 100644 --- a/openbb_terminal/core/log/generation/settings_logger.py +++ b/openbb_terminal/core/log/generation/settings_logger.py @@ -8,9 +8,8 @@ from types import FunctionType, ModuleType # IMPORTATION INTERNAL import openbb_terminal.feature_flags as obbff from openbb_terminal import config_terminal as cfg -from openbb_terminal.terminal_helper import is_packaged_application from openbb_terminal.core.log.generation.common import do_rollover - +from openbb_terminal.terminal_helper import is_packaged_application SENSITIVE_WORDS = [ "API", diff --git a/openbb_terminal/core/log/generation/user_logger.py b/openbb_terminal/core/log/generation/user_logger.py index acda5a0e8b1..635af0b83f9 100644 --- a/openbb_terminal/core/log/generation/user_logger.py +++ b/openbb_terminal/core/log/generation/user_logger.py @@ -2,12 +2,11 @@ import json import logging +from openbb_terminal.core.log.generation.common import do_rollover + # IMPORTATION THIRDPARTY - - # IMPORTATION INTERNAL from openbb_terminal.session.user import User -from openbb_terminal.core.log.generation.common import do_rollover logger = logging.getLogger(__name__) diff --git a/openbb_terminal/featflags_controller.py b/openbb_terminal/featflags_controller.py index 3f0f4b7711b..c80b86a7ac8 100644 --- a/openbb_terminal/featflags_controller.py +++ b/openbb_terminal/featflags_controller.py @@ -15,7 +15,7 @@ from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController -from openbb_terminal.rich_config import console, MenuText +from openbb_terminal.rich_config import MenuText, console from openbb_terminal.session.hub_model import patch_user_configs from openbb_terminal.session.user import User diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index 09e8fef2957..052838f2a6b 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -20,7 +20,6 @@ from functools import lru_cache from pathlib import Path from typing import Dict, List, Optional, Tuple, Union -from rich.table import Table import iso8601 import matplotlib import matplotlib.pyplot as plt @@ -34,16 +33,17 @@ from holidays import US as us_holidays from pandas._config.config import get_option from pandas.plotting import register_matplotlib_converters from PIL import Image, ImageDraw +from rich.table import Table from screeninfo import get_monitors from openbb_terminal import config_plot as cfgPlot from openbb_terminal import config_terminal as cfg from openbb_terminal import feature_flags as obbff -from openbb_terminal.rich_config import console from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, USER_EXPORTS_DIRECTORY, ) +from openbb_terminal.rich_config import console logger = logging.getLogger(__name__) diff --git a/openbb_terminal/keys_controller.py b/openbb_terminal/keys_controller.py index 968e6c32ccf..e8ce84b6010 100644 --- a/openbb_terminal/keys_controller.py +++ b/openbb_terminal/keys_controller.py @@ -9,8 +9,8 @@ from typing import Dict, List from tqdm import tqdm -from openbb_terminal import feature_flags as obbff, keys_view -from openbb_terminal import keys_model +from openbb_terminal import feature_flags as obbff +from openbb_terminal import keys_model, keys_view from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import EXPORT_ONLY_RAW_DATA_ALLOWED diff --git a/openbb_terminal/keys_model.py b/openbb_terminal/keys_model.py index 5b67cbbb62b..b4630a60497 100644 --- a/openbb_terminal/keys_model.py +++ b/openbb_terminal/keys_model.py @@ -33,11 +33,11 @@ from openbb_terminal.cryptocurrency.coinbase_helpers import ( CoinbaseProAuth, make_coinbase_request, ) +from openbb_terminal.helper_funcs import request +from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel from openbb_terminal.rich_config import console from openbb_terminal.session.hub_model import patch_user_configs from openbb_terminal.session.user import User -from openbb_terminal.helper_funcs import request -from openbb_terminal.portfolio.brokers.degiro.degiro_model import DegiroModel from openbb_terminal.terminal_helper import suppress_stdout logger = logging.getLogger(__name__) diff --git a/openbb_terminal/menu.py b/openbb_terminal/menu.py index 1d97b9184e4..549a1ef687b 100644 --- a/openbb_terminal/menu.py +++ b/openbb_terminal/menu.py @@ -1,6 +1,6 @@ +import logging import sys from typing import Optional -import logging from matplotlib import pyplot from prompt_toolkit import PromptSession diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 7594970dd15..1ac78bb0e9e 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -18,8 +18,6 @@ import pandas as pd from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from rich.markdown import Markdown -from openbb_terminal.session.session_model import logout -from openbb_terminal.session.user import User from openbb_terminal import feature_flags as obbff from openbb_terminal.config_terminal import theme @@ -47,6 +45,8 @@ from openbb_terminal.helper_funcs import ( ) from openbb_terminal.menu import session from openbb_terminal.rich_config import console, get_ordered_list_sources +from openbb_terminal.session.session_model import logout +from openbb_terminal.session.user import User from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 8c991173aa4..0f4d7473f14 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -1,4 +1,5 @@ from typing import Dict, Optional + import requests from openbb_terminal.rich_config import console diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index a269479dd2f..21b6676040b 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -1,7 +1,12 @@ +import json import os from pathlib import Path -import json from typing import Optional + +from openbb_terminal import config_plot as cfg_plot +from openbb_terminal import config_terminal as cfg +from openbb_terminal import feature_flags as obbff +from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config import paths from openbb_terminal.core.config.paths import ( HIST_FILE_PATH, @@ -10,11 +15,6 @@ from openbb_terminal.core.config.paths import ( ) from openbb_terminal.rich_config import console -from openbb_terminal import feature_flags as obbff -from openbb_terminal import config_terminal as cfg -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal.base_helpers import strtobool - SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py index e138cd44afd..c82a1bd3af2 100644 --- a/openbb_terminal/session/sdk_session.py +++ b/openbb_terminal/session/sdk_session.py @@ -1,6 +1,6 @@ +from openbb_terminal.rich_config import console from openbb_terminal.session import local_model as Local from openbb_terminal.session import session_model -from openbb_terminal.rich_config import console from openbb_terminal.session.user import User diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 66a670374a7..2334c3fb39c 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,14 +1,16 @@ from typing import Tuple + from prompt_toolkit import PromptSession -from openbb_terminal import terminal_controller + import openbb_terminal.session.local_model as Local +from openbb_terminal import terminal_controller +from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY +from openbb_terminal.rich_config import console from openbb_terminal.session.session_model import ( LoginStatus, create_session, login, ) -from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY -from openbb_terminal.rich_config import console def display_welcome_message(): diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index a4e356cabe5..cae5dbbe634 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -1,16 +1,18 @@ import importlib +import json import logging import os import sys -import json from enum import Enum from typing import Optional + import matplotlib.pyplot as plt -import openbb_terminal.session.local_model as Local + import openbb_terminal.session.hub_model as Hub -from openbb_terminal.session.user import User +import openbb_terminal.session.local_model as Local from openbb_terminal.helper_funcs import system_clear from openbb_terminal.rich_config import console +from openbb_terminal.session.user import User # pylint: disable=consider-using-f-string diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 286604e213e..2fbdcacb210 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -8,6 +8,7 @@ import os import os.path from pathlib import Path from typing import List, Optional, Union + import pytz # IMPORTATION THIRDPARTY @@ -15,12 +16,12 @@ from dotenv import set_key # IMPORTATION INTERNAL from openbb_terminal import config_plot as cfg_plot +from openbb_terminal import featflags_controller as obbff_ctrl from openbb_terminal import feature_flags as obbff from openbb_terminal.core.config import paths -from openbb_terminal import featflags_controller as obbff_ctrl from openbb_terminal.core.config.paths import ( - USER_ENV_FILE, USER_DATA_SOURCES_DEFAULT_FILE, + USER_ENV_FILE, ) from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end @@ -31,7 +32,7 @@ from openbb_terminal.helper_funcs import ( ) from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController -from openbb_terminal.rich_config import console, MenuText +from openbb_terminal.rich_config import MenuText, console from openbb_terminal.session.hub_model import patch_user_configs from openbb_terminal.session.user import User diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index 15f36c2f8c9..90da1ebdefb 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -5,11 +5,11 @@ __docformat__ = "numpy" import argparse import contextlib import difflib +import importlib import logging import os import re import sys -import importlib import time import webbrowser from datetime import datetime @@ -25,8 +25,6 @@ from prompt_toolkit.styles import Style from rich import panel from openbb_terminal import feature_flags as obbff -from openbb_terminal.session.user import User -from openbb_terminal.session import session_controller from openbb_terminal.common import feedparser_view from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, @@ -37,19 +35,24 @@ from openbb_terminal.core.config.paths import ( USER_ROUTINES_DIRECTORY, load_dotenv_with_priority, ) +from openbb_terminal.core.log.generation.custom_logger import log_terminal from openbb_terminal.helper_funcs import ( EXPORT_ONLY_RAW_DATA_ALLOWED, check_positive, get_flair, parse_and_split_input, ) -from openbb_terminal.menu import session, is_papermill +from openbb_terminal.keys_model import first_time_user +from openbb_terminal.menu import is_papermill, session from openbb_terminal.parent_classes import BaseController from openbb_terminal.reports.reports_model import ipykernel_launcher from openbb_terminal.rich_config import MenuText, console +from openbb_terminal.session import session_controller +from openbb_terminal.session.user import User from openbb_terminal.terminal_helper import ( bootup, check_for_updates, + is_installer, is_packaged_application, is_reset, print_goodbye, @@ -57,10 +60,7 @@ from openbb_terminal.terminal_helper import ( suppress_stdout, update_terminal, welcome_message, - is_installer, ) -from openbb_terminal.keys_model import first_time_user -from openbb_terminal.core.log.generation.custom_logger import log_terminal # pylint: disable=too-many-public-methods,import-outside-toplevel, too-many-function-args # pylint: disable=too-many-branches,no-member,C0302,too-many-return-statements, inconsistent-return-statements From 2d4e7c1aac9227f6546796ee6831343fc4ce4bfc Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Sun, 5 Feb 2023 19:31:00 +0000 Subject: [PATCH 200/230] ruff tests --- tests/openbb_terminal/account/test_account_controller.py | 2 +- tests/openbb_terminal/session/test_hub_model.py | 5 +++-- tests/openbb_terminal/session/test_local_model.py | 7 ++++--- tests/openbb_terminal/session/test_sdk_session.py | 1 + tests/openbb_terminal/session/test_session_model.py | 3 ++- tests/openbb_terminal/session/test_user.py | 1 - 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index 9832a4ba8ba..33da31f200d 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -1,6 +1,6 @@ # IMPORTATION STANDARD -import os import json +import os # IMPORTATION THIRDPARTY import pytest diff --git a/tests/openbb_terminal/session/test_hub_model.py b/tests/openbb_terminal/session/test_hub_model.py index ca5b7e46edf..909b7e115fa 100644 --- a/tests/openbb_terminal/session/test_hub_model.py +++ b/tests/openbb_terminal/session/test_hub_model.py @@ -1,6 +1,7 @@ -from unittest.mock import patch, MagicMock -import requests +from unittest.mock import MagicMock, patch + import pytest +import requests from openbb_terminal.session import hub_model diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 408bc5f2137..95701bbc810 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -1,10 +1,11 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY -from pathlib import Path -import pytest import json -from unittest.mock import patch, mock_open +from pathlib import Path +from unittest.mock import mock_open, patch + +import pytest # IMPORTATION INTERNAL from openbb_terminal.session import local_model diff --git a/tests/openbb_terminal/session/test_sdk_session.py b/tests/openbb_terminal/session/test_sdk_session.py index 52f96ae63a0..9e320a743d2 100644 --- a/tests/openbb_terminal/session/test_sdk_session.py +++ b/tests/openbb_terminal/session/test_sdk_session.py @@ -2,6 +2,7 @@ # IMPORTATION THIRDPARTY from unittest.mock import patch + import pytest # IMPORTATION INTERNAL diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index 1fb4a4b402f..e1df7830f9c 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -1,9 +1,10 @@ # IMPORTATION STANDARD # IMPORTATION THIRDPARTY -import os import json +import os from unittest.mock import patch + import pytest from requests import Response diff --git a/tests/openbb_terminal/session/test_user.py b/tests/openbb_terminal/session/test_user.py index cbe055069a2..71e512a2354 100644 --- a/tests/openbb_terminal/session/test_user.py +++ b/tests/openbb_terminal/session/test_user.py @@ -7,7 +7,6 @@ import pytest # IMPORTATION INTERNAL from openbb_terminal.session import user - # pylint: disable=protected-access, redefined-outer-name TEST_SESSION = { From 01706566b08ad1d3915585d1733b658af11c85f4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 09:58:29 +0000 Subject: [PATCH 201/230] fix qa --- .../common/quantitative_analysis/test_qa_view.py | 5 ++++- tests/openbb_terminal/test_settings_controller.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/openbb_terminal/common/quantitative_analysis/test_qa_view.py b/tests/openbb_terminal/common/quantitative_analysis/test_qa_view.py index 533d89dc08e..c4072a85827 100644 --- a/tests/openbb_terminal/common/quantitative_analysis/test_qa_view.py +++ b/tests/openbb_terminal/common/quantitative_analysis/test_qa_view.py @@ -73,7 +73,10 @@ def test_display_cusum_fail(): @pytest.mark.parametrize("yearly", [False, True]) -def test_display_seasonal(yearly): +def test_display_seasonal(mocker, yearly): + mocker.patch( + "openbb_terminal.helper_funcs.ask_file_overwrite", return_value=(False, True) + ) qa_view.display_seasonal("Data", df, "col2", yearly, "xlsx", None) diff --git a/tests/openbb_terminal/test_settings_controller.py b/tests/openbb_terminal/test_settings_controller.py index 4ade69c2be2..e7c552d36e0 100644 --- a/tests/openbb_terminal/test_settings_controller.py +++ b/tests/openbb_terminal/test_settings_controller.py @@ -12,8 +12,9 @@ def controller(mocker): True, ) mocker.patch("openbb_terminal.settings_controller.session", True) - mocker.patch("openbb_terminal.settings_controller.set_key") - mocker.patch("openbb_terminal.settings_controller.obbff") + mocker.patch("openbb_terminal.settings_controller.set_cfg_plot") + mocker.patch("openbb_terminal.settings_controller.set_path_config") + mocker.patch("openbb_terminal.settings_controller.obbff_ctrl") return SettingsController() From a14cd9c3a0ed0a8f6fb35a0f5ce3261dff4a2c7b Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 10:14:35 +0000 Subject: [PATCH 202/230] add help to settings cmds --- openbb_terminal/settings_controller.py | 58 +++++++++++++------ .../test_settings_controller.py | 10 +++- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 2fbdcacb210..8129efce514 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -226,26 +226,42 @@ class SettingsController(BaseController): ) @log_start_end(log=logger) - def call_colors(self, _): + def call_colors(self, other_args: List[str]): """Process colors command""" - console.print( - "\n1. Play with the terminal coloring embedded in our website https://openbb.co/customize\n" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="autoscaling", + description="Set the use of autoscaling in the plots", ) - console.print("2. Once happy, click 'Download Theme'\n") - console.print( - "3. The file 'openbb_config.richstyle.json' should be downloaded\n" - ) - console.print( - "4. Insert that config file inside /OpenBBUserData/styles/user/\n" - ) - console.print("5. Close the terminal and run it again.\n") + ns_parser = self.parse_simple_args(parser, other_args) + if ns_parser: + console.print( + "\n1. Play with the terminal coloring embedded in our website https://openbb.co/customize\n" + ) + console.print("2. Once happy, click 'Download Theme'\n") + console.print( + "3. The file 'openbb_config.richstyle.json' should be downloaded\n" + ) + console.print( + "4. Insert that config file inside /OpenBBUserData/styles/user/\n" + ) + console.print("5. Close the terminal and run it again.\n") @log_start_end(log=logger) - def call_dt(self, _): + def call_dt(self, other_args: List[str]): """Process dt command""" - obbff_ctrl.FeatureFlagsController.set_feature_flag( - "OPENBB_USE_DATETIME", not obbff.USE_DATETIME + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="dt", + description="Set the use of datetime in the plots", ) + ns_parser = self.parse_simple_args(parser, other_args) + if ns_parser: + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_DATETIME", not obbff.USE_DATETIME + ) @log_start_end(log=logger) def call_source(self, other_args: List[str]): @@ -283,11 +299,19 @@ class SettingsController(BaseController): console.print(e) @log_start_end(log=logger) - def call_autoscaling(self, _): + def call_autoscaling(self, other_args: List[str]): """Process autoscaling command""" - obbff_ctrl.FeatureFlagsController.set_feature_flag( - "OPENBB_USE_PLOT_AUTOSCALING", not obbff.USE_PLOT_AUTOSCALING + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="autoscaling", + description="Set the use of autoscaling in the plots", ) + ns_parser = self.parse_simple_args(parser, other_args) + if ns_parser: + obbff_ctrl.FeatureFlagsController.set_feature_flag( + "OPENBB_USE_PLOT_AUTOSCALING", not obbff.USE_PLOT_AUTOSCALING + ) @log_start_end(log=logger) def call_dpi(self, other_args: List[str]): diff --git a/tests/openbb_terminal/test_settings_controller.py b/tests/openbb_terminal/test_settings_controller.py index e7c552d36e0..01b6c13b8b0 100644 --- a/tests/openbb_terminal/test_settings_controller.py +++ b/tests/openbb_terminal/test_settings_controller.py @@ -12,9 +12,13 @@ def controller(mocker): True, ) mocker.patch("openbb_terminal.settings_controller.session", True) - mocker.patch("openbb_terminal.settings_controller.set_cfg_plot") - mocker.patch("openbb_terminal.settings_controller.set_path_config") - mocker.patch("openbb_terminal.settings_controller.obbff_ctrl") + mocker.patch("openbb_terminal.settings_controller.SettingsController.set_cfg_plot") + mocker.patch( + "openbb_terminal.settings_controller.SettingsController.set_path_config" + ) + mocker.patch( + "openbb_terminal.settings_controller.obbff_ctrl.FeatureFlagsController.set_feature_flag" + ) return SettingsController() From b1c79b42912630910cdd00ab522db7566e6469d2 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 10:22:21 +0000 Subject: [PATCH 203/230] ruff --- terminal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/terminal.py b/terminal.py index 9fbd3770ac0..a3a5684ec80 100644 --- a/terminal.py +++ b/terminal.py @@ -1,5 +1,6 @@ import multiprocessing import sys + from openbb_terminal.core.integration_tests import integration_controller from openbb_terminal.session import session_controller from openbb_terminal.terminal_helper import is_installer From 665250d1beacce32284715129852086108bdf230 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 11:04:45 +0000 Subject: [PATCH 204/230] fix pull keys bug --- openbb_terminal/account/account_controller.py | 2 +- openbb_terminal/account/account_model.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 836a5e636ae..08d2d5d4bef 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -130,7 +130,7 @@ class AccountController(BaseController): else: console.print("\n[info]Aborted.[/info]") else: - console.print("\n[info]No changes to apply.[/info]") + console.print("[info]No changes to apply.[/info]") def call_clear(self, other_args: List[str]): """Clear data""" diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 645313b788a..06406efc7ef 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -101,9 +101,10 @@ def get_diff_keys(keys: dict) -> dict: diff = {} if keys: for k, v in sorted(keys.items()): - old, new = get_var_diff(cfg, k, v) - if new is not None: - diff[k] = (old, new) + if hasattr(cfg, k): + old, new = get_var_diff(cfg, k, v) + if new is not None: + diff[k] = (old, new) return diff From 928e652d352017098636b69c61b61e13f9ac2cb0 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 11:05:06 +0000 Subject: [PATCH 205/230] point to dev --- openbb_terminal/session/hub_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 0f4d7473f14..f64c9b74e88 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -4,7 +4,7 @@ import requests from openbb_terminal.rich_config import console -BASE_URL = "http://127.0.0.1:8000/" +BASE_URL = "https://payments.openbb.dev/" TIMEOUT = 15 From d56e8da3c5ab66c2bc7c4b3723e788c46f1abf43 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Mon, 6 Feb 2023 12:23:46 +0000 Subject: [PATCH 206/230] account model tests --- openbb_terminal/account/account_model.py | 4 +- .../account/test_account_model.py | 214 ++++++++++++++++++ 2 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 tests/openbb_terminal/account/test_account_model.py diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 06406efc7ef..8cb119aac7f 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -28,14 +28,14 @@ def get_diff(configs: dict) -> dict: diff_settings = get_diff_settings(configs.get(SETTINGS, {})) if diff_settings: - console.print("Settings:", style="info") + console.print("[info]Settings:[/info]") for k, v in diff_settings.items(): configs_diff[SETTINGS][k] = v[1] console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") diff_keys = get_diff_keys(configs.get(KEYS, {})) if diff_keys: - console.print("Keys:", style="info") + console.print("[info]Keys:[/info]") for k, v in diff_keys.items(): configs_diff[KEYS][k] = v[1] console.print(f" [menu]{k}[/menu]: {v[0]} -> {v[1]}") diff --git a/tests/openbb_terminal/account/test_account_model.py b/tests/openbb_terminal/account/test_account_model.py new file mode 100644 index 00000000000..e9acf1777c3 --- /dev/null +++ b/tests/openbb_terminal/account/test_account_model.py @@ -0,0 +1,214 @@ +from unittest.mock import patch +from pathlib import Path + +from openbb_terminal import config_plot as cfg_plot +from openbb_terminal import config_terminal as cfg +from openbb_terminal import feature_flags as obbff +from openbb_terminal.account import account_model +from openbb_terminal.core.config import paths + + +def test_get_var_diff(): + class TestObj: # pylint: disable=too-few-public-methods + attr_str = "string" + attr_int = 10 + attr_float = 3.14 + attr_path = Path("/tmp/test") + attr_bool = True + + obj = TestObj() + + # Test string attribute + result = account_model.get_var_diff(obj, "attr_str", "new string") + assert result == ("string", "new string") + + # Test int attribute + result = account_model.get_var_diff(obj, "attr_int", "20") + assert result == (10, 20) + + # Test float attribute + result = account_model.get_var_diff(obj, "attr_float", "6.28") + assert result == (3.14, 6.28) + + # Test path attribute + result = account_model.get_var_diff(obj, "attr_path", "/tmp/new_test") + assert result == (Path("/tmp/test"), Path("/tmp/new_test")) + + # Test bool attribute + result = account_model.get_var_diff(obj, "attr_bool", "false") + assert result == (True, False) + + +def test_get_diff_keys(): + class TestObject: # pylint: disable=too-few-public-methods + a = 1 + b = 2.0 + c = "string" + + test_keys = {"a": 2, "b": 3.0, "c": "new_string"} + with patch("openbb_terminal.account.account_model.cfg", TestObject): + diff = account_model.get_diff_keys(test_keys) + assert diff == {"a": (1, 2), "b": (2.0, 3.0), "c": ("string", "new_string")} + + +def test_get_diff_keys_empty_keys(): + class TestObject: # pylint: disable=too-few-public-methods + a = 1 + b = 2.0 + c = "string" + + test_keys = {} + with patch("openbb_terminal.account.account_model.cfg", TestObject): + diff = account_model.get_diff_keys(test_keys) + assert not diff and isinstance(diff, dict) + + +def test_get_diff_keys_same_keys(): + class TestObject: # pylint: disable=too-few-public-methods + a = 1 + b = 2.0 + c = "string" + + test_keys = {"a": 1, "b": 2.0, "c": "string"} + with patch("openbb_terminal.account.account_model.cfg", TestObject): + diff = account_model.get_diff_keys(test_keys) + assert not diff and isinstance(diff, dict) + + +def test_get_diff_keys_new_keys(): + class TestObject: # pylint: disable=too-few-public-methods + a = 1 + b = 2.0 + + test_keys = {"a": 1, "b": 2.0, "c": "string"} + with patch("openbb_terminal.account.account_model.cfg", TestObject): + diff = account_model.get_diff_keys(test_keys) + assert not diff and isinstance(diff, dict) + + +def test_get_diff_settings_empty_settings(): + diff = account_model.get_diff_settings({}) + assert not diff and isinstance(diff, dict) + + +def test_get_diff_settings_no_diff(): + obbff.value = 1 + cfg.value = 1 + cfg_plot.value = 1 + paths.value = 1 + diff = account_model.get_diff_settings({"value": 1}) + assert not diff and isinstance(diff, dict) + + +def test_get_diff_settings_obbff_diff(): + obbff.value = 1 + diff = account_model.get_diff_settings({"value": 2}) + assert diff == {"value": (1, 2)} + + +def test_get_diff_settings_cfg_diff(): + cfg.value = 1 + diff = account_model.get_diff_settings({"value": 2}) + assert diff == {"value": (1, 2)} + + +def test_get_diff_settings_cfg_plot_diff(): + cfg_plot.value = 1 + diff = account_model.get_diff_settings({"value": 2}) + assert diff == {"value": (1, 2)} + + +def test_get_diff_settings_paths_diff(): + paths.value = 1 + diff = account_model.get_diff_settings({"value": 2}) + assert diff == {"value": (1, 2)} + + +def test_get_diff(): + configs = { + "features_settings": {"setting1": "value1"}, + "features_keys": {"key1": "value1"}, + } + expected = { + "features_settings": {"setting1": "value1"}, + "features_keys": {"key1": "value1"}, + } + + with patch( + "openbb_terminal.account.account_model.get_diff_settings" + ) as mock_get_diff_settings: + mock_get_diff_settings.return_value = {"setting1": ("old_value", "value1")} + + with patch( + "openbb_terminal.account.account_model.get_diff_keys" + ) as mock_get_diff_keys: + mock_get_diff_keys.return_value = {"key1": ("old_value", "value1")} + + result = account_model.get_diff(configs) + + assert result == expected + + +def test_get_diff_no_keys(): + configs = { + "features_settings": {"setting1": "value1"}, + } + expected = { + "features_settings": {"setting1": "value1"}, + } + + with patch( + "openbb_terminal.account.account_model.get_diff_settings" + ) as mock_get_diff_settings: + mock_get_diff_settings.return_value = {"setting1": ("old_value", "value1")} + + with patch( + "openbb_terminal.account.account_model.get_diff_keys" + ) as mock_get_diff_keys: + mock_get_diff_keys.return_value = {} + + result = account_model.get_diff(configs) + + assert result == expected + + +def test_get_diff_no_settings(): + configs = { + "features_keys": {"key1": "value1"}, + } + expected = { + "features_keys": {"key1": "value1"}, + } + + with patch( + "openbb_terminal.account.account_model.get_diff_settings" + ) as mock_get_diff_settings: + mock_get_diff_settings.return_value = {} + + with patch( + "openbb_terminal.account.account_model.get_diff_keys" + ) as mock_get_diff_keys: + mock_get_diff_keys.return_value = {"key1": ("old_value", "value1")} + + result = account_model.get_diff(configs) + + assert result == expected + + +def test_get_diff_no_settings_no_keys(): + configs = {} + expected = {} + + with patch( + "openbb_terminal.account.account_model.get_diff_settings" + ) as mock_get_diff_settings: + mock_get_diff_settings.return_value = {} + + with patch( + "openbb_terminal.account.account_model.get_diff_keys" + ) as mock_get_diff_keys: + mock_get_diff_keys.return_value = {} + + result = account_model.get_diff(configs) + + assert result == expected From 4cc22804857de7ffb88ed1c007b56d527cc827b0 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Mon, 6 Feb 2023 12:26:27 +0000 Subject: [PATCH 207/230] black --- tests/openbb_terminal/session/test_session_controller.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/openbb_terminal/session/test_session_controller.py b/tests/openbb_terminal/session/test_session_controller.py index 15cf50898c3..48f76e38ac8 100644 --- a/tests/openbb_terminal/session/test_session_controller.py +++ b/tests/openbb_terminal/session/test_session_controller.py @@ -30,7 +30,6 @@ def test_get_user_input_with_save(mock_prompt_session): def test_prompt(): - with patch( "openbb_terminal.session.session_controller.get_user_input", return_value=("email", "password", False), @@ -41,7 +40,6 @@ def test_prompt(): "openbb_terminal.session.session_controller.login_and_launch", return_value=True, ): - session_controller.prompt() assert get_user_input_mock.call_count == 1 create_session_mock.assert_called_once_with("email", "password", False) @@ -49,7 +47,6 @@ def test_prompt(): def test_prompt_guest_allowed(): - with patch( "openbb_terminal.session.session_controller.get_user_input", return_value=(None, None, False), From dcde2af2d60b5afc6f83b26ff8ea7d15e85bff26 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Mon, 6 Feb 2023 12:42:48 +0000 Subject: [PATCH 208/230] ruff --- tests/openbb_terminal/account/test_account_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/openbb_terminal/account/test_account_model.py b/tests/openbb_terminal/account/test_account_model.py index e9acf1777c3..b480ad09ef2 100644 --- a/tests/openbb_terminal/account/test_account_model.py +++ b/tests/openbb_terminal/account/test_account_model.py @@ -1,5 +1,5 @@ -from unittest.mock import patch from pathlib import Path +from unittest.mock import patch from openbb_terminal import config_plot as cfg_plot from openbb_terminal import config_terminal as cfg @@ -13,7 +13,7 @@ def test_get_var_diff(): attr_str = "string" attr_int = 10 attr_float = 3.14 - attr_path = Path("/tmp/test") + attr_path = Path("/tmp/test") # noqa: S108 attr_bool = True obj = TestObj() @@ -31,8 +31,8 @@ def test_get_var_diff(): assert result == (3.14, 6.28) # Test path attribute - result = account_model.get_var_diff(obj, "attr_path", "/tmp/new_test") - assert result == (Path("/tmp/test"), Path("/tmp/new_test")) + result = account_model.get_var_diff(obj, "attr_path", "/tmp/new_test") # noqa: S108 + assert result == (Path("/tmp/test"), Path("/tmp/new_test")) # noqa: S108 # Test bool attribute result = account_model.get_var_diff(obj, "attr_bool", "false") From 126ff86e6c42d35c582d96ea53db86f926139f48 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Mon, 6 Feb 2023 13:24:39 +0000 Subject: [PATCH 209/230] fix for empty feature settings --- openbb_terminal/session/session_model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/session/session_model.py b/openbb_terminal/session/session_model.py index cae5dbbe634..dd23c5ff836 100644 --- a/openbb_terminal/session/session_model.py +++ b/openbb_terminal/session/session_model.py @@ -55,10 +55,13 @@ def login(session: dict) -> LoginStatus: if response.status_code == 200: configs = json.loads(response.content) email = configs.get("email", "") + feature_settings = configs.get("features_settings", {}) User.load_user_info(session, email) Local.apply_configs(configs=configs) User.update_flair( - flair=configs.get("features_settings", {}).get("USE_FLAIR", None) + flair=feature_settings.get("USE_FLAIR", None) + if feature_settings + else None ) return LoginStatus.SUCCESS return LoginStatus.FAILED From 89c225b1484a0994d81cc9fbf756f732839df32c Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Mon, 6 Feb 2023 14:58:45 +0000 Subject: [PATCH 210/230] ruff --- tests/openbb_terminal/session/test_session_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/openbb_terminal/session/test_session_controller.py b/tests/openbb_terminal/session/test_session_controller.py index 48f76e38ac8..49ea20d2e34 100644 --- a/tests/openbb_terminal/session/test_session_controller.py +++ b/tests/openbb_terminal/session/test_session_controller.py @@ -1,4 +1,5 @@ from unittest.mock import patch + import pytest from openbb_terminal.session import session_controller From c7993c42baf21185d47420ef9d71e3791237c111 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 16:40:11 +0000 Subject: [PATCH 211/230] don't display config msg if wrong token --- openbb_terminal/session/hub_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index f64c9b74e88..8fa1c10b03c 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -165,7 +165,7 @@ def fetch_user_configs( headers={"Authorization": f"{token_type.title()} {token}"}, timeout=timeout, ) - if response.status_code != 200: + if response.status_code not in [200, 401]: console.print("[red]\nFailed to fetch configurations.[/red]") return response except requests.exceptions.ConnectionError: From 4475e74e43dc54bcb505aff99565b136c0c0c94f Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 23:02:05 +0000 Subject: [PATCH 212/230] general msgs --- openbb_terminal/session/hub_model.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 8fa1c10b03c..2140a40a845 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -7,6 +7,9 @@ from openbb_terminal.rich_config import console BASE_URL = "https://payments.openbb.dev/" TIMEOUT = 15 +CONNECTION_ERROR_MSG = "[red]Connection error.[/red]" +CONNECTION_TIMEOUT_MSG = "[red]Connection timeout.[/red]" + def create_session( email: str, password: str, base_url: str = BASE_URL, timeout: int = TIMEOUT @@ -37,10 +40,10 @@ def create_session( } return requests.post(url=base_url + "login", json=data, timeout=timeout) except requests.exceptions.ConnectionError: - console.print("\n[red]Connection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("\n[red]Connection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("\n[red]Failed to request login info.[/red]") @@ -79,10 +82,10 @@ def delete_session( console.print("[red]Failed to delete server session.[/red]") return response except requests.exceptions.ConnectionError: - console.print("[red]Connection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("[red]Failed to delete server session.[/red]") @@ -106,6 +109,7 @@ def process_session_response(response: requests.Response) -> dict: login = response.json() return login if response.status_code == 401: + print(response.json()) console.print("\n[red]Wrong credentials.[/red]") return {} if response.status_code == 403: @@ -169,10 +173,10 @@ def fetch_user_configs( console.print("[red]\nFailed to fetch configurations.[/red]") return response except requests.exceptions.ConnectionError: - console.print("[red]\nConnection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("[red]\nFailed to fetch configurations.[/red]") @@ -229,10 +233,10 @@ def patch_user_configs( console.print("[red]Failed to save remotely.[/red]") return response except requests.exceptions.ConnectionError: - console.print("[red]Connection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("[red]Failed to save remotely.[/red]") @@ -273,10 +277,10 @@ def clear_user_configs( console.print("[red]Failed to clear configurations.[/red]") return response except requests.exceptions.ConnectionError: - console.print("[red]Connection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("[red]Failed to clear configurations.[/red]") @@ -321,10 +325,10 @@ def upload_routine( console.print("[red]Error uploading your routine.[/red]") return response except requests.exceptions.ConnectionError: - console.print("[red]Connection error.[/red]") + console.print(f"\n{CONNECTION_ERROR_MSG}") return None except requests.exceptions.Timeout: - console.print("[red]\nConnection timeout.[/red]") + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") return None except Exception: console.print("[red]Failed to upload your routine.[/red]") From b764198ff52a0febb77c803037412ca83c3bcc2d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Mon, 6 Feb 2023 23:06:11 +0000 Subject: [PATCH 213/230] remove print --- openbb_terminal/session/hub_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 2140a40a845..c9413e96c09 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -109,7 +109,6 @@ def process_session_response(response: requests.Response) -> dict: login = response.json() return login if response.status_code == 401: - print(response.json()) console.print("\n[red]Wrong credentials.[/red]") return {} if response.status_code == 403: From b9bc0aaac7ef450cc2dd2a6fe7485b503da56522 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 8 Feb 2023 11:43:54 +0000 Subject: [PATCH 214/230] ruff --- openbb_terminal/account/account_controller.py | 6 ++++-- openbb_terminal/account/account_model.py | 8 +++++--- openbb_terminal/helper_funcs.py | 1 - openbb_terminal/keys_controller.py | 1 - openbb_terminal/session/local_model.py | 8 +++++--- openbb_terminal/session/sdk_session.py | 6 ++++-- openbb_terminal/settings_controller.py | 8 +++++--- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 08d2d5d4bef..2e3e43f5365 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -14,8 +14,10 @@ from openbb_terminal.featflags_controller import FeatureFlagsController from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import MenuText, console -from openbb_terminal.session import hub_model as Hub -from openbb_terminal.session import local_model as Local +from openbb_terminal.session import ( + hub_model as Hub, + local_model as Local, +) from openbb_terminal.session.user import User logger = logging.getLogger(__name__) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 8cb119aac7f..816a7812c04 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,9 +1,11 @@ from pathlib import Path from typing import Any, Dict -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal import config_terminal as cfg -from openbb_terminal import feature_flags as obbff +from openbb_terminal import ( + config_plot as cfg_plot, + config_terminal as cfg, + feature_flags as obbff, +) from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config import paths from openbb_terminal.rich_config import console diff --git a/openbb_terminal/helper_funcs.py b/openbb_terminal/helper_funcs.py index e706fef4ec9..d1dc8360c4e 100644 --- a/openbb_terminal/helper_funcs.py +++ b/openbb_terminal/helper_funcs.py @@ -44,7 +44,6 @@ from openbb_terminal import ( config_terminal as cfg, feature_flags as obbff, ) -from openbb_terminal.core.config import paths from openbb_terminal.core.config.paths import ( HOME_DIRECTORY, USER_EXPORTS_DIRECTORY, diff --git a/openbb_terminal/keys_controller.py b/openbb_terminal/keys_controller.py index 0ec63220762..97e6effe865 100644 --- a/openbb_terminal/keys_controller.py +++ b/openbb_terminal/keys_controller.py @@ -14,7 +14,6 @@ from openbb_terminal import ( keys_model, keys_view, ) -from openbb_terminal.core.config.paths import USER_ENV_FILE from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.helper_funcs import EXPORT_ONLY_RAW_DATA_ALLOWED diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 21b6676040b..877859f814a 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -3,9 +3,11 @@ import os from pathlib import Path from typing import Optional -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal import config_terminal as cfg -from openbb_terminal import feature_flags as obbff +from openbb_terminal import ( + config_plot as cfg_plot, + config_terminal as cfg, + feature_flags as obbff, +) from openbb_terminal.base_helpers import strtobool from openbb_terminal.core.config import paths from openbb_terminal.core.config.paths import ( diff --git a/openbb_terminal/session/sdk_session.py b/openbb_terminal/session/sdk_session.py index c82a1bd3af2..8c82129e07a 100644 --- a/openbb_terminal/session/sdk_session.py +++ b/openbb_terminal/session/sdk_session.py @@ -1,6 +1,8 @@ from openbb_terminal.rich_config import console -from openbb_terminal.session import local_model as Local -from openbb_terminal.session import session_model +from openbb_terminal.session import ( + local_model as Local, + session_model, +) from openbb_terminal.session.user import User diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index 8129efce514..a6cfd5ff6d4 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -15,9 +15,11 @@ import pytz from dotenv import set_key # IMPORTATION INTERNAL -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal import featflags_controller as obbff_ctrl -from openbb_terminal import feature_flags as obbff +from openbb_terminal import ( + config_plot as cfg_plot, + featflags_controller as obbff_ctrl, + feature_flags as obbff, +) from openbb_terminal.core.config import paths from openbb_terminal.core.config.paths import ( USER_DATA_SOURCES_DEFAULT_FILE, From 269c339184c65bc4e895744c473a630afb13b6e4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 8 Feb 2023 12:05:58 +0000 Subject: [PATCH 215/230] fix sources not updated real time --- openbb_terminal/settings_controller.py | 14 ++++-------- openbb_terminal/sources_controller.py | 30 +++++++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/openbb_terminal/settings_controller.py b/openbb_terminal/settings_controller.py index a6cfd5ff6d4..3bf77fa2000 100644 --- a/openbb_terminal/settings_controller.py +++ b/openbb_terminal/settings_controller.py @@ -286,19 +286,13 @@ class SettingsController(BaseController): other_args.insert(0, "-f") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: - try: - console.print("Loading sources from: " + str(ns_parser.file)) - with open(ns_parser.file): - # Try to open the file to get an exception if the file doesn't exist - pass - + if os.path.exists(ns_parser.file): obbff_ctrl.FeatureFlagsController.set_feature_flag( "OPENBB_PREFERRED_DATA_SOURCE_FILE", ns_parser.file ) - - except Exception as e: - console.print("Couldn't open the sources file!") - console.print(e) + console.print("[green]Sources file changed successfully![/green]") + else: + console.print("[red]Couldn't find the sources file![/red]") @log_start_end(log=logger) def call_autoscaling(self, other_args: List[str]): diff --git a/openbb_terminal/sources_controller.py b/openbb_terminal/sources_controller.py index 5317ea91603..a1257f8f50d 100644 --- a/openbb_terminal/sources_controller.py +++ b/openbb_terminal/sources_controller.py @@ -12,7 +12,7 @@ from typing import Dict, List # IMPORTATION THIRDPARTY # IMPORTATION INTERNAL from openbb_terminal import feature_flags as obbff -from openbb_terminal.core.config.paths import MISCELLANEOUS_DIRECTORY +from openbb_terminal.core.config.paths import USER_DATA_SOURCES_DEFAULT_FILE from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal.decorators import log_start_end from openbb_terminal.menu import session @@ -43,10 +43,22 @@ class SourcesController(BaseController): """Constructor""" super().__init__(queue) - self.commands_with_sources = dict() + self.commands_with_sources: Dict[str, List[str]] = {} + self.load_sources_json() + if session and obbff.USE_PROMPT_TOOLKIT: + choices: dict = {c: {} for c in self.controller_choices} + choices["get"] = {c: None for c in list(self.commands_with_sources.keys())} + choices["set"] = {c: None for c in list(self.commands_with_sources.keys())} + for cmd in list(self.commands_with_sources.keys()): + choices["set"][cmd] = {c: None for c in self.commands_with_sources[cmd]} + + self.completer = NestedCompleter.from_nested_dict(choices) + + def load_sources_json(self): + """Load the .json file""" # Loading in both source files: default sources and user sources - default_data_source = MISCELLANEOUS_DIRECTORY / "data_sources_default.json" + default_data_source = USER_DATA_SOURCES_DEFAULT_FILE user_data_source = Path(obbff.PREFERRED_DATA_SOURCE_FILE) # Opening default sources file from the repository root @@ -80,15 +92,6 @@ class SourcesController(BaseController): context ][menu] - if session and obbff.USE_PROMPT_TOOLKIT: - choices: dict = {c: {} for c in self.controller_choices} - choices["get"] = {c: None for c in list(self.commands_with_sources.keys())} - choices["set"] = {c: None for c in list(self.commands_with_sources.keys())} - for cmd in list(self.commands_with_sources.keys()): - choices["set"][cmd] = {c: None for c in self.commands_with_sources[cmd]} - - self.completer = NestedCompleter.from_nested_dict(choices) - def print_help(self): """Print help""" mt = MenuText("sources/") @@ -121,6 +124,7 @@ class SourcesController(BaseController): other_args.insert(0, "-c") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: + self.load_sources_json() try: the_item = self.commands_with_sources[ns_parser.cmd] except KeyError: @@ -171,6 +175,8 @@ class SourcesController(BaseController): other_args.insert(2, "-s") ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: + self.load_sources_json() + menus = ns_parser.cmd.split("_") num_menus = len(menus) From 555dae0bb7ef75a2348c079dc702173f37394d54 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Wed, 8 Feb 2023 12:14:02 +0000 Subject: [PATCH 216/230] missing return doc --- openbb_terminal/account/account_model.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index 816a7812c04..c913c95258a 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Tuple from openbb_terminal import ( config_plot as cfg_plot, @@ -21,8 +21,8 @@ def get_diff(configs: dict) -> dict: Returns ------- - bool - True if there is a diff. + dict + The diff. """ SETTINGS = "features_settings" KEYS = "features_keys" @@ -111,7 +111,7 @@ def get_diff_keys(keys: dict) -> dict: return diff -def get_var_diff(obj, name, value): +def get_var_diff(obj, name, value) -> Tuple[Any, Any]: """Set attribute in object. Parameters @@ -122,6 +122,11 @@ def get_var_diff(obj, name, value): The attribute name. value : str The attribute value. + + Returns + ------- + Tuple[Any, Any] + The old and new values. """ current_value = getattr(obj, name) From 759a2c7557725e86f2826802397071796b7a6b15 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 09:14:24 +0000 Subject: [PATCH 217/230] add guest if no email --- openbb_terminal/session/session_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 099e7c4d6f1..ab7d666b615 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -34,6 +34,9 @@ def get_user_input() -> Tuple[str, str, bool]: email = s.prompt( message="> Email: ", ) + if not email: + return "", "", False + password = s.prompt( message="> Password: ", is_password=True, From ead2e2e76a1bef5f37466e43596f7d3f15786c8d Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:55:05 +0000 Subject: [PATCH 218/230] Routine remote storage (#4143) * add upload routine command * remove prints * point to local again * add commands * add delete * add download command and upload 409 * list comprehension * add download & save plus upload fixe * add user uuid folder * add list command * ruff * en.yml detail * provide choices to delete & download * add choices to upload * change menu text * starting adding unit tests * bad english * Update test_print_help.txt * genearl msgs * remove print * add docstrings * add unit tests to account controller * change parser * fix routine upload test * add to be implemented tests hub * add to be implemented tests local * add total pages to title * split list into model and view * add docstring * print msg if character * add local model tests * hub tests routines * fix test * point to dev and glob * fix small bug on upload * missing return * log decorators missing * black * ruff --- openbb_terminal/account/account_controller.py | 244 +++++++++++++++++- openbb_terminal/account/account_model.py | 29 +++ openbb_terminal/account/account_view.py | 28 ++ openbb_terminal/miscellaneous/i18n/en.yml | 4 +- openbb_terminal/session/hub_model.py | 173 ++++++++++++- openbb_terminal/session/local_model.py | 51 +++- .../account/test_account_controller.py | 152 ++++++++++- .../account/test_account_model.py | 8 +- .../test_call_download.txt | 3 + .../test_call_list.txt | 4 + .../test_print_help.txt | 6 + .../openbb_terminal/session/test_hub_model.py | 190 +++++++++++--- .../session/test_local_model.py | 122 +++++++++ .../session/test_session_model.py | 4 +- 14 files changed, 951 insertions(+), 67 deletions(-) create mode 100644 openbb_terminal/account/account_view.py create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_download.txt create mode 100644 tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index 2e3e43f5365..dd461bb7728 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -1,16 +1,19 @@ import argparse import json import logging +import os from pathlib import Path from typing import Dict, List from prompt_toolkit.completion import NestedCompleter from openbb_terminal import feature_flags as obbff -from openbb_terminal.account.account_model import get_diff +from openbb_terminal.account.account_model import get_diff, get_routines_info +from openbb_terminal.account.account_view import display_routines_list from openbb_terminal.core.config.paths import USER_ROUTINES_DIRECTORY from openbb_terminal.decorators import log_start_end from openbb_terminal.featflags_controller import FeatureFlagsController +from openbb_terminal.helper_funcs import check_positive from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.rich_config import MenuText, console @@ -30,6 +33,10 @@ class AccountController(BaseController): "sync", "pull", "clear", + "list", + "upload", + "download", + "delete", ] PATH = "/account/" @@ -37,21 +44,36 @@ class AccountController(BaseController): def __init__(self, queue: List[str] = None): super().__init__(queue) self.ROUTINE_FILES: Dict[str, Path] = {} - self.ROUTINE_CHOICES: Dict[str, None] = {} + self.REMOTE_CHOICES: List[str] = [] self.update_runtime_choices() def update_runtime_choices(self): """Update runtime choices""" - self.ROUTINE_FILES = { - filepath.name: filepath - for filepath in USER_ROUTINES_DIRECTORY.rglob("*.openbb") - } - self.ROUTINE_CHOICES = {filename: None for filename in self.ROUTINE_FILES} + self.ROUTINE_FILES = self.get_routines() if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} choices["sync"] = {"--on": {}, "--off": {}} + choices["upload"]["--file"] = {c: {} for c in self.ROUTINE_FILES} + choices["upload"]["-f"] = choices["upload"]["--file"] + choices["download"]["--name"] = {c: {} for c in self.REMOTE_CHOICES} + choices["download"]["-n"] = choices["download"]["--name"] + choices["delete"]["--name"] = {c: {} for c in self.REMOTE_CHOICES} + choices["delete"]["-n"] = choices["delete"]["--name"] self.completer = NestedCompleter.from_nested_dict(choices) + def get_routines(self): + """Get routines""" + routines = { + filepath.name: filepath + for filepath in USER_ROUTINES_DIRECTORY.glob("*.openbb") + } + user_folder = USER_ROUTINES_DIRECTORY / User.get_uuid() + if os.path.exists(user_folder): + routines.update( + {filepath.name: filepath for filepath in user_folder.rglob("*.openbb")} + ) + return routines + def print_help(self): """Print help""" @@ -60,6 +82,12 @@ class AccountController(BaseController): mt.add_cmd("sync") mt.add_cmd("pull") mt.add_cmd("clear") + mt.add_raw("\n") + mt.add_info("_routines_") + mt.add_cmd("list") + mt.add_cmd("upload") + mt.add_cmd("download") + mt.add_cmd("delete") console.print(text=mt.menu_text, menu="Account") @log_start_end(log=logger) @@ -85,7 +113,7 @@ class AccountController(BaseController): action="store_true", default=False, ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: if ns_parser.on: if not obbff.SYNC_ENABLED: @@ -108,6 +136,7 @@ class AccountController(BaseController): else: console.print(f"sync is {sync}, use --on or --off to change.") + @log_start_end(log=logger) def call_pull(self, other_args: List[str]): """Pull data""" parser = argparse.ArgumentParser( @@ -116,7 +145,7 @@ class AccountController(BaseController): prog="pull", description="Pull and apply stored configurations from the cloud.", ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: response = Hub.fetch_user_configs(User.get_session()) if response: @@ -134,6 +163,7 @@ class AccountController(BaseController): else: console.print("[info]No changes to apply.[/info]") + @log_start_end(log=logger) def call_clear(self, other_args: List[str]): """Clear data""" parser = argparse.ArgumentParser( @@ -142,7 +172,7 @@ class AccountController(BaseController): prog="clear", description="Clear stored configurations from the cloud.", ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: i = console.input( "[red]This action is irreversible![/red]\n" @@ -153,3 +183,197 @@ class AccountController(BaseController): Hub.clear_user_configs(auth_header=User.get_auth_header()) else: console.print("\n[info]Aborted.[/info]") + + @log_start_end(log=logger) + def call_list(self, other_args: List[str]): + """List routines""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="list", + description="List routines available in the cloud.", + ) + parser.add_argument( + "-p", + "--page", + type=check_positive, + dest="page", + default=1, + help="The page number.", + ) + parser.add_argument( + "-s", + "--size", + type=check_positive, + dest="size", + default=10, + help="The number of results per page.", + ) + ns_parser = self.parse_known_args_and_warn(parser, other_args) + if ns_parser: + response = Hub.list_routines( + auth_header=User.get_auth_header(), + page=ns_parser.page, + size=ns_parser.size, + ) + df, page, pages = get_routines_info(response) + if not df.empty: + self.REMOTE_CHOICES = list(df["name"]) + self.update_runtime_choices() + display_routines_list(df, page, pages) + else: + console.print("[red]No routines found.[/red]") + + @log_start_end(log=logger) + def call_upload(self, other_args: List[str]): + """Upload""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="upload", + description="Upload a routine to the cloud.", + ) + parser.add_argument( + "-f", + "--file", + type=str, + dest="file", + required="-h" not in other_args, + help="The file to be loaded", + metavar="FILE", + nargs="+", + ) + parser.add_argument( + "-d", + "--description", + type=str, + dest="description", + help="The description of the routine", + default="", + nargs="+", + ) + parser.add_argument( + "-n", + "--name", + type=str, + dest="name", + help="The name of the routine.", + nargs="+", + ) + ns_parser = self.parse_known_args_and_warn(parser, other_args) + if ns_parser: + routine = Local.get_routine(file_name=" ".join(ns_parser.file)) + if routine: + description = " ".join(ns_parser.description) + name = ( + " ".join(ns_parser.name) + if ns_parser.name + else ns_parser.file.split(".openbb")[0] + ) + + response = Hub.upload_routine( + auth_header=User.get_auth_header(), + name=name, + description=description, + routine=routine, + ) + if response is not None and response.status_code == 409: + i = console.input( + "A routine with the same name already exists, " + "do you want to overwrite it? (y/n): " + ) + console.print("") + if i.lower() in ["y", "yes"]: + response = Hub.upload_routine( + auth_header=User.get_auth_header(), + name=name, + description=description, + routine=routine, + override=True, + ) + else: + console.print("[info]Aborted.[/info]") + + if response and response.status_code == 200: + self.REMOTE_CHOICES.append(name) + self.update_runtime_choices() + + @log_start_end(log=logger) + def call_download(self, other_args: List[str]): + """Download""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="download", + description="Download a routine from the cloud.", + ) + parser.add_argument( + "-n", + "--name", + type=str, + dest="name", + help="The name of the routine.", + required="-h" not in other_args, + nargs="+", + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-n") + ns_parser = self.parse_known_args_and_warn(parser, other_args) + if ns_parser: + response = Hub.download_routine( + auth_header=User.get_auth_header(), + name=" ".join(ns_parser.name), + ) + + if response and response.status_code == 200: + data = response.json() + if data: + name = data.get("name", "") + if name: + console.print(f"[info]Name:[/info] {name}") + + description = data.get("description", "") + if description: + console.print(f"[info]Description:[/info] {description}") + + script = data.get("script", "") + if script: + file_name = f"{name}.openbb" + file_path = Local.save_routine( + file_name=file_name, + routine=script, + ) + if file_path: + console.print(f"[info]Location:[/info] {file_path}") + + @log_start_end(log=logger) + def call_delete(self, other_args: List[str]): + """Delete""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="delete", + description="Delete a routine on the cloud.", + ) + parser.add_argument( + "-n", + "--name", + type=str, + dest="name", + help="The name of the routine", + required="-h" not in other_args, + nargs="+", + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-n") + ns_parser = self.parse_known_args_and_warn(parser, other_args) + if ns_parser: + name = " ".join(ns_parser.name) + response = Hub.delete_routine( + auth_header=User.get_auth_header(), + name=name, + ) + if response and response.status_code == 200: + if name in self.REMOTE_CHOICES: + self.REMOTE_CHOICES.remove(name) + self.update_runtime_choices() diff --git a/openbb_terminal/account/account_model.py b/openbb_terminal/account/account_model.py index c913c95258a..1b52cd11041 100644 --- a/openbb_terminal/account/account_model.py +++ b/openbb_terminal/account/account_model.py @@ -1,6 +1,9 @@ from pathlib import Path from typing import Any, Dict, Tuple +import numpy as np +import pandas as pd + from openbb_terminal import ( config_plot as cfg_plot, config_terminal as cfg, @@ -145,3 +148,29 @@ def get_var_diff(obj, name, value) -> Tuple[Any, Any]: return current_value, cast_value return None, None + + +def get_routines_info(response) -> Tuple[pd.DataFrame, int, int]: + """Get the routines list. + + Parameters + ---------- + response : requests.Response + The response. + + Returns + ------- + Tuple[pd.DataFrame, int, int] + The routines list, the current page and the total number of pages. + """ + df = pd.DataFrame() + if response and response.status_code == 200: + data = response.json() + page = data.get("page", 1) + pages = data.get("pages", 1) + items = data.get("items", []) + if items: + df = pd.DataFrame(items) + df.index = np.arange(1, len(df) + 1) + + return df, page, pages diff --git a/openbb_terminal/account/account_view.py b/openbb_terminal/account/account_view.py new file mode 100644 index 00000000000..f9c7bcd1c26 --- /dev/null +++ b/openbb_terminal/account/account_view.py @@ -0,0 +1,28 @@ +import pandas as pd + +from openbb_terminal.helper_funcs import print_rich_table + + +def display_routines_list(df: pd.DataFrame, page: int, pages: int): + """Display the routines list. + + Parameters + ---------- + df : pd.DataFrame + The routines list. + page : int + The current page. + pages : int + The total number of pages. + """ + title = f"Available routines - page {page}" + if pages: + title += f" of {pages}" + + print_rich_table( + df=df, + title=title, + headers=["Name", "Description"], + show_index=True, + index_name="#", + ) diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 1deb3582480..2fe478c6918 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -22,9 +22,11 @@ en: account/sync: Turns the cloud synchronization on/off account/pull: Pull data from cloud account/clear: Clear data from cloud - account/_routines_: Routines + account/_routines_: Cloud storage of routines + account/list: List available routines account/upload: Upload routine account/download: Download routine + account/delete: Delete routine keys/_keys_: This menu allows you to set your own API keys. Type 'about' for more information keys/mykeys: get your defined keys keys/_status_: Status diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index c9413e96c09..298ee23246e 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -158,7 +158,6 @@ def fetch_user_configs( Optional[requests.Response] The response from the get request. """ - token_type = session.get("token_type", "") token = session.get("access_token", "") @@ -168,7 +167,7 @@ def fetch_user_configs( headers={"Authorization": f"{token_type.title()} {token}"}, timeout=timeout, ) - if response.status_code not in [200, 401]: + if response.status_code not in [200, 401]: # 401: invalid token console.print("[red]\nFailed to fetch configurations.[/red]") return response except requests.exceptions.ConnectionError: @@ -212,7 +211,6 @@ def patch_user_configs( Optional[requests.Response] The response from the patch request. """ - if type_ not in ["keys", "settings"]: console.print("[red]\nInvalid patch type.[/red]") return None @@ -289,7 +287,9 @@ def clear_user_configs( def upload_routine( auth_header: str, name: str = "", + description: str = "", routine: str = "", + override: bool = False, base_url=BASE_URL, timeout: int = TIMEOUT, ) -> Optional[requests.Response]: @@ -298,18 +298,29 @@ def upload_routine( Parameters ---------- auth_header : str - + The authorization header, e.g. "Bearer ". name : str The name of the routine. routine : str The routine. + override : bool + Whether to override the routine if it already exists. base_url : str The base url, by default BASE_URL timeout : int The timeout, by default TIMEOUT - """ - data = {"name": name, "script": routine} + Returns + ------- + Optional[requests.Response] + The response from the post request. + """ + data = { + "name": name, + "description": description, + "script": routine, + "override": override, + } try: response = requests.post( @@ -320,8 +331,10 @@ def upload_routine( ) if response.status_code == 200: console.print("[green]Successfully uploaded your routine.[/green]") - else: - console.print("[red]Error uploading your routine.[/red]") + elif response.status_code != 409: # 409: routine already exists + console.print( + "[red]" + response.json().get("detail", "Unknown error.") + "[/red]" + ) return response except requests.exceptions.ConnectionError: console.print(f"\n{CONNECTION_ERROR_MSG}") @@ -334,10 +347,144 @@ def upload_routine( return None -def download_routine() -> Optional[requests.Response]: - """Download a routine from the server.""" +def download_routine( + auth_header: str, + name: str = "", + base_url=BASE_URL, + timeout: int = TIMEOUT, +) -> Optional[requests.Response]: + """Download a routine from the server. - # TODO: Implement test on `test_hub_model.py` when this is implemented. + Parameters + ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + name : str + The name of the routine. + base_url : str + The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT - console.print("[red]Not implemented yet.[/red]") - return None + Returns + ------- + Optional[requests.Response] + The response from the get request. + """ + try: + response = requests.get( + headers={"Authorization": auth_header}, + url=base_url + "terminal/script/" + name, + timeout=timeout, + ) + if response.status_code == 404: + console.print("[red]Routine not found.[/red]") + elif response.status_code != 200: + console.print("[red]Failed to download your routine.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print(f"\n{CONNECTION_ERROR_MSG}") + return None + except requests.exceptions.Timeout: + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") + return None + except Exception: + console.print("[red]Failed to download your routine.[/red]") + return None + + +def delete_routine( + auth_header: str, + name: str = "", + base_url=BASE_URL, + timeout: int = TIMEOUT, +) -> Optional[requests.Response]: + """Delete a routine from the server. + + Parameters + ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + name : str + The name of the routine. + base_url : str + The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT + + Returns + ------- + Optional[requests.Response] + The response from the delete request. + """ + try: + response = requests.delete( + headers={"Authorization": auth_header}, + url=base_url + "terminal/script/" + name, + timeout=timeout, + ) + if response.status_code == 200: + console.print("[green]Successfully deleted your routine.[/green]") + elif response.status_code == 404: + console.print("[red]Routine not found.[/red]") + else: + console.print("[red]Failed to delete your routine.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print(f"\n{CONNECTION_ERROR_MSG}") + return None + except requests.exceptions.Timeout: + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") + return None + except Exception: + console.print("[red]Failed to delete your routine.[/red]") + return None + + +def list_routines( + auth_header: str, + page: int = 1, + size: int = 10, + base_url=BASE_URL, + timeout: int = TIMEOUT, +) -> Optional[requests.Response]: + """List all routines from the server. + + Parameters + ---------- + auth_header : str + The authorization header, e.g. "Bearer ". + page : int + The page number. + size : int + The number of routines per page. + base_url : str + The base url, by default BASE_URL + timeout : int + The timeout, by default TIMEOUT + + Returns + ------- + Optional[requests.Response] + The response from the get request. + """ + fields = "name%2Cdescription" + + try: + response = requests.get( + headers={"Authorization": auth_header}, + url=f"{base_url}terminal/script?fields={fields}&page={page}&size={size}", + timeout=timeout, + ) + if response.status_code != 200: + console.print("[red]Failed to list your routines.[/red]") + return response + except requests.exceptions.ConnectionError: + console.print(f"\n{CONNECTION_ERROR_MSG}") + return None + except requests.exceptions.Timeout: + console.print(f"\n{CONNECTION_TIMEOUT_MSG}") + return None + except Exception: + console.print("[red]Failed to list your routines.[/red]") + return None diff --git a/openbb_terminal/session/local_model.py b/openbb_terminal/session/local_model.py index 877859f814a..969d93c7c87 100644 --- a/openbb_terminal/session/local_model.py +++ b/openbb_terminal/session/local_model.py @@ -16,6 +16,7 @@ from openbb_terminal.core.config.paths import ( USER_ROUTINES_DIRECTORY, ) from openbb_terminal.rich_config import console +from openbb_terminal.session.user import User SESSION_FILE_PATH = SETTINGS_DIRECTORY / "session.json" @@ -189,21 +190,61 @@ def cast_set_attr(obj, name, value): def get_routine( - name: str, routines_folder: Path = USER_ROUTINES_DIRECTORY + file_name: str, folder: Path = USER_ROUTINES_DIRECTORY ) -> Optional[str]: """Get the routine. Returns ------- - str + file_name : str The routine. - routines_folder : Path + folder : Path The routines folder. """ try: - with open(routines_folder / name) as f: + user_folder = USER_ROUTINES_DIRECTORY / User.get_uuid() + if os.path.exists(user_folder / file_name): + file_path = user_folder / file_name + else: + file_path = folder / file_name + + with open(file_path) as f: routine = "".join(f.readlines()) return routine except Exception: - console.print("[red]\nFailed to find routine.[/red]") + console.print("[red]Failed to find routine.[/red]") + return None + + +def save_routine( + file_name: str, routine: str, folder: Path = USER_ROUTINES_DIRECTORY +) -> Optional[Path]: + """Save the routine. + + Parameters + ---------- + file_name : str + The routine. + routine : str + The routine. + folder : Path + The routines folder. + + Returns + ------- + Optional[Path] + The path to the routine or None. + """ + try: + uuid = User.get_uuid() + user_folder = folder / uuid + if not os.path.exists(user_folder): + os.makedirs(user_folder) + + file_path = user_folder / file_name + with open(file_path, "w") as f: + f.write(routine) + return user_folder / file_name + except Exception: + console.print("[red]\nFailed to save routine.[/red]") return None diff --git a/tests/openbb_terminal/account/test_account_controller.py b/tests/openbb_terminal/account/test_account_controller.py index 33da31f200d..2d0722c9a85 100644 --- a/tests/openbb_terminal/account/test_account_controller.py +++ b/tests/openbb_terminal/account/test_account_controller.py @@ -33,6 +33,18 @@ CONFIGS = { }, } +ROUTINES = { + "items": [ + {"name": "scrip1", "description": "abc"}, + {"name": "script2", "description": "def"}, + {"name": "script3", "description": "ghi"}, + ], + "total": 3, + "page": 1, + "size": 10, + "pages": 1, +} + @pytest.fixture(scope="module") def vcr_config(): @@ -241,12 +253,16 @@ def test_call_func_expect_queue(expected_queue, func, queue): "call_sync", "call_pull", "call_clear", + "call_list", + "call_upload", + "call_download", + "call_delete", ], ) def test_call_func_no_parser(func, mocker): # MOCK PARSE_KNOWN_ARGS_AND_WARN mocker.patch( - target="openbb_terminal.account.account_controller.AccountController.parse_simple_args", + target="openbb_terminal.account.account_controller.AccountController.parse_known_args_and_warn", return_value=None, ) controller = account_controller.AccountController(queue=None) @@ -254,7 +270,7 @@ def test_call_func_no_parser(func, mocker): func_result = getattr(controller, func)(other_args=list()) assert func_result is None assert controller.queue == [] - controller.parse_simple_args.assert_called_once() + controller.parse_known_args_and_warn.assert_called_once() @pytest.mark.vcr @@ -395,3 +411,135 @@ def test_call_clear(mocker, input_value): else: mock_clear_user_configs.assert_not_called() assert controller.queue == [] + + +@pytest.mark.record_stdout +def test_call_list(mocker): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + mocker.patch( + target=f"{path_controller}.User.get_auth_header", + return_value="Bearer 123", + ) + mock_list_routines = mocker.patch( + target=f"{path_controller}.Hub.list_routines", + ) + response = Response() + response.status_code = 200 + response._content = json.dumps(ROUTINES).encode( # pylint: disable=protected-access + "utf-8" + ) + mock_list_routines.return_value = response + + controller.call_list(other_args=["--page", "1", "--size", "10"]) + + mock_list_routines.assert_called_once_with( + auth_header="Bearer 123", page=1, size=10 + ) + + +def test_call_upload(mocker): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + mocker.patch( + target=f"{path_controller}.User.get_auth_header", + return_value="Bearer 123", + ) + mock_get_routine = mocker.patch( + target=f"{path_controller}.Local.get_routine", + return_value="do something", + ) + mock_upload_routine = mocker.patch( + target=f"{path_controller}.Hub.upload_routine", + ) + + controller.call_upload( + other_args=[ + "--file", + "script1.openbb", + "--description", + "abc", + "--name", + "script1", + ] + ) + + mock_get_routine.assert_called_once_with(file_name="script1.openbb") + mock_upload_routine.assert_called_once_with( + auth_header="Bearer 123", + name="script1", + description="abc", + routine="do something", + ) + + +@pytest.mark.record_stdout +def test_call_download(mocker): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + mocker.patch( + target=f"{path_controller}.User.get_auth_header", + return_value="Bearer 123", + ) + mock_download_routine = mocker.patch( + target=f"{path_controller}.Hub.download_routine", + ) + response = Response() + response.status_code = 200 + content = { + "name": "script1", + "description": "abc", + "script": "do something", + } + response._content = json.dumps(content).encode( # pylint: disable=protected-access + "utf-8" + ) + mock_download_routine.return_value = response + mock_save_routine = mocker.patch( + target=f"{path_controller}.Local.save_routine", + return_value="path_to_file", + ) + + controller.call_download( + other_args=[ + "--name", + "script1", + ] + ) + + mock_download_routine.assert_called_once_with( + auth_header="Bearer 123", + name="script1", + ) + mock_save_routine.assert_called_once_with( + file_name="script1.openbb", + routine="do something", + ) + + +def test_call_delete(mocker): + controller = account_controller.AccountController(queue=None) + path_controller = "openbb_terminal.account.account_controller" + + mocker.patch( + target=f"{path_controller}.User.get_auth_header", + return_value="Bearer 123", + ) + mock_delete_routine = mocker.patch( + target=f"{path_controller}.Hub.delete_routine", + ) + + controller.call_delete( + other_args=[ + "--name", + "script1", + ] + ) + + mock_delete_routine.assert_called_once_with( + auth_header="Bearer 123", + name="script1", + ) diff --git a/tests/openbb_terminal/account/test_account_model.py b/tests/openbb_terminal/account/test_account_model.py index b480ad09ef2..f0071eda205 100644 --- a/tests/openbb_terminal/account/test_account_model.py +++ b/tests/openbb_terminal/account/test_account_model.py @@ -1,9 +1,11 @@ from pathlib import Path from unittest.mock import patch -from openbb_terminal import config_plot as cfg_plot -from openbb_terminal import config_terminal as cfg -from openbb_terminal import feature_flags as obbff +from openbb_terminal import ( + config_plot as cfg_plot, + config_terminal as cfg, + feature_flags as obbff, +) from openbb_terminal.account import account_model from openbb_terminal.core.config import paths diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_download.txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_download.txt new file mode 100644 index 00000000000..117bb8c8ae4 --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_download.txt @@ -0,0 +1,3 @@ +[info]Name:[/info] script1 +[info]Description:[/info] abc +[info]Location:[/info] path_to_file diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt b/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt new file mode 100644 index 00000000000..d550119c1f9 --- /dev/null +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_call_list.txt @@ -0,0 +1,4 @@ + name description +1 scrip1 abc +2 script2 def +3 script3 ghi diff --git a/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt b/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt index 7ccbf387561..34afd44238b 100644 --- a/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt +++ b/tests/openbb_terminal/account/txt/test_account_controller/test_print_help.txt @@ -3,3 +3,9 @@ Cloud storage of keys, settings and feature flags: pull Pull data from cloud clear Clear data from cloud +Cloud storage of routines: + list List available routines + upload Upload routine + download Download routine + delete Delete routine + diff --git a/tests/openbb_terminal/session/test_hub_model.py b/tests/openbb_terminal/session/test_hub_model.py index 909b7e115fa..8549c262317 100644 --- a/tests/openbb_terminal/session/test_hub_model.py +++ b/tests/openbb_terminal/session/test_hub_model.py @@ -435,63 +435,189 @@ def test_clear_user_configs_exception(): assert kwargs["timeout"] == hub_model.TIMEOUT -def test_upload_routine_success(): +@pytest.mark.parametrize( + "auth_header, name, description, routine, override, base_url, timeout, status_code", + [ + ("auth_header", "name", "description", "routine", True, "base_url", 10, 200), + ("auth_header", "name", "description", "routine", False, "base_url", 10, 400), + ], +) +def test_upload_routine( + auth_header, name, description, routine, override, base_url, timeout, status_code +): mock_response = MagicMock(spec=requests.Response) - mock_response.status_code = 200 + mock_response.status_code = status_code with patch( "openbb_terminal.session.hub_model.requests.post", return_value=mock_response - ) as requests_post_mock: - result = hub_model.upload_routine("auth_header", "name", "routine") + ) as requests_get_mock: + result = hub_model.upload_routine( + auth_header=auth_header, + name=name, + description=description, + routine=routine, + override=override, + base_url=base_url, + timeout=timeout, + ) assert result.status_code == mock_response.status_code - requests_post_mock.assert_called_once() - _, kwargs = requests_post_mock.call_args - assert kwargs["url"] == hub_model.BASE_URL + "terminal/script" - assert kwargs["headers"] == {"Authorization": "auth_header"} - assert kwargs["json"] == {"name": "name", "script": "routine"} - assert kwargs["timeout"] == hub_model.TIMEOUT + requests_get_mock.assert_called_once() + _, kwargs = requests_get_mock.call_args + assert kwargs["url"] == base_url + "terminal/script" + assert kwargs["headers"] == {"Authorization": auth_header} + assert kwargs["timeout"] == timeout -def test_upload_routine_failure(): +@pytest.mark.parametrize( + "side_effect", + [ + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + Exception, + ], +) +def test_upload_routine_error(side_effect): + with patch( + "openbb_terminal.session.hub_model.requests.post", + side_effect=side_effect, + ): + result = hub_model.upload_routine("auth_header", "name", "routine") + assert result is None + + +@pytest.mark.parametrize( + "auth_header, name, base_url, timeout, status_code", + [ + ("auth_header", "name", "base_url", 10, 200), + ("other_header", "other name", "other_url", 10, 400), + ], +) +def test_download_routine(auth_header, name, base_url, timeout, status_code): mock_response = MagicMock(spec=requests.Response) - mock_response.status_code = 400 + mock_response.status_code = status_code with patch( - "openbb_terminal.session.hub_model.requests.post", return_value=mock_response - ) as requests_post_mock: - result = hub_model.upload_routine("auth_header", "name", "routine") + "openbb_terminal.session.hub_model.requests.get", return_value=mock_response + ) as requests_get_mock: + result = hub_model.download_routine( + auth_header=auth_header, name=name, base_url=base_url, timeout=timeout + ) assert result.status_code == mock_response.status_code - requests_post_mock.assert_called_once() - _, kwargs = requests_post_mock.call_args - assert kwargs["url"] == hub_model.BASE_URL + "terminal/script" - assert kwargs["headers"] == {"Authorization": "auth_header"} - assert kwargs["json"] == {"name": "name", "script": "routine"} - assert kwargs["timeout"] == hub_model.TIMEOUT + requests_get_mock.assert_called_once() + _, kwargs = requests_get_mock.call_args + assert kwargs["url"] == base_url + f"terminal/script/{name}" + assert kwargs["headers"] == {"Authorization": auth_header} + assert kwargs["timeout"] == timeout -def test_upload_routine_connection_error(): +@pytest.mark.parametrize( + "side_effect", + [ + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + Exception, + ], +) +def test_download_routine_error(side_effect): with patch( - "openbb_terminal.session.hub_model.requests.post", - side_effect=requests.exceptions.ConnectionError, + "openbb_terminal.session.hub_model.requests.get", + side_effect=side_effect, ): - result = hub_model.upload_routine("auth_header", "name", "routine") + result = hub_model.download_routine("auth_header", "name", "routine") assert result is None -def test_upload_routine_timeout(): +@pytest.mark.parametrize( + "auth_header, name, base_url, timeout, status_code", + [ + ("auth_header", "name", "base_url", 10, 200), + ("other_header", "other name", "other_url", 10, 400), + ], +) +def test_delete_routine(auth_header, name, base_url, timeout, status_code): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = status_code + with patch( - "openbb_terminal.session.hub_model.requests.post", - side_effect=requests.exceptions.Timeout, + "openbb_terminal.session.hub_model.requests.delete", return_value=mock_response + ) as requests_get_mock: + result = hub_model.delete_routine( + auth_header=auth_header, name=name, base_url=base_url, timeout=timeout + ) + + assert result.status_code == mock_response.status_code + requests_get_mock.assert_called_once() + _, kwargs = requests_get_mock.call_args + assert kwargs["url"] == base_url + f"terminal/script/{name}" + assert kwargs["headers"] == {"Authorization": auth_header} + assert kwargs["timeout"] == timeout + + +@pytest.mark.parametrize( + "side_effect", + [ + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + Exception, + ], +) +def test_delete_routine_error(side_effect): + with patch( + "openbb_terminal.session.hub_model.requests.delete", + side_effect=side_effect, ): - result = hub_model.upload_routine("auth_header", "name", "routine") + result = hub_model.delete_routine("auth_header", "name") assert result is None -def test_upload_routine_exception(): +@pytest.mark.parametrize( + "auth_header, page, size, base_url, timeout, status_code", + [ + ("auth_header", 1, 10, "base_url", 10, 200), + ("other_header", 2, 20, "other_url", 10, 400), + ], +) +def test_list_routines(auth_header, page, size, base_url, timeout, status_code): + mock_response = MagicMock(spec=requests.Response) + mock_response.status_code = status_code + with patch( - "openbb_terminal.session.hub_model.requests.post", side_effect=Exception + "openbb_terminal.session.hub_model.requests.get", return_value=mock_response + ) as requests_get_mock: + result = hub_model.list_routines( + auth_header=auth_header, + page=page, + size=size, + base_url=base_url, + timeout=timeout, + ) + + assert result.status_code == mock_response.status_code + requests_get_mock.assert_called_once() + _, kwargs = requests_get_mock.call_args + assert ( + kwargs["url"] + == base_url + + f"terminal/script?fields=name%2Cdescription&page={page}&size={size}" + ) + assert kwargs["headers"] == {"Authorization": auth_header} + assert kwargs["timeout"] == timeout + + +@pytest.mark.parametrize( + "side_effect", + [ + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + Exception, + ], +) +def test_list_routines_error(side_effect): + with patch( + "openbb_terminal.session.hub_model.requests.get", + side_effect=side_effect, ): - result = hub_model.upload_routine("auth_header", "name", "routine") + result = hub_model.list_routines(auth_header="Bearer 123", page=1, size=10) assert result is None diff --git a/tests/openbb_terminal/session/test_local_model.py b/tests/openbb_terminal/session/test_local_model.py index 95701bbc810..4dfec207a3f 100644 --- a/tests/openbb_terminal/session/test_local_model.py +++ b/tests/openbb_terminal/session/test_local_model.py @@ -197,3 +197,125 @@ def test_apply_configs_sync(sync: str): assert paths.USER_DATA_DIRECTORY == Path("some/path/to/user/data") assert cfg.API_KEY_ALPHAVANTAGE == "test_av" assert cfg.API_FRED_KEY == "test_fred" + + +@pytest.mark.parametrize( + "exists", + [ + False, + True, + ], +) +def test_get_routine(mocker, exists: bool): + file_name = "test_routine.openbb" + uuid = "test_uuid" + routine = "do something" + + user_mock = mocker.patch( + "openbb_terminal.session.local_model.User.get_uuid", return_value=uuid + ) + exists_mock = mocker.patch( + "openbb_terminal.session.local_model.os.path.exists", return_value=exists + ) + open_mock = mocker.patch( + "openbb_terminal.session.local_model.open", + mock_open(read_data=json.dumps(routine)), + ) + + assert local_model.get_routine(file_name=file_name) == json.dumps(routine) + + user_mock.assert_called_once() + exists_mock.assert_called_with( + local_model.USER_ROUTINES_DIRECTORY / uuid / file_name + ) + if exists: + open_mock.assert_called_with( + local_model.USER_ROUTINES_DIRECTORY / uuid / file_name + ) + else: + open_mock.assert_called_with(local_model.USER_ROUTINES_DIRECTORY / file_name) + + +def test_get_routine_exception(mocker): + file_name = "test_routine.openbb" + uuid = "test_uuid" + + user_mock = mocker.patch( + "openbb_terminal.session.local_model.User.get_uuid", return_value=uuid + ) + exists_mock = mocker.patch("openbb_terminal.session.local_model.os.path.exists") + open_mock = mocker.patch( + "openbb_terminal.session.local_model.open", + side_effect=Exception("test exception"), + ) + + assert local_model.get_routine(file_name=file_name) is None + + user_mock.assert_called_once() + exists_mock.assert_called_with( + local_model.USER_ROUTINES_DIRECTORY / uuid / file_name + ) + open_mock.assert_called_with(local_model.USER_ROUTINES_DIRECTORY / uuid / file_name) + + +@pytest.mark.parametrize( + "exists", + [ + False, + True, + ], +) +def test_save_routine(mocker, exists: bool): + file_name = "test_routine.openbb" + uuid = "test_uuid" + routine = "do something" + + user_mock = mocker.patch( + "openbb_terminal.session.local_model.User.get_uuid", return_value=uuid + ) + exists_mock = mocker.patch( + "openbb_terminal.session.local_model.os.path.exists", return_value=exists + ) + makedirs_mock = mocker.patch("openbb_terminal.session.local_model.os.makedirs") + open_mock = mocker.patch( + "openbb_terminal.session.local_model.open", + ) + + assert ( + local_model.save_routine(file_name=file_name, routine=routine) + == local_model.USER_ROUTINES_DIRECTORY / uuid / file_name + ) + + user_mock.assert_called_once() + exists_mock.assert_called_with(local_model.USER_ROUTINES_DIRECTORY / uuid) + if exists: + makedirs_mock.assert_not_called() + else: + makedirs_mock.assert_called_with(local_model.USER_ROUTINES_DIRECTORY / uuid) + + open_mock.assert_called_with( + local_model.USER_ROUTINES_DIRECTORY / uuid / file_name, "w" + ) + + +def test_save_routine_exception(mocker): + file_name = "test_routine.openbb" + uuid = "test_uuid" + routine = "do something" + + user_mock = mocker.patch( + "openbb_terminal.session.local_model.User.get_uuid", return_value=uuid + ) + exists_mock = mocker.patch("openbb_terminal.session.local_model.os.path.exists") + open_mock = mocker.patch( + "openbb_terminal.session.local_model.open", + side_effect=Exception("test exception"), + ) + + assert local_model.save_routine(file_name=file_name, routine=routine) is None + + user_mock.assert_called_once() + exists_mock.assert_called_with(local_model.USER_ROUTINES_DIRECTORY / uuid) + open_mock.assert_called_with( + local_model.USER_ROUTINES_DIRECTORY / uuid / file_name, "w" + ) diff --git a/tests/openbb_terminal/session/test_session_model.py b/tests/openbb_terminal/session/test_session_model.py index e1df7830f9c..51b005f2b64 100644 --- a/tests/openbb_terminal/session/test_session_model.py +++ b/tests/openbb_terminal/session/test_session_model.py @@ -113,7 +113,9 @@ def test_login_success_response(): ): response = Response() response.status_code = 200 - response._content = json.dumps(CONFIGS) # pylint: disable=protected-access + response._content = json.dumps( # pylint: disable=protected-access + CONFIGS + ).encode("utf-8") mock_fetch_user_configs.return_value = response assert session_model.login(TEST_SESSION) == session_model.LoginStatus.SUCCESS From 11ee026f80b10dff24a9200e478bce5d9e97b16e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 10:49:21 +0000 Subject: [PATCH 219/230] change prompt display --- openbb_terminal/account/account_controller.py | 3 +-- openbb_terminal/session/session_controller.py | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/openbb_terminal/account/account_controller.py b/openbb_terminal/account/account_controller.py index dd461bb7728..0b7e331a9b2 100644 --- a/openbb_terminal/account/account_controller.py +++ b/openbb_terminal/account/account_controller.py @@ -152,8 +152,7 @@ class AccountController(BaseController): configs_diff = get_diff(configs=json.loads(response.content)) if configs_diff: i = console.input( - "\nDo you want to overwrite your local configurations " - "with the above? (y/n): " + "\nDo you want to load the configurations above? (y/n): " ) if i.lower() in ["y", "yes"]: Local.apply_configs(configs=configs_diff) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index ab7d666b615..31619e76cb0 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -16,7 +16,9 @@ from openbb_terminal.session.session_model import ( def display_welcome_message(): """Display welcome message""" with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: - console.print(f"[menu]{f.read()}[/menu]") + console.print(f"[menu]{f.read()}[/menu]\n") + console.print("Register : [cmds]https://my.openbb.co/register[/cmds]") + console.print("Ask support : [cmds]https://openbb.co/support[/cmds]") def get_user_input() -> Tuple[str, str, bool]: @@ -27,7 +29,9 @@ def get_user_input() -> Tuple[str, str, bool]: Tuple[str, str, bool] The user email, password and save login option. """ - console.print("[info]\nPlease enter your credentials:[/info]") + console.print( + "[info]\nPlease enter your credentials or press enter for guest mode:[/info]" + ) s: PromptSession = PromptSession() From 0fbcb47e1a73e1f43ce1cb6ff3f74083b9c83706 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 11:20:02 +0000 Subject: [PATCH 220/230] rewrite test --- .../test_price_target_from_analysts_TSLA.yaml | 2522 ++++++++++------- 1 file changed, 1467 insertions(+), 1055 deletions(-) diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_TSLA.yaml b/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_TSLA.yaml index a174ec6ffa3..68683663c52 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_TSLA.yaml +++ b/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_TSLA.yaml @@ -9,160 +9,217 @@ interactions: Connection: - keep-alive method: GET - uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?period1=1638680400&period2=1675832400&interval=1d&includePrePost=False&events=div%2Csplits%2CcapitalGains + uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?range=1d&interval=1d + response: + body: + string: '{"chart":{"result":[{"meta":{"currency":"USD","symbol":"TSLA","exchangeName":"NMS","instrumentType":"EQUITY","firstTradeDate":1277818200,"regularMarketTime":1675890004,"gmtoffset":-18000,"timezone":"EST","exchangeTimezoneName":"America/New_York","regularMarketPrice":201.29,"chartPreviousClose":196.81,"priceHint":2,"currentTradingPeriod":{"pre":{"timezone":"EST","end":1675953000,"start":1675933200,"gmtoffset":-18000},"regular":{"timezone":"EST","end":1675976400,"start":1675953000,"gmtoffset":-18000},"post":{"timezone":"EST","end":1675990800,"start":1675976400,"gmtoffset":-18000}},"dataGranularity":"1d","range":"1d","validRanges":["1d","5d","1mo","3mo","6mo","1y","2y","5y","10y","ytd","max"]},"timestamp":[1675866600],"indicators":{"quote":[{"close":[201.2899932861328],"open":[196.10000610351562],"volume":[180154500],"low":[194.30999755859375],"high":[203.0]}],"adjclose":[{"adjclose":[201.2899932861328]}]}}],"error":null}}' + headers: + Age: + - '0' + 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: + - '933' + content-type: + - application/json;charset=utf-8 + date: + - Thu, 09 Feb 2023 11:19:38 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: + - '2' + x-request-id: + - 55d75976-4fcc-4591-9f4d-f6488cff11df + x-yahoo-request-id: + - 2hstu99hu9lma + y-rid: + - 2hstu99hu9lma + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?period1=1638680400&period2=1675918800&interval=1d&includePrePost=False&events=div%2Csplits%2CcapitalGains response: body: string: !!binary | - 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 + AAA= headers: Age: - '0' @@ -189,7 +246,7 @@ interactions: content-type: - application/json;charset=utf-8 date: - - Wed, 08 Feb 2023 14:33:27 GMT + - Thu, 09 Feb 2023 11:19:38 GMT server: - ATS vary: @@ -199,11 +256,11 @@ interactions: x-envoy-upstream-service-time: - '7' x-request-id: - - 11239235-102b-40e8-91d5-60e465368920 + - 78ec0f7f-22e6-4cb9-9a7f-90991e11590e x-yahoo-request-id: - - 1ntt2lthu7c8g + - 8n0efrlhu9lma y-rid: - - 1ntt2lthu7c8g + - 8n0efrlhu9lma status: code: 200 message: OK @@ -221,902 +278,1257 @@ interactions: response: body: string: !!binary | - H4sIAAAAAAAAA+y9e2MTR5YH+nf4FB3tbjA37nZ3VT8hTi4Igp0VhAHizJDNZdtS2+7Qkhy1ZGMy - fPd7fqce3ZIlvzDg2SG7g1Vdr1Onqs6rTp367uuHP3df/uPZI+dgOqy+/w7/OlU+2t/sFKMOpYt8 - 8P2t7752XefFYVEM+rPJUbHu9H75u0Nfpvlk6rguFaj7k/Jw6gzyae5W43zg1tNJPi32TzY7g/Fw - Qq2cdL6/RdU292aj/rQcj9bu/HWUT5xiszMbDYq9clQMOl9vTk8Oi/Ee2v/mm6NxOXD8rzc3KeXt - 50/yyZv6h9bvu7/9vj66ePUir2eTwragk2hkutmhj69rDKezPtk8LkeD8bF3WEz2xpNhPuoX6/nq - ftDeSDU7qu8+pFF7o/HxD+bH2p27346KYwfpe2bwzoyGX+6tTdS/KHdnUkxnk5EzUZXu8ffjYvdN - OX26IndYr8wZvzud9V4l184E8Y6bv7dQjtdGd/7Sjdh5K9bpIyFmrdwceVUx2p8euMG98vtN/17p - umpWp5uj38rfAUuxubk59Ub5sDDQTDUc79dG663F0MLGfjF9NJpOyqJ+cPKSMN2MYzFnrTOkldCZ - w9bjlbWX55s2NFhO8X7tjk1Mvvlm4k3LYTnab37RcI7K/RyA8xb45pu1fHNV5p31v9D+XTvU0dxY - kddAiBQVaA/nyVyB5hsVe194h7P6YO0voPfuaL2gYfGQ7qohrfOKfllSJq239cFswmDd9d/feb8+ - VBvgbmtey/WcoVI7hyau/Oab8dr0Do2v3KSBWJhVVQNV+UP+g/2om7k792EuOT+6+aYWPuvG3mNJ - 7W366/XmTK3wUi2z4eaYfuLD8M7e5tCz471XVHUBYL9eWzqDtDjv6D7v7W22vrqrpvH9e2otV90e - U7c5d3t8p948vkS3edNtvdn6eka3o/YcF3NzrJDUnua9ZpJrdw/TbGhlsd6ieaP3tMbXFcXaPJMc - cKG8v0lUEr/6w0FDwIs7f6lcBWFBvSFdjsppm8rremu/dZDT+V0Xq4vRYHkx5Nhi+WBAcOSb8+Sn - KazzO+v0nercuwUm41X5blER66Gcw3y/6NxzNjacg/GwQGrdqafjyQn9qcpBUR+Mj9edI/o1XncO - ZruqRDHte7foP4f+a4Gxdrsup8VTmorb685tYLaY1s72qKbqk9t37t36bkNxwu9vXYwnOvWkv9k5 - mE4P67sbG/3ByKstm/X64+HGH/VGNXvr/VH/UA42ZUr/F4mw4+T1yYgqdhziScWEfnzf9LyCWz8a - DZhXX5RZMx0f/1hO6unz4s9ZUU+dTef29qgiHui84CYw9HJqJIHb93Sd+4NeuTvJJydLKtwfOCaz - R10Xg1btW5bx9AmIacFkDpQMCL/j/EWzgQ7Al3lRU/OnWfU9XYqw1uQTBu5hMss9Z62p/s03TVte - vyryyZLvTKC5b2dp6QbAewtlhnPgI/e9AwLBUAA86gdz2+7BpBfq3Xp/61YLJ/PTQkXm8iz6r7Ie - IdlsdqbF2+nGH/lRrurQanCcr+3srBG0jOOCMLzk67qDpdJ5/Xra38sPy964n9OG66w7OX0mSuKM - 7Mzcc2jenLV7zugeqhNto3+Bn5G3N6Hh179Nf0cGehrdc3YJyjeExvcONdk/cNawLCjFNRziWGbC - p+PDO6r0Pe6NqGg+IdqJKXD++U9nrRlNYQHPueBg3J8Nqei6M6H011+3ILnHHX09ucN/c293PDgx - lUsqnHtqIh5VBVpY65RcleQTpyQucVLRnq7rl4Rc4GdQ1odVfnJ3NB4REacSIPGUQT2rpr388JBo - YfegrAbE58zyqQsm9uPZlIihE1Hjmnt+PXFA12kMBvWn5oex3ZqkfLLPg621JKfmBtT//mSSnxDz - V2jwqRPnO2dKf7799s7ot8nv7cqU1KgxEuEdA1OuMoioTx8PDif3Dw8rkrw6PFej33yaXFPF+d6R - 2BKC82i66mKbcDj6LaAVE/h3kNfZHY9p541Q39FC+Og3+Tvy1niNUGLd6ZhBz5cTXA5/GR7C+df+ - HUKf3ZSdQ2LBbdiaqf3L2W/Avwv09YeHioLddb4OOEl0bDqjzE49ne12nPf3LgBIM625YqMj+oAp - JI7z6IiQ2yvraTEqJiSmFnUNZrburDWTWhggeUnRjtYjMJ0VHva72np/EURqhyE1dX5wfnrx81OP - Ub2mCt5x7uoqp/YYOhnxIjerq5tXFbYXjUVvO5ND25eYF9HjAYZyVExqAnYObgJpMo9fU/c5L5y7 - 9EUtoZ28mhV3HapQz/p9wsFdZ0Lopr63CfXUEf8CUaDR8UJAezwyhY5y74TQTAu58OrxbNInqXlc - k4jJ2Fwrabn8Px1COm8cwgVtwmkxUR++Du7c4XlslD/na4vb4XgwqwpCo/rhFW8Px5Mp2FIBNNKG - o+3YpsEX5LyORWd9+Bq46fTHo71yv3P3r07e749no+n2oHM3CPx0vbOb1wWx9sNxOZp27s6JEoeT - 8ijvn7jD/eEU89FZ7xxOxsSgpidbk2Kvc3c0q6r1DnFf0qsIUc8w+Bq9YIDU2OOHz5533r9/f+kx - NPLJKflmEaiN4wnI3ETNB0Hxazk9INr2kGaB1wrJPm3p5tatr1jAeVLQGn2Z79cs1Hx167tpOa2K - 71++6N0ngWLcf+P803laHNf05+Wj5uOzSdkvnJfjQX5COfdHeXVCgs3PtPGpKxReEOq+21Dt3vpu - iA77B9gs083ObLrnph3zGZR7s3NUFsdYAR2H5os2LRU7LgfTg81BcUS9upwgQk8yU5lXbk0rt9gM - FhqZHhDvcPvjijhm085/7PF/i2UBWqvUdQ/edmeWzWZnvH/3M/Q6PS6ntCk/RddffdXGMOkHvO6Y - hM91e9d5XEwdmi2notVPrb4s6iqHakH9HnK/RP5IPp/mZUVibjliCZWpXznqV7MBrXWHB0Dstibd - o4RaUtKi4FVGQKE+7aeKVMNCNVl7t1ogzs3KjYZUL1c9izcS1FvLkFoOwXAbGEHIiI4N1arxdmc1 - 8YSa/uHVw+RsG1XqjR/zfkHCypvtPijY4f7Czn02263K+qBo7/FV20DV6I4PTybl/kGbuhBHykfv - ipE3Iqw9Hu5uLdR5Pt4dT+tWhe2nDx/9ff3Hn3u9n3+1ZTEol1SJ8miz83f3l/tudzw8JJzuzm21 - 7UebxYCQMd8DtGWXhO2y3yr6owZraVlwlqbof5OUUEzelUVFjHRLK+gL9XKa1YI0u1a1+1R8kfgW - E2L07lHQKvZzsl1tJX/Ly41pzy9/DCZb/bfip5/Sw297P22/+mPvxbcn1Yven/nbYbi50Nz+eLxf - FS7UfZebpjW0sGCHr345FNu7D/8cvHxwMMlfP/Jf/fnz4/Dpr/7j2f0H7148+Tkb9u///eFP2aNF - TNByLqZnNh4Xe/24yAtfZrII/DiPB8FAJgnx/1QWywkz2nuNHtpArl6qSxrZ272LCXhdDlpNBHGc - xUKkoZRRJMJ4db32UhN+GMYiCv3EX8FG5lcCbc6yX60Y2WxSLezC+pxtyCSj3pgS+XD5NxquytEb - h3nI2YghAlFtkoqQT/oHRiXOIfqradogyEYqt0XKvn07JCAPSLDa7LRKePgMsUX1zi2jLWybWf/A - LfuY97p8V9SE6tR/S/+zzSwW9A5H+3Yg3JSqrkBkarWBIqb+Xn6EAq4Ub6XgyqYn/nKVpoL4bRDP - NcVf5psiyb/cI/pta5sPJMyNR4tl6zcaC7pwne/lk9IlLk0CtzvNd736aB/zTzIRiUJ+EKZpxiRb - y4LPJgVUMSsKNo0f0r+UY5vuqjW0Qbr3dGOPEvVGL999PBkTt/lzVjyo8v4b98Vsl2Q873i8tycg - ytL6ppIGNfi9ofP6k3FdE38huRU2uA/peVwNPkvHz4v9WZVPPlnfz16+yEe1+6m7fVkWQ1L7YHv5 - rH1/0nle7Hh7mhMN+yzdf+KumVhySz8cbSZJFiR+5scfp8dSSXyGkbiak5ByvD9WlAu9KlHy0q2a - xki7Jxmp3IcgdL0tX0tjmpueM/Y5JjggKkBN7xVTMFnd9sYh8f23J945Yst5rSiovLZ8fJFqx8fH - nhb8cuiKJJHUF+2yqTvN9+ticsSaxVUqE68k6C88VhhV3kzeDi46yN0ix+a4TJUZjWcIY+Clav1R - uzSO0ZTKH1Oxsn+ZERHXH4+r/KJVYLj1WMOj4U0vCmE9JXmu36qneltY/IStUdGfzsN3yra2uGWW - 1pqWo5PDvF6yLpbVOJhWu+Xg4q2TvDycTUt1cnh+lQol6+INbI2XGMLcxCj56wWONkibLaZzEtgy - esGnILVbE8rpj1Lka5yKEJ0WvpC+8NMgClIjg9uWO0ZwnzujoorzYM9VmC+n4THaQ4PdjSNpCFhb - hTAFWl3oEfXz/kGBNjd46ZR/0rDeuvz7g4cCfE4PyprEXWKeI5xZ7cP+n0+LgTMe3XXQtOsL148Z - 2Y6zYvRGmOZPpwg0T8DAtZ3UXrCb+WGaFEGWkOKW5UnUD3jwzrAYlDlpLZVSZGh/jMbGHPvVd9wB - TD3/cVjmozH1gANfV2trrjbvOn9Ria/MkZeDMy/n63IIc2k+mt6jzPckz3/lqTb2JkUxLGfDhVq7 - FexAS6rNd23P9S/T+VffbeiRfLfRGp5e4T/lR7k6wtYrHBnGEq6O4PmPNnjbNa+PvTf+IEl7cuIG - XhB40huWsGovLpPGyH35lr0qf0fEKK+nxaT96SN05c7KjzEA1u7ObfjWZVp+8vDF6DG+7pX7H6Nl - anKjX5WUcf/wfJyonXPZ7f0hp/ZOOSAJtxhNyomr4HTRTPuE6UfOXThjot0zmpZ7JY3/bmdYdtY7 - DZl4OYamjVKLFCWfTQ9coinlBDyDCtB+y3erokfLkNqZTmZF5/36qWqwzxIhIqZIX7kaI/6MCrPd - UX7kKiCW9UF1aPPPqsFDtKSnjHL38qouaDAAFAPUNjdqBeal5uhMH7EtlT3XO2aE96mVJXXp61J7 - FDJIt3hX/EB8j3BZF6950vrjQfGNmpzXNF2vX9//5eWW2+1tP3r60t1++Pr1N6bD17NJSfnPHz3c - fv6o+9L95fk25UKEKejzL0+3//bLI1VD820aHGV0f376lIpv//zUfXr/ySNU6Y+pY1ioysF/CZ+U - H1DN1zkfrtII3xTAZfBmfDQT0Z/h34Lgx7fB22C4/d8j//7jH/M62nkhckyNghtnkYTcvSjPAt8N - 437ghskeTWmQ5a4UfhLkYRCFyS5jr58fkgyWA3WwV/43dxb3+jL873+8uo//epMw+FsePH8q/9uP - X0wO//Zyd/JUvP715eCAWtCVtkdHZc0matQuHu+Jvx1w7QezJyeHT/svhkn+S5z9Ebx5fPLLs3eP - fhQ9H3Pfh3x0n9bnbx3lq+Hq6XYhlx2PJwOGklflWZ9cWq4uCbbzZfbdY+KS7lktn13EzXdx7sFb - bnKItV6f0dpxsbtXEZtbXYKWQj0bql1DLR0U/Tfj2dQ9NcDfsWvK+gUh98fx5DifDIpBs6Fa578X - O8JeW+ErhB14j5P9lguQcV8wnjf3GqeVCddn15K+8cPpvH49qw9bjkW/mzLTOY8cdnLgL/csBNPV - HjrFOR46KKE9dE5BcM/J55x1CuvV8VfbXWcCdx12X3qPkbXGeTjnhGQda+45picP31uJf/7T+c04 - 3eTW6cb4TLRLtpzO8t/839m7RTm6oHwOJ5Q555bH1XiXVujJopMLTx58U7AoWuNrulIOLL/97pEA - 1i+AkOpkLb+jRmyHWq1NzVjBeLQvxVQ5nTBw2ovFOKsYrEzmHVam1mFF/bqnnbJevyag4ZpiejFO - K/rzPafvGZjn/VSs88e6M+eoYn1s+tpRhVq6gJdKsdJLZZkfih1d47TSx+D61j+l7feWK58c5fmm - UWgG5XwNJFrPI5RsRkwjOGytqmG9v0WjrwpMBaPmLL+jal2tAgblDi3gK7i2zDm39PuH7JjE3/Gf - EkLutr7gv+Gwfjge5iXhuzn8UVB59SEp9mNllFifq4XGf2ZzXqsaPrraPHNGXetmQ1XhZ7OQvV9M - H47qJ/X+kyFNNDbEQvXqOD+pHyryQSX17pkvtOB3szhoLgJHb6fT7T6735nLfH9r/tf76/LQIfS0 - 0aI+LHrg2C50I/ync6bkrJR/JYstav9nS83fWV3sK+xBPlwejH+qH5EEMBmPQCd5Wzo7ys3s6Wy4 - S8uZ0JbQku3OJpNi1D95UWB3E61GxjplPC/2i7fE6+DlTpu3HCgvgrvOxv/3P4O/gnXx/n822j/C - 9/+50dR6RKuxWqj2W+6+u+++8t3Me/1f37q/f/v/tr5Q8n88nf79L7EufGqPpk0NqqwfFkdFNT7E - aJ6QSAjP6R+xZG4zSbz9kpbY7XumNO3Xp2OS0E9475BqC2eGu87twL+9jiRUf0qSHotC6ltJCIOo - TCtatwy1d5mHMGFau1kra5JSoPS9glp3aXOf0Sbk+wjO7T9n42nBvR02H7X4zp+n48MXSjClDLUI - +LuW4rfY4GL3CmCGS4D+cAvQKtC0DWoOMA2XzmpBNSZpf6K613lV3mfuvz2gTffb78uybMYlR3Ia - 4lP7ktC7seEYS6BTaW/4litLfesrTSOxe1+WIy5oxKdbt6i60hkrh71Y5qriCykFw0MsofamEmEc - Zwktotm0D4Hk5709iCbj+83BNqq4ERUp69ZXFH5w8kvNPEKvHQXFE+JfO2VxXAy2R0RjlMxyS8M+ - XJb5kJm8c/svp/NoRLoPe6uCi3ZeT/Gj06MuR4HNW9flign1W+3m/TfzxbeJukxGeaULPaCh/1hO - m7IkrnQeFhdoQBc63QA38YJ0t+lT9qygOuyUBNB+ed7jD0u8DdYxlHrKDmgow66WRMkP8tF+cX+3 - Hlez6emMZwWJBaMpOx3ZLFCoVnK70c7R987Ok/Dnh4/Ekx+33z55+MhHzy9OhrvjSsH6oncfn5o5 - wGrefohM6bzXE0mbqsQOrNlBnNhtPdvfJ1SQNFMUjk642qHhj2aKh4SBB3nNMvFG/kf+duMFlwHV - n4wrEitev1CVO/fmKsEtCkvaMp4lJzb2YO01AaRa2WiaaQaEvf6cxOtC06YfZyOw7Q2SgAb1hubc - j17+iG/FdM9+eoFt25o9/fnBWFXfHbeqb48GxVt8LfHDfO2S7EjrdHqCnL5OkAjd1KLFRWA/pxlE - EXBhm/mwwBECSCiyLJxv+7wSTJW+4l+tRp9xtYKdtEbc86H6wsZc+rTRucWUx0pa83OPZVxboUsj - C3/qOVTRv/UCotinsp5HE/7UC0iivzj8WoKkboOj5SgySQfpehmmmkS9HGVdi7FVCNMfHPUFBXHH - Zp4VLrVw/8YiVV0OD6tiN5/8rpjv+KiYQA0n6X82HZ9jTXaM/7JmH2rHKWdFktL6b+DACB/Ffk5a - JxwXtVfLVy0zv81bPNO4rEcUG7JvnX03CKqhu4f7Ri6JUFMlxWP7TtUljZ+OIX9Nx28K4h/f62VF - NOUlrJxGC6kJ8H6BSWPvzoLJ9cD56deXuqZy68RXp5x63IbqA/7tcfjLpDKdkHhaldO1297tO78F - v987VZTK2TrepDgES1/bcDf2153b396+03x6zZ82bt9ptwH/pGf5CXAAiwRD9Mvz7a4xgK6RDLm7 - pjq4Y0ChVof54Vof4//f//rPv9b+1/f/8y91sNilBu5P1/w73nT8glW7tSC+8/5/7ygtec0VnPhj - XI7Q0J176tKh1uNb+m4LMnUpjEsqqElRflxAniGY15RF9dlkvFdWpKrZOdFzpr6bKxKq7fkqbXwY - neS2JdRqSW3wXNy+66iqL5FaX1UWxifjJrc9QKW5DyvrDQuI8lxDf9oeKHUHaqyCtg2sXoR2Xa61 - gNOj0ng1w+JzI2h5emnNdg0wy0FcAIPv5tlZwO7p5ScsJ/32V8eIjp27zUXU9c6h9fy9e8rjdx1e - rFPe2Hc7taa2HVgJaAnaT3NiRifffQn2erfTef97a0XMzSh0+zG1SgR8Ap2Z1M7taTFcu61xqLYA - TAkLa6e9cP4yyFpfxE2DFZ4YuxiXLiu+gqkaumOnwWLuN/93T+diHtWvexbZGsi57le0Ml+IGpv/ - sNimHcKK5pohbjbDvTe/CjS7bS8D+/vCVzGXkltrT107Xh+s1+vVennHgHn8W/X7Jv755z9/+/0e - fugb47f3p/pKPO0fc6F77Q5mHyL92p31Aoaeu1zuj/r2ez1D6v7s3uaAwy0oQyliJeT7EB3W6juE - j3bJP6jkvFW1nmtpUG1WX2/etqi4/cPtb6rN299Wd2/fNuX+8JQpAaaU5htMCA1VWOUks6HgxwXp - 29+W3w6qb5t29/S9z6dEhTGHtL0eFMRxiZyu7ykyemdNzdu6vfh5W03P7fUWzOu3H7984j59EYeP - d25f+FLXJSfzQlN5iZm86DyumsVbF5/C0xOopu/ys6dqX/PEPXkW/xjGvYWZayQzx/lOnQTMm8NW - Aj2qPYTKAdTNurB+HfSJJLMCtzU2O37HUbfA8IuFws25g4V7fJBVVpCQD8rBgO9NEDwbCqB5CfIa - YdYo+RRAU8ZlN4mlphCNcddPWw7+YsWmmcMrNd26yo570Osw24/WnX2+af7bCHeb+Q8fsqzzb70R - nY7diZ27zpqNmtPaio7ai6rkH3WHFqo6fxry+dPSvZhjL6qb1osnVDnud6vNhevoPpXCxmrrzhfY - WjzhD14++/vfO+vOcOXemlAmbq3qjeU0d+E72qeAftm91bkzPxdItJfnhy5QBe+F1iT79BCam1Xp - tJZle2Hid3s/begQVzgnNP5sDwtqqBj08t0aVuUTpwdTvcNXu5xe95mzRYqaUsu+K4f7anhAi7ow - tlEf7eM2yD2lJqw/e5iFu78e7w+GOyd9UR3t/uGXT16Ex9vlg2p3+PTo1eNq9uqdX+7sPH/ce7lf - PnsXpn35arS9NTh8tfV8/OzFdvTzyzfRz39/EFKd8atfq1G+9bds+483/L2/tV9ub706fPX3QXdX - 7tP3+/tPuvdVnRfm7/Zi/2+pLYLr7axPfedbz/3+w/FRTw7k4CSST06io/6wf/Tkj/vHT7rZu8Gw - T30cTHcfR+9+Hh3U+a/R5NmLn8aDrefHP5fpEdWSvVH/XW+Ynbw6Sd+iz55U5bb/CNNXJw/eDbZ+ - OsrFL9n2MDraHf5Sbj9+dbj7+HgxPd2VDw7+ISq/eElwdre/fbb1U/UP+bf94iGn9/X3/YGo3gwe - 71s8LMMN+u5Jrv/t9sPjo1fvwv1najzfdpy8muIIRC+rDP81600n9UrjC9bFxOXdXZsFR7J5qazA - ubbp3SNV4vAuAiZUxd6Uf3Dzd50sOzq+p1vn1ME9Z5i/deey8WGuSIf9KZsVO7elPvlmWk7grWX7 - /whh/bwktSGoX9366iu20Xz13aA8cvpVXtegoxUxueGY5qZ4XZO6+ObkdTU+hj2YeSDnbyrfDfVl - hpBMRtt0tba5ocx5G/bWpSp7OCl24Tfmv/XvLpTY0H1WRDGLye44nwz+mcQpFcTCIAi/t5fz5xVb - ByZaZ4urabJ5eT+4BYdVg426X+s4hdS2/laxJyS+OTT0vZLqrfzgDkvH/sZ5q9sk69lhMTEquWPQ - y11x7CUkC1G46lZn06vOYnuey9deCfZ8s6MwoCSl1oTCVTYvafBNz/YTFVX/bxhZq97p0u5kfOwM - S3d5jm5ivhFYFeHLZFxlaW3Ommp6JEz6ct7ZNKpyf5/PeSf5sR6N+e+73dl0Srt/ATMH+XB3Rhx0 - 8mCKO64aenRkerUl3F0U4fr2Coar4ms0A/4n6upSCgp3Ot7fr9pdXQByB6cero4V9oSb5C/F28N8 - NCjgPMkug+0RQt482reL72ifry4qrPH9SFCukkDGB+Vtdmqh2MIdZzKu+LblfschgWFU2yvmIEDH - 0htP9jeE7/sbfJdGU+ygGDaEnBOIPPFg/JZ2reM7QjhBuAD0gmeBDpaBMTsAxEa5WCiG663f33fs - FiD6TIIP3HUOJkXBBLk6cerDvE+7lv0bsXAqBw6FtUc0AfXPBOQwnx7MrQiDRBc5hDqaGd8JxIEQ - R2LLf0eJuPXbN79BgVBjYa6At+/babVAv9eh3ZZsh/H+2ME/rhl0deLiTI634tx/c9Rh7uaTgz3Y - /jC3n1eiY3Gh5QvWfayJtkW/0wb6NLwuHxgsQHL+3ipt+IGFqVq27rln0z6W/nJQrrjQpZ82Cz1M - F9Y55TphegrOU040erEbXgQIV6x3s+JfHhQO3CL52JSRsXQtL+trf25Otp++2H746PkKrKi1vldW - VcfBv5ud/wiCQP12Jzwr2GHjweDUILkv3jt6g3hRFh4kXizTnTD2Qt4eceClqSQ8eSIWPfqTRqGX - +fIojLwoiKm4DMOdIPFElFQy8HyROSKjUtGOas/lEu+eBH7m+WnqiMALo6CizyLOXOHFIuu70kvC - 2A08GQg38tIk4YxU/468LBCOT6XiyJGexPfQl07iZVLo35En0oQmNKNUQEPJEvpXekEYOaGXBtlO - SOBK9BRlaF3KyE09IUJu1aWiIhPqN33OMuouoDZ9QdWln5hEILwgI1iob5lIyosi6QS+F6Ux/0ki - FEmSuIq9SMYOOkr6oefLDPiV0olpkPieiUD/BqpDapJKBREDmNFw0jAgSJJQ/8T4YgBFE+GnhKqU - hhEQbAJ5QRoeETYiUvm8IKIZICgDRw1EellIMHJRncioAgYRxFQ6cWkUIjUJgO8TOC6BRV0QXiQ6 - DUPuOg1QIMsimlBJQ0zi1rJJ1LLBlFOCSqQEmR9xiejAxSLJYl1GJfqMdwmGg3UWY8RxYBJCeGEA - UALMC9BBPamvgdsuE7wbutRO6MiUMJ0cuJknY7mTEnqDA/7dp3VBK8IHHqKQUO4TltRvmtk4i3jp - RLEkZPpxyh9F7DYlRPzuCa0WQiItKB6BoCUQhUeEpyRNaXS06ONwR0Q0Zxhd6ifIi8OQR05JhudA - l9ObQzVCTWdY/LQ7aBbDrJ94Aa1M2k0EGJCfSr3MdQJzIAhtjC/qxadVQEgKaTsQsDLMdIq6pjUW - 6f2sJ0nQugiDA2pQRhWPnsabekGSHaReHMcVKgnaifSRkEa4pcZogaOfmAYRxCEVjBLZB64S3gy0 - ER0snED/jmmQgtd0EAvabintXHyTbpMvD1xuZgXfZfK0sX+KhSywY3zJ5+RHpUXMJZbJrKuZtPp8 - OCmH+eSkRTYtM70Q17wal1wuGS65Gf0RuSTt2QU2GdDSk3IO0HkxQ8uCCyrbSja5yCIXDzGXsMr5 - +p+IRTbskZgXEWkpDwRxMblDuziTyRZ/3ZGSaB1vZEfSUvcjKhSIeIfIRRT3aLuFMdXcEb4XUAMu - MsMj2kd+WNEOyFJB/8qjALskffdE0p4hSk1MNA6DfkDknog20WcBWkK0NIxDbKtM/yRCAnJN2RFR - b/Bk/TOg8mBj1EHmYl+DvzLnCPnfQNJGB2dFZp9+JjF+BqD5fgbqEzF3xG+iv0THfQJdEP0jFIBV - hoAjpMb4J0HDPIVGQYxBJsRq0hRAkqxA1fywz/QsdglBEgwY4MRUN1Wcn/+RkjuREBKySP3CCAXo - M2GBCBiRPcIJSJaXED8m2LOK0U10N+gTaDEJE6S+EOA+CqUy1L9pEETZASGtdmrbR0aURsT/EpHo - 34wGljUEYYygCF0gm29dyiMgICROEBJBJxD0chB6OWBqkcJ6ILmJ4HFYIhIEVpxGihwKwX3E+reS - IDDo1EcvoWR+nHC3Kj8BnyAGGB4R3xbJAf8mqkxyDbVFHaThAWSykJh/wiOMsWh4GUj9G9IIxF2S - iyB0EeXnT8JtsolZsGSnB/PuSUryDLCNZHpESyWiEZLUF0ZHEKwkOk0CZMiY6yZHJMKE6QHKYFco - QSAy2OEENUvsh/gEMco0Ii5KazKkVR4GPDOCRC3ip4n+iZkImBd5CcQ2V38GQqjeEaEBcmsa7diN - ZXoDYgnvxGUZ7wJMnVaZ0L8JE0HEXZKkRHuEV0kYQjREBjAZ9zLIQI5qmqSMigYpY8ijtP4OsEFI - 8niS0eoKUq7dR3t+iiWLpUxNpcjwIZnQDkoq7DfIMhmNOEgx21jCKZaTCNUvh7cmaC6oBfZMxMs8 - xJKNolj/ps6pLNZNlmLb0/7R6zSMecOFcZ82TEgbGqQjFGiYlrAhHfjZrMoEso4nklT/Zn7PuE8J - BhJJsBezEBm0k0AO4qRifGO6aN+ZvS0TXt4ECH02PwnihBsDJUszRVmwDYX+icGonZlEAQ9SalTI - eAfVuoHv064VoCtZChEJ8lpCYyUo+5gdIpC+y+QI5CGI9W/MCm3GCMA7/hEhn+gDr8T4CNQv0Us6 - 3EmoG95HidghsVUtaNrZtOxjvaJ9taBp/xOFow6hl4S0gimvDzIsFXGJpFnC+NlawkResIL56xY3 - 0Szg4EivKgHCQ13R7o4kLWC1o7GaQ7uA8RsLONQLmNrOslgvYNqbgVq/FTQfglKmcivE4sECjqNU - LWAFwbsnEVqAxhGnvShhrPlbMcn6FSteICARqEXgs9jmM4xpECNbKu0sSY/4362Q9l5KwNMgguwI - Tb57kqRgD/4BUarAV3QkBXZDER/xNyZvIooZ19kWl98h1ZOYgE/zS2J5EILEE+UigZMJW5AA+/Qb - DckoeTeMQLpAw8MM3DclvisZcJ+H4eiZpyzCC+g9r0qa2CyTSGPZxNBZqNMQSItlRipVKLhOSIPq - xVBjCAUZS7Z2MalsVEgrLLqEJijOZI9bQkPhO4iL5eGirLHc2LQ/L8tC2G0E0EbSnZNyG0NUW8JV - rtaO+gMTtfllLMyLMtliVXd3/NZpDMurG1KSrjWru+oq+YLUutE0elrOwuUDR5lYL+iJyo1NinpW - TesfXqvk5gYCTUwPxiSxPX708qwB4rRqQKAdzqYO7rzrnxdFFo6luQKOOTo6JONrE+3vFBbPa93B - Pxx3YqLiipH8jcuAaJs9TQ/GFR8YKNd4oxB02FsYeK+KKRUe7+217/Hr7PywROCsdygwOj2OFlb2 - qjGuTuwre/ZpoL6f735uDS5r761rVBh7uLNQuD7MRxqHuk4xmYwnS/Cxh7h9BsO420U1l1m0VLNL - lCiNcmtgP2M5fwyLOsnlHOFwOcQrPlvNSiP+DDt7ewaUxX2Y74/KvRM4g+8DF56zTesVfvXwi8d9 - karsv3GmY0c/feHkGhWrTJXnQ7u/sPYXLPH1dDJ+UxgFTKVcjT9htDO+Kb16eEYr07WxSmiJU49/ - zvJJoaz9AZHgdRhvwPEgRIvVNoZWu/1y0q8Kp09TRpIfLckT/YOWXkR/qQ1V5Azkn7ZYNFmN5UJT - D7VX69nusJzaLf+MqFpNK2lSYG5W0BR1RnZh2s6bj3+dpvKDyfhwMD4e2YO06yP0tu3lxucWLBYI - BOycGFBWkI1mQN9tYOGuGmyTurWSEyyeXWoX312EbxvA0lKO2JxT4xSkyWQLz2lyVui3Z+bbrg/K - oWNiTSw7MGjAQZg0N68KhJme8z5a7AmE0xzmG2K5S1zCUZXdEV9h7XzvryKVpzC7ytjChXHWfl9d - aK4dM1lOpe6ZvK3UOyns31ITQaTteYArLQ6HFtExps7uZ9kg0SmMROx918yMUxIIys+ED/qrE3Xl - jD0LFjqzbeWnTryZK8zjrpnf3ekIG8AZuBww6jKXZIYnlj1rt8gnJ86vuHNflQhnu3CSa/t8YE+t - z9lg7vDEPV5orzEz4tr1rD+15kZ7mv26deJ9urjyLhievOaWX3PTyylZezTLMD1vjl09xbQw1XZZ - tn3b1ZYZd5tt3d499iQVqG2IiPJ6GOW2nMYCfVnqg0HfV7lfPLVvqzWeEAu7vnF8aEnus8pkw73C - wImru/XUnY2UPLpItatycWC1u1cVb939uUZcNFJOIXIw0BYL2i3BLNLT9GrZ6fCyJd05DYfpU50Q - n5bXFpDawNSuZ9xhxjRbKibTZueBgfZc+/nuqpKLW4Da3bUwrVr6c0fxy6h0m+QuwwOQvYJYLxFH - EX+Kf5hROFeUOzOSdQ7ftkRPgdSc7IkiTiBW7GctYhq8nylfLhjumzqrxMXlrbQ9NmzM28ZZQ8Qe - TDeiD9sIrFV8uATTMo5/q8BLvBgWNA8mEZQNMD4PR1Gw5cGeBDsL/tb0A/YxHDHDwnwk+l7qwUYu - HDaT0L/C0zZCGGr4L/XCZhuqmXID+J/0QraYu17Sd6kfGOcil5uF3ThiW7vHJjiYKFwFnrLRkCgK - 8wyGcuSKrqQOqAsp2AUBgMOIFbDdma1z/JcwMIwyGp5/kxARXwUR3UwwXJnkDmEQ3Qn6Lrfhchm0 - Sv8i13dDxkHIGHBx9r8SAfFnWgk9ZSu9xEKQrscdeKFZCacxEDEGlqyDJ7GgH/5RILYSH5bLFHZL - sRN70QFt7h0CBaYw2NBSKqFMlQF7N1EdGMNhC8fY1MgwBB6XVOMKeT4JrYCH0YU8jT518hPw8Agf - LmrLLf/dUE1JCnNf36x0jB9DoIl4EdFv+F8ETrRFZQEc7J2hBxsdQoNKXSXRVdLa9dgC7uo0V4u9 - 5N1QwuInDwSGJmhM754IH2vlKPYytrvTLGBNYIL4b4CNw9MFPKppk6o2oaaPwxDBiyrk/yX8FwuL - DeKYcv5LX7kSQUDD8VA57IW074BZDB4Q0kqpUg+H3+mOh1N3LzvjVHvZITZ/WyKnL0hThqEsYUXM - iVxmRSyMLzeSfLdRlR9HyqjKvdPKzbn+ZxU/6XDCz8R8MhGjB1DPFS+qZaX+pUQLjOCqYoXMvOgc - sQJFzhUrgOvLiBSq/NXFCYxZixLtw36IFRKcgOlhAjbcJqJbMjM0NAGZOsB3JqLCElHa3Fxpi3KY - +gkefoskDRMQUvykj0RkuJXIC1U3qtlYHyP929EHXMK7PH3gWp+QNDzl/s4jDaNlpf6lSANGcFXS - EBHnPIc0RMxczyENwPVlSIMqf3XSgDGvIA1h5AUX1jgSljOTm61xRHMaB1ePIV1D4oSgyeLegqz9 - JIiMlCnkKSkzmaOQVMJKmST6aMHIX5CKegEc70km2kpRkDhGghNZUM6EUZa4+Jdku57IgAKWnSqW - fuHDI/jwFotJ/aWx/puKV5MCG+wKFHR/hkj2n5KGPtegnktGJysK/ktRUj2IqxLTlMjGOcQ0Zcpy - DjHVSL8MPbVVrk5S9eBXUNWEKMD/LTvOIlVFKzErx0xWw+VklbhLamTI4BztvZE8A/jYA5ylFbeE - v7LeUICdzYmmDqTfFLRXMu1NeoKBYt+bKgL+QGNjpq8CfQsqFewQK/DZGwm+SIxenkZ2aANGXEWe - tdYd9WCugpGCfgJP1MQWycU8B9pMoHV+Vyn9rlL62SNVGxmItRxR6wRynK1iFlEKxJzJLCCJb8Uf - jVHMU/rvNmZV45ozd67x3QaRitaRBX9rnUXotwhG9mABB4141nk0yOeOENr0S1difjHnMHTaRV0X - PYvIn3VL0hxlXQeJXkGdtff33DFP3sbt/IRcEhE6Btb1oMM09hGxoS5MfyRklDqs4vVgw7b2EdGh - A0F+JHy0Am5eE07mWvyIeGmFwfxYuJmcHNJabEJhXhOCTjX7MbG00NnHQtW1I+kToedjIwaxcq8J - Jaqpj4gMRKv9SGiYs2J9GBquz/S0Ag2wslwYDe1IEo3UYj0vLuYvtkTYWd208iadF8WW6WfmIRtO - fDyXzuRMr0Wtm3U18BeIkzDiMk454hi39UF+WCBWQu4YBKxwzfwOzzs5BBipXiebHRK10ygOm5GE - rYGQthSEknCCcXjBvFvmUs9MEqHR/urrcQH86mPppFtBxD8v1ugyubxpNMJVO8m3rAHwjn8djQa+ - lwVx8uGNGt3hlL/SqpVrXRzZD2uhMeuetry4Ii6WqtISGQ/LfqMmXAtBWLoP1QZrUbsVAPKi7nz/ - SIO23Di2qENdbtT5ZFSO9uubOGoN2gVGvbBcGkdW1hJNMLliol1WLxGVaDrJRyqSWN4qtjwQkbu0 - 8Fx0orkiHxqqaHFfnBWuqLOCv5wdj2ix+DkxiRbmSPtT/4uHFVrBCm96YKHV8VbOCy7kfIzwQsvA - +bghhk4bopogQ6fdZi8YxOCM0EIrIwstM5vfrAhCKxf5DYwitHQp8QAuEU1ITfmlIgqt7vcTRhbi - /r5EF/oSXehLdKEv0YVuZnQhJlHL7+stPxg65StwWvczH1ZJpZePOsRNXHPkoZU3QlZJkDcwAJH6 - b9ncXTIYkZmlS4ckWg3BJ+WzX4IUfQlS9CVI0ZcgRV+CFH0JUvQlSNGXIEUfPUiRFjuWSc6n7VjL - jd+LZq0PiFu0rPKni1zkfJrYRc41Ry9y/s/EL3KuPYKRc7kYRs7HimK0Qhe70XGMzogN5FwllpHz - CaIZnQ3zJ4lo5Hy0mEbOtUQ1WmknMZmfLbJRc4K6lA98tthGzodEN2ofC5+Ob9Qecju9hK0uwnGd - wY647asFPFoE64JBj1SPHxz4aA5j7f/OOwXkitcYCOn8Plch4RoDI3F7X4IjXVdwJPx3ToAkxvhp - W/L5U78iYJLKWr6eby0t0L6J0HY3uYgUvjerKsXglrwmdTHpu9XEleXvean7MiL1lQTpU6O+JgG6 - afcziNDXJThfSFy+ZiH5aqJxe+V9OuF4lXfLRQThjyf+nu7qo4q61y3gfohYe86zIh9LhD1NOj9M - dP0AOrpceL2CyLpMUG2+mRh9lgaA3n7vKSc3j1P83DKW9V/NO6h75dti0DyCujsmfA7550S9atrP - q/6aG/j+fzmuE/mHb+8se2r0nvOOoB8Ub+86bnDPmX+a9D3NOENzS28G9f/tm3nNO5EKYGceYOOA - qTO1t9+wnPf+06WsA6X+THJTri1GC26Bi5NoPAKXvU3dciFsF8C8z2h36b435ppQmS5+E1CtmZz3 - NKU1Y2A9mPfjXLFemVr0QYVykvpZZFp5iq2u0W3s0ey4C/fzcDWCPezYbwxQmE224lD7YPjPFSU+ - SNgbkxx8wQfsWghwWmhb/GxujNcfifdksCBnXvJ/i/+0b5Yzc2n4UPwwSZP7V+U2uEFM/0iY3kNX - c4yLcJtU8ZqUOU1yZT5ztl/48o1zEQ2r01Bwu3sxbXPK3Fm7ybhefq7ttNTZZH4wH+KgfDVPk5Ue - ypfxMrmij8kZO2XOv2QJjq7tecgvriVfXEu+uJZ8cS354lryxbXki2vJF9eSj+hacnGpeZm0PHfL - sBrXxVLF9JwLemfeDpyrNX9T7yV/46vbZ8rEH1uEbgbeVkibrx9JA00QjCvxggtLz11AdBEF9OXx - 2OlPxnVdDNTNPcJxPnWUojlybv/99rxCStrncX4CdZQHjTItW8m6M57AHDQsa5QcjaflXqnsMd6y - o5VLXxFsLby27bRlLT1TNY0RvYv+IQGVJLIIt+tVmpPn3yFs3x5sDWPebqfPfZz22xStxbLkjKsV - ynChoNsKXLj4ec7IZOa02aOtls4I3reyjhroMmuTrbh091rzpsqdh30hE7a79i7/+bAYYQWppdVY - UpehfQX67efTFzuWIKR9q+NUGMZNo0aepdNfNIzXpQjSeREXVxAnGzlRA9UmUR9CnIQ/F0cxPhVH - kUqs9l2a1+cvcx3TVrlCGEUVkSIj3TGAZBpFQdckYxIyIeMGYKQBovElcYAbW+C79D2F4pslxGcj - SLskJ5BQE8bdgCgFtNWMhJQkJaEJqnkS4nIXLrHZJBTWUHRtWuDyJYkBfFOPBYYwgegdhEp6Vmkq - rxVxm++TDBFwfYFriqS34yoZib5+ku1wbR/VfAjNDocHgdQfIJwgpaBu2j44ySBFQdrk4lIbX6gT - uFlo09AfZNI16QxCV4D8FHckEYw/SWTMIwp9xCL0/TDqAX9ZBFmOYPIDNaCMJZVY4MZhALk4xp1P - mg+bJNUuS2TaNWmgK2boIHAi+KSIEij8OMYKpUnzdIRsNDD5CSs4uGNH8PFo/ITvTvoihsAtQj+J - GQnUg4PLkYmIAh6GyBB0EV1EuCfKwKQsd5tVE+BSXezMr6lXbeMP7D9nX4lrjD4B7oVSR54QgQzk - VsAaTLBDwrRP+voWdBHhk4YbkaIo0i1SGKUkCdjmQ6nKgh3bgG7xqgAlMeHKjy1AquMs20oj1REm - IwtiCNERacE7JEtHNI1bKWEsxUsSQZr4kc1WcMa2uoVTd3RVOGOotxEQl6j2ohSLguFN0KEuQPCl - aRonvZjvMUvH5Ce47evHvRiaKK/dTFJLPfouI0aAbpgqBqTF9UyPfPE5EzumwFVHEErStXhTxaSv - BQx4EG6FPi5bNs1vhbQoCXXdEOTHb0bskCIrcNcYkYhI77FpbKs07JokUTUpssSJfKILuDpMRI5U - PBgEoOuSPuNHcS9ig4tCjwA6oQ3LtIew3REpiLAVinhLA72jgX5lRwF6EkoBaAMiDV20L9PAfHdC - UIwsBc0N/EjatAbWJCVIHNEpUx17WBDN1M2atIVDN2/+XnUuJMwZuALOl+KTLRnyJPQEIS8jkuL3 - hA80RrYE6X6SaFlPwNqRMLGOCM1bkjToJE17Cy2+eoKAsQkTGSA246bTMILyHJFii6v5MpOZyd/S - 5a86IlwDh1EzxqVeaL4RTG0ZgkcJGgcpmwnNRJaCy4kkDBBX3BYixpH6RHR0ZVuUGhUJbYWFxi8D - 4/z1hYtc+OWPjQBtjv5mu1ZQq+dly7msJbKxbbQdXna1562VH3fHo0G9TIhsd7gQdG7pSdCHuchp - OfP7BwDnXGV9HrWLodOvjoy92Y1Cxo+zz4mMw0mhILk5CHk2KVwlNn82rLBjiD5IpdEQDuoNUuNe - /zEeFTdo6SgsOU8YwM+MrBuInofjY+cngOT0yqPis+Gn5dV8UzDzj/Fs4jwbT6Z746ocfzBm2gHO - V5X6Yh/6iPahBTepy1uJXiwPXd52o7pYcPNPaiNSMF2TiSjL5ixEyemHUjNEdD3TQKTQeBn7kKlx - RfMQv5coSS3qIhROGGbsESARiIq02DDmk/2I1KIQFpA0C2WPtKQEBoLA5wiqXdIRZQJvAN+LEUKL - tEkf52qwLCWZaNKQoLs2JeGBAUNJnPk4eMXJccDvJUYMhUp3STeLUxiadD7OhQXrHDH++jjHDyVb - iMJMpjsc7CqRXWisKb5H1KCfsPYkSRUMYpxiJU0/Kt1NPT8K4POg8xGtyFcmJZ8PiU2aT9+irk37 - OCmDKisD4UcOwhAFHKYpk1GIQF+kEomsF3phkkVQAtNMSoyLmmE9IgxxjA7wMx+GoSxLw8ymSRsj - NSzumrTEURifUMYy5mNJ6j2CLhUkfpPuxvAo4EM7lZ96Mow5pFYS+DROGNISKKc4pvZhdPEJoJjw - IKFpkwItUj8DuCKh4flsOYAdC6OMTLqLc+ZYBDaf3QwikTl2YV1KQ2p0OJyOcuAxgXa3BOEVuhst - xjgNQ+igUQx/BJquKCW1zYfdLLP5qE/Ts2Pq6/auBkzo86Ty2ohimjt4dISpSTsSk6EWpRAitmms - hTSJuyYd0oahXKwwYTFmGjcYDUNeMU0+rSC4LMFECIcdkzaNczqTtnNT3wC3APwrOxq4Tvh+0pUJ - vKRCkwZ8tFOwokKRysim7Wh0GqZKn3Yk6rOtlnZomDQD0uku7BW0VZp8jqwWqPpEH0zaDojSgZC2 - f66fpBa+BfivNqUxiFds13UXsQL9ZhnHMAlx+DeRwEYZh0TkQn6xxw9khLdx0tinanBoSYTdlTHO - uVMLm0PdBH6T7kYwqKWZzY8ikA2Hz/pTaZIGEzoJRCEIpKmsTNWxbVynu9x5IJt8DRxoJe18Mwa8 - LBkFbFYVMhZdO1ZElqPmNSrM+jGYMoufgEpl0iz+SGA9ci8yoXVt0nYQOg3vKBBSU9+ien4mrjad - iYTHj6UWCS0Rju0Yg+olWyp7J0P4xjjpJUSsI75No7JTwkgq014SwR4Ks1CYhUkPL2NRM02rGTzt - qLrqDGY6wrMhMVcDPE3BRBPLhU1a8+I0g89gYnlxRlMcRZnlxZn00khxK+bFGc2paPHiDOwkCC0v - tmnmxTaleXFGyzpRAReZ18L5SqaWN3dhX49Ew6sJuiiLA8uM2fksFqd4MZUTiWh4MbVDi7/hxbYf - zYuzmFZqFDX5mBA/srzYpjUvtmnDixNiYsRrDS+mcfoBSyKKF2cC4RKl5cUZ2x0tK84QE1N5IDEr - NmnDik3abPoMzpIgnJrVZvCNCW0aaPZDFqR0PnUkubxmxQntzyA4xYpRLlE8kHkxTU+GwyCzc9CP - T9NlaBjBFTa5ZiVpfryw0P5N7JZnXAX5XDq0BkOL+s6PsH5MbpKh6sZg6CZZp+y9qZuDnm0A5nTH - I+jgM4D22XC05PGEG4Ik83aCY15J+Ww40qSIKGU5KG7CAYJB0UMD0Rcz3r+6GW/xAb7L2/G2Vzy6 - N3eD62Lv8n1SS54G6rpMeel5prz0XFOexuRlbHm2yuqAUi3NIUHMdQh0CalvcdekE0SGJ6kwhc5A - qkLqSeGTKgGpNYHm4Mcyk5CBk4TtQGlEals3g3bIIrSEeS7j2yqsWKQQH20aN3skF1dpSTKnTCGL - xmnGqoEvSQvM2BNdmjRk0yCBuUznAzqhHL0kZOyUrYgZi/SkN8KzKyOFpavuswTKNwNaJe6fwNQS - 87CaflRaydpJKx+qQMw6shquSVO9DCqQTmeUnULBonp+CGNHKOFJRuOCigBfDlIF417Gjm3Kfy6F - qgKnngRu/AKqNjQi2HhgIw3D2KZxM4Dqd00aWItIQ6Hqccix9tMA10UyeN/L1KS76B73EWw+IqTD - 5kJQJIkEuBH840IvJE0HumiSZYkAGiQsA6QPwTqXsW8JdAMYy2LuRsjIpAGWlOyKpfLNWqLJljDO - Lqy1KztYkUIdwCbLhoaY1XNabVspjLpRgkknpVVsJSEuliU7rOWmW6kP6HGHRIRxZnNhFoqSprJq - dEt3cmXnKmjGUWpRE8MnLbKocuII1+YSHkNMMx7HZkykHPop3mqIRUT1aCGyuVVNNDWTwpaAa4gJ - 7odRN1kiTbobZVBZbXYEpx5WQUl/pWVs0ri9IaOwa9LsCIkbFao6nA8zWG116zqNQaRRLJp8BRx7 - WQZRMwZYc0O2DPl+2rVDhcXGZ6MYo4L3FnxINap0uhvxCwbS5uO1jSAL1VsWsBTptB2ETuMFDVzn - M/Utquen4srOZim6UWBjtYWw+sCtUC8YnW9W41W7EQluVeIGmfTRrMTJhWTzKlE6ab7DfCnhMBri - MYw0senUSySseCYdsBWBzauqPjxncelKtWvSW7rfHd3+qydUwIflH5NNawP2WqYpOo0G+eE7eHfC - s8mkDQAmjWtZEuV1fXh5cfe+Oleg1JaExUWkO7rtLZ17Zc8tvvKLU50ki+EKyu+SBHEPjy3gTqhe - CpgrPxXZFuxaxL7gehkKGffU1dbEkP8d09CWbvnKkwve6Qu7hny7enTOVVbPv7Bd5SaaDYxcekPt - BjfIwcVi6gaZEG6aE5DF0Q1xAlL4qb/Jh4f3Dl9Hvn9zMPSCgXrmEFA3AUejvB7kf74ObhKKnjJM - nx07q4wDnwcpv44nFS44HX4xPP2rG56IzdFaKKflBxmfuu1WVhug+mcU+6xGqBZg13XtMDnPqSxI - zvcqa+H1MuaouWoXMUkFMW7FwacozEScdU0ad+MkPEDiDCeUKZRPBFxI8J4iFI80FFHUIy3DwzPf - uGAD54ogIT1UsA0q4AANlE6gtuJ0n59H12lSZESQdm0a17RgI0pCNnlROmVlOUH4HJvuon8/bOUT - eDJV9WNc7SPwsyxSNq8ohHkiITWki3I4YcWVEQ6Dk/gerlFBKeHyph+VZriysJ2fkq7GNqkItgGb - JmU+CWTXpDM4WOAtyCTBOTpuqwQh10dwmgjvOIak5PTQn4zV+TXu9iW40U19EdIjBk5yQIcokn5m - 0zRHEmftJg2chTijTwK+HongLnxjkyYFJiCd7nLnYdLKjz12HhI0dRHDilgd8PWBVgS9TWIqEQ5D - HVWHKXCbwJaU0AfSmiKYqtAP3z1THxgwgeBLtoReTXBPyeAJOL/arqxzRnivE/2k3A80uSCIxVYA - RxgEKZF4ARFvz8Mak+5g8mnBcj7tvh2E7ZBS2HyJQDlS2vqm4S3d05UBDWMG1MIXwpbabl8VMAO4 - +vWpSM+VaZefphR6YDGlhafsJYFPKjhub9lhUy6qJ3ELLNXelcFB1MEkboaNGBBp2GpfFfjQYWe4 - DBaxcTJNokzdRk1wYzrl4GP6O1/Zpf7hz5QmkU2mtGuImHRNGveoJRvUdfUIhmRclJbqgVeV3tLd - 7ujmXz1BgTRRDrKpoA3qxxx/Rqe5wThml54YJmCTtgDoNIx3gp/djHX8GqKZsAHq9nV6C8F/Ekyn - an9L518VjSn7/LClhHpOuik8jxRxRNpJ8Ainn6mnZkObxHu2Ioi7Js0eSTCgo3rSkAXHtG/pBGzX - YYtMZELNI1pIqUOTNj0gnSLylgJAVzfgLYD/yo4H99KjLO3CV0wEkUk7qc+mfvjVwB3Mpk13Jh0w - r6OBCr6uielMqEbToUrjGCdFQDqbn8GJKeb6cRDYtG0f6TRt+iee1QA3D/yVbeLUh3of1ZeZ8hxM - 4yCCqR2TtWNw34vxUnHEjq+0FbNexG+pNlTe1oS9OpIqLhS1aCvitnwQyd5Cjx9g+pVJkJ0CPPIZ - rgbwEO7YibCAS2KXhLLTgIMURpGwgJuKBvCFHq9sb5TE1OLI7qIg46uydpni2n+k3nRO1GkXJ82i - MGmzi1A9Zh9LvUlM+3YXIfgfBwg0JYiqR4jEYLaRSZsupLLMGghMfQPgwgBe2RGZfSRoCnBz2yzV - gA8mE7uOTdoOSafNPkJ9X8Z2n9gO9T4SuI0vgiYfb0z7md1HJm3a5zTHNFD9c332y1fwLcB/5SMw - T2Y4G7GIzxDfoSX38EWDiD3uU2wRqUbMLvlxRPLhTogjQIi7JB9JP9CSm4NYlynHxVMAJ3iAmgU1 - hXCISUmSNAPyYj/Gi9A4UE0Cm7YI0WkO5cHej6o+EIpHp037Ot1F//BONPkGPD7H8INmGPChxp1/ - 9qFOuma8eM0ZB7YaH5ZfaISZnSARuE8/+a1ecOYHpXFCZ0FHKMZIxByCUpW22F3E/7+JvX5UHNcb - CC4/nkzPU9Y/qTGoDcv3TwnKz2Yea0FCmMqraTksVJyqG4WjllL+ec2JbXzt05jxuHi/uFnIekzN - 3wgEjcvqJuLn57K6Eejpj/H8w03EUJch+2Kg/pc3UE9ODqfj/mwyKUb9D7RSn2rqDFP1eWU/r716 - AbprMlqLWJxjtBawQ5xntF4A7lKW61N1L2K+JjHVi0XGMayzkHQinY7hG0iCM2L5xoIjv8GdSUQh - x/aFzpjIpCciRIKDR2UY+KidcIC5jKRTVhyiFGGS1EViGFRNOoAllRQWkyZxNo5Ijqf6bOXm0Omk - iES4nRaYNJWHZtPOR6xcri5jJH1Px1xKg5QdKiORZVQNH9ipSyQCYEhcB2QnrwR6kelGpRmsOImb - /Bgho2G7Zh8vk0SQ6ZC0OpWEO2EgqC14ayJWXpb5fshDimJo9UFAwncPfUHWh/ekRO2IA9az9yRH - uUNkLpGwKSuDV6RO434jNdA1aQGLWhTxjIiYg18nsHWj/SRLTJqnJEpFKz9lWxxCHkoorQjMLPmq - bZD48KaUpPMCBxn8H+F9FEv4UaIhnFz4bHHUWEsQMlx/6GI2cMbRKqEWEyEqhc/dwmK7uqEg8nC3 - jBrjfthQkaZbIgzR/A6vDj/eErilKyOEhAulpHQIx9Z0J8YJiUhsvuA7vdLWN+1u6Y6uDKcgVPsc - dZHBE3DQClvNq3wN/pV7gZtwkrWmIYB3nGhNQ0AYkAHjS9I2pg+ZDg8fSniS0Z4OUl90UTBm+4Re - WxzJkoO9S3jGAlAE2ldpKi5wmbCVz3cc4THr8/sSOo2tkAhaujod4Eollqyuj/nCMYxpX6d5IDjj - afIVeDAzC9mMAkcOccS7PUrTrhluAH9gjEbjg3czrCIaYTrNYDERMvl+ysuLo1jCbqTTdhg6LXGB - EmHndf0Wvudn5Moz6/uwU0Bvj8JARPAMDGiR9gLcekSget0BVlDMxzEZn1JQiiYw6OGhj1AvQErv - mIa2dMtXPp7AQRbC3y9swCClDRbHdgPyIV4i7AZEvkgiuwFNvtmApr7dIbqjqx9zpQi2j0NF6QuF - Jj9FUDzYCoNWPzEfp3aDOPTgn2oynCCGNZfjw7PBTiVxWZ+oc1cn8YYImGUQ07zDwR3hR9lxEp7Q - MO/A9TfqBXHKaAMY6rANARsEfSe2hTu3BCb93dJg72iwX9lx0DqmjaXATXBMgw6kNN8dgC8T8JbI - R7gFk27gVWm8ZxKAH+r68LRnb1nVrklbQHT75u/VD/MCFiZOTYdMYeZrTQdO+XAMFCYIANGajjDj - CP52PnTaDlCn7YxQAxluDZsZCalLETUzQgwpU4KAmpEQj+ikvSBUnMzOiIK8NSN6KHZGwFoSQMxx - /psZCWPmvnZGdLoBWKXtjOj6dkZUu82MaECua0aEzy+3ZHheImtvPIE3SBD3MwJcXZ3mW+58fC9C - RN9lNwLBaVxbj02aytOeVyc6Kh9O04ls6uu0br/VsRS8FhBrOYbfgUorL36WDH1+XwamUMmSY8yP - 0ug0dYwAEGkrX41Q1U+chRFf/YAW4TsbnhsoC2/DAxDEOOSIJ5rnckRmALLAc1GQOJnluThLzVLL - ZDkadBg0TDfwlbOBzfcjfpHFMF2TNtzKpA3TNfUNUzXtG6bL/SftfAWfZrp2GAtM14zXMF2DEMNU - NcIMz8UBbRC2sn27vpjnmnQzCpU2PNfUb+F7bkKufCCOkDoisHCmMRw/EgsnGBT0IMQTCYlSmrSB - 06RB+IkzoDruM1gwTft24eDuVtZi5YjzoAYaCqgkOmnaR1KoaBPcv6lu4FuA/5UdkFk+uFtGordd - Pim2DyLoEOfKksim7YB0GstHIJ/qp3p5JH7WdKjSuLwVh1na5Ee4K8fKEtQdm7YDQsQbP7L9o37U - iJwL4F/5MBfheyJhGQnCVWOetOQRI1hKyg4BEGT8dCvBi2BRaPPRgEgbMqUbvCo8EaJYRHxeRNo0 - 95pFYovVoyBquonAN7IQN6ViFXFKM0Iwan7BjdQ3IUwSuE1Jd9NJXDeMETgatSFn++pdHtWoTm5p - WHZU8tUTlo2YA0VpHNtsHBXGEYNE2bj3RJIo1Fr+jD5InEFor5RE+simDUgmzXdmBN974vq6G2e+ - 1yvHdU7B7kH8JKnaEvcpo0aa20KQZhIDeqT3IXa4w1gNExZEYpJfQ4TMDnf05x4LC6wC6/qQCmhZ - 9Oa6+YB7Pxnf1lyUhnDIGLe1RGJV0Lb4ebPWGhD8xpe0wpBJG9nCpI0whAfthLSyEHFwmXJEcCUL - icxTMpKWhQRk2wS3i5JAikYUUmC3RCE9DiMKAV4J2w61n3HIICUKCb7GaSUhnbTAqqSRg0xlIwfp - Rhs5SENxTXIQ1Fw/iVpkGYFj0rTFX4hLCpG0+Tmm6TQ7510UW3aOlRJEDTvns1hp6S9OqFN1GVbr - 0B5NZdiwc5O2yqdOG3Zu6ht2bdo37BwCPIcKNOxcw2fZuRrFIjfXozXc3KDDsBmDL8MmYZKSWWjz - fcvIfQu5b5m3KW2Ru4j+f5MD6sZWu7E77buzevD5z4EWbfffPyin/XE5+nxHZg2SiunBDUXSIzwk - VsyGnw9LFzjq+cw4WjyfuJ6T/C+HjJ/7kPFajhcvdrB4U48Ur/swMYjOvQETXeAGzNWOES95gMgm - MxwfiTTG41cqGcO9GnppmPHbTXDwDeCpGAVezEEKJQngvQCHX1Bb8VhVmHRxnQAiawbtDGouqQV+ - ILX0FTXpAMEXZdem8WgtxJko5MMv+HWzeBPhRU1h0l10L5JWPsATHAQii+CoTtALdT6Zwr8f8lAY - K7sivxANr35ITxGO/IQ+2QmaflSa4ULskCYfnuoc4FmGUZMMcDRI4pZOwy1bnVNCB4AfqO/DIxho - Ia0JZ4hUPezhUkWUcgSW0A8jHpbkM8MEzw7DqolzEojUfD9Fp0PosAnNkU7za8IIYhvhaWnIpDLD - dRSMBi/36jTPih/LVj6UNX4QO8Z3gJvgjSGEueEgEzKReNgL5fC+Fk4DBM4BEf1CsuAnJV/iQk/s - qqg+MGghB2kxJdR6ovn2Ea1zfrVdWfXCo+p45genJrjOwpGTcZmEdCl+8irKEtz5iFgVS0kR8nG5 - JYS0jMsvUUYKos1ni2NmaptGt3QvV7fNxupmloZN4EpO1Gpe5WvYr27IxC2m1gRwECLZmgAEeQjY - dCJJcdTP3XGYYFoD7JpLizJL+Vk20l+bVRXgwWHWfGhRhMqQyAfRnOb36/BMns3HsVqq34sWkU3D - AEabv2vS0HwSrHJdH7MVZYFtX6d5JKm6dKDzFXx8SSXLmnEgJIhUuk8SqXFwBGLaPhnupGiMKCON - dDTGdJIfF2QSZLL9SD1TjxDAbOJR6WYYKo33pKBt6+otfM9NyJUNmQlif+PokJT6TLJ1JwxkL0WQ - 3SSw7bPZIfKDLQ4y5ePsEOGO0h6/j62WH5GwHdPQlmr4yoo2LFNs7pzfealPFF/dO+Otl+B4QQZ2 - 61E+qbvCbj2Tr7eeqW43h+7nyvY5XFRSxxOELGUgEhHbykJ9hK/6IcJNpI0tbmmchnZcHPso5BD8 - QYZHIk06Irz7gkMscRqWKNLgEfcngU4MTRv3OWOig2CLkQhFD+eKQll5+Zg3BmEh9gm6iwcMcWky - DOMtDfaOBvuVHQeenSZGA3CTKEi6aD0IQ/Md9kNYMWCcEaSa27SFVqclAYmnFk193PkEudbtmrQF - RLdv/n6AWS+AEWNxOiS/kd2a9pAdDDLEk/cTdfqipiOkvR/yTQY1HSZtBmjSZjqogQwszkxHiJsB - fLSrZiRMdEg2PSNhBNoiejLjJxDsjGjImxkxQzEzEuIAMom66AD0wMxIKNnvxs6ISVuAddrMiKlv - ZkS3a2fEAHJNM6KMlAik54dZawIEYiP5PF4CK+zqNJ/7scEaxwDKoSsNBd8wARMxaTZyaiKt8hOP - eEOrvk7r9lsdZ2opZIjjBDcplebYewg+J3CB0kSQgg8LYjmZCFN4s5QqZplNO3qApvr8eK/BzKi4 - bcvMqKl/Y2Y03NaYGeeZbWNmZGbbMjMyM22ZGZnZtsyMnN8yMzKzNWnDpdpmRjDbtpkRzLRtZgSz - bZsZmdm2zIyK2Vo74xyvbdkZwWvbdkYw07adEcy2bWdEvm/ZrG9B9w1rbZkZFXIX0f9vYmacvwdz - g0xELePQ570F05gYb9glmLb57JoNZx+IrBO3Px4BEcXnj3u9BE0nCPClwPvcmGLL9WxyMyzXbZv1 - L883fnnx8CagZ3/38Mah5/GDZ58VPSqu14wQMa6qfOJy+kZh6JcXzkOGzeGQel9M+f/qpvxiuvch - RvxHL38803yvmr9JhntAdE0m+9ifs9jHpyz2Mcmq8dkGeyDwMqZ6Vf6KjyCGcJLnSELSj0TUNenY - SwOETY44RBI0iQwvr0WCFUZY7qMoCHpRSJqyuvgTpnEXdnifBfgsTpSPUZKqez/w8jHJgJ+o6pok - oh9FeBxL6QwSWr10cCYQRNKk8ZiWDwcTmw83dvUqYZYQSIhlHPvKaK9t9olP1Uj1lCqKuoTHGYzc - 8LLmV/EwItONSgOoTKjHD1U+TP5wqCQMwbRj07jNIrOuSSPMRAbje8q2BeghHHsFb37h4aYEIYvj - qEf9SQSdiuGzFfAbYT4uxUQIxEXDhw0/ZKN9HMEyoZL8fhyQoJIcLypJeDqygI2pPsJwRRwWKTRp - frBMxFmTn/B1I9RP0ozdr0IOzEzqNAIXBTDtZwixjUj0sNhnCV6OjHCVQLknklaMa0oIARSk9gMA - CxLlialKmIUU4GSFtK2FhXZlGxBCcoUqppBgg2TM/cEpi9pnUyDNkzIK0ZqEhSNO2GdLRhmc6RMp - hM3VzWzpZq/u0o5rW2xoUtCIhC0/CppMwBsqjP1I9RfEcFxKccfI5KMBgroZjm7xqgD5FhQ7RFqn - 1IMKdiT8ENGbaWpD2FZTH6Hmab+EErZVhNNPbb5BqKlvQfT/HRVn4hWfXfpiDvp5dWUCgV8WuwFq - n8IGMcHP/Z4YcFKStllPCeopol/no5tgYGkQtF3Xs+uIC37DRfT5rfsBouxTrr5alB0ty/+soiwg - uiZRFmfyc8KsOCXMogjJX2eLs4pOXVycVeWvIM5qpAAFLj60uVNHCbuRB99i0Sch3OPHJUky4uMq - GIsrkgy9uE+CjIc7sQl9pX/5f1Te5QqRS3IE/tb0I3EpLekPiU1Hou+RUAxXfIf9lx22wvNb0MJF - T/hLvTg+pahmyg3gf9ILK/qWuV7Sd6kf6aIfbjbC/6iGRwK1C7CEF7gKPPXeNMnMuCOCoRy5gqvH - 1KigGtRuhD+AyHdRJHA5WCk/MO34RwGJJfKImksP3MjjQ9UDkjrwwO+Bix+UQyV2/AOXhNd3T0iw - pV5Inur5qI16BDUVF70AFvgdT5AQQQVlBpRV1DNO0iIeJP4lqRbu5YSCA4y3kkApDPTCBURYTOov - jVVwkXc3XKQ4jxBy/jwJw1bNB4ixMqZtSttzmhPZnyw0fapGvz+ejabu8STncDHD8kw62a7C1HYJ - kTyj+EqwbOWvXdfBjt0rJ/XUITTV/Um5Wzhl7QzK+rDKT4qBMx5VJ87eeOIMx7tlVTiuu6K13AAy - zOspOKEiZnM8senE3Z2OCAGgU4UzcHercZ9ouKL4oGs1ETb1PEft7c5qaqCmf2oqPfGIK28MT3Ru - x2GqRIThxPk1n/YPlLWfF0AhCneUDwuQZN3vg6kxztiXLdzpyWEBsLk9V/fiDk/c44X2rok/DE9e - c8uvuelVkkt7PMsxfnqzrNoupz8ufFJJMKaij0VtmdO4X+aVo/64PFmEn9PoVflgxPXcgJpV0SrC - vK2d3sv7xe54fGoBgLOZPJ71J5M303pbTVDH0fazprZeCT/aD20zW7cq+2+c6Zi4X11OnVlNS9tp - ivKI9sZVNT5moGCl42/TSd5/4xZHWCmnelzAQwuUnFYDMdzXu1UO6WSCeqPx+LCgDemMxqqnRYKx - TCwwbarUVb1QIaxZIQCJeRmAOanvL8CzuIwU/zc4c7YJoBVCQSMOVMV0SiP+0VsmDizWaosALSTD - i4cg9JIjcL0DN/PCPjguscSIGHbi8ivRzKCJp4IZIuwn/yV+CC4XHgQkFvRCfsQrhKWAOKPvb8Gg - tMOf/B1JSvMBf4BDQd8nhql5NZhajA4SD3FuWNog+SAAF3RkCt7cZ3YMI1nkqWC4XrKC8RmWt7B/ - L7ZVpscl43Nxq+jvijbSJikXNonOt3vkpUmfu0VsycvskOlcpWaDNGBc2/7QTX7M7UErBYGcF2Xk - 5dtD48spz9sd9516elKV74jJ7pYkM1O9AycfOcABcdvZ9GCdBlcU03K0f+nd00wALFsEf0ZSMYtv - Ia3o2IM/BgmnvscO2TEN0os9+OZCuhYsLUcQPWEjdSED4y172lExfw49dguhjecpb0kWiiNK8uVC - N2CZm3YolYyULOg71F3MjnDSjXlnSbWzIOIKn8shIAtjGq1wbG1cOhP9gHeV9LDjIsdjZz7MC+Rv - hEGijlICgvaxS6NLmQaEgCIDqCxECyIItEfdGEBi4BhsqJQHD8/BZawTpNjRVJTF2BACfAg8uSnL - +BJtppCkKRGw2oDsFFI5ipEk7sm+YC0jdvjRAYwXsWmBe1YQgD1ggJEHjLoSQjbmhP9mgAFNU9mE - 4Ut5nrq4he9ASE8Z6Q4TIsTH6asBKJd2lr6JgCFyiHR1D6ziCJdnLmGqSSQMrUYOkUmaoxroYxcp - AqShZZlShXAVGNe6Y+hPKasjcBdy1YIIGcuI6B277B3EwLi8xFKl6ASMveBjEET8UwzK0SmKiP1u - MpksKmPOycaCOLlhaaQpbYlkdzwakTykdqaihj0usz1aIJxnFbwM3WxAWCCcLdiujXKaNj826cwi - b/G0bDnpNDg7n3ZaQ0OHUHJp4tjCMkIzMTUjzbrvsxqL10cMSUu07g0xQJEgAUqDvwdEaiCFEO0E - PUM1F1uAsrpogogd76KQpQS0XwfcCgeEcNMuYcXBIVykXN+I1PW5Nd5syEhYmxZMkNnkARc/R4P1 - bqhsCf4RYs9vEc3cIYEkOBBEK98xxUfguyOZID5CvBPz+Fy1h5lggSYxDQ/YjBGDVLuSMwX/FYpU - OFQkVpSAJapAERNHUXegCvQFz4O6DF2APyRFHcl4S1J/fUhQRKFAL0FxiVTyYNE5KF0AeowyRCgr - V1HWrSg+IiGPUayNJhpKRxlbYnzlWREMChtgHGVrUaAJJoUJszy2pXAsMzdgYwb77ALtkhkRXm/x - +NY7Trbof5AbERqe/zBGPwLlOiEWPyN1exnh0nlMt2Y1EakFomVpli5oSdYLq8WTGDeeTZx/jGcv - 0Uv/ICcaVZ0p8C0tfxnyZaFZoF4NlNdGvHSTH5t24YmnC9Eug7cLiH2wrDjajYQY15RoWX2QHxbO - eM/JnWlRFZgOyqSZLIrRpclbMwu4PJJh79L+I+omHA/3rSBWpbz+Db2B2KQ/CZfpTOSy3S92mZvr - PQ1ujueuqIzaiFAbmWg42JDQviAKQN7JmGYockm0EAIeez67kkVGL3yBBxCchN3BYyfx+0yDPCVU - hKjNIotgmQmSV6jJEnJYngtBySKl9zHRokIMaugqmkTiE4gzU5cQEgmLhixcaaJtvxBmBAigqqQa - gsGT6Z1E6yzBJTxCJdEyTWQMhCy5KRyA+mNkEGfZUOxEtJRqkXkgMzJRZl8SEWMWZUHZNKIcRh+T - 95BpGDfosd830MzKLT+RoabEg7tBrGaJmIFAlzSYvs/4Y3BDtlLLXsiaMaGgz7hiqhi6zOJiJr2e - mky2lWv1OTiSRD7ZyB56SpLVGQw6qigDe6hM62hQLQAvrgQYIWgqDPEuF4w1FWdB+GOQU0pP8/1J - PiTSCIvdaaJqSzBZXRQDfzioNouRJay2sCWt282Xc5XmVtnL0M9yoVpDQdvgXBsNtY1+bCp6UdOS - RdultOc+YWiSX5pMtpGtleTI70NTzlwlv7ESA8qFJYy9ja9M0AQoCmgfdEymk3i6iIWXVKmXUO5Q - lf6AAvuuak9RQpCqDIQqiJlkCdChoCs9DtqFQ6skY7U08VktR2xKkBJuiaCssWnpm+6jTy1lTKb5 - YSW01w0kkwmEPPWgGxIFAmUmogTpC9GRNG3qq0a5ucBRIDuZwyo/2nTUyBweL4bq8JgVFhymVKmy - IiQaW++eRKT28jWlPmuISp1lswNIOoYE9Ck1WFEcJrmh0qdBXCC/Ke1dy94JK5UBc5GEya1kYTtl - Kc8IoJlSl5Wo7CqN3mXdlVkYdXckfSZqMYvUCVv2eJQ8bkDFxWrukM/k0FRfq8KMO67J/ToaHmak - gql0ojRxj1FP48ocPSKHR4na0Loxbu4T7Rko+DZg0GeUsVDL7yDSsEQNJdvDE4kJThgDtTQlWyiU - XMvA4ciQxypYME9cBUvMeBPMnmLGI+WxrKzk/JQxHnrKIJEqAw+L/YlSFHSuwAweMQeOXFgsMuY2 - sattIaqAq5UL5tk4EGVjh2raVZK6q/vTR5sAVxtqGDRXsaGY+ZNgLs9DcXmO+RTS5cXB41btSm6A - 7SiJ4nDYxQQlr7tIsTkNaaY+0rJ/N2QTlKAW+xEvb1wiVn9oI9fa0kGdmE9uoFUImH3pRx/WIGxv - tbRVBkrXLhcLXFuac6Wqpr6oXMdU6XNTjgJJf0dhYu+wmEkoiNJVEoIyDuEvTFI1D07NnPrSd3VZ - tneZr64x1vgorJZGwEZBFrIcJWiBonEd9sOsAzbu8EtL/KGvSzoaAsc04XDLKOuyAVB3SEjOlNqH - lSvMZsDyd1RNwRJE4vLqDFz9URv0mWI4qi2uWCsxL3ZVGS3VmJUglOUfmTUvocDU7etiji6i+0hr - Fmoc3fplZBP6qM7LFs6Hh6V1KB+TmD90q/H+2Jz7LnLmfEFUaR1znj4fXmrHQePm2HJUHDtLu78i - d5d+2nD3MF1g7hKhKNJTYJ4+lrT8HUAyxCvdSRYtPOa8bymDX9aXxRAjZvvpi+2Hj54vRwoj0IWL - ybyjCX67Ez7ahYA2HgyW+/G0fDXxDEJ4gGfk0p2QtqvY8t89wV3yVOLxORGLHv1JETbDl0chqRpB - fIAncMMdmECipJKB54sMnDpFDEduz+USMOjgPiVfug2joMKmjkE7Y5FhpychVrQM4ISRJglnpPo3 - +0ArrQeai8T30GcjkhT6N2KhQIwnIQEXZtULn7QtQuz6NICDSBRI9BRlaF3KCEqRCLlVpmqZUL8R - lJTlHbzdqFyFE5MI+M42B5LDVfZQhftFTJGY/ySReo8vrohk8xVXKZM+/KHZb0eCAeJNCcRuDPRv - oFq56PhBxACCDach2+xD/RPji5XNSfjgIykNg9+AQF6Qhkf8fB5RtoAjVwYZP7iZsSNLqJ+LCHQi - w8t8SmQKIkgskX7PjRIAH4/+gmBnYFpCuuw5z12nAQpkGSx0EpHT49aySdSywZRTgkqkeF0i4hLR - AdvJsliXUYk+4x1KpsA6AwOAE71OwF05CBVriiOgg3pSXyEhNmUC4i+IJOvwO7kJzldlLHdIWgmC - A/5NMiIHmuADAmisPmIW8W/cms8iXjpRTNo7Hhfmj0KdLIQ68e7J/8/emybHjSQLg7+lU8TLripJ - 3Qkk9oUS2UOJlErdWlgiVctrK+OHTICZKCKBbABJirWYvUPMn+8Ic4b5zMZsvpu8C8wVxj0igASQ - QK4gKZZY/Z6YAGLx8PAtPDw8FEwEiRf20RGgS0LX0JgwLWsk0Fhq7Xvc06B7tZZk4jdD0+jI4ZHC - M+LlOHOwRqBpG4mfHim3NRvVqII2hsSu8dMslZM5f6A37qlUB5oG9CIpdJNXA3bA/V/N5k8jNLIx - dxADmU2SgtlO5ZGOuW0COnoLFbVs2iO8uNYIsJICnAgvAWnop8UU+rQfzD8jG9oIw7TVAeLKZFkV - VboPRS8MtWmmAAsD0dGUM1D9WQbNxmqgMs++qyOBNrMgvOpZbzgfCpLFW1UUWX38RxxdkkE0uRJY - 9A8sRi8KkYnlUI9K1UEEmFCaSpdiuGgPGOcyjKPppLlKKSAV5Dmth7FOsyij2BtOA6emolPpjHsQ - WD8sBKrBZ0DVqh8OqNcg9eJx0uvs/b//Fyx1FBWRSDKt9hrKECd0azubYlgzBZGFq9ZEuvihE/7q - hWLopT1/PMFo9SmYAMX3tL9X4/635PFrKOGH6ROR7AcBiVFBJwTqYOipK85Ne00A8Mq4/OANYXJi - h05vRA+rTRPqrkxHfkISqAZUEmLE7TT1EuIMBt4kdcIBLRNN47ro3gXTscZElFwxJ/gOuzwGJNCr - 2KrxOVGEoQK0rhCdCQkvt1etWR9blUycsAQ3THZnj854D7/VRjFvOU6Y5AtncCVMosAfXJXHe8S+ - kSP+rX60lRb2yrXEhjCyOXqZD5Ms8TCQEO8vZ+S6gMUZzYURozkQPJR7yg2syNZNOKa/2IA3Qbrr - J9CkP64GyBwU3tcju1Bzb1Z6VRyTKld+NhhBlk4SH5SAn1aocL/8qR4v5fp7pTp3Hzt45aIXDzwB - FhOFb71qlAArtYRf88Yyhq3UawVd/dhzzjt734T9ZPJ0I1xX+ffWKNOlwd2JHw5ZK/S4TolAZyUY - CmdZR+fptK61vboWPheq9T5Ba2FxV6Qed+FVOqZL/LHjeo27BiXEvXVovC9599OLJnxhYzAVQng1 - 6OwVyreHnY3o7DiNBufk39MIzZH+FSPzFbGK20fCBkiuM+NWw3OxShOiS2XmbMKVUF3V4DPvVY1D - 61kPbP29hw+5Y8t3swh5oY+BAbMjg6uF0/d4rT220ig6ywJgOjwYwA5VAE5w/xpPIDh9WHGAUTk4 - z2QlfGYFV+1WyNvl5YMI20Bb1hte7Xa8T4NgCuywl42Tw1SCiPvtCm8mUULPfmTQMQQWGxilQQ4s - 75q+2u2k8TQzTKehnyKXMZD5MqJHaTfpfYfEywvCWqCPMyB9knaKn3tsWXYaAE94cT9yYvd307Cq - paBMQD+engXep99lSdEWF8mmiQ2rzudIk1Vk24bzhy0QTwDUhSf0vZEfugItv+l9rFrxCBt7yr2Q - MoH/4Qo8lwNZymSaTqNmH5H7Fy8jMoijJPFcDIQDMZGOnBRPuowxCPfRj49E8joFVnf9gUMXNeTS - ucJ9Xpanw8HtU2BMh7JLF9dEQLJjP8GSYZT6Z1gPPmVLsaL3MntV3I6cYZQffKOxaSLmsTNFydzH - yE+6n0pk9oawj/y1RM+qZaV/fSvT7XYpwBP1Av0H07bRf/CkFzqDMJ0BTTRssH/oI/0g04NfFr2j - g0Zr0GBZWpFmfwvKH3mrrJxUcUjMHOeUojj/95KRE3vujH9B1DygJ5I4O+Sr6iNn6GEGqBQK0dNH - D56NHX92QgWUBD3qBJ+BBB48eIj/PaN9PYD/ikxZLHt6ir4NqFEsMIx99/QUPRfouig8jSvPCT6T - cnvsw8QZUL8FAMJHTKGY/SxDRIsLspRz3HKYh7Fz4WOSmTLsc8DUgRd4Z4ilZxgjNgGcP7gEAo8u - RXpz+TET/QnZJbWvf/+d/PbH0/o6YIqFZ/6wqW7+Gdr4189L2hAn02T0+DdABT0m9QZMgdfuDnn0 - F/rITAOnbw08wzPtvtXX9L7WV02nL2t9wzZUDV496kJ1sBCg3WMvgA6iGFug/Qlcx63WSBHEHfJb - h+rszk7nxEsCp9PtJFfjfkRfHL/Zh2cMK4ineHr8tdvZ0SzJsrqoZ0ZOOPTesMrhNAi6WQKmK6gL - 9VDKDKMYn6hkhlegplwwOZ+znEK8FhgUIFcuvO+dABTJjiCJki1rumUapqrakiFrmtLtOP0kCqZp - oZRsSYX/DEvJAEh5Gdk2REPtdkDZXPjRNKEClL22ZOg3QheWy5KypZ0dJX+1z/sqvvvAoaTvLrCD - I5jU/Ynf2fmtM2E/3/j92KEDHgQ+tAmvvnVCNwD+6dIyr114C4IUXux0fpJkSdbsfcxBCmO0JNkS - Ph4f8KIHoIJe+l4AKO+8wfgf9vpDFI1LrXyoaQYx7Y0nALA3q52i9dHZMeEXrOAZ8v9gDzc7khPo - MUmd8WS7IRWb4WOTs7F1JHtHVnYkney/7cAgJ8E00SXphJEfRmVDmeIajBeg6y9eipstfwczRVZM - TVe+SZ1hssvtMW6OnSa0kOuljh+c9qdXiRcE30yCXaVD6e6EY/cE7G3o8ij2BNYAwe8RG0WhwLv9 - 485sTthLxiU1AwPWTF5O02kMH86cIPH+ePjgjydPQe/0euTk/cF78thJn/hn5PHbyPUCkZUl/7FL - sMknqNRxR/IcBtsll6Orv68qxZaJITw1IJzR3rYTSUVp1I4kWlEIyZh6SDdNXZVVRTIk3ZiTQAoU - KcgfINQ6+YOCZk7+YFqdz0T+SJq6nGvf+u5iXi23UuZVVvm6pc+q41hR+CwZ0ELZ8/4MLGU8ZMwM - dqVn9nBnpUkOMfqbkxX89QYioqn/orjANVsuLTjTO677OmewF1Xrp/ZrxfipKzOTGkyMAHujSYjS - 88gJveAdrLBAgpSqUpnwIIiGfvgxDuDr7HT83+Pdr5UzJnXhRwomi0CfWJ38gHu1Xi//8gjK/dGl - ljyTY9QUWyqmHiKuwMrndmbFys4N2OqyuUYw8oyKBR/FKuWpqbwgH8OCiuVPaEiX3wihN2SypW6L - iG+C4mp9PWOzUwtaU+6GpqHUjbtUcSQ31PNztk4W1GeDLOyDVVphqmiPGseNu2KrNpYrosVN4H/M - 4Ucb2+sSNMV55wv75vA1Y6s3khfgsnkSqMbZBo9cNQq0oc4es823xSd1+k8iH1fK27aVqfcMwqUz - RJcgi7vbEqTMKFkHJMn+eiuYGhKx5p+bSSTLH4P5alYY817BGF4ZUXsF+3d97CKE4ZD6DAQhGTsB - WhR7oFC3QEpT4tp5V3RD6Y2EOE/rC7J8SRKdBS3yNtAJRD1B60rmcmvLkvPQpsqphheNlhlKZOS7 - IMXJgpJn/ifPFahjlR9BExAtUSz0kWeysy5HfIFHTPtrDIuI6YqNvHh5QHiOoYRQc2kchV7mnUco - BKiFOZaOUydOCbfcFmUy3mQO0Au2tNRSripjlz+thrp8O8IVZous3c5yFV9TbRa8gv6k2hJsa0Op - /4iWBl3bNXyGtfdu5+OxZcmG9AEdBbid6aIjO7fxls9PdY5AEIG1KaTRRBA+UdGwKuaWS+WHZQco - 20kp5dXeaCclc7AuEfeLxfnagq2KuK2JGzcR0KZGIzOEcZGM64TCl0VCpSjiubxAcSYIrhACM+cP - SZaUa4631xMOi5RFozZYlq9qTdSWP3FXC1r2TUK82Yxfx1lTb9JXlxOssUVTtqmxv6TySkYqQ8da - hqolr2JrLGt4XWN1WXtrG6z439/QbbW82xbAW9t4peCh422xAbsqfEskHy3ShjFLG6LgrIW1vQY3 - zXqNrGC70sJbaIFaO7amVnGDvfCzshkpzDLagnCPggAjkIrKd+y5/rR4TugZdbDlaaDoA/1XAPE+ - a60fxe58SHbaj9yrUt3TU/quzmxO42rJxkyTqVst2mQ+0+JOofTpKY0CGgSeE7PoGyoTMGYbTGhu - uw5j54o9X9IoAZ6AmcXm/KVgDbGdB4Hm/J1Pvl2ertT9LEdDs0pi3sh99oOgy/bODuM0mvihT631 - bDzv+Zu7N6ZslwtjQi5onGsWxLDP39y9MbGwu9A9BakBfxwa18xekRf81Zqjgrdx1bqjUqYoHelY - S/Ixs9/rwyquO2Yij4ILhkc8+uuACpMXI1jp1sW1xV4yATIGnZ5V6NAGStX+v//7/6F1MJkBrPF4 - 3t0dgqY4NOqHk2maB6X94nwCqX+OWSpYpBUo3pSmwKULRGbNdwi1IGC9COpRkBQBz7vRg8iqisNf - oclRQ4vjH9X+T//p/1u7ODx49c659Kfar+bw/OjlL89f/tr/9s2RfBQdxf7Z5NNPuyt2NYgvGjp7 - /FjRZNMifyWKZerWE/I3Yqq6/KQzowSgBYo3RiZikn46HePyw/HD0wHil/yWU1TfGZwP6ebgDvkL - bpdbDvkPfzyJ4tQJ01lsKOWJxUWY7lylDFvz7hB58ol9+oNBOoy9gTOB1bcj9B136BXgzCgACSCr - A1YFGyYdMd9SwU8XTkzcGTl973uXY9y7JrvkN9Kh746jaYwnYXZIRwZ0drr8/fcOjJO/zrbt+KdD - vkU8Xym7kY9+UYGe4MPJP1EB5WW7WXNd+sQL0dpZObr9hpnbcGvZuTo5b2jAsu2s/myz6/UBlmSb - 2cX3J0BD7Js69562fYwBZ9gW9Qq8zbJH7yCm3h08p2XwL5R4vn9IH587GFZJnmOaFXx9+IG9pge5 - 6Ju379gb+IuPH35kjz+S40s/Sei7kxP2bv/kOPOH0fcf37D302CIscAhOT7E9wcfj+n7g//9v5LE - C9woPsPXL48ZSC9jAOZsCqIDXr76/jV7S3/Ai2/339Lnb1k6avbqHX8VhtEFbgkTuidKh7t/fLD/ - Hb55w5t/E4VuRIf29vUL+gb+Hv5IX3xkDb393/8rHIw8WujHw5MP+/TtjwA24vbkI0N2CivvocPA - PP6BoYXihOTUhbTziiEH8YKbavTlj9+dsPLsxx9YDhZv+OpfOFcgO+jn15x+9i+A/h1+sgS7GwEz - zpWiDRWrvw+9HzzvfL6G/ENd2begE0Y1hd/OFT4BzcmKJ/Pl1fnyx/6nptLGfOmfTg7my+HLGph/ - ArVfA/JP9SBj6TqI54u/BJ3WUFqvadwLGwrL0nzpt84nfwwrmrnCb/eRGH6G90flkAaKgLoQKFZy - FtEwEz1UBHyMA1q3x7bHe7g53qPrWiqxiqKOxkeUJUdGxjldriQHs1r5qBgLkufJoPqZjj6XShxN - jZ2oqmw3dlKRY/W9UKG3pBe5eSBMKArPY2/MZENDLyhDl/Uim1pzPwXZ2tDFhx+XdlFQLfM9VCR1 - Qy8o15dNirQAXyD/mxtH5bAMS804quiO+j6oolnWh9rYR0kR1fdAddayHnSluQ+wZ8LQIYc/krnv - s16YElzWjdnYSUFX1ndAleqy9pvJqah4mzp4t7QDo3kAMy3eID72l0+0ajQ2PzMJ6pt/s8Is61oz - fnIDo755aocsJaLm1gvWSkMHH5djX2vmNWYCNbbOLaRlA2hGf8mSqu+DGl3LRtDcwWuQqAsmgFpv - y6jTUpon+LuP708+vP7x4OD9h5cLEMWMvZ9LxnvexixwDVvwk/fT9P0Z08QY74dlcDeQftsfe6DE - nZBF0Ze+JL6Tv6UhwIVlSGYOzLyFyW6t0viGrdVn5U6zJk6B0aMA3n0zARAid7fORP0G4w5/hdXd - 7qGT4KlCcgxLR9eJXUKHgmZLKQKWoptuq4B1g5/3g+DSuUreOqE/mWKUJS0/jWbls7GB3Vw2lLj9 - Q5dj76bjPnsrYaPoofKgQtV4/NZJ2DCussnPWv+YeK438MdOUHzLFoqYaz3O54X8wZaydWtVkfoE - 6IqVuTN2yFePH4kzd8G/qLeAffv50RPxwgkeP+nSwqPmsqNK0UF8sZMFZ3r4vqkiFMyrPkHAH+Aq - e5oO3p+dJV4KgAp6ObSR/SJJPCifVB5G0TBgOZzz9X7PmfjiL8nfwbYEk2zXeOMlmmV93Mf/XqTB - e+dccU319eXZZP9Ak19Nf3ozPZGPP9AtMNZh5gCY+RDE2IPZefz4CdndAyzSTByEfzv85A2mqQdQ - Z58L9Tz28fGjVcF41P2NODzs/BGbTOpneUT+eCKmIGYfP6bYZF250YDyh/jvqRdfZfHvjx/9JYeA - Fi62Q/E+RXDpp6d/4AxUxvL4yVM+kf8qwiDw0Tz6GWqXa0AzT2f4Y16pss8J/XKNUHVmHr3GEsyj - taAA92p1Sm6rB/U+qwcPZt4qgTuk4mH/sdw1VbDJrC6RROvJUxokvOBj5qdiXwl8JjXfSz6qB388 - 5EDxfd3TUXw6nTTA9BfJtmxNbqiGt5g3VTzclxQpr0jHfez/6gF59v5KqHwgCT7nYVLkrz1oqeQe - LXWLJzlHjh83ddjX4X+DQhVhguHUAo13opUa22Yi/ZQl8AEhz9FRQL2iAWYts0tshSI2mjgDkPQ7 - RNQLeKZxdLwWyNMwQQmJPsPmrnIUFjvT9S4Bc5AY8nad0a+ee2vkx+MLd4hCKa9CjkqZHPGI7nLM - l4uvgL2cGvA6OkoRZI/gBjF96Q8i4QyEWDJPIFg1d8s+eEb3VuhRf/oqGXlenk6qxw+v9ihd9+jR - fTcSKLa9cAiQsnYScZDg7vmDZ85kIsRRlPJrDNmeJS2PAuRZL/vOD5Q2HCrVJX5nHz+UKih6HgRV - 2XKebby8D8nJ1EvAZiGS0pPYJjtNUY/nk3H3mXpIWO4nj7A4b3qWl1w6CfmKGSqYJAqWHbMsVNwe - oGFnWA/TIRQaq+2UtYE9Qat4WXqXpZvy8YT1EI8AwydZtLWvxYcUjkcJ4QGyIIX91An8X1nKKqjx - lSEboiST56R/hTlGDfhFwU5INE0TtMDwmqXCdtOmqJ0ZBEzFwGQF/Ch4L3D/9kuSh2cxp3/n/6Db - TXguheTmQwJrlrGDx+E7XV4KW8MiL/e/wyPR2Xs8C30IFl561UE/VKXwd4BWuvPES6Oawvevk2zq - aNy8Q55Pr7oEVHSAB9m/jQL371kVlszLc/fDBBNe7JBqH/wDL56NhDXP2x85mJps4IGF6UJnaKF4 - YQJ8hTnFgAxgIvvTK5HeRunAWhmPfPNPyQCEFM4gzawGf/tOQq+lJLqNlXi5pEsUg4zwjsn8BVaQ - JYJnDLOXYqewDYOLmhXx9QPmBkACR+LVFeHS885JEF3SmzELmNwaa3hjHjTLr4XAVCTQHcHuaHqy - 4qQhOLIki5Yqkv0BoAkpOGMwrocZg3V5PcAecpJifk0cTGZwFkfj6og2xRCpRdHIH46uA0e03VWR - pFqaKOtrIEmXRUVtwhF2vRUZoazk0Q8J4gZsRv6rbSzZ+qyj6OwM1qS5yK3rF9j0AkAjGFPkhPye - AUSooeiipHT5DXUM+TjAMaw9qRDu6pYEAppynEMJuPRZwY+Mu3nb+efYwzyHuNiGing/kE1cH0H1 - MHtgjnw6x7nmYUqmPAk/z/ZKs4XZw2dMdSb5lZIgaNGsxOQNmbUxwuSFxYgqftCB1B9/yDbq6y0k - RbNBDtF/dOnJ0+yAWzIdQw9X82AUr5ZZQSTzaAjeHKv6mcpZtDAY8vfu2DysLOprZ+P25fefDPO1 - GqQJ9betFu447lfUTE3Y/5Orm8Lkbrw2qDvawnN4wQIcj6+fOu6pfJ0ZwzwXpoIlAmOpwmaLwizE - 7VOSRawloTNJRtEsvm0uym2lEDqanbg2hm6ugxUC6GZ1Fh4rHyk5nml6NCFPwUyqL2hs9IwNs/PL - 7FDxMe/uWW+kzEdpF4Ahhf/qipye0qnBkMyaAE6kMnn+rEF9Q2wAnb3nvrvaYZkN4JGQjdaHh6Aj - 79qAAiQpxppA7SfnnxWSAJ5rRVJ2NmJNsA5AQF4XSNYOiOujt2tChPth1wWRZptdW5ZRjawJ1ffU - SXt9FI7pbtYECTcARXYy5rrg2oDO30+88LrAydx5a4LENCd54UyuCzDmW1wTLLYVS/PPU5/k6gcg - m9tGvVbK45kXLepQAa0rmpB2Hjj2RVjlqPqCIfL21wE9xy+pecet4QXAsEVV8wmJhVMBKwYMCyVv - osvtT/uuN6T6T9x2ahwOW8ZsOdpvoZM2DjdHQ7y1gmQ/BEHVhAnmLg8XDaGujdPTvhPP0e9qNbP0 - vGsdjc0q0y2E5T03N0DP8xfmkh2V5IlxsvOxLR8zbz5cuCr8SwRHc/GWlUZ2uOjajCNb1OV11Wvv - kHzAHZxrM49gwWuuCdTL2PPIyyByMEkx+fra7KT1ITs8OuaX41yTiWRusDCJonNCEw0STOVN1ez1 - ocxW1gTvhZOMcDIvV4HuYeH3zLGw1UZwsRbfPH3Oc+Z49LzgLCsEO5k2tw4u1WLr9Neqoum2rA0c - yzK0gXdmubrpDCRJlzT1TFJs7qvwE2EQX03SaLfzEoPIstTueLGyMI0D4Wzg5InCh+UE+3hzEesZ - U0lniX6gwlwjg6v12sgv6Jg1Efbj/nqNYI25ZpzEH6zXDNaYawZTk//qJetjp1CxsdF1sTWr19jk - +tgr1mxsdn1sFmvONTuKxt76KM1q1Te3LjJ5pfrG1kdjXq2+wfURmFernZQBoJbHrKyNRxfWG7+A - gGkmzkLr66J11ngDlRbaXh/Ls9YbybXQ/vpIn7VfwPzE/+QFZRmJ4SOOK7rRtB94tH/aqOuOe/TG - eX886b0zdV2ybVHWJEkzTr49fP7x+PW7w+Njfllq77liyKphqbaoaoqqaZqkP3UHp2l8fuqAaNdV - TdctXVezlwNM+wyNypIkS/gycNJd/Bu78OVp6gxPz6L4dDDyA/fU9WNvkHou1PSclKbOepqeTZ3d - p0N3Eu9+9durg6MPf9CHU7aFmfKXpy/evzs+fHdyCl398TRI3d2/zyEio4qbw4OmSp8hHmYUfGOY - sA3T+gwxMeO1m6MJW5I/Q0zMmQwrIOSX9TBi4fUtinZXpEXV3vnSpca8sXZzJPKZio95Q/MGuebz - lCMlq/aasKFZd4VnKmb5zeHjM2WY8hLqBsnj81QxlUXWDeLj8xQeDau4a8CLBhhQlLsiRhpXoDeH - mc9UoDR5FW6QZD5P0dLoVLhBzHweQqaQ1znf4+6fzuIkTzHLnbCCRzpLB16tW/Hzz28R1GVwe0r4 - aTZVkiafnvI7LHeIgU/MSe4FW0D5SyL0w7MrMt9IXcJOfzykXcLfbTADHQpsU6CmoQ5xgnS3A+P9 - ZEjCi58ESxY1SfjXixeEbQ38TFOreICeS56AnsWEPp8meAtnwmMahbcvTgQDgBIO3+UXgUKrs3tA - DfhNj1z3Xo+doZf0+qcTJ05DL+5x79kpv7qKd9OrgCrSO0aZm46727irzQs8lpk9AkgBYWdsVyIK - 8QVmpuNHqfE4mogNJF4qljZADlkLb2gDZJc8whhOPCX9+Ek1ULAmK/na9CTbDfREA4y3JijayhoU - tW6vzSTFemY09QHEhRMOA4+o9RQh21tQBO3pLpOEbNlFkihRBB4KOk2m/c0oIavdqUmeXyKBDbqp - TH3eFZvyfJ5hcLN5zqb5rs5UhXk1uYF5+wH83pp5aStrMO+6vTYzL+u5yrxyPfNq8hbMS3u608yr - rEgR0+2leb8+v3wzPazV5yJymM6LcmXG4kprxDC924LcKBGDrsxJcj6UzYX53J16Jeiwq9FFvGFf - lZpNgZ9zemPtjupUR3ZNDKWyV0APk4K8AcTOSExX1iWxQgefB3kVSGzBq+pjGBUzCOevC4mUi/+V - YSN/2WBNRZCmfqulgpwB6FUztYmUs//+qIyxAu+zXnlcldvm3c/0Iva682Cul5yn0eQaD4Txg181 - 58J+l2HJX/3U94OgHzmxS0+NzRBUd/gKRJs/OL8SZre3CIzjmARruooBsY0HtrDMIBqPI9dPr9i3 - pkNcrNIDdlILr3Lgk+pkaVewdq+QOK6pIZhMmvaenpzfe4vHoOnNEMXLWvwBphEZYChWDFOfH2jr - 8ZtMHDYCekIMJzW76ierPhs2wBL7Hh9yCYXzhU7xvuz4inRqCLau9BhQmpV9UIa/rm2WGXwvW3kT - Tj3ZDSm8GUxXt6gZ1+E05mHJYtp5Ims7qtnZU43xsx5+zIZRTLAyu6+grnEfZTO9uaBTmlaestcL - YEbG0+QcJg8nus9mkl/P5IcXHrYkuCCmxjTZngDiJhQoiEqOqQdUGZXSt9F+E0Xk1+GKfY4jfsMD - zelmKKptm5rnyratgi0rW45qyo7197MoHjvp7i8Tb5ivY7IxjofC7FgjaRzybBY5aSF7qIswRRNZ - dVbD6nXh8xCqk7dQnSTOVQKaa+QRVpcmFKCNPSqcAHYSUMdkCAgmNIUdHsDGNEdjVAzDeBrCkFyW - pgczDnhxQjDaGYpnoADdXRVQ1Bup2c/JQmR5oHfjzwhbvkuyFEYgzLEsOYKyzNMRBIRloYLyDumc - TYPgiiTTBE/l0Pt9PLBahlcZqhG/h2DKjDoYGO6QHzw3ZMmV0ksPrI0iviYrShZ6g0inQZH+yYWY - sqPbnT1ldO0yLHHGQjL201GCd7KMx1cJXuQ4DVLPxUM0VKyEA68t8aV6qmFYlm0omnIG4sv2TM8w - +n8K8bUuKme8OIaWQOgcO2NyjA1gUrHpYCSARAIWxJQor1iDpNBOl7EWLBa65NFrTI3mpFAxAcMr - 7JJLj6U6CCOw+lmaicsoBpZw+tE0ffQZyK/N0cUunewjxji2CuVRwA/9C5rkr3MYunjA0UXNcAE1 - ErFDUzQUy2P+mr7nhbhi80DIOWnq0OkAQUb3gOILekkeGfjxADPm3cuydWWZaoBBdl2yLJgOfExo - AcUTZnXzK6sFIPQgis4FdhgudkCFca05o8K2zDLdgv/JqjqQFAfNsoF9BoLtDsu1dtCaGRgsAw4Y - ZGB40dRPKa7mB8Bl/Sty+D1aYEHghUOwQd5gxyDARv5ghDWAc1iiHG8wSh7RQ16EdooSLzxPULQd - O1PXJ6lz7mHWewKDBKhvTca1g7rjiTfATNu4pAMZ5syGByhjA96Pnb6P6D2a9gN/ALyL5h9CSV7C - UpOKtrOphwYtxeqjhGOOpysCmQQPebIi7Pleuq0j3UyiqDuy2tmTjesSb0k6xWzqwuUoEmgYRVKg - lV88vMobGAl1vBBdhsKl16een7bsNbvvnll9xRxIjon2muoZpqTcYbm2JT7RehhEIKuGHuFNgaiK - CGuK5EYKGiWxf4H5n6BRKseydgkmlMU0wWcB8wHR4Bh4z/u6Nbm1JWr+4WDiIpAmoXcFosfF3MQ+ - S+fnUH8g9UmSAUp6l+wfHAvP84u5UIizjL4MKUA2jMSo9AMeByEGS90UBtCFb4NgSnXCzCbEtFwH - eBcwLGphwJN7ObaeHJPtHU3HVAHXJcfSSz9Fd8ScA2MaBEIM7/oO/dcPArzT2S95MzwnaG8Fqli2 - 3S+uQAeW49xhidYaZp/PSsycXchYzHOG7ZEPwHDPsT0yAZoEPv5KliQyZjWhFvBxUQaeMNhy2+XW - ZFtrSEIF0K9DFKZ3nMTR2Mf1Oi7G0c94GU1BIo1h9KSTXkYs2yMmUUAeSzpUFYwjFJgRM8kyhPm5 - KdfFdlJMfvri3fMXrcu1ZXWzS7vzGoXrxsviodLYnU9NeKc3ykpXtvIraIDYcFZBAR/h+iMhu41f - fv+d/Mbvz2koAtI0PPOHCxrJS0Bj//p5pcbEyTQZPZ7t28beEAgtu8Vlhzz6S1aT1XvUzYtWm9wh - vxXomV7qtMMSxHe6nSRlV8Piu384oUjQkgcGh09e6PIPL71+8QPP0Fq4jxHK0MtbC/0Ubj7aKVxy - WyiBd8NyaNh1uF2+OfjCC4Kks/OvDu2+28FMZvCH3a/Uhbc+rNUwZ1L+8Ca6hN8sNVznZ4AQf4UL - 2uC1q/XolUhQ6zdWfkem2Z0kScpqwhtbtC36hraxI9uWaNv0BbYGBVTRYM+83Z2Ooipdw1C6uKrE - S6Q6HK943YLaA5z+0Z31aIqKXe7REhWz1KMhmnqxR0U05EqPstkFlHcVy5zrUan0aKrscqtCj7Ko - ycUeYUw45rxHA5CgVntUu5ZkdNWaMcqVHg1N1KRSjwCDohR7NDVRLfWoiKZV7hHw0LWgV12Tyz3K - PbXao2mJUnkeDUM0jFKPtmiaxR4NUZeq8yh1LdOCcZpzPUrVMSpZ9XyMpmhLJaxKomEVe5RF2Sz3 - qAJGdVvqGrJa7VExKz3qdkaI+RilCuVAD5pS6FHHbIfVMWqAVb0r2dJcj0alR00RpfIYNZhZtdij - ZmSkxHpUYSLM6jwqXRN6VbU5rCp6tUdAQ3mM8Mayqz0ahR6BOuTKPMq61TVsuyvpxlyPWqVHVRct - vdojZ7+sR11UTeCHfIxahvYZViW1K8t2V5HnaFWpSgDFguZKPapqNm28R3ihF7GqmKKqV8aoAp3q - eIno/BirtIrVlVKP8EYuUY5il6WcAvxZkTlQoavYctey5ihHtqtYBeYyyj1aGYNnYzTK3AEgSdUe - bb0LZbqqbM31aFXHqItmmVZpAupSj3JWhPeIibArPVpGVzONro05wyo9VvlRhjGq5TEqmdzLsAqS - Wy30KOuiUaVVGJ8pa0g9cz3OUc6czAFhrJekHMybXOqxBquG3QUwu/a8zJGrukOZkwBKVZIDFm2t - iFUpo93CGEF3yDBOc4475KokV6BImXJg8i29glWzKAFkbU5byYbZNTSlq9XMY5U7APFyWebIuarI - egQ0K2WsynO0CtwB47T1uTFKjDt+LptPnR0wmSyri+G67Fq7E7zWrrNDb5GcvcX7reKIJqinn/iN - HE/KtyTWBIuxALjmPN1ZHu5vc3uSMIMyC7SahauWTNG5wDrMXkZj9jpz6yz2icbkGaTwMMY0/YXn - JHvGLGho3vOHX6YJGJlXWXifQM1Z/o0l+ZdLT4mgZrFfLGQ0D2dlTzyOVMDk9wINO+N5yVlwiQAL - Veqp2OU2sYY2sZy7uuqD1gBzHl4xmkes1ZRmHfP0lZ29YuvNlWZdYEDjaTksjq9uWcNrjTkfIxj3 - AIV+TWMstt7aGOuX822TGQy14cu4RIJz5FlDoEqFQOWFBNpEjgfRJQ1yrp2mCuKzsssJhTf+wUu8 - 2T2c/NsqPdGKm8+VXMaNWsFU5gJ9ltI4LF6fPdB/BeHM/4Q+KPaQYHRzf+ZmfIYSFN03GO2N98+J - z3rZK14iRaFYavn0lL6beeHSuPo9Lrhm09Fc7SzXezpaUoxl8F5ajCceX1putm5euTBNwzwrC7/j - 3OVG0ZDNKq25YFJnPgm245tkiXn5pkfzpGuVSdeySadERTUO4Y3QLeGEukhg0tkl0Dsl6iuS6hKQ - BHplbFJL5XiJC7v6RWCnfaWvmwUY51h+lQyULfND/rvgVuQB3sUg+o0dixuEifNrZOh5hxXsg31W - nOAF1dktHcVeWXOYZpTdxTvrInsnfBok7hkma8pSm/6HIGTtJkQQ5hOelhs4Pc2uvskPaZTmoFqa - i6fcZXYSpWDdZB3uLK5MCaOzZ+vZ5OWHIvKn4gWfOJQP7F6ulUbC7/DabCDPp1dZZ6uNQrebR7Fm - 3++8aRoDGtfqXzFa659e1rZW57K0+hSya5FXmUBG5nvVBg6ilHwDlBYFqT8hJ954gpfHl1tcxBpC - yqp2mvp1o5Q70ctfqAg6L1wgsBA1eS+rwMLyqieFSVil7Swb+7xMvaRSSOhHAShYzo6ZBCe//bb/ - bv/NT8cnpwf7J/t//JEd/+jH19I9VS3CCb0XqwDD0YfXLw5vBgJGy4W+P+yfvH73qtz5CqT7xhui - wboK7Qa06Iqyh5blabgpttZh3lllsuCbILDzsn9lip7NxqKxPo/AvByzXOOryds+rTFT9xtIHjCT - PCDU1WQOvR+tNaH3ll6xtlrX7Ga31rpGS3L1YfNb49YQuNGYxu+tJHJZ2Ry4vxav2GQ3yfEL7ljE - DJAG3vjBjYacALsYJE5w0UZ3oMcR/MALPcNZEcJvsGNR0fS+QgRBLA+hkt49c8PwPT/Wlkc1yovq - 3mHdR75t2FyiuFvINhBx0+91ZdOPOY7YbTfvz/b5iDo7tj57CwZEZ0e3Zy+4Vu/sKMbsJarazo7M - /F1AA5Q7GXMyDx93jc19okTQ7TCqLX1ixKnpiiGbGvxnmd3OfhAUyuyPoynMMUCnmuiwtm3L0E1F - MQCwt/nGHffRKbaFHTEXXg4P/0mddPRX5p6jD4f8nkIARtPxBXfRoYNCRhcIOgV5+yqUsHVVVTfu - Q6b+5WonZqkT3RYleatOVEma78QqdWLaeN/jxj1o8z3Isl0ehiTq1lbDkOY7UaQKrgygic174JRZ - 7qGMKEMRLWW72UCXeqWTymzoioictmkPSg2i1DLl2rZoA9Ns3ompmaJe7EORZEmtosqUtuljnnCx - E73MgqqoS9uRVZUFoRO5NB+KCWRlbDMSzbBEpYquCp/LMOlau8MoT7piyzDpW8yGPjcbiqyVesCY - AH0bRNnzM64o5Rm3ZNGSt+nDUuW5ydCk0kBUFTfFtyIrxZgbiVYmK1UDBtlqJPPqAzspy13V2EaW - gG4VjTlkVdSgCkBsNw5lnrQ0RSmTFm4Ft0lXuiyVOoDZMLZi8to+yoPA/ewtpZU6N+N6RVpBJ5rZ - rqbFTkpkpUi2qFpbcWFNH0pZmmiGaNqtjsKQyhIR2FzfinLNuj7Uch/2tmpQruukPB2avm0nddiq - zoe2lf6Q55ncKOtARUUebNegxk4q/AH211aMrlpzfZhlswQJC0yTLQZiKXPIMuUK8ZqitLnBYEny - fAcVyrVEdbuVR82UmxWTQYN1gbbdbNT0YVW5Q29XHJoVcWjKW/MfxvZUOqnoWQmXUFuZoYpW7cOS - yhrKNkTJ3HwuVEm0VEWzDUNTAR+6UumtbMsplgarqc17M+f40FIq86KJ2hYdzNtwtmSU50QVVX3z - CZmnLFs2ynYPrBzkzUegzXdQQZFmi9rmYkRVK6tZBdhFq4oReXPuU83qCKCDMooURbS3kB9zMgp4 - sWwaSqD4NjdEFLWmgzKKJFNUNjduVQUwXDHSYT0oVZGkyZtTqloziDIzY4D4FtYBrLEr7ctKSR3J - uiFujiNlzm6WK4tXGfTpNu1jQH6lg/IUYAf65rJVmWcEpawg0PbZQhgpc+IUOlDKHajiFjQk13VQ - nmMZl8Sbj0BX5juwyiOQt9E48+tU6KAkTmUJeHHz9qUKhtSqVw0WA2BZtjcAtepRowHK2ziJ5tov - W5Us5Hobn+B8B2Z5ABi3vrmgq6pk7KA8xfSAwxZcMN9BRZLSMxubd2AAF1ScNtiHUe4Dz75sQajz - g1DL0s5GF9rmHVTXcSooNIMGSNPjZnFCd3rKO0sSs3ZeY0ba0AnY3jlUH07TPNhaNvAmDUm39YJZ - /4Zua+50MO4bz0aAXO12XkTjiRNewesXLAMrOfAmXjIY4eGxD54TFBzsgiwJinwiWzuYAkP/m6Ts - SAwdJQg5+80BGPINtm2A/PD+5FuSQXrEEukmXfLmzYuNwc1W08sQqquaptMA/zpYzXlYX0WBO3ZC - cuwMRkkDeOaJrFHwtEbwOK8uBY/uKDaBZ82Dd+TTO8Cd0MWI+nrwrBNFQvDUZvC01cAzVQ1veakD - j52aqYL3g+f2p8mIvI3iISLRG0xZMgjyOhyIdfDC/9knsknhVf8myfXwSg3w1lCnZdlAnXYdzEpP - qpnxj8+P6wDDbAsnsk7n2W4EjO/LLUOkZVvoGGsAqmae33mX5DiNPS8lGKXsxINRA5DWiWwgkEoz - kHxfbwmQmmRKViPm6ohxPx5Okxw+wj+QekCVDNAF2FRWIktNsk0AtJYsFXq4sgroAZAJSkfy3AnP - 68FTQebYFLxmKlwRPFnRrHqZg6dpVARPWQM8mGNZkNQTRclkTj14il0vEaHlwBuMakA0VwfxH+KR - yLl6GYBmMx2uiD9Nlgy5CTh9Hri3/q/TUdQAl87nVW2GS2rQJfPSRQM73jDkOt3HT/VVYXsRe66f - kuOpnyTz2pmCKFuZANSbQGzi4Lq5xXTDhmTVg8hUXgVEkNBDmg7/RRRP5qU0BRKVHpMzVhOQtryq - kGYwKg1TXAfjD3jonryEniLyDcC5BEitcbLVhrluxOQ6UC7lkhUAbKTGCpeoBurmOmnNT5hvQIko - BKnGUxcokwZDsQIe3glnSHUyWlJ6sjYP3mLLRsH0R7LGDS+lUUbbq4kYUNuAPKUeOqVGxBw4/qVT - sGfqAVT0DH9KI/2pq9OfLaGeq+VkrSfV4HAJ/WnwfyeySqW01WQbmvbKXGxLtqHbxurwvYguvZBm - KuLv6hcBOahsEWA0LwJWNgsprPTw8qqw1pmFZcAUqREwHmKxhAx14I8G+xogqtMmS5lEK2gTdcvF - E4Bnag08rHHzf00Ro1FrX814uAl7trwSeLqp1NurOJ81EvD9BFb8I88fAw5RiYj1qxIKJ6xB2SoK - zdV6OO2GSZ6nPgpqIybrQF1F3xWAbEYm34laRd5wKGv1XT2UK1gORSDNRiBXN13XB3KpUFwBQLnJ - PVIlSVB8DbYXAKdsYPoDeErB8mqgxJXloK7IttJgXOs0uUAVwude7IV9Lx7WQacLssR1stqIvHWg - U80F0NXgbxU+ASAzFDYDmYUvLZthxbK1Zgg3Edl6QWQvAG8lw0ZXQMs1cIdeK7LX9NgweHMZ3giv - tRq4qmo3WTg6TXaziR2mC8pyM2dVhGJ6IEOqXcwbNHVUFcLFDkQAzwBLO+PoRg+duTrPwDIABGKt - LWvUruVXt8IMXNbzqVbWtsKqmDQtrcHYMWiSkRUtMADKzmROsw2x4vSisDYkuR6oOgJcauIYlPrY - MkVuxtlq+sSA1ZYh1bobjNoVaLMrxKCLTypmlG3d6jqoOamR5pQaKbhUzxnUQWgtJjUe3raSnWBK - Kqji2sWJWetFWmInmNSVRFd3SuOKhMfGrcK2pq4b9a5/gE/e0CQ0BTmTLc3efx5ft2yOTVM1aOK+ - WgDr5MqyvSgGoMqnWW1ePq3IHaalGA1WtVnrY8iUXOqEgXfVAJ/GHYYLbOoGp38dGVowloY1sllr - Lzx34kGA96BwdDZAaWUarnl1vKIItBTg5cZ53mSVZ1ILQVs8yaZa79KsYRVLU6QGh7pZvy5ZWcOZ - dLPRWLybB3O4EiIpmLXCugHMlRhmBmCj2LbUlcUOAtk425uuRAtANk+4vfIOMwdyDUyiVsGbMfZh - ae8PHPLWi2O8Gu3NVVizcVaBuBmt1wnxomXVStDJcsPSvoYs1wKt2YZYCS4eyruSdIS1VJOStmgq - 2A08DpYgKZkl0Sgc1ZUcdKCfMXygFndWrZ94/eWURb3GSxhctdTKkeLVIK91jtVDvlw3FiFtdIop - K7uPKYgNxm09iAt5pgBds33b5A2rIs+0mpzFAFmNSlxq3gJ0dmY9NkrIFYlS0UypATq7h/b3HFFG - wZnXHEcA4NlggGcLvUbwVgsaMVRVVxrW9HZPrgFvKfJsQTayzYDGBb26UoQVLFxM3aznC7t2apf6 - b2w6tdZitlAbrJ0qdLZkmrW4k6XaXYpl3gZZKmykNOPOXAl3sBa1LaUJupqZbdQkCJbB+UFrBmul - ZQB6QGyzPhxEqnVpLhV1AB76NfN4kAZRt7LVhYsp2ar1IiGINfO6gtWFQOZbUY22q6qvJPEMpYkt - KALXZ1oMRMtcwwvYYjWRgtBZtRKvAbq11XAR3AUCeiXvCAe3zqppAPfD8xd5fCS/hWIZjM2WTQNV - zsFowzK2zlHcAOMiXlYyZlmwxbMiWKYCqGvg5Y3MFgRPX7qk54c8VuFlW5FA3NRijqVLX99mlWVB - yRytzfFTyko7FCamaWlAYX3kxVIdslLkRVNw3DwGTVnXAIO1/KHU0t7hhRcPorhugYyRhVIWp2k0 - Rq2Y5kqoWxewFbYjVgNQafBs1gBoNcWRrse1BbCaQ0eVlQwDUzZUu0G9KbVLzKXaQ5mtNJXmiMeV - rHlTNtE+qI1XV2o3ZVfwyyCACneny41he6ut0ME0kBeQXY1EWV+9KVTAWEuIkB1SWgauouiyWR9T - r9Q6/587fuySIyeFzs4bzRlltgOwIMh1JZPQVFRFbZDRSq2Vv1yNKNTM15aIaGmlCQfC1euVbx5+ - q64holUegMtFdDN0KyLPVnVLbgggZTsn6ipCRs3ib5nDqDFuVF7ZjjZVQ7brDUF+4UQVtKV2NAVS - VgumXwOQDcvLRiAb4r/rgFxmIFRgbGSOdRBp4zKzgQJlcx7G9TysDGIzW500R+OuZP+bGBJu1W8d - Z+dQytAuXrQz6DJ/TPOxE6VhN68KnWGollK7GSpzm1VdR9hQ8HJnltYYSio3gFfjaF0fxkW7oSsD - qK1qVJuaqer1a3h+s9NGPKMYJ7Ky+FDHamc6AD4dlku1cQL18K2ponNwzSxwYGsxxEBumvI6lK62 - a1YBtXn2V949YaDWh2M3YXcVsZ4D2RyOLa9mdzMAG4LtawFc7HKtgLfgfNmqJ1PWB3EDib4CxCvy - ky7ZuqU08JMq1UDb6PunkKlSJo0aWUdZyXlj6qZkNmhGhTs5t+V0hTo/K6rx527nW89x6dUF/ALM - 0t0FMCCaQJddtfna3SGPmvKfP3pYul/pFi5nOI0mfghg8gsq1rim4T2vmN3iNHf5yvK7b+bvcXLj - aOLiTe7ZD4HehBN7KVA6S0rPLmE58wOgjt2O6mgDVZf7bl/X8eiA46p9x1Acd6D2zwxPrakkpFcT - b9ZV/TU52Ve8hsUfDsFGabzZKXuTA5q/SfI3/PIOniNcSMYOvWhrhatv9oNglUuNnDiOLgWKOvos - JP6vHuun6ZajuTvYZ2Pmc0QCP0Hey/5Cq/RWFIpMTKs+u9ZkGmSt8LJ4czS9+4NTdODPFaAZ39n8 - 8Jnxgdg+7XYEufya5v4Gkg2que7zplLvU1rE1bNe4M9uplmnb6O2axQ9C7t+Pr26pq4zDbKw+29h - 2XlN/edG6kIAMJP3PADPetNg7nrsyv00D2sEx4UXp/RGHRCMURCg+MeLbFa9n2k6wezqiZcIKLiY - BExqJQE+rCBEKLhpP3Kvqlcc4bucCRZe4YTf566AcovfH3C9pRaq9FJ3nRY2r7lYOW7ebuk29LEz - 9E5RPrErklC2OuEQag1jzwtL96lv1NlbB9Qc/D/QAjDk5u18BSbIWrXJyHddLyw1AjKjto3i7Vdb - Uwy/Mfg2KCY39O4IcXycgGnkopiItqUOSbor1KHcy5PPQp7cU8zSmvVejRuglKtWCQVNsS0oRda2 - pxRuMv6pqaXkWHLGk6dANV8crej3tLJKzbKP70vTPbJ1Z2zZW6OQhS7WL0+sbE8wX4JYqexsfXFk - ohl3ikz0WyKT8ibyl6Z9FOXurHxui0IKW/jXTR6x57ZKHOj33UaGtEAdmYP6JkgEQ1huhURKcTJf - mgxR7473BCOy7i3Yzfpq0TRR79TCGEMNb4VoXk2HQ5Zo7U7onYPoMpz58LdTPXdL7dwWgdx72e6F - yao1v/jl8J0iE+nWlsP0DMKXZsIqLbjqb8iElW7NGimenfniCES6G156fixLuRXR8WV70u6ICOFn - C2+FQioHGG/OYv3MKGU95N8mpai3RCn38Ukl49W8O9pHuSWKqR42/9JoRLlDNCLfEo2U0iV8eQRy - dwwU6ZYIJEuQcgO08Vl5R9oIgL0p78ht0kc1T82XJkNk07yXIfcetDoJckfCGDF31205SEr3JvHF - r3hTC5oW9E15g287lXOX1A0me7sVgvmyXa7qHZEo8u25Se7kpl7xpN+2u3rm3Qly5Yk3713zKwPd - WoBaCyECNyJJpHurdTVg7ynj3ml2MwTSwmGbPzuB3O/VlCjmjkRD3ybF1ORp/+LI5I6kMOH3KtyS - YLkPb1XuUHgrvybkfoWzMtDtmSl3RulgCq37Fc7NUcZd2fVl9zLdu1lX7KzFJfB60cS3Fvps315k - 65d9vLOFkxM3RCDybemWL9pHot2RFS8SyG054N95njtyxvky5q5lUGtvz2ZrUrmxU1j27dmqdzIl - VnsZPFXjjkgU6/Zski9a5ah3JNiZX8B875bftLP2KOYOiZT7KIFboZDK3fD35LKk5k1mGP+8fPL2 - 3fHJA4FIt3Yk62aj0z6z5Gmaom1NJTeXxca8vVNZ95nF4b+vzBb8sDcmVG6TWr7wc56WfEeWPrdI - I/eBA0Andyhw4DZp5T4z46yhr+wburIA/uLVXPATfmFj9Be/cOzhw4fZ70Ija965WK4xSgPHFVjp - Ux+mIUrSU8c9zW6yo99hXPHU42+moZ/udnhwmvA6THzXi3vHaTQ4T3rfTaM0KziJvT7e2Ch9knaK - n3tjz429wSnesfi7aVjweTF0rpecp9HkGsHjYNRA+busGHL1EyAr6EdO7NIxzICvvc8yI4EVrqg8 - TFLgiNRL/kr8kHw8Pqi9oxLIQmB3dM6uhuMvdjtn0GgiTLz4LIrHTjjwsgsyQ6emgdNTeO0PndTP - 4az0NOuiue0HxWsUoWTzFYr0Y+EeP4QEH3c7V54TB8DxP9G/xbsAV6r+76kTpx5t4bvs59L7/GDs - 7DbJeuyy6yRReswuvFzhxj9SbcEL07mJym4TnQ0cJ5kyfEmUrHFPYPZ1EAWCIuDthoIT+MNQiPHy - 1OJXdeFXbeFXfeFXY+4rp74USb4qJPFdRnULFCVUnqvZ2fsm7CeTpyAkR7mIrStIKEAcFLwcd+0a - 2to19LVrGGvXMFmNh0XdgT8QpxzlW137WKvT+oEzOO+QS99NR7sdRf+6s/fBu/DCqcc0Wa6UsDYv - JUOp+ZaKo5mpMllSu5pszZop6dlNG1X1rqrLLTdqWF1dMltu1Na6htEypIqtd2XDrmt0VUN1VVo4 - 8C9A2YZuO8QgiYXoiVZQcRdaFG6gueuad/KT7wUueQy2y9dP2iGCm0DHF9Vc23N/eHTczkxrYtvi - VBcNvd0WDdFuWTrroqW2DaN1I8L+qHdIPuDCoaXpt0S1Fu7Nm1S1BlRs3qRiiXrLAl9VRattlQ9Q - 1hpRrTP/89cnLVl+WlfXW+ZWRe7aptJym2bX1FqeLlXqSkrLY9fUrilpN0UDB/stUYHd1eW2Wdbs - 2kbLVKBaXakQBtFOm9dBWbhIuREqeOel5CiOzvy0JUpQu4bWsmYE6lJblwfAZ0bLC1aQW+3DqXct - qZa6ro8SyL77yzRJvZbWg0gS5jWQROvTByShtz19d5okjmJPOHE+tSsgtK6m14q2LTCidCW17Tat - rq22PXN2V6sX65u3qcpd06pdgF2jgHicSYiWvAVAFKrRsogAolDt9ifQMtsWO3eaKA6Pjsnjd1Eo - vNrfP3pCvE8iOX5/eO9Z2AbGu+pZoLRA6aCd+VdFrfX5b3u9Zohtb3voYtsGgyHWK9y25/9VHCX0 - KEY0bmlfCUwyWW55xlSlq7W9BtSkrm21vVaF9a/Wcptao2elbVp44SQj8jKILslZHI2BKC68JPXD - YUu7C4AbtWW6EEAPtz2Hgix1lXpbfZtGla7RtuvxRvYcKkTxfuLFNIIlac8LpbRtRWpdWbkGz1bb - 1Kugj6RtzxYsNM1aOK+ZMF76oRMO2pMWWts2u2DJLe8myPf70UUyoMlJRk7ckh2hiW2vBM3Wd71g - JdHyKvAuksDL2PNITgdtRSd19bY99OixaJtlZR2MhbYdpVZXk9rWNUpXvxkHZJka2pYKimi1zHGa - 2PZeqCa2HVcGK9abUDVtk8LzKDon3zvB1GubDGSz9WAwRWk93ECxRLvtCAa5dXfIjZACOqIPvH5L - GxICmPiW3LbqVQ1Yi7SNXF3pSm3vUN1FIyHLIsRPwh14F14QTcZemJLDTxOxJe9j+4EtYDO0Hi/S - tbSWl316VzZbtm0xkvmG3E7sKC2QgRe6fjptS0mYXaNtt6wFEqL1Ju229Y7dtdreBr0RGYGpDvxw - 2CWvvNCLgSSYrNh3x34otigmtK7R+g5AV299CwAs9pYJwwCebrlJvWvWi9zWiQOtx1EUuF783//1 - PxNy+O+pn161QxC62bXalvKm0ZWUlqnMNrua2vLawgZp0fae7Y1Ii5MItcZ+knhpS85oQIXZtpNe - VoDp5LYPL+lq17TbPrul611FbtkwWUwJz/ID2PgTm9ornMDGAu2fAS2cXmXQPMSzxLTz0qDw87NB - FNDUQRRCeMhKUEwIsiRcXDKsCM40jQR6YHeA/TdW4AW+uFrPeiVcLjmjukBA1J5QJUl6FXgcnB2C - FDp3aHXpSU9GDkexd+FH04Tws83kWT/eI2iZAqnJSk/NU0Ss3fSLaRwjjupallTasrpZy+/gTX2z - Rk+VNm42AxgPitfhYRtom9rUym0+m+VqyI7bLj1tW0c9JP8lCGE0EyP9KHYLZ85R90RBMnHC3Y4x - L+U6e4dOHAK0ySx/wEywcVifcYjr9NgZiCE8vR3FguuPhWHsANDvIpHm/gid4CpJ8wZrG6hik+ZX - WauGYq5dQ1+3Bg2CWG8cxqzGpnjch1l1hl4+NWsBIIuyYmMeiHUhl0TLsDaraMsb9YhBdvImFXXR - 0MxyxU1xTdl3fxiti6pq/6tOjmSqm+HY1DfqEXEstTQ5m+L4aNoP/GREDtamZZqOU6LyeT3gQf5u - VNHctKKwRYWZHbmpwF8u73nOhRpxP1vP3KjAp56L9WqsLb7l9cW3vI1SuRWBr2ACiI3kr6J27c1E - Pu5b2BuJbp6uY6OqLCnHLcp92eyam6k62eqaurFRTaNrq9omNdcWSbVzcy/0r1PoPyw4D3LXQZah - p5DHrpRtCho9pY2SEFOTZWUe/JVUBUmCOcHe+kHgRyF57IlDkXBF8IREMd3NTtAfyb9lSTmeiOQ4 - msKad4e8dAbpsZcluCxmwuK/+I+tc8w9+w9B+Mvh26OTnz4cHh+9f3d8+BdB2Nu0tdqsarC69gfn - Vyz31STygWpHHp0cgkndfJb37RRGDHhI+SpshRRsPGEc2ecVeQa2hyu5fahTYd5ts0YSr5mTCMqc - +QGsndnDbocPoeiy4M8t/Sy7Q5b6Q5Z5TOuSdj1854y9jPxmGa+a1+joqqGWEYqejSpSnkhISi9g - 2aaFkRdsVv8IM1tuVDO9mgA7J14Q9PrTqycbtRFNMDS6WrXo7lzZh7F4xh/Uy83i9wfPHDKKvbPd - Ti9h+RQ5n2ZsSmcp7vWdIfBEJDihG3uXgivIkm1pmtUhUTgIgO9BboCsCNMTqHl+9PrxExAWSRpH - 4XDvOasLhizWJQfPevzDs55TBLXkSV4g8XNEzkYhgVozMiXTQnsYWCiVUklt1ZyhdRXdFte9RrkR - OqMUhbRVW7ibSghpp7F39ZmLV93Taejunl5vml5NDezp9uhVkYpnKbdq6vn06k9Ired+fD6K4lD4 - 1RmAarsCarUk21BXodZ/8rrkP1nd66FWSW2RWmk6hhapS4J1ldyedJVsUW2JXP+k0vUzp1dZ6Skm - 0ut6Sdqb21O7qt6edFUkrSsbamsqXLZEbc3rW+7F65+bXNs0BpBcVfmeXO9tVz5/Crdd2yLXz3ut - JZv3a617er1fa/1ZqHU8Tc4FL4hCwVJlw9BXodK3UIccQp3rIU9ZbZE8NYz+VVojT01Ru4aitNqm - bMii3tJw/6Qy9c9OpbphdRUgg1bJ1LC7lqG1SKYgS1tyedyT6Z0kU7mrSnZXktQWCRXsU9Xqyore - HqGC/WBZ94T6ZROqLXflVglV7+pgTMiK1SKhWvcS9UsnVFk3u5LRnvTTFIzhAnNCbc+cABhF+16i - ftmEalpKV7LsFgnV6kq23lVso0VCNUS7JdfBPaHeSUI1ZL0raWaLZIq5sM2uarVpoRqi3JLT/55M - b4xMlTb9UmhOtqj1Vamr2S37pUxDNO99/V8yleIpCFNqz4eEZKpjwmezPQEtm1ox2/09mX55ZGoq - Utdu0YxEMjVMFbNPt0imqqjb92T6BZOpasKKR27RNFXRz6UBqbYpTRXRuN+M+pLJVMdbHtQ2bVO5 - a5pK17Bb3IwyZdG490h9yWQqa3jMtb3IE1g6YWBf11Da83HJpiTq9x7+L5pMFbtr2m1KU7zXB0jV - bJFMDVvU7pdQXzKZKqD0FaNF7z6QqW4rXVydt0em1r00/bLJVINVuS63twWPZGqhRNXaCxWQDfN+ - pf9lk6lqwRKqxdg7TVW7qqRDmy06pAzjPuz0yyZTy+5qZotRpyq6TI2u3WYwn6GL5h3U+c/yLL3P - 5pP0YvLcbVPYbJJ0xuUpd04HDlCv6/ARrZB1JkvWQ17wmn/KtDPXn2AmTwctfd1ZJ2dMOYv3pllq - skZs2jmf0i3aYaP465W/WeaZYm7svYcsb+7gqtrQs5vLBJMjmY5rxutrS9xSbcpB7Qgd4bNqhqVu - m29oW029YBrWO8t1Pw3XNA3rKff7abieaZDXW2TeT8M1TcN6+1D303BN02DeT8PnMA3G/TR8DtOw - 3nbO/TRc0zSsF+V9Pw3XNA3reZDup+GapuF+Ff1ZTMP9KvqzmIb7VfTnMA3S/Sp6/WlYtr2BRQsb - A3h1ABk78dAPhTSaCEIAv73O3l9/QtcxXuuSjjzywUsmHu7Aefwih3aS/dfulNDRLtz4eJjNb77n - QceN2x4rb3os2vVo2Oig/W6181Cf2f7wAma54J4vlpoRASnWmF0Qs3ql1+FZtFaFg1IPdc7+bb39 - 9Vyc30n3wZtEcVpklmKFGqChcn59GVmv3ncywbzBpNB54DmJt2bv+eUjGwjIa8EGv5NtXWworWDD - /LywIayLBbUNLMhST7buMha0VmhB5rSg1WHh2YKbc4sX6G6iZJp0zAp760dOkhIqnpMvSb3sD9Kp - E5DDo+MvQsGsyQ+yKNv0KsVN2EhpgY30doVJ4Sb65BF563kpXl+7pky51uKS1ZOo7FBuWnY85DUw - RAeajqLAEcZ+/hm+Atf7k5S43pkXZxEvl37oRpcir/AiCs/84TR28DIdsksWff39d/LbH08pvBdO - TCbO0DvBi3wW1speHvHST/PqUQI1Hz1iDfpn5DG8EcFiHgJr/sfuLpGe4IffKHoKfUEp2sYfD5eM - Ju85AMSNUUyKk2kyepy19eTpug28dnkTj2bofvTkKbsgjeF6b+PZxLvFGrQBIc8cjDPMW4PfwsQJ - vYDd20aqL+gtnDORz4KgZoV2O47rvg6TNJ7isPh3eCf4+UvhLIrH1YJ7goDQEEL/oRAz7ZRBNk1g - UNn98OxTh9WA4oXBlcoNgigBRTNNs9Hy8uxNVoU/sT+C4A/g3ygMrvI37LJRYeS7rjcrN4pABwr9 - YOrlrxL/V4+vJKuYESgsc8POYQKo8P7SDCYEglBIaD2cS/xcGEOPdZrjgM31MoQ49G6rIi6ywNO/ - 5PemBtEwIviPAAvkcy9NBB6Myl6yUeLd8B2S+ilGvPEr4FAX0h80wHQOLrAl6LzRua5M9tjxw3qI - IxCLBUw1Dm0YMxFeHNukwiW/enFUDviqo08ssLfvumT2jqQRuXTSwSjwEzAqJuthHYN4ozAhaDnR - f05P4+iyCGjFvMJhB4IgK2UCKbI9aNEBsCf9A1OSxh5AVz8eVqjYEhIba6DU3OkpDo/S2lyV0kjn - H5sGUPKxJGMnCMpDWsiJfFT588QPgs15rR430/7Yp7NdZagbGmMcofntNo55fZHywgkHXrBkOCW2 - ROajuqBHlUFmPwCbsh8lwvNTT+BMSTFA5t4Iszs/kbfJIAIFcxZHY3IU+2M/IQJ5ywRLfndk/iZ/ - IVRWHaM0gLkTJqgxmdVG5l8BidAevoc2It4b+vCA+fwL7zi9CjIssnICBpxG2Hg6SXZ6vQBKieyT - mHqDEX1B//ne9y7FyWjy92QXLycxtc7M2trcyno2U+29HnkDzOeFMDBQkMRxExQ6CYY4oytyEkd4 - XykLniZ9sKBdAmR0FtDnAVBRKpKPCd55Ckoq9c98KH3psQZAtvpnsTP2yDQOCFgYMMyHuYUC9EMX - S1n/jx+NYZkGtsyjLnnsPSG7e+S3zJLyRETfE/biAcw6rBQTelkqGFDso8ienxZKgNjLv/pu8Qsf - T/6VPbMS2B9v+5tvsA34F5nlDFaPLjXkWOkMGt7kYARcEb6kw90lbjSgfCH+e+rFV8dUrkXx4//B - 8PEvSgwUY7udr37z3T86P/8PZr6x/guN5d08KLwUEyQqMR8G//E38mjy6RFv54+H+b/4zx9dcuYE - CbUSC8ZdhVVeRc+d8Bz02Qd6ke01ssyw1BNbxXNqSeLBjDmAAt3pAGzdYdRnFWKsIA6icW8y7fcU - T5M8r68Jiunpgix7rmA7miZIji3bfdUcuKb699hDgbYrf+OMJ08Z3mVF1TsktynRN98htH9md+12 - pEJ8t/Q1lEWc73bGIHPp+x1Vkiafno6dT/xZt3X+zOZjRzPxBed9Pt20X+BFNtbt2flBo1d/iVuf - O13i6AwaLilpdqShAkrh1mNaswskMRAJLG+AZemVxygvXED0MOzC3wsviCbIAl1QVOH0DKyRaex1 - iQOCIXGgx+iMnE0DsHcpc8T+gFx4I38QQGNYBkXC8IoM8S9brtGawEV413JyBUJjnIjkBDoFUgDN - dEVQVFGyTUeg34YjCtEZzG90iYvsxBvSVdMO2QcrchzRDRZs9JB19arc1THrinVRqMGbgREPgqlL - e1tl3DhmNnrqiMDh1ww8AeEJ1gb8xfIJlnJmXYNlNw0cgOuKDGLP9VOOgIUDWAxwCUa0UMCOoE3U - Qgyf8ZsDAPD5ySYkY1M2S7CUjPMSbKrKM19XAgFEHXLhVeb1EtBxlhksV+Qf3tlZ7F2R5yIMMXam - fS/o0pNm5IMH6Cd48qyLYgrscnLY9+KRE7use3g5ICdOPPFCdAuhKvvHFEhQ7hJFklRaxscbhB33 - 31NowAMsI2nvTxNorEtOfhSJSJgfhHPt9gy8KQcjir71E0oOgFLKlf/9X/8zwYkfnBN6mzHiqyLC - GXuXGL7Qf8FIBRKhWgLpqf41Gj5UfGTu0gl35QbONByMEHfY79F7hujQI4qNmJYlmODYcfkUID2+ - 2z8+2P+uy0YBwzmjqJdVUSVjfpU6v97ZSYkD5IaDg1F/JZuzG9VF8jolseOjleKAHQPEjGV+gdkj - uGwmXymKkbUnVpa+tSOEJRPgXRhHMYzxMf4m+PsJXxfjmmw59k5P2RoesJRhiM4TPXPJh4I0jl5q - oH8HheIZcCG1yBIPr5cPyJXnxDD4M+qtA4QhFhG1lFVi2kAIrSJygiglw4ghF1ASIm1bXVpr4MRj - 5xxVsoMi1p3GFFUhNA4QO+mIeJ8Anb4HrMhYhgv2ANEwnaBxiI84p/iVwnMJ/AX87ad0FHgLNusb - hhGFtE/WeQQGuh/CWD5EYGdCRcbm/WkC9JwkBOyDEKc3HQEP0lN7nPVj8uLwPWNxeqwUe8YyqeeM - EaJzz5swKFHtJkhTUHYIA6RqAIZAVToWRXVPomnKCO0RtO7HQB39qR+kAppA6GbCJfcM7LdgHQXk - GAbmOoDPH0BWA5rQdJ3SqRo5E5AnTFBgbJdIV+ow0xTzsgoNAI8CwaNNHILSd0vNIou8AGHIN/9/ - gqmgdIxDLyGjS+pJJ42ic+QYqHVGrqIp6eOo+Qh5SeZWm1FPNn8jGnYAkg3qPQLpCiiE1nD6kM1C - EIwS2D5guU0T0D6gJYAaIte5ysd4jEKbNplMqZ9qGEeX6YjjQs3RXAR4xIXWCEbY96AOUgnqw0+w - CgX77yJCFRT46ZVI9oN0RCcRR0L3dSpcEz5KKc0OUlyaxGiyeTApE9SP+A4ZADQKzsAAnaEukja5 - jKYB6ik+xyAFgC6cSQJEf+kD+JQRmQtmGmeqAjnxCnjlMmHT3820HmAKKC1kTlUEAkBwrrqE6xC0 - cbJ9iJhugjDWYn4uEC4X2EF66XkpyusZnWdI3k8pijgV0E8jfwyMBorU8ZGcKKHkGrNWuqAAvHCC - KaLkYxj452zaQKSmGe3BwgxEZXzFkYCUmYDx1M2wOYf/vjcCdRWxtSNY4wGYMVeEnhp3c1nDVX/e - JGXNnOkHV2j5ZIL/kpIENgplUNISCqhDjoEe0Dn5PbAc6H5cJmejndEihcwBCQMGBXL2Jydl61W6 - lHWYxoC5ARMDRBw1t/BLH1EAY/cHaFthmVF02cBsCCAISHQmA4ggDSJUyAitT3HIJDEww9mZP/CR - HFxvjMNHDLEx5mYfIAjFJsg8TgpsIiNs5hKUGHF9F8k78bwxcwWGdJrHXXxCreafXdERjKOQah2Q - rTjFzNvP9lKpjMrQ2/eGwNSMBhHmGYFScxPNQR/FNjQ/RIMT2/4BGkBDC6kzxy23xQEtPucuGHM0 - 9kBaISlD9QiVT1kS6kye4vwLzqXj4+QwKfgjOf74PZN4LpIO1zJo50wnXZDjIyccUBveydYHAB++ - YONyMhMaa04T+gepw0u9TIH1owhnDAznKEmoMcBwLpLn07QgkH8kThzD2JGrYMxc74JA4voQS1Ix - FboRCEXsJfA+gazKmAhmlYw9NlVOLoUnHps7rm9GDvAuVMBzIoA4NhuolsZXsQ+qmQsWanolyRQs - 4mPALhtTmHAbn8PJB4gkmcEQg3xits9sYGBIgY3CnesZvkQO37+n/uAcJh7zOECToOhVU0XZj6wv - UO3JFDG2x+t2UUd8RTUEWmTRJJ/uAy+Z+Ij6MYzjgu1VIZh8VGKJpnDWgyihdIO4HaGV1UUWRHkr - 4h2QTbxIhTgs9QB7KPOR6ahRkYkSZB70zqDZC6Oi4nsYRS6bo9nKJ85wiJLLpyucAFUAqgNaHz1U - KfQwhsmgvA7jKciR4dR3nYLFlHEEYzegRboH5zFdDuCKtWpxpvaoddaHps/QEkRh2mXmFBowVAsz - tExBSQUMcAd+oH3BaaooT6mZR1U6lealPpMAcCOSl0CB0xBoHPuDWWZLqIJGYfbiJ28wTanBOHBg - ggFJ1NiGcZ5FMbcCzj2weQd+MgYEglRNc6L4lk0oSTKLIWyYVjQL0LHrh1MqCtC8pBgHohFQK4Py - G46YFOjH0Tk09JVKaZAuAnIyZZxGK/Dp5YMBvHMTBNCYsnLUfEXbB7rkKgfZhS74Rxi2kPx76nm/ - euS//+v/5LhFOJ3g0rniVgzlgSmua9lo0I+KVZmEKVpjQE5AZ4BrJK4IpoDqKRziEORvkqNs1g+i - 36HjhlqcUMqqmEmbETcaYdAAlXN2FngFgiysFWbmOZr7ESWcZByB7uHtMTuOUzSsJ5wLrEdJ0AEi - DHxqDEycFPRHiLUL5EtBAcEx8FxKLRkm3RiERG7VfJwUCBgLh7h4g8WDxxYTA8reYJI68RApoSg0 - EB+MqVwqNguilmoRyuF8urskjHBZVLQZ2LiwFbB/KJ0m3Ip02KaZ08dmuTGbUVQB33SOZiv/2Vp+ - W+d81ZeKq09YPQbeKdA4dyXS17sdWGpmGwvT0E93O3ylL/CVfu+YJfX5boq+u2wHwutjfIX0Sdr5 - 3TbwT7FUDxUTBhScAuV8moG1WVzXW5C1zBg9cJDcX1CnFfnggwF7PACRMfNFFNsfY7VEQFcWSK1X - +7SZ5AN9fB59evuaQnzgodArbmcWl/OlNk5PA6fvQVnseW5Lexo0VfKGIDs7FWdJ4DcUp+Z5Z+9N - dPmsF/hr1SJ1LwUBlor+dAx4pH/bahWlSGfvW/i33OKz3jSo33estIPCGeRUIRRieRWQzCkpbPEX - P1eamXNJ5UEJpVoC2D+gzSodNb8UsnpaTtZlr/f8qwUElUGuzUdING7fVpooeIgqGFjKN+ikdIgs - yBJJ6DP6iUurP+YfhnU/KvJ83w79K9SE95PMOATtN8YaJQD6ToD2DOgvZvWjDplMcU2Fm0QzUZiZ - 0OjGYYCgweejk9lBqYmqW+AeBTRhPAHNaTSPkymDmsFZ6pzB7GRu+5k5BaoHF5SgElDpgAEGZgYT - 1nQ1AnbjGOUOwufHYB3G5yS6hMXuhOlXF3FJNUsKraBBmkaljp0B3dCEgldCGgnwBwipYL5mpjMu - ohmyY7ZWKaMeVSPTg+gMpAamWEcerZ3rqNMZgDwvvk2V0ezVXiEUuBAlme9G1cT4cid5OeUVPmal - 6L6cYEkCGDkDFs+TFeFqu71EV/VpnlgQMiVm4TJ2Jp2995chjqo2cndWD6OFeDwDUN7XNx+4Wwf6 - S7C9zoLIaYyBL5+n4vDjhWFmSwG0dVBVtn7Wgk1WRa02zrUl2L53wuEU1y+vkMj4nunj42kfd03i - J2vBaoiadI2gfusx5/kLvrR7C5pi6LGdTAR7LVhVUb/OKT8GM93L1gIvoniyLnSycY3QZRj8AEsL - J4ZFBw07KCAUQAYq+CGKYX2TeaeT9YhBEa3rHEJOtyd0E415VpmuAIhd7xN5CUJ7TYh1+yYg1iVp - UxBl0b5OpD4PnMH5B8QkBs756ZR7mk5i3KBEmni3vya85s3Ai5gk++6FnwChrguifo0gvvIwdGle - ZJE3b16sCad+ncL1RCQfwCLjm/L7SRINfAxVyTQCkwIU8tkg1pMIsqhdJ3+9cS5junX2jyjxJiNy - GARADeG6ILZ1kqYORIrEQUS+++47xlNrAiddJwUcHx18IMdUExxRGXV48nIDKCXRruWn2nM5i8+e - ZzHDULtgbxNuKmNQD42ggLUkLhtdutIZMz8v7gxL0tewEhs4uN2CWz+En2oi6IPlsUazzQncSqGm - o1hwktUtFbYKeVthaTFjr0Uri1JK3hWXGfpntczA1LjrrDL+EfU/j0XGh6h/FZK3IjnwQiCp8Vrs - 8WLk+PHYCa+Rkb93/P7IuSAnTuj94qwJne+dkX3mZkDfxnvcJaarwc9xccTAPaS7Pui+59Byc/bA - j2lQ8zUC/09/3Ee1vi7gNwAa14KvvH7fX48IbgJv3oWPISTnjr8RaMLRbPv5MASF4dEQhy555Q8d - 3DON4qvr1OOxg/x/OAKDYxJ7/noWJ9r+Ey/EaOuboNFv/Tga+4ORT976v07D6HOG9R8Obgt+mIKa - AjtzGrvRYPQ5w/tPJx0FuLX6gx+ArSmcjKLxZF2j84Zh/k8H9zquyD9E8k8/Ph9F8XrgvnUwMhJd - 4y8iYGIma5kkfumHGAYDEvH69QYPn/7e92A4aw3g2AsxIiyXJXn43QePRZYn1wj2fujG3iV57gzB - 5luPFznc3x8JR+g7T2MnRz+Pry+IwmscwjsndZIRGqqjcTR1fWc9+vn4T/Itxuoe0Wg8chTBv1d8 - HM+zoLuD2TGFdQ/5czgePPhzbFMU9uuLW/c1Y5OluUrZGUZ+jKp6VgpTBJCXUZTmx6SwFA0Cwv2i - KESRxA+40P25HZr7RZAUQTJojWc0osUVZsUzmAYJQHPG2ubvAoE/j33+i2PGUzwB4453O6XXKQZ7 - CYgSATpxdjsvi1/zLoX0aoLHnsqHsnulptgDzchAj1c/SzxmPXDQqjvGRfRmYAt5ITIO6EFvMo7Z - ge/8+GilWbrV6wuVU81zfQd4Whb/BEMsTjegqQMmEOq3skubtuUKyKOT+Z3v7Dh7dmqNY0vMwlw5 - 1uiptfzEez92QjehzSNc9JE+dWAN7TsCDVrY7bzAm5Zw8X3hJ35apTIoG6TLC9USQjaZ2SFl3BpO - /UEijMH8w7Nuo/HvTUV4LF9etEQP88UYhXG0LNnwD5xfrwTujSj+xugmpNowwZ3XcHCVn8mbOC4e - JkG/xg4ZOMHgMXVP/JWoKumhq+LJXJeLH5/542EJHH+MzFwDAJ5VxDHv0BK95GL4t0/joPu1+gJ+ - EvgZJruPkCyAKi4vL8VLVYziYU+RJAkLP4IJ8y6fR592H0lEIjKRH32tHkJt/Ai/suPX2E2PRVj3 - ACpAYu/t6x4SC/5gJOoCdoXQuxShKieLPEXCkuGGUXYitDoxHBcbANCrNvas19TNXEGUr+U3zoLY - DM5IjEWRlTizYtDIPLFVBQSvljE45iWhCWZA8M7xDS8ExsV5LRXXQMVDj17Sc4hkipF3O9Wgkby+ - UwaKdkRgxeX18ZDHDKyytEG6ygpREfM2Ps9P6HYIlyNZiTx/xsv8xQJxQwEms6Jc3lODBaEDoV9U - J/RghDDXY1X8zEDBHANAqKf9wAnhOcZ6YRRN6JH0MGI91aCaI+x6xAb8Abmhy6I9LzhqKXbBy7sg - Sp6/7n087lF26eWENJMi6+BgoSgpYGQ9MOZkCW+pUaIsgpBLlzmZQj/WcmB66fOQI8aABAgsSUmV - D3kxyoJj4EC/woH8e86AJ9nzUv7LS67Dfmmp0oz7ZmB8lsxnyaLSpLUXTOufgvkyCrpd3itBcUOs - N2cCcC1dMQOqenMuQVBW7eHqC4LEgxVQkESrrQby0nwpQE+L09SS7Hh+1dg69jyC5XfqVwyMmgrr - g9rlQeE3Gu0LJcbChcGGC4KNrH2/wfi8t/Y3kBKv3x2/Pjj8gEzKjG4+kwXLO5qkN2X6rwPN9a4D - ynxUs95eyk99fyE75U6zhXw1X6qewRrKbclp2bBvnNWUPyOrMVXI6Pr560YGm5vKa+G05cBcI381 - a9e5Fw0uuty714/SFLMrjPwxQb/dIJpcLVKdeUNreCP5unym6CdXAuYWpKEXlelpPLnC3MO1fNRg - Cc3yPTKQBWrezwbXBy7r7M1WFhwuWFfkEPqpN85WEz16FB1zYr1mP3BSCtkfK73Pji/RxHwu4dUF - llWx2RIM/PVg8j713Ojy9BeYB4DtAObwH/iTQVc9mrRtTwndsZic6pLU2SuETV1Pb6GTuM6/T2Xs - 7B39jWbESn0VwquAGgUUOsQVwggjjjYEgxI75jfMgXmRvblxkM7SxGN4eXlyfHg7WPHPzz3/VMG9 - pHf0N4HfNw6G64BYONj/kWg3j4KREw5PEw8PnH0LP2FNgVuQNwzEeZRM/M7eP/HPjXeeyYM0+TTj - j1P6bSYfTo5/nLEKCwK/cUCdBADaP/7xxjueJoKLSWxwEUex8vGYHNAXt4EJpsDYuYGSGrsRAM6m - eOouQX0l8N9MY71kDzcIyizJjxcntQo0O1fxlhZZDFr5jO7sba1xcoMmC7AkLFj81MeRvZg9rGe6 - tGatFMDpDWEoAj2z39l7Bb9ZDH7bdkSxx8gPWId/p8bqZQpS870fXH/HiR9gcng+2GP6tEav7RB8 - ESAoMZkB9II+3TJANL91DhBNin2bAAHH0UPpOc3gQTyW+GA9kmkfNCeYjv1wOs7g2ufPtwpU6IAA - dwJh6CQZXO/YK/LKSW4VtNhzAky8whdeH/gjycThFYPurkr4aYyeGSbg89+3Jd9zAHreNBamCd4z - 9PHDe3Lw/s2b/Q+ty9hZd/10wLp77qcDDNC8HqFeGF864uM7+Zbdg3RzxD2DAiAQBng+5cX+we1C - Mf4UdvaODo/f3x4Yw/6ETcnR+4/vbhkdfhijpX9AXr9bjexbheIKt8zwTjb0fnKpcAUCj7+7FbQA - p3wa4ILZIzS1/N0UtzS/SraCui0xy4AACzoaCvQ3WtDRMPDYgfC2pR7vbuyhX5519xZ+X2tnzvjX - MOtsf+z8Cubg6t21Q7ockjQJ8mHzLCi3AwgumjkcsEYVbhqKSeyxNTPI1+znDXaP6X0zxwVfmL+D - Vy1JkbpX5Yt6+E5GGg2HRflRH1tA6xdjDt3YuYTamGgPpjHMLyFrDDjs72F8QiXTFU35KQR0Q3FM - 07TxbOf9vfm4DoEcfpoEeC54tVbIAKTyMMLEmRXUzF8dVI0BqQ/lKEdrFKRhaf8HIz3wDbtQrmlb - qEBHtDpNDjmjkrmtthq6olXqQjT5ZtX/39y17aYNBNF3vsLyQ9VW9Y0WA2lwVaXq5aFVBOlDH9f2 - kjjChrKGlP5Rf6Nf1pnZBdaLnZioQlEiLnvzzvFeZvHMGRicCZmIwX3OhWdHL//+sWSQSRbtnAeI - FwCJreousfNqpq+15qDoK/SbF27BSw95WOE2IJWcnk7X+5THn63nX6BEVpQvkHB7JtnCBVEjLtc8 - dQ2hx/wa8FCRJvB/SZy7RCiWCYrMRMyWRHGBVGBJwhclEYpBmflq+ayIxeLtw1AeAWLD43bKdeZT - ByXB02LlSf8V5mKfJio3MlNqDJUqo5y6ypAkkG5V7Sx7pGBwQ9YMNKwFefA0CWiW0sW7lHnKBwiX - U/27e/CsF/dPWsjaTKcH5s1RM4Y+SQlaw5NmAtqB87VhPvlBS6+HTKsZ7UsfWpbcR7d4Ghlx4giR - STa5qpjv9SyL+JC0cHMHMldbihpqP0UM8BcWZFVwQKfW8jzDZkeV2o71Bhh2janpEhn1niIALKWg - 18g7LaviGac6FvYlpBxUomEg1LUW1bVwEiz4L6QPVmbWJhrFpszJpidnqbFyf4UU5Bb59uOiSVKs - hHpgsUmaLZu1dv6nvFJ9/Il+h8KKNy3QQHtypy04dXt8FZ+PKqsJHL1qMzqWqS20x2fng0ggyajr - diQ3/vbAShzILDc5tGU5bmNVq4lsylg8KNH6Ts+utp/byHugCuy3R4pinbDdBi11qjpN3ODdvcfG - Sn5VJwgt/VzZP2HjhsMoOqfCH73AW4de9AiQGLpa/nhSjuegrmEMa+U96BEtENp2plyGFcTCdzy+ - XImbCS9LimgykgEKQevlyDN7NV9kyZll4znKweDSDlSwX2GRTMC5thyvijMZihDTOhSdcB+QcBvi - m4lNkYxselPGgN6FjPbpTaiI8KBhuoKXFVnp5lnh3op36xGq0n7XHwS9YEB4q6Yf5X9b6Y1tSesz - 9Lf2btmayext/4Tq14po0GEJdYM4HSQhBiHs9tJ0EPvdNz50Us3K2ZylDinS/HoDU5OOGgu2xB26 - EZGWfVhjfIqlcPt8MOxNg3Aa+mE/CMLXCeen6QG6Rrs9lgbDYNjlvbA7TRMWcL9/7OU7ML7JHf3c - uynzWfQP6IO3uitXBAA= + H4sIAAAAAAAAA8y9a0PbSLIw/HnnVwideRlpkYUNCQE5CoeAkzDDbYBkLowPkeU2KNiSR5K5BPz8 + 9req+qKWLAOZnd1zMrtYUt+rq6urqquqXy/sHG6f/nbUMS7z0fDNa/xrDIP4wjdZbMI7C/pvvnu9 + 0GgYJ2PG+uEkvWaOsffxVwO+5EGaG40GZMjCNBrnRj/Ig8YwCfqNLE+DnF3c+WY/GaVQy5355jso + 5g8mcZhHSWzZ99dBajDfnMR9Nohi1jcX/PxuzJIB1r+4eJ1EfaO54Pvw5l4E+0F6lW1qz95Z14mf + X5wF2SRlqgbxipXkvgkfzzMcjumk/k0U95Mbd8zSQZKOgjhkTjC/Hawv5tXGmbcDo3bj5GZTPli2 + txSzGwPf23LwxgSGHw2slP/FfHbK8kkaGykv1KbvN6x3FeUHc1JH2dyU5Ots0pS/Wo920W4EU9XL + xIrte1GJmjfmwEcAjBX5sTtk8UV+2Wi1ozd+sx01GnxWcz8+i7rYF+b7fu7GwYjJ3uSiH1MrdjRk + 0KBxwfJOnKcRy97enQKki3FUUyxzBJhglqD1fm7p+nRZh+iWwaaWrV7SxcXUzaNRFF8UTzCc6+gi + wI7TElhctAJ/XqLt3GP9nhpqXBorphU9xDfIoA9nv5Sh+AbZpswdT7JL6x7B68UOg2HRkDw+JIcw + +jSCRMA3pz9JqVtec2pPnRFfAJ42r5ETUK/4yoGJixYXEyu3YXyRDwNRfeZFZa+izWBTfRTVeKUP + pdfy6MpVVT6LyqaIUgO/6WT+hGN4xNFs5CfwiB9G9sAfuWq8bTbMGHZ2waqdQUBOW7TZHvja18a8 + aZxOobaAN3sDzQbU7I2d+Tff0GxQNJv52tdHmo31OWalOeZA0qd5UExy1hjgNEtayRyN5sVTwHGH + Uyz/UXJAmYLQByqJT+GoXxBwZt/zVN5DBq3hexRHuU7lRTnrzMQUsyuyZSzu12fDFJUt6PehH4Ff + Jj9FZpFuOvAdyrS/w03GHQY9NoStB1LGwQUz28bysnGZjBi+OUaWJ+kd/AyjPssukxvHuIanxDEu + Jz2eg+Wh+x38M+Cf1g3rhyzK2QFMxQ+O8QNCluWZsRtnUDz9wW5/93qZ74Rvvnvenmhkaeibl3k+ + zrzl5bAfu5naZt0wGS1/yZaHk1v3S7YZ9f3Vdfjv5coL0wiyuxgKmgbsSSyFhzdFy3N2607cp736 + uZs10fHkXZRm+TH7c8Ky3PCNH3bjIeyBxglVgUOPcskJ/NAWZbb6e1EvDdK7mgJbfUMm7kHTrK+V + /k5tPCF0ImdE5pCSIcBt4x5mAxvAfZmQGqqf3arbIhdArUgHCLRxMqOBYRXFFxeLutxwyIK05jsR + aGrbqM1ddLBdyTMqdR9TpwYSCOoFdg/awbnVW5DvlXLfTb/7ToNJeVogSylNgf+v4CNyNr6Zs9t8 + +UtwHfAygA2GsaBmx4LeEowZQLjmq2Mgqpjn53k4CMbRXhIGsOBMxwjgM1ASI1Yz0zZg3gyrbcRt + LA60Df4ifGJ3kMLws7O8iwnYUtw2etDLKwDj1IAqw0vDQrSANyphwI4lJzxPxjbP3abWgIoGKdBO + nALj4cGwitEw1fGAMvaTcDKCrI6RwvvCgtaTNjW0kNr0G7i9pH8nC0eQOXD5RHSGDGuwzIiKAn9i + RLBL3A1hTWfZKQAX4dOPsvEwuPPiJAYiDjmQxEMCtMyrdoPxGGjh9mU07MM+J9EnY0Tsk0kOxNB4 + CZWL3XMhNZCuwxgk6Gfmh6CtTVKQXtBgM8HJ8blB6r+VpsEdbP4cDE1oxHht5PCztGTHZ2lXLwyv + AjSSI7RlnwKeAEQ9f98fp1vj8RA4L5PmKj5rwuTKIsYbYxWXxAqlwXRlbBdgGJ+1AGNaTRvTzF6S + wMqLsbwhmPD4bLWLaRbhCLw4hikHXc63Qvnwl/oDMF9o2gA+tSjNMWzBet+Kqb03Loruewi+cDTm + FMwzFlr0CnQsn0CimeWTnmlM28/oSDGtAd9GY/iAUwg7TucagLsXZTmLWQpsKssy3MwcwyomlclO + EkrBihYjkI0xF9c7X3r30CO+wvAtNzaNH08OD1wCtcUz2oYnisysMWwkJiSX2LUdDIe4vGAsYtnJ + FFi+sHkBPe7jUK5ZmkFnS/2GLqVl+Mqyx4Q4HnzhKPQpGE6YZ0CBbBKGAAPPSAHc0PYugB4aoick + CjA6QgSsj0bGwREN7gDMgMjMzZJJGgLXnGTAYhI0rQjQ5Z8mAJ0WDsACFmHOUv5hoWXbNI+F8Gcs + KNiOkv5kyACM/MFlt+MkzXFbYghGWHCwHHUa/Myd11DgzMbnCBszTOJBdGF692YQhskkznf7ptdq + NdcdsxdkDLb2cRLFuemVWIlxGl0H4V1jdDHKcT5MxxynCWxQ+d2HlA1ML54Mh44Juy/IVQCoIxx8 + hq3gAKGy9ztHx+Z0Ov3mMRT8yQx/U+3U8k2KZC7l8wG9+CXKL4G27cAsEK4A76NzN9999w9icPYZ + 4OhpcJERU/OP717nUT5kb05P9raAoUjCK+PBOGA3GfycdoqPR2kUMuM06Qd3kLIVB8M7YGwOYeFD + U5i5wtS9Xub1fvd6hA2Gl7hYct+c5IPGuik/I+X2zeuI3SAGmAbMFyxayHYT9fNLv8+uodUGvQCh + B54pCoaNDDCX+a1KJfkl7B2NMBnCjlnU818D+lfNi13Tcv3dg1fNSbTxzeTC+19oNb+JcliU/4mm + //EPHcIgHxDeEQkvNesZ71luwGwZQ8B+qPWUZcMARQtod0ztAvkD/jwPoiGwuVFMHCpRvygOh5M+ + 4LpBA4DtNgPZI0KxJAKkICyDTmF5WE9DEA0ZrzJzv9O6WJqV/9M9FegqZvH/ZFe/qwNqNMINt+gj + EjKgYyOONW5vksGekMEfwh4iZ7tYJFt+F4QMmJWr3RAp2PiisnKPJr1hlF0yfY3PWwa8xHYyvkuj + i0udusCOFMRfWezGALX3o96HSpnjpJfkmVZg92Cn86vz7nBv7/AXlRcH1QBRIrr2zV8bH7ca28lo + DDDtlZbabsdnfQDGG7kBzJEUxNZ1cNz5uP/wYNGvfz+12/Tk4ozRltZjAdQNm0wvGLlx2sD9BMcB + mxRL0yR9Oz99CJMWZ+wndgfJ/dW1FdbvrbEmpATIn4WED7s7kNh62Xq5vvFifWUDEmGbirOA9hSU + rCD54NNv7w+Oj3/eud3a+qV5HPy6dRP+dBoOP3zZ3dvqjD/9/GHr4NfjrdPtT82Dt5f7v7JPrV/f + NjMfKgPJa8KQDTe9Zqld/u1VCz5eMNqSsV85TAM8TdU+9gQUrXlgtLl+5V5so959CEgGLOk5iwOY + sL6XpxM2dYIvwa1332fx3TngWe6dzYCxO3VAAAEGqTfJWf8cgBPC2vHuS/VM2490BDd/lp5z3sS/ + p+Hu7iDQ19fXVjfW1l6ajmRX4PPLlY3mxqrpQMVZDnNXfLkdR33P/Pjzz4cfP238/P5m992nT8fB + zsXHo0/vmgDsYr7L012a7dJkT9tty7L9N/yIwcmd1L/faL5qeRa9YIo5QVEKxh/mZjt1+1bu3O96 + WCgGZCVm1286kS8UcknqQoF0CwcJEjowxtYyCOFskNye/bH8R9a1/ugv/eHCH3vZbkeLi1bsL0Ug + u9hTZ22j2ZzX8hBWbsw1otSFD9SFCJjP1FpZffUC5ntowyTHQAyZL2XTqpzZj65ByGSAGyApfDjd + 3/PNP25XQ+CTzkCuGebM2O0Ya903ryEj8FL0d+EMZMto0G00ICubzf7q27Kvf1v2jceym07sM1L2 + 8wGipj64wEUrhipERlIMR+3IfwEiW7y55q3S7ytvhX7XvRb9bnhNmISXaytPTP/ROQF/7Ozn9HDt + bL+kh76zs0cPd87hET3sOcN39PDB+W1CDx1n54IeLpztX+lh4Lzv0MONk32kh32FWSCKpW4Kwp6D + bccOXz2U6dAh9KLnA0fITvS2JYpHfmqtvdp4ZTsJPMEasp0AHtbXQEpuh8Mgy4yMo0w6CQFzcdT2 + PUh+pJpmDw9m0vsCzHVxkCUV8FbTCdzfbcsEXgr1DCgVgLC4TUt8kiJlgM0TtoqU4U5q8HqA24fd + dzzJ+UHMQl7TQl5t4eSp2lGqYkMjT7AjRpRnknGWWzTqEez2ITXkwqCji9jKYcOHwTriK+MHPSDC + usAodAJYtRaD2YdVdwaI0PVZmy+t2IeOJe6ftpXbbamRWFxMFxflSHw5EvgWl2tj0DP8bMnBfTa/ + v2dT04hwGNBblGSAW4FdgJNbYkJCgF+c5EaPIWsSXxA7YwxgXU9SZnx/n09dw9jNQRYcDo3L4JpB + boMNBjiuz7YDsAEJyUjPGJAYkG5x5Gd510/hZUqSOyroFbi3YoO2VSMJQ6i/b9xcAgc1Z45Nh9kg + 9nHQhP4936u9yAXZmT872h7Nv2sfdILNz5IqpFp8zAL5pHbTmvz65zx/P4G9QrwhQVYF+EYjXwHo + fcB9+Qrba6oa+5JtyYnIvPspVaN9kRXClq2aLXMO6jO+HA2DKBZfoC5c2UrL0ccpoDVn55dpckOK + tQ4CCiYEZpWYIY5fmcR8XFZEDAxgAuM8GkQsXRALa4KTXa3p8y7Wgoo9RDrjBhZjzIBs4Nx+tttC + D4dFi4PdAScJf1PXsG4fy2dARUPbAaSL3J/vbGgEkxwTKzNtgU9DYBWGw+TmvDcIYQkxb6HlzOVm + FpqzvIycU5XFAc7+GqQFxJQTBq30M6/VfIK/UZiBUgM7h+6nDFDuHC0eCrQKkzQ7BwR5Mh3rZ8Qs + 3+YiT4FKOFzoAwgiFyAqKGTJsqHMkvQGkwzwnRVISgsqKzo8f5zAxcM+9qyseLp1zjH5qeyrMjuq + Nc4Z6iL1InoiP7t8ToUZSktJzMH1nA5n4+AZ2abOZWnhjZ9YeKEkdtzM4fkr8LJ2BW6XqntqKV6W + luLFk0vxL3X2Ul+Tw9KavKQ1KVSKdtGT639PT5BjSf2xPPjntiOcC8rdDMg88K+uaQOr3WxHr3WT + kqUlG0pUWQkr9dMzNC8pztL5l6Jot6vsNwTRGQHRUaKIoupCEJHLVAgw4vWZmxgJL3JF90pYePcE + FnIBqsGh+i1Y2KvFQjqNSDkuPomEvRIS3jw59X+prz0dCUclJOwREpZESLVDdHzKOHpnW7abEAKd + IssLYpTt3MLTSvPlKvB4Pumvt+gH4VIjqnGDjfM6Kc458c+lJPeJc9l/LFt/nCzZf2RLJ8EgSCOQ + 5E4WFxsoRZyDbNVnt4eAj9sAHzzUm5MSTUaYZh36Jq/FhC6ekCSo4H1QwPtQIPFCi8ylFny//GVB + cs4LTXrdKlLlQkr9LX0hPbqu5KrCBQRtpWgJoyqUDZFctQOAfvlyZc12jnwhiwpmedc3D47fbp2f + dE5Odg8Pznd3zALpj7kt18IRCq4C2WiCUP5AaZe1ta8AP2ChhY5BbA4nME2ATSj/7eZsZO3aCE6G + SLHjXgH2tKBP9UUyWQQYWNthBR+sNTml0W37R+751qb76/7ehzwfi6N054u/vbi47SLPniC5ESO+ + Kq3rvSfWdQpkBrWL37Cir2pX9LGo6KnFfFVazPtPLuZv7CDhWPtKX8xAhJlzHwIBTUanBXcsiSJw + XmLHbjlCoiFWO8oSVOz23wbh1TC5wGROAsiiSRQeBbdv75ATXwPsOHU/bK6wVW+VvXCSwQCG7QEe + 3Lp3AyAOThITl6S4KGKwPNNc4pM7TDi1dsZ5ITUAG8eCjO32qUcCgXb7qCu4tlI8IiNe1K2woqa9 + CajNj8xuL9Nf8NQKEzxAmC/wv5kD28XFheXtNDo8WXZRlV1LnRz16MUoxGkE8ooIpJgpfZv+oOGz + bouF64tja+6j4CFX80LuFlsZdAreNVmNfyjtbNVVs9CaTtG6a+qsr6ysPaFD+VOp0KRgbbZWVlbd + o+PDHXPqbLx8+eqJGm6ohkSpSpRig+qLpKJRYSiqVYXWIylrPZTO4zF9R0zicQZ8JQjFgoF/QseB + orbLS2zzAkAenFlFRDmPE32LWkLkvGJ3mRWh0pfElIxbY85WTpK/XRX94+eI/id6PULynzovVlc2 + npioPaEbO6bfUJuw1VcvV3DjSa1XzZUVrqZ60XzxkuupVlZXXsrpzEqENeSUiyel/n2Ww1z3hYAI + izu/8wJ3DGN9eGhO2xMYpZPBMsGdAgDz8EAvGb5Ar4sVgxnvGcAN0x8e6Ico2X4wtrV8fdi6aIfy + q2Yxb5qaPXlh9tLsbuovqOrPZ8u2asu29LItKCvopIn7AQ0NNXUL2vjkbhqgjkr77nIw+cAgSMiR + 8Y47AG6EBmsXIMbCCsBsM3YZbbTQnofPMEGJ+6t7GcT9IfBEXDHd49R6cVHYv6LpqvgGc+GEfoC2 + UZA1LJj7CXAd2eLi5LUEQ3tpaWIPoDsTKGG3idLt2FboWJppZ05oSwm5loDTeZ8ChF3KlRIzZdP/ + hBoMe+DoveK8YeqyEfJB/TSI4oa5lDtnUHIKn2EPvZtZdMoKmUN0SowHvCVpblnUPf8Ng8ZdiY6N + XH+zUZs7u6CxZgQ7VGfPqGA4/4aVKpDkZ2m3Oni+shiAgKYEflGzzb/DOkMbqQSSV7swgexsBXIR + Tb+zErSfRkBNHVyVTyzpUCiuGaOHibamucIZ1/TqBjJgiVrduKZRuw6L0Tfj9L8FEeVnG6FPdGhw + BNs11TVpl9ZbCSByxyFI/Iq7YAaya5FlaBWcHCzffpFyCSnWRM4/jD16eChexdLC2Zy4QQ+mk5aL + o3LggdY0BPzfnPj441kTXzMQ1KYACH2Ifwb4Z+wD04x7zNhvIbOSKsUx6V0PYdAp0O4y01NQaGJE + oW3gm3pOlX2ATykbJdes/PXOQYz2Rg4sWq/jDMX3zLtxBNy9a6c3GQwg76lDY/UuHTFmpKRR9pZS + UWFz6/RZb3IBzE/qCEh44837qZfX6L1zCaxN9QQM1FQumou2rkhQ8wRkF9XjsKDiEGvpbzKPbZYn + 2IN5LaZyhCvciZ3ISTgT2wK6mQBIE5gyoIpqBh8eItjhFxchLefrnAraSi4KgKGLEStvkJz2fWXk + OADiNHjdbw9AFsrOBl2xVgKYZsLRoX9o2WchUM6uHN5wcXHIDfTOLhxsJ+gC5k+Lbvf4eo5xb8H2 + gH+MYaZx3esKgWLNY06uEZH95ZJaqktqKJ+hBw36EKBMF6LBWssuSe7FmqDt7UwTBTpI7SWviJRo + QJsnsy4cvV+ner9g6O2LAsZ0LrKDU6UTaiA9uY/nPHLXcsKzuOvnTk5nIVAsxebOOJ1WDd1qXOWC + gnGRflgs8AuJZMB8Cs7dvBgmvWDY6YBUi6vUn9jAUa48SdX4KRxIAfJstcyoyC3Q5Lueqe2WEa+Y + 42KyaSUuX1oWMufAzyUS77B127PiSnpcSke2CrigJ3r76wz/i0O02wlsfn4gDgATtUWXGKhE9Bd6 + G1jJw0PsdqDvvLmpxgPoo4K5Snw1jyA1wwfmQ0negcDPz5Iu/Xl4gLasALYnH/8gqtliTTAn6pKE + 8PLFiyfG1+MnqJkTfeWMozP6nh4CbcikboDdRtBX/Jz4gifmlphHwk7HAjpsjpHrvgYsvEeyqHuV + Ac2A7a8dkzw4a1OLktmRLEwsQ4KzhplrqG99fo2g6yBWmBxt3ov+oQI7DMYIaG9hgU3pjyqT8VWI + u2Y6y0Wu1HKRK13gyrS3ttCLzIwUpySwUn0xhn9He8oi4ZEWpw7qkZ5Aiw/89Dtw9jn+O8dDvlyd + qzuOMFX00JepdCalKcl9kPJ9PothejfOaSXg2yjbpvdyUblVbbZeLrKzfGmp67XW/rkf5JduCqsM + mGgb5A3y6AL0OqZPZJWckV5q5quFzMnHKM7XuRH9ags4P7UrJehMeiv+NejPC/xzJ1/lPxP5KSCs + sCuErwO5K4S4Zy1BDSbqzhJYiWHX3oRhuHlyQvbOqB/zzDtMR5Llry5C6sN6JUMid7ZM9/dUwMgw + k47PWsrqil3CWuXzSSw3AD/2MzYcKNjTiwJ9jOfnM3AswIXyrT+T43GYRuiglgCgktesnaCGk9Ol + oAIWpTGL3C9JFFumWRKN1BhzRAXgw+egAmA0qqCfwOi4J+hbknFu2rkbCNL39XCG4r1qoXwc+Zby + xEUQ4DkX6rn8qLafsft+cVFzfiI/Pt7bZBL3rUqShXtU5FOGUXBr1bYV2XYj0ee3IGQl2gFbh890 + IVvlSwA+OJwn4PN+RlWkCI60fcI1q41hU3hJbs64STpraytPtZeJ6biekW3WV9ZXuWyDRmJctuFn + DIGamUyRHS7WgLRdVjDwNVD2aI7c3cVF+PN6Qx5VoRep+16K8lBNy8meP0xgzXJuZxK7b5EnM8n3 + k1SEaPZCzFrcQysWrq9bfwpJ3wpquze7BRMAojr+AgYrXYTR7CXlMt7XQ5DxlugxyeARCiMzHBFX + ispfeOAWKjOcSCx1EoBSZcWWW9H2CZ4kAnZk00VBOZPPcbddbFja5gXEBerMkxQ9vYUHLADLgQlD + x5KsAYMGWK2trq0/ASs2w5lJNqUCI5p7sgqKlJO04PabdOIBjLNpF+dGaJTpmVLBc0+nD2Ey9CjB + nBLG5eIgNS5rthNsG89z8Lgln2sgGABjl7uXKRv43EsZHYGAhSeqeryHkhOl2jNHJsk0cdGpwc/p + R/KElFseM3nLy0BIF3hGYBDOWl2U13g5fJMZIRtqccTbfwMMxskYaLSsB5JbXWSreUVm00TZj96Q + TeUVcncS3N4C1MCZL16sQsn1pokFL5MsJ+PnXD0+PKTqGasG6idyyEf8KmBOdTrQUd6wyOCi8fhW + bjVtPi5ZBeRbKl45XmT+Qq6qg4n2TAqBUHwpnkllIF9goy/6TCnyBeVbGjllx0mQc+NmkHxIpx6I + 59aCPurQxnH4+jjQkx/XJDC5SKQ3Xr54CudPZ2Qm/SQQMD/K3kVD5MY5ymr0j6tS33KPPcscQDaC + BVGI4S9AIQo83lRgsNvUSzebjHGoQS8C1LjbJxOXE7REX2iqXZxNnXnZPDy1cPDkYN4A1YKNC60z + dHVxUTy4N0EKvErhwqc0IeUMlv5qfUbLyWO0FPLoyO4zrLtylUSbBXR/l1vg1EGt2rMkw2IuhDyk + DitBJswOb2IpGc2IsUSWuMMcDVnSJobqW2lYSueRtZLW4qJ2KGEj/RDl6+UyVtDYyLlJo5xOyxbQ + cAtIVMrfWlMgn5oApfXIjyAFGC1Atyfgcr4lNrFoKCT94S9CmLheUzCTTIZCyJqALlx6WlwUUpSk + pkjiHyv2S5JesfQ9aSdOQhj/4iLxvOrIT9eA8cwHMonzE4nP7dVxguS0CA9pHu4CT7FqWuYakdPL + CFZ/8TzbnNY3WX2RHZvA/j6rGGaczpwqK6w3epCSsVQcJGeXE37KBCOJjf6EoT0vP7M1PtLBKn4g + KsBEj4wMm3MNPpDMGCcgO/eGd7J0mPTRTJhAgTXLBkVpwOMI682MYXTFjM/Y389kcPuZA/Sza9pT + 1EbXMdTEujZXntIo7fxLK1Fq2kBmiVDCQ8GFAuogx8BstUIjOn4g7YsVOQxNNZxkyW8p+pciDX/6 + TPb7UMgfBxfyyG72zI5r8IkHfvlSmpbD/qDZlgOvBXC6YLfe8v8QNf9j+Y9ly/2nveykbDwMQmI2 + PE7pl5cP3777eLK9ddrZUQe0oQEsHYv7MLHuTfWwFkg5qg3saXaZTIb9Q2kSWczOpObks8og2urI + bqqMKoUAKK02YFMWjtIzB8IGUzJl7j+rOQctIWKYwfh1LmX0GCRQcTSTA1/qEtAAouJFgQo68k8T + rdfkRwtVcoXt2rROwgCsEQz3fk4igBqmaaNKTiIHcmxCB50iZ3Tqlrdqyya+AsXkDDYp/RiUFed4 + OUooKf7hY2SlMZo0MhPRFoa2OQtWNXpgwtWzq008sFnH7KJzO9bM53/YwlN3PijskAYxI0WPazzp + CIBYGYEhemDQRgMkM7/EM/sohnfg/7EbhjWaZGRmHxi8fwYQZN6o/QPOH3AU3vMbRxucPhMm/aMI + vf8uqh1xZb1iW2WViUcHoRlgRX8dBCrx2wCBwyeGSqDcArAqC0hUVtZezuWbBFH5SeywvVlzjZcv + qso5Xr22gSleSm6xsBTCZDTGw1zkEZUcQ/7kGFiBTctSKJevrYKJsehQOXZ7R7ZFhoUYEyi3KzrZ + OYWir1Bo53B/m7s78sAO3BjCwZPPp04YfhcU9uc7oWQIhbDoDI4EoC7eC+I7ejerzxSydpm7TpTn + xawTpO5/MZM6rd3bIpAclR9f8YJnn04R8gu/S2N3YsS1d78oBb1Vz2VlkbCgsAou3cUzBvjr35+c + elSsCGHinOyqT7ujEetHGHVqW+SjMDsy568fjvnXis3ecedn/l1+6Hzi76SPdo5EqSO0zcyYs3/I + 3/cnOa2twx5wD9csdd51Trc/8LQBA4Z0ihaEZW058tA4vojGJ2cpcoVnUvSV9cmaKyOFR+YnQvfR + LnPKZNWTyZ0KLVJKNeT+/SjzCp2K00fHAKmrQ455tkH/3nXdxIH2PHzKnLO06+WAvVFpE8F1A11H + GbBE8WctyvSJlGZljNx3aUD8USFoUucXlJScgqBXOcUde7KJKFdNoKurKpg9XbhkUCxqqfipquqg + JK7ujY2nZOADcRAxq/+ZIXWMd+sbrID085SW2rwxa0GYriPggEnA5Z5iJh5Uq1ZTrmvkFNG8jPrA + nMCuokhoUZroqNREAu2bMmturmndsYCsXafQlYKbjZbXWkaXjLUXzacg+8sc7ULd5jErh3NV2FGh + OJVLmST/V0/pg/MZ/bOwTXN3XPQ5gkpWn5QEtgRuPFaZ9OVBbj9O/ztjLCaxyQQYNVefkm4fq1l4 + /iCs15+SAh6rB115tgi6sPe/WP9XuoRVfYrYDRFfVKuuPwXC59R2SgpxZExWHsGpFONVyYnZ2Tnv + fOocnJ7v7Z6cdg46x2Iffntyen7cOTn8eLzdEbs2fjrd3RdZLpzOARfwhrAnbJ1+PO6cH2ztd8RM + vzs4l+ljfDk53TrmWrJL5/B49z1vlT6MnKOPJx8wx2lHeBQfd/YPP3XqepY5pV711St2bffg/fnb + j+/edY7P333c2xNMRtH0wMGd8pjxAEccWtkMuSIZT6nuCigLI+hTdMoCMcWsqcvEU8vqGS+dUNac + k+OhJXznFfCzDG4XMZgMh6Yz8c0eRq/j6abTLzKbzsA3GyIS7xAeMRakc+mbg9hcGjhj/jB0LqgK + 5AygsWvfRDGGyI/pjPzY/W3idj6BJP/yERrwDGQ5cd4e7vxGj7fO9ls174f4UgC/rxDmdBZhEsFX + 4PM5fz5XtfbEh53Dg47AGP6hqPxaR7hJGeFCZ/fgtHO8tX26e3ggUFD7cr51tCuwQ/9KAz0RqwHj + ZB2p+u/E+8HhTkeshR9PaGlwB3Rn7xAEetnYjrO/9StfOICcO+87pwqR97d2DwBnxSo6Odoqqhxr + aNtxftk9EJ7u3Hg/GJ4o3pA7z9fqKMpssobJ4wAQ9cwMgVu9Mh0zm/RGEcYMAZZrDEiW8UfURvGn + yRh+xa6Kh0kbGxuIvINYIuGEXggNAU/DnvwOeBqMo0Z0GzcIU2EZBKSQInSNQIxIuU+EiVgL/TpI + +oCdgLhfsiQe87drNLwBVlNWOpLvfYoJ2JOvGA0QmrkThXl3bjiin5w6nWLNnMo1cwsPWMx0DrHb + 5tKps4XFKT6Jcy5qNp2TumV9QJy+s+MfKA391Gm9bD5lEv7LjGRTTBk3CueTtf5CmI2iUem8kAXI + bJMCpqJy8ZkjPl+AzI06VLQ7wy+MQb+lQbND/BPaYNZVMmOWyT3opakoenb7Kf/UAxBcocFoi7+H + lyy8Krl8AidV95FHAqbTjndzeiG5WORKSfa/eI9Ob8hbb7q6o/qU7KB4Rubw3LG7/XJOvZXCmoad + joi/1Bdzyt1wsBteTdfKLvRsOuU+AzivTyDIF05mSs4d4uzdxdDYjlrMJbNiYQMHK1MaHU8URvWV + /DxAN4PVlQ0b1iN8W3vZtGEtzvCUY+pibN/HZN04cI9JjQar2dTkC9SByRJoxXlfeNvNBM2M/SJi + ZvqmtZkCD9zEQbXQnpMMOeOzqNHqapx/1JU+HtfcwIXNCnUV9xAeYTBjfTzRhu6e5d3iyYJpisna + EiMo58RgfuA2gkDhUFeeXaJKA1f7aUJbvYme+Rid9wORSEg45r5R/OVIMYVISFm+PUkxhOoxUGZa + H/zrkeDQii/b5BWmEIRa0aihGSd5FDLOAXeLEaY4QrQt1EQ8CfNZj4wSzJn0cmQS1hVIS5WolToc + TAgndKOgeML+pPCCAAI95tag5KKeAkpcc0pP1FmR/AKdehZfVSFayf4NsO+WDG2RBiJQbqxrJ3UW + mgJPKeKHW5okylKdtwWy3tX4eyxWM5e8cFpMrSgI+5Yooc9zMUXyVFEqM/1qvBd+kp6XDtBzOjfn + 5ircSh7Npau+hL6VPjzIiGs81dUCcZj2Uu5Qr2eQUEEJ6LIcbgklK/3XQsAERFKV8xz/QobqSAbj + SuwQ/FQitA4sRA/ET1v1bGYxVDqnLQ0d8UXziOI9Ms6C3ISsd36vOGby77mZB2FUqofyV2dM91P0 + B8aoOCB8zorQeTHQzB3blkJwgf3OGaT0hRoKsLHrxBIvnLFdub/hgls7W9GmaXqwzBswRxonpVcV + O1EXQO9E+jlyLuzwqbtq/WomKfzYUTRDbFmKXVRZaX92ZnCRbRYnlcz2GLXMphg9bzi8u9frQ85J + 6yfmxK2tWO03hcXSrMasAsuUhUnab5RNFKB+E/hyXABL5jKe9AFl6DrSg7UQ6hG+siqGnktat+TB + TmTZVgEoEGc20QbTIzDEWEG6ySumT1PdMQB66zJrbWPFdvNLFqObVxT3gUC+xG/iIyxVcmK6B0xG + YSKd+oAxSCZp49wrNk4iqIIImhyfMc61AwVxUeKvWgROFlwzJ7qIkxTEzbgT99GbRbpJMv5qSqMg + B0hwxUfsDs0DbqwRzIQGN6RVnC5qO4xfLjmLG2TCq+NHAfXHJ1Br5LnTaHJ8JdZUrgTkKkvllLZo + DATi0r1e24TJQm3w0O3ZFpkJdNC4Do9tuAOR4qOJjl2j3yQqXytyPdTqlerHUDTlzYGirQhQajg/ + 0gmK2itzstSZcRNi/BQrnPUZET7pWiQwDhTirxvmEnPOFCCuz2iVorUjRbvglfL8yKLXZ6eoEnSe + ZRcYmqsdUKmlbBlpoiZexzeqb8tOnEBt60zpN3WTeemHHs9mXNUyrqqM98gXehHnxhOnpM72Mke4 + iHuhf6/50qdTRzCL3gR9IGDd8iH3cZu74Mw8mZ4M/OJ8Ag9qIr9PnCiw4n3O7WfwUGoVOO2+TbhJ + 5nD6DsjPIcqbYpRxaxKMGiQ2fb6nJuJt5wLfItTXiy/vO+Qcpn3JPuKX0HaQfR8A/XVoHNxZvi/Z + d+3bxDEFBEzeVxgbJXd+FyTqGsW/dyI8AUjbojil4wlVcsWAhUIaOAIsAsoAWDVAk4sn5eDfha7w + zxqLUXR+Lp8YZDeR2OBCYACNWOifPT4pZ7G2aLttlUVf3HpWLOrEVZTXCpZ1qnrRSok+GwSTYS5z + dEtHApqLH7qian3cPOt6Z2YwyROzS1Lhk5bZOzMOWpIluydAmPjXLGJimfLJVLGvTPFgOgU580wt + 3BVPUKMTaUVsqyJdwMWsxrcynRLIzVIsK5NoJ+p5zCkaBJ/FFWh6LUf7xtvoeiv4UXS9663im5pq + 7wW+4tC73kt81JvvemuyPj7YrveK8oyDrrdOByDrq14hNaZ5VtoKSbCAFZH7Te30l1tzs01p5QFS + bLsI19RooZzegGKwU3Efcyt63dxsepHdXlqKX0dtO0E3QHSiiZXUhTZVLzY29GMA4Utruss4vGWp + yWGmd7ax1nRevdpw4LfrQAYJjlKmtVZrhXLBL+USICxlegGIR5nglzJp2FCubb31ijLCb5GxQI1y + resrqzzzymolM0eTUu71V+u8D/BLuUtIU8raWm3yXsAvzzoOypBZfbXOM7xa704rNAR31tRNrBjp + lAC8OPF2Ubk+vGaWZOpUZNxcY3zM7SDGcJTAFfflzQY/wC67ZP5AIS+Q80aDoj7zzf3DnY97nfOD + w9Pzd4cfD3ZMJ4cdty1vfkPfRz9HX/VKR9CsDeQxcmNt2e4oGAPbyWy9Xyle1WBPIzoj90nZrp2Z + x3T23fcRnZwCsdFONC5t5ZHmlSQs79X2LRkQGI6ooM39C8l39158032bMcimlaC3J08Trp/8Bfo6 + 8lPoV+wzBVd0e3bPz1m2T5DcxIEwVxJUeitckYgGBh7AEOAIFcEWzWMNFDontAzM7QgmGFcQMEX4 + yOhxnvVt6tzrlrZN8hhHXQJaG0cuObBHLsNO6/NTCjziDmzAnv4E5suy+NrFj1AJ9RBtiDGiAs7K + BCsCbHHNpci9RJt+muamsw5CYINiwHwB6gxpNKvmSuvl2otXfdZqbrxqvXgRDDZerGNyIgf/hE1l + YcbsMBxKzoNSeVjH0LfQgxxjH3CbWtQr2fhH3rHCb2ojc30nLGFHYhchLNQZd30oYhGsG1WQzXb/ + 9USaxvWl+d/An5z1CfkGWIViiaBoGpo20NuHh2oK3atxw3rjILzCLPlSAhyCP2jTbUbTKfBEFnof + OVY219tDdsx2K/dVAAed87MVv7WCPvhxuLiYuVmpBzHIlsykRNupJpa652DnMEsa+ihSo6KK/HDI + h95CJSvAP3NFmCYfn5CR5J6CunGPdVmYyqUEMRHYg5GPj7i9CQ9NsL/Fm/DS5VckZYCH6G6lK00Z + TDbKhGrN472JzqWvXaI05JIv9Umy3DndsCdgBeOkK1K8DFZoa4W9sNvFmPTS6qvtqKFW0/Gj7QDY + 1eRhtNDSVU8Z2qVEbkr0pM7q++Ru1EuGi4v8V3k4AmLOJwezeaV1vslplDlV8YlmCpsFKTNlsYUm + JyTj4n6XL1mDThSUwRjdiAAsdBGL3b/fWN3wgCqsrXnNaRsmy/3iWyW2gJO23N5ELwAhBJHr1AKG + POEW8mIdgzilLeWEtjNBzIBgAVFGttInb4KzFF3U7bZWFNa6dCdzx0C1JnginBV7IvRuaAUOaabJ + mY/3C6N7QFcsXjHFpsMGeEdtwHlb9Aa5KW6JSJ47OHmbprAYNT3+gc456IZIQrHiCRcVoJm49Mk3 + 0SQRLU3Dy0l8ZZD2yBjQLRjuH7FlLiVLpgefgyXTxmUuLo3AzFiSa9tx+aPaEEQ6N+XGcn7g4A1b + hHM2noRCAQqUk8MHsaOXpgeo6lngANXswqaHUX7IozFLgCCipISQIbDYXHuPJqhGRjtXBqAhi8IR + 8ogZ/LF50KAQ9vwpZobho7Fye1K4O0+AlqKT86TrcPgnNpoSJ13+F5gMVEyijXxzWlzdKwgUDR93 + hXnfub20Ihi5tlabFM0HccXXP/NPQldmc0cCPtsYG8GakXBgGVsCzZgfiaOtHJ50W4zUrx4F7dgl + /6RCR13I6mn56CQuHZ3QwYEM37ZAPlc52Yunzmd0S3JFVLvP9hQ+MVKYxFOxGMQBLfBVPLh8iA/k + FDCBJzp/E2e2fWXPH8635y+f1jJ+nydwV9wnU6hTnUiuGsoNC+DtJLxiuQxVYRc+b4LXr2j2uDBz + z6OAA2lSZz4ufVryWyjdT2bVUnTCAozfAACRq2jqeDuxagq95YCRunhebzEVEbsofs8Bqz60A9mp + lD+orqU1GjOTsuAxCpNRiALOY6fE2hI3GG4GfBCRi9uux9/mKt8M1gbAEMiQJc9tDV4gxAgX8QhK + 4l+HPqJmdRTcFu7jDP/yj5gGw2HQOFCuJMNH+IGXEB9DBGjkBAReVBWTC2oxlSlHBu6+V2jYhQtF + DUJojvxZHqBsC/MlntGjrIo/sTIl0LGQh4OrT6A7a7QOzGRApznZCVKbzc3m39O1cpmXoj4LRBWi + gvxIC3Y00V8+SSoAQX1tQCDnteLVp0zz4IqVPTpIS2niJRsYD4ofnOpBh0BI8NHbqOtf1gIHeGkH + BYki2lVMoZkEy1ZfREV93cw9Hoa1GgqtFBaWbeon8aiRCJeWPDzx8lqKBDI8w/XyTauCyxztWgIn + FUb+kznsDaErHkISLgN1Yq8Ju/knQH0kWLZ3n6NdgxZ5TOm7MMWBjPg3uIW/ULmHdWPP9IhkqsRs + +ElUkRXUaKw5KYzLaCA0/JEIv3YNTzwm7wieeFxMzNFDCk0WEndIvska4gZzUGCDDpZfw43nFJ64 + hcQtPKFBD4ZRjoStxBaRfjKsIup+rqj7rftLnXUOXV7jL1iziuxabflq13540F/bYoug2oRxjXAC + 6Vd4OSEQUx6hz5B2QClD6Pq5vAUDBcWtwjZImu2gQtKPafE1nVNu9JHa02iEUnyR31JRQ2frenhY + UFVJM+q6FmUgRubTPYjEFajwrPcyJwMM84ObIMoNmJWNjRXb+uwuf39ftUOaFlqgz3YbwcLqrXcq + VlG2o0PLKocGHdE9Je/4NWp5YnAwGEVpg+MADz0806XP5BrT3sLjKcaPpw7V8RSTx1Pawa2aJ4ok + S2h94hw4O84RIt2LdYV0uwrpzmfvsOGximoQbl60ohLCregId+QS0m250ZAfLnXca1ucbN6I4BoD + vGW5d4fH9vD1jgcdphyH7k9isDQ0EVdiJxmVHIgsW+bu1eT+hXhSlRMhxmdsFiXtaW0ZoWyivtFp + Ylt2vhY/TC59othORUSUDnju0cGaVHOfCfep4lySzRwbSIM71bPZsd8/3pm+KiCgq+KHyJ5Npyf+ + rnOggpNx644dH6fuAOWKk816gRUxS0ipO05ZCRZq19zgu+aYPrW9k7ODrr9DJPUYiebGxrrtbCMh + RUcP5wt+W11X7O/VHFwt4kW2ubfQv4Cx6JtNSItszBeXwggLi44tuhtMUiqcnw/knoGKDUSKY5fh + CTNPnfWS4AnDcFzjQSGI8TCbmzaIjul0nBtGmjX50C1UOtTXmJ4IkaymoKAXNV3mm8Lsd8uascLA + y3who8rhuq5mUgL8LgVAmdOOm/AH6x5DNd/hiWrmnZnjAC/ZRQVqYU/5CBC/rbdQxzM6W9/SnP4O + UZOQ5Q1xieNgMmzMHUL9XH/bEKCOZwyhvqViCFz3NgzukkneyC6jARAI7j7AL57Ruz6dxcV2Yf1d + WHqPEhDKxTNdlcxS8ZYnk/CSWyh1y+aWsIK2yf9Klxf10fJIfruFBdfMoIUdwrF7IOj//ZylKqhf + QYv7SchzADmuKfMsysy3FBoGOcVivssI7c6pL6otWIlHKums6M1zGrHn7lnTaWn98RNH0qSL6Oea + dSuaGfHdViAohcxBHdZmze40GJsOCaGDYYLU1qXpQ7A8q8ueaGlmVahGiUWcaTX815pFdNDXuBZU + uAwWHs5JRjGQUnDpfpt2PdF/HWl9EvwpV1OANOcEQkLtYyyDmF+uXVhpWCjvBouLiYicp+HiEEee + PGeIU50C5I/Pdw5ccx/WLbIFGFifjLF5g1ALov1zoTpvRMURoLp6IVT5Hh6Kr6Pk63Ztwg3rXUV5 + kUZwL44PkVKR+teMWd6gS9O7fi70urnLL+uLrtlpKR9TGUsZsESa50U+eKFc8ItpSK+GUXxVZOjj + K2WRafzOkxmyxG10S1Focu4jtbi4UKGgEie1KIIpHRqd5MFoTMHOiEBz9fW0/ShOIa/yWqNvmxG0 + 1Nc+NGIvflOwfIuLkB2dnHm0wk0rbvhFqjNb2vZinXzO7AbNOu52EJncAPU5GD1dmDGboDAtc1hP + LepR+k2sJ+Oh8awrp8zuAsvHZfw9lN3pEoV93zxmMLWm88E3t+KLCezzpvNVPf94YjpvfRMdW3rk + R/TONzujHprzfu+bnybw5aNv7oOUjWcCn3zzdzbOE9N575s//jlh6Z1m3f6ncGPZKq7zOesKdTY8 + 4Kaub45CdBa8HfXy4UF/2zncL384Zv3JbemKI3UwRn05YUNGTLV5RmePKRZKkwSog6F9ifpdFUBv + odkuHAfqa9saDi0T3aOMNwbdKItBY9t5oQfL+TVJ+mk4HWsUQfvOqd1j6Mk2j8WBzgJv/GbRCXV9 + Ss2FKoTsjNPa/ZIhdWEQvbAgo/nyeQUKubAwDzpufNHAUwnEXuMMXoLxWIGoeIUn3PfSZDhkqZ5e + /gofUjYGPNSziC8YZmZuP37gZ89nWRr+0zdFx/HkHyqqTYLlRsk/2DOxB+nimQJMX+eCSeJb2ULA + MmG52yqYV3wxkw5UgpGVX1bOV3yfKXJw8TsuKfsRGBBuIQTF3b3d4r7ixwf4QV1rJZeuSnqrkmgh + q+/v1HdY1urr9+orX+Qq4aNKoCWvvn9S37/8jENRCe/tmuBspL35hQ6FMD7Ur6R8RKXk76RyhCeh + //pNRU3bIld9bgZqOz9WvvMraGTqz5VUtR1vushZRCETOUm4/slhQHMZj46JR2zoUi8F9Jg9pk3i + 91pxJeaz5fOyLS/a0PxNClDGZfuUy/a0AR0N0DoV/ZonWQf2Ej/m3/EAeci20eUwk7seEKPLE25c + phJg79vDaxbmGbLXiDUYigrznpSyVgWbGV9JtWHOtMmPOR5ti/vIfEMb8ySO2q7rAdfMbIRRge6R + yffY1FFHOIpL0M6hTDw3onjiczuSTksjKDUVPqspMg7dl7arz2iwPPdSs1oz7s/v0Zc3CuXlhsvf + 3//u/jld3hGxqz6LVapbjbMpPyme6zWLJv/PaHJPVbmMp81am1LhqqtQ7/+0Kp6O85sw36UAkBsg + AMtknKiqNqW8/UttpDPrkSplr/UyWr1U46PhSOUpPozqV/f7cB7sUiXUPQZE1R0VAE/vi17HArV3 + cIG3zHxjjbs8Lpgp50MUxlJHyfBuEA2H2QFjpMct+cQiL/gT/fxm3//kc0pMbCASa0Hh/dT6yTF3 + WB/vlKOpUZsImnCUUqCmH+17xnx9Eygq1L9CtUDrTf5pplL1mR8ux5b6gkzlz7BUmCYIlnYSDEuH + vu9au0/l9EGGzZnOkDziq5qWfVVFJD0KoZeiLUyxJRRW0xH0n7ds0oVXm0qs5E5yczqIR1ozkBH1 + 6KCRn7iJj2h0GtP9kPptg2n5bJafyd0iVS32U49O0tA0FJUaWl/5WRXQdbKUmWoWLVDlFrlkMOsz + 73iGJ1/T5Y8wriAaolz0WSsRcXUJhvDmVmhoUbVZLsrt0z575a/byAhEoV5ZIivTcqbT5ZN9aF2s + cyJV6pjs4NjglzRqMUuFmG1gUeOG1+N+JrOp+bSG2w1hMsYArl9uzyHpstCyOCCVp4ZVhgGvXdzf + PelQGOJvqJZQdVfcOTi38q3j463fzncPtvc+7nROvrUNLlpt0XWJc5s4fPtjZ/v0fOvkZPf9wV9r + Qaifnmqic3B6vEujmNawUvdq29J0qdllcgOMDXeyGOMuC5ShX6LEgjEw+e/y23fbeOX9Mqpbjxkx + AGLj+k+rGWJW0TMg/ykMXpCLFpe80SOFWQgYWRygPUGGYTZiK4AVHjIfyaUzYTJUien0WSVsyYAB + OxQgSvGrx0wHff568IPRTFAHTzdPmngkgNYiICsOmR8yFdDvUrxlYyA5zBlDA8os3HTQOlK/dk64 + QbKSQ8CIFVZ02i0NxZVz7OEhYchjce9/Hh3FnopryxcsjAx+idHBGY8OqLwscjIfB7i64jK1bpFS + IqOaSRMZOdeciUWk3CvHQKA9BbaQjJWcfXnkf3HxWbxk9hiAjylfa5DiMcj72QXaDNGP2yfdoi9f + CttXzes61ezAZM2qSod1nQAwQbhrzAJRK8Q9qYmb5aUwbmGpiHQpKZdQmXHDkMHwdOAKnR6Zzgxm + HExTmKezMVSSAxoCt2Rdaq/IHaZWSJhPsW8mtJ8msZhu7v5dsQ0UGnoRJjtVVy6S2TbSfEIXcZbQ + 4Bs+bKs4cpD9cEJjN709ib7CoiGlMPc116L8cCiha72wi6vNxCidTBPJ4ocjvQyaZDp37Kko1M4N + w+OxwgeclfZ2fu8XXwPAZkbxEQaVLTuNiVv5MHBK7JuCeyBrYIyT1DDJE1CFeiAtHL++KC9dX+Tn + Z2HXOWQWXlaZoVBs4S/wd4Ez2cyWYg8byqApe4pXag6DC78Hs6GvJ3GBGWCLCkPPgK3YBG6Erp/D + 7jkxv6eZpWc9jHjp3OJdjuqjw7QcusGt/CZWHw3P6RNzGHLxPJtZjpO68ArRZmRBadsj31bBlgHI + T5l1ljsmTCvWnXSdSRcNamIrLZabTIERtrUgCX1fcoChMxsbgWrgXuZYnvHyWrADnoGWGmbo8wzT + qc6clW5fvAG8yKUF6w1rIxI1eTAijqY8d15chKbGl3ItOwwMCgV6G6dMN9rlOMcPiAXq0wkt9LjB + uOE6Kx36FvcJMu1mg790aQHX7Grip8J3qLR+78WBVe+ZU/cWpF28U1xLy0uJGKkKwyXROlYjQnCR + A/tUOVvyUPB3TLo5kel8zp0r0XBeGQgWdycyTbFoVe/dfCey8YBBgD4gPjJcFRpfvKWgKa+bzbtt + YhjqIovkRZgUg68v5tB8pI4kImhErV88eM4el5PyspyUAulIQU5aWkptCoJTyEmFPWkuXPlPyvv9 + wTfu9zmFElT7/Un9fn5S2YnEnTwdpDwg1pmFV5GJ8VYwpjEi8jUS6MQ3dY8nCqgotuvCpF9QXdJ8 + wsZAIWRUWGTgi7QWGqZGpkWBiOeQjdZlSRzejVKtescaptgUo6WgvL0i/4JXEKMcTrfkDDD2He1e + YmvCYnFdMWH3ll8mfanEJIjviGBpfpQdBAcW3oNsbza9JXxwZtvigU3k8qHJ3SnP+9E3znsaDIpZ + 39Gn9+FBP/SCyd95ZPJnZ5Dr050zUzjabMXRiIZKCiwySfl6PCeJHzvPSx1l9SkAJBiNmj56EZtJ + ifGaAWsJnrtleB5/IzxhgkCquStguvsoTHcfX1DyXIdXqo5oxLt+j8BjaQVHBIuynE1ZpeOlhCqI + KL6IoPri3Yr8WF9LEUysFnPU1HPjRNAsCIhulyH65RshShEfC3huPwrP7cfhuXy2udi1Nj3cVPB6 + 3YewZ/vW2f8s/ld3yV4G6NB1Hn+48MmlL4m//D/WHzdLtvWH+/C9janfL1OETc1hkWKZZbA3viUZ + pAAHT+0WY8/0DQTdR3ADxBtX+RXm6NGyWmjANjOAM+55tod/hTgwG65SszO0uIsouYaqWa++686W + m8WcljM5AZmowkQCQ6nyfDjd3xNutnVZnXLODyAiPD/3W5CWH8kt6Su91q3rBf1l7kUnbgzDPKAL + wqSfMHoWqc9unuwlNyzdDpDD1a4tLNhbrbLqXJStjpC/Rz9GJ/QDMdmwisLNEGPl4L4hBQhywV5I + 9NKT0tIQXvWiEu5fhd6CUhvur8rL84ChA2xZ6TrcSdjLgGsC1OIIYkvXEcrGZB6eOJ2ip179SCfC + 5/hsghxjV+9ov7S6B6jAE0ysFqv1rOv0gQ2vv1SYW1sPuKXeCFbZQsuem1kwxMNybt1TZaYDvATv + wv9G3wo0l2B0BBwdDKhr8uZnL2ee1/ZszvqG1WCBq2xwMgpyNCIkjnhKO2Bp77sqU+o9jVKnz6DU + I3GjREGsr4Dnn0es03YpVRLrGIk1RsbzyURr/1CRQ6Bu8lS/cnVFTd/IC0sXACIKghdZMTk24K4P + UumcgHtOtQEt5GBUPCOHz6GzXwbch28E3JhrsQu47euQKQC2Xw+wBB0MIwG3hMPt6Fg5jC4u6qc0 + so+sICtkFoi6QwxM6OMVXSa7ZeEEGATkOTmHCwgF+x7CMMH4EYrfk0UDGst2fjsbxhFkFCeYbmmX + kICoVC9b6pkcMyaGT7rpi8/opw/bLwZSNynyULdGeFVjwxtI2knJGVRpIRdabalJi7WthCTAauyY + VARpySxq2kdnVCfj69tWN8ULfEo0ZCr8dmVVgV1iCGoDKYqFC4gxDij8jdCSLaAdK8bMw+mI/AjW + FMMAjRiHxBQNEP9BDnffChlWGOTOHwjDMATopz/bReYsNDFCpuhfTP1KNMaDJP55q5UDmKtMYQ0K + BAnogMTSanF0SxZPYAcGYVZ6UVEU8FFT/+HmniGzVZlpUuWgdJ2g8kLTskDft1ipYRN7WOE6ZieP + g5doSuKfs3oag2sJ5abAFYvfz50EBZPISkgwoY0hKNZeArs4JbZmE8W9q6WmEjVheCfSbS5XXubU + zVxOM5fJmcto9CmxW4oUzJWlIiFLCfxTcqrsOuNdJ5l2Xqaa9kiNV5ak8dQSozWkWn6AxYzemmTu + 2kVE4yRs04Aihl3UqRWpF+U5qUPrYelAPUv8xB3IagcSjdlVoluokYij4xVtygfL5rFLqXdMxS+Z + pbKmuZTQvT5yU/pa3pTeahT/6S2JAn9lxY70tV4h9HW+vEXOF7Ffuawqqgu3n9TfoqHf4lVy4GYU + FzOvGicCW2znfrH4j+TCPUSrdSwDzLDGKnLiq5gz5pxFqHEG4dUJtWxh+ZgA8E9KYuU2UA7jjW9a + /Lg0sIobVAPa2/A31jgI24vnnldYdP9lJspl5XJCFqrCsmaRSoET9Zncc0KczaAncN2tQiAYVYOa + ypguEZr4RAzt22IHz19u6KCgXzZvE4FueCVeeUXUyBYxjBtjhfPQpEKyUF5GWpq0/CixarI+L56e + yQqVCM02U8nucUrDXH43tBnESXyHXlEg38bknoWLWAzH5xRrqnR6deg5hx5CQb2qhwdCGI3Jflde + lt+rZZn6cg2q8I9Pr9PbS9LaprRI37GzeGaRxu3SZzw/fIv8jAju0GH82gHiGn/9cIxuMpzxdjLJ + StLV3HjVxi4e89CVhBhpgsnbuPr+mfKtNUUIKHoKegmCSCQKgIk3ItrwDEh9IW4mSWIZcarrDPCi + zyGdrlcuvHOHYgYy57IuvUZw5mwrq1wahjt/rIlICEtyu8lFcPmylDVxLiqC3YxDubkV89t9jSQM + JynrGzeX0RB3gZylIRvTVcAAZDzQaevNV897KFCIxotMdT5Jjd8fbp65rouHpx6eeJdTK3HASIEy + rVwDAWtS0T+QSmEGZmZX6p47Gkm8UKucG+viqWexmzHt1kq0BF7IJe/bpygT8gVRUQIAYN+Q30kk + Zza/50/S5n7RDYwnVyJvpTCBjJ+V0FEUMLCXGPy2EN90dq6oXsuBQBgzfuVD3CddJvYNG4bOIi3A + 7/StntZyuJYv0uDG5BjsCgBwvXnNWd0b2ws3Q/yxen6j59y56Gng9/jdDBSSiBrE/ugNjm089cUB + X+Ot91qIA1IjhQBxEXOn57ecu2qYPIxLjro+q8ejKQTQA+V9egfc9WWAjlMsRVsU7nDKz+U5byTD + xM/wgRiRqtnlEbZ8f/LwcGORbKSw5qZ0+tVss9cDeSzOlpbsMUasRsRvD4rIKkgGNMTrVMKDSBpX + 5PioHQNynqdI+6Snjcpp7/W043Lan3raUTntFz3toJz2q55GhJ5sF35nzm+0CSAF+J35eMXQ8ARY + 3uCCQovv5mxkmefncXqOR/+wRemx4HFvSNAap2bvFmnuMLkA2P3GXPEBV1oD42D9zgoHnj67RrcP + zAaPFMFmJg90QcsWpzs8o06b1Gh/pNHimIqGFxfpq07KZD2Li7SPEU5VyV8Jt7AG9OsMr2hN8O7q + peVFCDOyApRMa0pa8N1Em8b3nYNTY/fA2Ol86uwdHu3j6/7hTgegjFkI+BhUTsqVv83a20iXENt2 + vyQRdMYxUFVNm+vPZDK2WoQL+2meO0ZhGkXuGMm/HuIDbQ9+RpeKhFRpyqUiu4rGB9AJvyljxRB7 + RSGGVagYtKo5FPEduQ+K2NQFYxJII5AnjOWVEX+GN6EiEgbanNWRT5y1oNQpmK5A9Rovf8eZDJ6c + +molC+kZgGNLBNLnLGyKENqq8cxAmidMLcmsBMM7OTmaegiPyYBcv2d6ElcY4ZlOaFWCdKCGhcbs + 2igb1VHOLI+cu+dzP4qI27vkWhQRknI1705150EgfIbFRIaV6SVuSkb2nNXjwc4Slt1UMCghzy7u + +6kkL7SmyqZ8s0ZxPYk5hPpcY4XEjI9OKk+LAM2fv7+n67CzJJ5+pkgvPe1OibQ0+pZzr2oWYvex + bMBrTetho7tUa8EhKGwD2RbJrhSpJhEXuv8dZvAXEZcIbT/+1J5rT+wQIouLH7VsmQt7PFleobnq + 4uKvWlpQWahNezrPCagyH8IMCc2ZVAQKiWibpTdAOw+QPHl4ILv1nBdFXCpAoyycyPH1r2BgpSra + EoQ4PbufVamRsC3Wvugqr4pJmn6Fr4qbJy0xeFRRlAQ/xmEwubjMBeNON8fHiQFzFmHRYGjgJRAp + tzQwBRWlyzo/Hu/Jm/fwJis/LdrLJfSkuSS/mKYcqUjH37zij63gFdv/adPon6qm0T8r0+g09zUD + yjgvJCx5IiqdflmhRcCQ4zq3gu94SzJyiUQXmp4iyYy0ClG/3l8uX1rCxUkgzSk656uWxl8muTj5 + rbvlRno2qwCQKuIf5J+5xIVXUxN6eKsw465YmJVTXAybtVdpsfjWnlP922HSq9QrPrlZ9JUVVeEb + N8yuq+adsCSvGsGJz7ZuCIg3nrocYNEA7c7mOOuKqGUBBW9dXVtX91bm1cCndcyADFZ3gfRPXtl1 + FNxRjOi5KZq/iFh4l8lk2H+vZ/XnJWiFp3WVSzOBeeWt4iYBXLxCtUksz87ePJaH7iGYKYaXublB + SIFMd3dgaQBLLzW5FjBwpJuKKBNVOC9LglkA0ln+E7urz0M9iB8eFqKZXgTY+Wt3H8lzxp8/5PgS + ajEnnIl/n42DeLfvBQ7d0gBPQLYjoJkYAcMLpQ0BcuVAHw7JBPvhge9H2dZwmNywPv8KIBQke5Jx + oIpbrj5w43FAyG0gcpaNAsbEzbUcR3RQzvPVIEhNLitw0A65VAvpIJ6qRMsEdYRiu7Sd0gAl98Zu + ySvoQET4FsXQHb6c+0lwlGuoQCIuJdZ0vRiy1uHJ9EkYaVKL2Ww2KI52gzxJG82WWVteB48W/1ce + yy+Z/x2nfrPRgmpSqixWVcI/aGFa13NVkzS0rtNFc7axlyfB7FIM/Ptr76zptLpO37vP7zzzLXBp + GZ4nBKEXO8EY74zqe9BQ6sE2HeHFhNqheIyXHRDf3XfzKx/kJGzHqpBDaHhaTxtKR5dRtpN3eEhp + y547+dPZT0LzCxwrxbYGQKhQ1XPCBkaZCC/Tp0tUIh7kwYo5ZTo6n3ffb01BPFAqcNamyJ2k6aEI + TAHv6jn3schm9jp1UQQPoTtTQA+pK6JF401WOUblm8l8FvFo6My9TLKcorfD9KgXCsggXJMpQb5Q + ArDA/CM8wLzgMMRtEZJzmJYmSOgu/wqkJX+zAF2CPzKO+HQOWfg7W+ItnEvScM4dcKbzScnf1XiL + DORCqPMcGqvrwGOk/W8EgeoCkXihcRbdyDh7EubkpvfqJWdYJ7kvo3lKIylTHkoUZw393J9IUzpn + kPPDjuPOz84wFwJy+YRBcD4oM8xX5/zLkS8LRQ4w+RgbQ933/oTaJakIklZS6GEgxZcCXZ8fjGR5 + fT26gij1VSDxInCeJOHluBTi4OsdOm/5nzQZVpdnCy8TuQWUz3oLG4o2jD3Jg+F2L0OdlcuvmuSP + PQofiI8wCf4p/xU3leNI30+ifkaX47gwYTk5ofEAE3Sf3zZ3Yd7mNVIhHqjcF7Hl8BkdlzDGu4jU + Lg358YWS5hvulV1p6Aa9qV2x0Itydxetq+HnzeqLhwd8eN1qAirU1VsclZU9CYvRMX5RYb/aUOXQ + Wxuff8/dEjxU3k+dWy5KkyOCow/W1/2OQvLIKY6EOUcMqGfejkF4i8haj/PVGkuEN7hJq35BIM1f + G0C8jpGcNHZ38NIdrEDaUyW1QoGM/Jdmch+lnUMciaAWsx1UGCjyQpxtXGYCjqFSABlkVIgHc3nS + OVVSdm7kaT5SulqzxmPVw4kyZ9zafm5R2etMxvuAFR7oapBJyYFJm10HfXKbXScpPD0XFyN1cUOO + thV4dgXSB/flDIQCSsVbnAlDJ88hS8exKHcKAkybCvJ5Dw/J7HJ8eEDLK8JQl/LTcSFgNV9cpcKC + JCAHIEkFET1+zizrq7E8yOV9RDbGfWQoLeoehd9yPqsirgGVamev+3k7w5sEak6R87OsclI7b6n2 + SzorTu0onj/X2+nDA9jw+w84LCj4f/GKGhfSPAvoyDjueg2150mpyCKkO0aXvsxcXMDRyQTGHxUw + SwsLObemkBRYik7y/SztSgTVPqltSfaSlP2aXfcz26ppSr+YQUspN9coNXcpA1HAmCvoPXNZQa6z + xkPUdclLj/ixqH7GjSkNQAo65RZeXitdNDnQT9f/WrXcSOWRiq91BWKpakvab4hLMDX00JcdpFqV + o15b4IZYSdrEbvdOkDhokNORW0SLqmQujyrsNZBJKwXlbVRKOFrrTl4eb0+xEk5KNj24Y7dntYMw + ls2coOppo5ukQ/EVHz3OCH483uPJ/K8GRkqxRIHLlA2ADkpo6JtVIQTlUqml9kh/Jrcun0lz72/Z + FWPap8qb28NDPH9jsmv1pzCmh4dnQEDyBPcFQQxQWZ3a0VnQ9VP4045k6ADiPoW4YKmAAvzOY7x8 + UXwQvr+SY0WhVV5KvckNtTwRrS8SAQS0SRR1oOmf/l6tc46LFrASKrLDDEfBcJvWmYhqHvTgpUsN + HmMiRC0lvmF+gWplJb5Br0qwCnNzy76lGndwoy1JyRxOq0yg88S2D+DMixnyWxLM/FY3Wh3aDPor + itiM84zWqDhpEEH+6Tsx2uKCIV7P7CKONlNAhBlNPl6IqBO7QY75qgv6yWJ8cWNJvrJvVbgQodiA + HcmabQtvY+TsNa45+WS+75zi5ZR58hGjLHBPs7YGdukpnIgdirNcwIXFLoaUAZLb1E1itOsW5mxZ + TkmgscozXEqlK6QmmZ9upuLRazozwKYCPKqHDPdDL1pwNiv2l7QUdUscH4ynDczhObzY6QtvaK9K + 7hW6TdsRt3lUUeGx005SwUpHh4MMd8L3ezpdw1uXtX3i1CozxaLWuMQit9VZAQmZBXtA3Jcidwnw + fwnyfwnxf3V+YsACJlUWEK34U8npYngRVwLDnwsMUcUs6/wCGT7NCG+T3yklZ5T4eDHPTds75LiM + DrJSpNYYTkcBXIn688CM2apA1qWPW5206rtgDMw8h3k7LvR/qXqEjpGOL6UfJy5Ugal6dHhJrdSS + 6ZlLRZEgvxTVykeHlTfmtKQORXCpFy3MBB8Eq6yXXDyIU6K6gBHkTEQRnDAwpNhYeAgoznUJu2xg + 7z0eLorf+kCpQDUoZpR4VqU28dBTVWx7PJaUyCV+i+vNy7nRENAWF4xOLaLaQo9gCwkQr7jj2C9j + +KQVnbEewV8GqKqR7bfG4waFtrLbMVUroAcilB8r6zbDtGG6xnj1DavT0DSn3CBSGesG80wks5kE + Ms4J59lwTmRCie3s6191zn0wk6B476GeVFi0XsrPMxb71LGxTK4zNKccFzKHNDQaqQ881lgp/lWv + nCi+3mh1iE/X5Yw82FLHnj5ywxZjJc2g3PYfubzpP23BcJlXLBhQgcp3oDEdX79ortvORU43v62s + y0CwWzs7551PnYPT873dk9POQefYu86dtyen58edk8OPx9sdb8TfT3f3IbGXO52DHe8ud951tk4/ + HnfOD7b2O94NvB+cY0qHnk5Ot45PvdPcwSAjsEbIdoTH2c+829w5+njyATOddrzD3Dnu7B9+6lT7 + sYUJog/nxTN2ZPfg/fnbj+/edY7P333c2/NOcoe3eJA7h8e773lV3k4+9S+k5vroUc21unIJKT6M + 5q/rsG1Hd/nl9grCh1e7dUjFrtC+ufy6gtqk4k6Nt3TVj/LSf1wvXijBYX8oLqkTfCVjmiGJxa8L + lYpumPWzZ/TDOs/LN6FkPIgiiWkY23q2jjMzNJdu8y4ZZNQkb+VdCyY0Ifc0ZXwlwqTNLyIiqJhL + NWW53o6iz3Q6vm6zxtm1YACfdfO199rzx2p2bqEIyF0xjK2IhjtKFO5l+YxaQFlb8to6edVHU2wy + UGc7NqrVSkMqqJk8/ARFkg1pRmmPTU5h8ckhUzcspYurGYRqFDEBiFBNdbPjkn0HSnI2v9ulqp81 + BprEmRHM7/xswb/c1znxfp7VbT79h/nSQV454ZCQnAE4bWNHwNCRFT3mV+zdkvpyGWSXs43clYZY + DBAR6UNEyPREnZUuFBP+1GihqXHu/oI3wnwzkSEPWtxCzMeJzYxH8tyrzMrXz1cuY9K7Fz8xLhHC + rv4yNtXt8uVrU1j/sM0aKohNeWeo2S4kzdysSbqW9LKkUvfm5CyTyVIR25FeMzxnFuLlIML8M04O + x5WDvHLuK3Y35gd1z8svLmybl3kOYzXFrByI/2EG66jKYN0I9moXeapXL19KlqrEGB1LzmSbM05f + gJk63PnNu8qd7bfEMO3lzo8nxF15+8hVnW5/8D7kTg1j9jV3ftk98N7mzt7h9tbp7uGB907jtr6n + KvnzR8WPfQIeaFfyQO/n8UBMxFOUd1OkzvHfyANJ184nzvCRVXqbn33NMWxKXDrQlzXFGkMzDpCh + IZMDBzUQup8GUrwLpRhMTTwl/FLs48oVoCaqAQUymRZb/sTXGYB+iXsY+PoB/7B09H/p60zEuK6p + vXJT7eppyGXhqPnxeA+vsTKXYMbzbul8w75PlpacyH+XFwT67HtA2rrjmxFmbzQckZmHq72AnAvi + Wml9uyHV2tk+VPX//p98XGIN2QBv6RMks9L5A9cV0B5a3YjPyt2yxYkq358AgUE0C/HxIz1mxaOW + 7VOOciBl26PHrHiU2bgWwYJeOpC9LJd2Z+7aQAvKokRdel+l6/Eel2A1lwM8LsEyr69hoPepkOaV + VI5F68oNVbkPOdb+AY0XucgKj1fyGzx8qS1/WfRcoFFdrvET4w+LWrQ4ETA1APZK9AoVkmXOgIIy + IJKGuVTfZlbquQzmhLEvpFN1KdaVFt6qrraeNXQIgupRgBHfM0drQnwoqhMfVMMIVIqYIYMBKm3F + pdBnFIEBVRInbJaJq076kOu+1U27yMNtaealjpOxOOnQVxeu4eRNq2xu03zsqs7/8P75vrp/Hov9 + 80+unlhfk9b1v8zfpf7e+8D/zN3iFvr/AyD6pQoi7CC/UoMgU75wWoJiBhCtWkC09GusWl2PbOGv + 7myrtSYOYqruC6n0P8ArVbS77rE/feu+kt2rq6NypVNxmNZnWZSixZz4nqssDw9n3do8bgbzozh3 + 5o7Pcn2Jdxv4KS19AvQvh1QiRT2q4d0vc6wyc2Eox68ywqgZmMG0ddnLQueK3OCTqnx17tFnyiMe + Z/vlHB7HieIo9x63WnZ48+e8eu9x5wsnnVAACe9x3mo6FR0nCx/O/TA/nsOI1UK/dJErWi1XwF8E + f4OuZBgZopSORz0MRK87qoDuVbTp+ld+3Qo6Omq5p0aUGcLO2OhNcgMotZGkxihJmdFnGMoTumtI + nDGAqho9xmKjH2W8kPX9fcWsHQMoucYpjM0YBXdGGEwyZkxiNHPCAy1yJEc5HiSljNqKMrx4y8AA + Ba7rfq6gcmX4tCrqrUrrl5E9FVLxhfvz3Txk1BtUtCEzZ65NVME43gURjj5PDMS0KBhGX7k7VEor + n9MRqAHj4ONVhFPrXtbqnV3mzq5z5YCkEzMHiNFPzHkPW7N2WZgJXLeJcZkspdk7OH675Sf4pf16 + mUcXffPd6xHLA4POnujalEaejKPQNMSdCb75LoqD+CuLzbq8eMVHkfWnZASU9GvEhkNmfEhGDDNV + ygWTfoQTpxXbguyVXIB+gAiN65aW7fDV7vDDq5+DaDnfa0bvWumH8Hblxx/Xx0t7P+7+/mVwsnQ3 + PNn7M7gdvfAr1V0kycWQNbIItnmqOuJ6Eq320e8fxyu7vZ0/+6dvL9PgvNP8/c/D9y8Ofmm+n2y9 + /Xqyf7gxCrd+3flxo1OFRBJesfzRytfYIFxjAWuubqyyVnMtWOu3+quvXgVhuL5ajH0s9iffTC48 + rO+cYgxqnQxSaArZLcB3VHDEWQQT7obJqKaSQc/DCTiP+loVrbW1jbWVlfUXq6svX668WJtfLtNK + rTRfvFhbefmi+apZ39sKJgAFi8LhnJFN0qGW9TLPx5m3vPzI2JazHGCcLefZMGjQM1aMVyobeZQP + 2eOAMVI2xGjvQRpemgYFAMH4yEMxTct4CMdTgZDSqoCvS7cj6CSaU/imlsPFz2+++060TjVjXbhs + JuFlIwpx3tH9MQNQrzdv4f+qmmpGdxxfqIFQVbw472I0gjlYxiyy/CC4xgyN1ZXb1RUqLFuiL3+l + qtbabWutVBV9KVc1CuJowLJclZYfXDroreTNrgQUROYsGARp1BhHccz6DeCl3Oz6Aud/CAyK+V/N + 1ov19Q2E6T+++8frhUbDOEoZ3lCYGY3GG/ioVT6Gv8hwy6q3OQ4to3vG8gBesuW9oPc+TXKWwfbw + Fnj7q8bJpAeCoHuTDAYrphHAEDGnBA0+L4u0ME2yjHv8+GZ5WN/acjLs/680fMzo3t7/WNtHpydB + nDX+082eRgxkgAxNBP5X2/6PznO14d08QOOF/43m/8NNE7Gkmjav/VevNlqvmhvNtX9Pi0Qrs2W5 + kTTETtIYJhcJp1zYKuX69lplZcjJDofRBTJCf2/Nf0tlYjd9YuylTbAPVACq5rZssu7l8Rivo3Sf + YFueqoX3yh0IRtSNWf6cYjc3N65g/II4GN7laCj0zCaLsnlwIa7w/GuFYa+E3j97rGE/dq/S2/5z + B9ljAS6ObykCwlRKkfq/qdSXrEEyj7Ii/pYRwa6fJMPguUWAV7igi8pyGF7+3B6iqg27pcrx1irI + D9CK0Zi21L9xGl0H4V1jdDHKZzs5r1QexXdjENKeV+IyH/ai/vNrB355NMmja/a8IkPMmbErFPW+ + YQilieH810l+N2TZJWN5iQOroxcZZW2QwJw1+oDv0RDAkWVAp1eaK6vNleZGa6W1LnlwVbMpGXdJ + utEwcBkKlrtdKlDOJ/ojpYcCusvXq5KA6SKEzKA1IUYU0qWfkLBMqBP9CcO6bdDzvzwUhCeqBoDd + hc0zRrleukf0jST2DKy60VxpNNcI2IYxZ/SSmaZPMwSaaywaqpHMbfU2mi/WX7HWxisQ3DaCVy/D + Fg3eoIulfIoTjzQc1kecSGXAP15TA/Dwj/8aR0GcQAsYPKghb1Dk7rOpcQ85/gEoMB4Gd54Ro85n + gatlgzhvQ+IU+Pl/uLyOQcrYKJqMKqV6QxDk6oqVm86GMES8zPVbGv/H62UxktfL2vAEhv8YXAcn + 9E1gOCbwTEYfyArIJfQDQlEaajjPy2TLX4DTTu8aLbfVclfdURSDIFRFkzdKxfLtNbvD4CsQowCv + stY//Ruaakyif8cASLp7suLvvqXm/Z2T+D1+HUQX/46aocrlcBhBwtb4aZjwlfOty1v1KsgDbpmb + 5Zj54s7Hi4PIJr5ERr4Apop7gIyojzf3xmmUNng/G1iNiURDKPjeUarhG/cmV0mb3r0ZKSWl6Zmj + CM/lFJk4TVDSxlxVihJM8ssG0JQoxT0DMgh17R6gIdSTpxNmTp2ZYvzS5AZqLvOMihHgHykw6cXB + dYN3oq4NKMMjnOxgTWLKIHUQDDO8TAI7igMUOjeoBdVLntoYRFyjWt4TT0D5CLeglpqy8LVWH4UJ + IFt8ZZvSWv2cJi3E+6345JzDdJ2fb308/dDY3tvtHJw2dnfOzxdlg+eTNIL0487O7nFn+7Tx8XgX + Uum0ED5/PNj9+WOHlxD7NvpanJ9vHx4cQPbdw4MGGq1gkTDBwMFjFkf9/2+lCcIPUs1zGDaPWn3F + EJatq+R6svLyzxc/t1rvblu3rdHuT3Fz6/27IHv56WQlwKnh/d7tE3AHL4ONVrPxYi1sNV68GsCU + tjaCxupK81UreNF6+eJVj6CH96zDTomgQ33lT9TY2l64+uKn337fwn976YvWz0Hr+GD1p+baSTr+ + +bSXHqyc/3Lav8QzdV5oN76Osgimnkqz94OVny+p9NvJ/t34IDwZvQo+rm18aV29v/t49LXzbmWv + iXMfIn+0Bfh5ZvLowQ0x3Q3ky26StM9PuRErH/vUAHRt4LlyKc9F4wZ2ycZjNT+epRH0ctSb4ZJL + x4jr2SO13bDeYAjb3PwceO6BZ5WiJrz9HK+EnBlgF1dNlJ0AcN8l6U2QoluAWlDt72Yp+2P06I06 + vDYs27g3yPYEiAytwDa9hvDKaRB/z+Fd2pC11e0jGNYcykcDw1oI3QFaXmZn5vn5JBsH42gPbReT + 1OzKPDl3JxMtBlAl/9JWPZAxo8W9bJYZUaWm3cYYdLj/I8uDGgvIbEqOARkGE3Ogch0Tqj1oG4F+ + CZ3FbGNqkFHzvVHYlFipY7zEFPwPRqaNc2zpvVZnuW1DtuTid+3l4cE467Y5ZAIZChCqENZHWk7R + EcwZnDW7hu/DAMYUjwbyB2crXeveuOiP0y3UcLPs/TDp4dXCnsEcIxyN9ygAh8cnz5g6BiKFNr6i + Ke60etZ1gQEL5eUtgf3/s/fm620cSb7o3/JTVKOnm9KYVaxcapMs9ZUg2WQf0laLMttWj68GBEAS + NhYOCqREufVA5xnO+b77ffc80H2FG7/IpQpAgZsoi56WZ1pEVm6RkZmxZWSkGXHt4vvMjRWMx4cu + wIoywNnbeQ/gEV7DyjT4iwnux5ec7poa94L7tq5BRid6/ZqAxg0b18sYGK0+Pwi6kYP5LuK8j0jh + 7q0HY3OHp08S1HoF7d3OetB3LWHRYsDU0gvG832P8T12DQiodHnChNSgj/rbItyNI/MLmKBh2Xib + 0fGknO2Y0J13/eiqQ80uBtddD1r/TlP13qCRzwYJKoCENC97g0I3qOAPQKIPfoCS1YhpBMe1VTUq + D21gVcpg1CxHabHBRVvrwXDdrAIGZeFQ8FJ0gQQPL3q8Lo9fd7vHmNlf+Tv+s+fitS/4bzQqn05G + nQHhuzr8MVBF5TEp9hNjlFifq4XGza2uWjV8DK155py6LrYhzV1LiDhfyD7sz56Oy53ycGdEE40N + sVB9+KZzVj415INK2t0zX8jc/KW5fs53xxYHzUVwLhu02u3nj1tzme+/mP/1/osrTkXQKc/GXaig + LOHW0VNHi/nwc7kgx7oubCP8p3Wu5GyUfyOLLWr/50vNX3ld7A72IB8u9yZ/LZ+RBDCdjPnkG9sy + 2OtP4b/97clon5YzoS2jJds+mU774+7ZLi6jg1YjY50yXvQP+2+J1yFSJG3eQY+lQcrd+L//o/er + WJfv/2Oj/kO//7eNqtYzWo3DhWr/6ITvHoev4rCIXv/py/CnL/+v2hdK/kdk0z/9KtdlTO3RtJlB + Dcqn/dP+cHKM0eyQSEgjWvsaS2aNSeLaS1piaw9cadqv305IQj/jvUOqLXuJBGsiXltHEqo/JUmP + RSHzbVDacHH3XctQe2ucvY5pvlnmrElt59+EkZa2S5/7nDYhuw8Ea/91Mpn1ubfj6qMV3/nzbHK8 + a4NWB2tmEfB3K8VvssHF7xXADJcA++ELQGtAszaoOcAsXDarBtWEpP2p6d7mwYFvxIIrbbp//NSU + 5TOuOJJliJf2JaF3YyNwlsBgONifdojP1YIyl1/csTQSu/flYMwFnfj0xRdU3eiMwwATP1/VBzbF + EqpvKpVmSihaRCezLgSS7w4OIJpMHlcH26gSJlRkUNa+ovCTs+9L5hF27Rgodoh/7Q36b/q9Le+F + Un5hYR81ZT5lJh+s/Rq0no1J9wGfD8BFW69n+NHapi7Hwuet23L96df2ieP54ls2jpEt9ISG/vVg + VpUlcaX1tH+JBmyh5Qa4iV3S3Wbstoc6L/vlsAPQ4HGLDw3eBusYSjl7Ph10uRKHaSNKzt6hj/fL + yfBktpzxHK/qjGfgtlUWKFQtWbkQcd97ezv6u6fP5M7XW293nj6L0fPu2Wh/MjSw7m4/xqdqDrCa + t54iUwXv7UTSphpgB5bBAZyjRmV5cniI581Ieu0HNhFah4afqykeEQaedEqWiTdwP35jl8uA6uN+ + SX/6etdUbj2Yq7TVNaTEM56GExt/sPaaADKtbFTNVAPCXn9B4nXf0qavT8Zg2xskAfXKDcu5n738 + Gt/6swP/aRfbtjZ79vOTiam+P6lV38IDIvjKL4m4r22SHWmdzs6Q07UJEqGrWrS4COwXNIMoAi7s + M5/2cYQAEoosD+db40DsqnQN/6o1+pyrEVUeHXfG3POx+cLGXPq00fqCKY+XtObnnt+N9EKXRRb+ + lHOoon/LBUTtMp7m0YQ/5QKS6C8OvxqQ1K5w1IwilwyQLpswVSXKZpS1PcZWIcx+CMwXFKRyC6yw + 0cL9DxapysHoeNjf70x/Msx3ctqfQg0n6f9kNrnAmhx84c5MDPswO27cf1NCsSKSA69EEnBI0Cet + kyjw0Hq13KmZ+X3e4pnGVT2i2JBdk+eaREaohuHBYFrCsDCdGSneuIOyIvbXN5C/ZpNf+sQ/Htll + RTSFnTGdFlIS4N0+Ji0gdhz0mVz3gr/+/aWtiSH3mAYHg1nEbZg+9om8pPr76dB1YqMrrEVr9/D+ + 31JRKufrRNZd/+5GuHG4Hqx9uXav+vSaP22s3au3Af8kG4EKFgmG6PsXW21nAL1LMuT+XdOBe8Zo + jVoddY7vdjH+//zTv/169z/j+N9+NQeLbWrg8exuXItsLtJ77//zntGS74aSE/zYDjVEwDA09WD2 + Rt+tQcYQv+eSBmpSlL/pQ54hmO8ai+rz6eRgMCRVzc+JnTPznQrW2p6vUseH00nWPKE2S2qD52Lt + fmCqvkRqfVVZGJ+cm9xWD5XmPqysN+pDlOca9tNWz6g7UGMNtHVg7SL06/JuDTg7KotXNyw+N4KW + Z5fWyb4DphnEBTAwDdUsYPdsd85YTvrHry0nOrbut5DDnq3rreOT/eGgPGIT947ZrkQEuVXKJVIy + 4419v1VaatuClYCWoP80J2a0OvsvwV7vt1rvf6qtiLkZhW7f9CbXmsWh2QIwJSysnfrC+dUha30R + NxVWeGL8YmxcVujFNnTPT4PHHEKa2VzMo/n1wCPbAjnX/YpW5gtRY/MfFtv0Q1jRXDXEh9VwH8yv + Astu68vA/25StC5PbqvLQG/We+vl+hAB6y2Yb/4x/Okh/sHligf4Yexwv64dzkbmTgPtH3iwQ5C8 + y+9sQqS/e2+dH8W9z+V+Ltfe2xkK2NZ18LDHF6eNoRSXtTuHEB3ulvcQtqBW8mcqOW9VLeda6g0f + Dv/wcM2jYu0va38ePlz7cnh/bc2V+zkypgSYUqpvMCFUVGGVk8yGgf8vgx41OviyN/yyavcgMjHo + 8D4g5pC21xMOLHP35/UDQ0bv2SvZ/grz+pqZnrX1Gszra9+83Am/3U31N3vYLpczt1xxMi81lVeY + ycvO46pZ/OLyU7g8gWb6rj57pvYNT9zO8/RrnW4vzFwlmQXBV+YkYN4cthLocRkdzUZDQF2tC+/X + QZ9IMusPDo9mD1txiwhDb3bEv1gofDh3sPCAD7IGQ0jIR4Nej+9NEDwbBqB5CfIGYbYo+S2Apoyr + bhJPTSEa79BQrOXgV1Zsqjm8VtN/qMz7fRK712G2H68HhzCV9/8x/on64T98yLLOv+1GDFp+J7bu + B3fdXqxvxcDsRVPy57JFC9WcP434/KlxL3awF9fNecjCLuxQ7anZXJT7h5hKYWPVdedLbC2e8Ccv + n//wQ2s9GK3cW1PKvBe8dxvLn8itBy3rU0C//N5q3ZufCyTqy/NDF6iB91Jrkn16CM3Vqgxqy7K+ + MPG7vp82EEGV1ivOCZ0/29M+NdTvbXf2S1iVz4JtmOqDLdgjgu3282CTFDWjln01GB2a4QEt9829 + ifL0ELdBHhg1Yf3500Lv//3NYW+0d9aVw9P9n+PBzq5+szV4MtwffXv66pvhyat38WBv78U32y8P + B8/f6byrXo23NnvHrzZfTJ7vbiXfvfwl+e6HJ5rqTF79fTjubP6t2Pr5F/7e3TwcbG2+On71Q6+9 + rw7p++PDnfZjU2fX/d1a7P8ttUVwvT3pUt+dzRdx9+nkdFv1VO8sUTtnyWl31D3d+fnxm5128a43 + 6lIfR7P9b5J3342Pys7fk+nz3b9Oepsv3nw3yE+pltoed99tj4qzV2f5W/S5rUy5rZ91/ursybve + 5l9PO/L7YmuUnO6Pvh9sffPqeP+bN4vp2b56cvSjHMb9lwRne+vL55t/Hf6o/nbYf8rpQ/v9sCeH + v/S+OfR4aMIN+t5WXP/LradvTl+904fPzXi+bAWd4QxHIHZZFfivWm82aVfa8YRjpoe8u0u34Eg2 + HxgrcMfa9B6QKnF8P4gfBMP+wYx/cPP3g6I4ffPAts6powfBqPM2nMvGh7kiLfanrFbs3Jb6zTdT + M4H3lu3/JoT105LUiqDe+eLOHbbR3PmqNzg19ztBR4fE5EYTmpv+65LUxV/OXuPtoWnL8EDOf2h8 + N8yXk/GAptxqm6HVNjeMOW/D37o0ZY+n/X34jcVv4/sLJTZsn0OOh7k/6Ux7/8zSnApiYRCEj5xx + a0GxDWCitZGxLdm8uh/cgsOqw0bZxeGkCYPtvg3ZExLfcAf4YED1Vn4IR4PA/8Z5a1glOZqAU8kD + h17zhFnLJvuyH5pbnVWvNovteSFfeyXYOw9bBgNGUqpNKFxlOwMafNWz/0RFzf87Rlart1w6xLuu + o0HYnGObmG8EVkX4MjlXWVqbJ1U1OxImfR3e2TSqweEhn/NOO2/saNx/X+2fzGZ46WEeM0ed0f4J + cdDpkxnuuFro0ZHr1ZcI91GE6/srGLTWeyfmzqYZ1j9R15YyUISzyeHhsN7VJSAPcOoRDjv7sKnu + cJP8pf/2uMNhkK3LYH2EkDdPD/3iOz3kq4sGa3w/EpRrQCDjg/E2W1oovnArmOIx8xbJEK2ABIZx + aaQkKyS9UdFkergh4zje4Ls0lmKL/qgi5Jw4HfTfPJm8pV0bxIGUgdALQC94FnzFMv8jjDkAIF9t + mA+LxXC99dHjwG8Bfh444IDZ036fCfLwLCiPO13atezfiIUzDOBQWEZEE1D/XEAQiG5uRTgkhsgh + 1NHMxIGQR1Keys34HSXS2u/Y/QYFQo2FuQLeHtXTZoHCiaE+zNp2mBxOAvwTukEPz0KcyfFWnPtv + jjrM3XwKsAfrH+b280p0LC60zoJ1H2uibtFv1YFehjfkA4MFSC7eW7Z0a2kxNK177tm1j6XfDMo1 + F7qK82qh63xhnVNuQB8X4VxyorGL3fEiQLhivbsV//KoH8Atko9NGRmNa7mpr8O5Odn6dnfr6bMX + K7Bi1vrBYDhsBfj3YeuPQgjzO5zyrGCHTXq9pUFyX7x37AaJkkIfZVGq8j2dRpq3RyqiPFeEp0im + cpv+5ImOilid6iRKRErFldZ7Iotkkg2ViGJZBLKgUsmeaS/kEu92RFxEcZ4HUkQ6EUP6LNMilFEq + i26ookynoYiUkGES5VnGGbn9nUSFkEFMpdIkUJHCdx2rIIsKJe3vJJJ5RhNaUErQUIqM/lWR0Emg + o1wUe5rAVegpKdC6UkmYR1JqbjWkorKQ5jd9LgrqTlCbsaTqKs5cQshIFAQL9a0yRXlJogIRR0me + 8p8sQZEsS4dplKg0QEdZV0exKoBfpYKUBonvhRT2N1CtqUkqJRIGsKDh5FoQJJm2PzG+FEDRRMQ5 + oSqnYQiCTSJP5PqUsJGQyheJhGaAoBSBGYiKCk0wclGbKKgCBiFSKp2FNAqZuwTAjwmckMCiLggv + Cp1qzV3nAgWKIqEJVTTELK0tm8wsG0w5JahETpDFCZdIjkIskiK1ZUyiy3hXYDhYZylGnAqXkDLS + AqAIzAvQQT2ZryKslxHvRiG1owOVE6azo7CIVKr2ckKvOOLfXVoXtCJi4CHRhPKYsGR+08ymRcJL + J0kVITNOc/4o07AqIdN3O7RaCIm0oHgEkpZAok8JT1me0+ho0ad6TyY0ZxhdHmfIS7XmkVOS4Tmy + 5ezmMI1Q0wUWP+0OmkVddLNI0Mqk3USAAfm5ssvcJjAHktDG+KJeYloFhCRN24GAVbqwKeqa1lhi + 97OdJEnrQosjalAlQx49jTePRFYc5VGapkNUkrQT6SMhjXBLjdECRz8pDUKkmgommeoCVxlvBtqI + ARaOsL9TGqTkNS1SSdstp52Lbyqs8tVRyM2s4LtMnjYOl1jIAjvGl86c/Gi0iLlEk8y6mkmbz8fT + wagzPauRTc9ML8U1r8clmyXDhpvRH5FL0p5dYJOClp5Sc4DOixlWFlxQ2VayyUUWuXiI2cAq5+v/ + RiyyYo/EvIhIK3UkiYupPdrFhco2+eueUkTreCMHipZ6nFAhIdM9IhdJuk3bTadUc0/GkaAGQmTq + U9pHsR7SDihySf+qU4Fdkr/bUbRniFITE0216Aoi90S0iT5L0BKipTrV2FaF/UmEBOSashOi3uDJ + 9qeg8mBj1EERYl+DvzLn0PyvULTRwVmR2aWfWYqfAjQ/LkB9EuaO+E30l+h4TKBLon+EArBKDTg0 + NcY/CRrmKTQKYgwqI1aT5wCSZAWqFusu07M0JAQpMGCAk1Ld3HB+/kcp7kRBSCgS8wsjlKDPhAUi + YET2CCcgWVFG/JhgL4aMbqK7okugpSRMkPpCgMcolCttf9MgiLIDQlrt1HaMjCRPiP9lMrO/GQ0s + a0jCGEGhQyCbb12qUyBAEyfQRNAJBLscpF0OmFqksB5IbiJ4ApaIJIGV5okhh1JyH6n9bSQIDDqP + 0YtWzI8z7tbkZ+ATxAD1KfFtmR3xb6LKJNdQW9RBro8gk2li/hmPMMWi4WWg7G9IIxB3SS6C0EWU + nz/JsMomZsGSnR3Mu52c5BlgG8n8lJZKQiMkqU8npxCsFDrNBDJUynWzUxJhdH6EMtgVRhBIHHY4 + Qc0S+yE+QYwyT4iL0prUtMq14JmRJGoRP83sT8yEYF4UZRDbQvsZCKF6p4QGyK15suc3lusNiCW8 + E5dlvEswdVpl0v4mTIiEuyRJifYIrxKtIRoiA5hMtwvIQIFpmqSMIQ1SpZBHaf0dYYOQ5LFT0OoS + Odfuor04x5LFUqamcmTEkExoB2VD7DfIMgWNWOSYbSzhHMtJavMr4K0JmgtqgT2T8DLXWLJJktrf + 1DmVxbopcmx72j92neqUN5xOu7RhNG1okA4t0TAtYUc68LNalRlknUhmuf3N/J5xnxMMJJJgLxYa + GbSTQA7SbMj4xnTRvnN7W2W8vAkQ+ux+EsQZNwZKlheGsmAbSvsTgzE7M0sED1JZVKh0D9XaIo5p + 10rQlSKHiAR5LaOxEpRdzA4RyDhkcgTyIFL7G7NCmzEB8EF8Ssgn+sArMT0F9cvsktZ7GXXD+yiT + eyS2mgVNO5uWfWpXdGwWNO1/onDUIfQSTSuY8rogw8oQl0S5JYyftSVM5AUrmL9uchPVAhandlVJ + EB7qinZ3omgBmx2N1az9AsZvLGBtFzC1XRSpXcC0N4VZv0NoPgSlytWmxuLBAk6T3CxgA8G7nQQt + QONI8+0kY6zFmynJ+kNWvEBAElALEbPYFjOMuUiRrYx2luWn/O+mpr2XE/A0CFGcosl3O1kO9hAf + EaUSsaEjObCrZXrK35i8ySRlXBebXH6PVE9iAjHNL4nlQoPEE+UigZMJm8iAffqNhlSSvRslIF2g + 4boA982J7yoGPOZhBHbmKYvwAnrPq5ImtigU0lg2KXQW6lQDaakqSKXSkutoGtR2CjWGUFCwZOsX + k8lGhXyIRZfRBKWF2uaW0JB+B3FxcLwoazQbmw7nZVkIu5UAWkm6c1JuZYiqS7jG1Towf2Cidr+c + hXlRJlusGu5P3gaVYXl1Q0bS9Wb10FwlX5BaN6pGl+UsXD4IjIn1kp6o3Ni0X54MZ+VfXpvkww0E + muBnAvk9wXMGiNOqHoF2fDILcOfd/rwssnAszRVwzNGyIRlfu2h/S1i8qPUA/3DciamJK0byNy4D + om32ND2aDPnAwLjGO4Wgxd7CwDve8n3Ymhwc1O/x2+zO8WDGcT6pwHh5HDWsHAwnuDpxaOzZy0A9 + mu9+bg02tfc2dCqMP9xZKFwed8YWh7YOR+tuwMcB4vY5DONuF9VssmiZZhuUKItyb2A/Zzl/DIs6 + yeUc4bAZ4hWfvWZlEX+Onb0+A8biPuocjgcHZ3AGPwQuomAL4al7uJLTL3FfBC9sIBSsfQMk6FhU + rDJVXgzt4cLaX7DEl7Pp5Je+U8BMKrT4k04745vSq4fntDJbG6uEljj1+F8nnWnfWPsFkeB1GG/A + 8SBEy9U2hlq73cG0O+wHXZoykvxoSZ7ZH7T0EvpLbZgi5yB/2WJRZVWWC0s9zF4tT/ZHg5nf8s/x + SgqtpGkfc7OCppgzskvTdt58/GuZyvemk+Pe5M3YH6TdHKH3bTcbn2uweCAQsHPqQFlBNqoBfbWB + hbtqsFXqi5WcYPHs0rr47iN8Ww+WlsGYzTklTkGqTLbwLJMzcyFjse3yaDAKXKyJpgODChyESQs7 + w/50Vs57Hy32BMLpDvMdsdwnLhGYyuGYr7C2HsWrSOUSZlcZW7gwztofmwvNZeAmKxiaeyZvh8H+ + tN/5hf1bSiKItD2PcKUl4NAiNsbU+f00DRKdwkjE3nfVzCD2t/Uz4YP+4Zm5csaeBQud+bY6Syfe + zBXmcVfN7/5sjA0Q9EIOGHWVSzKjM8+erVvkzlnwd9y5x8u2Sye5vs8n/tT6gg0Wjs7CNwvtVWZG + XLs+6c68udGfZr+unXgvFzfeBaOz19zya266mZLVR9OE6Xlz7OoppoVptkvT9q1XazLuVtu6vnv8 + SSpQWxER4/Uw7vhyFgv0pdEHg76vcr/4tnM6ODRBf7wovrDrK8eHmuR+MnTZcK9wcOLqbjkLT8ZG + Hl2k2sPB4sDK8GDYfxsezjUSopHBDCIHA+2xYN0S3CJdpldNp8NNS7q1DIfr05wQL8trC0itYKrX + c+4wE5otE5PpYeuJg/ZC+/n+qpKLW4Da3fcwrVr6c0fxTVS6TnKb8ABkryDWDeIo4k/xDzeK4Jpy + Z0GyzvHbmugpkZqTPVEkEHLFfrYipsP7ufLlguG+qrNKXGxupe6x4WPeVs4aMo1gupFd2EZgreLD + JZiWcfw7FFEWpbCgRTCJoKzA+CIcRcGWB3sS7Cz4W9IP2MdwxAwL86nsRnkEG7kM2ExC/8rI2ghh + qOG/1Aubbahmzg3gfyrSbDEPo6wbUj8wziUhNwu7ccK29ohNcDBRhAY8Y6MhURTmGQzlNJRtRR1Q + F0qyCwIAhxFLsN2ZrXP8lzAwSgoaXnybEJFeBxHtQjJcheIOYRDdE92Q2wi5DFqlf5Ebh5pxoBkD + Ic7+VyIg/UQrYdvYSq+wEFQYcQeRdithGQMJY6BhHeykkn7Ep0JuZjEslznslnIvjZIj2tx7BApM + YbCh5VTCmCoFezdRHRjDYQvH2MzIMAQelzLj0jyfhFbAw+hCnkWfOfkRPDzCR4jaajN+NzJTksPc + 13UrHePHEGgidhP6Df8LESSbVBbAwd6pI9joEBpU2SqZrZKXYcQW8NCmuVoaZe9GChY/dSQxNElj + ercjY6yV0zQq2O5Os4A1gQnivwIbh6cLeDTTpkxtQk0XhyGSF5Xm/2X8FwuLDeKYcv5LX7kSQUDD + iVBZb2vad8AsBg8IaaUM8wiH3/lehFP3qDjnVLvpEJu/NcjpC9KUYygNrIg5UcisiIXxZiPJVxvD + wceRMoaDg2Xl5kL/syE/6UDCz28pYmwD1AvFi2FTqd+VaIERXFesUEWUXCBWoMiFYgVwfRWRwpS/ + vjiBMVtRon7YD7FCgRMwPczAhutEdFMVjoZmIFNH+M5EVHoiSpubK21SDlM/ycOvkaRRBkKKn/SR + iAy3kkTadGOaTe0x0r8cfcAlvKvTB671G5KGb7m/i0jDuKnU74o0YATXJQ0Jcc4LSEPCzPUC0gBc + X4U0mPLXJw0Y8wrSoJNIXFrjyFjOzG63xpHMaRxcPYV0DYkTgiaLewuy9o5InJQp1ZKUmc1RSCrh + pUwSfaxgFC9IRdsCjvckE23mKEgcI8OJLChnxijLQvxLst22LIAClp2GLP3Ch0fy4S0Wk/lLY/0X + Fa+mfWywa1DQwxNEsv8taegLC+qFZHS6ouDvipLaQVyXmOZENi4gpjlTlguIqUX6Veipr3J9kmoH + v4KqZkQB/nvZcRapKlpJWTlmsqqbySpxl9zJkOIC7b2SPAV87AFOY8VNGa+sN5JgZ3OiaQDpNwft + VUx7s23JQLHvzTAB/kBjU6avEn1LKiX2iBXE7I0EXyRGL08jO7QBI6Ehz1brTrZhroKRgn4CT9TE + JsnFPAfWTGB1/tAo/aFR+tkj1RoZiLWcUusEclqsYhZJDsScyywgiW+mH41RzFP6rzZOhpVrzty5 + xlcbRCpqRxb8rXYWYd8iGPuDBRw0Emnqj3uduSOEOv2ylZhfzDkMLbuo26LnEfnzbkm6o6ybINEr + qLP1/p475unUcTs/IVdEhI2BdTPocI19RGyYC9MfCRkDG1bxZrDhW/uI6LCBID8SPmoBN28IJ3Mt + fkS81MJgfizcTM+OaS1WoTBvCEFLzX5MLC109rFQdeNI+o3Q87ERg1i5N4QS09RHRAai1X4kNMxZ + sT4MDTdnelqBBlhZLo2GeiSJSmrxnheX8xdrEHZWN228SedFsSb9zD1kw4mP59KZneu1aHWztgX+ + EnESxlwmGIw5xm151DnuI1ZCJ3AIWOGa+RWedwoIMFK9zh62SNTOk1RXI9G1gZC2JLQinGAckZh3 + y2z0zCQRGu2vvh4n4FefqiDfFAn/vFyjTXJ51WiCq3aKb1kD4L34JhoVcVSINPvwRp3usOSvtGrl + ehdH9sNaaMy7pzUXN8TFU1VaIpPRoFupCTdCEBr3odlgNWq3AkBe1K1HzyxozcaxRR3qaqPuTMeD + 8WF5G0dtQbvEqBeWS+XIylqiCybXn1qX1StEJZpNO2MTSaxTK9YciChsLDwXnWiuyIeGKlrcF+eF + K2qt4C/nxyNaLH5BTKKFObL+1L/zsEIrWOFtDyy0Ot7KRcGFgo8RXqgJnI8bYmjZEFUFGVp2m71k + EINzQgutjCzUZDa/XRGEVi7yWxhFqHEp8QCuEE3ITPmVIgqt7vc3jCzE/X2OLvQ5utDn6EKfowvd + zuhCTKKa7+s1Hwwt+Qos637uwyqp9OpRh7iJG448tPJGyCoJ8hYGIDL/Nc3dFYMRuVm6ckii1RD8 + pnz2c5Ciz0GKPgcp+hyk6HOQos9Bij4HKfocpOijBymyYkeT5Lxsx2o2fi+atT4gblFT5d8uclHw + 28QuCm44elHw3yZ+UXDjEYyCq8UwCj5WFKMVutitjmN0Tmyg4DqxjILfIJrR+TD/JhGNgo8W0yi4 + kahGK+0kLvOTRTaqTlAb+cAni20UfEh0o/qx8HJ8o/qQ6+kGtroIx00GO+K2rxfwaBGsSwY9Mj1+ + cOCjOYzV/7voFJAr3mAgpIv7XIWEGwyMxO19Do50U8GR8N8FAZIY48u25IunfkXAJJPVvJ6/aCxQ + v4lQdze5jBR+cDIcGgbX8JrU5aTvWhPXlr/npe6riNTXEqSXRn1DAnTV7icQoW9KcL6UuHzDQvL1 + ROP6yvvthONV3i2XEYQ/nvi73NVHFXVvWsD9ELH2gmdFPpYIu0w6P0x0/QA62iy8XkNkbRJUq28u + Rp+nAaC3jyLj5BZxip9bxrL+tXoH9WDwtt+rHkHdnxA+R/xzal417XaG3buhiOM/BWGQxMdv7zU9 + NfogeEfQ9/pv7weheBDMP036nmacofnCbgbz//WbedU7kQbgYB5g54BpM62332gw7/1nS3kHSvuZ + 5KaOtRgtuAUuTqLzCGx6m7rmQlgvgHk/od1l+96Ya8JkhvhNQNVmct7TlNaMg/Vo3o9zxXplatEF + FeqQ1M8i08pTbHONbuOAZidcuJ+HqxHsYcd+Y4DCbbIVh9pHo3+uKPFBwt6E5OBLPmBXQ0BQQ9vi + Z3djvPxIvKeABbmIsv9e/Kd+s5yZS8WH0qdZnj2+LrfBDWL6R8H0rkPLMS7DbXLDa3LmNNm1+cz5 + fuHNG+cyGlarouB+92La5pS583aTc738VNup0dlkfjAf4qB8PU+TlR7KV/EyuaaPyTk7Zc6/pAFH + N/Y85GfXks+uJZ9dSz67lnx2LfnsWvLZteSza8lHdC25vNTcJC3P3TIcTsp+o2J6wQW9c28HztWa + v6n3kr/x1e1zZeKPLUJXA68rpNXXj6SBZgjGlUXi0tJzGxBdRgF9+WYSdKeTsuz3zM09wnFnFhhF + cxys/bA2r5CS9vmmcwZ1lAeNMjVbyXowmcIcNBqUKDmezAYHA2OPiZqOVq58RbC28Oq205q19FzV + NEX0LvqHBFSSyBLcrjdpTl58h7B+e7A2jHm7nT33CepvU9QWS8MZVy2U4ULBsBa4cPHznJHJzWm1 + R2stnRO8b2UdM9Ama5Ov2Lh7vXnT5M7DvpAJ2119l3933B9jBZmlVVlSm9C+Av3+8/LFjgaE1G91 + LIVhfOjUyPN0+suG8boSQboo4uIK4uQjJ1qg6iTqQ4iTjOfiKKZLcRSpxGrfpXl9/irXMX2Va4RR + NBEpCtIdBSTTJBFtl0xJyISMK8BIBaLxZanAjS3wXfqeQ/EtMuKzCaRdkhNIqNFpWxClgLZakJCS + 5SQ0QTXPNC534RKbT0Jh1bLt0xKXL0kM4Jt6LDDoDKK30EZ6NmkqbxVxnx+TDCG4vsQ1RdLbcZWM + RN84K/a4doxqMYTmgMODQOoXCCdIKaibvg9OMkiJyKtcXGrjC3USNwt9GvqDytouXUDoEsjPcUcS + wfizTKU8Ih0jFmEc62Qb+CsSyHIEUyzMgAqWVFKJG4cCcnGKO580Hz5Jql2Rqbzt0kBXytBB4ETw + SZlkUPhxjKWVS/N0aDYauPyMFRzcsSP4eDRxxncnY5lC4JY6zlJGAvUQ4HJkJhPBw5AFgi6iiwT3 + RBmYnOVut2oELtWlwfyaelU3/sD+c/6VuMroI3AvlDqKpBRKqE3BGozYI2E6Jn19E7qIjEnDTUhR + lPkmKYxKkQTs86FUFWLPN2BbvC5AWUq4ilMPkOm4KDbzxHSEyShECiE6IS14j2TphKZxMyeM5XhJ + QuRZnPhsA2fqq3s4bUfXhTOFepsAcZlpL8mxKBjeDB3aAgRfnudptp3yPWYVuPwMt33jdDuFJspr + t1DU0jZ9VwkjwDZMFQVpcduuR774XMg9V+C6I9CKdC3eVCnpa4IBF3pTx7hsWTW/qWlREuraGuQn + rkYckCIrcdcYkYhI7/FpbKtct12SqJqSRRYkMdEFXB0mIkcqHgwC0HVJn4mTdDthg4tBjwQ6oQ2r + fBthuxNSEGErlOmmBXrPAv3KjwL0RCsJaAWRhjbaV7lw3wMNilHkoLkiTpRPW2BdUoHEEZ1y1bGH + JdFM26xLezhs8+7vdedCwZyBK+B8KT7bVJonYVsS8goiKfG2jIHGxJcg3U8RLduWsHZkTKwTQvOm + Ig06y/PthRZf7SBgbMZEBogtuOlcJ1CeE1JscTVfFapw+Zu2/HVHhGvgMGqmuNQLzTeBqa1A8ChJ + 4yBlM6OZKHJwOZlpgbjivhAxjjwmomMr+6LUqMxoKyw0fhUY568vXObCL3+sBGh39Hey7wW1cl62 + nMtqkI19o/Xwsqs9b738uD8Z98omIbLe4ULQucaToA9zkbNy5qMnAOdCZX0etYuh06+PjIOTW4WM + r08+JTKOp30Dye1ByPNpPzRi8yfDCjuG2INUGg3hoNwgNe71z5Nx/xYtHYOlYIcB/MTIuoXoeTp5 + E/wVIAXbg9P+J8NPzav5tmDmx8nJNHg+mc4OJsPB5IMxUw9wvqrUZ/vQR7QPLbhJXd1KtNscurzu + RnW54Oa/qY3IwHRDJqKimLMQZcsPpRaI6Hqugcig8Sr2IVfjmuYhfi9RkVrURigcrQv2CFAIREVa + rE75ZD8htUjDApIXWm2TlpTBQCBijqDaJh1RZfAGiKMUIbRIm4xxrgbLUlbIKg0Juu1TCh4YMJSk + RYyDV5wcC34vMWEoTLpNulmaw9Bk83EuLFnnSPE3xjm+Vmwh0oXK9zjYVaba0FhzfE+owThj7UmR + KihSnGJlVT8m3c6jOBHwebD5iFYUG5NSzIfELs2nb0nbp2OclEGVVULGSYAwRILDNBUq0Qj0RSqR + LLZ1pLMigRKYF0phXNQM6xFa4xgd4BcxDENFkevCp0kbIzUsbbu0wlEYn1CmKuVjSeo9gS4lsrhK + t1N4FPChncnPI6VTDqmViZjGCUNaBuUUx9QxjC4xAZQSHhQ0bVKgZR4XAFdmNLyYLQewY2GUiUu3 + cc6cSuHz2c0gkUXgF9aVNKRKh8PpKAcek2h3UxJeobvRYkxzraGDJin8EWi6kpzUthh2s8Lnoz5N + z56rb9u7HjA65knltZGkNHfw6NC5SwcKk2EWpZQy9WmshTxL2y6tacNQLlaY9BhzjTuMas0rpsqn + FQSXJZgI4bDj0q5xThfKd+7qO+AWgH/lRwPXiTjO2iqDl5R2acBHOwUrSstcJT7tR2PTMFXGtCNR + n221tEN1Vg3IptuwV9BWqfI5spow9Yk+uLQfEKWFVL5/rp/lHr4F+K83pSmIV+rXdRuxAuNqGacw + CXH4N5nBRplqInKaX+yJhUrwNk6exlQNDi2Z9LsyxTl37mELqBsRV+l2AoNaXvj8JAHZCPisP1cu + 6TBhk0AUgkC6ysZUnfrGbbrNnQtV5VvgQCtp57sx4GXJRLBZVapUtv1YEVmOmreocOvHYcotfgIq + V1m1+BOJ9ci9qIzWtUv7Qdg0vKNASF19j+r5mbjedGYKHj+eWmS0RDi2Ywqql22a7L0C4RvTbDsj + Yp3wbRqTnRNGcpVvZwnsoTAL6UJn23gZi5qpWi3gaUfVTWcw0xGeHYm5HuB5DiaaeS7s0pYX5wV8 + BjPPiwua4iQpPC8uVJQnhlsxLy5oTmWNFxdgJ0J7XuzTzIt9yvLigpZ1ZgIuMq+F85XKPW9uw76e + yIpXE3RJkQrPjNn5LJVLvJjKyUxWvJjaocVf8WLfj+XFRUorNUmqfExInHhe7NOWF/u048UZMTHi + tY4X0zhjwZKI4cWFRLhE5XlxwXZHz4oLxMQ0HkjMil3asWKXdpu+gLMkCKdltQV8Y7RPA82xZkHK + 5lNHistbVpzR/hRiiRWjXGZ4IPNimp4Ch0Fu56CfmKbL0TCCS1e5biVZfryw0P5F7JbnXAX5VDq0 + BcOK+sHXsH5Mb5Oh6tZg6DZZp/y9qduDni0AFrQnY+jgJwDtk+Go4fGEW4Ik93ZC4F5J+WQ4sqSI + KOWg178NBwgORU8dRJ/NeL93M97iA3xXt+NtrXh0b+4G1+Xe5ftNLXkWqJsy5eUXmfLyC015FpNX + seX5KqsDStU0hwwx1yHQZaS+pW2XzhAZnqTCHDoDqQp5pGRMqgSk1gyaQ5yqQkEGzjK2A+UJqW3t + Atohi9AK5rmCb6uwYpFDfPRp3OxRXNykFcmcKocsmuYFqwaxIi2wYE905dKQTUUGc5nNB3TSOHop + yNg5WxELFulJb4RnV0EKS9vcZxHGNwNaJe6fwNSS8rCqfkzayNpZLR+qQMo6shmuS1O9AiqQTReU + nUPBonqxhrFDK3iS0bigIsCXg1TBdLtgxzbjP5dDVYFTTwY3fglVGxoRbDywkWqd+jRuBlD9tksD + awlpKFQ91RxrPxe4LlLA+17lLt1G97iP4PMRIR02F4IiyxTATeAfpyNNmg500awoMgk0KFgGSB+C + da5g3xLoBjCWpdyNVIlLAyyl2BXL5Lu1RJOtYJxdWGvXdrAihVrAJsuGhpTVc1ptmzmMukmGSSel + VW5mGhfLsj3WcvPNPAb0uEMidVr4XJiFkqyqbBrdtJ1c27kKmnGSe9Sk8ElLPKqCNMG1uYzHkNKM + p6kbEymHcY63GlKZUD1aiGxuNRNNzeSwJeAaYob7YdRNkSmXbicFVFafncCph1VQ0l9pGbs0bm+o + RLddmh0hcaPCVIfzYQGrrW3dpjGIPElllW+AYy9LkVRjgDVXs2UojvO2HyosNjEbxRgVvLfgQ2pR + ZdPthF8wUD4fr22IQpu3LGApsmk/CJvGCxq4zufqe1TPT8W1nc1ydGPAxmrTsPrArdAuGJvvVuN1 + u5EZblXiBpmK0azCyYVi8ypROuW+w3yp4DCq8RhGnvl0HmUKVjyXFmxFYPOqqQ/PWVy6Mu269Kbt + d8+2/2qHCsSw/GOyaW3AXss0xabRID98B+9OeDa5tAPApXEtS6G8rQ8vL+4+NucKlNpUsLjIfM+2 + vWlzr+25xVd+caqTFSlcQfldEpFu47EF3Am1SwFzFeey2IRdi9gXXC+1VOm2udqaOfK/5xratC1f + e3LBO2Pp11DsV4/Nuc7q+R3bVW6j2cDJpbfUbnCLHFw8pm6RCeG2OQF5HN0SJyCDn/LPndHxg+PX + SRzfHgztMlDPAwLqNuBo3Cl7nf96LW4Tir5lmD45dlYZBz4NUv4+mQ5xwen4s+Hp9254IjZHa2Ew + G3yQ8aldb2W1Aap7TrFPaoSqAXZT1w6zi5zKRHaxV1kNr1cxR81Vu4xJSqS4FQefIl3ItGi7NO7G + KXiApAVOKHMonwi4kOE9RSgeuZZJsk1aRoRnvnHBBs4VIiM9VLINSnCABkpnUFtxus/Po9s0KTJS + 5G2fxjUt2IgyzSYvSuesLGcIn+PTbfQf61o+gadyUz/F1T4CvygSY/NKNMwTGakhbZTDCSuujHAY + nCyOcI0KSgmXd/2YNMNV6Hp+Troa26QS2AZ8mpT5TKi2SxdwsMBbkFmGc3TcVhGa6yM4TYJ3HDUp + OdvoT6Xm/Bp3+zLc6Ka+COkJA6c4oEOSqLjwaZojhbN2lwbONM7oM8HXIxHchW9s0qTABGTTbe5c + Z7X8NGLnIUlTlzCsiNUBXx9oRdDbFKYS4TDMUbXOgdsMtqSMPpDWlMBUhX747pn5wIBJBF/yJexq + gntKAU/A+dV2bZ0zwXud6CfnfqDJCZHKTQFHGAQpUXgBEW/PwxqT72HyacFyPu2+PYTtUEr6fIVA + OUr5+q7hTdvTtQHVKQPq4dOwpdbbNwXcAK5/fSqxc+Xa5acppR1YSmkZGXuJiEkFx+0tP2zKRfUs + rYFl2rs2OIg6mKXVsBEDIte19k2BDx12gctgCRsn8ywpzG3UDDemcw4+Zr/zlV3qH/5MeZb4ZE67 + hohJ26Vxj1qxQd1WT2BIxkVpZR54NelN2+2ebf7VDgrkmXGQzSVt0Djl+DM2zQ2mKbv0pDABu7QH + wKZhvJP87GZq49cQzYQN0LZv05sI/pNhOk37mzb/umjM2eeHLSXUc9bO4XlkiCPSQYZHOOPCPDWr + fRLv2UqRtl2aPZJgQEf1rCILgWvf0wnYrnWNTBTSzCNayKlDl3Y9IJ0j8pYBwFZ34C2A/8qPB/fS + kyJvw1dMisSlgzxmUz/8auAO5tOuO5cWzOtooJKva2I6M6pRdWjSOMbJEZDO5xdwYkq5fiqET/v2 + kc7zqn/iWRVw88Bf2yZOfZj3UWNVGM/BPBUJTO2YrD2H++0ULxUn7PhKW7HYTvgt1YrK+5qwVyfK + xIWiFn1F3JYXidpe6PEDTL8qE8US4EnMcFWAa7hjZ9IDrohdEsqWAQcpTBLpAXcVHeALPV7b3qiI + qaWJ30Wi4Kuyfpni2n9i3nTOzGkXJ92icGm3i1A9ZR9Lu0lc+34XIfgfBwh0JYiqJ4jE4LaRS7su + lLHMOghcfQfgwgBe+RG5fSRpCnBz2y1VwQeTmV/HLu2HZNNuH6F+rFK/T3yHdh9J3MaXosrHG9Nx + 4feRS7v2Oc0xDUz/XJ/98g18C/Bf+wgsUgXORjziC8R3qMk9fNEgYY/7HFtEmRGzS36akHy4p3EE + CHGX5CMVCyu5BYh1mXNcPANwhgeoWVAzCIeYlGVZNaAojVO8CI0D1Uz4tEeITXMoD/Z+NPWBUDw6 + 7dq36Tb6h3eiy3fg8TlGLKphwIcad/7Zhzpru/HiNWcc2Fp8eH5hEeZ2gkLgPvvkt3nBmR+Uxgmd + Bx2hGBOZcghKU9pjdxH//yL2+nH/TbmB4PKT6ewiZf03NQbVYXn0LUH5ycxjNUgIU53hbDDqmzhV + twpHNaX805oT6/g6pDHjcfFu/3Yh6xtq/lYgaDIY3kb8fDcY3gr0dCd4/uE2YqjNkH02UP/uDdTT + s+PZpHsynfbH3Q+0Ui81dY6p+qKyn9ZevQDdDRmtZSovMFpL2CEuMlovAHcly/VS3cuYr0lMjVJZ + cAzrQpNOZNMpfANJcEYs31Ry5De4M8lEc2xf6IyZyrZlgkhw8KjUIkbtjAPMFSSdsuKQ5AiTZC4S + w6Dq0gKWVFJYXJrE2TQhOZ7qs5WbQ6eTIpLgdppwaSoPzaaej1i5XF2lSMaRjbmUi5wdKhNZFFQN + H9ipS2YSYChcB2Qnrwx6kevGpBmsNEur/BQho2G7Zh8vl0SQaU1anUnCnVBIagvemoiVVxRxrHlI + SQqtXggSvrfRF2R9eE8q1E44YD17T3KUO0Tmkhmbsgp4Rdo07jdSA22XlrCoJQnPiEw5+HUGWzfa + z4rMpXlKklzW8nO2xSHkoYLSisDMiq/aiiyGN6UinRc4KOD/CO+jVMGPEg3h5CJmi6PFWoaQ4fZD + G7OBM45aCbOYCFE5fO4WFtv1DQVJhLtl1Bj3w4aKPN+UWqP5PV4dcbopcUtXJQgJp5WitIZja76X + 4oREZj5f8p1e5eu7djdtR9eGUxKqY466yOBJOGjpWvMm34J/7V7gJpwVtWkQ8I6TtWkQhAElGF+K + tjF9KGx4eK3gSUZ7WuSxbKNgyvYJu7Y4kiUHe1fwjAWgCLRv0lRc4jJhLZ/vOMJjNub3JWwaWyGT + tHRtWuBKJZasrY/5wjGMa9+meSA446nyDXgwM0tVjQJHDmnCuz3J87YbroA/MEZj8cG7GVYRizCb + ZrCYCLn8OOflxVEsYTeyaT8Mm1a4QImw87Z+Dd/zM3LtmY1j2CmgtydayASegYIW6bbArUcEqrcd + YAWlfBxT8CkFpWgCxTYe+tB2AVJ6zzW0aVu+9vEEDrIQ/n5hA4qcNlia+g3Ih3iZ9BsQ+TJL/AZ0 + +W4Duvp+h9iOrn/MlSPYPg4VVSwNmuIcQfFgKxS1flI+Tm2LVEfwT3UZgUhhzeX48GywM0lc1ifq + 3LZJvCECZilSmnc4uCP8KDtOwhMa5h24/ibbIs0ZbQDDHLYhYIOk78S2cOeWwKS/mxbsPQv2Kz8O + Wse0sQy4GY5p0IFS7nsA8FUG3pLECLfg0hW8Jo33TAT4oa0PT3v2ljXturQHxLbv/l7/ME+wMLE0 + HSqHma82HTjlwzGQzhAAojYduuAI/n4+bNoP0Kb9jFADBW4NuxnR1KVMqhkhhlQYQcDMiMYjOvm2 + 0IaT+RkxkNdmxA7FzwhYSwaIOc5/NSM6Ze7rZ8SmK4BN2s+Ire9nxLRbzYgF5KZmRMb8ckuB5yWK + +saTeIMEcT8TwNW2ab7lzsf3UiP6LrsRSE7j2nrq0lSe9rw50TH5cJrOVFXfpm37tY6V5LWAWMsp + /A5M2njxs2QY8/syMIUqlhxTfpTGpqljBIDIa/lmhKZ+FiyM+PoHtAjfWfFcYSy8FQ9AEGPNEU8s + z+WIzABkgeeiIHEyz3NxllrknslyNGgtKqYrYuNs4PPjhF9kcUzXpR23cmnHdF19x1Rd+47pcv9Z + Pd/AZ5muH8YC03XjdUzXIcQxVYswx3NxQCt0LTv264t5rktXozBpx3Nd/Rq+5ybk2gfiCKkjhYcz + T+H4kXk4waCgByGeiCZK6dIOTpcG4SfOgOq4z+DBdO37hYO7W0WNlSPOgxmollBJbNK1j6Q00Sa4 + f1fdwbcA/ys/ILd8cLeMRG+/fHJsH0TQIc5VZIlP+wHZNJaPRD7Vz+3yyOKi6tCkcXkr1UVe5Se4 + K8fKEtQdn/YDQsSbOPH9o35SiZwL4F/7MBfhexLpGQnCVWOerOSRIlhKzg4BEGTifDPDi2CJ9vlo + QOYVmbINXheeBFEsEj4vIm2aey0SucnqkUiqbhLwjULjplRqIk5ZRghGzS+4kfompUsCtznpbjaJ + 64YpAkejNuTs2LzLYxq1yU0Ly55Jvtph2Yg5UJKnqc/GUWGaMEiUjXtPJIlCreXP6IPEGYT2ykmk + T3zageTSfGdG8r0nrm+7CeZ7vXZc5xzsHsRPkaqtcJ8yqaS5TQRpJjFgm/Q+xA4PGKs6Y0EkJflV + I2S23rOft1lYYBXY1odUQMtie66bD7j3U/BtzUVpCIeMaV1LJFYFbYufN6utAclvfCkvDLm0ky1c + 2glDeNBOKi8LEQdXOUcEN7KQLCIjI1lZSEK2zXC7KBNKVqKQAbsmCtlxOFEI8CrYdqj9gkMGGVFI + 8jVOLwnZpAfWJJ0c5Co7Ocg2WslBFoobkoOg5sZZUiPLCByT5zX+QlxSyqzOzzFNy+ycd1Hq2TlW + ikgqds5nscrTX5xQ5+YyrNWhI5pKXbFzl/bKp007du7qO3bt2nfsHAI8hwp07NzC59m5GcUiN7ej + ddzcocOxGYcvxyZhklKF9vmxZ+Sxhzz2zNuV9shdRP+/yAF1Zavd2J91w5Oy9+nPgRZt94+eDGbd + yWD86Y7MKiT1Z0e3FEnP8JBY/2T06bB0iaOeT4yjxfOJmznJ/3zI+KkPGW/kePFyB4u39Ujxpg8T + RXLhDZjkEjdgrneMeMUDRDaZ4fhI5ikevzLJFO7V0Et1wW83wcFXwFMxEVHKQQoVCeDbAodfUFvx + WJXO2rhOAJG1gHYGNZfUglgoK30lVVog+KJq+zQerYU4k2g+/IJfN4s3CV7UlC7dRvcyq+UDPMlB + IIoEjuoEvTTnkzn8+yEP6dTYFfmFaHj1Q3pKcOQn7cmOqPoxaYYLsUOqfHiqc4BnpZMqKXA0SOKW + TcMt25xTQgeAH2gcwyMYaCGtCWeIVF1v41JFknMEFh3rhIel+Mwww7PDsGrinAQiNd9PsWkNHTaj + ObJpfk0YQWwTPC0NmVQVuI6C0eDlXpvmWYlTVcuHssYPYqf4DnAzvDGEMDccZEJlCg97oRze18Jp + gMQ5IKJfKBb8lOJLXOiJXRXNBwZNc5AWV8KsJ5rvGNE651fbtVUvPKqOZ35waoLrLBw5GZdJSJfi + J6+SIsOdj4RVsZwUoRiXWzSkZVx+SQpSEH0+WxwLV9s1uml7ub5tNjU3syxsEldyklrzJt/Cfn1D + Jm4x1SaAgxCp2gQgyINg04kixdE+d8dhgmkNsGsuLcoi52fZSH+tVpXAg8Os+dCi0MaQyAfRnOb3 + 6/BMns/HsVpu34uWiU/DAEabv+3S0HwyrHJbH7OVFMK3b9M8ktxcOrD5Bj6+pFIU1TgQEkQZ3SdL + zDg4AjFtnwJ3UixGjJFGBRZjNsmPCzIJctlxYp6pRwhgNvGYdDUMk8Z7UtC2bfUavucm5NqGzAyx + v3F0SEp9odi6o4XazhFkNxO+fTY7JLHY5CBTMc4OEe4o3+b3sc3yIxK25xraNA1fW9GGZYrNnfM7 + L4+J4pt7Z7z1MhwvKOG3HuWTuiv91nP5duu56n5z2H6ubZ/DRSVzPEHIMgYimbCtTNsjfNMPEW4i + bWxxy9Nc+3Fx7CPNIfhFgUciXTohvMeSQyxxGpYo0uAR9yeDTgxNG/c5U6KDYIuJ1HIb54rSWHn5 + mDcFYSH2CbqLBwxxaVLrdNOCvWfBfuXHgWenidEA3CwRWRutC63dd9gPYcWAcUaSau7THlqbVgQk + nlp09XHnE+TatuvSHhDbvvv7AWY9ASPG4nQofiO7Nu2aHQwKxJOPM3P6YqZD097XfJPBTIdLuwG6 + tJsOaqAAi3PToXEzgI92zYzozIZkszOiE9AWua0KfgLBz4iFvJoRNxQ3IxoHkFnSRgegB25GtGK/ + Gz8jLu0Btmk3I66+mxHbrp8RB8gNzYgxUiKQXqyL2gRIxEaKebwElm7bNJ/7scEaxwDGoSvXkm+Y + gIm4NBs5LZE2+VlEvKFW36Zt+7WOC7MUCsRxgpuUSXPsPQSfk7hA6SJIwYcFsZxchCm8WUoVi8Kn + AztAV31+vDdgZjTctmZmtNS/MjM6buvMjPPMtjIzMrOtmRmZmdbMjMxsa2ZGzq+ZGZnZurTjUnUz + I5ht3cwIZlo3M4LZ1s2MzGxrZkbDbL2dcY7X1uyM4LV1OyOYad3OCGZbtzMiP/ZsNvagx4611syM + BrmL6P8XMTPO34O5RSaimnHo096CqUyMt+wSTN18dsOGsw9E1lnYnYyBiP6nj3vdgKYzBPgy4H1q + TLHl+mR6OyzXdZv19y82vt99ehvQc7h/fOvQ882T558UPSau1wkhYjIcdqYhp28Vhr7fDZ4ybAGH + 1Ptsyv+9m/L7s4MPMeI/e/n1ueZ70/xtMtwDohsy2afxnMU+XbLYpySrpucb7IHAq5jqTflrPoKo + 4STPkYRUnMik7dJplAuETU44RBI0iQIvryWSFUZY7pNEiO1Ek6ZsLv7oPG3DDh+zAF+kmfExynJz + 7wdePi4p+Imqtksi+lGCx7GMzqCg1asAZwIiUS6Nx7RiOJj4fLixm1cJi4xAQizjNDZGe2uzz2Kq + RqqnMlHUFTzOYOSGlzW/iocRuW5MGkAV0jx+aPJh8odDJWEIph2fxm0WVbRdGmEmChjfc7YtQA/h + 2Ct48wsPN2UIWZwm29SfQtCpFD5bgt8Ii3EpJkEgLho+bPiajfZpAsuESfL7cUCCSXK8qCzj6SgE + G1NjhOFKOCySdml+sEymRZWf8XUj1M/ygt2vNAdmJnUagYsETPsFQmwjEj0s9kWGlyMTXCUw7omk + FeOaEkIAidx/AGAiM56YpoRbSAInK6RtLSy0a9uAEJJLm5hCkg2SKfcHpyxqn02BNE/GKERrEhaO + NGOfLZUUcKbPlJQ+1zazaZu9vks7rm2xoclAIzO2/BhoCglvKJ3GielPpHBcynHHyOWjAYK6Go5t + 8boAxR4UP0Rap9SDCXYkY43ozTS1GrbVPEaoedovWsG2inD6uc93CHX1PYjxv6LiTLzik0tfzEE/ + ra5MIPDLYrdA7TPYICb4qd8TA04GpG2WM4J6hujXnfFtMLBUCNoqy5ObiAt+y0X0+a37AaLst1x9 + tSg7bsr/pKIsILohURZn8nPCrFwSZlGE5K/zxVlDpy4vzpry1xBnLVKAghAf6typZYTdJIJvseyS + EB7x45IkGfFxFYzFQ5IMo7RLgkyEO7EZfaV/+X9UPuQKSUhyBP6W9CMLKa3oD4lNp7IbkVAMV/yA + /ZcDtsLzW9AyRE/4S70EMaWoZs4N4H8q0kP6VoRR1g2pHxWiH242wf+oRkQCdQiwZCRCA555b5pk + ZtwRwVBOQ8nVU2pUUg1qN8EfQBSHKCJCDlbKD0wH8akgsUSdUnP5UZhEfKh6RFIHHvg9CvGDcqjE + XnwUkvD6bocEW+qF5KntGLVRj6Cm4nJbwAK/F0kSIqigKoCyIfWMk7SEB4l/SaqFezmh4AjjHSqg + FAZ6GQIiLCbzl8Yquci7Wy5SXEQIOX+ehGGrdnqIsTKhbUrbc9Yhsj9daHqpRrc7ORnPwjfTDoeL + GQ3OpZP1KkxtG4jkOcVXguUr/yEMA+zYg8G0nAWEprI7Hez3g0EZ9Abl8bBz1u8Fk/HwLDiYTIPR + ZH8w7AdhuKK1jgNk1Cln4ISGmM3xxKqTcH82JgSATvWDXrg/nHSJhhuKD7pWEmEzz3OU0f5JSQ2U + 9E9JpacRceWN0ZnNbQVMlYgwnAV/78y6R8bazwugL/vhuDPqgyTbfp/MnHHGv2wRzs6O+wCb2wtt + L+HoLHyz0N4N8YfR2Wtu+TU3vUpyqY+nGePLm2XVdln+uPDJJMGY+l0sas+cJt1BZxiYPyFPFuFn + Gb0mH4y4nBtQtSpqRZi31dMHnW5/fzJZWgDgbC6PZ31n+sus3DIT1Aqs/ayqbVfC1/5D3czWHg66 + vwSzCXG/cjALTkpa2kFVlEd0MBkOJ28YKFjp+Nts2un+EvZPsVKWelzAQw2UDq0GYriv94cdSCdT + 1BtPJsd92pDBeGJ6WiQYTWKBa9OkruuFCmHNCwFIzMsAzEnjeAGexWVk+L/DWbBFAK0QCipxYNif + zWjEX0dN4sBirboIUEMyvHgIwig7Bdc7CotId8FxiSUmxLCzkF+JZgZNPBXMEGE/+S/xQ3A5fSRI + LNjW/IiXhqWAOGMcb8KgtMef4j1FSvMRf4BDQTcmhml5NZhaig6yCHFuWNog+UCACwYqB2/uMjuG + kSyJTDDcKFvB+BzLW9i/l9sqszcDxufiVrHfDW2kTTJY2CQ23++Rly594RbxJa+yQ2ZzlaoNUoFx + Y/vDNvkxtwetFARyXpSRm7eHxVcwuGh3PA7K2dlw8I6Y7P6AZGaqdxR0xgFwQNz2ZHa0ToPr92eD + 8eGVd081AbBsEfwFScUsvmla0WkEfwwSTuOIHbJTGmSURvDNhXQtWVpOIHrCRhpCBsZb9rSjUv6s + I3YLoY0XGW9JFooTSvLlwlCwzE07lEomRhaMA+ouZUc4Faa8s5TZWRBxZczlEJCFMY1WOLY2Lp3J + ruBdpSLsuCSI2JkP8wL5G2GQqKOcgKB9HNLocqYBGlAUAJWFaEkEgfZomAJIDByD1UZ5iPAcXME6 + QY4dTUVZjNUQ4DXwFOYs4yu0mUOSpoRgtQHZOaRyFCNJPFJdyVpGGvCjAxgvYtMC96wgAHvAACMP + GA0VhGzMCf8tAAOaprIZw5fzPLVxCz+AkJ4z0gMmRIiP0zUDMC7tLH0TAUPkEBXaHljFkSHPXMZU + k0gYWk0CIpM0RyXQxy5SBEhFywqjCuEqMK51p9CfclZH4C4UmgWhGcuI6J2G7B3EwIS8xHKj6AjG + nvgYBBH/9HuD8RJFxH53mUwWjTHnbGNBnNzwNNKV9kSyPRmPSR4yO9NQw20uszVeIJznFbwK3axA + WCCcNdhujHK6Nj826SySaPG0rJl0OpxdTDu9oaFFKLkycaxhGaGZmJqRZt2NWY3F6yOOpGVW94YY + YEiQBKXB3yMiNZBCiHaCnqFaiC1AWW00QcSOd5FmKQHtl4Jb4YAQYd4mrAQ4hEuM6xuRui63xpsN + GRlr05IJMps84OIXWLDejYwtIT5F7PlNopl7JJCII0m08h1TfAS+O1UZ4iOkeymPLzR7mAkWaBLT + cMFmjBSkOlScKfmvNKQioCKpoQQsUQlDTAJD3YEq0Bc8DxoydAJ/SIo6Vemmov66kKCIQoFeguIS + qeTBonNQOgF6jDJEKIehoaybSXpKQh6j2BpNLJSBMbak+MqzIhkUNsAExtZiQJNMCjNmeWxL4Vhm + oWBjBvvsAu2KGRFeb4n41jtOtuh/kBsRGp7/MEY/AuU6IxZ/Qup2E+GyeUy3TkoiUgtEy9MsW9CT + rF2vxZMYNzmZBj9OTl6il+5Rh2jU8FyBr7H8VciXh2aBelVQ3hjxsk1+bNqFJ54uRbsc3i4h9sGy + Elg3EmJcM6Jl5VHnuB9MDoJOMOsP+5gOyqSZ7PfHVyZv1Szg8kiBvUv7j6ibDCLct4JYlfP6d/QG + YpP9JEOmM0nIdr80ZG5u9zS4OZ67ojJmI0JtZKIRYENC+4IoAHmnYJphyCXRQgh47PkcKhYZI72L + BxCCjN3B0yCLu0yDIiNUaNRmkUWyzATJS1uyhByW5zQoWWL0PiZaVIhB1aGhSSQ+gTgzddGQSFg0 + ZOHKEm3/hTAjQQBNJdMQDJ5M7xRaZwku4xEaiZZpImNAs+RmcADqj5FBnGVDcZDQUiplEYHMqMyY + fUlETFmUBWWziAoYfUzeNdMwbjBiv2+gmZVbfiLDTEkEd4PUzBIxA4kuaTDdmPHH4Gq2UqttzZox + oaDLuGKqqENmcSmT3shMJtvKrfosThWRTzay68hIsjaDQUcVY2DXxrSOBs0CiNKhBCMETYUhPuSC + qaXiLAh/DHJK6VnncNoZEWmExW6ZqPoSTFYXxcC/HA0f9seesPrCnrRuVV8uVJprZa9CPwcL1SoK + Wgfnxmiob/RjU9HLmpY82q6kPXcJQ9POlclkHdlWSU7iLjTlIjTyGysxoFxYwtjb+MoETYKigPZB + x2Q6iaeLWHjJjXoJ5Q5V6Q8ocBya9gwlBKkqQKhEyiRLgg6Jtoo4aBcOrbKC1dIsZrUcsSlBSrgl + grLEpqVvto8utVQwmeaHldBeWygmEwh5GkE3JAoEykxECdIXoiNZ2tQ1jXJzIjAgB0XAKj/aDMzI + Ah4vhhrwmA0WAqZUubEiZBZb73YSUnv5mlKXNUSjzrLZASQdQwL6jBpsKA6TXG30aRAXyG9Ge7ey + d8ZKpWAukjG5VSxs5yzlOQG0MOqyEZVDo9GHrLsyC6PuTlXMRC1lkTpjyx6PkscNqLhYyR3ymRya + 6lpVmHHHNbnfwMLDjFQylc6MJh4x6mlcRWBHFPAoURtaN8bNfaI9BwXfBhRdRhkLtfwOIg1LllCy + IzyRmOGEUZilqdhCYeRaBg5HhjxWyYJ5FhpYUsabZPaUMh4pj2VlI+fnjHEdGYNEbgw8LPZnRlGw + uRIzeMocOAlhsSiY26ShtYWYAqFVLphn40CUjR2m6dBI6qHtzx5tAlxrqGHQQsOGUuZPkrk8DyXk + OeZTyJAXB4/btKu4AbajZIbDYRcTlLzuEsPmLKSF+UjL/t2ITVCSWuwmvLxxidj8oY1cWksHdeI+ + hcKqEDD70o8urEHY3mZpmwyULkMuJkJfmnOVqWa+mNzAVelyU4EByX5HYWLvsJgpKIgqNBKCMQ7h + L0xSJQ/OzJz50g1tWbZ3ua+hM9bEKGyWhmCjIAtZgRG0QNG4DvthloKNO/zSEn/o2pKBhSBwTQTc + MsqGbAC0HRKSC6P2YeVKtxmw/ANTU7IEkYW8OkVoP1qDPlOMwLTFFUsj5qWhKWOlGrcSpLH8I7Pk + JSRc3a4tFtgito+8ZKEmsK1fRTahj+a8bOF8eDTwDuUTEvNH4XByOHHnvoucubMgqtSOOZfPhxvt + OGjcHVuO+2+Cxu6vyd1VnFfcXecLzF0hFEW+BObysaTn7wCSIV7pTrJo4XHnfY0MvqkvjyFGzNa3 + u1tPn71oRgojMISLybyjCX6HUz7ahYA26fWa/Xhqvpp4BkEf4Rm5fE/TdpWb8bsd3CXPFR6fk6nc + pj85wmbE6lSTqiHSIzyBq/dgAkmyoRJRLAtw6hwxHLm9kEvAoIP7lHzpVidiiE2dgnamssBOzzRW + tBJwwsizjDNy+5t9oI3WA81F4buO2YikpP2NWCgQ40lIwIVZ88InbQuNXZ8LOIgkQqGnpEDrSiVQ + iqTmVpmqFdL8RlBSlnfwdqNxFc5cQvCdbQ4kh6vs2oT7RUyRlP9kiXmPLx0SyeYrrkplXfhDs9+O + AgPEmxKI3Sjsb6DauOjEImEAwYZzzTZ7bX9ifKmxOckYfCSnYfAbEMgTuT7l5/OIsgmOXCkKfnCz + YEcWbZ+LEDZR4GU+IzKJBBJLYt9zowTAx6O/INgFmJZUIXvOc9e5QIGigIVOIXJ6Wls2mVk2mHJK + UIkcr0skXCI5YjtZkdoyJtFlvEPJlFhnYABworcJuCsLbVhTmgAd1JP5CgmxKiOIvyCSbMDv5GY4 + X1Wp2iNpRYgj/k0yIgea4AMCaKwxYhbxb9yaLxJeOklK2jseF+aP0pwsaJt4tyMRCBIP9vEIYJJI + NISJLM+PQval1ns40+Cz2jzOkJdqzSOnJMNzZMvZzWEaoaYLLH6+Ul7oAmxUQsaIzTN+Old2mdsE + v7inmAdmKfUSSz7k1bQdcP6rC5s6gpCN2EEGZDNJEtFOxVGC2DZDHn0ORi2y4ggP16ZDVJK0E+kj + IQ12WoTQ534Qf0ak+ghu2qoLXGUmqqLicyh+MLTgSAE5HNEhyqVgf3nK0VhTMHOXr45CbuYc96qv + Ng6XXUGcv9UCI2v2/5hO3gTdyfFZaLx/SBk9rXkmzrt6LFTtTggTclXpOR8u7gF+LofTycnx6ipz + DqlEz7kefJ0qL6Np//Bk2Gmo2FnozFoQTD/GBWqFzYDZ6mDcZavBrD8dlRutR//v/yRVRyogMXBc + bYvKBJ1xr7GzE7g1M4jGXbXB02Uw7ozf9cfRuD/bGIyO4a1+QiJA/Tv3981ofzO4u0UlBuPZvSh4 + PBwGUzDoMqA6cD3tRUvT3uAAfGlcvugf0uRMOzy9E76sdlKyuXJ2NCiDkqrRKhnD4/Zk1i+DTrfb + P551xl0uMzmZNnn3njMdV5iIOVPMS3xDl7uEBH6KbdE/ZzKBqwDXDScHYWnLPVqs2exbVR53xnNw + 02S3HvGMbyCv0Yv5A8dJk3za6Z6Fx5PhoHs2P97nJi94bvOaR7vQwqP5WtEKN7Kl9bLsJjm3h2kJ + 2f78Rm5yWKzW3Hhi1hwRHt498w1ccluvwjH/MgO+DtJ7g5KaHIwWHWSe1r43I7tW81FV+rI4DhZ3 + 5a3BCLZ0WQ6ICQxmC6vw8XxWM17m6z+aq/P7xw6eXOxPu/2QlIla3sail4ApdcF+9Y25DbtQ70bQ + tT/td35pPfrzeL88fnAtXC/u30+2Mnvs3F0OxoemFb6uM7dAqxIGhVXU0eV12tTao6YWbsuq7b+l + 1sb1U5Fm3I3PZiNW8UedXn/lqcEc4nY67O8bfPtjexW+0BhNRTg+67Ye1crfHHautc52Z5PuL8F/ + nUwgjuyfmWV+Sazi+Ci8BpKbxLjL4bleZRWi58osyYSXQvUiB6+sVw0Gra82SNZ/9MUX1rA16DkP + +XAfjgHVlcHLudNv2FqPjKZRN5YNadPhYoC5VEE4wfk1biB09knjIKGy+4ujlZRtCl6229C3a8sP + J2gDsmz/8Oxhq/+2Ozyh7fDIjdPCNAeRtdvVvhxPSr774aAzCKw3cDQbemBt1/zpYWs2PXGC6cl4 + MMMuMyBbNWKD12658TcsXluQdIF9zED8Nr5fz94watnrIe2J/nR/0pn2/pml+WIpKjPkzNcHw/7b + f4pY6vOLuGkyw2qyOXKwCndsuHzZAngioE774X7/aDDuhVz+uu+x6voVNpPyVkgR0P9BA/d0wIVM + 5nAaDeeI1r74ZhJ0p5Oy7PfgCEdkYnbUmeGmywhOuGs/rEXB1oy2em/Q7bBSE7zpnOGc18Tp6OD4 + lDZmh7fLOnQiWrKjQYmS48lscIB6lOVUsbr10n2qH0dWGLUX39g3LUIcuyyKs8fw/OTz1ECYL4HJ + tJ9jvqvmSr/bEXzcHg9xoz7kfxC2jf/BTS8YgxDOgAMNp+YfTnKG4ItfOb/Rwd4a7CzLFTn623A+ + 07ZqysULBonKcM4ryu7/jfKoM+33qv1LpOYO30iy28Fr1c87h31EgJpRIb59dOerUWdQ3VAhJsFX + nSiblsCdO1/gv6+4rzv0X31T1su+fg3bBtWoFzicDnqvX8NyAdNFLTVaSJdIB/PtmYzjTpftFgSI + HTFDUf2ch4iLhyL2O+5imA+nndMBgszMw74ETBN4w/4BsPQVfMSOCed33tACn7yJ+OXyXUP6y+Bh + 0Pj5n/8Mfn3/oLkOiWLjg8Hhqro+m9r4x08XtBEdn5RHd38lVPA1qW0SBbZ694O1P3LSiAYyS0Wa + F91Op5/oPNWdXtLv9vJ9LXQ3SWOxtk7VSUKgdnf7Q+pgMkUL3F9oedzlGqmDeD/4tcU8u3W/9bJf + Djut9VZ5Ntqf8Ifd7ceUhlvB9AS3x7d6rfs6j/N8HXzmqDM+7G+byuOT4XDdBWA6o7pUD1TmcDJF + iikzfSI21SOR84mJKWRrkUBBdOW0v9cZEiO5r6JMiDhVqSoSmcSpTNZbnf1yMjyZuSKInlHU/ssL + 1/fMlsAzqFm63iI+czqYnJRMO/EZT2dRlxNYr3omHtuMvvtPj21P9W8vLID87RQdPKf5fHw8aN3/ + tXVsfm4P9qcdHmt3OKA26dNmZ9wb0tZZ5zJbPfpKNJQ+3G/9GItY6OIxwo8KTSgVefj97lNb9Clx + n68H/SFhu7UN1x/z+cVkMppr5UVDM0Byf3RMAPer2jMIHoQ2+kXKu8H7e5P4bUfyknosZ53R8YcN + qd6MHZtwY2vF6X2R34+L4PFOiwZ5PDwpkzh+aVYeHLKpTF39sgVY9bKlrMTyF5JQhMx0Iv886xyW + D60oZiWx1yUX6vVnncHw9f7JWdkfDv98PHwoW7zuXlrsviRRm7p8Pu2HpoEA+RMzilqBbx/vtqo5 + MR/NBmkYGO3K8uuT2cmUMg46w7L//os77+89IJazsRG8/O7pd8Hdzuze4CC4uzPp9YeRKRv84WGA + Ju+Bn+Mw8hca7Hrw5ujsL5clYBdRIFwYCA+4tw+jRnVCdDNE6JL0R0YEt4qzuMilSAnwfIn+6Ehn + dfqzTH0MmVmgPqJA/KpbQn1irS7eszuD3vk7db6V+Z1qKn9s2nPZcVyS9FwwoHMpz3cHJCLjdrGR + 1OVGvoEjlVVUyKy+JUphP1+DQKzqv04soKx5WmG3fKfX2/Lbq70o9jTmLkg9TWUqmmGICG1uyIKg + nc874/7wW1KtiH7MVWWKcGc4ORyMv58OKbe6Fv+X6cM/yQNDc+nHjGSVkFOmjr/Zvlhvw+esUbn3 + 6yzCGyrGMtiFROoL4IrEeytgLojXXnJd1JcbyKINpVgzTlymPMvI5wRiOKfifBYk6KYTDHfMCX38 + auJkqxGGVdEZVsHcNMC5ikdiRb2B37/lOfXNIGsnXQutGI7ziMXfledel23M85vzm8B/xqTHjT1a + DyBs287P7dvCtxpbG0fiHFyungRmLR+CR8sDQ26o9ciI4B+KTzbrH08G0IU/tC3HxR2EF87Ql6Ro + ZOd394EgOdnj8iBBPfrTB8G0ItSqz169RFyEGESkucSYH9Vk3ksj6lFNzL06dgHh+JCtAmFYjjpD + iA6PiHN+AFJWhaZdNjavKH0tam0D9xLRviBMzjkt2jZg5mFbz1Up83xrF4Xf4abmgwmfN1ojEQVH + gx5R8eCckgeDt/1eyKZTe8ksBFom03Afe8bdZnlu9bggK/4Ex4cpK2ZB++ungY0iVAYsF40m476z + vwOKkGohitLurDOdBVZEOy9W8XXmAHauC0tduKvmsWtTl0OdP3DohZUu9bB1MYtvqFa5p8Bi1FjC + HF7I5kxIGqzCrcgmFfth6/vdPBdp/AL2ABxY9mCq9sLcxfOzOEdEiEisDGeT4zB8y6Thspi7mCp/ + MW/iNGclc5Gzr3VW4kyoF5D788n5lQnbIuI+eHHjmADCM4TMMY0rcLsurOWcR1TqJN7SC5CzMOyF + Y9rMPlG6sFtLe/tqxOE8ZrGSG1wUkeqKqJ3PshaVFSK8QdJKMf4qNplmkX5hdm1j503ZdYX9Cypf + Skg16LiCoAprzWVkjYsavqqwelF7VxZY8d+XeA7p4m5vALwrC68Mnoxkfr4Ae1n4LqB8XOQmhFlu + iMG5EtYerbDHXK2RS8iuXPgDuECjHNtQq36EXvu5cNwYVjFribhPhkP4GNWZ76jfG5zUbwJ9xZY0 + H+iJE/xvSOS9am1/Mu0tO13P9ie9s7m6r1/ztyaxeTZdLLkyluSst1h0lfjMxTu10q9fs59Pd9jv + TI1/DdMEeGWTCG1l18Np58yk37AfgA2xbLxv/liThswBQ8hRfZfDa89P16x3K0fDcSMRGfKx+RHA + Nvu7HcbryfFgPGBp3Y3nO/vl9zcmd5gFr49T9mR1bgqP7Zff35iMY92495qoBv3psOey+RS07acr + joq+ThelO6YyderIY52jj05+b3ac+NheEd7PbXj43Pp3PWVi0j4iTbfJc23aL49pGRNPdxVa3MBc + tf/vf/0/XAfhCkjHs5F17wcQxanRwfj4ZObdzn7uvCWq/wviUBhfKmK8Mw5yywqikeZbAUsQpC8S + ewxjGcaItYyrxgpxti7T5NGKFg8naf9vQ/k//vq3wfaXenr0bPskm4y+/eXdXzf6ev/H3e7z7+L9 + rKemmX54ya6609MVnd29K5VUKvj3QEhRpPeCLwNZCJHea1VLgRYDI86sk6icvX09gv7RGYxfd4Hg + 4Fe/pPY73V8O+RjwfvBHHIvnneAPg9HxZDrrjGeV+ydvivOLGOZ5mTJG6b0fiOO3Juu9gfRw2u92 + jkn97oT7nd5hvwanWwJYAa4OiRVmmDxie3iCrNPONOhV62lv0H8zwhl18DD4NWjxt93JyRSXXe4H + LSGyvLVuv+91aJz2szugs1nP7FHwciX36B7nKKWQ8fJ/gAP5suuuuXVO2UJc25XjgzYEZ8MRcufs + 5S8rGsiLwtWvjrW2nqKkObSuf39Ji8jkqaXv3PYufMrQFpsFdlyA6PvA1LdPn3AZ/KUSTx4/4+ST + DjwngyeIpILPz16Yz3xXi7/sfGu+0F8kX/xgkj8Eu28GZcnfXr403x6/3HUGMf7+/bb5fjI8hLvv + ONh9hu9Pv9/l70//z/8uy/6wN5ke4PPXuwakr6cEzMEJ0Q76+M3elvnKP+jD5uMdTm+aiNPm07f2 + 03g8OcXhb8Cnnzzcx7tPH/8NX7Zt89uTcW/CQ9vZavMX+vvsB/7wvWlo5//873H3qM+Ffnj28sVj + /voDgQ3cvvzeIHtGqvdhx4C5+3eDFsZJ4FcX1s43BjnAS/8QJ8No9G8vTXnz4z3KkfaGT//AXBHx + 4Owtu34en9L679jLI+juiDbjUiluqF79u3H/7/3+L8s1xN+byu4QUzhqKLyzVPglsU5TvFwur5bL + 7w7eriqdLpf+8eXT5XL42ADzj8T3G0D+sRlklG6CeLn418TUVpROGhrvj1cUFvFy6Z3O28GIVJql + wjuPsRh+ou/P550XGAFNrk6mZOW7UJEeJgHfT4dcd8MchG/gGHyDFVumWHVSx54Q85TDLWO/Li9F + B10tPyqzBYMnZXcxm0fvqZJF08pOlBLFyk4W6FhzL0z0LuhFrB6IIYrhk2l/ZGjDil5AQy/qRWR6 + dT812rqiixc/XNhFjbUs97BAqVf0Arp+0aTE5+CL6P/qxsEcLsLSahwt8I7mPpjRXNSHWtnHHCNq + 7oF51kU9JHJ1HyTPjMed4NkPwVJ+1Ythghd1k63spMYrmztgpnpR+6uXU53xrurg2ws7SFcPoOLi + K8jH44snWqUrm69Egubmty8xy4lejR8vYDQ3z3LIhYtodes1aWVFB99fjH29eq8ZEWhl61ZCumgA + q9E/J0k198FC10UjWN3BFlHUcyaApbeLVmcuV0/w377/7uWLrR+ePv3uxdfnIMoIez/NCe++jcpF + DS0Myu9OZt8dGE4Mzz6UwXEg5z0e9YmJd8bGUX4upxx0/Fd29a2pIU4cqMyF5cNGpvFno6xX5V67 + Jl7TRp8M6dufjwmESe9hk4j6Z3gYviPt7uGzTomLg8EuqY69zrQX8FAgtsz5ugIoc65C0g2yHw+H + bzpn5U5nPDg+gT8llz+ZVOXd2EhunheUrPzD6ti3J6N98zVGozBR9anCovC42SnNMM7c5LvWvy/7 + vX53MOoM61+Noohw6lM/L8F7o8o26aoRGwVYYzX2jPvBv91diyp7wT/YXGDyflq7F512hnfvrXPh + o9VljxaKdqen950bZh/fV1Wkgr7qPQB+B1r2yaz73cFB2Z8RoGEy78RofgXltDt/GflwMjkcmjDN + Xt/f6BwPop/Lv5BsSSLZw3S7X+o8//4x/mvPht91fpG9TG29OTh+/FSLb05+3D55KXZf8BmY6dAZ + ACobQjTt0+zcvXsvePiIsMjBNgKb9+xtv3sy6xPULrtWr28y765dFoy19V+DjnUvXzOTyXaWteD9 + vWhGZPbuXcam6ao36fL+iP7rpD89c37ud9f+6CHgwvV2GO8nAJezHrzHDCyM5e69B3Yi/1GHIbSj + WfuJas/XoGYeVPgzZql5oxMMcyuhalUmvZUljEnrnALWrNWaM1vdabZZ3blTWatCa5CaHu7fFeuZ + IpksXw/iKL/3gN2Bz8l0diqTG1B20JA/Z6O68/4LC5Q92H19NH19crwCpj/GRV5osaIaHipfVfHZ + 45hImqvI494dvOvT8tz494DpQ1Ai7f2kgn/foJbm7KNz3eKy5lFnMF3V4X5C/9etVQmP4TgdssMT + V1rZtiHpr02MHiLyFh011EtNmM2z9aCQjNjJcadLlP5+ECU1PLMjna1F9HRcgkLCZri6K4/CemdJ + sh6QOBik4sM649x+75MtP+tgeD+QvPIWlqOcX464hXsx5ueLXwJ7fjXgxTleEcGjACfE/HHQnYQH + RMTK5QWCqt4se+crPlzh2/z8qTzq933EqA17P3WD1/UG387vTULGdn98SJCadsqoW+L4/M5Xnf+/ + vXddb9tIFkV/x0/Ri5OMk1kEiPtFsTRbtuXEmdjRWEpmZq9vPm2QgEhEIMABQMtMJt+3HuL82Y9w + XuM8ynqSU9XdAAEQoAASkqxYWbMsEOhLdXXdurq7arEQ4ihKeaZCtmlJy6MAeTbKvvM7ow33RnWJ + p+Xj904FRc9PQVX2nNc7Lz+E5G+eG3oJWC1EUkYS22encejxEjJuQFMfCQvw5BF21Jte2CXXTkI+ + Z6YKRoKChcc61BS3COjJM6yHMQ8KjTV0y1rBvj7HGz+GMWRRpXy8SD3Fm77wSRFV6QvxCYXkaUL4 + KVmQxH7qBP4vLDIV1PjckE1RNshzMl5hKFF8ooAnJFqmCVphmE2psOe0K3rXRgFTMzBhAb/xPQrc + //w5yc9oMcf/4H/RPSe8hUJyEyKBdcvcwVvvgyEvha1hkVfHf8Wbz9l7vPJ8AlZeuhqgL6pS+K9L + L6HbT7w0qip8/zrJJo8ennfI8+VqSEBNB3hf/dsocP+cVWExuzz3OEwwrsUBqfbBP/Di2UhY87z9 + mYMRyCYeWJkudIZWihcmwFsYOgwIASZyvFyJNOmkA+tlvNnNPyUTEFQ4gzSAGvwdOwnNPkl0Gyvx + csmQKAaZYSrJ/AVWkCWC9wmzl+KgsBWDC5uW+PobhgBAEkfy1RXh2vOuSBBd0wSYBUzujTVMjAfN + 8uwPGHEEuiPYHY1CVpw0BEcGhrNUkRxPAE1IwRmLcV3MWGzI6wH2ZNsUDfML4mDMgss4mldHtCuG + SC2KZv50dhs4ou22RZJqaaKsd0CSroia1IQj7HovMkJpyY9AJIgbsBv5U99YsvV1R9HlJaxLc6Fb + 1y+w6XsAjeDBIifk6QQQoYaii5Iy5InoGPJxgHNYf+J3eahbkihJlOMcSsClzwp+ZNzN284/xx6G + M8QFN1RUFVGRiesjqB4GCcyRT+c41z1MzZQn4Z/r/dJscfbkGVOfSZ45EgQtmpYYoyGzOGYYo7B4 + rIrfdiD1dyCy3fp6K0nRbJBD9B9d+urr7DpbspxDD6tNMIoZZFqIZH4kgjfHqn6kchatDIb8owc2 + D61Ffe1s3L/8/p1hvlaDNKH+vtXCA8d9S83UhP3fubopTO7Oa4O6+y08VBcswvGy+oXjXsi3GRjM + c2EqWLwvFhFsvTDMzrl9SLJja0noLJJZtD7ktnHUrdU5OhqEuPYg3UYHLU7RretsvUQ+U3I80yho + Qh5pmVRf0APSazbMLjGzm8VnvLtno5myeVS7AAwp/FdX5OKCTg2ey6w5xYn3fA1l6zWXdUNsAIOj + 577b7sZMd3hkCdmoOzwEnXm3BRS9DN0VqOPk6vbg2QFJAM/tIolfkOgI1ksQkLcFknUA4vr0TUeI + cE/stiAyLHUoSTqqkY5Q/UQdtbfGdjS4TUeQcBNQZNdjbguuHej8h4UX3tr0cXdeR5CY5iQvnMVt + AabuAhbbjqVh5qlPsv0tyOa2Ua+VwnXmRYs6VEDrisad3QSOfRHa3FffMkTefhfQc/ySmnfcGt4C + DFtUNV+T2DoVsGLAo6Hk++h6/yu/3YZU/4nbTo3DYcuYPUf7LXTSxw3naIrJKUj2IAiqLiwwRHm4 + bQh1bVxcjJ14g37b1cyi8Ha6H5tVplsIN/fc3AC91F+YS3ZfkkfHyS7J9nzXvPmGYVv4bxAczcV7 + VhrZDaNbM45sUe+sXkcn5B3u4NyaeYQZfzoC9Sr2PPIqiByMRUy+uC3QNFHaDN2zHbKT0zOeA+eW + TCRzh4VJFF0RGlaQYMRuqmZvD2V213XcCyeZ4WRet4HuSeF57VjYazO4WItvnj7ngXM8emlwHRqC + XU/bWAeXarF1+mt17MiaIcuSq5map6pjT55MNNu2jUvb0zyN+yr8RJjEq0UaHQ5e4UGyLII75k8W + lnEgXE6cPB74tBxHHxMUsZ4xYnQW7QcqbDQyWXVrI8/DsW4iHMfjbo1gjY1mnMSfdGsGa2w0gxHI + f/GS7tgpVGxstCu21vUam+yOvWLNxma7Y7NYc6PZWTT3uqM0q1XfXFdk8kr1jXVHY16tvsHuCMyr + 1U7KBFDLz610xqML642fQcA0E2eh9a5oXTfeQKWFtrtjed16I7kW2u+O9HX7Bcwv/A9eUJaReHzE + cUU3Wo4Dj/ZPG3Xd+Ygmlvfni9FbU9cl2xZlTZI04/zbk+c/nr1+e3J2xnOijp4rhqwalmqLqqao + mqZJ+tfu5CKNry4cEO26qum6petq9nKCIZ6hUVmSZAlfBk56iH9jF758nTrTi8sovpjM/MC9cP3Y + m6SeCzU9J6Xxs75OL5fO4ddTdxEffv7rNy9P3/1Gf1ywLcyUv7x48cPbs5O35xfQ1W9fB6l7+OcN + RGRUcXd40FTpI8TDmoLvDBO2YVofISbWvHZ3NGFL8keIiQ2ToQVCfu6GEQuztCjaQ5EWVXvnU5ca + m8ba3ZHIRyo+Ng3NO+Saj1OOlKzaW8KGZj0UnqmY5XeHj4+UYcpLqDskj49TxVQWWXeIj49TeDSs + 4m4BLxpgQFEeihhpXIHeHWY+UoHS5FW4Q5L5OEVLo1PhDjHzcQiZQnDnfI97fLE+J3mBoe6EFh7p + LCZ4tW7Fz7+5RVAXxu1rwm+0qZK0+PA1T1V5QAz8xZzkXrAHlD8nwji8XJHNRuqidvrzKe0S/u6D + GehQYJsCNQ0NiBOkhwMY7wdDEl78Q7BkUZOE/3rxgrCtgX/S8CoeoOeaR6FnZ0KfLxNMtpnwM43C + mxfngqEqmnDyNs/3Ca2u030a8EyvXY9ez52pl4zGFwsnTkMvHnHv2QVPU8W7GVVAFWkqUeam4+42 + 7mrzAo+FZ48AUkDYJduViEJ8geHp+HVqvI4mYgOJl4qlDZAT1sL3tAFySJ7iGU68Kf3lV9WDgjWh + yTvTk2w30BM9YLw3QdFWOlBU116bSYr1zGjqHYgLJ5wGHlHrKUK296AI2tNDJgnZsoskUaIIvBR0 + kSzHu1FCVntQE0G/RAI7dFOZ+rwrNuX5PMPg1vOcTfNDnakK82pyA/OOA3jem3lpKx2Yt2uvzczL + eq4yr1zPvJq8B/PSnh408yotKWK5vzQf1weZb6aHTn1uI4flpihX1iyu9EYMy4ctyI0SMejKhiTn + Q9ldmG9k0CtBh13N3sc79lWp2XTwc0NvdO6oTnVkuWIolX0D9LAoyBtA7JrEdKUriRU6+DjIq0Bi + W15Vf4ZRMYpw/roQTLn4Xxk28ocd1lQEaerXWirIGYDmm6kNppz991tljBV4n43K46oklXc/0nzr + dffBXC+5SqPFLV4I4xe/au6F/VuGJX/109gPgnHkxC69NbZGUN3lKxBt/uRqJaxTuAiM45gEa8rH + gNjGC1tYZhLN55Hrpyv2rekSF6v0Gbuphfkc+KQ6WegVrD0qBI9raggmk8a+pzfnj97gNWiaHqKY + scWfYBiRCR7FimHq8wttI57OxGEjoDfEcFKzfD9Z9fWwAZbY9/iQSyjcLHSBubHjFRnUEGxd6Tmg + NCv7WRn+urZZdPCjbOVNOPVkaVJ4MxiyblszrsNpzMOSxdjzRJIPFG1wJMuzZyP8mA2jGGBlnbSg + rnEfZTNNXzAoTWsWtvfaT0H+C9EyxXJeABM0XyZXwGxJKkxAZuEdWgqRkiPmM6p7ShHbaDeJIvJc + t+KYo4RndaBh3AzV01TFtBXT092xJMmW57mWqfz5MornTnr488Kb5suWbEjzqbC+xUgaR7ieNE5J + yA3qNsTQ2FWDdkjsCX3nrDRJlnhhNiFP59AjDotVf0ooR5ETaIW8gVZI4IFMCKdkmWDxZcgS0kQk + vfa8dEjmgGAMQIC3eOcRHnjNxz6aqdnjYisWPNCf8R2jAQMfcVSwGgTIw0uIc5mWhu/GTkIz7gQr + MsFkCBHefwaamuBF9QTQwyIwuBheAVrKoipNMFpSuBKL+Fi0lAA03cegQeH9noWNRWTtQDUHR4py + W8JmTRcp6hxQcVRt8IRwfvjew5YEF2yiOY3uKYBtE/YlfRTVtsG8cmXbVlWUPo5qyo71gKXPnvhc + M1rirEAaIeuwujR6CW3saSHcgJOA7U+mgGBCY2aiJMK4anO0QqfxMoQhuSwmGIY3QZGFVyugeAYK + 0N3q3oRUj9jyXZLFSwPLEcuSUyjL3KpBQFjYOyjvkMHlEsVXskzwCiAV4B4skaarDNWI3xNYN80G + eAvFKURyo1L+UYh1FGLKgW6DEFNvX4glzlxI5n46SzAN1Hy+SjB37DJIPRev7FG5AqqqP+tJNQzL + sg1FUy5Bftme6RnG+Hchv7qics2Mc2gJpM6ZMydn2ACGMFxOZgKIJOBBDMD0DWuQFNoZMt4Co2RI + nr5Go8FJoWICy7xwSK49FlgljNIZD2pzHcXAE84YjIynH4EA2x1dLM/tGDHGsVUojxJ+6r+nYUUH + J2BUgj3lomp4DzUScUADwhTLY7SsseeF6B/yQMo5aerQ6QBJRnec4/c0LyeZ+PEEY3Q+CrOuwgzz + jinabQmzYDnxMX4OFE/YIl9gMghN+SCKrgR29zZGG5zrzTUZ9mWY6Rb8n6yqE0lx0DCb2Jcg2R6w + YOsHrZmJwQJugUkGpheNNJei83CCy54VOfkJbbAg8MIpWCHfY8cgwWb+ZEboCslhcbm8ySx5Su+U + EtopirzwKkHZduYsXZ+kzpWHiTYIDBKgvjch1w/qzhbeBIP7owcJhJizHh6gjA34OHbGPqL3dDkO + /AkwLxqACCV5BetwKtsulx6atBSrTxOOOR4dDYQS/Mhjo2HPj+Kti3gziaIeyCqIN/eWpFuSLjF/ + g3A9iwR6aCspkMrPQFKBA3yEOl6IrkPh2htTP3Nf9po9di+tsWJOJMdEe031DFN6yN6uPfF5Tl0z + IKqmHuFNgaSKCGtq7fdBoyT232O0OWiUirGsXer2wcDklwHzONOjePCe93VvYmtP1HznYJg0ECah + twLJ42I0dJ8FD3Xo7gPdASHoCoO3xy/PhOd5KkCU4SyCOEMKkA0jMSr8gMVBhsFaN4UBDOHbJFhS + lbC2CdF9+BLTj8OqFga86F2M3VQ3y6ed1yhkAi+Lg0pjDz5g4IPeviolU+XJYYA/cVaBUE9RTSfk + sPHLv/9NfuWZbRqKgBwNL/3plkbyEtDYf/2zVWPiYpnMvlzvpsbeFAgty69yQJ7+IavJ6j0d5kWr + TR6QXwv0TNMtHbCw7YPhIElZ0lZ8950TigTseWR3+OSFLv/wyhsXP/C4qYVMiVCGplUt9FPISXRQ + SD9bKIFZWzk0LFHtkG/ZvfCCIBkc/NeAdj8cYHwx+MMyHw3hrQ8mDUYyyn98H13DMwvYNvgnQIhP + 4ZY2eO1qPZqsCGr9ysofsHwEkiRlNQ9Y2Db6hrZxgIF6dYm+wNYOZMsWdV3WpbxdGL5lDCVZGqoY + 8FSiUFO8YhIEcwQ4/W247lEVJancoyaaRqlHS5SNco+2Qn+XepQtTO4nb/RoVHqkcaVKPWKDVqVH + 2y72qIqGXe5RUZWhYShDNDCrParVHk2W6KrQoyUqZqlHQzT1Yo+KaMiVHmVzCGQ1VCxzo0el0qOp + ZrOW9yiLWmkeYUxWcR4NQIJa7VEdWpIB87g5RrnSo6GJWnkeAQZFKfZoaqJa6lERTasyj7YxtKBX + XavMozxSqz2aliiV59HIqTfr0RZNs9ijkdFuYR6loWVaME5zo0epOkYlq56P0RRtqYRVSTSsYo+y + KJvlHlXAqG5LQ0NWqz0qVe7Q7YwQ8zFKFcqBHjSl0KOes09hjBpgVR9KtrTRY5U7NKXKjxrMrFrs + UTMyUmI9qjARZnUelaEJvaraBlYVvdojoKE8Rnhj2dUeixIAqEOuzKOsW0PDtoeSbmz0qFV6VHXR + 0qs9cvbLetRF1QR+yMeoZWhfY1VSh7JsDxV5g1aVqgRQLGiu1KOqZtPGe4QXehGriimqemWMKtCp + jlJuc4xVWsXqSqlHeCOXKEexy1JOAf6syByoMFRseWhZG5Qj21WsAnOVdQeM2izJVShS4g4ASar2 + aOtDKDNUZWujR6s6Rl00y7QK4l8rUQ68MIsyh4bg3tQdmmkMbUvf6LHKjzKMUS2PUcnkXoZVkNxq + oUdZF40qrcL4TFlD6tnocYNyNmQOCGO9JOVg3uRSjzVYNewhgDm0N2WOXNUdyoYEUKqSHLBoa0Ws + ShntFsYIugNsANPc4A65KskVKFKmHJh8S69g1SxKAFnb0FayYQ4NTRlqNfNY5Q5AvFyWOXKuKrIe + Ac1KGavyBq0Cd8A4bX1jjBLjjn+WTcTBAZiFYKbgQWGWVO8ck+oNDmgOy/VbzK4VRzQ0Pv3Ec4F8 + Vc7RWHNMjR29a44QnkUA/za3mQkzmrMjXuuDsiVze+NIH8ZNo6cFBxtrSfaJngY0SOHHHBMEFH4n + 2W+Mv4ZLGP7j52UChvQqO1goUJOdf2PpBeTSr0RQs1Nn7LBqfpCW/eInWAUMuy/QA288IjrbaRaS + AaF+mENu92to98u5967+uBxgzsMEp/lZuZrSrGMeOHNwVGy9udK6CzxKeVE+kMdX8KzhTmPOxwgL + GIBCv6UxFlvvbYz1Lou+yQyG2vBlXiLBDfKsIVClQqDyVgJtIseX0TU9Xl07TRXEZ2VvJhTe+Dsv + 8dZZQPm3Nj3RirvPlVzGjVrBVObgfZbSQxm8PvtB/xWES/8DptxjPxI8Vz1eO1GfoQRFFxWeM8fM + d+KzUfaKl0hRKJZavrig79bexjSufo8Ljud0tlE7izKfzm4oxmKH31iMhzy/sdzaN9C6MA0AvS4L + z3HuVqRoyGaV1twyqWu/C9v8SbKQwNwB2jzpWmXStWzSKVFRjUN4I3R3KKFuIJh0loL6oER9RVK9 + ASSBJqxNaqkc08ewpDMCu2csfdEswDjH8iQ2ULbMD/lzwXXKj5YXj+/v7Dzd4YA6T2BDb1q0sA+O + WXGC6bGz/CDFXllzGOCUZQJed5G9Ez5MEvcSw0RlQVX/QxCydhMiCJuhVssNXFxkSXfy6yGlOaiW + 5uIpdwueRylYN1mHB9srU8IYHNl6Nnn5dYz8VzG9KA7lHcsI1mokPHvYbgN5vlxlnbUbhW43j6Jj + 32+9ZRoDGjv1rxi99U/TxHXqXJbaTyFLytxmAhmZH1UbeBml5I9AaVGQ+gty7s0XmLq+3OI21hBS + VnXQ1K8bpXyjoPyFiqCrQuqCrajJe2kDC4vonhQmoU3bWRz4TZl6TaWQMI4CULCcHTMJTn799fjt + 8ff/ODu/eHl8fvzbb9nFk3F8K91T1SKc04xcBRhO371+cXI3EDBaLvT97vj89dtvyp23IN3vvSka + rG1oN6BFW8oeWpYHAKfY6sK868pkyzdBYDd1/8QUPZuNbWN9HoF5OWdRztvJ2zGtsVb3O0geMJM8 + INR2ModmZutN6L2hyd3adc1yyvXWNVqS7YfN89V1ELjRnB7laSVyWdkcuD8Vk3uyHHY8tR7bPceb + KqAsuNGQE+AQD4wSXLTR80HzCB4wlWi4LkJ47jx2QpJeVUEQxPIQKoHlMzcM39dkbXlUo7yo7o/W + feRbo80lijuibJMUNzZfVzY2meOI5dn54fKYj2hwYOvrt2BADA50e/2Ca/XBgWKsX6KqHRzIzN8F + NEC5kzEn8/Bx19jGJ0oEwwGj2tInRpyarhiyqcF/ljkcHAdBoczxPFrCHAN0qokOa9u2DN1UFAMA + e5NvTnIfnWJb2BFz4eXw8EfqpKNPmXuO/jjhGRIBGE3HF9xFhw4KGV0g6BTk7atQwtZVVd25D5n6 + l6udmKVOdFuU5L06USVpsxOr1IlpY6bJnXvQNnuQZbs8DEnUrb2GIW12okgVXBlAE7v3wCmz3EMZ + UYYiWsp+s4Eu9UonldnQFRE5bdcelBpEqWXKtXFL3DB278TUTFEv9qFIsqRWUWVK+/SxSbjYiV5m + QVXUpf3IqsqC0Ilcmg/FBLIy9hmJZliiUkVXhc9lmHSt32GUJ12xZZj0PWZD35gNRdZKPeCZAH0f + RNmbM64o5Rm3ZNGS9+nDUuWNydCk0kBUFTfF9yIrxdgYiVYmK1UDBtlrJJvqAzspy13V2EeWgG4V + jQ1kVdSgCkDsNw5lk7Q0RSmTFm4F90lXuiyVOoDZMPZi8to+yoPA/ew9pZW6MeN6RVpBJ5rZr6bF + TkpkpUi2qFp7cWFNH0pZmmiGaNq9jsKQyhIR2Fzfi3LNuj7Uch/2vmpQruukPB2avm8nddiqzoe2 + l/6QN5ncKOtARUUe7Negxk4q/AH2116MrlobfZhlswQJC0yTPQZiKRvIMuUK8ZqitLvBYEnyZgcV + yrVEdb+VR82UmxWTQYN1gbbfbNT0YVW5Q+9XHJoVcWjKe/Mfnu2pdFLRsxIuofYyQxWt2ocllTWU + bYiSuftcqJJoqYpmG4amAj50pdJb2ZZTLA1WU7v3Zm7woaVU5kUTtT062LThbMkoz4kqqvruE7JJ + WbZslO0eWDnIu49A2+yggiLNFrXdxYiqVlazCrCLVhUj8u7cp5rVEUAHZRQpimjvIT82ZBTwYtk0 + lEDx7W6IKGpNB2UUSaao7G7cqgpguGKkw3pQqiJJk3enVLVmEGVmxgPie1gHsMautC8rJXUk64a4 + O46UDbtZrixeZdCn+7SPB/IrHZSnADvQd5etyiYjKGUFgbbPHsJI2RCn0IFS7kAV96Ahua6D8hzL + uCTefQS6stmBVR6BvI/G2VynQgclcSpLwIu7ty9VMKRWvWqwGADLsr8BqFWPGj2gvI+TaKP9slXJ + jlzv4xPc7MAsDwDPre8u6KoqGTsoTzG94LAHF2x2UJGk9M7G7h0YwAUVpw32YZT7wLsvexDq5iDU + srSj17J276C6jlNBoRn0gDS9UhcndKenvLMkMWvnNcbCDZ2A7Z1D9ekyzQ9bywbm8JB0Wy+Y9d/T + bc2DAZ77xrsRIFeHgxcs/hy8fsFiv5KX3sJLJjO8IPfOc4KCg12QJUGRz2XrAG/D6/8pKQcSQ0cJ + Qs5+GwCGfINtHyDf/XD+LckgPWUhfJMh+f77FzuDm62mb0KormqaTg/418FqbsL6TRS4cyckZ85k + ljSAZ57LGgVPawSP8+qN4NEdxSbwrE3wTn2afdwJXTxRXw+eda5ICJ7aDJ7WDjxT1TC/TB147NZM + Fby/ee54mczImyieIhK9yZJdDCevw4lYBy/8zz6XTQqv+p+SXA+v1ABvDXValg3UadfBzO5sVmH+ + 8flZHWAYQOJc1uk8242A8X25mxBp2RY6xhqAqpnnt941OUtjz0sJnlJ24smsAUjrXDYQSKUZSL6v + dwOQmmRKViPm6ojxOJ4ukxw+wj+QekCVDNAt2FRakaUm2SYAWkuWCr1cWQX0JZAJSkfy3Amv6sFT + QebYFLxmKmwJnqxoVr3Mwds0KoKndAAP5lgWJPVcUTKZUw+eYtdLRGg58CazGhDN9iB+J56KnKtv + AtBspsOW+NNkyZCbgNM3gXvj/7KcRQ1w6Xxe1Wa4pAZdsildNLDjDUOu0338Vl8Vthex5/opOVv6 + SbKpnSmIspUJQL0JxCYOrptbjD1qSFY9iEzlVUAECT2lgfhfRPFiU0pTIFHpMTljNQFpy22FNINR + aZjiOhj/hoEFyCvoKSJ/BDhvAFJrnGy1Ya4bMdkFyhu5pAWAjdRY4RLVQN1cJ635DfMdKBGFINV4 + 6hZl0mAoVsDDbHSGVCejJWUka5vgbbdsFIzoJGvc8FIaZbTdTsSA2gbkKfXQKTUi5qXjXzsFe6Ye + QEXP8Kc00p/anv5sCfVcLSdrI6kGhzfQnwb/O5dVKqWtJtvQtFtzsS3Zhm4b7eF7EV17IQ0HxN/V + LwJyUNkiwGheBLQ2Cyms9PJyW1jrzMIyYIrUCBg/YnEDGerAHw32NUBUp01uZBKtoE3UPRdPAJ6p + NfCwxs3/jiJGo9a+mvFwE/ZsuRV4uqnU26s4nzUS8IcFrPhnnj8HHKISEetXJRROWIOyVRSaq/Vw + 2g2TvEl9FNRGTNaB2kbfFYBsRibfiWojbziUtfquHsoWlkMRSLMRyPama3cgbxSKLQCUm9wjVZIE + xddgewFwyg6mP4CnFCyvBkpsLQd1RbaVBuNap8EFqhA+92IvHHvxtA46XZAlrpPVRuR1gU41t0BX + g782fAJAZihsBjI7vnTTDCuWrTVDuIvI1gsiewt4rQwbXQEt18Adeq3I7uixYfDmMrwRXqsduKpq + N1k4Og12s4sdpgvKzWZOW4RieCBDql3MGzR0VBXC7Q5EAM8ASzvj6EYPndmeZ2AZAAKx1pY1atfy + 7a0wA5f1fKqVzlZYFZOmpTUYOwYNMtLSAgOg7EzmNNsQLacXhbUhyfVA1RHgjSaOQamPLVPkZpy1 + 0ycGrLYMqdbdYNSuQJtdIQZdfFIxo+zrVtdBzUmNNKfUSMEb9ZxBHYTWdlLjx9ta2QmmpIIqrl2c + mLVepBvsBJO6kujqTmlckfCzcW3Y1tR1o971D/DJO5qEpiBnsqXZ+8/P1900x6apGjRwXy2AdXLl + pr0oBqDKp1ltXj615A7TUowGq9qs9TFkSi51wsBbNcCncYfhFpu6welfR4YWjKVhjWzW2gvPnXgS + YE4Ejs4GKK1MwzWvjluKQEsBXm6c511WeSa1ELTtk2yq9S7NGlaxNEVqcKib9euS1hrOpJuNxvbd + PJjDVoikYNYK6wYwWzHMGsBGsW2prcUOAtk427uuRAtANk+43XqHmQPZAZOoVTBI/jEs7f2JQ954 + cYx5kr5fhTUbZxWIm9F6mxBvW1a1gk6WG5b2NWTZCbRmG6IVXPwobyvpCGupJiVt0VCwO3gcLEFS + MkuiUTiqrRx0oJ/x+EAt7qxaP3H35ZRFvcY3MLhqqZUrxe0gr3WO1UN+s24sQtroFFNau48piA3G + bT2IW3mmAF2zfdvkDasiz7SanMUAWY1KvNG8BejszHpslJAtiVLRTKkBOpvGiN4gyii49JrPEWAi + WDDAs4VeI3jtDo0YqqorDWt6eyTXgHcj8mxBNrLNgMYFvdrqhBUsXEzdrOcLu3Zqb/Tf2HRqre1s + oTZYO1XobMk0a3EnS7W7FDd5G2SpsJHSjDuzFe5gLWpbShN0NTPbqEkQLIPzg9YMVqtlAHpAbLP+ + OIhU69K8UdQBeOjXzM+DNIi61lYXLqZkq9aLhCDWzGsLqwuBzLeiGm1XVW8l8QyliS0oArszLR5E + y1zDW9iinUhB6KxaidcAXWc1XAR3i4Bu5R3h4NZZNQ3gvnv+Ij8fyTNt3ARjs2XTQJUbMNqwjK1z + FDfAuI2XlYxZtmzxtATLVAB1Dby8k9mC4Ok3Lun5JY82vGwrEoibWsyxcOndbVZZFpTM0dp8fkpp + tUNhYpiWBhTWn7y4UYe0OnnRdDhuE4OmrGuAwVr+UGpp7+S9F0+iuG6BjCcLpeycptF4asU0W6Gu + K2AttiPaAag0eDZrALSazpF249oCWM1HR5VWhoEpG6rdoN6U2iXmjdpDWa80leYTj62seVM20T6o + Pa+u1G7KtvDLIIAKd6fLjcf22q3QwTSQt5BdjUTprt4UKmCsG4iQXVK6CVxF0WWz/ky9Uuv8f+74 + sUtOnRQ6u2o0Z5T1DsCWQ66tTEJTURW1QUYrtVb+zWpEoWa+doOIllpNOBCuXq988+O3agcRrfID + uFxEN0PXEnm2qltywwFStnOithEyanb+ljmMGs+Nyq3taFM1ZLveEOQJJ6qg3WhHUyBltWD6NQDZ + sLxsBLLh/HcdkDcZCBUYG5mjCyJtXGY2UKBsbsLYzcPKIDaz1UnzadxW9r+JR8Kt+q3j7B5KGdrt + i3YGXeaPab52ojTs5lWhMwzVUmo3Q2Vus6pdhA0FL3dmaY1HSeUG8Gocrd1h3LYb2hpAra1RbWqm + qtev4Xlmp514RjHOZWX7pY52dzoAPh2WS7XnBOrh66iic3DN7ODA3mKIgdw05XUobbdrVgG1efZb + 754wUOuPYzdht41Yz4FsPo4tt7O7GYANh+1rAdzucq2At+V+WdubKd1B3EGit4C4JT/pkq1bSgM/ + qVINtI2+fwqZKmXSqJF1lFbOG1M3JbNBMyrcybkvpyvU+VlRjf8cDr71HJemLuBJPku5C2BANIAu + Syf62j0gT5vinz99UsqvdA/JGS6ihR8CmDxBRYc0DT/wilkWp43kKzfnvtnM4+TG0cLFrM7Zg0Az + 4cReCpTOgtKzJCyXfgDUcTgYy0CczuXl2HJNzXVV25XGEggozQHbzjPNmkpCulp4667q0+RkXzEN + iz+dYrr4psxO2Zsc0PxNkr/hyTt4jHAhmTs00VaL1DfHQdAmqZETx9G1QFFHfwuJ/4vH+mnKcrSR + Vn49Zj5HJPAT5L3sL7RKs6JQZGJY9XVak2WQtcLLYoZsmvuDU3TgbxSgEd/Z/PCZ8YHYPhwOBLn8 + msb+BpINqrHu86ZS70NaxNWzUeCvM9N06duo7RpFz9auny9Xt9R1pkG2dv8tLDtvqf/cSN0KAEby + 3gTg2WgZbKQAr+SneVIjON57cUoz6oBgjIIAxT8msmmbn2m5wOjqiZcIKLiYBExqJQH+aCFEKLjp + OHJX1RRH+C5ngq0pnPD7Rgoot/j9syxzcKHKKHW7tLB7ze3Kcfd2Sxnf587Uu0D5xFIkoWx1winU + msaeF5Zyxu/U2RsH1Bz8P9ACMOTu7XwOJkin2mTmu64XlhoBmVHbRjH71d4UwzMG3wfF5IbeAyGO + HxdgGrkoJqJ9qUOSHgp1KI/y5KOQJ48Uc2PNeq/GHVDKqldCQVNsD0qRtf0phZuMv2tqKTmWnPni + a6CaT45W9EdaaVOz7OP71HSPbD0YW/beKGSri/XTEyv7E8ynIFYqO1ufHJloxoMiE/2eyKS8ifyp + aR9FeTgrn/uikMIW/m2TR+y5vRIH+n33kSE9UEfmoL4LEsEjLPdCIqVzMp+aDFEfjvcET2Q9WrC7 + 9dWjaaI+qIUxHjW8F6L5ZjmdskBrD0LvvIyuw7UPfz/V87DUzn0RyKOX7VGYtK35yS+HHxSZSPe2 + HKZ3ED41E1bpwVV/RyasdG/WSPHuzCdHINLD8NLza1nKvYiOT9uT9kBECL9beC8UUrnAeHcW60dG + Kd2Qf5+Uot4TpTyeTyoZr+bD0T7KPVFM9bL5p0YjygOiEfmeaKQULuHTI5CHY6BI90QgWYCUO6CN + j8o70scB2LvyjtwnfVTj1HxqMkQ2zUcZ8uhBq5MgD+QYI8buui8HSSlvEl/8ine1oOlB35Q3+PZT + OQ9J3WCwt3shmE/b5ao+EIki35+b5EFu6hVv+u27q2c+nEOuPPDmo2u+NdC9HVDr4YjAnUgS6dFq + bQfsI2U8Os3uhkB6uGzzeyeQx72aEsU8kNPQ90kxNXHaPzkyeSAhTHhehXsSLI/HW5UHdLyVpwl5 + XOG0Bro/M+XBKB0MofW4wrk7yngou74sL9Ojm7VlZz0ugbudJr63o8/2/Z1s/bSvd/Zwc+KOCES+ + L93ySftItAey4kUCuS8H/FvPc2fOPF/GPLQIav3t2exNKnd2C8u+P1v1QYbE6i+Cp2o8EIli3Z9N + 8kmrHPWBHHbmCZgf3fK7dtYfxTwgkfJ4SuBeKKSSG/6RXG6oeZcRxj8un7z9cHzyQCDSvV3JutvT + aR9Z8DRN0famkruLYmPe362sx8ji8N/nZg9+2DsTKvdJLZ/4PU9LfiBLn3ukkceDA0AnD+jgwH3S + ymNkxnVDn9t3lLIA/mJqLniEJ2yMPvGEY0+ePMmeC410zLlYrjFLA8cVWOkLH6YhStILx73IMtnR + 7zCueOnxN8vQTw8H/HCa8DpMfNeLR2dpNLlKRn9dRmlWcBF7Y8zYKH2QDoqfR3PPjb3JBeZY/Ldp + WPB5O3Sul1yl0eIWweNg1ED5b1kx5OonQFYwjpzYpWNYA1+bzzIjgRYpKk+SFDgi9ZI/ET8kP569 + rM1RCWQhsByd69Rw/MXh4BIaTYSFF19G8dwJJ16WIDN0ahq4uIDX/tRJ/RzOSk/rLprb/qyYRhFK + NqdQpB8LefwQEvx5OFh5ThwAx/+D/i3mAmxV/V9LJ0492sJfs8cb8/nB2Fk2yXrssnSSKD3WCS9b + ZPwj1Ra8MN2YqCyb6HrgOMmU4UuipEOewOzrJAoERcDshoIT+NNQiDF5avGruvWrtvWrvvWrsfGV + U1+KJF8Vkvguo7otihIqb9QcHP0xHCeLr0FIznIRW1eQUIA4KJgct3MNrXMNvXMNo3MNk9V4UtQd + +IA45SjfK+1jrU4bB87kakCufTedHQ4U/YvB0TvvvRcuPabJcqWEtXkpGUpttlQczVqVyZI61GRr + 3UxJz+7aqKoPVV3uuVHDGuqS2XOjtjY0jJ4hVWx9KBt2XaNtDdW2tPDSfw/KNnT7IQZJLJye6AUV + D6FF4Q6au615J//wvcAlX4Lt8sVX/RDBXaDjk2qu77k/OT3rZ6Y1sW9xqouG3m+Lhmj3LJ110VL7 + htG6E2F/Ojoh73Dh0NP026LWMyZUIACt3yYVW5R7pgBVE/VbgPJOaODk+evzniw/bajrPXOrIg9t + U+m5TXNoan1TgDSUlJ7HrqlDU6olq9uggZfHPVGBPdTrKXevGbONnqlAtYZS4RhEP23eBmXhIuVO + qOCtl5LTOLr0054oQR0afesDoC61d3kAfGb0vGAFudU/nPrQkmqp6/YogRy7Py+T1OtpPYgkYd4C + SfQ+fUASet/T96BJ4jT2hHPnQ78CQhtqvdtNylBS+27TGtpq3zNnD7V6sb57m6o8NK3aBdgtCogv + MwnRk7cAiEI1ehYRQBSq3f8EWmbfYudBE8XJ6Rn58m0UCt8cH59+RbwPIjn74eTRs7APjA/Vs0Bp + gdJBP/Ovilrv89/3es0Q+9720MW+DQajwVHR9/x/E0cJvYoRzXvaVwKTTJZ7njFVGWp9rwE1aWhb + fa9VYf2r9dym1uhZ6ZsWXjjJjLwKomtyGUdzIIr3XpL64bSn3QXAjdozXQigh/ueQ0GWhkq9rb5P + o8rQsPpu9B6I4oeFF9MTLEl/XiilbytSG8rKLXi2+qZeBX0kfXu2YKFp1sJ5y4Txyg+dcNKftND6 + ttkFS+55+1h+3I8ukgENTjJz4p7sCE3seyVoimrPUgFWEj2vAh8iCbyKPY/kdNDX6aSh3reHHj0W + fbOsrIOx0Lej1BpqUt+6Rhnqd+OALFND31JBEa2eOU4T+94L1cS+z5XBivUuVE3fpPA8iq7IT06w + 9PomA9ns/TCYooh6301aot1zk6rcuzvkTkgBHdEvvXFPGxICmPiW3LfqVQ1Yi/SNXF0ZSn3vUD1E + IyGLIsRvwr303ntBtJh7YUpOPizEnryP/R9sAZuh9/MiQ0vredmnD2WzZ9sWTzLfkduJXaUFMvBC + 10+XfSkJc2j07Za1QEL03qTdt96xh1bf26B3IiMw1IEfTofkGy/0YiAJJiuO3bkfij2KCW1o9L4D + MNR73wIAi71nwjCAp3tuUh+a9SK3d+JA63EWBa4X/89//9+EnPxr6aerfghCN4dW31LeNIaS0jOV + 2eZQU3teW9ggLfres70TaXEeodY4ThIv7ckZDagw+3bSywowndz35SVdHZp233e3dH2oyD0bJtsp + 4Vl+ARsfsamjwg1sLND/HdDC7VUGzRO8S0w7Lw0KPz+bRAENHUQhhB9ZCYoJQZaE99cMK4KzTCOB + XtidYP+NFXiBT67Ws1EJlzfcUd0iIGpvqJIkXQUeB+eAIIVuXFq98aYnI4fT2HvvR8uE8LvN5Nk4 + PiJomQKpycpIzUNEdG76xTKOEUd1LUsqbVndreW38Ka+WWOkSjs3mwGMF8Xr8LAPtE1tauU2n61j + NWTXbW+8bVtHPSR/EoQwWouRcRS7hTvnqHuiIFk44eHA2JRyg6MTJw4B2mQdP2At2DiszzjEdXrs + EsQQ3t6OYsH158I0dgDot5FIY3+ETrBK0rzB2gaq2KTxVTrVUMzONfSuNeghiG7jMNY1dsXjMcyq + M/XyqekEgCzKio1xILpCLomWYe1W0ZZ36hEP2cm7VMRLWma54q64pux7PI26oqraf9vJkUx1Nxyb + +k49Io6lniZnVxyfLseBn8zIy860TMNxSlQ+dwMe5O9OFc1dKwp7VFjbkbsK/JvlPY+5UCPu1+uZ + OxX41HPRrUZn8S13F9/yPkrlXgS+ggEgdpK/ijq0dxP5uG9h7yS6ebiOnaqyoBz3KPdlc2jupupk + a2jqxk41jaGtarvU7CySaufmUejfptB/UnAe5K6DLEJPIY5dKdoUNHpBGyUhhibLynz2J1IVJAnG + BHvjB4EfheRLT5yKhCuCr0gU093sBP2R/FsWlOMrkZxFS1jzHpBXziQ987IAl8VIWPyJP+wdY+7Z + fwjCH07enJ7/493J2ekPb89O/iAIR7u2VhtVDVbX/uRqxWJfLSIfqHbm0ckhGNTNZ3HfLmDEgIeU + r8JahGDjAePIMa/II7A9aeX2oU6FTbdNhyBeaycRlLn0A1g7sx+HAz6EosuC/+7psewOudEfcpPH + tC5o15O3ztzLyG8d8ap5jY6uGmoZoejZqSLliYSkNAHLPi3MvGC3+qcY2XKnmulqAeyceEEwGi9X + X+3URrTAo9HVqkV3Z2sfxvYZ/6xebha/f/bMIbPYuzwcjBIWT5HzacamdJbi0diZAk9EghO6sXct + uIIs2ZamWQMShZMA+B7kBsiKMD2Hmlenr7/8CoRFksZROD16zuqCIYt1yctnI/7h2cgpglryJG+R + +Dki16OQQK0ZmZLpoT08WCiVQknt1ZyhDRXdFrumUW6EziidQtqrLdxNJYT009jb+sjFbfd0Grp7 + pNe7pldTA3u6P3pVpOJdyr2aer5c/Q6p9cqPr2ZRHAq/OBNQbSugVkuyDbUNtf6F1yX/m9W9HWqV + 1B6plYZj6JG6JFhXyf1JV8kW1Z7I9XcqXT9yepWVkWIivXYL0t7cnjpU9f6kqyJpQ9lQe1PhsiVq + HdO3PIrX3ze59mkMILmq8iO5PtqufP4Ubrv2Ra4f91pLNh/XWo/0+rjW+r1Q63yZXAleEIWCpcqG + obeh0jdQh5xAndshT1ntkTw1PP2r9EaemqIODUXptU3ZkEW9p+H+TmXq751KdcMaKkAGvZKpYQ8t + Q+uRTEGW9uTyeCTTB0mm8lCV7KEkqT0SKtinqjWUFb0/QgX7wbIeCfXTJlRbHsq9Eqo+1MGYkBWr + R0K1HiXqp06osm4OJaM/6acpeIYLzAm1P3MCYBTtR4n6aROqaSlDybJ7JFRrKNn6ULGNHgnVEO2e + XAePhPogCdWQ9aGkmT2SKcbCNoeq1aeFaohyT07/RzK9MzJV+vRLoTnZo9ZXpaFm9+yXMg3RfPT1 + f8pUircgTKk/HxKSqY4Bn83+BLRsasVo949k+umRqalIQ7tHMxLJ1DBVjD7dI5mqom4/kuknTKaq + CSseuUfTVEU/lwak2qc0VUTjcTPqUyZTHbM8qH3apvLQNJWhYfe4GWXKovHokfqUyVTW8JprfydP + YOmEB/uGhtKfj0s2JVF/9PB/0mSq2EPT7lOaYl4fIFWzRzI1bFF7XEJ9ymSqgNJXjB69+0Cmuq0M + cXXeH5laj9L00yZTDVblutzfFjySqYUSVevvqIBsmI8r/U+bTFULllA9nr3TVHWoSjq02aNDyjAe + j51+2mRq2UPN7PHUqYouU2No93mYz9BF8wHq/Gd5lN5nm0F6MXjuviFsdgk64/KQOxcTB6jXdfiI + WkSdyYL1kBe85u8y7MztB5jJw0FLXwy6xIwpR/HeNUpN1ohNO+dTukc7bBR/Wvm7RZ4pxsY+esLi + 5k5W1Yae3V0kmBzJdFxrXu8scUu1KQf1I3SEj6oZFrpts6F9NfWWaeh2l+txGm5pGrop98dpuJ1p + kLstMh+n4Zamods+1OM03NI0mI/T8DFMg/E4DR/DNHTbznmchluahm6nvB+n4ZamoZsH6XEabmka + HlfRH8U0PK6iP4ppeFxFfwzTID2uortPw03bG1i0sDGAqQPI3Imnfiik0UIQAnj2Bkd/+ge6jjGt + SzrzyDsvWXi4A+fxRA79BPuv3Smho9268fEkm998z4OOG7c9Wm96bNv1aNjooP3utfNQH9n+5D3M + csE9Xyy1JgJSrLFOENO+0uvwMupU4WWphzpn/77e/nouznPSvfMWUZwWmaVYoQZoqJynLyPd6v1V + Jhg3mBQ6Dzwn8Tr2nicf2UFA3go2eE62rthQesGG+XFhQ+iKBbUPLMjSSLYeMha0XmhB5rSg1WHh + 2ZbMucUEursomSYd02Jv/dRJUkLFc/IpqZfjSbp0AnJyevZJKJiO/CCLsk1TKe7CRkoPbKT3K0wK + meiTp+SN56WYvrajTLnV4pI1kqjsUO5adjzhNfCIDjQdRYEjzP38M3wFrvcXKXG9Sy/OTrxc+6Eb + XYu8wosovPSny9jBZDrkkGz7+u9/k19/+5rC+96JycKZeueYyGdrrezlKS/9dV49SqDm06esQf+S + fAlvRLCYp8Ca/3F4SKSv8MOvFD2FvqAUbeO3JzeMJu85AMTNUUyKi2Uy+zJr66uvuzbw2uVNPF2j + ++lXX7MEaQzXRzvPJuYWa9AGhDxz8Jxh3ho8Cwsn9AKWt41UX9AsnGuRzw5BrQsdDhzXfR0mabzE + YfHv8E7w85fCZRTPqwWPBAGhIYT+QyFm2imDbJnAoLL88OzTgNWA4oXBlcpNgigBRbNMs9Hy8uxN + VoX/Yn8EwZ/Av1EYrPI3LNmoMPNd11uXm0WgA4VxsPTyV4n/i8dXklXMCBSWjWHnMAFUmL80gwmB + IBQSWg/nEj8XxjBineY4YHN9E0IcmtuqiIvs4Okf8rypQTSNCP4jwAL5yksTgR9GZS/ZKDE3/ICk + foon3ngKONSF9IEeMN2AC2wJOm90riuTPXf8sB7iCMRiAVONQ5vGTIQXx7aocMkvXhyVD3zV0ScW + ODp2XbJ+R9KIXDvpZBb4CRgVi25Yx0O8UZgQtJzoPxcXcXRdBLRiXuGwA0GQlTKBFNketOgE2JP+ + gSlJYw+gqx8PK1RsCYmNNVBq7uICh0dpbaNKaaSbP5sGUPKxJHMnCMpD2sqJfFT574UfBLvzWj1u + luO5T2e7ylB3NMY4QvPbbRxzd5HywgknXnDDcEpsicxHdcGIKoPMfgA2ZQ8lwvNTT+BMSTFANt4I + 65yfyNtkEoGCuYyjOTmN/bmfEIG8YYIlzx2Zv8lfCJVVxywNYO6EBWpMZrWRzVdAIrSHn6CNiPeG + PjxgPv+9d5auggyLrJyAB04jbDxdJAejUQClRPZJTL3JjL6g//zke9fiYrb4c3KIyUlMbbC2tna3 + sp6tVftoRL4H5vNCGBgoSOK4CQqdBI84oytyEUeYr5QdniZjsKBdAmR0GdDfE6CiVCQ/JpjzFJRU + 6l/6UPraYw2AbPUvY2fukWUcELAwYJhPcgsF6IculrL+v3w6h2Ua2DJPh+RL7ytyeER+zSwpT0T0 + fcVefAazDivFhCZLBQOKfRTZ768LJUDs5V99t/iFjyf/yn6zEtgfb/uPf8Q24F9klktYPbrUkGOl + M2h4k5MZcEX4ig73kLjRhPKF+K+lF6/OqFyL4i//D8PHf1FioBg7HHz+q+/+Nvjn/2HmG+u/0Fje + zWeFl2KCRCXmw+AP/0meLj485e389iT/F//5bUgunSChVmLBuKuwyjfRcye8An32jiayvUWWmZZ6 + Yqt4Ti1JPFkzB1Cgu5yArTuNxqxCjBXESTQfLZbjkeJpkueNNUExPV2QZc8VbEfTBMmxZXusmhPX + VP8ceyjQDuU/OvPF1wzvsqLqA5LblOibHxDaP7O7DgdS4Xy39AWURZwfDuYgc+n7A1WSFh++njsf + +G/d1vlvNh8HmokvOO/z6ab9Ai+yse7Pzp81evVvcOtzp0scXULDJSXNrjRUQClkPaY1h0ASE5HA + 8gZYlqY8RnnhAqKn4RD+vveCaIEsMARFFS4vwRpZxt6QOCAYEgd6jC7J5TIAe5cyR+xPyHtv5k8C + aAzLoEiYrsgU/7LlGq0JXIS5lpMVCI15IpJz6BRIATTTiqCoomSbzkC/TWcUokuY3+gaF9mJN6Wr + pgNyDFbkPKIbLNjoCevqm3JXZ6wr1kWhBm8GRjwJli7trc24ccxs9NQRgcOvGXgCwhOsDfiL5RMs + 5ay7BstuGTgA14pMYs/1U46ArQPYDnAJRrRQwI6gTdRCDJ/xmwMA8PnJJiRjUzZLsJSM8xJsqsoz + X1cCAUQd8t6rzOs1oOMyM1hW5Dvv8jL2VuS5CEOMneXYC4b0phl55wH6Cd48G6KYArucnIy9eObE + LuseXk7IuRMvvBDdQqjKvlsCCcpDokiSSsv4mEHYcf+1hAY8wDKS9vEygcaG5PzvIhEJ84Nwrt2f + gXflYETRt35CyQFQSrnyf/77/yY48ZMrQrMZI74qIpyxd4nhC/0XjFQgEaolkJ7qX6PhQ8VH5i5d + cFdu4CzDyQxxh/2e/sAQHXpEsRHTsgQTHDsunwKkx7fHZy+P/zpko4DhXFLUy6qokjlPpc7TOzsp + cYDccHAw6s9lc51RXSSvUxI7PlopDtgxQMxY5meYPYLLZvK5ohhZe2Jl6Vs7QlgyAd6FeRTDGL/E + Z4LPX/F1Ma7JbsbexQVbwwOWMgzReaJ3LvlQkMbRSw3076BQvAQupBZZ4mF6+YCsPCeGwV9Sbx0g + DLGIqKWsEtMGQmgVkRNEKZlGDLmAkhBp2xrSWhMnnjtXqJIdFLHuMqaoCqFxgNhJZ8T7AOj0PWBF + xjJcsAeIhuUCjUP8iXOKXyk818BfwN9+SkeBWbBZ3zCMKKR9ss4jMND9EMbyLgI7EyoyNh8vE6Dn + JCFgH4Q4vekMeJDe2uOsH5MXJz8wFqfXSrFnLJN6zhwhuvK8BYMS1W6CNAVlpzBAqgZgCFSlY1FU + 9yRapozQnkLrfgzUMV76QSqgCYRuJlxyr8F+A9ZRQM5gYK4D+PwbyGpAE5quSzpVM2cB8oQJCjzb + JdKVOsw0xbysQgPAo0DwaBOHoPTdUrPIIi9AGPLN/3/AVFA6xqGXkDEk9aSTRtEVcgzUuiSraEnG + OGo+Ql6SudXW1JPN34weOwDJBvWegnQFFEJrOH3IZiEIRglsH7DclgloH9ASQA2R66zyMZ6h0KZN + Jkvqp5rG0XU647hQczQXAZ5xoTWDEY49qINUgvrwA6xCwf57H6EKCvx0JZLjIJ3RScSR0H2dCteE + T1NKs5MUlyYxmmweTMoC9SO+QwYAjYIzMEFnqIukTa6jZYB6is8xSAGgC2eRANFf+wA+ZUTmglnG + mapATlwBr1wnbPqHmdYDTAGlhcypikAACM5qSLgOQRsn24eI6SYIYy3m5wLh8h47SK89L0V5vabz + DMnHKUURpwL6aebPgdFAkTo+khMllFxj1koXFIDvnWCJKPkxDPwrNm0gUtOM9mBhBqIyXnEkIGUm + YDwNM2xu4H/szUBdRWztCNZ4AGbMitBb424ua7jqz5ukrJkz/WSFlk8m+K8pSWCjUAYlLaGAOuQM + 6AGdkz8By4Hux2VyNto1LVLIHJAwYFAgZ39wUrZepUtZh2kMmBswMUDEUXMLv4wRBTB2f4K2FZaZ + RdcNzIYAgoBEZzKACNIgQoWM0PoUh0wSAzNcXvoTH8nB9eY4fMQQG2Nu9gGCUGyCzOOkwCYywmau + QYkR13eRvBPPmzNXYEineT7EX6jV/MsVHcE8CqnWAdmKU8y8/WwvlcqoDL1jbwpMzWgQYV4TKDU3 + 0Rz0UWxD81M0OLHtv0EDaGghdea45bY4oMXn3AVjjuYeSCskZageofIpS0KdyVOcf8G5dnycHCYF + /07OfvyJSTwXSYdrGbRzloshyPGZE06oDe9k6wOAD1+wcTmZCY01lwn9g9ThpV6mwMZRhDMGhnOU + JNQYYDgXyfNlWhDIfydOHMPYkatgzFzvgkDi+hBLUjEVuhEIRewl8D6ArMqYCGaVzD02VU4uhRce + mzuub2YO8C5UwHsigDg2G6iW5qvYB9XMBQs1vZJkCRbxGWCXjSlMuI3P4eQDRJLMYIhBPjHbZz0w + MKTARuHO9QxfIofvX0t/cgUTj3EcoElQ9KqpouxH1heo9mSKGNvjdYeoIz6nGgItsmiRT/dLL1n4 + iPo5jOM926tCMPmoxBJN4awHUULpBnE7QytriCyI8lbEHJBNvEiFOCz1AHso85HpqFGRiRJkHvTO + oNkLo6LiexpFLpuj9conznCIksunK5wAVQCqA1ofPVQp9DCHyaC8DuMpyJHp0nedgsWUcQRjN6BF + ugfnMV0O4Iq1anGt9qh1NoamL9ESRGE6ZOYUGjBUCzO0LEFJBQxwBx7QvuA0VZSn1MyjKp1K81Kf + SQC4EckroMBlCDSO/cEssyVUQaMwe/GDN1mm1GCcODDBgCRqbMM4L6OYWwFXHti8Ez+ZAwJBqqY5 + UXzLJpQkmcUQNkwrmgXo2PXDJRUFaF5SjAPRCKiVQflNZ0wKjOPoChr6XKU0SBcBOZkyTqMV+PTy + wQDeuQkCaExZOWq+ou0DXXKVg+xCF/wzPLaQ/Gvpeb945H/++//huEU4neDaWXErhvLAEte1bDTo + R8WqTMIUrTEgJ6AzwDUSVwRTQPUUDnEK8jfJUbbuB9Hv0HFDLU4oZVXMpM2MG40waIDKubwMvAJB + FtYKa/Mczf2IEk4yj0D38PaYHccpGtYTznusR0nQASIMfGoMLJwU9EeItQvkS0EBwTHxXEotGSbd + GIREbtX8uCgQMBYOcfEGiwePLSYmlL3BJHXiKVJCUWggPhhTuVRsFkQt1SKUw/l0D0kY4bKoaDOw + cWErYP9QOk24FemwTTNnjM1yYzajqAK+6RytV/7rtfy+zvmqLxVXn7B6DLwLoHHuSqSvDwew1Mw2 + Fpahnx4O+Epf4Cv90RkL6vPXJfrush0Ib4znK6QP0sG/bQP/FEuNUDHhgYILoJwPa7B2O9f1BmQt + M0ZfOkjuL6jTirzzwYA9m4DIWPsiiu3PsVoioCsLpNY3x7SZ5B39+Tz68OY1hfilh0KvuJ1ZXM6X + 2ri4CJyxB2Wx540t7WXQVMmbguwcVJwlgd9QnJrng6Pvo+tno8DvVIvUvRQEWCr6yzngkf7tq1WU + IoOjb+HfcovPRsugft+x0g4KZ5BThaMQN1cByZySwhZ/8XOlmQ2XVH4ooVRLAPsHtFmlo+aXQlZP + y8m67PXefLWFoDLItc0TEo3bt5UmCh6iCgZu5Bt0UjpEFmSJJPQ3+olLqz/mH4Z1PyryfN8O/SvU + hPeTzDgE7TfHGiUAxk6A9gzoL2b1ow5ZLHFNhZtEa1GYmdDoxmGAoMHno5PZQamJqlvgHgU0YTwB + zWk0j5Mlg5rBWeqcwexkbvu1OQWqBxeUoBJQ6YABBmYGE9Z0NQJ24xzlDsLnx2AdxlckuobF7oLp + VxdxSTVLCq2gQZpGpY6dCd3QhIIrIY0E+AOEVDBfM9MZF9EM2TFbq5RRj6qR6UF0BlIDU6wjj97u + ddTpDECeF9+nymj2arc4Clw4JZnvRtWc8eVO8nLIK/yZlaL7coIlCWDkTNh5nqwIV9v9BbqqD/PE + DiFTYhauY2cxOPrhOsRR1Z7cXdfD00L8PANQ3hd3f3C3DvRXYHtdBpHTeAa+fJ+Kw48Jw4yeDtDW + QVXZ+ukEm6yKWu05155g+8kJp0tcv3yDRMb3TL88W45x1yT+qhOshqhJtwjqtx5znr/gS7s3oCmm + HtvJRLA7waqKunmLsJ6Bme5la4EXUbzoCp18mwSZYfAdLC2cGBYd9NhBAaEAMlDB36IY1jeZdzrp + RgyKaN3mEHK6PaebaMyzynQFQOx6H8grENodIdbtu4BYl6RdQZRF+zaR+jxwJlfvEJN4cM5Pl9zT + dB7jBiXSxNvjjvCadwMvYpIcu+/9BAi1K4j6LYL4jYdHlzZFFvn++xcd4dRvU7iei+QdWGR8U/44 + SaKJj0dVMo3ApACFfD2IbhJBFrXb5K/vneuYbp19FyXeYkZOggCoIewKYl83aepApEicROSvf/0r + 46mOwEm3SQFnpy/fkTOqCU6pjDo5f7UDlJJo1/JT7b2c7XfPszPDULtgbxNuKuOhHnqCAtaSuGx0 + 6Upnzvy8uDMsSV/ASmzi4HYLbv0QfquJoA+WnzVab07gVgo1HcWCk6xuqbDXkbcWS4s1e21bWZRC + 8rZcZugf1TIDQ+N2WWV8F40/jkXGu2i8Cskbkbz0QiCpeSf2eDFz/HjuhLfIyD85/njmvCfnTuj9 + 7HSEzvcuyTFzM6Bv4wfcJaarwY9xccTAPaG7Pui+59Byc/alH9NDzbcIPFc133jjsd8N03cA3F/8 + +Rhtjq5YvQvQvPc+HiG5cvydQBNO19vPJyEoDI8ecRiSb/ypg3umUby6zZWoH0dzfzLzyRv/l2UY + dRoDmv4LL8TD1ndBoq9jB2XVyQyMo0Xs+d2s4zsG9jsHtwXfLUFNgZ25jN1oMvuY4f2Lk84C3Fr9 + mx+ArSmcz6L5oqvReccw/28H9zpW5DuR/MWPr2ZR3A3cNw6ejETX+IsImJjJWiaJX/khHoMBoXP7 + eoMfn/7J92A4nQZw5oV4IiyXJfnxu3ceO1me3CLYx6Ebe9fkuTMFm6+b4OBw/3QqnKLvPI2dHP38 + fH1BFN7iEN46qZPM0FCdzaOl6zvd6OfHv5Bv8azuKT2NR04j+HfFx/E8O3T3cn1Noeslfw7HZ5/9 + PrYpCvv1xa37mrHJ0kal7A4jv0ZVvSuFIQLIqyhK82tSWIoeAsL9oihEkcQvuND9uQMa+0WQFEEy + aI1n9ESLK6yLZzBNEoDmkrXN3wUC/z33+RPHjKd4Ap47PhyUXqd42EtAlAjQiXM4eFX8mncppKsF + XnsqX8oelZpiP2hEBnq9+lniMeuBg1bdMS6iNwNbyAuReUAvepN5zC5859dHK83SrV5fqNxq3ug7 + wNuy+CeYYnG6AU0dMIFQv5Vd2rQtV0AeXWzufGfX2bNbaxxbYnbMlWON3lrLb7yPYyd0E9o8wkV/ + 0l8DWEP7jkAPLRwOXmCmJVx8v/cTP61SGZQN0psL1RJCNpnZJWXcGk79SSLMwfzDu26z+b+bivCz + fHnREj1sFmMUxtFyw4Z/4PyyErg3oviMp5uQasMEd17DySq/k7dwXLxMgn6NAzJxgsmX1D3xJ6Kq + ZISuiq82utz+85k/n5bA8efIzDUA4F1FHPMBLTFK3k//88M8GH6hvoBHAo9hcvgUyQKo4vr6WrxW + xSiejhRJkrDwU5gw7/p59OHwqUQkIhP56RfqCdTGj/CUXb/GbkbshPUIoAIkjt68HiGx4AMjURew + K4TetQhVOVnkIRJuGG4YZTdCqxPDcbEDAKNqY89GTd1sFET5Wn7jbDmbwRmJsSiyEmdWPDSySWxV + AcGrZQyOcUlogBkQvBt8wwuBcXFVS8U1UPGjR6/oPUSyxJN3B9VDI3l9pwwU7YjAissb4yWPNVhl + aYN0lRWiIuZNfJXf0B0QLkeyEnn8jFf5iy3ihgJM1kW5vKcGC0IHQr+oTujFCGGjx6r4WYOCMQaA + UC/GgRPC7xjrhVG0oFfSw4j1VINqjrDbERvwB+SGLov2puCopdgtLx+CKHn+evTj2YiyyygnpLUU + 6YKDraKkgJFuYGzIEt5So0TZBiGXLhsyhX6s5cD02udHjhgDEiCwJCVVPuTFKAvOgQP9Cgfy7zkD + nme/b+S/vGQX9ktLldbctwbjo2Q+SxaVJq29ZVp/F8yXUdD98l4JijtivQ0TgGvpihlQ1ZsbAYKy + ak/aLwgSD1ZAQRK1Ww3kpflSgN4Wp6El2fX8qrF15nkEyx/UrxgYNRXWB7XLg8IzGu1bJcbWhcGO + C4KdrH2/wfh8tPZ3kBKv3569fnnyDpmUGd18JguWd7RI78r07wLN7a4DynxUs96+kZ/G/lZ2yp1m + W/lqs1Q9gzWU25PTsmHfOaspv0dWY6qQ0fXz140MtjGVt8JpNwNzi/zVrF03XjS46HLv3jhKU4yu + MPPnBP12k2ix2qY684Y6eCP5unyt6BcrAWML0qMXlelpvLnC3MO1fNRgCa3jPTKQBWrerwc3Bi4b + HK1XFhwuWFfkEPqpN89WEyN6FR1jYr1mDzgpheiPld7X15doYD6X8OoCi6rYbAkGfjeYvA8jN7q+ + +BnmAWB7CXP4HT4y6KpXk/btKaE7FosLXZIGR4VjU7fTW+gkrvOvCxk7e0uf0Yxo1VfheBVQo4BC + h7hCGOGJox3BoMSO8Q1zYF5kb+4cpMs08RheXp2fndwPVvyrK8+/UHAv6S19JvB852C4DoiFl8d/ + J9rdo2DmhNOLxMMLZ9/CI6wpcAvyjoG4ipKFPzj6C/65884zeZAmH9b8cUG/reXD+dnf16zCDoHf + OaBOAgAdn/39zjteJoKLQWxwEUex8uMZeUlf3AcmmAJj9wZKauxOALhc4q27BPWVwJ+ZxnrFftwh + KOsgP16c1CrQ7F7FG1pkO2jlO7rrt7XGyR2aLMCSsGDxUx9H9mL9o5vp0pu1UgBnNIWhCPTO/uDo + G3hmZ/D7tiOKPUZ+wDr8MzVWr1OQmj/4we13nPgBBofngz2jvzr02g/BFwGCEos1QC/or3sGiMa3 + zgGiQbHvEyDgOHopPacZvIjHAh90I5n+QXOC5dwPl/MMrmP++16BCh0Q4E4gTJ0kg+ste0W+cZJ7 + BS32nAADr/CF1zv+k2TicMWge6gSfhmjZ4YJ+Pz5vuR7DsDIW8bCMsE8Qz+++4G8/OH774/f9S5j + 192N0wnr7rmfTvCA5u0I9cL40hkf3/m3LA/S3RH3GgqAQJjg/ZQXxy/vF4r5h3BwdHpy9sP9gTEd + L9iUnP7w49t7RocfxmjpvySv37Yj+16hWOGWGeZkQ+8nlworEHj83b2gBTjlwwQXzB6hoeUfpril + 8VWyFdR9iVkGBFjQ0VSgz2hBR9PAYxfC+5Z6vLu5h3551t0beL7Vzpz5L2HW2fHc+QXMwfbd9UO6 + HJI0CfJh8ygo9wMILpo5HLBGFe4aikXssTUzyNfs8Q67x/C+meOCL8zfwquepEjdq3KiHr6TkUbT + aVF+1J8toPWLZw7d2LmG2hhoD6YxzJOQNR44HB/h+YRKpCsa8lMI6IbinIZp49HOx0eb5zoEcvJh + EeC94HatkAlI5WmEgTMrqNlMHVQ9A1J/lKN8WqMgDUv7P3jSA9+whHJN20IFOqLVaXDINZVsbLXV + 0BWtUndEk29WAXFO6BExmOd5Mhoc/en/+38JSzLpHOWXB2hcAAxsVddFfquZ/qw9Dop3hX7xQjH0 + 0hHGYYVpwFByxfe0v2/m42/Jl6+hhB+mX2HA7YBFC09oaMT4veeKlUG/86aAD55pAv8X05i7NKCY + n9DMTDSyJQ1xgaHAJhNvkdKAYlAmWsZ/DMfJ4uubUdkBiQ3b7fSrEF0KOBJcLZZ2+s/xK8J0xr8e + Vd/UHFQqUTkF1cEggXSqarlsx4HBhLx3wMJa0Bs8TQOslioO75R943eAUJwWf4sbe72oP6kga8NO + N/BNJ46hT2wErdHj+gm0A+vryvHJl4X39Sgr1Dxal948WbIt3OLdjBEZJ0l8Fk2uPMzj4idC4yEV + 0s1tjLnc0lFD7Y8RB+hhwagKAtjUhW+jypkdXiqj9QY05I1xdjmq1PsYEeC4NOk1xp1mVXGNU6aF + dQk2DlqigRDqWjuqa+FOcOF9wPDB/Jh1FRvhKp3TMz1zx61I7jfwBmOLvP3Hi6aRYiW0A8PVpPlk + c6GdPsfLzMd/4b3DhIxXLbCB58mFtsip0/Fl/Lzin5qQU6zajB1StRba4ye/g0iRxLKuD46Y4m+P + WIYHeix3snmWpZti5dKENVURHvQl+ZHuXWXPbca7YQqs1SPNYj1xcgXNbKo6S7wSd3fLGSv2k68g + Cu+f8fNP2HjlwiheToX/o//Anyf0n2IGSExdzZwn6bsIzDXMYc1vD45oWCA82+l6LK0gFr72xqfL + ZHbmpSnNaHLIEhSC1ethnNnzaOFPDsgA11ECJpcWoMJgiEX8BNa16btleMBSEeK7JzQ74TohYZbi + 20lW4eRwQP/ww4CjFyzb5+iMFklG0DDtYeSHfirO/VD8Ofnz+0M0pSVFsmVFtii+edM73b8tQTMg + 7PQZ3rce/ey8d9jnDL6Ew7WkYdBBhIry2LUmBiYhVHTXtcaSokkAJOfKIHJcgRrS3nQFrEmXGgsn + Rg3diJGWMLzH/BRxIpqeZeuXsnFpSIYpy4Y68by7gQCvRou648q2bCuebiiX7sSRPcns2v0ToG96 + Hf3ZaJbOg6P/HzaEA+q8RQUA headers: Accept-Ranges: - bytes Age: - - '903' + - '62' Cache-Control: - public, max-age=120 Connection: @@ -1124,24 +1536,24 @@ interactions: Content-Encoding: - gzip Content-Length: - - '50744' + - '70983' Content-Type: - text/html; charset=utf-8 Date: - - Wed, 08 Feb 2023 14:33:27 GMT + - Thu, 09 Feb 2023 11:19:38 GMT ETag: - - W/"4572b-wWEyhwy8Rsegva6I5lXBhE7JryA" + - W/"545bc-BrSLN/o4h6f6QgzXBR2obTA3V3k" Last-Modified: - - Wed, 08 Feb 2023 14:18:24 GMT + - Thu, 09 Feb 2023 11:18:36 GMT Vary: - Accept-Encoding, Authorization, Fastly-SSL, X-Device, X-Continent-Code, X-Piano-Disabled, X-User-Logged-In, X-User-Has-Access Via: - 1.1 varnish, 1.1 varnish X-Cache: - - HIT, HIT + - HIT, MISS X-Cache-Hits: - - 1, 1 + - 2, 0 X-Country-Code: - PT X-Forwarded-For: @@ -1151,9 +1563,9 @@ interactions: X-Powered-By: - Fenrir X-Served-By: - - cache-iad-kcgs7200148-IAD, cache-lis1490035-LIS + - cache-iad-kcgs7200148-IAD, cache-lis1490025-LIS X-Timer: - - S1675866808.807115,VS0,VE1 + - S1675941579.832234,VS0,VE122 status: code: 200 message: OK From 7dce18008b217df804a3a264c54d4a05d7591727 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 11:21:00 +0000 Subject: [PATCH 221/230] rewrite expected --- .../test_session_controller/test_display_welcome_message.txt | 3 +++ .../txt/test_session_controller/test_get_user_input.txt | 2 +- .../test_session_controller/test_get_user_input_with_save.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt index 42f658c8f03..07014fb7a25 100644 --- a/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt @@ -17,3 +17,6 @@ Investment research for Everyone, Anywhere. [/menu] + +Register : [cmds]https://my.openbb.co/register[/cmds] +Ask support : [cmds]https://openbb.co/support[/cmds] diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt index 5b74d9dff68..0c17b3a6f94 100644 --- a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input.txt @@ -1,2 +1,2 @@ [info] -Please enter your credentials:[/info] +Please enter your credentials or press enter for guest mode:[/info] diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt index 5b74d9dff68..0c17b3a6f94 100644 --- a/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_get_user_input_with_save.txt @@ -1,2 +1,2 @@ [info] -Please enter your credentials:[/info] +Please enter your credentials or press enter for guest mode:[/info] From 418b7aae26285aff2fc1b78dc4e3173f36eaad0e Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 11:43:33 +0000 Subject: [PATCH 222/230] rewrite call --- .../test_price_target_from_analysts_raw.yaml | 3092 ++++++++++------- .../test_price_target_from_analysts_raw.txt | 2 +- 2 files changed, 1756 insertions(+), 1338 deletions(-) diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_raw.yaml b/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_raw.yaml index 886353a6b51..bad99be7d57 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_raw.yaml +++ b/tests/openbb_terminal/stocks/fundamental_analysis/cassettes/test_business_insider_view/test_price_target_from_analysts_raw.yaml @@ -9,447 +9,505 @@ interactions: Connection: - keep-alive method: GET - uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?period1=1580792400&period2=1675832400&interval=1d&includePrePost=False&events=div%2Csplits%2CcapitalGains + uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?range=1d&interval=1d + response: + body: + string: '{"chart":{"result":[{"meta":{"currency":"USD","symbol":"TSLA","exchangeName":"NMS","instrumentType":"EQUITY","firstTradeDate":1277818200,"regularMarketTime":1675890004,"gmtoffset":-18000,"timezone":"EST","exchangeTimezoneName":"America/New_York","regularMarketPrice":201.29,"chartPreviousClose":196.81,"priceHint":2,"currentTradingPeriod":{"pre":{"timezone":"EST","start":1675933200,"end":1675953000,"gmtoffset":-18000},"regular":{"timezone":"EST","start":1675953000,"end":1675976400,"gmtoffset":-18000},"post":{"timezone":"EST","start":1675976400,"end":1675990800,"gmtoffset":-18000}},"dataGranularity":"1d","range":"1d","validRanges":["1d","5d","1mo","3mo","6mo","1y","2y","5y","10y","ytd","max"]},"timestamp":[1675866600],"indicators":{"quote":[{"low":[194.30999755859375],"high":[203.0],"volume":[180154500],"close":[201.2899932861328],"open":[196.10000610351562]}],"adjclose":[{"adjclose":[201.2899932861328]}]}}],"error":null}}' + headers: + Age: + - '0' + 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: + - '933' + content-type: + - application/json;charset=utf-8 + date: + - Thu, 09 Feb 2023 11:43:18 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: + - ff4eae5c-784c-4627-9600-35ab016b4be5 + x-yahoo-request-id: + - 2da83ohhu9n2m + y-rid: + - 2da83ohhu9n2m + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://query2.finance.yahoo.com/v8/finance/chart/TSLA?period1=1580878800&period2=1675918800&interval=1d&includePrePost=False&events=div%2Csplits%2CcapitalGains response: body: string: !!binary | - H4sIAAAAAAAAAIW9W68uyZEd9l/O8/Z23i98EyxDMmANxhrqQSAaRpt9htNWX+ju5kg0wf/utSIz - 965Y+RlDAqfr1KmvKi9xz1gRf/vyx3/59pffvvzub19++frrX37A1R/+9uXHr799y1t//Msvv3z9 - 6Y9//fK7L//ln/79l7cvv/71x//r5x/w19//0//+7/D3r/8DP//pT1//4dsfv+LuP/ynf8LN73/6 - 9bdf/vLj159++/1f/8zb/+v/8V/+t9//V/zLP3//y6+//f6Xb7/7+u+//Q3/ElPvI44Uwhs+/6e/ - /PDtL//p21/+29fffv893xdbr6O1kebblz/9+NvP//zPv37FAP+nOAJ/8Rse+n9//sm+8E+/f4zm - 9/sf9qj+3Y9ff/n+j9/+z//w9b//n//151/+2xf52D/iX/m1Wd5nf1sL8o+/fP3X73/+y6//yw8/ - /4p/q+kdH/wzH/yP3/+EMaS3vTg2ne9/+tM/4hs/f8dF+/MvX/mfa3S//mYLbZMqbXAGX3/67mOW - jTeuWf79Y6z/5jv3Kz7fOUP4/3vnn3/+9bd/84X79x8vnKGU1y/EG7/79rdv/8Mv3/7EoX7/Gykm - fseF5n7gL7j8129/+P67/8y//woiW/9c+Uf88Wf8me3PZn/Gv+KPxD8q/4iBf/71Nz7847f/48s3 - f1+bj8H++Ge8K9YRRrLp83LGHM7lnGVdxjDq2Je51P1ALDmeB2rs54EWSjqXM+z3ppDbvptiSudu - CjOcy1n3y1Kt5TzQSjwP9NTPAxjF/lrC7/d7c6xnvDmVM96cczoPlDj2e3MNZxa5tzPIPEo/d2fO - +2UlxLlfhsGcWZTcznhLqfE8UPM4D7R0ZlF6PLMos5/xYghn1SuWcj9QsRD7vTXHM4ta+xkvfnRW - vfZyVp2zO++d6UyoxX6G3rDF+4GW69mAVvLZgNbHGW8b/Yy3zXpWvYdyVr2DSPbLeh5nvBjNGW+v - 7ax6b+Wseu/5zKKDtvYDIL8zXlDM2QBQwdkA7OGZBSZ3xotRng3Ai88G4ImzARjXmRBW5Ax9gmL2 - AxNbfx4o7WwAyPBMCF89Q5+4fx6YY28AhtL2BswQ654QOH3Oc5cLvy9b36s+A8XBuowh7vFO0Mge - Ly5nPA9Ayu+vxdz2LGZsKZ8HepjngT7reWCMdN47257FTCmd8Sbw7n4g4efngTL2BsxU+96AmcBQ - 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 - stQ8NJr68qBZMEyTlOQVPXVgWTgp+zau/WFPTS+P2HdTLBR7k0+eGUlPJnhLB8AulX5Nilbae3nn - xat9NynWxdPDtl60NzB7l0juIMNO1W1Ku4/t2rXctUlVzWi9INxs61VSOV7RBOZTyYvuKujM48z8 - /kffaiYzGtz6s9EyBdt0z8S0qsiyJEYxQEBkGVTZOL5JRsAMVHlVVh5j0qWmtMWsOdqsGNl9eUie - q11zkXLmVt5R3kTGD27G+eoHxJKVV45n90QfVw7s1XK3r9RILXrKeOddaVNLgPZ+lVSl0yK1Tfs9 - 0XGnf7Ilp1YYnVe76Fc1S5sogm/+/s3fieD6+ssvP//y5Xc//eWHH/7+9/8Plfdewl9HAQA= + H4sIAAAAAAAAAIW9W88tyZEd9l/O8+ftvF/4JliGbcAajDXUg0AQRpvs4bTVF7q7ORJN8L9rrcjM + 81Ws3MLMAIfV9dWuykvcM1bE37784V+++fnXL7/525efv/3lL9/j6nd/+/LDt79+w1t/+MvPP3/7 + 4x/++uU3X/7TP/37Lx9ffvnrD//PT9/jP3/7T//nv8N/f/vf8PMf//TtP3zzw7e4+w//4Z9w87sf + f/n157/88O2Pv/72r3/m7f/1//pP/8dv/zP+8s/f/fzLr7/9+Zs/fvvvv/kVf4mp9xFHCuEDn//T + X77/5uf/8M3P/+XbX3/7Hd8XW69jhhDKx5c//fDrT//8z798iwH+T3EE/uJXPPT///SjfeGffvsY + zW/3H/ao/t0P3/783R+++Z//4dv/+n//559+/i9f5GP/iL/iuRTiK82PtSD/+PO3//rdT3/55X/5 + /qdf8Lc6XzH3jy9/5qP/+3c/YhTpYy+PTei7H//0j/jKT3/ksv3552/5P/f4fvzjmtSs2Wbwy6+2 + 9nYrZ1uGa55//zraf+OdvRV95/7Mu3f++adffv03XjjD0Bfub9wvxBv/+M2v3/xvP3/zI4f63a+k + mfhHLjV3BP+By3/95vvv/vgf+d+/gMzWnyv/iT/8hH+z/dvs3/hX/JP4T+U/MfDfv/7Kh3/45r99 + +f3f1/ZjbD/8Ge+KdYQZbbZ2OWdZlzGMOvZlLnU/EEuO54Ea+3mghZLO5QxtXaaQ276bYkrnbgoz + nMtZ98tSreU80Eo8D/TUzwMYxf5awu/3e3OsZ7w5lTPeDIo4D5Q49ntzDWcWubczyDxKP3dBSftl + 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 + /t8BI8NEolVHAQA= headers: Age: - '0' @@ -476,7 +534,7 @@ interactions: content-type: - application/json;charset=utf-8 date: - - Wed, 08 Feb 2023 14:33:51 GMT + - Thu, 09 Feb 2023 11:43:18 GMT server: - ATS vary: @@ -484,13 +542,13 @@ interactions: x-envoy-decorator-operation: - finance-chart-api--mtls-production-ir2.finance-k8s.svc.yahoo.local:4080/* x-envoy-upstream-service-time: - - '11' + - '9' x-request-id: - - 6b1b6079-113d-494f-8002-437c5f87bf1a + - b6499351-ffd1-477f-9d43-e6537efa6a1e x-yahoo-request-id: - - 6g1akjthu7cmf + - 6qdmhrdhu9n2m y-rid: - - 6g1akjthu7cmf + - 6qdmhrdhu9n2m status: code: 200 message: OK @@ -508,897 +566,1257 @@ interactions: response: body: string: !!binary | - H4sIAAAAAAAAA+y9e2MTR5YH+nf4FB3tbjA37nZ3VT8hTi4Igp0VhAHizJDNZdtS2+7Qkhy1ZGMy - fPd7fqce3ZIlvzDg2SG7g1Vdr1Onqs6rTp367uuHP3df/uPZI+dgOqy+/w7/OlU+2t/sFKMOpYt8 - 8P2t7752XefFYVEM+rPJUbHu9H75u0Nfpvlk6rguFaj7k/Jw6gzyae5W43zg1tNJPi32TzY7g/Fw - Qq2cdL6/RdU292aj/rQcj9bu/HWUT5xiszMbDYq9clQMOl9vTk8Oi/Ee2v/mm6NxOXD8rzc3KeXt - 50/yyZv6h9bvu7/9vj66ePUir2eTwragk2hkutmhj69rDKezPtk8LkeD8bF3WEz2xpNhPuoX6/nq - ftDeSDU7qu8+pFF7o/HxD+bH2p27346KYwfpe2bwzoyGX+6tTdS/KHdnUkxnk5EzUZXu8ffjYvdN - OX26IndYr8wZvzud9V4l184E8Y6bv7dQjtdGd/7Sjdh5K9bpIyFmrdwceVUx2p8euMG98vtN/17p - umpWp5uj38rfAUuxubk59Ub5sDDQTDUc79dG663F0MLGfjF9NJpOyqJ+cPKSMN2MYzFnrTOkldCZ - w9bjlbWX55s2NFhO8X7tjk1Mvvlm4k3LYTnab37RcI7K/RyA8xb45pu1fHNV5p31v9D+XTvU0dxY - kddAiBQVaA/nyVyB5hsVe194h7P6YO0voPfuaL2gYfGQ7qohrfOKfllSJq239cFswmDd9d/feb8+ - VBvgbmtey/WcoVI7hyau/Oab8dr0Do2v3KSBWJhVVQNV+UP+g/2om7k792EuOT+6+aYWPuvG3mNJ - 7W366/XmTK3wUi2z4eaYfuLD8M7e5tCz471XVHUBYL9eWzqDtDjv6D7v7W22vrqrpvH9e2otV90e - U7c5d3t8p948vkS3edNtvdn6eka3o/YcF3NzrJDUnua9ZpJrdw/TbGhlsd6ieaP3tMbXFcXaPJMc - cKG8v0lUEr/6w0FDwIs7f6lcBWFBvSFdjsppm8rremu/dZDT+V0Xq4vRYHkx5Nhi+WBAcOSb8+Sn - KazzO+v0nercuwUm41X5blER66Gcw3y/6NxzNjacg/GwQGrdqafjyQn9qcpBUR+Mj9edI/o1XncO - ZruqRDHte7foP4f+a4Gxdrsup8VTmorb685tYLaY1s72qKbqk9t37t36bkNxwu9vXYwnOvWkv9k5 - mE4P67sbG/3ByKstm/X64+HGH/VGNXvr/VH/UA42ZUr/F4mw4+T1yYgqdhziScWEfnzf9LyCWz8a - DZhXX5RZMx0f/1hO6unz4s9ZUU+dTef29qgiHui84CYw9HJqJIHb93Sd+4NeuTvJJydLKtwfOCaz - R10Xg1btW5bx9AmIacFkDpQMCL/j/EWzgQ7Al3lRU/OnWfU9XYqw1uQTBu5hMss9Z62p/s03TVte - vyryyZLvTKC5b2dp6QbAewtlhnPgI/e9AwLBUAA86gdz2+7BpBfq3Xp/61YLJ/PTQkXm8iz6r7Ie - IdlsdqbF2+nGH/lRrurQanCcr+3srBG0jOOCMLzk67qDpdJ5/Xra38sPy964n9OG66w7OX0mSuKM - 7Mzcc2jenLV7zugeqhNto3+Bn5G3N6Hh179Nf0cGehrdc3YJyjeExvcONdk/cNawLCjFNRziWGbC - p+PDO6r0Pe6NqGg+IdqJKXD++U9nrRlNYQHPueBg3J8Nqei6M6H011+3ILnHHX09ucN/c293PDgx - lUsqnHtqIh5VBVpY65RcleQTpyQucVLRnq7rl4Rc4GdQ1odVfnJ3NB4REacSIPGUQT2rpr388JBo - YfegrAbE58zyqQsm9uPZlIihE1Hjmnt+PXFA12kMBvWn5oex3ZqkfLLPg621JKfmBtT//mSSnxDz - V2jwqRPnO2dKf7799s7ot8nv7cqU1KgxEuEdA1OuMoioTx8PDif3Dw8rkrw6PFej33yaXFPF+d6R - 2BKC82i66mKbcDj6LaAVE/h3kNfZHY9p541Q39FC+Og3+Tvy1niNUGLd6ZhBz5cTXA5/GR7C+df+ - HUKf3ZSdQ2LBbdiaqf3L2W/Avwv09YeHioLddb4OOEl0bDqjzE49ne12nPf3LgBIM625YqMj+oAp - JI7z6IiQ2yvraTEqJiSmFnUNZrburDWTWhggeUnRjtYjMJ0VHva72np/EURqhyE1dX5wfnrx81OP - Ub2mCt5x7uoqp/YYOhnxIjerq5tXFbYXjUVvO5ND25eYF9HjAYZyVExqAnYObgJpMo9fU/c5L5y7 - 9EUtoZ28mhV3HapQz/p9wsFdZ0Lopr63CfXUEf8CUaDR8UJAezwyhY5y74TQTAu58OrxbNInqXlc - k4jJ2Fwrabn8Px1COm8cwgVtwmkxUR++Du7c4XlslD/na4vb4XgwqwpCo/rhFW8Px5Mp2FIBNNKG - o+3YpsEX5LyORWd9+Bq46fTHo71yv3P3r07e749no+n2oHM3CPx0vbOb1wWx9sNxOZp27s6JEoeT - 8ijvn7jD/eEU89FZ7xxOxsSgpidbk2Kvc3c0q6r1DnFf0qsIUc8w+Bq9YIDU2OOHz5533r9/f+kx - NPLJKflmEaiN4wnI3ETNB0Hxazk9INr2kGaB1wrJPm3p5tatr1jAeVLQGn2Z79cs1Hx167tpOa2K - 71++6N0ngWLcf+P803laHNf05+Wj5uOzSdkvnJfjQX5COfdHeXVCgs3PtPGpKxReEOq+21Dt3vpu - iA77B9gs083ObLrnph3zGZR7s3NUFsdYAR2H5os2LRU7LgfTg81BcUS9upwgQk8yU5lXbk0rt9gM - FhqZHhDvcPvjijhm085/7PF/i2UBWqvUdQ/edmeWzWZnvH/3M/Q6PS6ntCk/RddffdXGMOkHvO6Y - hM91e9d5XEwdmi2notVPrb4s6iqHakH9HnK/RP5IPp/mZUVibjliCZWpXznqV7MBrXWHB0Dstibd - o4RaUtKi4FVGQKE+7aeKVMNCNVl7t1ogzs3KjYZUL1c9izcS1FvLkFoOwXAbGEHIiI4N1arxdmc1 - 8YSa/uHVw+RsG1XqjR/zfkHCypvtPijY4f7Czn02263K+qBo7/FV20DV6I4PTybl/kGbuhBHykfv - ipE3Iqw9Hu5uLdR5Pt4dT+tWhe2nDx/9ff3Hn3u9n3+1ZTEol1SJ8miz83f3l/tudzw8JJzuzm21 - 7UebxYCQMd8DtGWXhO2y3yr6owZraVlwlqbof5OUUEzelUVFjHRLK+gL9XKa1YI0u1a1+1R8kfgW - E2L07lHQKvZzsl1tJX/Ly41pzy9/DCZb/bfip5/Sw297P22/+mPvxbcn1Yven/nbYbi50Nz+eLxf - FS7UfZebpjW0sGCHr345FNu7D/8cvHxwMMlfP/Jf/fnz4/Dpr/7j2f0H7148+Tkb9u///eFP2aNF - TNByLqZnNh4Xe/24yAtfZrII/DiPB8FAJgnx/1QWywkz2nuNHtpArl6qSxrZ272LCXhdDlpNBHGc - xUKkoZRRJMJ4db32UhN+GMYiCv3EX8FG5lcCbc6yX60Y2WxSLezC+pxtyCSj3pgS+XD5NxquytEb - h3nI2YghAlFtkoqQT/oHRiXOIfqradogyEYqt0XKvn07JCAPSLDa7LRKePgMsUX1zi2jLWybWf/A - LfuY97p8V9SE6tR/S/+zzSwW9A5H+3Yg3JSqrkBkarWBIqb+Xn6EAq4Ub6XgyqYn/nKVpoL4bRDP - NcVf5psiyb/cI/pta5sPJMyNR4tl6zcaC7pwne/lk9IlLk0CtzvNd736aB/zTzIRiUJ+EKZpxiRb - y4LPJgVUMSsKNo0f0r+UY5vuqjW0Qbr3dGOPEvVGL999PBkTt/lzVjyo8v4b98Vsl2Q873i8tycg - ytL6ppIGNfi9ofP6k3FdE38huRU2uA/peVwNPkvHz4v9WZVPPlnfz16+yEe1+6m7fVkWQ1L7YHv5 - rH1/0nle7Hh7mhMN+yzdf+KumVhySz8cbSZJFiR+5scfp8dSSXyGkbiak5ByvD9WlAu9KlHy0q2a - xki7Jxmp3IcgdL0tX0tjmpueM/Y5JjggKkBN7xVTMFnd9sYh8f23J945Yst5rSiovLZ8fJFqx8fH - nhb8cuiKJJHUF+2yqTvN9+ticsSaxVUqE68k6C88VhhV3kzeDi46yN0ix+a4TJUZjWcIY+Clav1R - uzSO0ZTKH1Oxsn+ZERHXH4+r/KJVYLj1WMOj4U0vCmE9JXmu36qneltY/IStUdGfzsN3yra2uGWW - 1pqWo5PDvF6yLpbVOJhWu+Xg4q2TvDycTUt1cnh+lQol6+INbI2XGMLcxCj56wWONkibLaZzEtgy - esGnILVbE8rpj1Lka5yKEJ0WvpC+8NMgItFUy+C25Y4R3OfOqKjiPNhzFebLaXiM9tBgd+NIGgLW - ViFMgVYXekT9vH9QoM0NXjrlnzSsty7//uChAJ/Tg7ImcZeY5whnVvuw/+fTYuCMR3cdNO36wvVj - RrbjrBi9Eab50ykCzRMwcG0ntRfsZn6YJkWQJaS4ZXkS9QMevDMsBmVOWkulFBnaH6OxMcd+9R13 - AFPPfxyW+WhMPeDA19XamqvNu85fVOIrc+Tl4MzL+bocwlyaj6b3KPM9yfNfeaqNvUlRDMvZcKHW - bgU70JJq813bc/3LdP7Vdxt6JN9ttIanV/hP+VGujrD1CkeGsYSrI3j+ow3eds3rY++NP0jSnpy4 - gRcEnvSGJazai8ukMXJfvmWvyt8RMcrraTFpf/oIXbmz8mMMgLW7cxu+dZmWnzx8MXqMr3vl/sdo - mZrc6FclZdw/PB8naudcdnt/yKm9Uw5Iwi1Gk3LiKjhdNNM+YfqRcxfOmGj3jKblXknjv9sZlp31 - TkMmXo6haaPUIkXJZ9MDl2hKOQHPoAK03/LdqujRMqR2ppNZ0Xm/fqoa7LNEiIgp0leuxog/o8Js - d5QfuQqIZX1QHdr8s2rwEC3pKaPcvbyqCxoMAMUAtc2NWoF5qTk600dsS2XP9Y4Z4X1qZUld+rrU - HoUM0i3eFT8Q3yNc1sVrnrT+eFB8oybnNU3X69f3f3m55XZ724+evnS3H75+/Y3p8PVsUlL+80cP - t58/6r50f3m+TbkQYQr6/MvT7b/98kjV0HybBkcZ3Z+fPqXi2z8/dZ/ef/IIVfpj6hgWqnLwX8In - 5QdU83XOh6s0wjcFcBm8GR/NRPRn+Lcg+PFt8DYYbv/3yL//+Me8jnZeiBxTo+DGWSQhdy/Ks8B3 - w7gfuGGyR1MaZLkrhZ8EeRhEYbLL2OvnhySD5UAd7JX/zZ3Fvb4M//sfr+7jv94kDP6WB8+fyv/2 - 4xeTw7+93J08Fa9/fTk4oBZ0pe3RUVmziRq1i8d74m8HXPvB7MnJ4dP+i2GS/xJnfwRvHp/88uzd - ox9Fz8fc9yEf3af1+VtH+Wq4erpdyGXH48mAoeRVedYnl5arS4LtfJl995i4pHtWy2cXcfNdnHvw - lpscYq3XZ7R2XOzuVcTmVpegpVDPhmrXUEsHRf/NeDZ1Tw3wd+yasn5ByP1xPDnOJ4Ni0Gyo1vnv - xY6w11b4CmEH3uNkv+UCZNwXjOfNvcZpZcL12bWkb/xwOq9fz+rDlmPR76bMdM4jh50c+Ms9C8F0 - tYdOcY6HDkpoD51TENxz8jlnncJ6dfzVdteZwF2H3ZfeY2StcR7OOSFZx5p7junJw/dW4p//dH4z - Tje5dboxPhPtki2ns/w3/3f2blGOLiifwwllzrnlcTXepRV6sujkwpMH3xQsitb4mq6UA8tvv3sk - gPULIKQ6WcvvqBHboVZrUzNWMB7tSzFVTicMnPZiMc4qBiuTeYeVqXVYUb/uaaes168JaLimmF6M - 04r+fM/pewbmeT8V6/yx7sw5qlgfm752VKGWLuClUqz0Ulnmh2JH1zit9DG4vvVPafu95conR3m+ - aRSaQTlfA4nW8wglmxHTCA5bq2pY72/R6KsCU8GoOcvvqFpXq4BBuUML+AquLXPOLf3+ITsm8Xf8 - p4SQu60v+G84rB+Oh3lJ+G4OfxRUXn1Iiv1YGSXW52qh8Z/ZnNeqho+uNs+cUde62VBV+NksZO8X - 04ej+km9/2RIE40NsVC9Os5P6oeKfFBJvXvmCy343SwOmovA0dvpdLvP7nfmMt/fmv/1/ro8dAg9 - bbSoD4seOLYL3Qj/6ZwpOSvlX8lii9r/2VLzd1YX+wp7kA+XB+Of6kckAUzGI9BJ3pbOjnIzezob - 7tJyJrQltGS7s8mkGPVPXhTY3USrkbFOGc+L/eIt8Tp4udPmLQfKi+Cus/H//c/gr2BdvP+fjfaP - 8P1/bjS1HtFqrBaq/Za77+67r3w3817/17fu79/+v60vlPwfT6d//0usC5/ao2lTgyrrh8VRUY0P - MZonJBLCc/pHLJnbTBJvv6QldvueKU379emYJPQT3juk2sKZ4a5zO/BvryMJ1Z+SpMeikPpWEsIg - KtOK1i1D7V3mIUyY1m7WypqkFCh9r6DWXdrcZ7QJ+T6Cc/vP2XhacG+HzUctvvPn6fjwhRJMKUMt - Av6upfgtNrjYvQKY4RKgP9wCtAo0bYOaA0zDpbNaUI1J2p+o7nVelfeZ+28PaNP99vuyLJtxyZGc - hvjUviT0bmw4xhLoVNobvuXKUt/6StNI7N6X5YgLGvHp1i2qrnTGymEvlrmq+EJKwfAQS6i9qYiM - +YmgRTSb9iGQ/Ly3B9FkfL852EYVN6IiZd36isIPTn6pmUfotaOgeEL8a6csjovB9ohojJJZbmnY - h8syHzKTd27/5XQejUj3YW9VcNHO6yl+dHrU5Siweeu6XDGhfqvdvP9mvvg2UZfJKK90oQc09B/L - aVOWxJXOw+ICDehCpxvgJl6Q7jZ9yp4VVIedkgDaL897/GGJt8E6hlJP2QENZdjVkij5QT7aL+7v - 1uNqNj2d8awgsWA0ZacjmwUK1UpuN9o5+t7ZeRL+/PCRePLj9tsnDx/56PnFyXB3XClYX/Tu41Mz - B1jN2w+RKZ33eiJpU5XYgTU7iBO7rWf7+4QKkmaKwtEJVzs0/NFM8ZAw8CCvWSbeyP/I32684DKg - +pNxRWLF6xeqcufeXCW4RWFJW8az5MTGHqy9JoBUKxtNM82AsNefk3hdaNr042wEtr1BEtCg3tCc - +9HLH/GtmO7ZTy+wbVuzpz8/GKvqu+NW9e3RoHiLryV+mK9dkh1pnU5PkNPXCRKhm1q0uAjs5zSD - KAIubDMfFjhCAAlFloXzbZ9XgqnSV/yr1egzrlawk9aIez5UX9iYS582OreY8lhJa37usYxrK3Rp - ZOFPPYcq+rdeQBT7VNbzaMKfegFJ9BeHX0uQ1G1wtBxFJukgXS/DVJOol6OsazG2CmH6g6O+oCDu - 2MyzwqUW7t9YpKrL4WFV7OaT3xXzHR8VE6jhJP3PpuNzrMmO8V/W7EPtOOWsSFJa/w0cGOGj2M9J - 64TjovZq+apl5rd5i2cal/WIYkP2rbPvBkE1dPdw38glEWqqpHhs36m6pPHTMeSv6fhNQfzje72s - iKa8hJXTaCE1Ad4vMGns3VkwuR44P/36UtdUbp346pRTj9tQfcC/PQ5/mVSmExJPq3K6dtu7fee3 - 4Pd7p4pSOVvHmxSHYOlrG+7G/rpz+9vbd5pPr/nTxu077Tbgn/QsPwEOYJFgiH55vt01BtA1kiF3 - 11QHdwwo1OowP1zrY/z/+1//+dfa//r+f/6lDha71MD96Zp/x5uOX7BqtxbEd97/7x2lJa+5ghN/ - jMsRGrpzT1061Hp8S99tQaYuhXFJBTUpyo8LyDME85qyqD6bjPfKilQ1Oyd6ztR3c0VCtT1fpY0P - o5PctoRaLakNnovbdx1V9SVS66vKwvhk3OS2B6g092FlvWEBUZ5r6E/bA6XuQI1V0LaB1YvQrsu1 - FnB6VBqvZlh8bgQtTy+t2a4BZjmIC2Dw3Tw7C9g9vfyE5aTf/uoY0bFzt7mIut45tJ6/d095/K7D - i3XKG/tup9bUtgMrAS1B+2lOzOjkuy/BXu92Ou9/b62IuRmFbj+mVomAT6Azk9q5PS2Ga7c1DtUW - gClhYe20F85fBlnri7hpsMITYxfj0mXFVzBVQ3fsNFjM/eb/7ulczKP6dc8iWwM51/2KVuYLUWPz - HxbbtENY0VwzxM1muPfmV4Fmt+1lYH9f+CrmUnJr7alrx+uD9Xq9Wi/vGDCPf6t+38Q///znb7/f - ww99Y/z2/lRfiaf9Yy50r93B7EOkX7uzXsDQc5fL/VHffq9nSN2f3dsccLgFZShFrIR8H6LDWn2H - 8NEu+QeVnLeq1nMtDarN6uvN2xYVt3+4/U21efvb6u7t26bcH54yJcCU0nyDCaGhCqucZDYU/Lgg - ffvb8ttB9W3T7p6+9/mUqDDmkLbXg4I4LpHT9T1FRu+sqXlbtxc/b6vpub3egnn99uOXT9ynL+Lw - 8c7tC1/quuRkXmgqLzGTF53HVbN46+JTeHoC1fRdfvZU7WueuCfP4h/DuLcwc41k5jjfqZOAeXPY - SqBHtYdQOYC6WRfWr4M+kWRW4LbGZsfvOOoWGH6xULg5d7Bwjw+yygoS8kE5GPC9CYJnQwE0L0Fe - I8waJZ8CaMq47Cax1BSiMe76acvBX6zYNHN4paZbV9lxD3odZvvRurPPN81/G+FuM//hQ5Z1/q03 - otOxO7Fz11mzUXNaW9FRe1GV/KPu0EJV509DPn9auhdz7EV103rxhCrH/W61uXAd3adS2Fht3fkC - W4sn/MHLZ3//e2fdGa7cWxPKxK1VvbGc5i58R/sU0C+7tzp35ucCifby/NAFquC90Jpknx5Cc7Mq - ndaybC9M/G7vpw0d4grnhMaf7WFBDRWDXr5bw6p84vRgqnf4apfT6z5ztkhRU2rZd+VwXw0PaFEX - xjbqo33cBrmn1IT1Zw+zcPfX4/3BcOekL6qj3T/88smL8Hi7fFDtDp8evXpczV6988udneePey/3 - y2fvwrQvX422twaHr7aej5+92I5+fvkm+vnvD0KqM371azXKt/6Wbf/xhr/3t/bL7a1Xh6/+Puju - yn36fn//Sfe+qvPC/N1e7P8ttUVwvZ31qe9867nffzg+6smBHJxE8slJdNQf9o+e/HH/+Ek3ezcY - 9qmPg+nu4+jdz6ODOv81mjx78dN4sPX8+OcyPaJasjfqv+sNs5NXJ+lb9NmTqtz2H2H66uTBu8HW - T0e5+CXbHkZHu8Nfyu3Hrw53Hx8vpqe78sHBP0TlFy8Jzu72t8+2fqr+If+2Xzzk9L7+vj8Q1ZvB - 432Lh2W4Qd89yfW/3X54fPTqXbj/TI3n246TV1McgehlleG/Zr3ppF5pfMG6mLi8u2uz4Eg2L5UV - ONc2vXukShzeRcCEqtib8g9u/q6TZUfH93TrnDq45wzzt+5cNj7MFemwP2WzYue21CffTMsJvLVs - /x8hrJ+XpDYE9atbX33FNpqvvhuUR06/yusadLQiJjcc09wUr2tSF9+cvK7Gx7AHMw/k/E3lu6G+ - zBCSyWibrtY2N5Q5b8PeulRlDyfFLvzG/Lf+3YUSG7rPiihmMdkd55PBP5M4pYJYGATh9/Zy/rxi - 68BE62xxNU02L+8Ht+CwarBR92sdp5Da1t8q9oTEN4eGvldSvZUf3GHp2N84b3WbZD07LCZGJXcM - erkrjr2EZCEKV93qbHrVWWzPc/naK8Geb3YUBpSk1JpQuMrmJQ2+6dl+oqLq/w0ja9U7XdqdjI+d - Yekuz9FNzDcCqyJ8mYyrLK3NWVNNj4RJX847m0ZV7u/zOe8kP9ajMf99tzubTmn3L2DmIB/uzoiD - Th5MccdVQ4+OTK+2hLuLIlzfXsFwVXyNZsD/RF1dSkHhTsf7+1W7qwtA7uDUw9Wxwp5wk/yleHuY - jwYFnCfZZbA9QsibR/t28R3t89VFhTW+HwnKVRLI+KC8zU4tFFu440zGFd+23O84JDCManvFHATo - WHrjyf6G8H1/g+/SaIodFMOGkHMCkScejN/SrnV8RwgnCBeAXvAs0MEyMGYHgNgoFwvFcL31+/uO - 3QJEn0nwgbvOwaQomCBXJ059mPdp17J/IxZO5cChsPaIJqD+mYAc5tODuRVhkOgih1BHM+M7gTgQ - 4khs+e8oEbd+++Y3KBBqLMwV8PZ9O60W6Pc6tNuS7TDeHzv4xzWDrk5cnMnxVpz7b446zN18crAH - 2x/m9vNKdCwutHzBuo810bbod9pAn4bX5QODBUjO31ulDT+wMFXL1j33bNrH0l8OyhUXuvTTZqGH - 6cI6p1wnTE/BecqJRi92w4sA4Yr1blb8y4PCgVskH5syMpau5WV97c/NyfbTF9sPHz1fgRW11vfK - quo4+Hez8x9BEKjf7oRnBTtsPBicGiT3xXtHbxAvysKDxItluhPGXsjbIw68NJWEJ0/Eokd/0ij0 - Ml8ehZEXBTEVl2G4EySeiJJKBp4vMkdkVCraUe25XOLdk8DPPD9NHRF4YRRU9FnEmSu8WGR9V3pJ - GLuBJwPhRl6aJJyR6t+RlwXC8alUHDnSk/ge+tJJvEwK/TvyRJrQhGaUCmgoWUL/Si8IIyf00iDb - CQlciZ6iDK1LGbmpJ0TIrbpUVGRC/abPWUbdBdSmL6i69BOTCIQXZAQL9S0TSXlRJJ3A96I05j9J - hCJJElexF8nYQUdJP/R8mQG/UjoxDRLfMxHo30B1SE1SqSBiADMaThoGBEkS6p8YXwygaCL8lFCV - 0jACgk0gL0jDI8JGRCqfF0Q0AwRl4KiBSC8LCUYuqhMZVcAggphKJy6NQqQmAfB9AsclsKgLwotE - p2HIXacBCmRZRBMqaYhJ3Fo2iVo2mHJKUImUIPMjLhEduFgkWazLqESf8S7BcLDOYow4DkxCCC8M - AEqAeQE6qCf1NXDbZYJ3Q5faCR2ZEqaTAzfzZCx3UkJvcMC/+7QuaEX4wEMUEsp9wpL6TTMbZxEv - nSiWhEw/TvmjiN2mhIjfPaHVQkikBcUjELQEovCI8JSkKY2OFn0c7oiI5gyjS/0EeXEY8sgpyfAc - 6HJ6c6hGqOkMi592B81imPUTL6CVSbuJAAPyU6mXuU5gDgShjfFFvfi0CghJIW0HAlaGmU5R17TG - Ir2f9SQJWhdhcEANyqji0dN4Uy9IsoPUi+O4QiVBO5E+EtIIt9QYLXD0E9MggjikglEi+8BVwpuB - NqKDhRPo3zENUvCaDmJB2y2lnYtv0m3y5YHLzazgu0yeNvZPsZAFdowv+Zz8qLSIucQymXU1k1af - DyflMJ+ctMimZaYX4ppX45LLJcMlN6M/IpekPbvAJgNaelLOATovZmhZcEFlW8kmF1nk4iHmElY5 - X/8TsciGPRLzIiIt5YEgLiZ3aBdnMtnirztSEq3jjexIWup+RIUCEe8QuYjiHm23MKaaO8L3AmrA - RWZ4RPvIDyvaAVkq6F95FGCXpO+eSNozRKmJicZh0A+I3BPRJvosQEuIloZxiG2V6Z9ESECuKTsi - 6g2erH8GVB5sjDrIXOxr8FfmHCH/G0ja6OCsyOzTzyTGzwA0389AfSLmjvhN9JfouE+gC6J/hAKw - yhBwhNQY/yRomKfQKIgxyIRYTZoCSJIVqJof9pmexS4hSIIBA5yY6qaK8/M/UnInEkJCFqlfGKEA - fSYsEAEjskc4AcnyEuLHBHtWMbqJ7gZ9Ai0mYYLUFwLcR6FUhvo3DYIoOyCk1U5t+8iI0oj4XyIS - /ZvRwLKGIIwRFKELZPOtS3kEBITECUIi6ASCXg5CLwdMLVJYDyQ3ETwOS0SCwIrTSJFDIbiPWP9W - EgQGnfroJZTMjxPuVuUn4BPEAMMj4tsiOeDfRJVJrqG2qIM0PIBMFhLzT3iEMRYNLwOpf0MagbhL - chGELqL8/Em4TTYxC5bs9GDePUlJngG2kUyPaKlENEKS+sLoCIKVRKdJgAwZc93kiESYMD1AGewK - JQhEBjucoGaJ/RCfIEaZRsRFaU2GtMrDgGdGkKhF/DTRPzETAfMiL4HY5urPQAjVOyI0QG5Nox27 - sUxvQCzhnbgs412AqdMqE/o3YSKIuEuSlGiP8CoJQ4iGyAAm414GGchRTZOUUdEgZQx5lNbfATYI - SR5PMlpdQcq1+2jPT7FksZSpqRQZPiQT2kFJhf0GWSajEQcpZhtLOMVyEqH65fDWBM0FtcCeiXiZ - h1iyURTr39Q5lcW6yVJse9o/ep2GMW+4MO7ThglpQ4N0hAIN0xI2pAM/m1WZQNbxRJLq38zvGfcp - wUAiCfZiFiKDdhLIQZxUjG9MF+07s7dlwsubAKHP5idBnHBjoGRppigLtqHQPzEYtTOTKOBBSo0K - Ge+gWjfwfdq1AnQlSyEiQV5LaKwEZR+zQwTSd5kcgTwEsf6NWaHNGAF4xz8i5BN94JUYH4H6JXpJ - hzsJdcP7KBE7JLaqBU07m5Z9rFe0rxY07X+icNQh9JKQVjDl9UGGpSIukTRLGD9bS5jIC1Ywf93i - JpoFHBzpVSVAeKgr2t2RpAWsdjRWc2gXMH5jAYd6AVPbWRbrBUx7M1Drt4LmQ1DKVG6FWDxYwHGU - qgWsIHj3JEIL0DjitBcljDV/KyZZv2LFCwQkArUIfBbbfIYxDWJkS6WdJekR/7sV0t5LCXgaRJAd - ocl3T5IU7ME/IEoV+IqOpMBuKOIj/sbkTUQx4zrb4vI7pHoSE/BpfkksD0KQeKJcJHAyYQsSYJ9+ - oyEZJe+GEUgXaHiYgfumxHclA+7zMBw985RFeAG951VJE5tlEmksmxg6C3UaAmmxzEilCgXXCWlQ - vRhqDKEgY8nWLiaVjQpphUWX0ATFmexxS2gofAdxsTxclDWWG5v252VZCLuNANpIunNSbmOIaku4 - ytXaUX9goja/jIV5USZbrOrujt86jWF5dUNK0rVmdVddJV+QWjeaRk/LWbh84CgT6wU9UbmxSVHP - qmn9w2uV3NxAoInpwZgktsePXp41QJxWDQi0w9nUwZ13/fOiyMKxNFfAMUdHh2R8baL9ncLiea07 - +IfjTkxUXDGSv3EZEG2zp+nBuOIDA+UabxSCDnsLA+9VMaXC47299j1+nZ0flgic9Q4FRqfH0cLK - XjXG1Yl9Zc8+DdT3893PrcFl7b11jQpjD3cWCteH+UjjUNcpJpPxZAk+9hC3z2AYd7uo5jKLlmp2 - iRKlUW4N7Gcs549hUSe5nCMcLod4xWerWWnEn2Fnb8+AsrgP8/1RuXcCZ/B94MJztmm9wq8efvG4 - L1KV/TfOdOzopy+cXKNilanyfGj3F9b+giW+nk7GbwqjgKmUq/EnjHbGN6VXD89oZbo2Vgktcerx - z1k+KZS1PyASvA7jDTgehGix2sbQardfTvpV4fRpykjyoyV5on/Q0ovoL7WhipyB/NMWiyarsVxo - 6qH2aj3bHZZTu+WfEVWraSVNCszNCpqizsguTNt58/Gv01R+MBkfDsbHI3uQdn2E3ra93PjcgsUC - gYCdEwPKCrLRDOi7DSzcVYNtUrdWcoLFs0vt4ruL8G0DWFrKEZtzapyCNJls4TlNzgr99sx82/VB - OXRMrIllBwYNOAiT5uZVgTDTc95Hiz2BcJrDfEMsd4lLOKqyO+IrrJ3v/VWk8hRmVxlbuDDO2u+r - C821YybLqdQ9k7eVeieF/VtqIoi0PQ9wpcXh0CI6xtTZ/SwbJDqFkYi975qZcUoCQfmZ8EF/daKu - nLFnwUJntq381Ik3c4V53DXzuzsdYQM4A5cDRl3mkszwxLJn7Rb55MT5FXfuqxLhbBdOcm2fD+yp - 9TkbzB2euMcL7TVmRly7nvWn1txoT7Nft068TxdX3gXDk9fc8mtuejkla49mGabnzbGrp5gWptou - y7Zvu9oy426zrdu7x56kArUNEVFeD6PcltNYoC9LfTDo+yr3i6f2bbXGE2Jh1zeODy3JfVaZbLhX - GDhxdbeeurORkkcXqXZVLg6sdveq4q27P9eIi0bKKUQOBtpiQbslmEV6ml4tOx1etqQ7p+EwfaoT - 4tPy2gJSG5ja9Yw7zJhmS8Vk2uw8MNCeaz/fXVVycQtQu7sWplVLf+4ofhmVbpPcZXgAslcQ6yXi - KOJP8Q8zCueKcmdGss7h25boKZCakz1RxAnEiv2sRUyD9zPlywXDfVNnlbi4vJW2x4aNeds4a4jY - g+lG9GEbgbWKD5dgWsbxbxV4iRfDgubBJIKyAcbn4SgKtjzYk2Bnwd+afsA+hiNmWJiPRN9LPdjI - hcNmEvpXeNpGCEMN/6Ve2GxDNVNuAP+TXsgWc9dL+i71A+Nc5HKzsBtHbGv32AQHE4WrwFM2GhJF - YZ7BUI5c0ZXUAXUhBbsgAHAYsQK2O7N1jv8SBoZRRsPzbxIi4qsgopsJhiuT3CEMojtB3+U2XC6D - Vulf5PpuyDgIGQMuzv5XIiD+TCuhp2yll1gI0vW4Ay80K+E0BiLGwJJ18CQW9MM/CsRW4sNymcJu - KXZiLzqgzb1DoMAUBhtaSiWUqTJg7yaqA2M4bOEYmxoZhsDjkmpcIc8noRXwMLqQp9GnTn4CHh7h - w0VtueW/G6opSWHu65uVjvFjCDQRLyL6Df+LwIm2qCyAg70z9GCjQ2hQqaskukpaux5bwF2d5mqx - l7wbSlj85IHA0ASN6d0T4WOtHMVexnZ3mgWsCUwQ/w2wcXi6gEc1bVLVJtT0cRgieFGF/L+E/2Jh - sUEcU85/6StXIghoOB4qh72Q9h0wi8EDQlopVerh8Dvd8XDq7mVnnGovO8Tmb0vk9AVpyjCUJayI - OZHLrIiF8eVGku82qvLjSBlVuXdauTnX/6ziJx1O+JmYTyZi9ADqueJFtazUv5RogRFcVayQmRed - I1agyLliBXB9GZFClb+6OIExa1GifdgPsUKCEzA9TMCG20R0S2aGhiYgUwf4zkRUWCJKm5srbVEO - Uz/Bw2+RpGECQoqf9JGIDLcSeaHqRjUb62Okfzv6gEt4l6cPXOsTkoan3N95pGG0rNS/FGnACK5K - GiLinOeQhoiZ6zmkAbi+DGlQ5a9OGjDmFaQhjLzgwhpHwnJmcrM1jmhO4+DqMaRrSJwQNFncW5C1 - nwSRkTKFPCVlJnMUkkpYKZNEHy0Y+QtSUS+A4z3JRFspChLHSHAiC8qZMMoSF/+SbNcTGVDAslPF - 0i98eAQf3mIxqb801n9T8WpSYINdgYLuzxDJ/lPS0Oca1HPJ6GRFwX8pSqoHcVVimhLZOIeYpkxZ - ziGmGumXoae2ytVJqh78CqqaEAX4v2XHWaSqaCVm5ZjJaricrBJ3SY0MGZyjvTeSZwAfe4CztOKW - 8FfWGwqwsznR1IH0m4L2Sqa9SU8wUOx7U0XAH2hszPRVoG9BpYIdYgU+eyPBF4nRy9PIDm3AiKvI - s9a6ox7MVTBS0E/giZrYIrmY50CbCbTO7yql31VKP3ukaiMDsZYjap1AjrNVzCJKgZgzmQUk8a34 - ozGKeUr/3casalxz5s41vtsgUtE6suBvrbMI/RbByB4s4KARzzqPBvncEUKbfulKzC/mHIZOu6jr - omcR+bNuSZqjrOsg0Suos/b+njvmydu4nZ+QSyJCx8C6HnSYxj4iNtSF6Y+EjFKHVbwebNjWPiI6 - dCDIj4SPVsDNa8LJXIsfES+tMJgfCzeTk0Nai00ozGtC0KlmPyaWFjr7WKi6diR9IvR8bMQgVu41 - oUQ19RGRgWi1HwkNc1asD0PD9ZmeVqABVpYLo6EdSaKRWqznxcX8xZYIO6ubVt6k86LYMv3MPGTD - iY/n0pmc6bWodbOuBv4CcRJGXMYpRxzjtj7IDwvESsgdg4AVrpnf4XknhwAj1etks0OidhrFYTOS - sDUQ0paCUBJOMA4vmHfLXOqZSSI02l99PS6AX30snXQriPjnxRpdJpc3jUa4aif5ljUA3vGvo9HA - 97IgTj68UaM7nPJXWrVyrYsj+2EtNGbd05YXV8TFUlVaIuNh2W/UhGshCEv3odpgLWq3AkBe1J3v - H2nQlhvHFnWoy406n4zK0X59E0etQbvAqBeWS+PIylqiCSZXTLTL6iWiEk0n+UhFEstbxZYHInKX - Fp6LTjRX5ENDFS3ui7PCFXVW8Jez4xEtFj8nJtHCHGl/6n/xsEIrWOFNDyy0Ot7KecGFnI8RXmgZ - OB83xNBpQ1QTZOi02+wFgxicEVpoZWShZWbzmxVBaOUiv4FRhJYuJR7AJaIJqSm/VESh1f1+wshC - 3N+X6EJfogt9iS70JbrQzYwuxCRq+X295QdDp3wFTut+5sMqqfTyUYe4iWuOPLTyRsgqCfIGBiBS - /y2bu0sGIzKzdOmQRKsh+KR89kuQoi9Bir4EKfoSpOhLkKIvQYq+BCn6EqToowcp0mLHMsn5tB1r - ufF70az1AXGLllX+dJGLnE8Tu8i55uhFzv+Z+EXOtUcwci4Xw8j5WFGMVuhiNzqO0RmxgZyrxDJy - PkE0o7Nh/iQRjZyPFtPIuZaoRivtJCbzs0U2ak5Ql/KBzxbbyPmQ6EbtY+HT8Y3aQ26nl7DVRTiu - M9gRt321gEeLYF0w6JHq8YMDH81hrP3feaeAXPEaAyGd3+cqJFxjYCRu70twpOsKjoT/zgmQxBg/ - bUs+f+pXBExSWcvX862lBdo3EdruJheRwvdmVaUY3JLXpC4mfbeauLL8PS91X0akvpIgfWrU1yRA - N+1+BhH6ugTnC4nL1ywkX000bq+8Tyccr/JuuYgg/PHE39NdfVRR97oF3A8Ra895VuRjibCnSeeH - ia4fQEeXC69XEFmXCarNNxOjz9IA0NvvPeXk5nGKn1vGsv6reQd1r3xbDJpHUHfHhM8h/5yoV037 - edVfcwPf/y/HdSL/8O2dZU+N3nPeEfSD4u1dxw3uOfNPk76nGWdobunNoP6/fTOveSdSAezMA2wc - MHWm9vYblvPef7qUdaDUn0luyrXFaMEtcHESjUfgsrepWy6E7QKY9xntLt33xlwTKtPFbwKqNZPz - nqa0ZgysB/N+nCvWK1OLPqhQTlI/i0wrT7HVNbqNPZodd+F+Hq5GsIcd+40BCrPJVhxqHwz/uaLE - Bwl7Y5KDL/iAXQsBTgtti5/NjfH6I/GeDBbkzEv+b/Gf9s1yZi4NH4ofJmly/6rcBjeI6R8J03vo - ao5xEW6TKl6TMqdJrsxnzvYLX75xLqJhdRoKbncvpm1OmTtrNxnXy8+1nZY6m8wP5kMclK/mabLS - Q/kyXiZX9DE5Y6fM+ZcswdG1PQ/5xbXki2vJF9eSL64lX1xLvriWfHEt+eJa8hFdSy4uNS+Tludu - GVbjuliqmJ5zQe/M24FzteZv6r3kb3x1+0yZ+GOL0M3A2wpp8/UjaaAJgnElXnBh6bkLiC6igL48 - Hjv9ybiui4G6uUc4zqeOUjRHzu2/355XSEn7PM5PoI7yoFGmZStZd8YTmIOGZY2So/G03CuVPcZb - drRy6SuCrYXXtp22rKVnqqYxonfRPySgkkQW4Xa9SnPy/DuE7duDrWHM2+30uY/TfpuitViWnHG1 - QhkuFHRbgQsXP88ZmcycNnu01dIZwftW1lEDXWZtshWX7l5r3lS587AvZMJ2197lPx8WI6wgtbQa - S+oytK9Av/18+mLHEoS0b3WcCsO4adTIs3T6i4bxuhRBOi/i4griZCMnaqDaJOpDiJPw5+Ioxqfi - KFKJ1b5L8/r8Za5j2ipXCKOoIlJkpDsGkEyjKOiaZExCJmTcAIw0QDS+JA5wYwt8l76nUHyzhPhs - BGmX5AQSasK4GxClgLaakZCSpCQ0QTVPQlzuwiU2m4TCGoquTQtcviQxgG/qscAQJhC9g1BJzypN - 5bUibvN9kiECri9wTZH0dlwlI9HXT7Idru2jmg+h2eHwIJD6A4QTpBTUTdsHJxmkKEibXFxq4wt1 - AjcLbRr6g0y6Jp1B6AqQn+KOJILxJ4mMeUShj1iEvh9GPeAviyDLEUx+oAaUsaQSC9w4DCAXx7jz - SfNhk6TaZYlMuyYNdMUMHQROBJ8UUQKFH8dYoTRpno6QjQYmP2EFB3fsCD4ejZ/w3UlfxBC4Regn - MSOBenBwOTIRUcDDEBmCLqKLCPdEGZiU5W6zagJcqoud+TX1qm38gf3n7CtxjdEnwL1Q6sgTIpCB - 3ApYgwl2SJj2SV/fgi4ifNJwI1IURbpFCqOUJAHbfChVWbBjG9AtXhWgJCZc+bEFSHWcZVtppDrC - ZGRBDCE6Ii14h2TpiKZxKyWMpXhJIkgTP7LZCs7YVrdw6o6uCmcM9TYC4hLVXpRiUTC8CTrUBQi+ - NE3jpBfzPWbpmPwEt339uBdDE+W1m0lqqUffZcQI0A1TxYC0uJ7pkS8+Z2LHFLjqCEJJuhZvqpj0 - tYABD8Kt0Mdly6b5rZAWJaGuG4L8+M2IHVJkBe4aIxIR6T02jW2Vhl2TJKomRZY4kU90AVeHiciR - igeDAHRd0mf8KO5FbHBR6BFAJ7RhmfYQtjsiBRG2QhFvaaB3NNCv7ChAT0IpAG1ApKGL9mUamO9O - CIqRpaC5gR9Jm9bAmqQEiSM6ZapjDwuimbpZk7Zw6ObN36vOhYQ5A1fA+VJ8siVDnoSeIORlRFL8 - nvCBxsiWIN1PEi3rCVg7EibWEaF5S5IGnaRpb6HFV08QMDZhIgPEZtx0GkZQniNSbHE1X2YyM/lb - uvxVR4Rr4DBqxrjUC803gqktQ/AoQeMgZTOhmchScDmRhAHiittCxDhSn4iOrmyLUqMioa2w0Phl - YJy/vnCRC7/8sRGgzdHfbNcKavW8bDmXtUQ2to22w8uu9ry18uPueDSolwmR7Q4Xgs4tPQn6MBc5 - LWd+/wDgnKusz6N2MXT61ZGxN7tRyPhx9jmRcTgpFCQ3ByHPJoWrxObPhhV2DNEHqTQawkG9QWrc - 6z/Go+IGLR2FJecJA/iZkXUD0fNwfOz8BJCcXnlUfDb8tLyabwpm/jGeTZxn48l0b1yV4w/GTDvA - +apSX+xDH9E+tOAmdXkr0YvlocvbblQXC27+SW1ECqZrMhFl2ZyFKDn9UGqGiK5nGogUGi9jHzI1 - rmge4vcSJalFXYTCCcOMPQIkAlGRFhvGfLIfkVoUwgKSZqHskZaUwEAQ+BxBtUs6okzgDeB7MUJo - kTbp41wNlqUkE00aEnTXpiQ8MGAoiTMfB684OQ74vcSIoVDpLulmcQpDk87HubBgnSPGXx/n+KFk - C1GYyXSHg10lsguNNcX3iBr0E9aeJKmCQYxTrKTpR6W7qedHAXwedD6iFfnKpOTzIbFJ8+lb1LVp - HydlUGVlIPzIQRiigMM0ZTIKEeiLVCKR9UIvTLIISmCaSYlxUTOsR4QhjtEBfubDMJRlaZjZNGlj - pIbFXZOWOArjE8pYxnwsSb1H0KWCxG/S3RgeBXxop/JTT4Yxh9RKAp/GCUNaAuUUx9Q+jC4+ARQT - HiQ0bVKgRepnAFckNDyfLQewY2GUkUl3cc4ci8Dms5tBJDLHLqxLaUiNDofTUQ48JtDuliC8Qnej - xRinYQgdNIrhj0DTFaWktvmwm2U2H/VpenZMfd3e1YAJfZ5UXhtRTHMHj44wNWlHYjLUohRCxDaN - tZAmcdekQ9owlIsVJizGTOMGo2HIK6bJpxUElyWYCOGwY9KmcU5n0nZu6hvgFoB/ZUcD1wnfT7oy - gZdUaNKAj3YKVlQoUhnZtB2NTsNU6dOORH221dIODZNmQDrdhb2CtkqTz5HVAlWf6INJ2wFROhDS - 9s/1k9TCtwD/1aY0BvGK7bruIlag3yzjGCYhDv8mEtgo45CIXMgv9viBjPA2Thr7VA0OLYmwuzLG - OXdqYXOom8Bv0t0IBrU0s/lRBLLh8Fl/Kk3SYEIngSgEgTSVlak6to3rdJc7D2STr4EDraSdb8aA - lyWjgM2qQsaia8eKyHLUvEaFWT8GU2bxE1CpTJrFHwmsR+5FJrSuTdoOQqfhHQVCaupbVM/PxNWm - M5Hw+LHUIqElwrEdY1C9ZEtl72QI3xgnvYSIdcS3aVR2ShhJZdpLIthDYRYKszDp4WUsaqZpNYOn - HVVXncFMR3g2JOZqgKcpmGhiubBJa16cZvAZTCwvzmiKoyizvDiTXhopbsW8OKM5FS1enIGdBKHl - xTbNvNimNC/OaFknKuAi81o4X8nU8uYu7OuRaHg1QRdlcWCZMTufxeIUL6ZyIhENL6Z2aPE3vNj2 - o3lxFtNKjaImHxPiR5YX27TmxTZteHFCTIx4reHFNE4/YElE8eJMIFyitLw4Y7ujZcUZYmIqDyRm - xSZtWLFJm02fwVkShFOz2gy+MaFNA81+yIKUzqeOJJfXrDih/RkEp1gxyiWKBzIvpunJcBhkdg76 - 8Wm6DA0juMIm16wkzY8XFtq/id3yjKsgn0uH1mBoUd/5EdaPyU0yVN0YDN0k65S9N3Vz0LMNwJzu - eAQdfAbQPhuOljyecEOQZN5OcMwrKZ8NR5oUEaUsB8VNOEAwKHpoIPpixvtXN+MtPsB3eTve9opH - 9+ZucF3sXb5PasnTQF2XKS89z5SXnmvK05i8jC3PVlkdUKqlOSSIuQ6BLiH1Le6adILI8CQVptAZ - SFVIPSl8UiUgtSbQHPxYZhIycJKwHSiNSG3rZtAOWYSWMM9lfFuFFYsU4qNN42aP5OIqLUnmlClk - 0TjNWDXwJWmBGXuiS5OGbBokMJfpfEAnlKOXhIydshUxY5Ge9EZ4dmWksHTVfZZA+WZAq8T9E5ha - Yh5W049KK1k7aeVDFYhZR1bDNWmql0EF0umMslMoWFTPD2HsCCU8yWhcUBHgy0GqYNzL2LFN+c+l - UFXg1JPAjV9A1YZGBBsPbKRhGNs0bgZQ/a5JA2sRaShUPQ451n4a4LpIBu97mZp0F93jPoLNR4R0 - 2FwIiiSRADeCf1zohaTpQBdNsiwRQIOEZYD0IVjnMvYtgW4AY1nM3QgZmTTAkpJdsVS+WUs02RLG - 2YW1dmUHK1KoA9hk2dAQs3pOq20rhVE3SjDppLSKrSTExbJkh7XcdCv1AT3ukIgwzmwuzEJR0lRW - jW7pTq7sXAXNOEotamL4pEUWVU4c4dpcwmOIacbj2IyJlEM/xVsNsYioHi1ENreqiaZmUtgScA0x - wf0w6iZLpEl3owwqq82O4NTDKijpr7SMTRq3N2QUdk2aHSFxo0JVh/NhBqutbl2nMYg0ikWTr4Bj - L8sgasYAa27IliHfT7t2qLDY+GwUY1Tw3oIPqUaVTncjfsFA2ny8thFkoXrLApYinbaD0Gm8oIHr - fKa+RfX8VFzZ2SxFNwpsrLYQVh+4FeoFo/PNarxqNyLBrUrcIJM+mpU4uZBsXiVKJ813mC8lHEZD - PIaRJjadeomEFc+kA7YisHlV1YfnLC5dqXZNekv3u6Pbf/WECviw/GOyaW3AXss0RafRID98B+9O - eDaZtAHApHEtS6K8rg8vL+7eV+cKlNqSsLiIdEe3vaVzr+y5xVd+caqTZDFcQfldkiDu4bEF3AnV - SwFz5aci24Jdi9gXXC9DIeOeutqaGPK/Yxra0i1feXLBO31h15BvV4/Oucrq+Re2q9xEs4GRS2+o - 3eAGObhYTN0gE8JNcwKyOLohTkAKP/U3+fDw3uHryPdvDoZeMFDPHALqJuBolNeD/M/XwU1C0VOG - 6bNjZ5Vx4PMg5dfxpMIFp8Mvhqd/dcMTsTlaC+W0/CDjU7fdymoDVP+MYp/VCNUC7LquHSbnOZUF - yfleZS28XsYcNVftIiapIMatOPgUhZmIs65J426chAdInOGEMoXyiYALCd5ThOKRhiKKeqRleHjm - Gxds4FwRJKSHCrZBBRyggdIJ1Fac7vPz6DpNiowI0q5N45oWbERJyCYvSqesLCcIn2PTXfTvh618 - Ak+mqn6Mq30EfpZFyuYVhTBPJKSGdFEOJ6y4MsJhcBLfwzUqKCVc3vSj0gxXFrbzU9LV2CYVwTZg - 06TMJ4HsmnQGBwu8BZkkOEfHbZUg5PoIThPhHceQlJwe+pOxOr/G3b4EN7qpL0J6xMBJDugQRdLP - bJrmSOKs3aSBsxBn9EnA1yMR3IVvbNKkwASk013uPExa+bHHzkOCpi5iWBGrA74+0Iqgt0lMJcJh - qKPqMAVuE9iSEvpAWlMEUxX64btn6gMDJhB8yZbQqwnuKRk8AedX25V1zgjvdaKflPuBJhcEsdgK - 4AiDICUSLyDi7XlYY9IdTD4tWM6n3beDsB1SCpsvEShHSlvfNLyle7oyoGHMgFr4QthS2+2rAmYA - V78+Fem5Mu3y05RCDyymtPCUvSTwSQXH7S07bMpF9SRugaXauzI4iDqYxM2wEQMiDVvtqwIfOuwM - l8EiNk6mSZSp26gJbkynHHxMf+cru9Q//JnSJLLJlHYNEZOuSeMetWSDuq4ewZCMi9JSPfCq0lu6 - 2x3d/KsnKJAmykE2FbRB/Zjjz+g0NxjH7NITwwRs0hYAnYbxTvCzm7GOX0M0EzZA3b5ObyH4T4Lp - VO1v6fyrojFlnx+2lFDPSTeF55Eijkg7CR7h9DP11Gxok3jPVgRx16TZIwkGdFRPGrLgmPYtnYDt - OmyRiUyoeUQLKXVo0qYHpFNE3lIA6OoGvAXwX9nx4F56lKVd+IqJIDJpJ/XZ1A+/GriD2bTpzqQD - 5nU0UMHXNTGdCdVoOlRpHOOkCEhn8zM4McVcPw4Cm7btI52mTf/Esxrg5oG/sk2c+lDvo/oyU56D - aRxEMLVjsnYM7nsxXiqO2PGVtmLWi/gt1YbK25qwV0dSxYWiFm1F3JYPItlb6PEDTL8yCbJTgEc+ - w9UAHsIdOxEWcEnsklB2GnCQwigSFnBT0QC+0OOV7Y2SmFoc2V0UZHxV1i5TXPuP1JvOiTrt4qRZ - FCZtdhGqx+xjqTeJad/uIgT/4wCBpgRR9QiRGMw2MmnThVSWWQOBqW8AXBjAKzsis48ETQFubpul - GvDBZGLXsUnbIem02Ueo78vY7hPbod5HArfxRdDk441pP7P7yKRN+5zmmAaqf67PfvkKvgX4r3wE - 5skMZyMW8RniO7TkHr5oELHHfYotItWI2SU/jkg+3AlxBAhxl+Qj6QdacnMQ6zLluHgK4AQPULOg - phAOMSlJkmZAXuzHeBEaB6pJYNMWITrNoTzY+1HVB0Lx6LRpX6e76B/eiSbfgMfnGH7QDAM+1Ljz - zz7USdeMF68548BW48PyC40wsxMkAvfpJ7/VC878oDRO6CzoCMUYiZhDUKrSFruL+P83sdePiuN6 - A8Hlx5Ppecr6JzUGtWH5/ilB+dnMYy1ICFN5NS2HhYpTdaNw1FLKP685sY2vfRozHhfvFzcLWY+p - +RuBoHFZ3UT8/FxWNwI9/TGef7iJGOoyZF8M1P/yBurJyeF03J9NJsWo/4FW6lNNnWGqPq/s57VX - L0B3TUZrEYtzjNYCdojzjNYLwF3Kcn2q7kXM1ySmerHIOIZ1FpJOpNMxfANJcEYs31hw5De4M4ko - 5Ni+0BkTmfREhEhw8KgMAx+1Ew4wl5F0yopDlCJMkrpIDIOqSQewpJLCYtIkzsYRyfFUn63cHDqd - FJEIt9MCk6by0Gza+YiVy9VljKTv6ZhLaZCyQ2Uksoyq4QM7dYlEAAyJ64Ds5JVALzLdqDSDFSdx - kx8jZDRs1+zjZZIIMh2SVqeScCcMBLUFb03Eyssy3w95SFEMrT4ISPjuoS/I+vCelKgdccB69p7k - KHeIzCUSNmVl8IrUadxvpAa6Ji1gUYsinhERc/DrBLZutJ9kiUnzlESpaOWnbItDyEMJpRWBmSVf - tQ0SH96UknRe4CCD/yO8j2IJP0o0hJMLny2OGmsJQobrD13MBs44WiXUYiJEpfC5W1hsVzcURB7u - llFj3A8bKtJ0S4Qhmt/h1eHHWwK3dGWEkHChlJQO4dia7sQ4IRGJzRd8p1fa+qbdLd3RleEUhGqf - oy4yeAIOWmGreZWvwb9yL3ATTrLWNATwjhOtaQgIAzJgfEnaxvQh0+HhQwlPMtrTQeqLLgrGbJ/Q - a4sjWXKwdwnPWACKQPsqTcUFLhO28vmOIzxmfX5fQqexFRJBS1enA1ypxJLV9TFfOIYx7es0DwRn - PE2+Ag9mZiGbUeDIIY54t0dp2jXDDeAPjNFofPBuhlVEI0ynGSwmQibfT3l5cRRL2I102g5DpyUu - UCLsvK7fwvf8jFx5Zn0fdgro7VEYiAiegQEt0l6AW48IVK87wAqK+Tgm41MKStEEBj089BHqBUjp - HdPQlm75yscTOMhC+PuFDRiktMHi2G5APsRLhN2AyBdJZDegyTcb0NS3O0R3dPVjrhTB9nGoKH2h - 0OSnCIoHW2HQ6ifm49RuEIce/FNNhhPEsOZyfHg22KkkLusTde7qJN4QAbMMYpp3OLgj/Cg7TsIT - GuYduP5GvSBOGW0AQx22IWCDoO/EtnDnlsCkv1sa7B0N9is7DlrHtLEUuAmOadCBlOa7A/BlAt4S - +Qi3YNINvCqN90wC8ENdH5727C2r2jVpC4hu3/y9+mFewMLEqemQKcx8renAKR+OgcIEASBa0xFm - HMHfzodO2wHqtJ0RaiDDrWEzIyF1KaJmRoghZUoQUDMS4hGdtBeEipPZGVGQt2ZED8XOCFhLAog5 - zn8zI2HM3NfOiE43AKu0nRFd386IareZEQ3Idc2I8PnllgzPS2TtjSfwBgnifkaAq6vTfMudj+9F - iOi77EYgOI1r67FJU3na8+pER+XDaTqRTX2d1u23OpaC1wJiLcfwO1Bp5cXPkqHP78vAFCpZcoz5 - URqdpo4RACJt5asRqvqJszDiqx/QInxnw3MDZeFteACCGIcc8UTzXI7IDEAWeC4KEiezPBdnqVlq - mSxHgw6DhukGvnI2sPl+xC+yGKZr0oZbmbRhuqa+YaqmfcN0uf+kna/g00zXDmOB6ZrxGqZrEGKY - qkaY4bk4oA3CVrZv1xfzXJNuRqHShuea+i18z03IlQ/EEVJHBBbONIbjR2LhBIOCHoR4IiFRSpM2 - cJo0CD9xBlTHfQYLpmnfLhzc3cparBxxHtRAQwGVRCdN+0gKFW2C+zfVDXwL8L+yAzLLB3fLSPS2 - yyfF9kEEHeJcWRLZtB2QTmP5CORT/VQvj8TPmg5VGpe34jBLm/wId+VYWYK6Y9N2QIh440e2f9SP - GpFzAfwrH+YifE8kLCNBuGrMk5Y8YgRLSdkhAIKMn24leBEsCm0+GhBpQ6Z0g1eFJ0IUi4jPi0ib - 5l6zSGyxehRETTcR+EYW4qZUrCJOaUYIRs0vuJH6JoRJArcp6W46ieuGMQJHozbkbF+9y6Ma1ckt - DcuOSr56wrIRc6AojWObjaPCOGKQKBv3nkgShVrLn9EHiTMI7ZWSSB/ZtAHJpPnOjOB7T1xfd+PM - 93rluM4p2D2InyRVW+I+ZdRIc1sI0kxiQI/0PsQOdxirYcKCSEzya4iQ2eGO/txjYYFVYF0fUgEt - i95cNx9w7yfj25qL0hAOGeO2lkisCtoWP2/WWgOC3/iSVhgyaSNbmLQRhvCgnZBWFiIOLlOOCK5k - IZF5SkbSspCAbJvgdlESSNGIQgrsliikx2FEIcArYduh9jMOGaREIcHXOK0kpJMWWJU0cpCpbOQg - 3WgjB2korkkOgprrJ1GLLCNwTJq2+AtxSSGSNj/HNJ1m57yLYsvOsVKCqGHnfBYrLf3FCXWqLsNq - HdqjqQwbdm7SVvnUacPOTX3Drk37hp1DgOdQgYada/gsO1ejWOTmerSGmxt0GDZj8GXYJExSMgtt - vm8ZuW8h9y3zNqUtchfR/29yQN3Yajd2p313Vg8+/znQou3++wfltD8uR5/vyKxBUjE9uKFIeoSH - xIrZ8PNh6QJHPZ8ZR4vnE9dzkv/lkPFzHzJey/HixQ4Wb+qR4nUfJgbRuTdgogvcgLnaMeIlDxDZ - ZIbjI5HGePxKJWO4V0MvDTN+uwkOvgE8FaPAizlIoSQBvBfg8AtqKx6rCpMurhNAZM2gnUHNJbXA - D6SWvqImHSD4ouzaNB6thTgThXz4Bb9uFm8ivKgpTLqL7kXSygd4goNAZBEc1Ql6oc4nU/j3Qx4K - Y2VX5Bei4dUP6SnCkZ/QJztB049KM1yIHdLkw1OdAzzLMGqSAY4GSdzSabhlq3NK6ADwA/V9eAQD - LaQ14QyRqoc9XKqIUo7AEvphxMOSfGaY4NlhWDVxTgKRmu+n6HQIHTahOdJpfk0YQWwjPC0NmVRm - uI6C0eDlXp3mWfFj2cqHssYPYsf4DnATvDGEMDccZEImEg97oRze18JpgMA5IKJfSBb8pORLXOiJ - XRXVBwYt5CAtpoRaTzTfPqJ1zq+2K6teeFQdz/zg1ATXWThyMi6TkC7FT15FWYI7HxGrYikpQj4u - t4SQlnH5JcpIQbT5bHHMTG3T6Jbu5eq22VjdzNKwCVzJiVrNq3wN+9UNmbjF1JoADkIkWxOAIA8B - m04kKY76uTsOE0xrgF1zaVFmKT/LRvprs6oCPDjMmg8tilAZEvkgmtP8fh2eybP5OFZL9XvRIrJp - GMBo83dNGppPglWu62O2oiyw7es0jyRVlw50voKPL6lkWTMOhASRSvdJIjUOjkBM2yfDnRSNEWWk - kY7GmE7y44JMgky2H6ln6hECmE08Kt0MQ6XxnhS0bV29he+5CbmyITNB7G8cHZJSn0m27oSB7KUI - spsEtn02O0R+sMVBpnycHSLcUdrj97HV8iMStmMa2lINX1nRhmWKzZ3zOy/1ieKre2e89RIcL8jA - bj3KJ3VX2K1n8vXWM9Xt5tD9XNk+h4tK6niCkKUMRCJiW1moj/BVP0S4ibSxxS2N09COi2MfhRyC - P8jwSKRJR4R3X3CIJU7DEkUaPOL+JNCJoWnjPmdMdBBsMRKh6OFcUSgrLx/zxiAsxD5Bd/GAIS5N - hmG8pcHe0WC/suPAs9PEaABuEgVJF60HYWi+w34IKwaMM4JUc5u20Oq0JCDx1KKpjzufINe6XZO2 - gOj2zd8PMOsFMGIsTofkN7Jb0x6yg0GGePJ+ok5f1HSEtPdDvsmgpsOkzQBN2kwHNZCBxZnpCHEz - gI921YyEiQ7JpmckjEBbRE9m/ASCnRENeTMjZihmRkIcQCZRFx2AHpgZCSX73dgZMWkLsE6bGTH1 - zYzodu2MGECuaUaUkRKB9Pwwa02AQGwkn8dLYIVdneZzPzZY4xhAOXSloeAbJmAiJs1GTk2kVX7i - EW9o1ddp3X6r40wthQxxnOAmpdIcew/B5wQuUJoIUvBhQSwnE2EKb5ZSxSyzaUcP0FSfH+81mBkV - t22ZGTX1b8yMhtsaM+M8s23MjMxsW2ZGZqYtMyMz25aZkfNbZkZmtiZtuFTbzAhm2zYzgpm2zYxg - tm0zIzPblplRMVtrZ5zjtS07I3ht284IZtq2M4LZtu2MyPctm/Ut6L5hrS0zo0LuIvr/TcyM8/dg - bpCJqGUc+ry3YBoT4w27BNM2n12z4ewDkXXi9scjIKL4/HGvl6DpBAG+FHifG1NsuZ5Nboblum2z - /uX5xi8vHt4E9OzvHt449Dx+8OyzokfF9ZoRIsZVlU9cTt8oDP3ywnnIsDkcUu+LKf9f3ZRfTPc+ - xIj/6OWPZ5rvVfM3yXAPiK7JZB/7cxb7+JTFPiZZNT7bYA8EXsZUr8pf8RHEEE7yHElI+pGIuiYd - e2mAsMkRh0iCJpHh5bVIsMIIy30UBUEvCklTVhd/wjTuwg7vswCfxYnyMUpSde8HXj4mGfATVV2T - RPSjCI9jKZ1BQquXDs4EgkiaNB7T8uFgYvPhxq5eJcwSAgmxjGNfGe21zT7xqRqpnlJFUZfwOIOR - G17W/CoeRmS6UWkAlQn1+KHKh8kfDpWEIZh2bBq3WWTWNWmEmchgfE/ZtgA9hGOv4M0vPNyUIGRx - HPWoP4mgUzF8tgJ+I8zHpZgIgbho+LDhh2y0jyNYJlSS348DElSS40UlCU9HFrAx1UcYrojDIoUm - zQ+WiThr8hO+boT6SZqx+1XIgZlJnUbgogCm/QwhthGJHhb7LMHLkRGuEij3RNKKcU0JIYCC1H4A - YEGiPDFVCbOQApyskLa1sNCubANCSK5QxRQSbJCMuT84ZVH7bAqkeVJGIVqTsHDECftsySiDM30i - hbC5upkt3ezVXdpxbYsNTQoakbDlR0GTCXhDhbEfqf6CGI5LKe4YmXw0QFA3w9EtXhUg34Jih0jr - lHpQwY6EHyJ6M01tCNtq6iPUPO2XUMK2inD6qc03CDX1LYj+v6PiTLzis0tfzEE/r65MIPDLYjdA - 7VPYICb4ud8TA05K0jbrKUE9RfTrfHQTDCwNgrbrenYdccFvuIg+v3U/QJR9ytVXi7KjZfmfVZQF - RNckyuJMfk6YFaeEWRQh+etscVbRqYuLs6r8FcRZjRSgwMWHNnfqKGE38uBbLPokhHv8uCRJRnxc - BWNxRZKhF/dJkPFwJzahr/Qv/4/Ku1whckmOwN+afiQupSX9IbHpSPQ9Eorhiu+w/7LDVnh+C1q4 - 6Al/qRfHpxTVTLkB/E96YUXfMtdL+i71I130w81G+B/V8EigdgGW8AJXgafemyaZGXdEMJQjV3D1 - mBoVVIPajfAHEPkuigQuByvlB6Yd/yggsUQeUXPpgRt5fKh6QFIHHvg9cPGDcqjEjn/gkvD67gkJ - ttQLyVM9H7VRj6Cm4qIXwAK/4wkSIqigzICyinrGSVrEg8S/JNXCvZxQcIDxVhIohYFeuIAIi0n9 - pbEKLvLuhosU5xFCzp8nYdiq+QAxVsa0TWl7TnMi+5OFpk/V6PfHs9HUPZ7kHC5mWJ5JJ9tVmNou - IZJnFF8Jlq38tes62LF75aSeOoSmuj8pdwunrJ1BWR9W+UkxcMaj6sTZG0+c4Xi3rArHdVe0lhtA - hnk9BSdUxGyOJzaduLvTESEAdKpwBu5uNe4TDVcUH3StJsKmnueovd1ZTQ3U9E9NpSceceWN4YnO - 7ThMlYgwnDi/5tP+gbL28wIoROGO8mEBkqz7fTA1xhn7soU7PTksADa35+pe3OGJe7zQ3jXxh+HJ - a275NTe9SnJpj2c5xk9vllXb5fTHhU8qCcZU9LGoLXMa98u8ctQflyeL8HMavSofjLieG1CzKlpF - mLe103t5v9gdj08tAHA2k8ez/mTyZlpvqwnqONp+1tTWK+FH+6FtZutWZf+NMx0T96vLqTOraWk7 - TVEe0d64qsbHDBSsdPxtOsn7b9ziCCvlVI8LeGiBktNqIIb7erfKIZ1MUG80Hh8WtCGd0Vj1tEgw - lokFpk2VuqoXKoQ1KwQgMS8DMCf1/QV4FpeR4v8GZ842AbRCKGjEgaqYTmnEP3rLxIHFWm0RoIVk - ePEQhF5yBK534GZe2AfHJZYYEcNOXH4lmhk08VQwQ4T95L/ED8HlwoOAxIJeyI94hbAUEGf0/S0Y - lHb4k78jSWk+4A9wKOj7xDA1rwZTi9FB4iHODUsbJB8E4IKOTMGb+8yOYSSLPBUM10tWMD7D8hb2 - 78W2yvS4ZHwubhX9XdFG2iTlwibR+XaPvDTpc7eILXmZHTKdq9RskAaMa9sfusmPuT1opSCQ86KM - vHx7aHw55Xm7475TT0+q8h0x2d2SZGaqd+DkIwc4IG47mx6s0+CKYlqO9i+9e5oJgGWL4M9IKmbx - LaQVHXvwxyDh1PfYITumQXqxB99cSNeCpeUIoidspC5kYLxlTzsq5s+hx24htPE85S3JQnFESb5c - 6AYsc9MOpZKRkgV9h7qL2RFOujHvLKl2FkRc4XM5BGRhTKMVjq2NS2eiH/Cukh52XOR47MyHeYH8 - jTBI1FFKQNA+dml0KdOAEFBkAJWFaEEEgfaoGwNIDByDDZXy4OE5uIx1ghQ7moqyGBtCgA+BJzdl - GV+izRSSNCUCVhuQnUIqRzGSxD3ZF6xlxA4/OoDxIjYtcM8KArAHDDDygFFXQsjGnPDfDDCgaSqb - MHwpz1MXt/AdCOkpI91hQoT4OH01AOXSztI3ETBEDpGu7oFVHOHyzCVMNYmEodXIITJJc1QDfewi - RYA0tCxTqhCuAuNadwz9KWV1BO5CrloQIWMZEb1jl72DGBiXl1iqFJ2AsRd8DIKIf4pBOTpFEbHf - TSaTRWXMOdlYECc3LI00pS2R7I5HI5KH1M5U1LDHZbZHC4TzrIKXoZsNCAuEswXbtVFO0+bHJp1Z - 5C2eli0nnQZn59NOa2joEEouTRxbWEZoJqZmpFn3fVZj8fqIIWmJ1r0hBigSJEBp8PeASA2kEKKd - oGeo5mILUFYXTRCx410UspSA9uuAW+GAEG7aJaw4OISLlOsbkbo+t8abDRkJa9OCCTKbPODi52iw - 3g2VLcE/Quz5LaKZOySQBAeCaOU7pvgIfHckE8RHiHdiHp+r9jATLNAkpuEBmzFikGpXcqbgv0KR - CoeKxIoSsEQVKGLiKOoOVIG+4HlQl6EL8IekqCMZb0nqrw8JiigU6CUoLpFKHiw6B6ULQI9Rhghl - 5SrKuhXFRyTkMYq10URD6ShjS4yvPCuCQWEDjKNsLQo0waQwYZbHthSOZeYGbMxgn12gXTIjwust - Ht96x8kW/Q9yI0LD8x/G6EegXCfE4mekbi8jXDqP6dasJiK1QLQszdIFLcl6YbV4EuPGs4nzj/Hs - JXrpH+REo6ozBb6l5S9Dviw0C9SrgfLaiJdu8mPTLjzxdCHaZfB2AbEPlhVHu5EQ45oSLasP8sPC - Ge85uTMtqgLTQZk0k0UxujR5a2YBl0cy7F3af0TdhOPhvhXEqpTXv6E3EJv0J+EynYlctvvFLnNz - vafBzfHcFZVRGxFqIxMNBxsS2hdEAcg7GdMMRS6JFkLAY89nV7LI6IUv8ACCk7A7eOwkfp9pkKeE - ihC1WWQRLDNB8go1WUIOy3MhKFmk9D4mWlSIQQ1dRZNIfAJxZuoSQiJh0ZCFK0207RfCjAABVJVU - QzB4Mr2TaJ0luIRHqCRapomMgZAlN4UDUH+MDOIsG4qdiJZSLTIPZEYmyuxLImLMoiwom0aUw+hj - 8h4yDeMGPfb7BppZueUnMtSUeHA3iNUsETMQ6JIG0/cZfwxuyFZq2QtZMyYU9BlXTBVDl1lczKTX - U5PJtnKtPgdHksgnG9lDT0myOoNBRxVlYA+VaR0NqgXgxZUAIwRNhSHe5YKxpuIsCH8Mckrpab4/ - yYdEGmGxO01UbQkmq4ti4A8H1WYxsoTVFrakdbv5cq7S3Cp7GfpZLlRrKGgbnGujobbRj01FL2pa - smi7lPbcJwxN8kuTyTaytZIc+X1oypmr5DdWYkC5sISxt/GVCZoARQHtg47JdBJPF7Hwkir1Esod - qtIfUGDfVe0pSghSlYFQBTGTLAE6FHSlx0G7cGiVZKyWJj6r5YhNCVLCLRGUNTYtfdN99KmljMk0 - P6yE9rqBZDKBkKcedEOiQKDMRJQgfSE6kqZNfdUoNxc4CmQnc1jlR5uOGpnD48VQHR6zwoLDlCpV - VoREY+vdk4jUXr6m1GcNUamzbHYASceQgD6lBiuKwyQ3VPo0iAvkN6W9a9k7YaUyYC6SMLmVLGyn - LOUZATRT6rISlV2l0busuzILo+6OpM9ELWaROmHLHo+Sxw2ouFjNHfKZHJrqa1WYccc1uV9Hw8OM - VDCVTpQm7jHqaVyZo0fk8ChRG1o3xs19oj0DBd8GDPqMMhZq+R1EGpaooWR7eCIxwQljoJamZAuF - kmsZOBwZ8lgFC+aJq2CJGW+C2VPMeKQ8lpWVnJ8yxkNPGSRSZeBhsT9RioLOFZjBI+bAkQuLRcbc - Jna1LUQVcLVywTwbB6Js7FBNu0pSd3V/+mgT4GpDDYPmKjYUM38SzOV5KC7PMZ9Curw4eNyqXckN - sB0lURwOu5ig5HUXKTanIc3UR1r274ZsghLUYj/i5Y1LxOoPbeRaWzqoE/PJDbQKAbMv/ejDGoTt - rZa2ykDp2uVigWtLc65U1dQXleuYKn1uylEg6e8oTOwdFjMJBVG6SkJQxiH8hUmq5sGpmVNf+q4u - y/Yu89U1xhofhdXSCNgoyEKWowQtUDSuw36YdcDGHX5piT/0dUlHQ+CYJhxuGWVdNgDqDgnJmVL7 - sHKF2QxY/o6qKViCSFxenYGrP2qDPlMMR7XFFWsl5sWuKqOlGrMShLL8I7PmJRSYun1dzNFFdB9p - zUKNo1u/jGxCH9V52cL58LC0DuVjEvOHbjXeH5tz30XOnC+IKq1jztPnw0vtOGjcHFuOimNnafdX - 5O7STxvuHqYLzF0iFEV6CszTx5KWvwNIhnilO8mihcec9y1l8Mv6shhixGw/fbH98NHz5UhhBLpw - MZl3NMFvd8JHuxDQxoPBcj+elq8mnkEID/CMXLoT0nYVW/67J7hLnko8Pidi0aM/KcJm+PIoJFUj - iA/wBG64AxNIlFQy8HyRgVOniOHI7blcAgYd3KfkS7dhFFTY1DFoZywy7PQkxIqWAZww0iThjFT/ - Zh9opfVAc5H4HvpsRJJC/0YsFIjxJCTgwqx64ZO2RYhdnwZwEIkCiZ6iDK1LGUEpEiG3ylQtE+o3 - gpKyvIO3G5WrcGISAd/Z5kByuMoeqnC/iCkS858kUu/xxRWRbL7iKmXShz80++1IMEC8KYHYjYH+ - DVQrFx0/iBhAsOE0ZJt9qH9ifLGyOQkffCSlYfAbEMgL0vCIn88jyhZw5Mog4wc3M3ZkCfVzEYFO - ZHiZT4lMQQSJJdLvuVEC4OPRXxDsDExLSJc957nrNECBLIOFTiJyetxaNolaNphySlCJFK9LRFwi - OmA7WRbrMirRZ7xDyRRYZ2AAcKLXCbgrB6FiTXEEdFBP6iskxKZMQPwFkWQdfic3wfmqjOUOSStB - cMC/SUbkQBN8QACN1UfMIv6NW/NZxEsnikl7x+PC/FGok4VQJ949EQgEiQf7/n/23mw5biRZEH2W - viJOdlVJ6k4gsS+UyL6USKlUrYUlUtVV01bGQSbATBSRQDaAJMVazM5H3Jf5hPmGO2bXbOZPzg/c - X7juEQEkgARyBUmxxOpzxAQQi4eHb+Hh4UFHgC4JXUNjwrSskUBjqbUfcE+D7tVakonfDE2jI4dH - Cs+Il+PMwRqBpm0kfnqk3NZsVKMK2hgSu8ZPs1RO5vyB3rinUh1oGtCLpNBNXg3YAfd/NZs/jdDI - xtxBDGQ2SQpmO5VHOua2CejoLVTUsmmP8OJaI8BKCnAivASkoZ8WU+jTfjD/jGxoIwzTVgeIK5Nl - VVTpPhS9MNSmmQIsDERHU85A9WcZNBurgco8+66OBNrMgvCqZ73hfChIFm9VUWT18R9xdEkG0eRK - YNE/sBi9KEQmlkM9KlUHEWBCaSpdiuGiPWCcyzCOppPmKqWAVJDntB7GOs2ijGJvOA2cmopOpTPu - QWD9sBCoBp8BVat+OKBeg9SLx0mvs/e//ycsdRQVkUgyrfYayhAndGs7m2JYMwWRhavWRLr4oRP+ - 6oVi6KU9fzzBaPUpmADF97S/V+P+t+Txayjhh+kTkewHAYlRQScE6mDoqSvOTXtNAPDKuPzgDWFy - YodOb0QPq00T6q5MR35CEqgGVBJixO009RLiDAbeJHXCAS0TTeO66N4F07HGRJRcMSf4Drs8BiTQ - q9iq8TlRhKECtK4QnQkJL7dXrVkfW5VMnLAEN0x2Z4/OeA+/1UYxbzlOmOQLZ3AlTKLAH1yVx3vE - vpEj/q1+tJUW9sq1xIYwsjl6mQ+TLPEwkBDvL2fkuoDFGc2FEaM5EDyUe8oNrMjWTTimv9iAN0G6 - 6yfQpD+uBsgcFN7XI7tQc29WelUckypXfjYYQZZOEh+UgJ9WqHC//KkeL+X6e6U6dx87eOWiFw88 - ARYThW+9apQAK7WEX/PGMoat1GsFXf3Yc847e9+E/WTydCNcV/n31ijTpcHdiR8OWSv0uE6JQGcl - GApnWUfn6bSutb26Fj4XqvU+QWthcVekHnfhVTqmS/yx43qNuwYlxL11aLwveffTiyZ8YWMwFUJ4 - NejsFcq3h52N6Ow4jQbn5N/TCM2R/hUj8xWxittHwgZIrjPjVsNzsUoToktl5mzClVBd1eAz71WN - Q+tZD2z9vYcPuWPLd7MIeaGPgQGzI4OrhdP3eK09ttIoOssCYDo8GMAOVQBOcP8aTyA4fVhxgFE5 - OM9kJXxmBVftVsjb5eWDCNtAW9YbXu12vE+DYArssJeNk8NUgoj77QpvJlFCz35k0DEEFhsYpUEO - LO+avtrtpPE0M0ynoZ8ilzGQ+TKiR2k36X2PxMsLwlqgjzMgfZJ2ip97bFl2GgBPeHE/cmL3d9Ow - qqWgTEA/np4F3qffZUnRFhfJpokNq87nSJNVZNuG84ctEE8A1IUn9L2RH7oCLb/pfaxa8Qgbe8q9 - kDKB/+EKPJcDWcpkmk6jZh+R+xcvIzKIoyTxXAyEAzGRjpwUT7qMMQj30Y+PRPI6BVZ3/YFDFzXk - 0rnCfV6Wp8PB7VNgTIeySxfXRECyYz/BkmGU+mdYDz5lS7Gi9zJ7VdyOnGGUH3yjsWki5rEzRcnc - x8hPup9KZPaGsI/8tUTPqmWlf30r0+12KcAT9QL9B9O20X/wpBc6gzCdAU00bLB/6CP9INODXxa9 - o4NGa9BgWVqRZn8Lyh95q6ycVHFIzBznlKI4//eSkRN77ox/QdQ8oCeSODvkq+ojZ+hhBqgUCtHT - Rw+ejR1/dkIFlAQ96gSfgQQePHiI/z2jfT2A/4pMWSx7eoq+DahRLDCMfff0FD0X6LooPI0rzwk+ - k3J77MPEGVC/BQDCR0yhmP0sQ0SLC7KUc9xymIexc+Fjkpky7HPA1IEXeGeIpWcYIzYBnD+4BAKP - LkV6c/kxE/0J2SW1r3//nfz2x9P6OmCKhWf+sKlu/hna+NfPS9oQJ9Nk9Pg3QAU9JvUGTIHX7g55 - 9Bf6yO3XvjXQbbvvyWd9TZV1Wxt4sqy4Z67nnOln9qMuVAcLAdo99gLoIIqxBdqfwHXcao0UQdwh - v3Wozu7sdE68JHA63U5yNe5H9MXxm314xrCCeIqnx1+7nR3Nkiyri3pm5IRD7w2rHE6DoJslYLqC - ulAPpcwwivGJSmZ4BWrKBZPzOcspxGuBQQFy5cL7wQlAkewIeH2oopqaZdqqoiuqbundjtNPomCa - FgrZkl34z9DtrP+Ul5FtXYSXoGou/GiaUPGJbzGJDPQaoQPLZSnZ0s6Okr/a510V333gMNJ3F9j+ - EUzp/sTv7PzWmbCfb/x+7NDhDgIf2oRX3zqhGwD3dGmZ1y68BTEKL3Y6P0myJGv2PmYglTXAqmwJ - H48PeNEDUEAvfS8AhHfeYPQPe/0hisalVj7UNIN49sYTANib1U7R9ujsmPAL1u8M9X+wh5sdyQn0 - mKTOeLLdkIrN8LHJ2dg6kr2jyju6SfbfdmCQk2Ca6JJ0wogPY7KhTHEFxgvQ1RcvxY2Wv4ORIium - pivfpM4w2eXWGDfGThNayPVSxw9O+9OrxAuCbybBrtKhdHfCsXsC1nZG7WBppeX3nXf7x53ZVBQL - 14wH+DF5OU2nMXw4c4LE++Phgz+ePAVl0+uRk/cH78ljJ33in5HHbyPXC0RWlvzHLsEmn6Amx23I - cxhjl1yOrv6+quhaJnvwqIBwRnvbRA4hdNlIOECO677OZc+Lqjiu/VqRxnVlZiNiQwQRiDoKJ/TI - Cb3gHZh8MLpSVQrvgyAa+uHHOICvs+O6f493v1bOGCHAjxRkqECfWJ38xG21Xi//8gjK/dGlpgXD - MdUNS1H4EHEFZgdXfBW1n2vUqh1fM2k8xVth0bRKeaq7FxwQX1Cx/Ak1e/mNEHpDJm7rfNZ8VwaX - D+tpv04taE2HyZuGUjfuUsWR3FDPz8VcsqA+G2TBMV9phanrPaqtG930qzaW6+jFTeB/zANBG9vr - ErQNeOcL++bwNWOrN5IX4LJ5EqgS3gaP3FgQaEOdPWotSNvik3ohJ5GPpvu2bWUmTwbh0hlCo0he - 3N2WIGWm2jogacbXW8HUkBky/9xMIllCC0ygscKYV5+wvYJCXh+lCFY4pCsXWF6NnQDtqj0wArbA - RFP6zHmHWEPpjSQ3Ty4KAnxJKo8FLfI2cClK16PriuNya8tShNCmyglPF42W2YVk5LsgusmCkmf+ - J1j7U/cOPwgjIFqiWOgjo2QR90fc0CSm/TVuzsbUciQvXh4QnukkIdQZM45CL/MRIhQC1MJML8ep - E6eE26iL8qluMge4Fl9aaikrlbHLn1ZDXe4UdYXZohPaWKrXa6rNttBxVVtbgjlYlfqPaF7QtW7D - Z1gD7HY+HluWbEgfcMGCmyouutNyw275/FTnCGxDMDGFNJoIwicqGlbF3HJR/LDshmH+3FJ23438 - uZmbZ4mMXyzD1xZsVcRtTdzoykRDGi1LWCJ5JOM6ofBlkVApinguL1CcCYIrhMDM+UOSpQaa4+31 - hMMiZdGoDVbLmvOw+rPiUBRmWekANVEQ4C5ikXTHnutPi7G+z+gSPU/lQB/ov2DqR7PW+lHszodV - pf3IvSrVPT2l7+qUThpXSzZmi0rdatEm5UOLO4XSYMvjTt4g8JyY7aBRmw/jrkABcckPi7Ar9nxJ - Pf08iSLbX/tLQZYw/4FA8/bNJ9AsT1fqfpajoZmhMPfTPvtB0O1zZ4dxGk380Ke6LhvPe/7m7o0p - 81Xhvs4FjVXJNiL2+Zu7Nya2dR66sH4N4I9DY5PYK/KCv1pzVPA2rspGKmWK0pGOtSQfM+1XvzVy - 3fse+U52MDziO7gHVJi8GIGdWLc3HXvJBMgY1nBZhQ5toFTt//t//l9aBw8kgoXEc+ftEFRk0Kgf - TqZpvrH8i/MJpP45njRlu6Ww8EppGjtqXjFd2CF0xQjWlqSogqQIGLOuCwr+i8NfoclRQ4t/Cy1r - epwmw3fq4dvvxtHJwdve+KfX8Sfr6KiXpJp1/Lf/djmJL0/iy90VuxrEFw2dPX6sWrDs+ytRbFOV - n5C/EUU1TfNJZ0YJQAsUb4xMxCT9dDpG5Q3LktMB4pf8llNU3xmcD+kWww75Czq9LYf8hz+eRHHq - hOksvoPyxOIiTHeuUoZZjDtEnnxin/5gkA5jb+BMwHZ1hL7jDr0CnBkFIAFkdcAAYcOkI+ZeSPx0 - 4cTEnZHTD753OUZXNNklv5EOfXccTWOMZt0hHVk2rU6Xv//BgXHy15nnn3865BtO85WyW3XoF1VV - 8cPJP1AB5WW7WXNd+sQL0dpZOepwx+wruEflXJ2cNzRg2XZWf+Yffn2AJdnWWPH9CdAQ+6bOvadt - H+OmMbZFbeq3WQbIHcTUu4PntAz+hRLP9w/p43MHQyPIczwqja8PP7DXNBibvnn7jr2Bv/j44Uf2 - +CM5vvSThL47OWHv9k+Os9Ukff/xDXs/DYYYzxOS40N8f/DxmL4/+D//K0m8wI3iM3z98piB9DIG - YM6mIDrg5asfXrO39Ae8+Hb/LX3+lqWUZK/e8VdhGF3gthKhuyB0uPvHB/vf45s3vPk3UehGdGhv - X7+gb+Dv4Y/0xUfW0Nv/87/CwcijhX48PPmwT9/+CGAjbk8+MmSnYOwPHQbm8T8ZWihOSE5dSDuv - GHIQL+iHpi9//P6ElWc//sByTp9O1b9wrkB20M+vOf3sXwD9Ozw6FLsbATPOlaINFau/D71/et75 - fA35n3Vl34JOGNUUfjtX+AQ0JyuezJdX58sf+5+aShvzpX86OZgvhy9rYP4J1H4NyD/Vg4yl6yCe - L/4SdFpDab2mcS9sKCxL86XfOp/8Maxo5gq/3Udi+BneH5W3RSkC6jYyWcnZruhM9FAR8DEOaN0e - 21Hq4X5Sj65bqcQqijq6x1qWHBkZ53S5khzMauWjYixInieD6mc6+lwqcTQ1dqKqst3YSUWO1fdC - hd6SXuTmgTChKDyPvTGTDQ29oAxd1otsas39FGRrQxcfflzaRUG1zPdQkdQNvaBcXzYp0gJ8gfxv - bhyVwzIsNeOoojvq+6CKZlkfamMfJUVU3wPVWct60JXmPsCeCUOHHP5I5r7PemFKcFk3ZmMnBV1Z - 3wFVqsvabyanouJt6uDd0g6M5gHMtHiD+NhfPtGq0dj8zCSob/7NCrOsa834yQ2M+uapHbKUiJpb - L1grDR18XI59rZnXmAnU2Dq3kJYNoBn9JUuqvg9qdC0bQXMHr0GiLpgAar0to05LaZ7g7z++P/nw - +seDg/cfXi5AFDP2fi4Z73kbs1gPbMFP3k/T92dME2OED5ZBXzr9tj/2QIk7IYuEK31JfCd/SyN6 - CsuQzByYeQuT3Vql8Q1bq8/KnWZNnAKjRwG8+2YCIETubp2J+g1GGv0Kq7vdQyfBkwHkGJaOrhO7 - hA4FzZZSHB1FNw2lA+sGP+8HwaVzlbx1Qn8yxUAtWn4azcpnYwO7uWwocfuHLsfeTcd99lbCRtFD - 5UGFqvH4rZOwYVxlk5+1/jHxXG/gj52g+JYtFDFfapzPC/mDLWXr1qoi9QnQFStzZ+yQrx4/Emfu - gn9RbwH79vOjJ+KFEzx+0qWFR81lR5Wig/hiJ4tn8vB9U0UomFd9goA/wFX2NB28PztLvBQAFfRy - NBD7RZJ4UD5tNIyiYcDyMObr/Z4z8cVfkr+DbQkm2a7xxks0y/q4j/+9SIP3zrnimurry7PJ/oEm - v5r+9GZ6Ih9/oCEOrMPMATDzIYixB7Pz+PETsrsHWKSnaQn/dvjJG0xTD6DOPhfqeezj40ergvGo - +xtxeBTZIzaZ1M/yiPzxRExBzD5+TLHJunKjAeUP8d9TL77KwtkeP/pLDgEtXGyH4n2K4NJPT//A - GaiM5fGTp3wi/1WEQeCjefQz1C7XgGaezvDHvFJlnxP65Rqh6sw8eo0lmEdrQQHu1eqU3FYP6n1W - Dx7MvFUCd0jFw/5juWuqYJNZXSKJ1pOnNK5uwcfMT8W+EvhMar6XfFQP/njIgeIxO6ej+HQ6aYDp - L5Jt2ZrcUA1vIm2qeLgvKVJekY772P/VA/Ls/ZVQ+UASfM6DDMhfe9BSyT1a6hZPY4wcP27qsK/D - /waFKsIEIxAFGi1AKzW2zUT6KTuED0Keo6OAekUDzFpml9gKRWw0cQYg6XeIqBfwTKNQeC2Qp2GC - EhJ9hs1d5SgsdqbrXQLmIDHk7TqjXz331siPR+fsEIVSXoUclTI54jGb5ZgvF18Bezk14JUylCLI - HsHdXvrSH0TCGQixZJ5AsGruln3wjO6t0ON69FUy8rw8JUSPH0DpUbru0eN3biRQbHvhECBl7STi - IMGQjwfPnMlEiKMo5VcRsT1LWh4FyLNe9p0fCmk4GKJL/N4dfrBEUPQ8hKCy5TzbeHkfkpOpl4DN - QiSlJ5k93EWgaWbxjBHuPlMPCcvf4BEWGknP45BLJyFfMUMFEz3AsmOWSYLbAzRoA+vhkcZCY7Wd - sjawJ2gVLzztspQRPp6SGuIxHvgki7b2tfiQwvEoISziF8W/nzqB/ytLOwE1vjJkQ5Rk8pz0rzBP - mAG/KNgJiaZpghYYXpVQ2G7aFLUzg4CpGJisgB/n6gXu335J8uAG5vTv/F90uwkj0UluPiSwZhk7 - eKSt0+WlsDUs8nL/ezzWlL3H80yHYOGlVx30Q1UKfw9opTtPvDSqKXz/OsmmjoaaOuT59KpLQEUH - eBjt2yhw/55VYQk5PHc/TPDQ6g6p9sE/8OLZSFjzvP2Rg+lFBh5YmC50hhaKFybAV5gXBMgAJrI/ - vRLpjVIOrJXx2Bb/lAxASOEM0uwo8LfvJPRqKaLbWImXS7pEMcgI74nKX2AFWSJ4UiB7KXYK2zC4 - qFkRX//E831I4Ei8uiJcet45CaJLertVAZNbYw1vvYFmeWpnPE4M3RHsjqYYKU4agiNLsmipItkf - AJqQgjMG43qYMViX1wPsIScp5tfEwQOJZ3E0ro5oUwyRWhSN/OHoOnBE210VSaqlibK+BpJ0WVTU - Jhxh11uREcpKHv2QIG7AZuS/2saSrc86is7OYE2ai9y6foFNLwA0gjFFTshzBSNCDUUXJaXLb5lh - yMcBjmHtSYVwV7ckENCU4xxKwKXPCn5k3M3bzj/HHuYqwsU2VMQc/zZxfQTVwwxAOfLpHOeahymZ - 8iT8PNsrzRZmD58x1Znk10KBoEWzEg9gZtbGCBMQFSOqeJgwqQ8ezjbq6y0kRbNBDtF/dOnJ0+xM - SDIdQw9X82AU08OvIJKzM7GsOVb1M5WzaGEw5O/dsXlYWdTXzsbty+8/GeZrNUgT6m9bLdxx3K+o - mZqw/ydXN4XJ3XhtUBcYzvNwwAIcD6yeOu6pfJ1ZPzwXpoIl82DpPmaLwizE7VOSRawloTNJRtEs - vm0uym2lEDqaYbA2hm6ugxUC6GZ1Fp7EHCk5nmmKEyFPo0iqL2hs9IwNsyN/7BzeMe/uWW+kzEdp - F4Ahhf/qipye0qlpuI8XqUyVFp7tmTXEBtDZe+67y0PNN4RHQjZaHx6CjrxrAwqRZKwJ1H5yfl3w - KJsgCeC5ViQpPeZBWROsAxCQ1wWStQPi+ujtmhDhfth1QaQreleXNVQja0L1A3XSXh+Fa6K5LoXj - BqBI6PbfdcG1AZ2/n3jhdYGTufPWBIlpTvLCmVwXYMy3uCZYbCuW5pClPsnVgFvcNuq1Ui6uvGhR - hwpoXdGkcvPAsS9L74JfMkTe/jqg5/glNe+4NbwAGLaoaj4hsXAqYMWAYaHkTXS5/Vm59YZU/4nb - To3DYcuYLUf7LXTSxtHAaIiZp0n2QxBUTZhg/tFw0RDq2jg97TvxHP2uVjNLsbcGwc4q0y2E5T03 - N0BPwxbmkh2F5Lkk2Fqi9UOazYcLV4V/ieBoLt6y0sgOF12bcWSL+nzmhiXqtXdIPuAOzrWZR7Dg - NdcE6mXseeRlEDmYaJB8fV2gaetDdnh0zBPcX5OJZG6wMImic0KzlRFMx0nV7PWhzFbWBO+Fk4xw - Mi9Xge5h4ffMsbDVRnCxFt88fc4zTnj0vODsTDU7mTa3Di7VYuv01wNb6num4QwGsqFpitk/U/qK - Z7r24Mz1NNPkvgo/EQbx1SSNdjsvMYgsS8+KlyMK0zgQzgZOnuxzWE6Si7cPsJ4xHWSWJgMqzDUy - uFqvjTzJ9qyJsB/312sEa8w14yT+YL1msMZcM5he9FcvWR87hYqNja6LrVm9xibXx16xZmOz62Oz - WHOu2VE09tZHaVarvrl1kckr1Te2PhrzavUNro/AvFrtpAwAtTxmZW08urDe+AUETDNxFlpfF62z - xhuotND2+lietd5IroX210f6rP0C5if+Jy8oy0gMH3Fc0Y2m/cCj/dNGXXfco7fG+uNJ752p65Jt - i7ImSZpx8u3h84/Hr98dHh/zC896zxVDVg1LtUVVU1RN0yT9qTs4TePzUwdEu65qum7pupq9HGDy - RmhUliRZwpeBk+7i39iFL09TZ3h6FsWng5EfuKeuH3uD1HOhpuekNPHM0/Rs6uw+HbqTePer314d - HH34gz6csi3MlL88ffH+3fHhu5NT6OqPp0Hq7v59DhEZVdwcHjRV+gzxMKPgG8OEbZjWZ4iJGa/d - HE3YkvwZYmLOZFgBIb+shxELU7Ar2l2RFlV750uXGvPG2s2RyGcqPuYNzRvkms9TjpSs2mvChmbd - FZ6pmOU3h4/PlGHKS6gbJI/PU8VUFlk3iI/PU3g0rOKuAS8aYEBR7ooYaVyB3hxmPlOB0uRVuEGS - +TxFS6NT4QYx83kImUJW1HyPu386i5M8xSx3wgoe6SyZbrVuxc8/v0VQl8HtKeGn2VRJmnx6yu+h - 2iEGPjEnuRdsAeUvidAPz67IfCN1CTv98ZB2CX+3wQx0KLBNgZqGOsQJ0t0OjPeTIQkvfhIsWdQk - 4V8vXhC2NfAzTa3iAXouefpmFhP6fJrgTVoJj2kU3r44EQxV0YTDd/llXtDq7C4vA37TI9e912Nn - 6CW9/unEidPQi3vce3bK78Dh3fQqoIr0njDmpuPuNu5q8wKP5TWOAFJA2BnblYhCfIGZ6fhRajyO - JmIDiZeKpQ2QQ9bCG9oA2SWPMIYTT0k/flINFKzJ6bs2Pcl2Az3RAOOtCYq2sgZFrdtrM0mxnhlN - fQBx4YTDwCNqPUXI9hYUQXu6yyQhW3aRJEoUgYeCTpNpfzNKyGp3alJPl0hgg24qU593xaY8n2cY - 3Gyes2m+qzNVYV5NbmDefgC/t2Ze2soazLtur83My3quMq9cz7yavAXz0p7uNPMqK1LEdHtp3q+/ - T6SZHtbqcxE5TOdFuTJjcaU1YpjebUFulIhBV+YkOR/K5sJ87hqqEnTY1egi3rCvSs2mwM85vbF2 - R3WqI7tkgVLZK6CHSUHeAGJnJKYr65JYoYPPg7wKJLbgVfUxjIoZhPPXhUTKxf/KsJG/bLCmIkhT - v9VSQc4A9KKG2kTK2X9/VMZYgfdZrzyuyo2x7md6mWrdeTDXS87TaHKNB8Lq7nlm58J+l2HJv+L9 - ziXQM1wQdqu1MLudS2AcxyRY01UMiG08sIVlBtF4HLl+esW+NR3iYpUesJNaeJUDn1QnS7uCtXuF - xHFNDcFk0rT39OT83ls8Bk1vhiheM0Ivq6b3KzsxTH1+oK3Hb7Ny2AjoCTGc1OzmvKz6bNgAS+x7 - fMglFM4XOsVbL+Mr0qkh2LrSY0BpVvZBGf66tllm8L1s5Z3dmpxdd8KbwXR1i5pxHU5jHpYspp0n - srajmp09XR/jNdpjLxtGMcHK7L6CusZ9lM305oJOaVp5yl4vgBkZT5NzmDyc6D6bSX41ph9eeNiS - 4IKYGtNkewKIm1CgICo5ph5QZVRK30b7TRSR3yAp9jmO+A0PNKeboai2bWqeK9u2CrasbDmqKTvW - 3/EecCfd/WXiDfN1TDbG8VCYHWskjUOezSInLWQPdRGmaCKrzmpYvS58HkJ18haqk8S5SkBzjTzC - 6tKEArSxR4UTwE4C6pgM8cZumsKO3pA+cuIxKoZhPA1hSC5L04MZB7w4IRjtDMUzUIDurgoo6o3U - 7OdkIbI80LvxZ4Qt3yVZCiMQ5liWHEFZ5ukIAsKyUEF5h3TOpkFwRZJpgqdy6P0+Hlgtw6sM1Yjf - QzBlRh0MDHfIPz03ZMmV0ksPrI0iviYrShZ6g0inQZH+yYWYsqPbnT11dO0yLHHGQjL201GCd7KM - x1cJXoM2DVLPxUM0VKyEA68t8aV6qmFYlm0omnIG4sv2TM8w+n8K8bUuKme8OIaWQOgcO2NyjA1g - UrHpYCSARAIWxJQor1iDpNBOl7EWvej50WtMjeakUDEBwyvskkuPpToII7D6WZqJyygGlnD60TR9 - 9BnIr83Rxa5s6yPGOLYK5VHAD/0LmuSvcxi6eMDRRc1wATUSsUNTNBTLY/6avueFuGLzQMg5aerQ - 6QBBRveA4gt6KSoZ+PEAM+bdy7J1ZZlqXKMsC6YDHxNaQPGEWd0CE0ECEHoQRecCOwwXO6DCuNac - UWFbZpluwf9kVR1IioNm2cA+A8F2h+VaO2jNDAyWAQcMMjC8aOqnFFfzA+Cy/hU5/AEtsCDwwiHY - IG+wY7y73h+MsAZwDkuU4w1GySN6yIvQTlHihecJirZjZ+r6JHXOPcx6T2CQAPWtybh2UHc88QaY - aRuXdCDDnNnwAGVswPux0/cRvUfTfuAPgHfR/EMoyUtYalLRdjb10KClWH2UcMzxdEUgk+AhT1aE - Pd9Lt3Wkm0kUdUdWO3uycV3iLUmnmE1duBxFAg2jSAq08ouHF+ECI6GOF6LLULj0+tTz05a9Zvfd - M6uvmAPJMdFeUz3DlJQ7LNe2xCdaD4MIZNXQI7wpEFURYU2R3EhBoyT2LzD/EzRK5VjWLsGEspgm - +CxgPiAaHAPveV+3Jre2RM13DiYuAmkSelcgelzMTeyzdH4O9QdSnyQZoKR3yf7BsfA8v5gLhTjL - 6MuQAmTDSIxKP+BxEGKw1E1hAF34NgimVCfMbEJMy3WAd8HDohYGPLmXY+vJMdne0fTOniJdlxxL - L/0U3RFzDoxpEAgx3jbv0H/9IIA5d/ySN8NzgvZWoIpl2/3iCnRgOc4dlmitYfb5rMTM2YWMxTxn - 2B75AAz3HNsjE6BJ4OOvZEkiY1YTagEfF2XgCYMtt11uTba1hiRUAP06RGF6x0kcjX1cr+NiHP2M - l9EUJNIYRk866WXEsj1iEgXksaRDVQG97xuFHE1uzhHm56ZcF9tJMfnpi3fPX7Qu15bVzS7tzmuA - zGEbDbgbs2Xa8s86NeGd3igrXdnKr6ABYsNZBQV8hOuPhOw2fvn9d/Ibvz+noQhI0/DMHy5oJC8B - jf3r55UaEyfTZPR4tm8be0MgtOwWlx3y6C9ZTVbvUTcvWm1yh/xWoGd6qdMOSxDf6XaSlF0Ni+++ - c0KRoCUPDA6fvNDlH156/eIHnqG1cB8jlKGXtxb6Kdx8tFO45LZQAu+G5dCw63C7fHPwhRcESWfn - Xx3afbeDmczgD7tfqQtvfVirYc6k/OFNdAm/WWq4zs8AIf4KF7TBa1fr0SuRoNZvrPyOTLM7SZKU - 1YQ3tmhb9A1tY0e2LdG26QtsDQqoosGeebs7HUVVuoahdHFViZdIdThe8boFtQc4/aM769EUFbvc - oyUqZqlHQzT1Yo+KaMiVHmWzCyjvKpY516NS6dFU2eVWhR5lUZOLPcKYcMx5jwYgQa32qHYtyeiq - NWOUKz0amqhJpR4BBkUp9mhqolrqURFNq9wj4KFrQa+6Jpd7lHtqtUfTEqXyPBqGaBilHm3RNIs9 - GqIuVedR6lqmBeM053qUqmNUsur5GE3RlkpYlUTDKvYoi7JZ7lEFjOq21DVktdqjYlZ61O2MEPMx - ShXKgR40pdCjjtkOq2PUAKt6V7KluR6NSo+aIkrlMWows2qxR83ISIn1qMJEmNV5VLom9Kpqc1hV - 9GqPgIbyGOGNZVd7NAo9AnXIlXmUdatr2HZX0o25HrVKj6ouWnq1R85+WY+6qJrAD/kYtQztM6xK - aleW7a4iz9GqUpUAigXNlXpU1WzaeI/wQi9iVTFFVa+MUQU61fES0fkxVmkVqyulHuGNXKIcxS5L - OQX4syJzoEJXseWuZc1RjmxXsQrMZZR7tDIGz8ZolLkDQJKqPdp6F8p0Vdma69GqjlEXzTKt0gTU - pR7lrAjvERNhV3q0jK5mGl0bc4ZVeqzyowxjVMtjVDK5l2EVJLda6FHWRaNKqzA+U9aQeuZ6nKOc - OZkDwlgvSTmYN7nUYw1WDbsLYHbteZkjV3WHMicBlKokByzaWhGrUka7hTGC7pBhnOYcd8hVSa5A - kTLlwORbegWrZlECyNqctpINs2toSlermccqdwDi5bLMkXNVkfUIaFbKWJXnaBW4A8Zp63NjlBh3 - /Fw2nzo7YDJZVhfDddm1did4rV1nh94iOXuL91vFEU1QTz/xGzmelG9JrAkWYwFwzXm6szzc3+b2 - JGEGZRZoNQtXLZmic4F1mL2Mxux15tZZ7BONyTNI4WGMafoLz0n2jFnQ0LznD79MEzAyr7LwPoGa - s/wbS/Ivl54SQc1iv1jIaB7Oyp54HKmAye8FGnbG85Kz4BIBFqrUU7HLbWINbWI5d3XVB60B5jy8 - YjSPWKspzTrm6Ss7e8XWmyvNusCAxtNyWBxf3bKG1xpzPkYw7gEK/ZrGWGy9tTHWL+fbJjMYasOX - cYkE58izhkCVCoHKCwm0iRwPoksa5Fw7TRXEZ2WXEwpv/IOXeLN7OPm3VXqiFTefK7mMG7WCqcwF - +iylcVi8Pnug/wrCmf8JfVDsIcHo5v7MzfgMJSi6bzDaG++fE5/1sle8RIpCsdTy6Sl9N/PCpXH1 - e1xwzaajudpZrvd0tKQYy+C9tBhPPL603GzdvHJhmoZ5VhZ+x7nLjaIhm1Vac8GkznwSbMc3yRLz - 8k2P5knXKpOuZZNOiYpqHMIboVvCCXWRwKSzS6B3StRXJNUlIAn0ytiklsrxEhd29YvATvtKXzcL - MM6x/CoZKFvmh/x3wa3IA7yLQfQbOxY3CBPn18jQ8w4r2Af7rDjBC6qzWzqKvbLmMM0ou4t31kX2 - Tvg0SNwzTNaUpTb9D0HI2k2IIMwnPC03cHqaXX2TH9IozUG1NBdPucvsJErBusk63FlcmRJGZ8/W - s8nLD0XkT8ULPnEoH9i9XCuNhN/htdlAnk+vss5WG4VuN49izb7fedM0BjSu1b9itNY/vaxtrc5l - afUpZNcirzKBjMz3qg0cRCn5BigtClJ/Qk688QQvjy+3uIg1hJRV7TT160Ypd6KXv1ARdF64QGAh - avJeVoGF5VVPCpOwSttZNvZ5mXpJpZDQjwJQsJwdMwlOfvtt/93+m5+OT04P9k/2//gjO/7Rj6+l - e6pahBN6L1YBhqMPr18c3gwEjJYLfX/YP3n97lW58xVI9403RIN1FdoNaNEVZQ8ty9NwU2ytw7yz - ymTBN0Fg52X/yhQ9m41FY30egXk5ZrnGV5O3fVpjpu43kDxgJnlAqKvJHHo/WmtC7y29Ym21rtnN - bq11jZbk6sPmt8atIXCjMY3fW0nksrI5cH8tXrHJbpLjF9yxiBkgDbzxgxsNOQF2MUic4KKN7kCP - I/iBF3qGsyKE32DHoqLpfYUIglgeQiW9e+aG4Xt+rC2PapQX1b3Duo9827C5RHG3kG0g4qbf68qm - H3Mcsdtu3p/t8xF1dmx99hYMiM6Obs9ecK3e2VGM2UtUtZ0dmfm7gAYodzLmZB4+7hqb+0SJoNth - VFv6xIhT0xVDNjX4zzK7nf0gKJTZH0dTmGOATjXRYW3blqGbimIAYG/zjTvuo1NsCztiLrwcHv6T - Ounor8w9Rx8O+T2FAIym4wvuokMHhYwuEHQK8vZVKGHrqqpu3IdM/cvVTsxSJ7otSvJWnaiSNN+J - VerEtPG+x4170OZ7kGW7PAxJ1K2thiHNd6JIFVwZQBOb98Aps9xDGVGGIlrKdrOBLvVKJ5XZ0BUR - OW3THpQaRKllyrVt0Qam2bwTUzNFvdiHIsmSWkWVKW3TxzzhYid6mQVVUZe2I6sqC0Incmk+FBOv - PNxmJJphiUoVXRU+l2HStXaHUZ50xZZh0reYDX1uNhRZK/WAMQH6Noiy52dcUcozbsmiJW/Th6XK - c5OhSaWBqCpuim9FVooxNxKtTFaqBgyy1Ujm1Qd2Upa7qrGNLAHdKhpzyKqoQRWA2G4cyjxpaYpS - Ji3cCm6TrnRZKnUAs2FsxeS1fZQHgfvZW0ordW7G9Yq0gk40s11Ni52UyEqRbFG1tuLCmj6UsjTR - DNG0Wx2FIZUlIrC5vhXlmnV9qOU+7G3VoFzXSXk6NH3bTuqwVZ0PbSv9Ic8zuVHWgYqKPNiuQY2d - VPgD7K+tGF215vowy2YJEhaYJlsMxFLmkGXKFeI1RWlzg8GS5PkOKpRriep2K4+aKTcrJoMG6wJt - u9mo6cOqcoferjg0K+LQlLfmP4ztqXRS0bMSLqG2MkMVrdqHJZU1lG2Ikrn5XKiSaKmKZhuGpgI+ - dKXSW9mWUywNVlOb92bO8aGlVOZFE7UtOpi34WzJKM+JKqr65hMyT1m2bJTtHlg5yJuPQJvvoIIi - zRa1zcWIqlZWswqwi1YVI/Lm3Kea1RFAB2UUKYpobyE/5mQU8GLZNJRA8W1uiChqTQdlFEmmqGxu - 3KoKYLhipMN6UKoiSZM3p1S1ZhBlZsYA8S2sA1hjV9qXlZI6knVD3BxHypzdLFcWrzLo023ax4D8 - SgflKcAO9M1lqzLPCEpZQaDts4UwUubEKXSglDtQxS1oSK7roDzHMi6JNx+Brsx3YJVHIG+jcebX - qdBBSZzKEvDi5u1LFQypVa8aLAbAsmxvAGrVo0YDlLdxEs21X7YqWcj1Nj7B+Q7M8gAwbn1zQVdV - ydhBeYrpAYctuGC+g4okpWc2Nu/AAC6oOG2wD6PcB5592YJQ5wehlqWdjS60zTuoruNUUGgGDZCm - x83ihO70lHeWJGbtvMaMtKETsL1zqD6cpnmwtWzgTRqSbusFs/4N3dbc6WDcN56NALna7byIxhMn - vILXL1gGVnLgTbxkMMLDYx88Jyg42AVZEhT5RLZ2MAWG/jdJ2ZEYOkoQcvabAzDkG2zbAPnh/cm3 - JIP0iCXSTbrkzZsXG4ObraaXIVRXNU2nAf51sJrzsL6KAnfshOTYGYySBvDME1mj4GmN4HFeXQoe - 3VFsAs+aB+/Ip3eAO6GLEfX14FknioTgqc3gaauBZ6oa3vJSBx47NVMF75+e258mI/I2ioeIRG8w - ZckgyOtwINbBC/9nn8gmhVf9myTXwys1wFtDnZZlA3XadTArPalmxj8+P64DDLMtnMg6nWe7ETC+ - L7cMkZZtoWOsAaiaeX7nXZLjNPa8lGCUshMPRg1AWieygUAqzUDyfb0lQGqSKVmNmKsjxv14OE1y - +Aj/QOoBVTJAF2BTWYksNck2AdBaslTo4coqoAdAJigdyXMnPK8HTwWZY1PwmqlwRfBkRbPqZQ6e - plERPGUN8GCOZUFSTxQlkzn14Cl2vUSElgNvMKoB0VwdxO/EI5Fz9TIAzWY6XBF/miwZchNw+jxw - b/1fp6OoAS6dz6vaDJfUoEvmpYsGdrxhyHW6j5/qq8L2IvZcPyXHUz9J5rUzBVG2MgGoN4HYxMF1 - c4vphg3JqgeRqbwKiCChhzQd/osonsxLaQokKj0mZ6wmIG15VSHNYFQaprgOxn/ioXvyEnqKyDcA - 5xIgtcbJVhvmuhGT60C5lEtWALCRGitcohqom+ukNT9hvgElohCkGk9doEwaDMUKeHgnnCHVyWhJ - 6cnaPHiLLRsF0x/JGje8lEYZba8mYkBtA/KUeuiUGhFz4PiXTsGeqQdQ0TP8KY30p65Of7aEeq6W - k7WeVIPDJfSnwf+dyCqV0laTbWjaK3OxLdmGbhurw/ciuvRCmqmIv6tfBOSgskWA0bwIWNkspLDS - w8urwlpnFpYBU6RGwHiIxRIy1IE/GuxrgKhOmyxlEq2gTdQtF08Anqk18LDGzf81RYxGrX014+Em - 7NnySuDpplJvr+J81kjA9xNY8Y88fww4RCUi1q9KKJywBmWrKDRX6+G0GyZ5nvooqI2YrAN1FX1X - ALIZmXwnahV5w6Gs1Xf1UK5gORSBNBuBXN10XR/IpUJxBQDlJvdIlSRB8TXYXgCcsoHpD+ApBcur - gRJXloO6IttKg3Gt0+QCVQife7EX9r14WAedLsgS18lqI/LWgU41F0BXg79V+ASAzFDYDGQWvrRs - hhXL1poh3ERk6wWRvQC8lQwbXQEt18Adeq3IXtNjw+DNZXgjvNZq4Kqq3WTh6DTZzSZ2mC4oy82c - VRGK6YEMqXYxb9DUUVUIFzsQATwDLO2Moxs9dObqPAPLABCItbasUbuWX90KM3BZz6daWdsKq2LS - tLQGY8egSUZWtMAAKDuTOc02xIrTi8LakOR6oOoIcKmJY1DqY8sUuRlnq+kTA1ZbhlTrbjBqV6DN - rhCDLj6pmFG2davroOakRppTaqTgUj1nUAehtZjUeHjbSnaCKamgimsXJ2atF2mJnWBSVxJd3SmN - KxIeG7cK25q6btS7/gE+eUOT0BTkTLY0e/95fN2yOTZN1aCJ+2oBrJMry/aiGIAqn2a1efm0IneY - lmI0WNVmrY8hU3KpEwbeVQN8GncYLrCpG5z+dWRowVga1shmrb3w3IkHAd6DwtHZAKWVabjm1fGK - ItBSgJcb53mTVZ5JLQRt8SSbar1Ls4ZVLE2RGhzqZv26ZGUNZ9LNRmPxbh7M4UqIpGDWCusGMFdi - mBmAjWLbUlcWOwhk42xvuhItANk84fbKO8wcyDUwiVoFb8bYh6W9P3DIWy+O8Wq0N1dhzcZZBeJm - tF4nxIuWVStBJ8sNS/saslwLtGYbYiW4eCjvStIR1lJNStqiqWA38DhYgqRklkSjcFRXctCBfsbw - gVrcWbV+4vWXUxb1Gi9hcNVSK0eKV4O81jlWD/ly3ViEtNEppqzsPqYgNhi39SAu5JkCdM32bZM3 - rIo802pyFgNkNSpxqXkL0NmZ9dgoIVckSkUzpQbo7B7a33NEGQVnXnMcAYBngwGeLfQawVstaMRQ - VV1pWNPbPbkGvKXIswXZyDYDGhf06koRVrBwMXWzni/s2qld6r+x6dRai9lCbbB2qtDZkmnW4k6W - ancplnkbZKmwkdKMO3Ml3MFa1LaUJuhqZrZRkyBYBucHrRmslZYB6AGxzfpwEKnWpblU1AF46NfM - 40EaRN3KVhcupmSr1ouEINbM6wpWFwKZb0U12q6qvpLEM5QmtqAIXJ9pMRAtcw0vYIvVRApCZ9VK - vAbo1lbDRXAXCOiVvCMc3DqrpgHcD89f5PGR/BaKZTA2WzYNVDkHow3L2DpHcQOMi3hZyZhlwRbP - imCZCqCugZc3MlsQPH3pkp4f8liFl21FAnFTizmWLn19m1WWBSVztDbHTykr7VCYmKalAYX1kRdL - dchKkRdNwXHzGDRlXQMM1vKHUkt7hxdePIjiugUyRhZKWZym0Ri1YporoW5dwFbYjlgNQKXBs1kD - oNUUR7oe1xbAag4dVVYyDEzZUO0G9abULjGXag9lttJUmiMeV7LmTdlE+6A2Xl2p3ZRdwS+DACrc - nS43hu2ttkIH00BeQHY1EmV99aZQAWMtIUJ2SGkZuIqiy2Z9TL1S6/x/7vixS46cFDo7bzRnlNkO - wIIg15VMQlNRFbVBRiu1Vv5yNaJQM19bIqKllSYcCFevV755+K26hohWeQAuF9HN0K2IPFvVLbkh - gJTtnKirCBk1i79lDqPGuFF5ZTvaVA3ZrjcE+YUTVdCW2tEUSFktmH4NQDYsLxuBbIj/rgNymYFQ - gbGROdZBpI3LzAYKlM15GNfzsDKIzWx10hyNu5L9b2JIuFW/dZydQylDu3jRzqDL/DHNx06Uht28 - KnSGoVpK7WaozG1WdR1hQ8HLnVlaYyip3ABejaN1fRgX7YauDKC2qlFtaqaq16/h+c1OG/GMYpzI - yuJDHaud6QD4dFgu1cYJ1MO3porOwTWzwIGtxRADuWnK61C62q5ZBdTm2V9594SBWh+O3YTdVcR6 - DmRzOLa8mt3NAGwItq8FcLHLtQLegvNlq55MWR/EDST6ChCvyE+6ZOuW0sBPqlQDbaPvn0KmSpk0 - amQdZSXnjambktmgGRXu5NyW0xXq/Kyoxp+7nW89x6VXF/ALMEt3F8CAaAJddtXma3eHPGrKf/7o - Yel+pVu4nOE0mvghgMkvqFjjmob3vGJ2i9Pc5SvL776Zv8fJjaOJize5Zz8EehNO7KVA6SwpPbuE - 5cwPgDp2O6qjDVRd7rt9XcejA46r9h1DcdyB2j8zPLWmkpBeTbxZV/XX5GRf8RoWfzgEG6XxZqfs - TQ5o/ibJ3/DLO3iOcCEZO/SirRWuvtkPglUuNXLiOLoUKOros5D4v3qsn6ZbjubuYJ+Nmc8RCfwE - eS/7C63SW1EoMjGt+uxak2mQtcLL4s3R9O4PTtGBP1eAZnxn88Nnxgdi+7TbEeTya5r7G0g2qOa6 - z5tKvU9pEVfPeoE/u5lmnb6N2q5R9Czs+vn06pq6zjTIwu6/hWXnNfWfG6kLAcBM3vMAPOtNg7nr - sSv30zysERwXXpzSG3VAMEZBgOIfL7JZ9X6m6QSzqydeIqDgYhIwqZUE+LCCEKHgpv3IvapecYTv - ciZYeIUTfp+7Asotfn/A9ZZaqNJL3XVa2LzmYuW4ebul29DHztA7RfnErkhC2eqEQ6g1jD0vLN2n - vlFnbx1Qc/D/QAvAkJu38xWYIGvVJiPfdb2w1AjIjNo2irdfbU0x/Mbg26CY3NC7I8TxcQKmkYti - ItqWOiTprlCHci9PPgt5ck8xS2vWezVugFKuWiUUNMW2oBRZ255SuMn4p6aWkmPJGU+eAtV8cbSi - 39PKKjXLPr4vTffI1p2xZW+NQha6WL88sbI9wXwJYqWys/XFkYlm3Cky0W+JTMqbyF+a9lGUu7Py - uS0KKWzhXzd5xJ7bKnGg33cbGdICdWQO6psgEQxhuRUSKcXJfGkyRL073hOMyLq3YDfrq0XTRL1T - C2MMNbwVonk1HQ5ZorU7oXcOostw5sPfTvXcLbVzWwRy72W7Fyar1vzil8N3ikykW1sO0zMIX5oJ - q7Tgqr8hE1a6NWukeHbmiyMQ6W546fmxLOVWRMeX7Um7IyKEny28FQqpHGC8OYv1M6OU9ZB/m5Si - 3hKl3McnlYxX8+5oH+WWKKZ62PxLoxHlDtGIfEs0UkqX8OURyN0xUKRbIpAsQcoN0MZn5R1pIwD2 - prwjt0kf1Tw1X5oMkU3zXobce9DqJMgdCWPE3F235SAp3ZvEF7/iTS1oWtA35Q2+7VTOXVI3mOzt - Vgjmy3a5qndEosi35ya5k5t6xZN+2+7qmXcnyJUn3rx3za8MdGsBai2ECNyIJJHurdbVgL2njHun - 2c0QSAuHbf7sBHK/V1OimDsSDX2bFFOTp/2LI5M7ksKE36twS4LlPrxVuUPhrfyakPsVzspAt2em - 3Bmlgym07lc4N0cZd2XXl93LdO9mXbGzFpfA60UT31ros317ka1f9vHOFk5O3BCByLelW75oH4l2 - R1a8SCC35YB/53nuyBnny5i7lkGtvT2brUnlxk5h2bdnq97JlFjtZfBUjTsiUazbs0m+aJWj3pFg - Z34B871bftPO2qOYOyRS7qMEboVCKnfD35PLkpo3mWH88/LJ23fHJw8EIt3akaybjU77zJKnaYq2 - NZXcXBYb8/ZOZd1nFof/vjJb8MPemFC5TWr5ws95WvIdWfrcIo3cBw4AndyhwIHbpJX7zIyzhr6y - b+jKAviLV3PBT/iFjdFf/MKxhw8fZr8Ljax552K5xigNHFdgpU99mIYoSU8d9zS7yY5+h3HFU4+/ - mYZ+utvhwWnC6zDxXS/uHafR4DzpfT+N0qzgJPb6eGOj9EnaKX7ujT039ganeMfi76ZhwefF0Lle - cp5Gk2sEj4NRA+XvsmLI1U+ArKAfObFLxzADvvY+y4wEVrii8jBJgSNSL/kr8UPy8fig9o5KIAuB - 3dE5uxqOv9jtnEGjiTDx4rMoHjvhwMsuyAydmgZOT+G1P3RSP4ez0tOsi+a2HxSvUYSSzVco0o+F - e/wQEnzc7Vx5ThwAx/9E/xbvAlyp+r+nTpx6tIXvs59L7/ODsbPbJOuxy66TROkxu/ByhRv/SLUF - L0znJiq7TXQ2cJxkyvAlUbLGPYHZ10EUCIqAtxsKTuAPQyHGy1OLX9WFX7WFX/WFX425r5z6UiT5 - qpDEdxnVLVCUUHmuZmfvm7CfTJ6CkBzlIrauIKEAcVDwcty1a2hr19DXrmGsXcNkNR4WdQf+QJxy - lG917WOtTusHzuC8Qy59Nx3tdhT9687eB+/CC6ce02S5UsLavJQMpeZbKo5mpspkSe1qsjVrpqRn - N21U1buqLrfcqGF1dclsuVFb6xpGy5Aqtt6VDbuu0VUN1VVp4cC/AGUbuu0QgyQWoidaQcVdaFG4 - geaua97JT74XuOQx2C5fP2mHCG4CHV9Uc23P/eHRcTszrYlti1NdNPR2WzREu2XprIuW2jaM1o0I - +6PeIfmAC4eWpt8S1Vq4N29S1RpQsXmTiiXqLQt8VRWttlU+QFlrRLXO/M9fn7Rk+WldXW+ZWxW5 - a5tKy22aXVNrebpUqSspLY9dU7umpN0UDRzst0QFdleX22ZZs2sbLVOBanWlQhhEO21eB2XhIuVG - qOCdl5KjODrz05YoQe0aWsuaEahLbV0eAJ8ZLS9YQW61D6fetaRa6ro+SiD77i/TJPVaWg8iSZjX - QBKtTx+QhN729N1pkjiKPeHE+dSugNC6ml4r2rbAiNKV1LbbtLq22vbM2V2tXqxv3qYqd02rdgF2 - jQLicSYhWvIWAFGoRssiAohCtdufQMtsW+zcaaI4PDomj99FofBqf//oCfE+ieT4/eG9Z2EbGO+q - Z4HSAqWDduZfFbXW57/t9Zohtr3toYttGwyGWK9w257/V3GU0KMY0bilfSUwyWS55RlTla7W9hpQ - k7q21fZaFda/Wsttao2elbZp4YWTjMjLILokZ3E0BqK48JLUD4ct7S4AbtSW6UIAPdz2HAqy1FXq - bfVtGlW6RtuuxxvZc6gQxfuJF9MIlqQ9L5TSthWpdWXlGjxbbVOvgj6Stj1bsNA0a+G8ZsJ46YdO - OGhPWmht2+yCJbe8myDf70cXyYAmJxk5cUt2hCa2vRI0W9/1gpVEy6vAu0gCL2PPIzkdtBWd1NXb - 9tCjx6JtlpV1MBbadpRaXU1qW9coXf1mHJBlamhbKiii1TLHaWLbe6Ga2HZcGaxYb0LVtE0Kz6Po - nPzgBFOvbTKQzdaDwRSl9XADxRLttiMY5NbdITdCCuiIPvD6LW1ICGDiW3Lbqlc1YC3SNnJ1pSu1 - vUN1F42ELIsQPwl34F14QTQZe2FKDj9NxJa8j+0HtoDN0Hq8SNfSWl726V3ZbNm2xUjmG3I7saO0 - QAZe6PrptC0lYXaNtt2yFkiI1pu029Y7dtdqexv0RmQEpjrww2GXvPJCLwaSYLJi3x37odiimNC6 - Rus7AF299S0AsNhbJgwDeLrlJvWuWS9yWycOtB5HUeB68X/95/9IyOG/p3561Q5B6GbXalvKm0ZX - UlqmMtvsamrLawsbpEXbe7Y3Ii1OItQa+0nipS05owEVZttOelkBppPbPrykq13Tbvvslq53Fbll - w2QxJTzLD2DjT2xqr3ACGwu0fwa0cHqVQfMQzxLTzkuDws/PBlFAUwdRCOEhK0ExIciScHHJsCI4 - 0zQS6IHdAfbfWIEX+OJqPeuVcLnkjOoCAVF7QpUk6VXgcXB2CFLo3KHVpSc9GTkcxd6FH00Tws82 - k2f9eI+gZQqkJis9NU8RsXbTL6ZxjDiqa1lSacvqZi2/gzf1zRo9Vdq42QxgPCheh4dtoG1qUyu3 - +WyWqyE7brv0tG0d9ZD8lyCE0UyM9KPYLZw5R90TBcnECXc7xryU6+wdOnEI0Caz/AEzwcZhfcYh - rtNjZyCG8PR2FAuuPxaGsQNAv4tEmvsjdIKrJM0brG2gik2aX2WtGoq5dg193Ro0CGK9cRizGpvi - cR9m1Rl6+dSsBYAsyoqNeSDWhVwSLcParKItb9QjBtnJm1TURUMzyxU3xTVl3/1htC6qqv2vOjmS - qW6GY1PfqEfEsdTS5GyK46NpP/CTETlYm5ZpOk6Jyuf1gAf5u1FFc9OKwhYVZnbkpgJ/ubznORdq - xP1sPXOjAp96Ltarsbb4ltcX3/I2SuVWBL6CCSA2kr+K2rU3E/m4b2FvJLp5uo6NqrKkHLco92Wz - a26m6mSra+rGRjWNrq1qm9RcWyTVzs290L9Oof+w4DzIXQdZhp5CHrtStilo9JQ2SkJMTZaVefBX - UhUkCeYEe+sHgR+F5LEnDkXCFcETEsV0NztBfyT/liXleCKS42gKa94d8tIZpMdeluCymAmL/+I/ - ts4x9+w/BOEvh2+PTn76cHh89P7d8eFfBGFv09Zqs6rB6tofnF+x3FeTyAeqHXl0cggmdfNZ3rdT - GDHgIeWrsBVSsPGEcWSfV+QZ2B6u5PahToV5t80aSbxmTiIoc+YHsHZmD7sdPoSiy4I/t/Sz7A5Z - 6g9Z5jGtS9r18J0z9jLym2W8al6jo6uGWkYoejaqSHkiISm9gGWbFkZesFn9I8xsuVHN9GoC7Jx4 - QdDrT6+ebNRGNMHQ6GrVortzZR/G4hl/UC83i98fPHPIKPbOdju9hOVT5HyasSmdpbjXd4bAE5Hg - hG7sXQquIEu2pWlWh0ThIAC+B7kBsiJMT6Dm+dHrx09AWCRpHIXDveesLhiyWJccPOvxD896ThHU - kid5gcTPETkbhQRqzciUTAvtYWChVEoltVVzhtZVdFtc9xrlRuiMUhTSVm3hbiohpJ3G3tVnLl51 - T6ehu3t6vWl6NTWwp9ujV0UqnqXcqqnn06s/IbWe+/H5KIpD4VdnAKrtCqjVkmxDXYVa/8Hrkv/G - 6l4PtUpqi9RK0zG0SF0SrKvk9qSrZItqS+T6J5Wunzm9ykpPMZFe10vS3tye2lX19qSrImld2VBb - U+GyJWprXt9yL17/3OTapjGA5KrK9+R6b7vy+VO47doWuX7eay3ZvF9r3dPr/Vrrz0Kt42lyLnhB - FAqWKhuGvgqVvoU65BDqXA95ymqL5Klh9K/SGnlqito1FKXVNmVDFvWWhvsnlal/dirVDaurABm0 - SqaG3bUMrUUyBVnaksvjnkzvJJnKXVWyu5KktkioYJ+qVldW9PYIFewHy7on1C+bUG25K7dKqHpX - B2NCVqwWCdW6l6hfOqHKutmVjPakn6ZgDBeYE2p75gTAKNr3EvXLJlTTUrqSZbdIqFZXsvWuYhst - Eqoh2i25Du4J9U4SqiHrXUkzWyRTzIVtdlWrTQvVEOWWnP73ZHpjZKq06ZdCc7JFra9KXc1u2S9l - GqJ57+v/kqkUT0GYUns+JCRTHRM+m+0JaNnUitnu78n0yyNTU5G6dotmJJKpYaqYfbpFMlVF3b4n - 0y+YTFUTVjxyi6apin4uDUi1TWmqiMb9ZtSXTKY63vKgtmmbyl3TVLqG3eJmlCmLxr1H6ksmU1nD - Y67tRZ7A0gkD+7qG0p6PSzYlUb/38H/RZKrYXdNuU5rivT5AqmaLZGrYona/hPqSyVQBpa8YLXr3 - gUx1W+ni6rw9MrXupemXTaYarMp1ub0teCRTCyWq1l6ogGyY9yv9L5tMVQuWUC3G3mmq2lUlHdps - 0SFlGPdhp182mVp2VzNbjDpV0WVqdO02g/kMXTTvoM5/lmfpfTafpBeT526bwmaTpDMuT7lzOnCA - el2Hj2iFrDNZsh7ygtf8U6aduf4EM3k6aOnrzjo5Y8pZvDfNUpM1YtPO+ZRu0Q4bxV+v/M0yzxRz - Y+89ZHlzB1fVhp7dXCaYHMl0XDNeX1vilmpTDmpH6AifVTMsddt8Q9tq6gXTsN5ZrvtpuKZpWE+5 - 30/D9UyDvN4i834armka1tuHup+Ga5oG834aPodpMO6n4XOYhvW2c+6n4ZqmYb0o7/tpuKZpWM+D - dD8N1zQN96voz2Ia7lfRn8U03K+iP4dpkO5X0etPw7LtDSxa2BjAqwPI2ImHfiik0UQQAvjtdfb+ - +hO6jvFal3TkkQ9eMvFwB87jFzm0k+y/dqeEjnbhxsfDbH7zPQ86btz2WHnTY9GuR8NGB+13q52H - +sz2hxcwywX3fLHUjAhIscbsgpjVK70Oz6K1KhyUeqhz9m/r7a/n4vxOug/eJIrTIrMUK9QADZXz - 68vIevW+lwnmDSaFzgPPSbw1e88vH9lAQF4LNvidbOtiQ2kFG+bnhQ1hXSyobWBBlnqydZexoLVC - CzKnBa0OC88W3JxbvEB3EyXTpGNW2Fs/cpKUUPGcfEnqZX+QTp2AHB4dfxEKZk1+kEXZplcpbsJG - SgtspLcrTAo30SePyFvPS/H62jVlyrUWl6yeRGWHctOy4yGvgSE60HQUBY4w9vPP8BW43p+kxPXO - vDiLeLn0Qze6FHmFF1F45g+nsYOX6ZBdsujr77+T3/54SuG9cGIycYbeCV7ks7BW9vKIl36aV48S - qPnoEWvQPyOP4Y0IFvMQWPM/dneJ9AQ//EbRU+gLStE2/ni4ZDR5zwEgboxiUpxMk9HjrK0nT9dt - 4LXLm3g0Q/ejJ0/ZBWkM13sbzybeLdagDQh55mCcYd4a/BYmTugF7N42Un1Bb+GciXwWBDUrtNtx - XPd1mKTxFIfFv8M7wc9fCmdRPK4W3BMEhIYQ+g+FmGmnDLJpAoPK7odnnzqsBhQvDK5UbhBECSia - aZqNlpdnb7Iq/In9EQR/AP9GYXCVv2GXjQoj33W9WblRBDpQ6AdTL3+V+L96fCVZxYxAYZkbdg4T - QIX3l2YwIRCEQkLr4Vzi58IYeqzTHAdsrpchxKF3WxVxkQWe/iW/NzWIhhHBfwRYIJ97aSLwYFT2 - ko0S74bvkNRPMeKNXwGHupD+oAGmc3CBLUHnjc51ZbLHjh/WQxyBWCxgqnFow5iJ8OLYJhUu+dWL - o3LAVx19YoG9fdcls3ckjcilkw5GgZ+AUTFZD+sYxBuFCUHLif5zehpHl0VAK+YVDjsQBFkpE0iR - 7UGLDoA96R+YkjT2ALr68bBCxZaQ2FgDpeZOT3F4lNbmqpRGOv/YNICSjyUZO0FQHtJCTuSjyp8n - fhBszmv1uJn2xz6d7SpD3dAY4wjNb7dxzOuLlBdOOPCCJcMpsSUyH9UFPaoMMvsB2JT9KBGen3oC - Z0qKATL3Rpjd+Ym8TQYRKJizOBqTo9gf+wkRyFsmWPK7I/M3+QuhsuoYpQHMnTBBjcmsNjL/CkiE - 9vADtBHx3tCHB8znX3jH6VWQYZGVEzDgNMLG00my0+sFUEpkn8TUG4zoC/rPD753KU5Gk78nu3g5 - ial1ZtbW5lbWs5lq7/XIG2A+L4SBgYIkjpug0EkwxBldkZM4wvtKWfA06YMF7RIgo7OAPg+AilKR - fEzwzlNQUql/5kPpS481ALLVP4udsUemcUDAwoBhPswtFKAfuljK+n/8aAzLNLBlHnXJY+8J2d0j - v2WWlCci+p6wFw9g1mGlmNDLUsGAYh9F9vy0UALEXv7Vd4tf+Hjyr+yZlcD+eNvffINtwL/ILGew - enSpIcdKZ9DwJgcj4IrwJR3uLnGjAeUL8d9TL746pnItih//d4aPf1FioBjb7Xz1m+/+0fn5vzPz - jfVfaCzv5kHhpZggUYn5MPiPv5FHk0+PeDt/PMz/xX/+6JIzJ0iolVgw7iqs8ip67oTnoM8+0Its - r5FlhqWe2CqeU0sSD2bMARToTgdg6w6jPqsQYwVxEI17k2m/p3ia5Hl9TVBMTxdk2XMF29E0QXJs - 2e6r5sA11b/HHgq0XfkbZzx5yvAuK6reIblNib75DqH9M7trtyMV4rulr6Es4ny3MwaZS9/vqJI0 - +fR07Hziz7qt82c2HzuaiS847/Pppv0CL7Kxbs/ODxq9+kvc+tzpEkdn0HBJSbMjDRVQCrce05pd - IImBSGB5AyxLrzxGeeECoodhF/5eeEE0QRbogqIKp2dgjUxjr0scEAyJAz1GZ+RsGoC9S5kj9gfk - whv5gwAawzIoEoZXZIh/2XKN1gQuwruWkysQGuNEJCfQKZACaKYrgqKKkm06Av02HFGIzmB+o0tc - ZCfekK6adsg+WJHjiG6wYKOHrKtX5a6OWVesi0IN3gyMeBBMXdrbKuPGMbPRU0cEDr9m4AkIT7A2 - 4C+WT7CUM+saLLtp4ABcV2QQe66fcgQsHMBigEswooUCdgRtohZi+IzfHACAz082IRmbslmCpWSc - l2BTVZ75uhIIIOqQC68yr5eAjrPMYLki33lnZ7F3RZ6LMMTYmfa9oEtPmpEPHqCf4MmzLoopsMvJ - Yd+LR07ssu7h5YCcOPHEC9EthKrsuymQoNwliiSptIyPNwg77r+n0IAHWEbS3p8m0FiXnPwoEpEw - Pwjn2u0ZeFMORhR96yeUHACllCv/6z//R4ITPzgn9DZjxFdFhDP2LjF8of+CkQokQrUE0lP9azR8 - qPjI3KUT7soNnGk4GCHusN+j9wzRoUcUGzEtSzDBsePyKUB6fLd/fLD/fZeNAoZzRlEvq6JKxvwq - dX69s5MSB8gNBwej/ko2Zzeqi+R1SmLHRyvFATsGiBnL/AKzR3DZTL5SFCNrT6wsfWtHCEsmwLsw - jmIY42P8TfD3E74uxjXZcuydnrI1PGApwxCdJ3rmkg8FaRy91ED/DgrFM+BCapElHl4vH5Arz4lh - 8GfUWwcIQywiaimrxLSBEFpF5ARRSoYRQy6gJETatrq01sCJx845qmQHRaw7jSmqQmgcIHbSEfE+ - ATp9D1iRsQwX7AGiYTpB4xAfcU7xK4XnEvgL+NtP6SjwFmzWNwwjCmmfrPMIDHQ/hLF8iMDOhIqM - zfvTBOg5SQjYByFObzoCHqSn9jjrx+TF4XvG4vRYKfaMZVLPGSNE5543YVCi2k2QpqDsEAZI1QAM - gap0LIrqnkTTlBHaI2jdj4E6+lM/SAU0gdDNhEvuGdhvwToKyDEMzHUAn/8EWQ1oQtN1Sqdq5ExA - njBBgbFdIl2pw0xTzMsqNAA8CgSPNnEISt8tNYss8gKEId/8/wmmgtIxDr2EjC6pJ500is6RY6DW - GbmKpqSPo+Yj5CWZW21GPdn8jWjYAUg2qPcIpCugEFrD6UM2C0EwSmD7gOU2TUD7gJYAaohc5yof - 4zEKbdpkMqV+qmEcXaYjjgs1R3MR4BEXWiMYYd+DOkglqA8/wSoU7L+LCFVQ4KdXItkP0hGdRBwJ - 3depcE34KKU0O0hxaRKjyebBpExQP+I7ZADQKDgDA3SGukja5DKaBqin+ByDFAC6cCYJEP2lD+BT - RmQumGmcqQrkxCvglcuETX8303qAKaC0kDlVEQgAwbnqEq5D0MbJ9iFiugnCWIv5uUC4XGAH6aXn - pSivZ3SeIXk/pSjiVEA/jfwxMBooUsdHcqKEkmvMWumCAvDCCaaIko9h4J+zaQORmma0BwszEJXx - FUcCUmYCxlM3w+Yc/vveCNRVxNaOYI0HYMZcEXpq3M1lDVf9eZOUNXOmH1yh5ZMJ/ktKEtgolEFJ - SyigDjkGekDn5A/AcqD7cZmcjXZGixQyByQMGBTI2Z+clK1X6VLWYRoD5gZMDBBx1NzCL31EAYzd - H6BthWVG0WUDsyGAICDRmQwggjSIUCEjtD7FIZPEwAxnZ/7AR3JwvTEOHzHExpibfYAgFJsg8zgp - sImMsJlLUGLE9V0k78TzxswVGNJpHnfxCbWaf3ZFRzCOQqp1QLbiFDNvP9tLpTIqQ2/fGwJTMxpE - mGcESs1NNAd9FNvQ/BANTmz7n9AAGlpInTluuS0OaPE5d8GYo7EH0gpJGapHqHzKklBn8hTnX3Au - HR8nh0nBH8nxxx+YxHORdLiWQTtnOumCHB854YDa8E62PgD48AUbl5OZ0FhzmtA/SB1e6mUKrB9F - OGNgOEdJQo0BhnORPJ+mBYH8I3HiGMaOXAVj5noXBBLXh1iSiqnQjUAoYi+B9wlkVcZEMKtk7LGp - cnIpPPHY3HF9M3KAd6ECnhMBxLHZQLU0vop9UM1csFDTK0mmYBEfA3bZmMKE2/gcTj5AJMkMhhjk - E7N9ZgMDQwpsFO5cz/Alcvj+PfUH5zDxmMcBmgRFr5oqyn5kfYFqT6aIsT1et4s64iuqIdAiiyb5 - dB94ycRH1I9hHBdsrwrB5KMSSzSFsx5ECaUbxO0IrawusiDKWxHvgGziRSrEYakH2EOZj0xHjYpM - lCDzoHcGzV4YFRXfwyhy2RzNVj5xhkOUXD5d4QSoAlAd0ProoUqhhzFMBuV1GE9BjgynvusULKaM - Ixi7AS3SPTiP6XIAV6xVizO1R62zPjR9hpYgCtMuM6fQgKFamKFlCkoqYIA78APtC05TRXlKzTyq - 0qk0L/WZBIAbkbwECpyGQOPYH8wyW0IVNAqzFz95g2lKDcaBAxMMSKLGNozzLIq5FXDugc078JMx - IBCkapoTxbdsQkmSWQxhw7SiWYCOXT+cUlGA5iXFOBCNgFoZlN9wxKRAP47OoaGvVEqDdBGQkynj - NFqBTy8fDOCdmyCAxpSVo+Yr2j7QJVc5yC50wT/CsIXk31PP+9Uj//Wf/zfHLcLpBJfOFbdiKA9M - cV3LRoN+VKzKJEzRGgNyAjoDXCNxRTAFVE/hEIcgf5McZbN+EP0OHTfU4oRSVsVM2oy40QiDBqic - s7PAKxBkYa0wM8/R3I8o4STjCHQPb4/ZcZyiYT3hXGA9SoIOEGHgU2Ng4qSgP0KsXSBfCgoIjoHn - UmrJMOnGICRyq+bjpEDAWDjExRssHjy2mBhQ9gaT1ImHSAlFoYH4YEzlUrFZELVUi1AO59PdJWGE - y6KizcDGha2A/UPpNOFWpMM2zZw+NsuN2YyiCvimczRb+c/W8ts656u+VFx9wuox8E6Bxrkrkb7e - 7cBSM9tYmIZ+utvhK32Br/R7xyypz/dT9N1lOxBeH+MrpE/Szu+2gX+KpXqomDCg4BQo59MMrM3i - ut6CrGXG6IGD5P6COq3IBx8M2OMBiIyZL6LY/hirJQK6skBqvdqnzSQf6OPz6NPb1xTiAw+FXnE7 - s7icL7Vxeho4fQ/KYs9zW9rToKmSNwTZ2ak4SwK/oTg1zzt7b6LLZ73AX6sWqXspCLBU9KdjwCP9 - 21arKEU6e9/Cv+UWn/WmQf2+Y6UdFM4gpwqhEMurgGROSWGLv/i50sycSyoPSijVEsD+AW1W6aj5 - pZDV03KyLnu9518tIKgMcm0+QqJx+7bSRMFDVMHAUr5BJ6VDZEGWSEKf0U9cWv0x/zCs+1GR5/t2 - 6F+hJryfZMYhaL8x1igB0HcCtGdAfzGrH3XIZIprKtwkmonCzIRGNw4DBA0+H53MDkpNVN0C9yig - CeMJaE6jeZxMGdQMzlLnDGYnc9vPzClQPbigBJWASgcMMDAzmLCmqxGwG8codxA+PwbrMD4n0SUs - didMv7qIS6pZUmgFDdI0KnXsDOiGJhS8EtJIgD9ASAXzNTOdcRHNkB2ztUoZ9agamR5EZyA1MMU6 - 8mjtXEedzgDkefFtqoxmr/YKocCFKMl8N6omxpc7ycspr/AxK0X35QRLEsDIGbB4nqwIV9vtJbqq - T/PEgpApMQuXsTPp7L2/DHFUtZG7s3oYLcTjGYDyvr75wN060F+C7XUWRE5jDHz5PBWHHy8MM1sK - oK2DqrL1sxZssipqtXGuLcH2gxMOp7h+eYVExvdMHx9P+7hrEj9ZC1ZD1KRrBPVbjznPX/Cl3VvQ - FEOP7WQi2GvBqor6dU75MZjpXrYWeBHFk3Whk41rhC7D4AdYWjgxLDpo2EEBoQAyUME/oxjWN5l3 - OlmPGBTRus4h5HR7QjfRmGeV6QqA2PU+kZcgtNeEWLdvAmJdkjYFURbt60Tq88AZnH9ATGLgnJ9O - uafpJMYNSqSJd/trwmveDLyISbLvXvgJEOq6IOrXCOIrD0OX5kUWefPmxZpw6tcpXE9E8gEsMr4p - v58k0cDHUJVMIzApQCGfDWI9iSCL2nXy1xvnMqZbZ99FiTcZkcMgAGoI1wWxrZM0dSBSJA4i8v33 - 3zOeWhM46Top4Pjo4AM5pprgiMqow5OXG0ApiXYtP9Wey1l89jyLGYbaBXubcFMZg3poBAWsJXHZ - 6NKVzpj5eXFnWJK+hpXYwMHtFtz6IfxUE0EfLI81mm1O4FYKNR3FgpOsbqmwVcjbCkuLGXstWlmU - UvKuuMzQP6tlBqbGXWeV8V3U/zwWGR+i/lVI3orkwAuBpMZrsceLkePHYye8Rkb+wfH7I+eCnDih - 94uzJnS+d0b2mZsBfRvvcZeYrgY/x8URA/eQ7vqg+55Dy83ZAz+mQc3XCPw//HEf1fq6gN8AaFwL - vvL6fX89IrgJvHkXPoaQnDv+RqAJR7Pt58MQFIZHQxy65JU/dHDPNIqvrlOPxw7y/+EIDI5J7Pnr - WZxo+0+8EKOtb4JGv/XjaOwPRj556/86DaPPGdbvHNwW/DAFNQV25jR2o8Hoc4b3H046CnBr9Z9+ - ALamcDKKxpN1jc4bhvm/ObjXcUW+E8k//Ph8FMXrgfvWwchIdI2/iICJmaxlkvilH2IYDEjE69cb - PHz6B9+D4aw1gGMvxIiwXJbk4XcfPBZZnlwj2PuhG3uX5LkzBJtvPV7kcP9wJByh7zyNnRz9PL6+ - IAqvcQjvnNRJRmiojsbR1PWd9ejn4z/Itxire0Sj8chRBP9e8XE8z4LuDmbHFNY95M/hePDgz7FN - UdivL27d14xNluYqZWcY+TGq6lkpTBFAXkZRmh+TwlI0CAj3i6IQRRI/4EL353Zo7hdBUgTJoDWe - 0YgWV5gVz2AaJADNGWubvwsE/jz2+S+OGU/xBIw73u2UXqcY7CUgSgToxNntvCx+zbsU0qsJHnsq - H8rulZpiDzQjAz1e/SzxmPXAQavuGBfRm4Et5IXIOKAHvck4Zge+8+OjlWbpVq8vVE41z/Ud4GlZ - /BMMsTjdgKYOmECo38oubdqWKyCPTuZ3vrPj7NmpNY4tMQtz5Vijp9byE+/92AndhDaPcNFH+tSB - NbTvCDRoYbfzAm9awsX3hZ/4aZXKoGyQLi9USwjZZGaHlHFrOPUHiTAG8w/Puo3GvzcV4bF8edES - PcwXYxTG0bJkwz9wfr0SuDei+Bujm5BqwwR3XsPBVX4mb+K4eJgE/Ro7ZOAEg8fUPfFXoqqkh66K - J3NdLn585o+HJXD8MTJzDQB4VhHHvENL9JKL4d8+jYPu1+oL+EngZ5jsPkKyAKq4vLwUL1Uxioc9 - RZIkLPwIJsy7fB592n0kEYnIRH70tXoItfEj/MqOX2M3PRZh3QOoAIm9t697SCz4g5GoC9gVQu9S - hKqcLPIUCUuGG0bZidDqxHBcbABAr9rYs15TN3MFUb6W3zgLYjM4IzEWRVbizIpBI/PEVhUQvFrG - 4JiXhCaYAcE7xze8EBgX57VUXAMVDz16Sc8hkilG3u1Ug0by+k4ZKNoRgRWX18dDHjOwytIG6Sor - REXM2/g8P6HbIVyOZCXy/Bkv8xcLxA0FmMyKcnlPDRaEDoR+UZ3QgxHCXI9V8TMDBXMMAKGe9gMn - hOcY64VRNKFH0sOI9VSDao6w6xEb8Afkhi6L9rzgqKXYBS/vgih5/rr38bhH2aWXE9JMiqyDg4Wi - pICR9cCYkyW8pUaJsghCLl3mZAr9WMuB6aXPQ44YAxIgsCQlVT7kxSgLjoED/QoH8u85A55kz0v5 - Ly+5DvulpUoz7puB8VkynyWLSpPWXjCtfwrmyyjodnmvBMUNsd6cCcC1dMUMqOrNuQRBWbWHqy8I - Eg9WQEESrbYayEvzpQA9LU5TS7Lj+VVj69jzCJbfqV8xMGoqrA9qlweF32i0L5QYCxcGGy4INrL2 - /Qbj897a30BKvH53/Prg8AMyKTO6+UwWLO9okt6U6b8ONNe7DijzUc16eyk/9f2F7JQ7zRby1Xyp - egZrKLclp2XDvnFWU/6MrMZUIaPr568bGWxuKq+F05YDc4381axd5140uOhy714/SlPMrjDyxwT9 - doNocrVIdeYNreGN5OvymaKfXAmYW5CGXlSmp/HkCnMP1/JRgyU0y/fIQBaoeT8bXB+4rLM3W1lw - uGBdkUPop944W0306FF0zIn1mv3ASSlkf6z0Pju+RBPzuYRXF1hWxWZLMPDXg8n71HOjy9NfYB4A - tgOYw+/wJ4OuejRp254SumMxOdUlqbNXCJu6nt5CJ3Gdf5/K2Nk7+hvNiJX6KoRXATUKKHSIK4QR - RhxtCAYldsxvmAPzIntz4yCdpYnH8PLy5PjwdrDin597/qmCe0nv6G8Cv28cDNcBsXCw/yPRbh4F - IyccniYeHjj7Fn7CmgK3IG8YiPMomfidvX/gnxvvPJMHafJpxh+n9NtMPpwc/zhjFRYEfuOAOgkA - tH/84413PE0EF5PY4CKOYuXjMTmgL24DE0yBsXMDJTV2IwCcTfHUXYL6SuC/mcZ6yR5uEJRZkh8v - TmoVaHau4i0tshi08hnd2dta4+QGTRZgSViw+KmPI3sxe1jPdGnNWimA0xvCUAR6Zr+z9wp+sxj8 - tu2IYo+RH7AO/06N1csUpOZ7P7j+jhM/wOTwfLDH9GmNXtsh+CJAUGIyA+gFfbplgGh+6xwgmhT7 - NgECjqOH0nOawYN4LPHBeiTTPmhOMB374XScwbXPn28VqNABAe4EwtBJMrjesVfklZPcKmix5wSY - eIUvvD7wR5KJwysG3V2V8NMYPTNMwOe/b0u+5wD0vGksTBO8Z+jjh/fk4P2bN/sfWpexs+766YB1 - 99xPBxigeT1CvTC+dMTHd/Ituwfp5oh7BgVAIAzwfMqL/YPbhWL8KezsHR0ev789MIb9CZuSo/cf - 390yOvwwRkv/gLx+txrZtwrFFW6Z4Z1s6P3kUuEKBB5/dytoAU75NMAFs0doavm7KW5pfpVsBXVb - YpYBARZ0NBTob7Sgo2HgsQPhbUs93t3YQ7886+4t/L7Wzpzxr2HW2f7Y+RXMwdW7a4d0OSRpEuTD - 5llQbgcQXDRzOGCNKtw0FJPYY2tmkK/ZzxvsHtP7Zo4LvjB/B69akiJ1r8oX9fCdjDQaDovyoz62 - gNYvxhy6sXMJtTHRHkxjmF9C1hhw2N/D+IRKpiua8lMI6IbimKZp49nO+3vzcR0COfw0CfBc8Gqt - kAFI5WGEiTMrqJm/OqgaA1IfylGO1ihIw9L+D0Z64Bt2oVzTtlCBjmh1mhxyRiVzW201dEWr1IVo - 8s0qIM4BDRGDeR4nvc7eX//3/yTskklnLz88QPMCYGKrui7yU830sTYcFM8K/eqFYuilPczDCtOA - qeSK72l/r8b9b8nj11DCD9MnmHA7YNnCE5oaMb7wXLEy6A/eEPDBb5rA/4tpzl2aUMxP6M1MNLMl - TXGBqcAGA2+S0oRiUCaaxt+E/WTydDkq10Biw3Y7/SpEZwKOBFeLpZ3+E/yKMB3zr3vVNzWBSiUq - p6A6mCSQTlUtl204MJiQCwcsrAk9wdM0wGqp4vCO2Dd+BgjFafFZnNvrRf1JBdkq7LSEb9biGPqL - jeD/b+5aehqHgfC9vyLKefOEpC2iQQjEbVerAgeObuwuQTSFOu1u/v16ZpzWcRMUVqsKVUpaPyb2 - Jz/G6cx8g+HhhVRy1PnaMp+8NdK7ITNqZofSx5YlH4VbPE0fYeJIWVA0uXY3r80sB+MhGXRzR31u - S8p6an9FDOANC0RV8JRObeQFls2OLtWM9R4Y9sL0dMmsel8RAMaR9BriTlNVOOO0x8KhBPUDS/QM - hC5pWZeEk2Ah/kD4YG1mbaNR1tUKbXpWjFsr93eVArFFfjzd9PUUKoEeWNZ5v2WzIed/9pfUx3fw - O5TOoh6ABtiTe0PB6drj2/jc6aw+cMyq/eg4trYwHJ+9DyKCRKzrbkYb/3BgCQc0y82PbVk+t7Hq - 1YREWYsHJjqP+N9V831If49UgcP2iCzWOdtv0KRTdWniVtzdD2ys6Kc+QRjpl9r+CYRbDqPgnKo+ - eFG3EV5MBkigrqaXJ9V8rdQ14LDW3oMBhgUC204uiFYQCv8Wi59b+XwvqgoZTWZEUKi0XgFxZh/W - b0V+4bhwjvKAXNpTFdxvUKSQ6lxbzbflBVERQtoI2QkPhIQNxTeTdZnPXLxpY8Dghtg+g3ssIgMl - GJ8QFGVR+aui9F/k1W4GqnQYh5MoOYsRby36n/xvW61xHbI+A3/r4IXtGGU37ZO6XVsMg66WUD9a - 8EmeAglhnHA+WYTxeagaqWfl65pxDxVp8atWUxOPGm9sAzt0LyID27ADfoqN9MdiMk2WUbpMw3Qc - RelZLsRpWgCu0X7CeDSNprFI0njJcxaJcPzZx4/U+EZ39MvguVq9Zn8BRxRaeu9OBAA= + H4sIAAAAAAAAA8y9a0PbSLIw/HnnVwideRlpkYUNCQE5CoeAkzDDbYBkLowPkeU2KNiSR5K5BPz8 + 9req+qKWLAOZnd1zMrtYUt+rq6urqquqXy/sHG6f/nbUMS7z0fDNa/xrDIP4wjdZbMI7C/pvvnu9 + 0GgYJ2PG+uEkvWaOsffxVwO+5EGaG40GZMjCNBrnRj/Ig8YwCfqNLE+DnF3c+WY/GaVQy5355jso + 5g8mcZhHSWzZ99dBajDfnMR9Nohi1jcX/PxuzJIB1r+4eJ1EfaO54Pvw5l4E+0F6lW1qz95Z14mf + X5wF2SRlqgbxipXkvgkfzzMcjumk/k0U95Mbd8zSQZKOgjhkTjC/Hawv5tXGmbcDo3bj5GZTPli2 + txSzGwPf23LwxgSGHw2slP/FfHbK8kkaGykv1KbvN6x3FeUHc1JH2dyU5Ots0pS/Wo920W4EU9XL + xIrte1GJmjfmwEcAjBX5sTtk8UV+2Wi1ozd+sx01GnxWcz8+i7rYF+b7fu7GwYjJ3uSiH1MrdjRk + 0KBxwfJOnKcRy97enQKki3FUUyxzBJhglqD1fm7p+nRZh+iWwaaWrV7SxcXUzaNRFF8UTzCc6+gi + wI7TElhctAJ/XqLt3GP9nhpqXBorphU9xDfIoA9nv5Sh+AbZpswdT7JL6x7B68UOg2HRkDw+JIcw + +jSCRMA3pz9JqVtec2pPnRFfAJ42r5ETUK/4yoGJixYXEyu3YXyRDwNRfeZFZa+izWBTfRTVeKUP + pdfy6MpVVT6LyqaIUgO/6WT+hGN4xNFs5CfwiB9G9sAfuWq8bTbMGHZ2waqdQUBOW7TZHvja18a8 + aZxOobaAN3sDzQbU7I2d+Tff0GxQNJv52tdHmo31OWalOeZA0qd5UExy1hjgNEtayRyN5sVTwHGH + Uyz/UXJAmYLQByqJT+GoXxBwZt/zVN5DBq3hexRHuU7lRTnrzMQUsyuyZSzu12fDFJUt6PehH4Ff + Jj9FZpFuOvAdyrS/w03GHQY9NoStB1LGwQUz28bysnGZjBi+OUaWJ+kd/AyjPssukxvHuIanxDEu + Jz2eg+Wh+x38M+Cf1g3rhyzK2QFMxQ+O8QNCluWZsRtnUDz9wW5/93qZ74Rvvnvenmhkaeibl3k+ + zrzl5bAfu5naZt0wGS1/yZaHk1v3S7YZ9f3Vdfjv5coL0wiyuxgKmgbsSSyFhzdFy3N2607cp736 + uZs10fHkXZRm+TH7c8Ky3PCNH3bjIeyBxglVgUOPcskJ/NAWZbb6e1EvDdK7mgJbfUMm7kHTrK+V + /k5tPCF0ImdE5pCSIcBt4x5mAxvAfZmQGqqf3arbIhdArUgHCLRxMqOBYRXFFxeLutxwyIK05jsR + aGrbqM1ddLBdyTMqdR9TpwYSCOoFdg/awbnVW5DvlXLfTb/7ToNJeVogSylNgf+v4CNyNr6Zs9t8 + +UtwHfAygA2GsaBmx4LeEowZQLjmq2Mgqpjn53k4CMbRXhIGsOBMxwjgM1ASI1Yz0zZg3gyrbcRt + LA60Df4ifGJ3kMLws7O8iwnYUtw2etDLKwDj1IAqw0vDQrSANyphwI4lJzxPxjbP3abWgIoGKdBO + nALj4cGwitEw1fGAMvaTcDKCrI6RwvvCgtaTNjW0kNr0G7i9pH8nC0eQOXD5RHSGDGuwzIiKAn9i + RLBL3A1hTWfZKQAX4dOPsvEwuPPiJAYiDjmQxEMCtMyrdoPxGGjh9mU07MM+J9EnY0Tsk0kOxNB4 + CZWL3XMhNZCuwxgk6Gfmh6CtTVKQXtBgM8HJ8blB6r+VpsEdbP4cDE1oxHht5PCztGTHZ2lXLwyv + AjSSI7RlnwKeAEQ9f98fp1vj8RA4L5PmKj5rwuTKIsYbYxWXxAqlwXRlbBdgGJ+1AGNaTRvTzF6S + wMqLsbwhmPD4bLWLaRbhCLw4hikHXc63Qvnwl/oDMF9o2gA+tSjNMWzBet+Kqb03Loruewi+cDTm + FMwzFlr0CnQsn0CimeWTnmlM28/oSDGtAd9GY/iAUwg7TucagLsXZTmLWQpsKssy3MwcwyomlclO + EkrBihYjkI0xF9c7X3r30CO+wvAtNzaNH08OD1wCtcUz2oYnisysMWwkJiSX2LUdDIe4vGAsYtnJ + FFi+sHkBPe7jUK5ZmkFnS/2GLqVl+Mqyx4Q4HnzhKPQpGE6YZ0CBbBKGAAPPSAHc0PYugB4aoick + CjA6QgSsj0bGwREN7gDMgMjMzZJJGgLXnGTAYhI0rQjQ5Z8mAJ0WDsACFmHOUv5hoWXbNI+F8Gcs + KNiOkv5kyACM/MFlt+MkzXFbYghGWHCwHHUa/Myd11DgzMbnCBszTOJBdGF692YQhskkznf7ptdq + NdcdsxdkDLb2cRLFuemVWIlxGl0H4V1jdDHKcT5MxxynCWxQ+d2HlA1ML54Mh44Juy/IVQCoIxx8 + hq3gAKGy9ztHx+Z0Ov3mMRT8yQx/U+3U8k2KZC7l8wG9+CXKL4G27cAsEK4A76NzN9999w9icPYZ + 4OhpcJERU/OP717nUT5kb05P9raAoUjCK+PBOGA3GfycdoqPR2kUMuM06Qd3kLIVB8M7YGwOYeFD + U5i5wtS9Xub1fvd6hA2Gl7hYct+c5IPGuik/I+X2zeuI3SAGmAbMFyxayHYT9fNLv8+uodUGvQCh + B54pCoaNDDCX+a1KJfkl7B2NMBnCjlnU818D+lfNi13Tcv3dg1fNSbTxzeTC+19oNb+JcliU/4mm + //EPHcIgHxDeEQkvNesZ71luwGwZQ8B+qPWUZcMARQtod0ztAvkD/jwPoiGwuVFMHCpRvygOh5M+ + 4LpBA4DtNgPZI0KxJAKkICyDTmF5WE9DEA0ZrzJzv9O6WJqV/9M9FegqZvH/ZFe/qwNqNMINt+gj + EjKgYyOONW5vksGekMEfwh4iZ7tYJFt+F4QMmJWr3RAp2PiisnKPJr1hlF0yfY3PWwa8xHYyvkuj + i0udusCOFMRfWezGALX3o96HSpnjpJfkmVZg92Cn86vz7nBv7/AXlRcH1QBRIrr2zV8bH7ca28lo + DDDtlZbabsdnfQDGG7kBzJEUxNZ1cNz5uP/wYNGvfz+12/Tk4ozRltZjAdQNm0wvGLlx2sD9BMcB + mxRL0yR9Oz99CJMWZ+wndgfJ/dW1FdbvrbEmpATIn4WED7s7kNh62Xq5vvFifWUDEmGbirOA9hSU + rCD54NNv7w+Oj3/eud3a+qV5HPy6dRP+dBoOP3zZ3dvqjD/9/GHr4NfjrdPtT82Dt5f7v7JPrV/f + NjMfKgPJa8KQDTe9Zqld/u1VCz5eMNqSsV85TAM8TdU+9gQUrXlgtLl+5V5so959CEgGLOk5iwOY + sL6XpxM2dYIvwa1332fx3TngWe6dzYCxO3VAAAEGqTfJWf8cgBPC2vHuS/VM2490BDd/lp5z3sS/ + p+Hu7iDQ19fXVjfW1l6ajmRX4PPLlY3mxqrpQMVZDnNXfLkdR33P/Pjzz4cfP238/P5m992nT8fB + zsXHo0/vmgDsYr7L012a7dJkT9tty7L9N/yIwcmd1L/faL5qeRa9YIo5QVEKxh/mZjt1+1bu3O96 + WCgGZCVm1286kS8UcknqQoF0CwcJEjowxtYyCOFskNye/bH8R9a1/ugv/eHCH3vZbkeLi1bsL0Ug + u9hTZ22j2ZzX8hBWbsw1otSFD9SFCJjP1FpZffUC5ntowyTHQAyZL2XTqpzZj65ByGSAGyApfDjd + 3/PNP25XQ+CTzkCuGebM2O0Ya903ryEj8FL0d+EMZMto0G00ICubzf7q27Kvf1v2jceym07sM1L2 + 8wGipj64wEUrhipERlIMR+3IfwEiW7y55q3S7ytvhX7XvRb9bnhNmISXaytPTP/ROQF/7Ozn9HDt + bL+kh76zs0cPd87hET3sOcN39PDB+W1CDx1n54IeLpztX+lh4Lzv0MONk32kh32FWSCKpW4Kwp6D + bccOXz2U6dAh9KLnA0fITvS2JYpHfmqtvdp4ZTsJPMEasp0AHtbXQEpuh8Mgy4yMo0w6CQFzcdT2 + PUh+pJpmDw9m0vsCzHVxkCUV8FbTCdzfbcsEXgr1DCgVgLC4TUt8kiJlgM0TtoqU4U5q8HqA24fd + dzzJ+UHMQl7TQl5t4eSp2lGqYkMjT7AjRpRnknGWWzTqEez2ITXkwqCji9jKYcOHwTriK+MHPSDC + usAodAJYtRaD2YdVdwaI0PVZmy+t2IeOJe6ftpXbbamRWFxMFxflSHw5EvgWl2tj0DP8bMnBfTa/ + v2dT04hwGNBblGSAW4FdgJNbYkJCgF+c5EaPIWsSXxA7YwxgXU9SZnx/n09dw9jNQRYcDo3L4JpB + boMNBjiuz7YDsAEJyUjPGJAYkG5x5Gd510/hZUqSOyroFbi3YoO2VSMJQ6i/b9xcAgc1Z45Nh9kg + 9nHQhP4936u9yAXZmT872h7Nv2sfdILNz5IqpFp8zAL5pHbTmvz65zx/P4G9QrwhQVYF+EYjXwHo + fcB9+Qrba6oa+5JtyYnIvPspVaN9kRXClq2aLXMO6jO+HA2DKBZfoC5c2UrL0ccpoDVn55dpckOK + tQ4CCiYEZpWYIY5fmcR8XFZEDAxgAuM8GkQsXRALa4KTXa3p8y7Wgoo9RDrjBhZjzIBs4Nx+tttC + D4dFi4PdAScJf1PXsG4fy2dARUPbAaSL3J/vbGgEkxwTKzNtgU9DYBWGw+TmvDcIYQkxb6HlzOVm + FpqzvIycU5XFAc7+GqQFxJQTBq30M6/VfIK/UZiBUgM7h+6nDFDuHC0eCrQKkzQ7BwR5Mh3rZ8Qs + 3+YiT4FKOFzoAwgiFyAqKGTJsqHMkvQGkwzwnRVISgsqKzo8f5zAxcM+9qyseLp1zjH5qeyrMjuq + Nc4Z6iL1InoiP7t8ToUZSktJzMH1nA5n4+AZ2abOZWnhjZ9YeKEkdtzM4fkr8LJ2BW6XqntqKV6W + luLFk0vxL3X2Ul+Tw9KavKQ1KVSKdtGT639PT5BjSf2xPPjntiOcC8rdDMg88K+uaQOr3WxHr3WT + kqUlG0pUWQkr9dMzNC8pztL5l6Jot6vsNwTRGQHRUaKIoupCEJHLVAgw4vWZmxgJL3JF90pYePcE + FnIBqsGh+i1Y2KvFQjqNSDkuPomEvRIS3jw59X+prz0dCUclJOwREpZESLVDdHzKOHpnW7abEAKd + IssLYpTt3MLTSvPlKvB4Pumvt+gH4VIjqnGDjfM6Kc458c+lJPeJc9l/LFt/nCzZf2RLJ8EgSCOQ + 5E4WFxsoRZyDbNVnt4eAj9sAHzzUm5MSTUaYZh36Jq/FhC6ekCSo4H1QwPtQIPFCi8ylFny//GVB + cs4LTXrdKlLlQkr9LX0hPbqu5KrCBQRtpWgJoyqUDZFctQOAfvlyZc12jnwhiwpmedc3D47fbp2f + dE5Odg8Pznd3zALpj7kt18IRCq4C2WiCUP5AaZe1ta8AP2ChhY5BbA4nME2ATSj/7eZsZO3aCE6G + SLHjXgH2tKBP9UUyWQQYWNthBR+sNTml0W37R+751qb76/7ehzwfi6N054u/vbi47SLPniC5ESO+ + Kq3rvSfWdQpkBrWL37Cir2pX9LGo6KnFfFVazPtPLuZv7CDhWPtKX8xAhJlzHwIBTUanBXcsiSJw + XmLHbjlCoiFWO8oSVOz23wbh1TC5wGROAsiiSRQeBbdv75ATXwPsOHU/bK6wVW+VvXCSwQCG7QEe + 3Lp3AyAOThITl6S4KGKwPNNc4pM7TDi1dsZ5ITUAG8eCjO32qUcCgXb7qCu4tlI8IiNe1K2woqa9 + CajNj8xuL9Nf8NQKEzxAmC/wv5kD28XFheXtNDo8WXZRlV1LnRz16MUoxGkE8ooIpJgpfZv+oOGz + bouF64tja+6j4CFX80LuFlsZdAreNVmNfyjtbNVVs9CaTtG6a+qsr6ysPaFD+VOp0KRgbbZWVlbd + o+PDHXPqbLx8+eqJGm6ohkSpSpRig+qLpKJRYSiqVYXWIylrPZTO4zF9R0zicQZ8JQjFgoF/QseB + orbLS2zzAkAenFlFRDmPE32LWkLkvGJ3mRWh0pfElIxbY85WTpK/XRX94+eI/id6PULynzovVlc2 + npioPaEbO6bfUJuw1VcvV3DjSa1XzZUVrqZ60XzxkuupVlZXXsrpzEqENeSUiyel/n2Ww1z3hYAI + izu/8wJ3DGN9eGhO2xMYpZPBMsGdAgDz8EAvGb5Ar4sVgxnvGcAN0x8e6Ico2X4wtrV8fdi6aIfy + q2Yxb5qaPXlh9tLsbuovqOrPZ8u2asu29LItKCvopIn7AQ0NNXUL2vjkbhqgjkr77nIw+cAgSMiR + 8Y47AG6EBmsXIMbCCsBsM3YZbbTQnofPMEGJ+6t7GcT9IfBEXDHd49R6cVHYv6LpqvgGc+GEfoC2 + UZA1LJj7CXAd2eLi5LUEQ3tpaWIPoDsTKGG3idLt2FboWJppZ05oSwm5loDTeZ8ChF3KlRIzZdP/ + hBoMe+DoveK8YeqyEfJB/TSI4oa5lDtnUHIKn2EPvZtZdMoKmUN0SowHvCVpblnUPf8Ng8ZdiY6N + XH+zUZs7u6CxZgQ7VGfPqGA4/4aVKpDkZ2m3Oni+shiAgKYEflGzzb/DOkMbqQSSV7swgexsBXIR + Tb+zErSfRkBNHVyVTyzpUCiuGaOHibamucIZ1/TqBjJgiVrduKZRuw6L0Tfj9L8FEeVnG6FPdGhw + BNs11TVpl9ZbCSByxyFI/Iq7YAaya5FlaBWcHCzffpFyCSnWRM4/jD16eChexdLC2Zy4QQ+mk5aL + o3LggdY0BPzfnPj441kTXzMQ1KYACH2Ifwb4Z+wD04x7zNhvIbOSKsUx6V0PYdAp0O4y01NQaGJE + oW3gm3pOlX2ATykbJdes/PXOQYz2Rg4sWq/jDMX3zLtxBNy9a6c3GQwg76lDY/UuHTFmpKRR9pZS + UWFz6/RZb3IBzE/qCEh44837qZfX6L1zCaxN9QQM1FQumou2rkhQ8wRkF9XjsKDiEGvpbzKPbZYn + 2IN5LaZyhCvciZ3ISTgT2wK6mQBIE5gyoIpqBh8eItjhFxchLefrnAraSi4KgKGLEStvkJz2fWXk + OADiNHjdbw9AFsrOBl2xVgKYZsLRoX9o2WchUM6uHN5wcXHIDfTOLhxsJ+gC5k+Lbvf4eo5xb8H2 + gH+MYaZx3esKgWLNY06uEZH95ZJaqktqKJ+hBw36EKBMF6LBWssuSe7FmqDt7UwTBTpI7SWviJRo + QJsnsy4cvV+ner9g6O2LAsZ0LrKDU6UTaiA9uY/nPHLXcsKzuOvnTk5nIVAsxebOOJ1WDd1qXOWC + gnGRflgs8AuJZMB8Cs7dvBgmvWDY6YBUi6vUn9jAUa48SdX4KRxIAfJstcyoyC3Q5Lueqe2WEa+Y + 42KyaSUuX1oWMufAzyUS77B127PiSnpcSke2CrigJ3r76wz/i0O02wlsfn4gDgATtUWXGKhE9Bd6 + G1jJw0PsdqDvvLmpxgPoo4K5Snw1jyA1wwfmQ0negcDPz5Iu/Xl4gLasALYnH/8gqtliTTAn6pKE + 8PLFiyfG1+MnqJkTfeWMozP6nh4CbcikboDdRtBX/Jz4gifmlphHwk7HAjpsjpHrvgYsvEeyqHuV + Ac2A7a8dkzw4a1OLktmRLEwsQ4KzhplrqG99fo2g6yBWmBxt3ov+oQI7DMYIaG9hgU3pjyqT8VWI + u2Y6y0Wu1HKRK13gyrS3ttCLzIwUpySwUn0xhn9He8oi4ZEWpw7qkZ5Aiw/89Dtw9jn+O8dDvlyd + qzuOMFX00JepdCalKcl9kPJ9PothejfOaSXg2yjbpvdyUblVbbZeLrKzfGmp67XW/rkf5JduCqsM + mGgb5A3y6AL0OqZPZJWckV5q5quFzMnHKM7XuRH9ags4P7UrJehMeiv+NejPC/xzJ1/lPxP5KSCs + sCuErwO5K4S4Zy1BDSbqzhJYiWHX3oRhuHlyQvbOqB/zzDtMR5Llry5C6sN6JUMid7ZM9/dUwMgw + k47PWsrqil3CWuXzSSw3AD/2MzYcKNjTiwJ9jOfnM3AswIXyrT+T43GYRuiglgCgktesnaCGk9Ol + oAIWpTGL3C9JFFumWRKN1BhzRAXgw+egAmA0qqCfwOi4J+hbknFu2rkbCNL39XCG4r1qoXwc+Zby + xEUQ4DkX6rn8qLafsft+cVFzfiI/Pt7bZBL3rUqShXtU5FOGUXBr1bYV2XYj0ee3IGQl2gFbh890 + IVvlSwA+OJwn4PN+RlWkCI60fcI1q41hU3hJbs64STpraytPtZeJ6biekW3WV9ZXuWyDRmJctuFn + DIGamUyRHS7WgLRdVjDwNVD2aI7c3cVF+PN6Qx5VoRep+16K8lBNy8meP0xgzXJuZxK7b5EnM8n3 + k1SEaPZCzFrcQysWrq9bfwpJ3wpquze7BRMAojr+AgYrXYTR7CXlMt7XQ5DxlugxyeARCiMzHBFX + ispfeOAWKjOcSCx1EoBSZcWWW9H2CZ4kAnZk00VBOZPPcbddbFja5gXEBerMkxQ9vYUHLADLgQlD + x5KsAYMGWK2trq0/ASs2w5lJNqUCI5p7sgqKlJO04PabdOIBjLNpF+dGaJTpmVLBc0+nD2Ey9CjB + nBLG5eIgNS5rthNsG89z8Lgln2sgGABjl7uXKRv43EsZHYGAhSeqeryHkhOl2jNHJsk0cdGpwc/p + R/KElFseM3nLy0BIF3hGYBDOWl2U13g5fJMZIRtqccTbfwMMxskYaLSsB5JbXWSreUVm00TZj96Q + TeUVcncS3N4C1MCZL16sQsn1pokFL5MsJ+PnXD0+PKTqGasG6idyyEf8KmBOdTrQUd6wyOCi8fhW + bjVtPi5ZBeRbKl45XmT+Qq6qg4n2TAqBUHwpnkllIF9goy/6TCnyBeVbGjllx0mQc+NmkHxIpx6I + 59aCPurQxnH4+jjQkx/XJDC5SKQ3Xr54CudPZ2Qm/SQQMD/K3kVD5MY5ymr0j6tS33KPPcscQDaC + BVGI4S9AIQo83lRgsNvUSzebjHGoQS8C1LjbJxOXE7REX2iqXZxNnXnZPDy1cPDkYN4A1YKNC60z + dHVxUTy4N0EKvErhwqc0IeUMlv5qfUbLyWO0FPLoyO4zrLtylUSbBXR/l1vg1EGt2rMkw2IuhDyk + DitBJswOb2IpGc2IsUSWuMMcDVnSJobqW2lYSueRtZLW4qJ2KGEj/RDl6+UyVtDYyLlJo5xOyxbQ + cAtIVMrfWlMgn5oApfXIjyAFGC1Atyfgcr4lNrFoKCT94S9CmLheUzCTTIZCyJqALlx6WlwUUpSk + pkjiHyv2S5JesfQ9aSdOQhj/4iLxvOrIT9eA8cwHMonzE4nP7dVxguS0CA9pHu4CT7FqWuYakdPL + CFZ/8TzbnNY3WX2RHZvA/j6rGGaczpwqK6w3epCSsVQcJGeXE37KBCOJjf6EoT0vP7M1PtLBKn4g + KsBEj4wMm3MNPpDMGCcgO/eGd7J0mPTRTJhAgTXLBkVpwOMI682MYXTFjM/Y389kcPuZA/Sza9pT + 1EbXMdTEujZXntIo7fxLK1Fq2kBmiVDCQ8GFAuogx8BstUIjOn4g7YsVOQxNNZxkyW8p+pciDX/6 + TPb7UMgfBxfyyG72zI5r8IkHfvlSmpbD/qDZlgOvBXC6YLfe8v8QNf9j+Y9ly/2nveykbDwMQmI2 + PE7pl5cP3777eLK9ddrZUQe0oQEsHYv7MLHuTfWwFkg5qg3saXaZTIb9Q2kSWczOpObks8og2urI + bqqMKoUAKK02YFMWjtIzB8IGUzJl7j+rOQctIWKYwfh1LmX0GCRQcTSTA1/qEtAAouJFgQo68k8T + rdfkRwtVcoXt2rROwgCsEQz3fk4igBqmaaNKTiIHcmxCB50iZ3Tqlrdqyya+AsXkDDYp/RiUFed4 + OUooKf7hY2SlMZo0MhPRFoa2OQtWNXpgwtWzq008sFnH7KJzO9bM53/YwlN3PijskAYxI0WPazzp + CIBYGYEhemDQRgMkM7/EM/sohnfg/7EbhjWaZGRmHxi8fwYQZN6o/QPOH3AU3vMbRxucPhMm/aMI + vf8uqh1xZb1iW2WViUcHoRlgRX8dBCrx2wCBwyeGSqDcArAqC0hUVtZezuWbBFH5SeywvVlzjZcv + qso5Xr22gSleSm6xsBTCZDTGw1zkEZUcQ/7kGFiBTctSKJevrYKJsehQOXZ7R7ZFhoUYEyi3KzrZ + OYWir1Bo53B/m7s78sAO3BjCwZPPp04YfhcU9uc7oWQIhbDoDI4EoC7eC+I7ejerzxSydpm7TpTn + xawTpO5/MZM6rd3bIpAclR9f8YJnn04R8gu/S2N3YsS1d78oBb1Vz2VlkbCgsAou3cUzBvjr35+c + elSsCGHinOyqT7ujEetHGHVqW+SjMDsy568fjvnXis3ecedn/l1+6Hzi76SPdo5EqSO0zcyYs3/I + 3/cnOa2twx5wD9csdd51Trc/8LQBA4Z0ihaEZW058tA4vojGJ2cpcoVnUvSV9cmaKyOFR+YnQvfR + LnPKZNWTyZ0KLVJKNeT+/SjzCp2K00fHAKmrQ455tkH/3nXdxIH2PHzKnLO06+WAvVFpE8F1A11H + GbBE8WctyvSJlGZljNx3aUD8USFoUucXlJScgqBXOcUde7KJKFdNoKurKpg9XbhkUCxqqfipquqg + JK7ujY2nZOADcRAxq/+ZIXWMd+sbrID085SW2rwxa0GYriPggEnA5Z5iJh5Uq1ZTrmvkFNG8jPrA + nMCuokhoUZroqNREAu2bMmturmndsYCsXafQlYKbjZbXWkaXjLUXzacg+8sc7ULd5jErh3NV2FGh + OJVLmST/V0/pg/MZ/bOwTXN3XPQ5gkpWn5QEtgRuPFaZ9OVBbj9O/ztjLCaxyQQYNVefkm4fq1l4 + /iCs15+SAh6rB115tgi6sPe/WP9XuoRVfYrYDRFfVKuuPwXC59R2SgpxZExWHsGpFONVyYnZ2Tnv + fOocnJ7v7Z6cdg46x2Iffntyen7cOTn8eLzdEbs2fjrd3RdZLpzOARfwhrAnbJ1+PO6cH2ztd8RM + vzs4l+ljfDk53TrmWrJL5/B49z1vlT6MnKOPJx8wx2lHeBQfd/YPP3XqepY5pV711St2bffg/fnb + j+/edY7P333c2xNMRtH0wMGd8pjxAEccWtkMuSIZT6nuCigLI+hTdMoCMcWsqcvEU8vqGS+dUNac + k+OhJXznFfCzDG4XMZgMh6Yz8c0eRq/j6abTLzKbzsA3GyIS7xAeMRakc+mbg9hcGjhj/jB0LqgK + 5AygsWvfRDGGyI/pjPzY/W3idj6BJP/yERrwDGQ5cd4e7vxGj7fO9ls174f4UgC/rxDmdBZhEsFX + 4PM5fz5XtfbEh53Dg47AGP6hqPxaR7hJGeFCZ/fgtHO8tX26e3ggUFD7cr51tCuwQ/9KAz0RqwHj + ZB2p+u/E+8HhTkeshR9PaGlwB3Rn7xAEetnYjrO/9StfOICcO+87pwqR97d2DwBnxSo6Odoqqhxr + aNtxftk9EJ7u3Hg/GJ4o3pA7z9fqKMpssobJ4wAQ9cwMgVu9Mh0zm/RGEcYMAZZrDEiW8UfURvGn + yRh+xa6Kh0kbGxuIvINYIuGEXggNAU/DnvwOeBqMo0Z0GzcIU2EZBKSQInSNQIxIuU+EiVgL/TpI + +oCdgLhfsiQe87drNLwBVlNWOpLvfYoJ2JOvGA0QmrkThXl3bjiin5w6nWLNnMo1cwsPWMx0DrHb + 5tKps4XFKT6Jcy5qNp2TumV9QJy+s+MfKA391Gm9bD5lEv7LjGRTTBk3CueTtf5CmI2iUem8kAXI + bJMCpqJy8ZkjPl+AzI06VLQ7wy+MQb+lQbND/BPaYNZVMmOWyT3opakoenb7Kf/UAxBcocFoi7+H + lyy8Krl8AidV95FHAqbTjndzeiG5WORKSfa/eI9Ob8hbb7q6o/qU7KB4Rubw3LG7/XJOvZXCmoad + joi/1Bdzyt1wsBteTdfKLvRsOuU+AzivTyDIF05mSs4d4uzdxdDYjlrMJbNiYQMHK1MaHU8URvWV + /DxAN4PVlQ0b1iN8W3vZtGEtzvCUY+pibN/HZN04cI9JjQar2dTkC9SByRJoxXlfeNvNBM2M/SJi + ZvqmtZkCD9zEQbXQnpMMOeOzqNHqapx/1JU+HtfcwIXNCnUV9xAeYTBjfTzRhu6e5d3iyYJpisna + EiMo58RgfuA2gkDhUFeeXaJKA1f7aUJbvYme+Rid9wORSEg45r5R/OVIMYVISFm+PUkxhOoxUGZa + H/zrkeDQii/b5BWmEIRa0aihGSd5FDLOAXeLEaY4QrQt1EQ8CfNZj4wSzJn0cmQS1hVIS5WolToc + TAgndKOgeML+pPCCAAI95tag5KKeAkpcc0pP1FmR/AKdehZfVSFayf4NsO+WDG2RBiJQbqxrJ3UW + mgJPKeKHW5okylKdtwWy3tX4eyxWM5e8cFpMrSgI+5Yooc9zMUXyVFEqM/1qvBd+kp6XDtBzOjfn + 5ircSh7Npau+hL6VPjzIiGs81dUCcZj2Uu5Qr2eQUEEJ6LIcbgklK/3XQsAERFKV8xz/QobqSAbj + SuwQ/FQitA4sRA/ET1v1bGYxVDqnLQ0d8UXziOI9Ms6C3ISsd36vOGby77mZB2FUqofyV2dM91P0 + B8aoOCB8zorQeTHQzB3blkJwgf3OGaT0hRoKsLHrxBIvnLFdub/hgls7W9GmaXqwzBswRxonpVcV + O1EXQO9E+jlyLuzwqbtq/WomKfzYUTRDbFmKXVRZaX92ZnCRbRYnlcz2GLXMphg9bzi8u9frQ85J + 6yfmxK2tWO03hcXSrMasAsuUhUnab5RNFKB+E/hyXABL5jKe9AFl6DrSg7UQ6hG+siqGnktat+TB + TmTZVgEoEGc20QbTIzDEWEG6ySumT1PdMQB66zJrbWPFdvNLFqObVxT3gUC+xG/iIyxVcmK6B0xG + YSKd+oAxSCZp49wrNk4iqIIImhyfMc61AwVxUeKvWgROFlwzJ7qIkxTEzbgT99GbRbpJMv5qSqMg + B0hwxUfsDs0DbqwRzIQGN6RVnC5qO4xfLjmLG2TCq+NHAfXHJ1Br5LnTaHJ8JdZUrgTkKkvllLZo + DATi0r1e24TJQm3w0O3ZFpkJdNC4Do9tuAOR4qOJjl2j3yQqXytyPdTqlerHUDTlzYGirQhQajg/ + 0gmK2itzstSZcRNi/BQrnPUZET7pWiQwDhTirxvmEnPOFCCuz2iVorUjRbvglfL8yKLXZ6eoEnSe + ZRcYmqsdUKmlbBlpoiZexzeqb8tOnEBt60zpN3WTeemHHs9mXNUyrqqM98gXehHnxhOnpM72Mke4 + iHuhf6/50qdTRzCL3gR9IGDd8iH3cZu74Mw8mZ4M/OJ8Ag9qIr9PnCiw4n3O7WfwUGoVOO2+TbhJ + 5nD6DsjPIcqbYpRxaxKMGiQ2fb6nJuJt5wLfItTXiy/vO+Qcpn3JPuKX0HaQfR8A/XVoHNxZvi/Z + d+3bxDEFBEzeVxgbJXd+FyTqGsW/dyI8AUjbojil4wlVcsWAhUIaOAIsAsoAWDVAk4sn5eDfha7w + zxqLUXR+Lp8YZDeR2OBCYACNWOifPT4pZ7G2aLttlUVf3HpWLOrEVZTXCpZ1qnrRSok+GwSTYS5z + dEtHApqLH7qian3cPOt6Z2YwyROzS1Lhk5bZOzMOWpIluydAmPjXLGJimfLJVLGvTPFgOgU580wt + 3BVPUKMTaUVsqyJdwMWsxrcynRLIzVIsK5NoJ+p5zCkaBJ/FFWh6LUf7xtvoeiv4UXS9663im5pq + 7wW+4tC73kt81JvvemuyPj7YrveK8oyDrrdOByDrq14hNaZ5VtoKSbCAFZH7Te30l1tzs01p5QFS + bLsI19RooZzegGKwU3Efcyt63dxsepHdXlqKX0dtO0E3QHSiiZXUhTZVLzY29GMA4Utruss4vGWp + yWGmd7ax1nRevdpw4LfrQAYJjlKmtVZrhXLBL+USICxlegGIR5nglzJp2FCubb31ijLCb5GxQI1y + resrqzzzymolM0eTUu71V+u8D/BLuUtIU8raWm3yXsAvzzoOypBZfbXOM7xa704rNAR31tRNrBjp + lAC8OPF2Ubk+vGaWZOpUZNxcY3zM7SDGcJTAFfflzQY/wC67ZP5AIS+Q80aDoj7zzf3DnY97nfOD + w9Pzd4cfD3ZMJ4cdty1vfkPfRz9HX/VKR9CsDeQxcmNt2e4oGAPbyWy9Xyle1WBPIzoj90nZrp2Z + x3T23fcRnZwCsdFONC5t5ZHmlSQs79X2LRkQGI6ooM39C8l39158032bMcimlaC3J08Trp/8Bfo6 + 8lPoV+wzBVd0e3bPz1m2T5DcxIEwVxJUeitckYgGBh7AEOAIFcEWzWMNFDontAzM7QgmGFcQMEX4 + yOhxnvVt6tzrlrZN8hhHXQJaG0cuObBHLsNO6/NTCjziDmzAnv4E5suy+NrFj1AJ9RBtiDGiAs7K + BCsCbHHNpci9RJt+muamsw5CYINiwHwB6gxpNKvmSuvl2otXfdZqbrxqvXgRDDZerGNyIgf/hE1l + YcbsMBxKzoNSeVjH0LfQgxxjH3CbWtQr2fhH3rHCb2ojc30nLGFHYhchLNQZd30oYhGsG1WQzXb/ + 9USaxvWl+d/An5z1CfkGWIViiaBoGpo20NuHh2oK3atxw3rjILzCLPlSAhyCP2jTbUbTKfBEFnof + OVY219tDdsx2K/dVAAed87MVv7WCPvhxuLiYuVmpBzHIlsykRNupJpa652DnMEsa+ihSo6KK/HDI + h95CJSvAP3NFmCYfn5CR5J6CunGPdVmYyqUEMRHYg5GPj7i9CQ9NsL/Fm/DS5VckZYCH6G6lK00Z + TDbKhGrN472JzqWvXaI05JIv9Umy3DndsCdgBeOkK1K8DFZoa4W9sNvFmPTS6qvtqKFW0/Gj7QDY + 1eRhtNDSVU8Z2qVEbkr0pM7q++Ru1EuGi4v8V3k4AmLOJwezeaV1vslplDlV8YlmCpsFKTNlsYUm + JyTj4n6XL1mDThSUwRjdiAAsdBGL3b/fWN3wgCqsrXnNaRsmy/3iWyW2gJO23N5ELwAhBJHr1AKG + POEW8mIdgzilLeWEtjNBzIBgAVFGttInb4KzFF3U7bZWFNa6dCdzx0C1JnginBV7IvRuaAUOaabJ + mY/3C6N7QFcsXjHFpsMGeEdtwHlb9Aa5KW6JSJ47OHmbprAYNT3+gc456IZIQrHiCRcVoJm49Mk3 + 0SQRLU3Dy0l8ZZD2yBjQLRjuH7FlLiVLpgefgyXTxmUuLo3AzFiSa9tx+aPaEEQ6N+XGcn7g4A1b + hHM2noRCAQqUk8MHsaOXpgeo6lngANXswqaHUX7IozFLgCCipISQIbDYXHuPJqhGRjtXBqAhi8IR + 8ogZ/LF50KAQ9vwpZobho7Fye1K4O0+AlqKT86TrcPgnNpoSJ13+F5gMVEyijXxzWlzdKwgUDR93 + hXnfub20Ihi5tlabFM0HccXXP/NPQldmc0cCPtsYG8GakXBgGVsCzZgfiaOtHJ50W4zUrx4F7dgl + /6RCR13I6mn56CQuHZ3QwYEM37ZAPlc52Yunzmd0S3JFVLvP9hQ+MVKYxFOxGMQBLfBVPLh8iA/k + FDCBJzp/E2e2fWXPH8635y+f1jJ+nydwV9wnU6hTnUiuGsoNC+DtJLxiuQxVYRc+b4LXr2j2uDBz + z6OAA2lSZz4ufVryWyjdT2bVUnTCAozfAACRq2jqeDuxagq95YCRunhebzEVEbsofs8Bqz60A9mp + lD+orqU1GjOTsuAxCpNRiALOY6fE2hI3GG4GfBCRi9uux9/mKt8M1gbAEMiQJc9tDV4gxAgX8QhK + 4l+HPqJmdRTcFu7jDP/yj5gGw2HQOFCuJMNH+IGXEB9DBGjkBAReVBWTC2oxlSlHBu6+V2jYhQtF + DUJojvxZHqBsC/MlntGjrIo/sTIl0LGQh4OrT6A7a7QOzGRApznZCVKbzc3m39O1cpmXoj4LRBWi + gvxIC3Y00V8+SSoAQX1tQCDnteLVp0zz4IqVPTpIS2niJRsYD4ofnOpBh0BI8NHbqOtf1gIHeGkH + BYki2lVMoZkEy1ZfREV93cw9Hoa1GgqtFBaWbeon8aiRCJeWPDzx8lqKBDI8w/XyTauCyxztWgIn + FUb+kznsDaErHkISLgN1Yq8Ju/knQH0kWLZ3n6NdgxZ5TOm7MMWBjPg3uIW/ULmHdWPP9IhkqsRs + +ElUkRXUaKw5KYzLaCA0/JEIv3YNTzwm7wieeFxMzNFDCk0WEndIvska4gZzUGCDDpZfw43nFJ64 + hcQtPKFBD4ZRjoStxBaRfjKsIup+rqj7rftLnXUOXV7jL1iziuxabflq13540F/bYoug2oRxjXAC + 6Vd4OSEQUx6hz5B2QClD6Pq5vAUDBcWtwjZImu2gQtKPafE1nVNu9JHa02iEUnyR31JRQ2frenhY + UFVJM+q6FmUgRubTPYjEFajwrPcyJwMM84ObIMoNmJWNjRXb+uwuf39ftUOaFlqgz3YbwcLqrXcq + VlG2o0PLKocGHdE9Je/4NWp5YnAwGEVpg+MADz0806XP5BrT3sLjKcaPpw7V8RSTx1Pawa2aJ4ok + S2h94hw4O84RIt2LdYV0uwrpzmfvsOGximoQbl60ohLCregId+QS0m250ZAfLnXca1ucbN6I4BoD + vGW5d4fH9vD1jgcdphyH7k9isDQ0EVdiJxmVHIgsW+bu1eT+hXhSlRMhxmdsFiXtaW0ZoWyivtFp + Ylt2vhY/TC59othORUSUDnju0cGaVHOfCfep4lySzRwbSIM71bPZsd8/3pm+KiCgq+KHyJ5Npyf+ + rnOggpNx644dH6fuAOWKk816gRUxS0ipO05ZCRZq19zgu+aYPrW9k7ODrr9DJPUYiebGxrrtbCMh + RUcP5wt+W11X7O/VHFwt4kW2ubfQv4Cx6JtNSItszBeXwggLi44tuhtMUiqcnw/knoGKDUSKY5fh + CTNPnfWS4AnDcFzjQSGI8TCbmzaIjul0nBtGmjX50C1UOtTXmJ4IkaymoKAXNV3mm8Lsd8uascLA + y3who8rhuq5mUgL8LgVAmdOOm/AH6x5DNd/hiWrmnZnjAC/ZRQVqYU/5CBC/rbdQxzM6W9/SnP4O + UZOQ5Q1xieNgMmzMHUL9XH/bEKCOZwyhvqViCFz3NgzukkneyC6jARAI7j7AL57Ruz6dxcV2Yf1d + WHqPEhDKxTNdlcxS8ZYnk/CSWyh1y+aWsIK2yf9Klxf10fJIfruFBdfMoIUdwrF7IOj//ZylKqhf + QYv7SchzADmuKfMsysy3FBoGOcVivssI7c6pL6otWIlHKums6M1zGrHn7lnTaWn98RNH0qSL6Oea + dSuaGfHdViAohcxBHdZmze40GJsOCaGDYYLU1qXpQ7A8q8ueaGlmVahGiUWcaTX815pFdNDXuBZU + uAwWHs5JRjGQUnDpfpt2PdF/HWl9EvwpV1OANOcEQkLtYyyDmF+uXVhpWCjvBouLiYicp+HiEEee + PGeIU50C5I/Pdw5ccx/WLbIFGFifjLF5g1ALov1zoTpvRMURoLp6IVT5Hh6Kr6Pk63Ztwg3rXUV5 + kUZwL44PkVKR+teMWd6gS9O7fi70urnLL+uLrtlpKR9TGUsZsESa50U+eKFc8ItpSK+GUXxVZOjj + K2WRafzOkxmyxG10S1Focu4jtbi4UKGgEie1KIIpHRqd5MFoTMHOiEBz9fW0/ShOIa/yWqNvmxG0 + 1Nc+NGIvflOwfIuLkB2dnHm0wk0rbvhFqjNb2vZinXzO7AbNOu52EJncAPU5GD1dmDGboDAtc1hP + LepR+k2sJ+Oh8awrp8zuAsvHZfw9lN3pEoV93zxmMLWm88E3t+KLCezzpvNVPf94YjpvfRMdW3rk + R/TONzujHprzfu+bnybw5aNv7oOUjWcCn3zzdzbOE9N575s//jlh6Z1m3f6ncGPZKq7zOesKdTY8 + 4Kaub45CdBa8HfXy4UF/2zncL384Zv3JbemKI3UwRn05YUNGTLV5RmePKRZKkwSog6F9ifpdFUBv + odkuHAfqa9saDi0T3aOMNwbdKItBY9t5oQfL+TVJ+mk4HWsUQfvOqd1j6Mk2j8WBzgJv/GbRCXV9 + Ss2FKoTsjNPa/ZIhdWEQvbAgo/nyeQUKubAwDzpufNHAUwnEXuMMXoLxWIGoeIUn3PfSZDhkqZ5e + /gofUjYGPNSziC8YZmZuP37gZ89nWRr+0zdFx/HkHyqqTYLlRsk/2DOxB+nimQJMX+eCSeJb2ULA + MmG52yqYV3wxkw5UgpGVX1bOV3yfKXJw8TsuKfsRGBBuIQTF3b3d4r7ixwf4QV1rJZeuSnqrkmgh + q+/v1HdY1urr9+orX+Qq4aNKoCWvvn9S37/8jENRCe/tmuBspL35hQ6FMD7Ur6R8RKXk76RyhCeh + //pNRU3bIld9bgZqOz9WvvMraGTqz5VUtR1vushZRCETOUm4/slhQHMZj46JR2zoUi8F9Jg9pk3i + 91pxJeaz5fOyLS/a0PxNClDGZfuUy/a0AR0N0DoV/ZonWQf2Ej/m3/EAeci20eUwk7seEKPLE25c + phJg79vDaxbmGbLXiDUYigrznpSyVgWbGV9JtWHOtMmPOR5ti/vIfEMb8ySO2q7rAdfMbIRRge6R + yffY1FFHOIpL0M6hTDw3onjiczuSTksjKDUVPqspMg7dl7arz2iwPPdSs1oz7s/v0Zc3CuXlhsvf + 3//u/jld3hGxqz6LVapbjbMpPyme6zWLJv/PaHJPVbmMp81am1LhqqtQ7/+0Kp6O85sw36UAkBsg + AMtknKiqNqW8/UttpDPrkSplr/UyWr1U46PhSOUpPozqV/f7cB7sUiXUPQZE1R0VAE/vi17HArV3 + cIG3zHxjjbs8Lpgp50MUxlJHyfBuEA2H2QFjpMct+cQiL/gT/fxm3//kc0pMbCASa0Hh/dT6yTF3 + WB/vlKOpUZsImnCUUqCmH+17xnx9Eygq1L9CtUDrTf5pplL1mR8ux5b6gkzlz7BUmCYIlnYSDEuH + vu9au0/l9EGGzZnOkDziq5qWfVVFJD0KoZeiLUyxJRRW0xH0n7ds0oVXm0qs5E5yczqIR1ozkBH1 + 6KCRn7iJj2h0GtP9kPptg2n5bJafyd0iVS32U49O0tA0FJUaWl/5WRXQdbKUmWoWLVDlFrlkMOsz + 73iGJ1/T5Y8wriAaolz0WSsRcXUJhvDmVmhoUbVZLsrt0z575a/byAhEoV5ZIivTcqbT5ZN9aF2s + cyJV6pjs4NjglzRqMUuFmG1gUeOG1+N+JrOp+bSG2w1hMsYArl9uzyHpstCyOCCVp4ZVhgGvXdzf + PelQGOJvqJZQdVfcOTi38q3j463fzncPtvc+7nROvrUNLlpt0XWJc5s4fPtjZ/v0fOvkZPf9wV9r + Qaifnmqic3B6vEujmNawUvdq29J0qdllcgOMDXeyGOMuC5ShX6LEgjEw+e/y23fbeOX9Mqpbjxkx + AGLj+k+rGWJW0TMg/ykMXpCLFpe80SOFWQgYWRygPUGGYTZiK4AVHjIfyaUzYTJUien0WSVsyYAB + OxQgSvGrx0wHff568IPRTFAHTzdPmngkgNYiICsOmR8yFdDvUrxlYyA5zBlDA8os3HTQOlK/dk64 + QbKSQ8CIFVZ02i0NxZVz7OEhYchjce9/Hh3FnopryxcsjAx+idHBGY8OqLwscjIfB7i64jK1bpFS + IqOaSRMZOdeciUWk3CvHQKA9BbaQjJWcfXnkf3HxWbxk9hiAjylfa5DiMcj72QXaDNGP2yfdoi9f + CttXzes61ezAZM2qSod1nQAwQbhrzAJRK8Q9qYmb5aUwbmGpiHQpKZdQmXHDkMHwdOAKnR6Zzgxm + HExTmKezMVSSAxoCt2Rdaq/IHaZWSJhPsW8mtJ8msZhu7v5dsQ0UGnoRJjtVVy6S2TbSfEIXcZbQ + 4Bs+bKs4cpD9cEJjN709ib7CoiGlMPc116L8cCiha72wi6vNxCidTBPJ4ocjvQyaZDp37Kko1M4N + w+OxwgeclfZ2fu8XXwPAZkbxEQaVLTuNiVv5MHBK7JuCeyBrYIyT1DDJE1CFeiAtHL++KC9dX+Tn + Z2HXOWQWXlaZoVBs4S/wd4Ez2cyWYg8byqApe4pXag6DC78Hs6GvJ3GBGWCLCkPPgK3YBG6Erp/D + 7jkxv6eZpWc9jHjp3OJdjuqjw7QcusGt/CZWHw3P6RNzGHLxPJtZjpO68ArRZmRBadsj31bBlgHI + T5l1ljsmTCvWnXSdSRcNamIrLZabTIERtrUgCX1fcoChMxsbgWrgXuZYnvHyWrADnoGWGmbo8wzT + qc6clW5fvAG8yKUF6w1rIxI1eTAijqY8d15chKbGl3ItOwwMCgV6G6dMN9rlOMcPiAXq0wkt9LjB + uOE6Kx36FvcJMu1mg790aQHX7Grip8J3qLR+78WBVe+ZU/cWpF28U1xLy0uJGKkKwyXROlYjQnCR + A/tUOVvyUPB3TLo5kel8zp0r0XBeGQgWdycyTbFoVe/dfCey8YBBgD4gPjJcFRpfvKWgKa+bzbtt + YhjqIovkRZgUg68v5tB8pI4kImhErV88eM4el5PyspyUAulIQU5aWkptCoJTyEmFPWkuXPlPyvv9 + wTfu9zmFElT7/Un9fn5S2YnEnTwdpDwg1pmFV5GJ8VYwpjEi8jUS6MQ3dY8nCqgotuvCpF9QXdJ8 + wsZAIWRUWGTgi7QWGqZGpkWBiOeQjdZlSRzejVKtescaptgUo6WgvL0i/4JXEKMcTrfkDDD2He1e + YmvCYnFdMWH3ll8mfanEJIjviGBpfpQdBAcW3oNsbza9JXxwZtvigU3k8qHJ3SnP+9E3znsaDIpZ + 39Gn9+FBP/SCyd95ZPJnZ5Dr050zUzjabMXRiIZKCiwySfl6PCeJHzvPSx1l9SkAJBiNmj56EZtJ + ifGaAWsJnrtleB5/IzxhgkCquStguvsoTHcfX1DyXIdXqo5oxLt+j8BjaQVHBIuynE1ZpeOlhCqI + KL6IoPri3Yr8WF9LEUysFnPU1HPjRNAsCIhulyH65RshShEfC3huPwrP7cfhuXy2udi1Nj3cVPB6 + 3YewZ/vW2f8s/ld3yV4G6NB1Hn+48MmlL4m//D/WHzdLtvWH+/C9janfL1OETc1hkWKZZbA3viUZ + pAAHT+0WY8/0DQTdR3ADxBtX+RXm6NGyWmjANjOAM+55tod/hTgwG65SszO0uIsouYaqWa++686W + m8WcljM5AZmowkQCQ6nyfDjd3xNutnVZnXLODyAiPD/3W5CWH8kt6Su91q3rBf1l7kUnbgzDPKAL + wqSfMHoWqc9unuwlNyzdDpDD1a4tLNhbrbLqXJStjpC/Rz9GJ/QDMdmwisLNEGPl4L4hBQhywV5I + 9NKT0tIQXvWiEu5fhd6CUhvur8rL84ChA2xZ6TrcSdjLgGsC1OIIYkvXEcrGZB6eOJ2ip179SCfC + 5/hsghxjV+9ov7S6B6jAE0ysFqv1rOv0gQ2vv1SYW1sPuKXeCFbZQsuem1kwxMNybt1TZaYDvATv + wv9G3wo0l2B0BBwdDKhr8uZnL2ee1/ZszvqG1WCBq2xwMgpyNCIkjnhKO2Bp77sqU+o9jVKnz6DU + I3GjREGsr4Dnn0es03YpVRLrGIk1RsbzyURr/1CRQ6Bu8lS/cnVFTd/IC0sXACIKghdZMTk24K4P + UumcgHtOtQEt5GBUPCOHz6GzXwbch28E3JhrsQu47euQKQC2Xw+wBB0MIwG3hMPt6Fg5jC4u6qc0 + so+sICtkFoi6QwxM6OMVXSa7ZeEEGATkOTmHCwgF+x7CMMH4EYrfk0UDGst2fjsbxhFkFCeYbmmX + kICoVC9b6pkcMyaGT7rpi8/opw/bLwZSNynyULdGeFVjwxtI2knJGVRpIRdabalJi7WthCTAauyY + VARpySxq2kdnVCfj69tWN8ULfEo0ZCr8dmVVgV1iCGoDKYqFC4gxDij8jdCSLaAdK8bMw+mI/AjW + FMMAjRiHxBQNEP9BDnffChlWGOTOHwjDMATopz/bReYsNDFCpuhfTP1KNMaDJP55q5UDmKtMYQ0K + BAnogMTSanF0SxZPYAcGYVZ6UVEU8FFT/+HmniGzVZlpUuWgdJ2g8kLTskDft1ipYRN7WOE6ZieP + g5doSuKfs3oag2sJ5abAFYvfz50EBZPISkgwoY0hKNZeArs4JbZmE8W9q6WmEjVheCfSbS5XXubU + zVxOM5fJmcto9CmxW4oUzJWlIiFLCfxTcqrsOuNdJ5l2Xqaa9kiNV5ak8dQSozWkWn6AxYzemmTu + 2kVE4yRs04Aihl3UqRWpF+U5qUPrYelAPUv8xB3IagcSjdlVoluokYij4xVtygfL5rFLqXdMxS+Z + pbKmuZTQvT5yU/pa3pTeahT/6S2JAn9lxY70tV4h9HW+vEXOF7Ffuawqqgu3n9TfoqHf4lVy4GYU + FzOvGicCW2znfrH4j+TCPUSrdSwDzLDGKnLiq5gz5pxFqHEG4dUJtWxh+ZgA8E9KYuU2UA7jjW9a + /Lg0sIobVAPa2/A31jgI24vnnldYdP9lJspl5XJCFqrCsmaRSoET9Zncc0KczaAncN2tQiAYVYOa + ypguEZr4RAzt22IHz19u6KCgXzZvE4FueCVeeUXUyBYxjBtjhfPQpEKyUF5GWpq0/CixarI+L56e + yQqVCM02U8nucUrDXH43tBnESXyHXlEg38bknoWLWAzH5xRrqnR6deg5hx5CQb2qhwdCGI3Jflde + lt+rZZn6cg2q8I9Pr9PbS9LaprRI37GzeGaRxu3SZzw/fIv8jAju0GH82gHiGn/9cIxuMpzxdjLJ + StLV3HjVxi4e89CVhBhpgsnbuPr+mfKtNUUIKHoKegmCSCQKgIk3ItrwDEh9IW4mSWIZcarrDPCi + zyGdrlcuvHOHYgYy57IuvUZw5mwrq1wahjt/rIlICEtyu8lFcPmylDVxLiqC3YxDubkV89t9jSQM + JynrGzeX0RB3gZylIRvTVcAAZDzQaevNV897KFCIxotMdT5Jjd8fbp65rouHpx6eeJdTK3HASIEy + rVwDAWtS0T+QSmEGZmZX6p47Gkm8UKucG+viqWexmzHt1kq0BF7IJe/bpygT8gVRUQIAYN+Q30kk + Zza/50/S5n7RDYwnVyJvpTCBjJ+V0FEUMLCXGPy2EN90dq6oXsuBQBgzfuVD3CddJvYNG4bOIi3A + 7/StntZyuJYv0uDG5BjsCgBwvXnNWd0b2ws3Q/yxen6j59y56Gng9/jdDBSSiBrE/ugNjm089cUB + X+Ot91qIA1IjhQBxEXOn57ecu2qYPIxLjro+q8ejKQTQA+V9egfc9WWAjlMsRVsU7nDKz+U5byTD + xM/wgRiRqtnlEbZ8f/LwcGORbKSw5qZ0+tVss9cDeSzOlpbsMUasRsRvD4rIKkgGNMTrVMKDSBpX + 5PioHQNynqdI+6Snjcpp7/W043Lan3raUTntFz3toJz2q55GhJ5sF35nzm+0CSAF+J35eMXQ8ARY + 3uCCQovv5mxkmefncXqOR/+wRemx4HFvSNAap2bvFmnuMLkA2P3GXPEBV1oD42D9zgoHnj67RrcP + zAaPFMFmJg90QcsWpzs8o06b1Gh/pNHimIqGFxfpq07KZD2Li7SPEU5VyV8Jt7AG9OsMr2hN8O7q + peVFCDOyApRMa0pa8N1Em8b3nYNTY/fA2Ol86uwdHu3j6/7hTgegjFkI+BhUTsqVv83a20iXENt2 + vyQRdMYxUFVNm+vPZDK2WoQL+2meO0ZhGkXuGMm/HuIDbQ9+RpeKhFRpyqUiu4rGB9AJvyljxRB7 + RSGGVagYtKo5FPEduQ+K2NQFYxJII5AnjOWVEX+GN6EiEgbanNWRT5y1oNQpmK5A9Rovf8eZDJ6c + +molC+kZgGNLBNLnLGyKENqq8cxAmidMLcmsBMM7OTmaegiPyYBcv2d6ElcY4ZlOaFWCdKCGhcbs + 2igb1VHOLI+cu+dzP4qI27vkWhQRknI1705150EgfIbFRIaV6SVuSkb2nNXjwc4Slt1UMCghzy7u + +6kkL7SmyqZ8s0ZxPYk5hPpcY4XEjI9OKk+LAM2fv7+n67CzJJ5+pkgvPe1OibQ0+pZzr2oWYvex + bMBrTetho7tUa8EhKGwD2RbJrhSpJhEXuv8dZvAXEZcIbT/+1J5rT+wQIouLH7VsmQt7PFleobnq + 4uKvWlpQWahNezrPCagyH8IMCc2ZVAQKiWibpTdAOw+QPHl4ILv1nBdFXCpAoyycyPH1r2BgpSra + EoQ4PbufVamRsC3Wvugqr4pJmn6Fr4qbJy0xeFRRlAQ/xmEwubjMBeNON8fHiQFzFmHRYGjgJRAp + tzQwBRWlyzo/Hu/Jm/fwJis/LdrLJfSkuSS/mKYcqUjH37zij63gFdv/adPon6qm0T8r0+g09zUD + yjgvJCx5IiqdflmhRcCQ4zq3gu94SzJyiUQXmp4iyYy0ClG/3l8uX1rCxUkgzSk656uWxl8muTj5 + rbvlRno2qwCQKuIf5J+5xIVXUxN6eKsw465YmJVTXAybtVdpsfjWnlP922HSq9QrPrlZ9JUVVeEb + N8yuq+adsCSvGsGJz7ZuCIg3nrocYNEA7c7mOOuKqGUBBW9dXVtX91bm1cCndcyADFZ3gfRPXtl1 + FNxRjOi5KZq/iFh4l8lk2H+vZ/XnJWiFp3WVSzOBeeWt4iYBXLxCtUksz87ePJaH7iGYKYaXublB + SIFMd3dgaQBLLzW5FjBwpJuKKBNVOC9LglkA0ln+E7urz0M9iB8eFqKZXgTY+Wt3H8lzxp8/5PgS + ajEnnIl/n42DeLfvBQ7d0gBPQLYjoJkYAcMLpQ0BcuVAHw7JBPvhge9H2dZwmNywPv8KIBQke5Jx + oIpbrj5w43FAyG0gcpaNAsbEzbUcR3RQzvPVIEhNLitw0A65VAvpIJ6qRMsEdYRiu7Sd0gAl98Zu + ySvoQET4FsXQHb6c+0lwlGuoQCIuJdZ0vRiy1uHJ9EkYaVKL2Ww2KI52gzxJG82WWVteB48W/1ce + yy+Z/x2nfrPRgmpSqixWVcI/aGFa13NVkzS0rtNFc7axlyfB7FIM/Ptr76zptLpO37vP7zzzLXBp + GZ4nBKEXO8EY74zqe9BQ6sE2HeHFhNqheIyXHRDf3XfzKx/kJGzHqpBDaHhaTxtKR5dRtpN3eEhp + y547+dPZT0LzCxwrxbYGQKhQ1XPCBkaZCC/Tp0tUIh7kwYo5ZTo6n3ffb01BPFAqcNamyJ2k6aEI + TAHv6jn3schm9jp1UQQPoTtTQA+pK6JF401WOUblm8l8FvFo6My9TLKcorfD9KgXCsggXJMpQb5Q + ArDA/CM8wLzgMMRtEZJzmJYmSOgu/wqkJX+zAF2CPzKO+HQOWfg7W+ItnEvScM4dcKbzScnf1XiL + DORCqPMcGqvrwGOk/W8EgeoCkXihcRbdyDh7EubkpvfqJWdYJ7kvo3lKIylTHkoUZw393J9IUzpn + kPPDjuPOz84wFwJy+YRBcD4oM8xX5/zLkS8LRQ4w+RgbQ933/oTaJakIklZS6GEgxZcCXZ8fjGR5 + fT26gij1VSDxInCeJOHluBTi4OsdOm/5nzQZVpdnCy8TuQWUz3oLG4o2jD3Jg+F2L0OdlcuvmuSP + PQofiI8wCf4p/xU3leNI30+ifkaX47gwYTk5ofEAE3Sf3zZ3Yd7mNVIhHqjcF7Hl8BkdlzDGu4jU + Lg358YWS5hvulV1p6Aa9qV2x0Itydxetq+HnzeqLhwd8eN1qAirU1VsclZU9CYvRMX5RYb/aUOXQ + Wxuff8/dEjxU3k+dWy5KkyOCow/W1/2OQvLIKY6EOUcMqGfejkF4i8haj/PVGkuEN7hJq35BIM1f + G0C8jpGcNHZ38NIdrEDaUyW1QoGM/Jdmch+lnUMciaAWsx1UGCjyQpxtXGYCjqFSABlkVIgHc3nS + OVVSdm7kaT5SulqzxmPVw4kyZ9zafm5R2etMxvuAFR7oapBJyYFJm10HfXKbXScpPD0XFyN1cUOO + thV4dgXSB/flDIQCSsVbnAlDJ88hS8exKHcKAkybCvJ5Dw/J7HJ8eEDLK8JQl/LTcSFgNV9cpcKC + JCAHIEkFET1+zizrq7E8yOV9RDbGfWQoLeoehd9yPqsirgGVamev+3k7w5sEak6R87OsclI7b6n2 + SzorTu0onj/X2+nDA9jw+w84LCj4f/GKGhfSPAvoyDjueg2150mpyCKkO0aXvsxcXMDRyQTGHxUw + SwsLObemkBRYik7y/SztSgTVPqltSfaSlP2aXfcz26ppSr+YQUspN9coNXcpA1HAmCvoPXNZQa6z + xkPUdclLj/ixqH7GjSkNQAo65RZeXitdNDnQT9f/WrXcSOWRiq91BWKpakvab4hLMDX00JcdpFqV + o15b4IZYSdrEbvdOkDhokNORW0SLqmQujyrsNZBJKwXlbVRKOFrrTl4eb0+xEk5KNj24Y7dntYMw + ls2coOppo5ukQ/EVHz3OCH483uPJ/K8GRkqxRIHLlA2ADkpo6JtVIQTlUqml9kh/Jrcun0lz72/Z + FWPap8qb28NDPH9jsmv1pzCmh4dnQEDyBPcFQQxQWZ3a0VnQ9VP4045k6ADiPoW4YKmAAvzOY7x8 + UXwQvr+SY0WhVV5KvckNtTwRrS8SAQS0SRR1oOmf/l6tc46LFrASKrLDDEfBcJvWmYhqHvTgpUsN + HmMiRC0lvmF+gWplJb5Br0qwCnNzy76lGndwoy1JyRxOq0yg88S2D+DMixnyWxLM/FY3Wh3aDPor + itiM84zWqDhpEEH+6Tsx2uKCIV7P7CKONlNAhBlNPl6IqBO7QY75qgv6yWJ8cWNJvrJvVbgQodiA + HcmabQtvY+TsNa45+WS+75zi5ZR58hGjLHBPs7YGdukpnIgdirNcwIXFLoaUAZLb1E1itOsW5mxZ + TkmgscozXEqlK6QmmZ9upuLRazozwKYCPKqHDPdDL1pwNiv2l7QUdUscH4ynDczhObzY6QtvaK9K + 7hW6TdsRt3lUUeGx005SwUpHh4MMd8L3ezpdw1uXtX3i1CozxaLWuMQit9VZAQmZBXtA3Jcidwnw + fwnyfwnxf3V+YsACJlUWEK34U8npYngRVwLDnwsMUcUs6/wCGT7NCG+T3yklZ5T4eDHPTds75LiM + DrJSpNYYTkcBXIn688CM2apA1qWPW5206rtgDMw8h3k7LvR/qXqEjpGOL6UfJy5Ugal6dHhJrdSS + 6ZlLRZEgvxTVykeHlTfmtKQORXCpFy3MBB8Eq6yXXDyIU6K6gBHkTEQRnDAwpNhYeAgoznUJu2xg + 7z0eLorf+kCpQDUoZpR4VqU28dBTVWx7PJaUyCV+i+vNy7nRENAWF4xOLaLaQo9gCwkQr7jj2C9j + +KQVnbEewV8GqKqR7bfG4waFtrLbMVUroAcilB8r6zbDtGG6xnj1DavT0DSn3CBSGesG80wks5kE + Ms4J59lwTmRCie3s6191zn0wk6B476GeVFi0XsrPMxb71LGxTK4zNKccFzKHNDQaqQ881lgp/lWv + nCi+3mh1iE/X5Yw82FLHnj5ywxZjJc2g3PYfubzpP23BcJlXLBhQgcp3oDEdX79ortvORU43v62s + y0CwWzs7551PnYPT873dk9POQefYu86dtyen58edk8OPx9sdb8TfT3f3IbGXO52DHe8ud951tk4/ + HnfOD7b2O94NvB+cY0qHnk5Ot45PvdPcwSAjsEbIdoTH2c+829w5+njyATOddrzD3Dnu7B9+6lT7 + sYUJog/nxTN2ZPfg/fnbj+/edY7P333c2/NOcoe3eJA7h8e773lV3k4+9S+k5vroUc21unIJKT6M + 5q/rsG1Hd/nl9grCh1e7dUjFrtC+ufy6gtqk4k6Nt3TVj/LSf1wvXijBYX8oLqkTfCVjmiGJxa8L + lYpumPWzZ/TDOs/LN6FkPIgiiWkY23q2jjMzNJdu8y4ZZNQkb+VdCyY0Ifc0ZXwlwqTNLyIiqJhL + NWW53o6iz3Q6vm6zxtm1YACfdfO199rzx2p2bqEIyF0xjK2IhjtKFO5l+YxaQFlb8to6edVHU2wy + UGc7NqrVSkMqqJk8/ARFkg1pRmmPTU5h8ckhUzcspYurGYRqFDEBiFBNdbPjkn0HSnI2v9ulqp81 + BprEmRHM7/xswb/c1znxfp7VbT79h/nSQV454ZCQnAE4bWNHwNCRFT3mV+zdkvpyGWSXs43clYZY + DBAR6UNEyPREnZUuFBP+1GihqXHu/oI3wnwzkSEPWtxCzMeJzYxH8tyrzMrXz1cuY9K7Fz8xLhHC + rv4yNtXt8uVrU1j/sM0aKohNeWeo2S4kzdysSbqW9LKkUvfm5CyTyVIR25FeMzxnFuLlIML8M04O + x5WDvHLuK3Y35gd1z8svLmybl3kOYzXFrByI/2EG66jKYN0I9moXeapXL19KlqrEGB1LzmSbM05f + gJk63PnNu8qd7bfEMO3lzo8nxF15+8hVnW5/8D7kTg1j9jV3ftk98N7mzt7h9tbp7uGB907jtr6n + KvnzR8WPfQIeaFfyQO/n8UBMxFOUd1OkzvHfyANJ184nzvCRVXqbn33NMWxKXDrQlzXFGkMzDpCh + IZMDBzUQup8GUrwLpRhMTTwl/FLs48oVoCaqAQUymRZb/sTXGYB+iXsY+PoB/7B09H/p60zEuK6p + vXJT7eppyGXhqPnxeA+vsTKXYMbzbul8w75PlpacyH+XFwT67HtA2rrjmxFmbzQckZmHq72AnAvi + Wml9uyHV2tk+VPX//p98XGIN2QBv6RMks9L5A9cV0B5a3YjPyt2yxYkq358AgUE0C/HxIz1mxaOW + 7VOOciBl26PHrHiU2bgWwYJeOpC9LJd2Z+7aQAvKokRdel+l6/Eel2A1lwM8LsEyr69hoPepkOaV + VI5F68oNVbkPOdb+AY0XucgKj1fyGzx8qS1/WfRcoFFdrvET4w+LWrQ4ETA1APZK9AoVkmXOgIIy + IJKGuVTfZlbquQzmhLEvpFN1KdaVFt6qrraeNXQIgupRgBHfM0drQnwoqhMfVMMIVIqYIYMBKm3F + pdBnFIEBVRInbJaJq076kOu+1U27yMNtaealjpOxOOnQVxeu4eRNq2xu03zsqs7/8P75vrp/Hov9 + 80+unlhfk9b1v8zfpf7e+8D/zN3iFvr/AyD6pQoi7CC/UoMgU75wWoJiBhCtWkC09GusWl2PbOGv + 7myrtSYOYqruC6n0P8ArVbS77rE/feu+kt2rq6NypVNxmNZnWZSixZz4nqssDw9n3do8bgbzozh3 + 5o7Pcn2Jdxv4KS19AvQvh1QiRT2q4d0vc6wyc2Eox68ywqgZmMG0ddnLQueK3OCTqnx17tFnyiMe + Z/vlHB7HieIo9x63WnZ48+e8eu9x5wsnnVAACe9x3mo6FR0nCx/O/TA/nsOI1UK/dJErWi1XwF8E + f4OuZBgZopSORz0MRK87qoDuVbTp+ld+3Qo6Omq5p0aUGcLO2OhNcgMotZGkxihJmdFnGMoTumtI + nDGAqho9xmKjH2W8kPX9fcWsHQMoucYpjM0YBXdGGEwyZkxiNHPCAy1yJEc5HiSljNqKMrx4y8AA + Ba7rfq6gcmX4tCrqrUrrl5E9FVLxhfvz3Txk1BtUtCEzZ65NVME43gURjj5PDMS0KBhGX7k7VEor + n9MRqAHj4ONVhFPrXtbqnV3mzq5z5YCkEzMHiNFPzHkPW7N2WZgJXLeJcZkspdk7OH675Sf4pf16 + mUcXffPd6xHLA4POnujalEaejKPQNMSdCb75LoqD+CuLzbq8eMVHkfWnZASU9GvEhkNmfEhGDDNV + ygWTfoQTpxXbguyVXIB+gAiN65aW7fDV7vDDq5+DaDnfa0bvWumH8Hblxx/Xx0t7P+7+/mVwsnQ3 + PNn7M7gdvfAr1V0kycWQNbIItnmqOuJ6Eq320e8fxyu7vZ0/+6dvL9PgvNP8/c/D9y8Ofmm+n2y9 + /Xqyf7gxCrd+3flxo1OFRBJesfzRytfYIFxjAWuubqyyVnMtWOu3+quvXgVhuL5ajH0s9iffTC48 + rO+cYgxqnQxSaArZLcB3VHDEWQQT7obJqKaSQc/DCTiP+loVrbW1jbWVlfUXq6svX668WJtfLtNK + rTRfvFhbefmi+apZ39sKJgAFi8LhnJFN0qGW9TLPx5m3vPzI2JazHGCcLefZMGjQM1aMVyobeZQP + 2eOAMVI2xGjvQRpemgYFAMH4yEMxTct4CMdTgZDSqoCvS7cj6CSaU/imlsPFz2+++060TjVjXbhs + JuFlIwpx3tH9MQNQrzdv4f+qmmpGdxxfqIFQVbw472I0gjlYxiyy/CC4xgyN1ZXb1RUqLFuiL3+l + qtbabWutVBV9KVc1CuJowLJclZYfXDroreTNrgQUROYsGARp1BhHccz6DeCl3Oz6Aud/CAyK+V/N + 1ov19Q2E6T+++8frhUbDOEoZ3lCYGY3GG/ioVT6Gv8hwy6q3OQ4to3vG8gBesuW9oPc+TXKWwfbw + Fnj7q8bJpAeCoHuTDAYrphHAEDGnBA0+L4u0ME2yjHv8+GZ5WN/acjLs/680fMzo3t7/WNtHpydB + nDX+082eRgxkgAxNBP5X2/6PznO14d08QOOF/43m/8NNE7Gkmjav/VevNlqvmhvNtX9Pi0Qrs2W5 + kTTETtIYJhcJp1zYKuX69lplZcjJDofRBTJCf2/Nf0tlYjd9YuylTbAPVACq5rZssu7l8Rivo3Sf + YFueqoX3yh0IRtSNWf6cYjc3N65g/II4GN7laCj0zCaLsnlwIa7w/GuFYa+E3j97rGE/dq/S2/5z + B9ljAS6ObykCwlRKkfq/qdSXrEEyj7Ii/pYRwa6fJMPguUWAV7igi8pyGF7+3B6iqg27pcrx1irI + D9CK0Zi21L9xGl0H4V1jdDHKZzs5r1QexXdjENKeV+IyH/ai/vNrB355NMmja/a8IkPMmbErFPW+ + YQilieH810l+N2TZJWN5iQOroxcZZW2QwJw1+oDv0RDAkWVAp1eaK6vNleZGa6W1LnlwVbMpGXdJ + utEwcBkKlrtdKlDOJ/ojpYcCusvXq5KA6SKEzKA1IUYU0qWfkLBMqBP9CcO6bdDzvzwUhCeqBoDd + hc0zRrleukf0jST2DKy60VxpNNcI2IYxZ/SSmaZPMwSaaywaqpHMbfU2mi/WX7HWxisQ3DaCVy/D + Fg3eoIulfIoTjzQc1kecSGXAP15TA/Dwj/8aR0GcQAsYPKghb1Dk7rOpcQ85/gEoMB4Gd54Ro85n + gatlgzhvQ+IU+Pl/uLyOQcrYKJqMKqV6QxDk6oqVm86GMES8zPVbGv/H62UxktfL2vAEhv8YXAcn + 9E1gOCbwTEYfyArIJfQDQlEaajjPy2TLX4DTTu8aLbfVclfdURSDIFRFkzdKxfLtNbvD4CsQowCv + stY//Ruaakyif8cASLp7suLvvqXm/Z2T+D1+HUQX/46aocrlcBhBwtb4aZjwlfOty1v1KsgDbpmb + 5Zj54s7Hi4PIJr5ERr4Apop7gIyojzf3xmmUNng/G1iNiURDKPjeUarhG/cmV0mb3r0ZKSWl6Zmj + CM/lFJk4TVDSxlxVihJM8ssG0JQoxT0DMgh17R6gIdSTpxNmTp2ZYvzS5AZqLvOMihHgHykw6cXB + dYN3oq4NKMMjnOxgTWLKIHUQDDO8TAI7igMUOjeoBdVLntoYRFyjWt4TT0D5CLeglpqy8LVWH4UJ + IFt8ZZvSWv2cJi3E+6345JzDdJ2fb308/dDY3tvtHJw2dnfOzxdlg+eTNIL0487O7nFn+7Tx8XgX + Uum0ED5/PNj9+WOHlxD7NvpanJ9vHx4cQPbdw4MGGq1gkTDBwMFjFkf9/2+lCcIPUs1zGDaPWn3F + EJatq+R6svLyzxc/t1rvblu3rdHuT3Fz6/27IHv56WQlwKnh/d7tE3AHL4ONVrPxYi1sNV68GsCU + tjaCxupK81UreNF6+eJVj6CH96zDTomgQ33lT9TY2l64+uKn337fwn976YvWz0Hr+GD1p+baSTr+ + +bSXHqyc/3Lav8QzdV5oN76Osgimnkqz94OVny+p9NvJ/t34IDwZvQo+rm18aV29v/t49LXzbmWv + iXMfIn+0Bfh5ZvLowQ0x3Q3ky26StM9PuRErH/vUAHRt4LlyKc9F4wZ2ycZjNT+epRH0ctSb4ZJL + x4jr2SO13bDeYAjb3PwceO6BZ5WiJrz9HK+EnBlgF1dNlJ0AcN8l6U2QoluAWlDt72Yp+2P06I06 + vDYs27g3yPYEiAytwDa9hvDKaRB/z+Fd2pC11e0jGNYcykcDw1oI3QFaXmZn5vn5JBsH42gPbReT + 1OzKPDl3JxMtBlAl/9JWPZAxo8W9bJYZUaWm3cYYdLj/I8uDGgvIbEqOARkGE3Ogch0Tqj1oG4F+ + CZ3FbGNqkFHzvVHYlFipY7zEFPwPRqaNc2zpvVZnuW1DtuTid+3l4cE467Y5ZAIZChCqENZHWk7R + EcwZnDW7hu/DAMYUjwbyB2crXeveuOiP0y3UcLPs/TDp4dXCnsEcIxyN9ygAh8cnz5g6BiKFNr6i + Ke60etZ1gQEL5eUtgf3/s/fm620cSb7o3/JTVKOnm9KYVaxcapMs9ZUg2WQf0laLMttWj68GBEAS + NhYOCqREufVA5xnO+b77ffc80H2FG7/IpQpAgZsoi56WZ1pEVm6RkZmxZWSkGXHt4vvMjRWMx4cu + wIoywNnbeQ/gEV7DyjT4iwnux5ec7poa94L7tq5BRid6/ZqAxg0b18sYGK0+Pwi6kYP5LuK8j0jh + 7q0HY3OHp08S1HoF7d3OetB3LWHRYsDU0gvG832P8T12DQiodHnChNSgj/rbItyNI/MLmKBh2Xib + 0fGknO2Y0J13/eiqQ80uBtddD1r/TlP13qCRzwYJKoCENC97g0I3qOAPQKIPfoCS1YhpBMe1VTUq + D21gVcpg1CxHabHBRVvrwXDdrAIGZeFQ8FJ0gQQPL3q8Lo9fd7vHmNlf+Tv+s+fitS/4bzQqn05G + nQHhuzr8MVBF5TEp9hNjlFifq4XGza2uWjV8DK155py6LrYhzV1LiDhfyD7sz56Oy53ycGdEE40N + sVB9+KZzVj415INK2t0zX8jc/KW5fs53xxYHzUVwLhu02u3nj1tzme+/mP/1/osrTkXQKc/GXaig + LOHW0VNHi/nwc7kgx7oubCP8p3Wu5GyUfyOLLWr/50vNX3ld7A72IB8u9yZ/LZ+RBDCdjPnkG9sy + 2OtP4b/97clon5YzoS2jJds+mU774+7ZLi6jg1YjY50yXvQP+2+J1yFSJG3eQY+lQcrd+L//o/er + WJfv/2Oj/kO//7eNqtYzWo3DhWr/6ITvHoev4rCIXv/py/CnL/+v2hdK/kdk0z/9KtdlTO3RtJlB + Dcqn/dP+cHKM0eyQSEgjWvsaS2aNSeLaS1piaw9cadqv305IQj/jvUOqLXuJBGsiXltHEqo/JUmP + RSHzbVDacHH3XctQe2ucvY5pvlnmrElt59+EkZa2S5/7nDYhuw8Ea/91Mpn1ubfj6qMV3/nzbHK8 + a4NWB2tmEfB3K8VvssHF7xXADJcA++ELQGtAszaoOcAsXDarBtWEpP2p6d7mwYFvxIIrbbp//NSU + 5TOuOJJliJf2JaF3YyNwlsBgONifdojP1YIyl1/csTQSu/flYMwFnfj0xRdU3eiMwwATP1/VBzbF + EqpvKpVmSihaRCezLgSS7w4OIJpMHlcH26gSJlRkUNa+ovCTs+9L5hF27Rgodoh/7Q36b/q9Le+F + Un5hYR81ZT5lJh+s/Rq0no1J9wGfD8BFW69n+NHapi7Hwuet23L96df2ieP54ls2jpEt9ISG/vVg + VpUlcaX1tH+JBmyh5Qa4iV3S3Wbstoc6L/vlsAPQ4HGLDw3eBusYSjl7Ph10uRKHaSNKzt6hj/fL + yfBktpzxHK/qjGfgtlUWKFQtWbkQcd97ezv6u6fP5M7XW293nj6L0fPu2Wh/MjSw7m4/xqdqDrCa + t54iUwXv7UTSphpgB5bBAZyjRmV5cniI581Ieu0HNhFah4afqykeEQaedEqWiTdwP35jl8uA6uN+ + SX/6etdUbj2Yq7TVNaTEM56GExt/sPaaADKtbFTNVAPCXn9B4nXf0qavT8Zg2xskAfXKDcu5n738 + Gt/6swP/aRfbtjZ79vOTiam+P6lV38IDIvjKL4m4r22SHWmdzs6Q07UJEqGrWrS4COwXNIMoAi7s + M5/2cYQAEoosD+db40DsqnQN/6o1+pyrEVUeHXfG3POx+cLGXPq00fqCKY+XtObnnt+N9EKXRRb+ + lHOoon/LBUTtMp7m0YQ/5QKS6C8OvxqQ1K5w1IwilwyQLpswVSXKZpS1PcZWIcx+CMwXFKRyC6yw + 0cL9DxapysHoeNjf70x/Msx3ctqfQg0n6f9kNrnAmhx84c5MDPswO27cf1NCsSKSA69EEnBI0Cet + kyjw0Hq13KmZ+X3e4pnGVT2i2JBdk+eaREaohuHBYFrCsDCdGSneuIOyIvbXN5C/ZpNf+sQ/Htll + RTSFnTGdFlIS4N0+Ji0gdhz0mVz3gr/+/aWtiSH3mAYHg1nEbZg+9om8pPr76dB1YqMrrEVr9/D+ + 31JRKufrRNZd/+5GuHG4Hqx9uXav+vSaP22s3au3Af8kG4EKFgmG6PsXW21nAL1LMuT+XdOBe8Zo + jVoddY7vdjH+//zTv/169z/j+N9+NQeLbWrg8exuXItsLtJ77//zntGS74aSE/zYDjVEwDA09WD2 + Rt+tQcYQv+eSBmpSlL/pQ54hmO8ai+rz6eRgMCRVzc+JnTPznQrW2p6vUseH00nWPKE2S2qD52Lt + fmCqvkRqfVVZGJ+cm9xWD5XmPqysN+pDlOca9tNWz6g7UGMNtHVg7SL06/JuDTg7KotXNyw+N4KW + Z5fWyb4DphnEBTAwDdUsYPdsd85YTvrHry0nOrbut5DDnq3rreOT/eGgPGIT947ZrkQEuVXKJVIy + 4419v1VaatuClYCWoP80J2a0OvsvwV7vt1rvf6qtiLkZhW7f9CbXmsWh2QIwJSysnfrC+dUha30R + NxVWeGL8YmxcVujFNnTPT4PHHEKa2VzMo/n1wCPbAjnX/YpW5gtRY/MfFtv0Q1jRXDXEh9VwH8yv + Astu68vA/25StC5PbqvLQG/We+vl+hAB6y2Yb/4x/Okh/sHligf4Yexwv64dzkbmTgPtH3iwQ5C8 + y+9sQqS/e2+dH8W9z+V+Ltfe2xkK2NZ18LDHF6eNoRSXtTuHEB3ulvcQtqBW8mcqOW9VLeda6g0f + Dv/wcM2jYu0va38ePlz7cnh/bc2V+zkypgSYUqpvMCFUVGGVk8yGgf8vgx41OviyN/yyavcgMjHo + 8D4g5pC21xMOLHP35/UDQ0bv2SvZ/grz+pqZnrX1Gszra9+83Am/3U31N3vYLpczt1xxMi81lVeY + ycvO46pZ/OLyU7g8gWb6rj57pvYNT9zO8/RrnW4vzFwlmQXBV+YkYN4cthLocRkdzUZDQF2tC+/X + QZ9IMusPDo9mD1txiwhDb3bEv1gofDh3sPCAD7IGQ0jIR4Nej+9NEDwbBqB5CfIGYbYo+S2Apoyr + bhJPTSEa79BQrOXgV1Zsqjm8VtN/qMz7fRK712G2H68HhzCV9/8x/on64T98yLLOv+1GDFp+J7bu + B3fdXqxvxcDsRVPy57JFC9WcP434/KlxL3awF9fNecjCLuxQ7anZXJT7h5hKYWPVdedLbC2e8Ccv + n//wQ2s9GK3cW1PKvBe8dxvLn8itBy3rU0C//N5q3ZufCyTqy/NDF6iB91Jrkn16CM3Vqgxqy7K+ + MPG7vp82EEGV1ivOCZ0/29M+NdTvbXf2S1iVz4JtmOqDLdgjgu3282CTFDWjln01GB2a4QEt9829 + ifL0ELdBHhg1Yf3500Lv//3NYW+0d9aVw9P9n+PBzq5+szV4MtwffXv66pvhyat38WBv78U32y8P + B8/f6byrXo23NnvHrzZfTJ7vbiXfvfwl+e6HJ5rqTF79fTjubP6t2Pr5F/7e3TwcbG2+On71Q6+9 + rw7p++PDnfZjU2fX/d1a7P8ttUVwvT3pUt+dzRdx9+nkdFv1VO8sUTtnyWl31D3d+fnxm5128a43 + 6lIfR7P9b5J3342Pys7fk+nz3b9Oepsv3nw3yE+pltoed99tj4qzV2f5W/S5rUy5rZ91/ursybve + 5l9PO/L7YmuUnO6Pvh9sffPqeP+bN4vp2b56cvSjHMb9lwRne+vL55t/Hf6o/nbYf8rpQ/v9sCeH + v/S+OfR4aMIN+t5WXP/LradvTl+904fPzXi+bAWd4QxHIHZZFfivWm82aVfa8YRjpoe8u0u34Eg2 + HxgrcMfa9B6QKnF8P4gfBMP+wYx/cPP3g6I4ffPAts6powfBqPM2nMvGh7kiLfanrFbs3Jb6zTdT + M4H3lu3/JoT105LUiqDe+eLOHbbR3PmqNzg19ztBR4fE5EYTmpv+65LUxV/OXuPtoWnL8EDOf2h8 + N8yXk/GAptxqm6HVNjeMOW/D37o0ZY+n/X34jcVv4/sLJTZsn0OOh7k/6Ux7/8zSnApiYRCEj5xx + a0GxDWCitZGxLdm8uh/cgsOqw0bZxeGkCYPtvg3ZExLfcAf4YED1Vn4IR4PA/8Z5a1glOZqAU8kD + h17zhFnLJvuyH5pbnVWvNovteSFfeyXYOw9bBgNGUqpNKFxlOwMafNWz/0RFzf87Rlart1w6xLuu + o0HYnGObmG8EVkX4MjlXWVqbJ1U1OxImfR3e2TSqweEhn/NOO2/saNx/X+2fzGZ46WEeM0ed0f4J + cdDpkxnuuFro0ZHr1ZcI91GE6/srGLTWeyfmzqYZ1j9R15YyUISzyeHhsN7VJSAPcOoRDjv7sKnu + cJP8pf/2uMNhkK3LYH2EkDdPD/3iOz3kq4sGa3w/EpRrQCDjg/E2W1oovnArmOIx8xbJEK2ABIZx + aaQkKyS9UdFkergh4zje4Ls0lmKL/qgi5Jw4HfTfPJm8pV0bxIGUgdALQC94FnzFMv8jjDkAIF9t + mA+LxXC99dHjwG8Bfh444IDZ036fCfLwLCiPO13atezfiIUzDOBQWEZEE1D/XEAQiG5uRTgkhsgh + 1NHMxIGQR1Keys34HSXS2u/Y/QYFQo2FuQLeHtXTZoHCiaE+zNp2mBxOAvwTukEPz0KcyfFWnPtv + jjrM3XwKsAfrH+b280p0LC60zoJ1H2uibtFv1YFehjfkA4MFSC7eW7Z0a2kxNK177tm1j6XfDMo1 + F7qK82qh63xhnVNuQB8X4VxyorGL3fEiQLhivbsV//KoH8Atko9NGRmNa7mpr8O5Odn6dnfr6bMX + K7Bi1vrBYDhsBfj3YeuPQgjzO5zyrGCHTXq9pUFyX7x37AaJkkIfZVGq8j2dRpq3RyqiPFeEp0im + cpv+5ImOilid6iRKRErFldZ7Iotkkg2ViGJZBLKgUsmeaS/kEu92RFxEcZ4HUkQ6EUP6LNMilFEq + i26ookynoYiUkGES5VnGGbn9nUSFkEFMpdIkUJHCdx2rIIsKJe3vJJJ5RhNaUErQUIqM/lWR0Emg + o1wUe5rAVegpKdC6UkmYR1JqbjWkorKQ5jd9LgrqTlCbsaTqKs5cQshIFAQL9a0yRXlJogIRR0me + 8p8sQZEsS4dplKg0QEdZV0exKoBfpYKUBonvhRT2N1CtqUkqJRIGsKDh5FoQJJm2PzG+FEDRRMQ5 + oSqnYQiCTSJP5PqUsJGQyheJhGaAoBSBGYiKCk0wclGbKKgCBiFSKp2FNAqZuwTAjwmckMCiLggv + Cp1qzV3nAgWKIqEJVTTELK0tm8wsG0w5JahETpDFCZdIjkIskiK1ZUyiy3hXYDhYZylGnAqXkDLS + AqAIzAvQQT2ZryKslxHvRiG1owOVE6azo7CIVKr2ckKvOOLfXVoXtCJi4CHRhPKYsGR+08ymRcJL + J0kVITNOc/4o07AqIdN3O7RaCIm0oHgEkpZAok8JT1me0+ho0ad6TyY0ZxhdHmfIS7XmkVOS4Tmy + 5ezmMI1Q0wUWP+0OmkVddLNI0Mqk3USAAfm5ssvcJjAHktDG+KJeYloFhCRN24GAVbqwKeqa1lhi + 97OdJEnrQosjalAlQx49jTePRFYc5VGapkNUkrQT6SMhjXBLjdECRz8pDUKkmgommeoCVxlvBtqI + ARaOsL9TGqTkNS1SSdstp52Lbyqs8tVRyM2s4LtMnjYOl1jIAjvGl86c/Gi0iLlEk8y6mkmbz8fT + wagzPauRTc9ML8U1r8clmyXDhpvRH5FL0p5dYJOClp5Sc4DOixlWFlxQ2VayyUUWuXiI2cAq5+v/ + RiyyYo/EvIhIK3UkiYupPdrFhco2+eueUkTreCMHipZ6nFAhIdM9IhdJuk3bTadUc0/GkaAGQmTq + U9pHsR7SDihySf+qU4Fdkr/bUbRniFITE0216Aoi90S0iT5L0BKipTrV2FaF/UmEBOSashOi3uDJ + 9qeg8mBj1EERYl+DvzLn0PyvULTRwVmR2aWfWYqfAjQ/LkB9EuaO+E30l+h4TKBLon+EArBKDTg0 + NcY/CRrmKTQKYgwqI1aT5wCSZAWqFusu07M0JAQpMGCAk1Ld3HB+/kcp7kRBSCgS8wsjlKDPhAUi + YET2CCcgWVFG/JhgL4aMbqK7okugpSRMkPpCgMcolCttf9MgiLIDQlrt1HaMjCRPiP9lMrO/GQ0s + a0jCGEGhQyCbb12qUyBAEyfQRNAJBLscpF0OmFqksB5IbiJ4ApaIJIGV5okhh1JyH6n9bSQIDDqP + 0YtWzI8z7tbkZ+ATxAD1KfFtmR3xb6LKJNdQW9RBro8gk2li/hmPMMWi4WWg7G9IIxB3SS6C0EWU + nz/JsMomZsGSnR3Mu52c5BlgG8n8lJZKQiMkqU8npxCsFDrNBDJUynWzUxJhdH6EMtgVRhBIHHY4 + Qc0S+yE+QYwyT4iL0prUtMq14JmRJGoRP83sT8yEYF4UZRDbQvsZCKF6p4QGyK15suc3lusNiCW8 + E5dlvEswdVpl0v4mTIiEuyRJifYIrxKtIRoiA5hMtwvIQIFpmqSMIQ1SpZBHaf0dYYOQ5LFT0OoS + Odfuor04x5LFUqamcmTEkExoB2VD7DfIMgWNWOSYbSzhHMtJavMr4K0JmgtqgT2T8DLXWLJJktrf + 1DmVxbopcmx72j92neqUN5xOu7RhNG1okA4t0TAtYUc68LNalRlknUhmuf3N/J5xnxMMJJJgLxYa + GbSTQA7SbMj4xnTRvnN7W2W8vAkQ+ux+EsQZNwZKlheGsmAbSvsTgzE7M0sED1JZVKh0D9XaIo5p + 10rQlSKHiAR5LaOxEpRdzA4RyDhkcgTyIFL7G7NCmzEB8EF8Ssgn+sArMT0F9cvsktZ7GXXD+yiT + eyS2mgVNO5uWfWpXdGwWNO1/onDUIfQSTSuY8rogw8oQl0S5JYyftSVM5AUrmL9uchPVAhandlVJ + EB7qinZ3omgBmx2N1az9AsZvLGBtFzC1XRSpXcC0N4VZv0NoPgSlytWmxuLBAk6T3CxgA8G7nQQt + QONI8+0kY6zFmynJ+kNWvEBAElALEbPYFjOMuUiRrYx2luWn/O+mpr2XE/A0CFGcosl3O1kO9hAf + EaUSsaEjObCrZXrK35i8ySRlXBebXH6PVE9iAjHNL4nlQoPEE+UigZMJm8iAffqNhlSSvRslIF2g + 4boA982J7yoGPOZhBHbmKYvwAnrPq5ImtigU0lg2KXQW6lQDaakqSKXSkutoGtR2CjWGUFCwZOsX + k8lGhXyIRZfRBKWF2uaW0JB+B3FxcLwoazQbmw7nZVkIu5UAWkm6c1JuZYiqS7jG1Towf2Cidr+c + hXlRJlusGu5P3gaVYXl1Q0bS9Wb10FwlX5BaN6pGl+UsXD4IjIn1kp6o3Ni0X54MZ+VfXpvkww0E + muBnAvk9wXMGiNOqHoF2fDILcOfd/rwssnAszRVwzNGyIRlfu2h/S1i8qPUA/3DciamJK0byNy4D + om32ND2aDPnAwLjGO4Wgxd7CwDve8n3Ymhwc1O/x2+zO8WDGcT6pwHh5HDWsHAwnuDpxaOzZy0A9 + mu9+bg02tfc2dCqMP9xZKFwed8YWh7YOR+tuwMcB4vY5DONuF9VssmiZZhuUKItyb2A/Zzl/DIs6 + yeUc4bAZ4hWfvWZlEX+Onb0+A8biPuocjgcHZ3AGPwQuomAL4al7uJLTL3FfBC9sIBSsfQMk6FhU + rDJVXgzt4cLaX7DEl7Pp5Je+U8BMKrT4k04745vSq4fntDJbG6uEljj1+F8nnWnfWPsFkeB1GG/A + 8SBEy9U2hlq73cG0O+wHXZoykvxoSZ7ZH7T0EvpLbZgi5yB/2WJRZVWWC0s9zF4tT/ZHg5nf8s/x + SgqtpGkfc7OCppgzskvTdt58/GuZyvemk+Pe5M3YH6TdHKH3bTcbn2uweCAQsHPqQFlBNqoBfbWB + hbtqsFXqi5WcYPHs0rr47iN8Ww+WlsGYzTklTkGqTLbwLJMzcyFjse3yaDAKXKyJpgODChyESQs7 + w/50Vs57Hy32BMLpDvMdsdwnLhGYyuGYr7C2HsWrSOUSZlcZW7gwztofmwvNZeAmKxiaeyZvh8H+ + tN/5hf1bSiKItD2PcKUl4NAiNsbU+f00DRKdwkjE3nfVzCD2t/Uz4YP+4Zm5csaeBQud+bY6Syfe + zBXmcVfN7/5sjA0Q9EIOGHWVSzKjM8+erVvkzlnwd9y5x8u2Sye5vs8n/tT6gg0Wjs7CNwvtVWZG + XLs+6c68udGfZr+unXgvFzfeBaOz19zya266mZLVR9OE6Xlz7OoppoVptkvT9q1XazLuVtu6vnv8 + SSpQWxER4/Uw7vhyFgv0pdEHg76vcr/4tnM6ODRBf7wovrDrK8eHmuR+MnTZcK9wcOLqbjkLT8ZG + Hl2k2sPB4sDK8GDYfxsezjUSopHBDCIHA+2xYN0S3CJdpldNp8NNS7q1DIfr05wQL8trC0itYKrX + c+4wE5otE5PpYeuJg/ZC+/n+qpKLW4Da3fcwrVr6c0fxTVS6TnKb8ABkryDWDeIo4k/xDzeK4Jpy + Z0GyzvHbmugpkZqTPVEkEHLFfrYipsP7ufLlguG+qrNKXGxupe6x4WPeVs4aMo1gupFd2EZgreLD + JZiWcfw7FFEWpbCgRTCJoKzA+CIcRcGWB3sS7Cz4W9IP2MdwxAwL86nsRnkEG7kM2ExC/8rI2ghh + qOG/1Aubbahmzg3gfyrSbDEPo6wbUj8wziUhNwu7ccK29ohNcDBRhAY8Y6MhURTmGQzlNJRtRR1Q + F0qyCwIAhxFLsN2ZrXP8lzAwSgoaXnybEJFeBxHtQjJcheIOYRDdE92Q2wi5DFqlf5Ebh5pxoBkD + Ic7+VyIg/UQrYdvYSq+wEFQYcQeRdithGQMJY6BhHeykkn7Ep0JuZjEslznslnIvjZIj2tx7BApM + YbCh5VTCmCoFezdRHRjDYQvH2MzIMAQelzLj0jyfhFbAw+hCnkWfOfkRPDzCR4jaajN+NzJTksPc + 13UrHePHEGgidhP6Df8LESSbVBbAwd6pI9joEBpU2SqZrZKXYcQW8NCmuVoaZe9GChY/dSQxNElj + ercjY6yV0zQq2O5Os4A1gQnivwIbh6cLeDTTpkxtQk0XhyGSF5Xm/2X8FwuLDeKYcv5LX7kSQUDD + iVBZb2vad8AsBg8IaaUM8wiH3/lehFP3qDjnVLvpEJu/NcjpC9KUYygNrIg5UcisiIXxZiPJVxvD + wceRMoaDg2Xl5kL/syE/6UDCz28pYmwD1AvFi2FTqd+VaIERXFesUEWUXCBWoMiFYgVwfRWRwpS/ + vjiBMVtRon7YD7FCgRMwPczAhutEdFMVjoZmIFNH+M5EVHoiSpubK21SDlM/ycOvkaRRBkKKn/SR + iAy3kkTadGOaTe0x0r8cfcAlvKvTB671G5KGb7m/i0jDuKnU74o0YATXJQ0Jcc4LSEPCzPUC0gBc + X4U0mPLXJw0Y8wrSoJNIXFrjyFjOzG63xpHMaRxcPYV0DYkTgiaLewuy9o5InJQp1ZKUmc1RSCrh + pUwSfaxgFC9IRdsCjvckE23mKEgcI8OJLChnxijLQvxLst22LIAClp2GLP3Ch0fy4S0Wk/lLY/0X + Fa+mfWywa1DQwxNEsv8taegLC+qFZHS6ouDvipLaQVyXmOZENi4gpjlTlguIqUX6Veipr3J9kmoH + v4KqZkQB/nvZcRapKlpJWTlmsqqbySpxl9zJkOIC7b2SPAV87AFOY8VNGa+sN5JgZ3OiaQDpNwft + VUx7s23JQLHvzTAB/kBjU6avEn1LKiX2iBXE7I0EXyRGL08jO7QBI6Ehz1brTrZhroKRgn4CT9TE + JsnFPAfWTGB1/tAo/aFR+tkj1RoZiLWcUusEclqsYhZJDsScyywgiW+mH41RzFP6rzZOhpVrzty5 + xlcbRCpqRxb8rXYWYd8iGPuDBRw0Emnqj3uduSOEOv2ylZhfzDkMLbuo26LnEfnzbkm6o6ybINEr + qLP1/p475unUcTs/IVdEhI2BdTPocI19RGyYC9MfCRkDG1bxZrDhW/uI6LCBID8SPmoBN28IJ3Mt + fkS81MJgfizcTM+OaS1WoTBvCEFLzX5MLC109rFQdeNI+o3Q87ERg1i5N4QS09RHRAai1X4kNMxZ + sT4MDTdnelqBBlhZLo2GeiSJSmrxnheX8xdrEHZWN228SedFsSb9zD1kw4mP59KZneu1aHWztgX+ + EnESxlwmGIw5xm151DnuI1ZCJ3AIWOGa+RWedwoIMFK9zh62SNTOk1RXI9G1gZC2JLQinGAckZh3 + y2z0zCQRGu2vvh4n4FefqiDfFAn/vFyjTXJ51WiCq3aKb1kD4L34JhoVcVSINPvwRp3usOSvtGrl + ehdH9sNaaMy7pzUXN8TFU1VaIpPRoFupCTdCEBr3odlgNWq3AkBe1K1HzyxozcaxRR3qaqPuTMeD + 8WF5G0dtQbvEqBeWS+XIylqiCybXn1qX1StEJZpNO2MTSaxTK9YciChsLDwXnWiuyIeGKlrcF+eF + K2qt4C/nxyNaLH5BTKKFObL+1L/zsEIrWOFtDyy0Ot7KRcGFgo8RXqgJnI8bYmjZEFUFGVp2m71k + EINzQgutjCzUZDa/XRGEVi7yWxhFqHEp8QCuEE3ITPmVIgqt7vc3jCzE/X2OLvQ5utDn6EKfowvd + zuhCTKKa7+s1Hwwt+Qos637uwyqp9OpRh7iJG448tPJGyCoJ8hYGIDL/Nc3dFYMRuVm6ckii1RD8 + pnz2c5Ciz0GKPgcp+hyk6HOQos9Bij4HKfocpOijBymyYkeT5Lxsx2o2fi+atT4gblFT5d8uclHw + 28QuCm44elHw3yZ+UXDjEYyCq8UwCj5WFKMVutitjmN0Tmyg4DqxjILfIJrR+TD/JhGNgo8W0yi4 + kahGK+0kLvOTRTaqTlAb+cAni20UfEh0o/qx8HJ8o/qQ6+kGtroIx00GO+K2rxfwaBGsSwY9Mj1+ + cOCjOYzV/7voFJAr3mAgpIv7XIWEGwyMxO19Do50U8GR8N8FAZIY48u25IunfkXAJJPVvJ6/aCxQ + v4lQdze5jBR+cDIcGgbX8JrU5aTvWhPXlr/npe6riNTXEqSXRn1DAnTV7icQoW9KcL6UuHzDQvL1 + ROP6yvvthONV3i2XEYQ/nvi73NVHFXVvWsD9ELH2gmdFPpYIu0w6P0x0/QA62iy8XkNkbRJUq28u + Rp+nAaC3jyLj5BZxip9bxrL+tXoH9WDwtt+rHkHdnxA+R/xzal417XaG3buhiOM/BWGQxMdv7zU9 + NfogeEfQ9/pv7weheBDMP036nmacofnCbgbz//WbedU7kQbgYB5g54BpM62332gw7/1nS3kHSvuZ + 5KaOtRgtuAUuTqLzCGx6m7rmQlgvgHk/od1l+96Ya8JkhvhNQNVmct7TlNaMg/Vo3o9zxXplatEF + FeqQ1M8i08pTbHONbuOAZidcuJ+HqxHsYcd+Y4DCbbIVh9pHo3+uKPFBwt6E5OBLPmBXQ0BQQ9vi + Z3djvPxIvKeABbmIsv9e/Kd+s5yZS8WH0qdZnj2+LrfBDWL6R8H0rkPLMS7DbXLDa3LmNNm1+cz5 + fuHNG+cyGlarouB+92La5pS583aTc738VNup0dlkfjAf4qB8PU+TlR7KV/EyuaaPyTk7Zc6/pAFH + N/Y85GfXks+uJZ9dSz67lnx2LfnsWvLZteSza8lHdC25vNTcJC3P3TIcTsp+o2J6wQW9c28HztWa + v6n3kr/x1e1zZeKPLUJXA68rpNXXj6SBZgjGlUXi0tJzGxBdRgF9+WYSdKeTsuz3zM09wnFnFhhF + cxys/bA2r5CS9vmmcwZ1lAeNMjVbyXowmcIcNBqUKDmezAYHA2OPiZqOVq58RbC28Oq205q19FzV + NEX0LvqHBFSSyBLcrjdpTl58h7B+e7A2jHm7nT33CepvU9QWS8MZVy2U4ULBsBa4cPHznJHJzWm1 + R2stnRO8b2UdM9Ama5Ov2Lh7vXnT5M7DvpAJ2119l3933B9jBZmlVVlSm9C+Av3+8/LFjgaE1G91 + LIVhfOjUyPN0+suG8boSQboo4uIK4uQjJ1qg6iTqQ4iTjOfiKKZLcRSpxGrfpXl9/irXMX2Va4RR + NBEpCtIdBSTTJBFtl0xJyISMK8BIBaLxZanAjS3wXfqeQ/EtMuKzCaRdkhNIqNFpWxClgLZakJCS + 5SQ0QTXPNC534RKbT0Jh1bLt0xKXL0kM4Jt6LDDoDKK30EZ6NmkqbxVxnx+TDCG4vsQ1RdLbcZWM + RN84K/a4doxqMYTmgMODQOoXCCdIKaibvg9OMkiJyKtcXGrjC3USNwt9GvqDytouXUDoEsjPcUcS + wfizTKU8Ih0jFmEc62Qb+CsSyHIEUyzMgAqWVFKJG4cCcnGKO580Hz5Jql2Rqbzt0kBXytBB4ETw + SZlkUPhxjKWVS/N0aDYauPyMFRzcsSP4eDRxxncnY5lC4JY6zlJGAvUQ4HJkJhPBw5AFgi6iiwT3 + RBmYnOVut2oELtWlwfyaelU3/sD+c/6VuMroI3AvlDqKpBRKqE3BGozYI2E6Jn19E7qIjEnDTUhR + lPkmKYxKkQTs86FUFWLPN2BbvC5AWUq4ilMPkOm4KDbzxHSEyShECiE6IS14j2TphKZxMyeM5XhJ + QuRZnPhsA2fqq3s4bUfXhTOFepsAcZlpL8mxKBjeDB3aAgRfnudptp3yPWYVuPwMt33jdDuFJspr + t1DU0jZ9VwkjwDZMFQVpcduuR774XMg9V+C6I9CKdC3eVCnpa4IBF3pTx7hsWTW/qWlREuraGuQn + rkYckCIrcdcYkYhI7/FpbKtct12SqJqSRRYkMdEFXB0mIkcqHgwC0HVJn4mTdDthg4tBjwQ6oQ2r + fBthuxNSEGErlOmmBXrPAv3KjwL0RCsJaAWRhjbaV7lw3wMNilHkoLkiTpRPW2BdUoHEEZ1y1bGH + JdFM26xLezhs8+7vdedCwZyBK+B8KT7bVJonYVsS8goiKfG2jIHGxJcg3U8RLduWsHZkTKwTQvOm + Ig06y/PthRZf7SBgbMZEBogtuOlcJ1CeE1JscTVfFapw+Zu2/HVHhGvgMGqmuNQLzTeBqa1A8ChJ + 4yBlM6OZKHJwOZlpgbjivhAxjjwmomMr+6LUqMxoKyw0fhUY568vXObCL3+sBGh39Hey7wW1cl62 + nMtqkI19o/Xwsqs9b738uD8Z98omIbLe4ULQucaToA9zkbNy5qMnAOdCZX0etYuh06+PjIOTW4WM + r08+JTKOp30Dye1ByPNpPzRi8yfDCjuG2INUGg3hoNwgNe71z5Nx/xYtHYOlYIcB/MTIuoXoeTp5 + E/wVIAXbg9P+J8NPzav5tmDmx8nJNHg+mc4OJsPB5IMxUw9wvqrUZ/vQR7QPLbhJXd1KtNscurzu + RnW54Oa/qY3IwHRDJqKimLMQZcsPpRaI6Hqugcig8Sr2IVfjmuYhfi9RkVrURigcrQv2CFAIREVa + rE75ZD8htUjDApIXWm2TlpTBQCBijqDaJh1RZfAGiKMUIbRIm4xxrgbLUlbIKg0Juu1TCh4YMJSk + RYyDV5wcC34vMWEoTLpNulmaw9Bk83EuLFnnSPE3xjm+Vmwh0oXK9zjYVaba0FhzfE+owThj7UmR + KihSnGJlVT8m3c6jOBHwebD5iFYUG5NSzIfELs2nb0nbp2OclEGVVULGSYAwRILDNBUq0Qj0RSqR + LLZ1pLMigRKYF0phXNQM6xFa4xgd4BcxDENFkevCp0kbIzUsbbu0wlEYn1CmKuVjSeo9gS4lsrhK + t1N4FPChncnPI6VTDqmViZjGCUNaBuUUx9QxjC4xAZQSHhQ0bVKgZR4XAFdmNLyYLQewY2GUiUu3 + cc6cSuHz2c0gkUXgF9aVNKRKh8PpKAcek2h3UxJeobvRYkxzraGDJin8EWi6kpzUthh2s8Lnoz5N + z56rb9u7HjA65knltZGkNHfw6NC5SwcKk2EWpZQy9WmshTxL2y6tacNQLlaY9BhzjTuMas0rpsqn + FQSXJZgI4bDj0q5xThfKd+7qO+AWgH/lRwPXiTjO2iqDl5R2acBHOwUrSstcJT7tR2PTMFXGtCNR + n221tEN1Vg3IptuwV9BWqfI5spow9Yk+uLQfEKWFVL5/rp/lHr4F+K83pSmIV+rXdRuxAuNqGacw + CXH4N5nBRplqInKaX+yJhUrwNk6exlQNDi2Z9LsyxTl37mELqBsRV+l2AoNaXvj8JAHZCPisP1cu + 6TBhk0AUgkC6ysZUnfrGbbrNnQtV5VvgQCtp57sx4GXJRLBZVapUtv1YEVmOmreocOvHYcotfgIq + V1m1+BOJ9ci9qIzWtUv7Qdg0vKNASF19j+r5mbjedGYKHj+eWmS0RDi2Ywqql22a7L0C4RvTbDsj + Yp3wbRqTnRNGcpVvZwnsoTAL6UJn23gZi5qpWi3gaUfVTWcw0xGeHYm5HuB5DiaaeS7s0pYX5wV8 + BjPPiwua4iQpPC8uVJQnhlsxLy5oTmWNFxdgJ0J7XuzTzIt9yvLigpZ1ZgIuMq+F85XKPW9uw76e + yIpXE3RJkQrPjNn5LJVLvJjKyUxWvJjaocVf8WLfj+XFRUorNUmqfExInHhe7NOWF/u048UZMTHi + tY4X0zhjwZKI4cWFRLhE5XlxwXZHz4oLxMQ0HkjMil3asWKXdpu+gLMkCKdltQV8Y7RPA82xZkHK + 5lNHistbVpzR/hRiiRWjXGZ4IPNimp4Ch0Fu56CfmKbL0TCCS1e5biVZfryw0P5F7JbnXAX5VDq0 + BcOK+sHXsH5Mb5Oh6tZg6DZZp/y9qduDni0AFrQnY+jgJwDtk+Go4fGEW4Ik93ZC4F5J+WQ4sqSI + KOWg178NBwgORU8dRJ/NeL93M97iA3xXt+NtrXh0b+4G1+Xe5ftNLXkWqJsy5eUXmfLyC015FpNX + seX5KqsDStU0hwwx1yHQZaS+pW2XzhAZnqTCHDoDqQp5pGRMqgSk1gyaQ5yqQkEGzjK2A+UJqW3t + Atohi9AK5rmCb6uwYpFDfPRp3OxRXNykFcmcKocsmuYFqwaxIi2wYE905dKQTUUGc5nNB3TSOHop + yNg5WxELFulJb4RnV0EKS9vcZxHGNwNaJe6fwNSS8rCqfkzayNpZLR+qQMo6shmuS1O9AiqQTReU + nUPBonqxhrFDK3iS0bigIsCXg1TBdLtgxzbjP5dDVYFTTwY3fglVGxoRbDywkWqd+jRuBlD9tksD + awlpKFQ91RxrPxe4LlLA+17lLt1G97iP4PMRIR02F4IiyxTATeAfpyNNmg500awoMgk0KFgGSB+C + da5g3xLoBjCWpdyNVIlLAyyl2BXL5Lu1RJOtYJxdWGvXdrAihVrAJsuGhpTVc1ptmzmMukmGSSel + VW5mGhfLsj3WcvPNPAb0uEMidVr4XJiFkqyqbBrdtJ1c27kKmnGSe9Sk8ElLPKqCNMG1uYzHkNKM + p6kbEymHcY63GlKZUD1aiGxuNRNNzeSwJeAaYob7YdRNkSmXbicFVFafncCph1VQ0l9pGbs0bm+o + RLddmh0hcaPCVIfzYQGrrW3dpjGIPElllW+AYy9LkVRjgDVXs2UojvO2HyosNjEbxRgVvLfgQ2pR + ZdPthF8wUD4fr22IQpu3LGApsmk/CJvGCxq4zufqe1TPT8W1nc1ydGPAxmrTsPrArdAuGJvvVuN1 + u5EZblXiBpmK0azCyYVi8ypROuW+w3yp4DCq8RhGnvl0HmUKVjyXFmxFYPOqqQ/PWVy6Mu269Kbt + d8+2/2qHCsSw/GOyaW3AXss0xabRID98B+9OeDa5tAPApXEtS6G8rQ8vL+4+NucKlNpUsLjIfM+2 + vWlzr+25xVd+caqTFSlcQfldEpFu47EF3Am1SwFzFeey2IRdi9gXXC+1VOm2udqaOfK/5xratC1f + e3LBO2Pp11DsV4/Nuc7q+R3bVW6j2cDJpbfUbnCLHFw8pm6RCeG2OQF5HN0SJyCDn/LPndHxg+PX + SRzfHgztMlDPAwLqNuBo3Cl7nf96LW4Tir5lmD45dlYZBz4NUv4+mQ5xwen4s+Hp9254IjZHa2Ew + G3yQ8aldb2W1Aap7TrFPaoSqAXZT1w6zi5zKRHaxV1kNr1cxR81Vu4xJSqS4FQefIl3ItGi7NO7G + KXiApAVOKHMonwi4kOE9RSgeuZZJsk1aRoRnvnHBBs4VIiM9VLINSnCABkpnUFtxus/Po9s0KTJS + 5G2fxjUt2IgyzSYvSuesLGcIn+PTbfQf61o+gadyUz/F1T4CvygSY/NKNMwTGakhbZTDCSuujHAY + nCyOcI0KSgmXd/2YNMNV6Hp+Troa26QS2AZ8mpT5TKi2SxdwsMBbkFmGc3TcVhGa6yM4TYJ3HDUp + OdvoT6Xm/Bp3+zLc6Ka+COkJA6c4oEOSqLjwaZojhbN2lwbONM7oM8HXIxHchW9s0qTABGTTbe5c + Z7X8NGLnIUlTlzCsiNUBXx9oRdDbFKYS4TDMUbXOgdsMtqSMPpDWlMBUhX747pn5wIBJBF/yJexq + gntKAU/A+dV2bZ0zwXud6CfnfqDJCZHKTQFHGAQpUXgBEW/PwxqT72HyacFyPu2+PYTtUEr6fIVA + OUr5+q7hTdvTtQHVKQPq4dOwpdbbNwXcAK5/fSqxc+Xa5acppR1YSmkZGXuJiEkFx+0tP2zKRfUs + rYFl2rs2OIg6mKXVsBEDIte19k2BDx12gctgCRsn8ywpzG3UDDemcw4+Zr/zlV3qH/5MeZb4ZE67 + hohJ26Vxj1qxQd1WT2BIxkVpZR54NelN2+2ebf7VDgrkmXGQzSVt0Djl+DM2zQ2mKbv0pDABu7QH + wKZhvJP87GZq49cQzYQN0LZv05sI/pNhOk37mzb/umjM2eeHLSXUc9bO4XlkiCPSQYZHOOPCPDWr + fRLv2UqRtl2aPZJgQEf1rCILgWvf0wnYrnWNTBTSzCNayKlDl3Y9IJ0j8pYBwFZ34C2A/8qPB/fS + kyJvw1dMisSlgzxmUz/8auAO5tOuO5cWzOtooJKva2I6M6pRdWjSOMbJEZDO5xdwYkq5fiqET/v2 + kc7zqn/iWRVw88Bf2yZOfZj3UWNVGM/BPBUJTO2YrD2H++0ULxUn7PhKW7HYTvgt1YrK+5qwVyfK + xIWiFn1F3JYXidpe6PEDTL8qE8US4EnMcFWAa7hjZ9IDrohdEsqWAQcpTBLpAXcVHeALPV7b3qiI + qaWJ30Wi4Kuyfpni2n9i3nTOzGkXJ92icGm3i1A9ZR9Lu0lc+34XIfgfBwh0JYiqJ4jE4LaRS7su + lLHMOghcfQfgwgBe+RG5fSRpCnBz2y1VwQeTmV/HLu2HZNNuH6F+rFK/T3yHdh9J3MaXosrHG9Nx + 4feRS7v2Oc0xDUz/XJ/98g18C/Bf+wgsUgXORjziC8R3qMk9fNEgYY/7HFtEmRGzS36akHy4p3EE + CHGX5CMVCyu5BYh1mXNcPANwhgeoWVAzCIeYlGVZNaAojVO8CI0D1Uz4tEeITXMoD/Z+NPWBUDw6 + 7dq36Tb6h3eiy3fg8TlGLKphwIcad/7Zhzpru/HiNWcc2Fp8eH5hEeZ2gkLgPvvkt3nBmR+Uxgmd + Bx2hGBOZcghKU9pjdxH//yL2+nH/TbmB4PKT6ewiZf03NQbVYXn0LUH5ycxjNUgIU53hbDDqmzhV + twpHNaX805oT6/g6pDHjcfFu/3Yh6xtq/lYgaDIY3kb8fDcY3gr0dCd4/uE2YqjNkH02UP/uDdTT + s+PZpHsynfbH3Q+0Ui81dY6p+qKyn9ZevQDdDRmtZSovMFpL2CEuMlovAHcly/VS3cuYr0lMjVJZ + cAzrQpNOZNMpfANJcEYs31Ry5De4M8lEc2xf6IyZyrZlgkhw8KjUIkbtjAPMFSSdsuKQ5AiTZC4S + w6Dq0gKWVFJYXJrE2TQhOZ7qs5WbQ6eTIpLgdppwaSoPzaaej1i5XF2lSMaRjbmUi5wdKhNZFFQN + H9ipS2YSYChcB2Qnrwx6kevGpBmsNEur/BQho2G7Zh8vl0SQaU1anUnCnVBIagvemoiVVxRxrHlI + SQqtXggSvrfRF2R9eE8q1E44YD17T3KUO0Tmkhmbsgp4Rdo07jdSA22XlrCoJQnPiEw5+HUGWzfa + z4rMpXlKklzW8nO2xSHkoYLSisDMiq/aiiyGN6UinRc4KOD/CO+jVMGPEg3h5CJmi6PFWoaQ4fZD + G7OBM45aCbOYCFE5fO4WFtv1DQVJhLtl1Bj3w4aKPN+UWqP5PV4dcbopcUtXJQgJp5WitIZja76X + 4oREZj5f8p1e5eu7djdtR9eGUxKqY466yOBJOGjpWvMm34J/7V7gJpwVtWkQ8I6TtWkQhAElGF+K + tjF9KGx4eK3gSUZ7WuSxbKNgyvYJu7Y4kiUHe1fwjAWgCLRv0lRc4jJhLZ/vOMJjNub3JWwaWyGT + tHRtWuBKJZasrY/5wjGMa9+meSA446nyDXgwM0tVjQJHDmnCuz3J87YbroA/MEZj8cG7GVYRizCb + ZrCYCLn8OOflxVEsYTeyaT8Mm1a4QImw87Z+Dd/zM3LtmY1j2CmgtydayASegYIW6bbArUcEqrcd + YAWlfBxT8CkFpWgCxTYe+tB2AVJ6zzW0aVu+9vEEDrIQ/n5hA4qcNlia+g3Ih3iZ9BsQ+TJL/AZ0 + +W4Duvp+h9iOrn/MlSPYPg4VVSwNmuIcQfFgKxS1flI+Tm2LVEfwT3UZgUhhzeX48GywM0lc1ifq + 3LZJvCECZilSmnc4uCP8KDtOwhMa5h24/ibbIs0ZbQDDHLYhYIOk78S2cOeWwKS/mxbsPQv2Kz8O + Wse0sQy4GY5p0IFS7nsA8FUG3pLECLfg0hW8Jo33TAT4oa0PT3v2ljXturQHxLbv/l7/ME+wMLE0 + HSqHma82HTjlwzGQzhAAojYduuAI/n4+bNoP0Kb9jFADBW4NuxnR1KVMqhkhhlQYQcDMiMYjOvm2 + 0IaT+RkxkNdmxA7FzwhYSwaIOc5/NSM6Ze7rZ8SmK4BN2s+Ire9nxLRbzYgF5KZmRMb8ckuB5yWK + +saTeIMEcT8TwNW2ab7lzsf3UiP6LrsRSE7j2nrq0lSe9rw50TH5cJrOVFXfpm37tY6V5LWAWMsp + /A5M2njxs2QY8/syMIUqlhxTfpTGpqljBIDIa/lmhKZ+FiyM+PoHtAjfWfFcYSy8FQ9AEGPNEU8s + z+WIzABkgeeiIHEyz3NxllrknslyNGgtKqYrYuNs4PPjhF9kcUzXpR23cmnHdF19x1Rd+47pcv9Z + Pd/AZ5muH8YC03XjdUzXIcQxVYswx3NxQCt0LTv264t5rktXozBpx3Nd/Rq+5ybk2gfiCKkjhYcz + T+H4kXk4waCgByGeiCZK6dIOTpcG4SfOgOq4z+DBdO37hYO7W0WNlSPOgxmollBJbNK1j6Q00Sa4 + f1fdwbcA/ys/ILd8cLeMRG+/fHJsH0TQIc5VZIlP+wHZNJaPRD7Vz+3yyOKi6tCkcXkr1UVe5Se4 + K8fKEtQdn/YDQsSbOPH9o35SiZwL4F/7MBfhexLpGQnCVWOerOSRIlhKzg4BEGTifDPDi2CJ9vlo + QOYVmbINXheeBFEsEj4vIm2aey0SucnqkUiqbhLwjULjplRqIk5ZRghGzS+4kfompUsCtznpbjaJ + 64YpAkejNuTs2LzLYxq1yU0Ly55Jvtph2Yg5UJKnqc/GUWGaMEiUjXtPJIlCreXP6IPEGYT2ykmk + T3zageTSfGdG8r0nrm+7CeZ7vXZc5xzsHsRPkaqtcJ8yqaS5TQRpJjFgm/Q+xA4PGKs6Y0EkJflV + I2S23rOft1lYYBXY1odUQMtie66bD7j3U/BtzUVpCIeMaV1LJFYFbYufN6utAclvfCkvDLm0ky1c + 2glDeNBOKi8LEQdXOUcEN7KQLCIjI1lZSEK2zXC7KBNKVqKQAbsmCtlxOFEI8CrYdqj9gkMGGVFI + 8jVOLwnZpAfWJJ0c5Co7Ocg2WslBFoobkoOg5sZZUiPLCByT5zX+QlxSyqzOzzFNy+ycd1Hq2TlW + ikgqds5nscrTX5xQ5+YyrNWhI5pKXbFzl/bKp007du7qO3bt2nfsHAI8hwp07NzC59m5GcUiN7ej + ddzcocOxGYcvxyZhklKF9vmxZ+Sxhzz2zNuV9shdRP+/yAF1Zavd2J91w5Oy9+nPgRZt94+eDGbd + yWD86Y7MKiT1Z0e3FEnP8JBY/2T06bB0iaOeT4yjxfOJmznJ/3zI+KkPGW/kePFyB4u39Ujxpg8T + RXLhDZjkEjdgrneMeMUDRDaZ4fhI5ikevzLJFO7V0Et1wW83wcFXwFMxEVHKQQoVCeDbAodfUFvx + WJXO2rhOAJG1gHYGNZfUglgoK30lVVog+KJq+zQerYU4k2g+/IJfN4s3CV7UlC7dRvcyq+UDPMlB + IIoEjuoEvTTnkzn8+yEP6dTYFfmFaHj1Q3pKcOQn7cmOqPoxaYYLsUOqfHiqc4BnpZMqKXA0SOKW + TcMt25xTQgeAH2gcwyMYaCGtCWeIVF1v41JFknMEFh3rhIel+Mwww7PDsGrinAQiNd9PsWkNHTaj + ObJpfk0YQWwTPC0NmVQVuI6C0eDlXpvmWYlTVcuHssYPYqf4DnAzvDGEMDccZEJlCg97oRze18Jp + gMQ5IKJfKBb8lOJLXOiJXRXNBwZNc5AWV8KsJ5rvGNE651fbtVUvPKqOZ35waoLrLBw5GZdJSJfi + J6+SIsOdj4RVsZwUoRiXWzSkZVx+SQpSEH0+WxwLV9s1uml7ub5tNjU3syxsEldyklrzJt/Cfn1D + Jm4x1SaAgxCp2gQgyINg04kixdE+d8dhgmkNsGsuLcoi52fZSH+tVpXAg8Os+dCi0MaQyAfRnOb3 + 6/BMns/HsVpu34uWiU/DAEabv+3S0HwyrHJbH7OVFMK3b9M8ktxcOrD5Bj6+pFIU1TgQEkQZ3SdL + zDg4AjFtnwJ3UixGjJFGBRZjNsmPCzIJctlxYp6pRwhgNvGYdDUMk8Z7UtC2bfUavucm5NqGzAyx + v3F0SEp9odi6o4XazhFkNxO+fTY7JLHY5CBTMc4OEe4o3+b3sc3yIxK25xraNA1fW9GGZYrNnfM7 + L4+J4pt7Z7z1MhwvKOG3HuWTuiv91nP5duu56n5z2H6ubZ/DRSVzPEHIMgYimbCtTNsjfNMPEW4i + bWxxy9Nc+3Fx7CPNIfhFgUciXTohvMeSQyxxGpYo0uAR9yeDTgxNG/c5U6KDYIuJ1HIb54rSWHn5 + mDcFYSH2CbqLBwxxaVLrdNOCvWfBfuXHgWenidEA3CwRWRutC63dd9gPYcWAcUaSau7THlqbVgQk + nlp09XHnE+TatuvSHhDbvvv7AWY9ASPG4nQofiO7Nu2aHQwKxJOPM3P6YqZD097XfJPBTIdLuwG6 + tJsOaqAAi3PToXEzgI92zYzozIZkszOiE9AWua0KfgLBz4iFvJoRNxQ3IxoHkFnSRgegB25GtGK/ + Gz8jLu0Btmk3I66+mxHbrp8RB8gNzYgxUiKQXqyL2gRIxEaKebwElm7bNJ/7scEaxwDGoSvXkm+Y + gIm4NBs5LZE2+VlEvKFW36Zt+7WOC7MUCsRxgpuUSXPsPQSfk7hA6SJIwYcFsZxchCm8WUoVi8Kn + AztAV31+vDdgZjTctmZmtNS/MjM6buvMjPPMtjIzMrOtmRmZmdbMjMxsa2ZGzq+ZGZnZurTjUnUz + I5ht3cwIZlo3M4LZ1s2MzGxrZkbDbL2dcY7X1uyM4LV1OyOYad3OCGZbtzMiP/ZsNvagx4611syM + BrmL6P8XMTPO34O5RSaimnHo096CqUyMt+wSTN18dsOGsw9E1lnYnYyBiP6nj3vdgKYzBPgy4H1q + TLHl+mR6OyzXdZv19y82vt99ehvQc7h/fOvQ882T558UPSau1wkhYjIcdqYhp28Vhr7fDZ4ybAGH + 1Ptsyv+9m/L7s4MPMeI/e/n1ueZ70/xtMtwDohsy2afxnMU+XbLYpySrpucb7IHAq5jqTflrPoKo + 4STPkYRUnMik7dJplAuETU44RBI0iQIvryWSFUZY7pNEiO1Ek6ZsLv7oPG3DDh+zAF+kmfExynJz + 7wdePi4p+Imqtksi+lGCx7GMzqCg1asAZwIiUS6Nx7RiOJj4fLixm1cJi4xAQizjNDZGe2uzz2Kq + RqqnMlHUFTzOYOSGlzW/iocRuW5MGkAV0jx+aPJh8odDJWEIph2fxm0WVbRdGmEmChjfc7YtQA/h + 2Ct48wsPN2UIWZwm29SfQtCpFD5bgt8Ii3EpJkEgLho+bPiajfZpAsuESfL7cUCCSXK8qCzj6SgE + G1NjhOFKOCySdml+sEymRZWf8XUj1M/ygt2vNAdmJnUagYsETPsFQmwjEj0s9kWGlyMTXCUw7omk + FeOaEkIAidx/AGAiM56YpoRbSAInK6RtLSy0a9uAEJJLm5hCkg2SKfcHpyxqn02BNE/GKERrEhaO + NGOfLZUUcKbPlJQ+1zazaZu9vks7rm2xoclAIzO2/BhoCglvKJ3GielPpHBcynHHyOWjAYK6Go5t + 8boAxR4UP0Rap9SDCXYkY43ozTS1GrbVPEaoedovWsG2inD6uc93CHX1PYjxv6LiTLzik0tfzEE/ + ra5MIPDLYrdA7TPYICb4qd8TA04GpG2WM4J6hujXnfFtMLBUCNoqy5ObiAt+y0X0+a37AaLst1x9 + tSg7bsr/pKIsILohURZn8nPCrFwSZlGE5K/zxVlDpy4vzpry1xBnLVKAghAf6typZYTdJIJvseyS + EB7x45IkGfFxFYzFQ5IMo7RLgkyEO7EZfaV/+X9UPuQKSUhyBP6W9CMLKa3oD4lNp7IbkVAMV/yA + /ZcDtsLzW9AyRE/4S70EMaWoZs4N4H8q0kP6VoRR1g2pHxWiH242wf+oRkQCdQiwZCRCA555b5pk + ZtwRwVBOQ8nVU2pUUg1qN8EfQBSHKCJCDlbKD0wH8akgsUSdUnP5UZhEfKh6RFIHHvg9CvGDcqjE + XnwUkvD6bocEW+qF5KntGLVRj6Cm4nJbwAK/F0kSIqigKoCyIfWMk7SEB4l/SaqFezmh4AjjHSqg + FAZ6GQIiLCbzl8Yquci7Wy5SXEQIOX+ehGGrdnqIsTKhbUrbc9Yhsj9daHqpRrc7ORnPwjfTDoeL + GQ3OpZP1KkxtG4jkOcVXguUr/yEMA+zYg8G0nAWEprI7Hez3g0EZ9Abl8bBz1u8Fk/HwLDiYTIPR + ZH8w7AdhuKK1jgNk1Cln4ISGmM3xxKqTcH82JgSATvWDXrg/nHSJhhuKD7pWEmEzz3OU0f5JSQ2U + 9E9JpacRceWN0ZnNbQVMlYgwnAV/78y6R8bazwugL/vhuDPqgyTbfp/MnHHGv2wRzs6O+wCb2wtt + L+HoLHyz0N4N8YfR2Wtu+TU3vUpyqY+nGePLm2XVdln+uPDJJMGY+l0sas+cJt1BZxiYPyFPFuFn + Gb0mH4y4nBtQtSpqRZi31dMHnW5/fzJZWgDgbC6PZ31n+sus3DIT1Aqs/ayqbVfC1/5D3czWHg66 + vwSzCXG/cjALTkpa2kFVlEd0MBkOJ28YKFjp+Nts2un+EvZPsVKWelzAQw2UDq0GYriv94cdSCdT + 1BtPJsd92pDBeGJ6WiQYTWKBa9OkruuFCmHNCwFIzMsAzEnjeAGexWVk+L/DWbBFAK0QCipxYNif + zWjEX0dN4sBirboIUEMyvHgIwig7Bdc7CotId8FxiSUmxLCzkF+JZgZNPBXMEGE/+S/xQ3A5fSRI + LNjW/IiXhqWAOGMcb8KgtMef4j1FSvMRf4BDQTcmhml5NZhaig6yCHFuWNog+UCACwYqB2/uMjuG + kSyJTDDcKFvB+BzLW9i/l9sqszcDxufiVrHfDW2kTTJY2CQ23++Rly594RbxJa+yQ2ZzlaoNUoFx + Y/vDNvkxtwetFARyXpSRm7eHxVcwuGh3PA7K2dlw8I6Y7P6AZGaqdxR0xgFwQNz2ZHa0ToPr92eD + 8eGVd081AbBsEfwFScUsvmla0WkEfwwSTuOIHbJTGmSURvDNhXQtWVpOIHrCRhpCBsZb9rSjUv6s + I3YLoY0XGW9JFooTSvLlwlCwzE07lEomRhaMA+ouZUc4Faa8s5TZWRBxZczlEJCFMY1WOLY2Lp3J + ruBdpSLsuCSI2JkP8wL5G2GQqKOcgKB9HNLocqYBGlAUAJWFaEkEgfZomAJIDByD1UZ5iPAcXME6 + QY4dTUVZjNUQ4DXwFOYs4yu0mUOSpoRgtQHZOaRyFCNJPFJdyVpGGvCjAxgvYtMC96wgAHvAACMP + GA0VhGzMCf8tAAOaprIZw5fzPLVxCz+AkJ4z0gMmRIiP0zUDMC7tLH0TAUPkEBXaHljFkSHPXMZU + k0gYWk0CIpM0RyXQxy5SBEhFywqjCuEqMK51p9CfclZH4C4UmgWhGcuI6J2G7B3EwIS8xHKj6AjG + nvgYBBH/9HuD8RJFxH53mUwWjTHnbGNBnNzwNNKV9kSyPRmPSR4yO9NQw20uszVeIJznFbwK3axA + WCCcNdhujHK6Nj826SySaPG0rJl0OpxdTDu9oaFFKLkycaxhGaGZmJqRZt2NWY3F6yOOpGVW94YY + YEiQBKXB3yMiNZBCiHaCnqFaiC1AWW00QcSOd5FmKQHtl4Jb4YAQYd4mrAQ4hEuM6xuRui63xpsN + GRlr05IJMps84OIXWLDejYwtIT5F7PlNopl7JJCII0m08h1TfAS+O1UZ4iOkeymPLzR7mAkWaBLT + cMFmjBSkOlScKfmvNKQioCKpoQQsUQlDTAJD3YEq0Bc8DxoydAJ/SIo6Vemmov66kKCIQoFeguIS + qeTBonNQOgF6jDJEKIehoaybSXpKQh6j2BpNLJSBMbak+MqzIhkUNsAExtZiQJNMCjNmeWxL4Vhm + oWBjBvvsAu2KGRFeb4n41jtOtuh/kBsRGp7/MEY/AuU6IxZ/Qup2E+GyeUy3TkoiUgtEy9MsW9CT + rF2vxZMYNzmZBj9OTl6il+5Rh2jU8FyBr7H8VciXh2aBelVQ3hjxsk1+bNqFJ54uRbsc3i4h9sGy + Elg3EmJcM6Jl5VHnuB9MDoJOMOsP+5gOyqSZ7PfHVyZv1Szg8kiBvUv7j6ibDCLct4JYlfP6d/QG + YpP9JEOmM0nIdr80ZG5u9zS4OZ67ojJmI0JtZKIRYENC+4IoAHmnYJphyCXRQgh47PkcKhYZI72L + BxCCjN3B0yCLu0yDIiNUaNRmkUWyzATJS1uyhByW5zQoWWL0PiZaVIhB1aGhSSQ+gTgzddGQSFg0 + ZOHKEm3/hTAjQQBNJdMQDJ5M7xRaZwku4xEaiZZpImNAs+RmcADqj5FBnGVDcZDQUiplEYHMqMyY + fUlETFmUBWWziAoYfUzeNdMwbjBiv2+gmZVbfiLDTEkEd4PUzBIxA4kuaTDdmPHH4Gq2UqttzZox + oaDLuGKqqENmcSmT3shMJtvKrfosThWRTzay68hIsjaDQUcVY2DXxrSOBs0CiNKhBCMETYUhPuSC + qaXiLAh/DHJK6VnncNoZEWmExW6ZqPoSTFYXxcC/HA0f9seesPrCnrRuVV8uVJprZa9CPwcL1SoK + Wgfnxmiob/RjU9HLmpY82q6kPXcJQ9POlclkHdlWSU7iLjTlIjTyGysxoFxYwtjb+MoETYKigPZB + x2Q6iaeLWHjJjXoJ5Q5V6Q8ocBya9gwlBKkqQKhEyiRLgg6Jtoo4aBcOrbKC1dIsZrUcsSlBSrgl + grLEpqVvto8utVQwmeaHldBeWygmEwh5GkE3JAoEykxECdIXoiNZ2tQ1jXJzIjAgB0XAKj/aDMzI + Ah4vhhrwmA0WAqZUubEiZBZb73YSUnv5mlKXNUSjzrLZASQdQwL6jBpsKA6TXG30aRAXyG9Ge7ey + d8ZKpWAukjG5VSxs5yzlOQG0MOqyEZVDo9GHrLsyC6PuTlXMRC1lkTpjyx6PkscNqLhYyR3ymRya + 6lpVmHHHNbnfwMLDjFQylc6MJh4x6mlcRWBHFPAoURtaN8bNfaI9BwXfBhRdRhkLtfwOIg1LllCy + IzyRmOGEUZilqdhCYeRaBg5HhjxWyYJ5FhpYUsabZPaUMh4pj2VlI+fnjHEdGYNEbgw8LPZnRlGw + uRIzeMocOAlhsSiY26ShtYWYAqFVLphn40CUjR2m6dBI6qHtzx5tAlxrqGHQQsOGUuZPkrk8DyXk + OeZTyJAXB4/btKu4AbajZIbDYRcTlLzuEsPmLKSF+UjL/t2ITVCSWuwmvLxxidj8oY1cWksHdeI+ + hcKqEDD70o8urEHY3mZpmwyULkMuJkJfmnOVqWa+mNzAVelyU4EByX5HYWLvsJgpKIgqNBKCMQ7h + L0xSJQ/OzJz50g1tWbZ3ua+hM9bEKGyWhmCjIAtZgRG0QNG4DvthloKNO/zSEn/o2pKBhSBwTQTc + MsqGbAC0HRKSC6P2YeVKtxmw/ANTU7IEkYW8OkVoP1qDPlOMwLTFFUsj5qWhKWOlGrcSpLH8I7Pk + JSRc3a4tFtgito+8ZKEmsK1fRTahj+a8bOF8eDTwDuUTEvNH4XByOHHnvoucubMgqtSOOZfPhxvt + OGjcHVuO+2+Cxu6vyd1VnFfcXecLzF0hFEW+BObysaTn7wCSIV7pTrJo4XHnfY0MvqkvjyFGzNa3 + u1tPn71oRgojMISLybyjCX6HUz7ahYA26fWa/Xhqvpp4BkEf4Rm5fE/TdpWb8bsd3CXPFR6fk6nc + pj85wmbE6lSTqiHSIzyBq/dgAkmyoRJRLAtw6hwxHLm9kEvAoIP7lHzpVidiiE2dgnamssBOzzRW + tBJwwsizjDNy+5t9oI3WA81F4buO2YikpP2NWCgQ40lIwIVZ88InbQuNXZ8LOIgkQqGnpEDrSiVQ + iqTmVpmqFdL8RlBSlnfwdqNxFc5cQvCdbQ4kh6vs2oT7RUyRlP9kiXmPLx0SyeYrrkplXfhDs9+O + AgPEmxKI3Sjsb6DauOjEImEAwYZzzTZ7bX9ifKmxOckYfCSnYfAbEMgTuT7l5/OIsgmOXCkKfnCz + YEcWbZ+LEDZR4GU+IzKJBBJLYt9zowTAx6O/INgFmJZUIXvOc9e5QIGigIVOIXJ6Wls2mVk2mHJK + UIkcr0skXCI5YjtZkdoyJtFlvEPJlFhnYABworcJuCsLbVhTmgAd1JP5CgmxKiOIvyCSbMDv5GY4 + X1Wp2iNpRYgj/k0yIgea4AMCaKwxYhbxb9yaLxJeOklK2jseF+aP0pwsaJt4tyMRCBIP9vEIYJJI + NISJLM+PQval1ns40+Cz2jzOkJdqzSOnJMNzZMvZzWEaoaYLLH6+Ul7oAmxUQsaIzTN+Old2mdsE + v7inmAdmKfUSSz7k1bQdcP6rC5s6gpCN2EEGZDNJEtFOxVGC2DZDHn0ORi2y4ggP16ZDVJK0E+kj + IQ12WoTQ534Qf0ak+ghu2qoLXGUmqqLicyh+MLTgSAE5HNEhyqVgf3nK0VhTMHOXr45CbuYc96qv + Ng6XXUGcv9UCI2v2/5hO3gTdyfFZaLx/SBk9rXkmzrt6LFTtTggTclXpOR8u7gF+LofTycnx6ipz + DqlEz7kefJ0qL6Np//Bk2Gmo2FnozFoQTD/GBWqFzYDZ6mDcZavBrD8dlRutR//v/yRVRyogMXBc + bYvKBJ1xr7GzE7g1M4jGXbXB02Uw7ozf9cfRuD/bGIyO4a1+QiJA/Tv3981ofzO4u0UlBuPZvSh4 + PBwGUzDoMqA6cD3tRUvT3uAAfGlcvugf0uRMOzy9E76sdlKyuXJ2NCiDkqrRKhnD4/Zk1i+DTrfb + P551xl0uMzmZNnn3njMdV5iIOVPMS3xDl7uEBH6KbdE/ZzKBqwDXDScHYWnLPVqs2exbVR53xnNw + 02S3HvGMbyCv0Yv5A8dJk3za6Z6Fx5PhoHs2P97nJi94bvOaR7vQwqP5WtEKN7Kl9bLsJjm3h2kJ + 2f78Rm5yWKzW3Hhi1hwRHt498w1ccluvwjH/MgO+DtJ7g5KaHIwWHWSe1r43I7tW81FV+rI4DhZ3 + 5a3BCLZ0WQ6ICQxmC6vw8XxWM17m6z+aq/P7xw6eXOxPu/2QlIla3sail4ApdcF+9Y25DbtQ70bQ + tT/td35pPfrzeL88fnAtXC/u30+2Mnvs3F0OxoemFb6uM7dAqxIGhVXU0eV12tTao6YWbsuq7b+l + 1sb1U5Fm3I3PZiNW8UedXn/lqcEc4nY67O8bfPtjexW+0BhNRTg+67Ye1crfHHautc52Z5PuL8F/ + nUwgjuyfmWV+Sazi+Ci8BpKbxLjL4bleZRWi58osyYSXQvUiB6+sVw0Gra82SNZ/9MUX1rA16DkP + +XAfjgHVlcHLudNv2FqPjKZRN5YNadPhYoC5VEE4wfk1biB09knjIKGy+4ujlZRtCl6229C3a8sP + J2gDsmz/8Oxhq/+2Ozyh7fDIjdPCNAeRtdvVvhxPSr774aAzCKw3cDQbemBt1/zpYWs2PXGC6cl4 + MMMuMyBbNWKD12658TcsXluQdIF9zED8Nr5fz94watnrIe2J/nR/0pn2/pml+WIpKjPkzNcHw/7b + f4pY6vOLuGkyw2qyOXKwCndsuHzZAngioE774X7/aDDuhVz+uu+x6voVNpPyVkgR0P9BA/d0wIVM + 5nAaDeeI1r74ZhJ0p5Oy7PfgCEdkYnbUmeGmywhOuGs/rEXB1oy2em/Q7bBSE7zpnOGc18Tp6OD4 + lDZmh7fLOnQiWrKjQYmS48lscIB6lOVUsbr10n2qH0dWGLUX39g3LUIcuyyKs8fw/OTz1ECYL4HJ + tJ9jvqvmSr/bEXzcHg9xoz7kfxC2jf/BTS8YgxDOgAMNp+YfTnKG4ItfOb/Rwd4a7CzLFTn623A+ + 07ZqysULBonKcM4ryu7/jfKoM+33qv1LpOYO30iy28Fr1c87h31EgJpRIb59dOerUWdQ3VAhJsFX + nSiblsCdO1/gv6+4rzv0X31T1su+fg3bBtWoFzicDnqvX8NyAdNFLTVaSJdIB/PtmYzjTpftFgSI + HTFDUf2ch4iLhyL2O+5imA+nndMBgszMw74ETBN4w/4BsPQVfMSOCed33tACn7yJ+OXyXUP6y+Bh + 0Pj5n/8Mfn3/oLkOiWLjg8Hhqro+m9r4x08XtBEdn5RHd38lVPA1qW0SBbZ694O1P3LSiAYyS0Wa + F91Op5/oPNWdXtLv9vJ9LXQ3SWOxtk7VSUKgdnf7Q+pgMkUL3F9oedzlGqmDeD/4tcU8u3W/9bJf + Djut9VZ5Ntqf8Ifd7ceUhlvB9AS3x7d6rfs6j/N8HXzmqDM+7G+byuOT4XDdBWA6o7pUD1TmcDJF + iikzfSI21SOR84mJKWRrkUBBdOW0v9cZEiO5r6JMiDhVqSoSmcSpTNZbnf1yMjyZuSKInlHU/ssL + 1/fMlsAzqFm63iI+czqYnJRMO/EZT2dRlxNYr3omHtuMvvtPj21P9W8vLID87RQdPKf5fHw8aN3/ + tXVsfm4P9qcdHmt3OKA26dNmZ9wb0tZZ5zJbPfpKNJQ+3G/9GItY6OIxwo8KTSgVefj97lNb9Clx + n68H/SFhu7UN1x/z+cVkMppr5UVDM0Byf3RMAPer2jMIHoQ2+kXKu8H7e5P4bUfyknosZ53R8YcN + qd6MHZtwY2vF6X2R34+L4PFOiwZ5PDwpkzh+aVYeHLKpTF39sgVY9bKlrMTyF5JQhMx0Iv886xyW + D60oZiWx1yUX6vVnncHw9f7JWdkfDv98PHwoW7zuXlrsviRRm7p8Pu2HpoEA+RMzilqBbx/vtqo5 + MR/NBmkYGO3K8uuT2cmUMg46w7L//os77+89IJazsRG8/O7pd8Hdzuze4CC4uzPp9YeRKRv84WGA + Ju+Bn+Mw8hca7Hrw5ujsL5clYBdRIFwYCA+4tw+jRnVCdDNE6JL0R0YEt4qzuMilSAnwfIn+6Ehn + dfqzTH0MmVmgPqJA/KpbQn1irS7eszuD3vk7db6V+Z1qKn9s2nPZcVyS9FwwoHMpz3cHJCLjdrGR + 1OVGvoEjlVVUyKy+JUphP1+DQKzqv04soKx5WmG3fKfX2/Lbq70o9jTmLkg9TWUqmmGICG1uyIKg + nc874/7wW1KtiH7MVWWKcGc4ORyMv58OKbe6Fv+X6cM/yQNDc+nHjGSVkFOmjr/Zvlhvw+esUbn3 + 6yzCGyrGMtiFROoL4IrEeytgLojXXnJd1JcbyKINpVgzTlymPMvI5wRiOKfifBYk6KYTDHfMCX38 + auJkqxGGVdEZVsHcNMC5ikdiRb2B37/lOfXNIGsnXQutGI7ziMXfledel23M85vzm8B/xqTHjT1a + DyBs287P7dvCtxpbG0fiHFyungRmLR+CR8sDQ26o9ciI4B+KTzbrH08G0IU/tC3HxR2EF87Ql6Ro + ZOd394EgOdnj8iBBPfrTB8G0ItSqz169RFyEGESkucSYH9Vk3ksj6lFNzL06dgHh+JCtAmFYjjpD + iA6PiHN+AFJWhaZdNjavKH0tam0D9xLRviBMzjkt2jZg5mFbz1Up83xrF4Xf4abmgwmfN1ojEQVH + gx5R8eCckgeDt/1eyKZTe8ksBFom03Afe8bdZnlu9bggK/4Ex4cpK2ZB++ungY0iVAYsF40m476z + vwOKkGohitLurDOdBVZEOy9W8XXmAHauC0tduKvmsWtTl0OdP3DohZUu9bB1MYtvqFa5p8Bi1FjC + HF7I5kxIGqzCrcgmFfth6/vdPBdp/AL2ABxY9mCq9sLcxfOzOEdEiEisDGeT4zB8y6Thspi7mCp/ + MW/iNGclc5Gzr3VW4kyoF5D788n5lQnbIuI+eHHjmADCM4TMMY0rcLsurOWcR1TqJN7SC5CzMOyF + Y9rMPlG6sFtLe/tqxOE8ZrGSG1wUkeqKqJ3PshaVFSK8QdJKMf4qNplmkX5hdm1j503ZdYX9Cypf + Skg16LiCoAprzWVkjYsavqqwelF7VxZY8d+XeA7p4m5vALwrC68Mnoxkfr4Ae1n4LqB8XOQmhFlu + iMG5EtYerbDHXK2RS8iuXPgDuECjHNtQq36EXvu5cNwYVjFribhPhkP4GNWZ76jfG5zUbwJ9xZY0 + H+iJE/xvSOS9am1/Mu0tO13P9ie9s7m6r1/ztyaxeTZdLLkyluSst1h0lfjMxTu10q9fs59Pd9jv + TI1/DdMEeGWTCG1l18Np58yk37AfgA2xbLxv/liThswBQ8hRfZfDa89P16x3K0fDcSMRGfKx+RHA + Nvu7HcbryfFgPGBp3Y3nO/vl9zcmd5gFr49T9mR1bgqP7Zff35iMY92495qoBv3psOey+RS07acr + joq+ThelO6YyderIY52jj05+b3ac+NheEd7PbXj43Pp3PWVi0j4iTbfJc23aL49pGRNPdxVa3MBc + tf/vf/0/XAfhCkjHs5F17wcQxanRwfj4ZObdzn7uvCWq/wviUBhfKmK8Mw5yywqikeZbAUsQpC8S + ewxjGcaItYyrxgpxti7T5NGKFg8naf9vQ/k//vq3wfaXenr0bPskm4y+/eXdXzf6ev/H3e7z7+L9 + rKemmX54ya6609MVnd29K5VUKvj3QEhRpPeCLwNZCJHea1VLgRYDI86sk6icvX09gv7RGYxfd4Hg + 4Fe/pPY73V8O+RjwfvBHHIvnneAPg9HxZDrrjGeV+ydvivOLGOZ5mTJG6b0fiOO3Juu9gfRw2u92 + jkn97oT7nd5hvwanWwJYAa4OiRVmmDxie3iCrNPONOhV62lv0H8zwhl18DD4NWjxt93JyRSXXe4H + LSGyvLVuv+91aJz2szugs1nP7FHwciX36B7nKKWQ8fJ/gAP5suuuuXVO2UJc25XjgzYEZ8MRcufs + 5S8rGsiLwtWvjrW2nqKkObSuf39Ji8jkqaXv3PYufMrQFpsFdlyA6PvA1LdPn3AZ/KUSTx4/4+ST + DjwngyeIpILPz16Yz3xXi7/sfGu+0F8kX/xgkj8Eu28GZcnfXr403x6/3HUGMf7+/bb5fjI8hLvv + ONh9hu9Pv9/l70//z/8uy/6wN5ke4PPXuwakr6cEzMEJ0Q76+M3elvnKP+jD5uMdTm+aiNPm07f2 + 03g8OcXhb8Cnnzzcx7tPH/8NX7Zt89uTcW/CQ9vZavMX+vvsB/7wvWlo5//873H3qM+Ffnj28sVj + /voDgQ3cvvzeIHtGqvdhx4C5+3eDFsZJ4FcX1s43BjnAS/8QJ8No9G8vTXnz4z3KkfaGT//AXBHx + 4Owtu34en9L679jLI+juiDbjUiluqF79u3H/7/3+L8s1xN+byu4QUzhqKLyzVPglsU5TvFwur5bL + 7w7eriqdLpf+8eXT5XL42ADzj8T3G0D+sRlklG6CeLn418TUVpROGhrvj1cUFvFy6Z3O28GIVJql + wjuPsRh+ou/P550XGAFNrk6mZOW7UJEeJgHfT4dcd8MchG/gGHyDFVumWHVSx54Q85TDLWO/Li9F + B10tPyqzBYMnZXcxm0fvqZJF08pOlBLFyk4W6FhzL0z0LuhFrB6IIYrhk2l/ZGjDil5AQy/qRWR6 + dT812rqiixc/XNhFjbUs97BAqVf0Arp+0aTE5+CL6P/qxsEcLsLSahwt8I7mPpjRXNSHWtnHHCNq + 7oF51kU9JHJ1HyTPjMed4NkPwVJ+1Ythghd1k63spMYrmztgpnpR+6uXU53xrurg2ws7SFcPoOLi + K8jH44snWqUrm69Egubmty8xy4lejR8vYDQ3z3LIhYtodes1aWVFB99fjH29eq8ZEWhl61ZCumgA + q9E/J0k198FC10UjWN3BFlHUcyaApbeLVmcuV0/w377/7uWLrR+ePv3uxdfnIMoIez/NCe++jcpF + DS0Myu9OZt8dGE4Mzz6UwXEg5z0e9YmJd8bGUX4upxx0/Fd29a2pIU4cqMyF5cNGpvFno6xX5V67 + Jl7TRp8M6dufjwmESe9hk4j6Z3gYviPt7uGzTomLg8EuqY69zrQX8FAgtsz5ugIoc65C0g2yHw+H + bzpn5U5nPDg+gT8llz+ZVOXd2EhunheUrPzD6ti3J6N98zVGozBR9anCovC42SnNMM7c5LvWvy/7 + vX53MOoM61+Noohw6lM/L8F7o8o26aoRGwVYYzX2jPvBv91diyp7wT/YXGDyflq7F512hnfvrXPh + o9VljxaKdqen950bZh/fV1Wkgr7qPQB+B1r2yaz73cFB2Z8RoGEy78RofgXltDt/GflwMjkcmjDN + Xt/f6BwPop/Lv5BsSSLZw3S7X+o8//4x/mvPht91fpG9TG29OTh+/FSLb05+3D55KXZf8BmY6dAZ + ACobQjTt0+zcvXsvePiIsMjBNgKb9+xtv3sy6xPULrtWr28y765dFoy19V+DjnUvXzOTyXaWteD9 + vWhGZPbuXcam6ao36fL+iP7rpD89c37ud9f+6CHgwvV2GO8nAJezHrzHDCyM5e69B3Yi/1GHIbSj + WfuJas/XoGYeVPgzZql5oxMMcyuhalUmvZUljEnrnALWrNWaM1vdabZZ3blTWatCa5CaHu7fFeuZ + IpksXw/iKL/3gN2Bz8l0diqTG1B20JA/Z6O68/4LC5Q92H19NH19crwCpj/GRV5osaIaHipfVfHZ + 45hImqvI494dvOvT8tz494DpQ1Ai7f2kgn/foJbm7KNz3eKy5lFnMF3V4X5C/9etVQmP4TgdssMT + V1rZtiHpr02MHiLyFh011EtNmM2z9aCQjNjJcadLlP5+ECU1PLMjna1F9HRcgkLCZri6K4/CemdJ + sh6QOBik4sM649x+75MtP+tgeD+QvPIWlqOcX464hXsx5ueLXwJ7fjXgxTleEcGjACfE/HHQnYQH + RMTK5QWCqt4se+crPlzh2/z8qTzq933EqA17P3WD1/UG387vTULGdn98SJCadsqoW+L4/M5Xnf+/ + vXddb9tIFkV/x0/Ri5OMk1kEiPtFsTRbtuXEmdjRWEpmZq9vPm2QgEhEIMABQMtMJt+3HuL82Y9w + XuM8ynqSU9XdAAEQoAASkqxYWbMsEOhLdXXdurq7arEQ4ihKeaZCtmlJy6MAeTbKvvM7ow33RnWJ + p+Xj904FRc9PQVX2nNc7Lz+E5G+eG3oJWC1EUkYS22encejxEjJuQFMfCQvw5BF21Jte2CXXTkI+ + Z6YKRoKChcc61BS3COjJM6yHMQ8KjTV0y1rBvj7HGz+GMWRRpXy8SD3Fm77wSRFV6QvxCYXkaUL4 + KVmQxH7qBP4vLDIV1PjckE1RNshzMl5hKFF8ooAnJFqmCVphmE2psOe0K3rXRgFTMzBhAb/xPQrc + //w5yc9oMcf/4H/RPSe8hUJyEyKBdcvcwVvvgyEvha1hkVfHf8Wbz9l7vPJ8AlZeuhqgL6pS+K9L + L6HbT7w0qip8/zrJJo8ennfI8+VqSEBNB3hf/dsocP+cVWExuzz3OEwwrsUBqfbBP/Di2UhY87z9 + mYMRyCYeWJkudIZWihcmwFsYOgwIASZyvFyJNOmkA+tlvNnNPyUTEFQ4gzSAGvwdOwnNPkl0Gyvx + csmQKAaZYSrJ/AVWkCWC9wmzl+KgsBWDC5uW+PobhgBAEkfy1RXh2vOuSBBd0wSYBUzujTVMjAfN + 8uwPGHEEuiPYHY1CVpw0BEcGhrNUkRxPAE1IwRmLcV3MWGzI6wH2ZNsUDfML4mDMgss4mldHtCuG + SC2KZv50dhs4ou22RZJqaaKsd0CSroia1IQj7HovMkJpyY9AJIgbsBv5U99YsvV1R9HlJaxLc6Fb + 1y+w6XsAjeDBIifk6QQQoYaii5Iy5InoGPJxgHNYf+J3eahbkihJlOMcSsClzwp+ZNzN284/xx6G + M8QFN1RUFVGRiesjqB4GCcyRT+c41z1MzZQn4Z/r/dJscfbkGVOfSZ45EgQtmpYYoyGzOGYYo7B4 + rIrfdiD1dyCy3fp6K0nRbJBD9B9d+urr7DpbspxDD6tNMIoZZFqIZH4kgjfHqn6kchatDIb8owc2 + D61Ffe1s3L/8/p1hvlaDNKH+vtXCA8d9S83UhP3fubopTO7Oa4O6+y08VBcswvGy+oXjXsi3GRjM + c2EqWLwvFhFsvTDMzrl9SLJja0noLJJZtD7ktnHUrdU5OhqEuPYg3UYHLU7RretsvUQ+U3I80yho + Qh5pmVRf0APSazbMLjGzm8VnvLtno5myeVS7AAwp/FdX5OKCTg2ey6w5xYn3fA1l6zWXdUNsAIOj + 577b7sZMd3hkCdmoOzwEnXm3BRS9DN0VqOPk6vbg2QFJAM/tIolfkOgI1ksQkLcFknUA4vr0TUeI + cE/stiAyLHUoSTqqkY5Q/UQdtbfGdjS4TUeQcBNQZNdjbguuHej8h4UX3tr0cXdeR5CY5iQvnMVt + AabuAhbbjqVh5qlPsv0tyOa2Ua+VwnXmRYs6VEDrisad3QSOfRHa3FffMkTefhfQc/ySmnfcGt4C + DFtUNV+T2DoVsGLAo6Hk++h6/yu/3YZU/4nbTo3DYcuYPUf7LXTSxw3naIrJKUj2IAiqLiwwRHm4 + bQh1bVxcjJ14g37b1cyi8Ha6H5tVplsIN/fc3AC91F+YS3ZfkkfHyS7J9nzXvPmGYVv4bxAczcV7 + VhrZDaNbM45sUe+sXkcn5B3u4NyaeYQZfzoC9Sr2PPIqiByMRUy+uC3QNFHaDN2zHbKT0zOeA+eW + TCRzh4VJFF0RGlaQYMRuqmZvD2V213XcCyeZ4WRet4HuSeF57VjYazO4WItvnj7ngXM8emlwHRqC + XU/bWAeXarF1+mt17MiaIcuSq5map6pjT55MNNu2jUvb0zyN+yr8RJjEq0UaHQ5e4UGyLII75k8W + lnEgXE6cPB74tBxHHxMUsZ4xYnQW7QcqbDQyWXVrI8/DsW4iHMfjbo1gjY1mnMSfdGsGa2w0gxHI + f/GS7tgpVGxstCu21vUam+yOvWLNxma7Y7NYc6PZWTT3uqM0q1XfXFdk8kr1jXVHY16tvsHuCMyr + 1U7KBFDLz610xqML642fQcA0E2eh9a5oXTfeQKWFtrtjed16I7kW2u+O9HX7Bcwv/A9eUJaReHzE + cUU3Wo4Dj/ZPG3Xd+Ygmlvfni9FbU9cl2xZlTZI04/zbk+c/nr1+e3J2xnOijp4rhqwalmqLqqao + mqZJ+tfu5CKNry4cEO26qum6petq9nKCIZ6hUVmSZAlfBk56iH9jF758nTrTi8sovpjM/MC9cP3Y + m6SeCzU9J6Xxs75OL5fO4ddTdxEffv7rNy9P3/1Gf1ywLcyUv7x48cPbs5O35xfQ1W9fB6l7+OcN + RGRUcXd40FTpI8TDmoLvDBO2YVofISbWvHZ3NGFL8keIiQ2ToQVCfu6GEQuztCjaQ5EWVXvnU5ca + m8ba3ZHIRyo+Ng3NO+Saj1OOlKzaW8KGZj0UnqmY5XeHj4+UYcpLqDskj49TxVQWWXeIj49TeDSs + 4m4BLxpgQFEeihhpXIHeHWY+UoHS5FW4Q5L5OEVLo1PhDjHzcQiZQnDnfI97fLE+J3mBoe6EFh7p + LCZ4tW7Fz7+5RVAXxu1rwm+0qZK0+PA1T1V5QAz8xZzkXrAHlD8nwji8XJHNRuqidvrzKe0S/u6D + GehQYJsCNQ0NiBOkhwMY7wdDEl78Q7BkUZOE/3rxgrCtgX/S8CoeoOeaR6FnZ0KfLxNMtpnwM43C + mxfngqEqmnDyNs/3Ca2u030a8EyvXY9ez52pl4zGFwsnTkMvHnHv2QVPU8W7GVVAFWkqUeam4+42 + 7mrzAo+FZ48AUkDYJduViEJ8geHp+HVqvI4mYgOJl4qlDZAT1sL3tAFySJ7iGU68Kf3lV9WDgjWh + yTvTk2w30BM9YLw3QdFWOlBU116bSYr1zGjqHYgLJ5wGHlHrKUK296AI2tNDJgnZsoskUaIIvBR0 + kSzHu1FCVntQE0G/RAI7dFOZ+rwrNuX5PMPg1vOcTfNDnakK82pyA/OOA3jem3lpKx2Yt2uvzczL + eq4yr1zPvJq8B/PSnh408yotKWK5vzQf1weZb6aHTn1uI4flpihX1iyu9EYMy4ctyI0SMejKhiTn + Q9ldmG9k0CtBh13N3sc79lWp2XTwc0NvdO6oTnVkuWIolX0D9LAoyBtA7JrEdKUriRU6+DjIq0Bi + W15Vf4ZRMYpw/roQTLn4Xxk28ocd1lQEaerXWirIGYDmm6kNppz991tljBV4n43K46oklXc/0nzr + dffBXC+5SqPFLV4I4xe/au6F/VuGJX/109gPgnHkxC69NbZGUN3lKxBt/uRqJaxTuAiM45gEa8rH + gNjGC1tYZhLN55Hrpyv2rekSF6v0Gbuphfkc+KQ6WegVrD0qBI9raggmk8a+pzfnj97gNWiaHqKY + scWfYBiRCR7FimHq8wttI57OxGEjoDfEcFKzfD9Z9fWwAZbY9/iQSyjcLHSBubHjFRnUEGxd6Tmg + NCv7WRn+urZZdPCjbOVNOPVkaVJ4MxiyblszrsNpzMOSxdjzRJIPFG1wJMuzZyP8mA2jGGBlnbSg + rnEfZTNNXzAoTWsWtvfaT0H+C9EyxXJeABM0XyZXwGxJKkxAZuEdWgqRkiPmM6p7ShHbaDeJIvJc + t+KYo4RndaBh3AzV01TFtBXT092xJMmW57mWqfz5MornTnr488Kb5suWbEjzqbC+xUgaR7ieNE5J + yA3qNsTQ2FWDdkjsCX3nrDRJlnhhNiFP59AjDotVf0ooR5ETaIW8gVZI4IFMCKdkmWDxZcgS0kQk + vfa8dEjmgGAMQIC3eOcRHnjNxz6aqdnjYisWPNCf8R2jAQMfcVSwGgTIw0uIc5mWhu/GTkIz7gQr + MsFkCBHefwaamuBF9QTQwyIwuBheAVrKoipNMFpSuBKL+Fi0lAA03cegQeH9noWNRWTtQDUHR4py + W8JmTRcp6hxQcVRt8IRwfvjew5YEF2yiOY3uKYBtE/YlfRTVtsG8cmXbVlWUPo5qyo71gKXPnvhc + M1rirEAaIeuwujR6CW3saSHcgJOA7U+mgGBCY2aiJMK4anO0QqfxMoQhuSwmGIY3QZGFVyugeAYK + 0N3q3oRUj9jyXZLFSwPLEcuSUyjL3KpBQFjYOyjvkMHlEsVXskzwCiAV4B4skaarDNWI3xNYN80G + eAvFKURyo1L+UYh1FGLKgW6DEFNvX4glzlxI5n46SzAN1Hy+SjB37DJIPRev7FG5AqqqP+tJNQzL + sg1FUy5Bftme6RnG+Hchv7qics2Mc2gJpM6ZMydn2ACGMFxOZgKIJOBBDMD0DWuQFNoZMt4Co2RI + nr5Go8FJoWICy7xwSK49FlgljNIZD2pzHcXAE84YjIynH4EA2x1dLM/tGDHGsVUojxJ+6r+nYUUH + J2BUgj3lomp4DzUScUADwhTLY7SsseeF6B/yQMo5aerQ6QBJRnec4/c0LyeZ+PEEY3Q+CrOuwgzz + jinabQmzYDnxMX4OFE/YIl9gMghN+SCKrgR29zZGG5zrzTUZ9mWY6Rb8n6yqE0lx0DCb2Jcg2R6w + YOsHrZmJwQJugUkGpheNNJei83CCy54VOfkJbbAg8MIpWCHfY8cgwWb+ZEboCslhcbm8ySx5Su+U + EtopirzwKkHZduYsXZ+kzpWHiTYIDBKgvjch1w/qzhbeBIP7owcJhJizHh6gjA34OHbGPqL3dDkO + /AkwLxqACCV5BetwKtsulx6atBSrTxOOOR4dDYQS/Mhjo2HPj+Kti3gziaIeyCqIN/eWpFuSLjF/ + g3A9iwR6aCspkMrPQFKBA3yEOl6IrkPh2htTP3Nf9po9di+tsWJOJMdEe031DFN6yN6uPfF5Tl0z + IKqmHuFNgaSKCGtq7fdBoyT232O0OWiUirGsXer2wcDklwHzONOjePCe93VvYmtP1HznYJg0ECah + twLJ42I0dJ8FD3Xo7gPdASHoCoO3xy/PhOd5KkCU4SyCOEMKkA0jMSr8gMVBhsFaN4UBDOHbJFhS + lbC2CdF9+BLTj8OqFga86F2M3VQ3y6ed1yhkAi+Lg0pjDz5g4IPeviolU+XJYYA/cVaBUE9RTSfk + sPHLv/9NfuWZbRqKgBwNL/3plkbyEtDYf/2zVWPiYpnMvlzvpsbeFAgty69yQJ7+IavJ6j0d5kWr + TR6QXwv0TNMtHbCw7YPhIElZ0lZ8950TigTseWR3+OSFLv/wyhsXP/C4qYVMiVCGplUt9FPISXRQ + SD9bKIFZWzk0LFHtkG/ZvfCCIBkc/NeAdj8cYHwx+MMyHw3hrQ8mDUYyyn98H13DMwvYNvgnQIhP + 4ZY2eO1qPZqsCGr9ysofsHwEkiRlNQ9Y2Db6hrZxgIF6dYm+wNYOZMsWdV3WpbxdGL5lDCVZGqoY + 8FSiUFO8YhIEcwQ4/W247lEVJancoyaaRqlHS5SNco+2Qn+XepQtTO4nb/RoVHqkcaVKPWKDVqVH + 2y72qIqGXe5RUZWhYShDNDCrParVHk2W6KrQoyUqZqlHQzT1Yo+KaMiVHmVzCGQ1VCxzo0el0qOp + ZrOW9yiLWmkeYUxWcR4NQIJa7VEdWpIB87g5RrnSo6GJWnkeAQZFKfZoaqJa6lERTasyj7YxtKBX + XavMozxSqz2aliiV59HIqTfr0RZNs9ijkdFuYR6loWVaME5zo0epOkYlq56P0RRtqYRVSTSsYo+y + KJvlHlXAqG5LQ0NWqz0qVe7Q7YwQ8zFKFcqBHjSl0KOes09hjBpgVR9KtrTRY5U7NKXKjxrMrFrs + UTMyUmI9qjARZnUelaEJvaraBlYVvdojoKE8Rnhj2dUeixIAqEOuzKOsW0PDtoeSbmz0qFV6VHXR + 0qs9cvbLetRF1QR+yMeoZWhfY1VSh7JsDxV5g1aVqgRQLGiu1KOqZtPGe4QXehGriimqemWMKtCp + jlJuc4xVWsXqSqlHeCOXKEexy1JOAf6syByoMFRseWhZG5Qj21WsAnOVdQeM2izJVShS4g4ASar2 + aOtDKDNUZWujR6s6Rl00y7QK4l8rUQ68MIsyh4bg3tQdmmkMbUvf6LHKjzKMUS2PUcnkXoZVkNxq + oUdZF40qrcL4TFlD6tnocYNyNmQOCGO9JOVg3uRSjzVYNewhgDm0N2WOXNUdyoYEUKqSHLBoa0Ws + ShntFsYIugNsANPc4A65KskVKFKmHJh8S69g1SxKAFnb0FayYQ4NTRlqNfNY5Q5AvFyWOXKuKrIe + Ac1KGavyBq0Cd8A4bX1jjBLjjn+WTcTBAZiFYKbgQWGWVO8ck+oNDmgOy/VbzK4VRzQ0Pv3Ec4F8 + Vc7RWHNMjR29a44QnkUA/za3mQkzmrMjXuuDsiVze+NIH8ZNo6cFBxtrSfaJngY0SOHHHBMEFH4n + 2W+Mv4ZLGP7j52UChvQqO1goUJOdf2PpBeTSr0RQs1Nn7LBqfpCW/eInWAUMuy/QA288IjrbaRaS + AaF+mENu92to98u5967+uBxgzsMEp/lZuZrSrGMeOHNwVGy9udK6CzxKeVE+kMdX8KzhTmPOxwgL + GIBCv6UxFlvvbYz1Lou+yQyG2vBlXiLBDfKsIVClQqDyVgJtIseX0TU9Xl07TRXEZ2VvJhTe+Dsv + 8dZZQPm3Nj3RirvPlVzGjVrBVObgfZbSQxm8PvtB/xWES/8DptxjPxI8Vz1eO1GfoQRFFxWeM8fM + d+KzUfaKl0hRKJZavrig79bexjSufo8Ljud0tlE7izKfzm4oxmKH31iMhzy/sdzaN9C6MA0AvS4L + z3HuVqRoyGaV1twyqWu/C9v8SbKQwNwB2jzpWmXStWzSKVFRjUN4I3R3KKFuIJh0loL6oER9RVK9 + ASSBJqxNaqkc08ewpDMCu2csfdEswDjH8iQ2ULbMD/lzwXXKj5YXj+/v7Dzd4YA6T2BDb1q0sA+O + WXGC6bGz/CDFXllzGOCUZQJed5G9Ez5MEvcSw0RlQVX/QxCydhMiCJuhVssNXFxkSXfy6yGlOaiW + 5uIpdwueRylYN1mHB9srU8IYHNl6Nnn5dYz8VzG9KA7lHcsI1mokPHvYbgN5vlxlnbUbhW43j6Jj + 32+9ZRoDGjv1rxi99U/TxHXqXJbaTyFLytxmAhmZH1UbeBml5I9AaVGQ+gty7s0XmLq+3OI21hBS + VnXQ1K8bpXyjoPyFiqCrQuqCrajJe2kDC4vonhQmoU3bWRz4TZl6TaWQMI4CULCcHTMJTn799fjt + 8ff/ODu/eHl8fvzbb9nFk3F8K91T1SKc04xcBRhO371+cXI3EDBaLvT97vj89dtvyp23IN3vvSka + rG1oN6BFW8oeWpYHAKfY6sK868pkyzdBYDd1/8QUPZuNbWN9HoF5OWdRztvJ2zGtsVb3O0geMJM8 + INR2ModmZutN6L2hyd3adc1yyvXWNVqS7YfN89V1ELjRnB7laSVyWdkcuD8Vk3uyHHY8tR7bPceb + KqAsuNGQE+AQD4wSXLTR80HzCB4wlWi4LkJ47jx2QpJeVUEQxPIQKoHlMzcM39dkbXlUo7yo7o/W + feRbo80lijuibJMUNzZfVzY2meOI5dn54fKYj2hwYOvrt2BADA50e/2Ca/XBgWKsX6KqHRzIzN8F + NEC5kzEn8/Bx19jGJ0oEwwGj2tInRpyarhiyqcF/ljkcHAdBoczxPFrCHAN0qokOa9u2DN1UFAMA + e5NvTnIfnWJb2BFz4eXw8EfqpKNPmXuO/jjhGRIBGE3HF9xFhw4KGV0g6BTk7atQwtZVVd25D5n6 + l6udmKVOdFuU5L06USVpsxOr1IlpY6bJnXvQNnuQZbs8DEnUrb2GIW12okgVXBlAE7v3wCmz3EMZ + UYYiWsp+s4Eu9UonldnQFRE5bdcelBpEqWXKtXFL3DB278TUTFEv9qFIsqRWUWVK+/SxSbjYiV5m + QVXUpf3IqsqC0Ilcmg/FBLIy9hmJZliiUkVXhc9lmHSt32GUJ12xZZj0PWZD35gNRdZKPeCZAH0f + RNmbM64o5Rm3ZNGS9+nDUuWNydCk0kBUFTfF9yIrxdgYiVYmK1UDBtlrJJvqAzspy13V2EeWgG4V + jQ1kVdSgCkDsNw5lk7Q0RSmTFm4F90lXuiyVOoDZMPZi8to+yoPA/ew9pZW6MeN6RVpBJ5rZr6bF + TkpkpUi2qFp7cWFNH0pZmmiGaNq9jsKQyhIR2Fzfi3LNuj7Uch/2vmpQruukPB2avm8nddiqzoe2 + l/6QN5ncKOtARUUe7Negxk4q/AH2116MrlobfZhlswQJC0yTPQZiKRvIMuUK8ZqitLvBYEnyZgcV + yrVEdb+VR82UmxWTQYN1gbbfbNT0YVW5Q+9XHJoVcWjKe/Mfnu2pdFLRsxIuofYyQxWt2ocllTWU + bYiSuftcqJJoqYpmG4amAj50pdJb2ZZTLA1WU7v3Zm7woaVU5kUTtT062LThbMkoz4kqqvruE7JJ + WbZslO0eWDnIu49A2+yggiLNFrXdxYiqVlazCrCLVhUj8u7cp5rVEUAHZRQpimjvIT82ZBTwYtk0 + lEDx7W6IKGpNB2UUSaao7G7cqgpguGKkw3pQqiJJk3enVLVmEGVmxgPie1gHsMautC8rJXUk64a4 + O46UDbtZrixeZdCn+7SPB/IrHZSnADvQd5etyiYjKGUFgbbPHsJI2RCn0IFS7kAV96Ahua6D8hzL + uCTefQS6stmBVR6BvI/G2VynQgclcSpLwIu7ty9VMKRWvWqwGADLsr8BqFWPGj2gvI+TaKP9slXJ + jlzv4xPc7MAsDwDPre8u6KoqGTsoTzG94LAHF2x2UJGk9M7G7h0YwAUVpw32YZT7wLsvexDq5iDU + srSj17J276C6jlNBoRn0gDS9UhcndKenvLMkMWvnNcbCDZ2A7Z1D9ekyzQ9bywbm8JB0Wy+Y9d/T + bc2DAZ77xrsRIFeHgxcs/hy8fsFiv5KX3sJLJjO8IPfOc4KCg12QJUGRz2XrAG/D6/8pKQcSQ0cJ + Qs5+GwCGfINtHyDf/XD+LckgPWUhfJMh+f77FzuDm62mb0KormqaTg/418FqbsL6TRS4cyckZ85k + ljSAZ57LGgVPawSP8+qN4NEdxSbwrE3wTn2afdwJXTxRXw+eda5ICJ7aDJ7WDjxT1TC/TB147NZM + Fby/ee54mczImyieIhK9yZJdDCevw4lYBy/8zz6XTQqv+p+SXA+v1ABvDXValg3UadfBzO5sVmH+ + 8flZHWAYQOJc1uk8242A8X25mxBp2RY6xhqAqpnnt941OUtjz0sJnlJ24smsAUjrXDYQSKUZSL6v + dwOQmmRKViPm6ojxOJ4ukxw+wj+QekCVDNAt2FRakaUm2SYAWkuWCr1cWQX0JZAJSkfy3Amv6sFT + QebYFLxmKmwJnqxoVr3Mwds0KoKndAAP5lgWJPVcUTKZUw+eYtdLRGg58CazGhDN9iB+J56KnKtv + AtBspsOW+NNkyZCbgNM3gXvj/7KcRQ1w6Xxe1Wa4pAZdsildNLDjDUOu0338Vl8Vthex5/opOVv6 + SbKpnSmIspUJQL0JxCYOrptbjD1qSFY9iEzlVUAECT2lgfhfRPFiU0pTIFHpMTljNQFpy22FNINR + aZjiOhj/hoEFyCvoKSJ/BDhvAFJrnGy1Ya4bMdkFyhu5pAWAjdRY4RLVQN1cJ635DfMdKBGFINV4 + 6hZl0mAoVsDDbHSGVCejJWUka5vgbbdsFIzoJGvc8FIaZbTdTsSA2gbkKfXQKTUi5qXjXzsFe6Ye + QEXP8Kc00p/anv5sCfVcLSdrI6kGhzfQnwb/O5dVKqWtJtvQtFtzsS3Zhm4b7eF7EV17IQ0HxN/V + LwJyUNkiwGheBLQ2Cyms9PJyW1jrzMIyYIrUCBg/YnEDGerAHw32NUBUp01uZBKtoE3UPRdPAJ6p + NfCwxs3/jiJGo9a+mvFwE/ZsuRV4uqnU26s4nzUS8IcFrPhnnj8HHKISEetXJRROWIOyVRSaq/Vw + 2g2TvEl9FNRGTNaB2kbfFYBsRibfiWojbziUtfquHsoWlkMRSLMRyPama3cgbxSKLQCUm9wjVZIE + xddgewFwyg6mP4CnFCyvBkpsLQd1RbaVBuNap8EFqhA+92IvHHvxtA46XZAlrpPVRuR1gU41t0BX + g782fAJAZihsBjI7vnTTDCuWrTVDuIvI1gsiewt4rQwbXQEt18Adeq3I7uixYfDmMrwRXqsduKpq + N1k4Og12s4sdpgvKzWZOW4RieCBDql3MGzR0VBXC7Q5EAM8ASzvj6EYPndmeZ2AZAAKx1pY1atfy + 7a0wA5f1fKqVzlZYFZOmpTUYOwYNMtLSAgOg7EzmNNsQLacXhbUhyfVA1RHgjSaOQamPLVPkZpy1 + 0ycGrLYMqdbdYNSuQJtdIQZdfFIxo+zrVtdBzUmNNKfUSMEb9ZxBHYTWdlLjx9ta2QmmpIIqrl2c + mLVepBvsBJO6kujqTmlckfCzcW3Y1tR1o971D/DJO5qEpiBnsqXZ+8/P1900x6apGjRwXy2AdXLl + pr0oBqDKp1ltXj615A7TUowGq9qs9TFkSi51wsBbNcCncYfhFpu6welfR4YWjKVhjWzW2gvPnXgS + YE4Ejs4GKK1MwzWvjluKQEsBXm6c511WeSa1ELTtk2yq9S7NGlaxNEVqcKib9euS1hrOpJuNxvbd + PJjDVoikYNYK6wYwWzHMGsBGsW2prcUOAtk427uuRAtANk+43XqHmQPZAZOoVTBI/jEs7f2JQ954 + cYx5kr5fhTUbZxWIm9F6mxBvW1a1gk6WG5b2NWTZCbRmG6IVXPwobyvpCGupJiVt0VCwO3gcLEFS + MkuiUTiqrRx0oJ/x+EAt7qxaP3H35ZRFvcY3MLhqqZUrxe0gr3WO1UN+s24sQtroFFNau48piA3G + bT2IW3mmAF2zfdvkDasiz7SanMUAWY1KvNG8BejszHpslJAtiVLRTKkBOpvGiN4gyii49JrPEWAi + WDDAs4VeI3jtDo0YqqorDWt6eyTXgHcj8mxBNrLNgMYFvdrqhBUsXEzdrOcLu3Zqb/Tf2HRqre1s + oTZYO1XobMk0a3EnS7W7FDd5G2SpsJHSjDuzFe5gLWpbShN0NTPbqEkQLIPzg9YMVqtlAHpAbLP+ + OIhU69K8UdQBeOjXzM+DNIi61lYXLqZkq9aLhCDWzGsLqwuBzLeiGm1XVW8l8QyliS0oArszLR5E + y1zDW9iinUhB6KxaidcAXWc1XAR3i4Bu5R3h4NZZNQ3gvnv+Ij8fyTNt3ARjs2XTQJUbMNqwjK1z + FDfAuI2XlYxZtmzxtATLVAB1Dby8k9mC4Ok3Lun5JY82vGwrEoibWsyxcOndbVZZFpTM0dp8fkpp + tUNhYpiWBhTWn7y4UYe0OnnRdDhuE4OmrGuAwVr+UGpp7+S9F0+iuG6BjCcLpeycptF4asU0W6Gu + K2AttiPaAag0eDZrALSazpF249oCWM1HR5VWhoEpG6rdoN6U2iXmjdpDWa80leYTj62seVM20T6o + Pa+u1G7KtvDLIIAKd6fLjcf22q3QwTSQt5BdjUTprt4UKmCsG4iQXVK6CVxF0WWz/ky9Uuv8f+74 + sUtOnRQ6u2o0Z5T1DsCWQ66tTEJTURW1QUYrtVb+zWpEoWa+doOIllpNOBCuXq988+O3agcRrfID + uFxEN0PXEnm2qltywwFStnOithEyanb+ljmMGs+Nyq3taFM1ZLveEOQJJ6qg3WhHUyBltWD6NQDZ + sLxsBLLh/HcdkDcZCBUYG5mjCyJtXGY2UKBsbsLYzcPKIDaz1UnzadxW9r+JR8Kt+q3j7B5KGdrt + i3YGXeaPab52ojTs5lWhMwzVUmo3Q2Vus6pdhA0FL3dmaY1HSeUG8Gocrd1h3LYb2hpAra1RbWqm + qtev4Xlmp514RjHOZWX7pY52dzoAPh2WS7XnBOrh66iic3DN7ODA3mKIgdw05XUobbdrVgG1efZb + 754wUOuPYzdht41Yz4FsPo4tt7O7GYANh+1rAdzucq2At+V+WdubKd1B3EGit4C4JT/pkq1bSgM/ + qVINtI2+fwqZKmXSqJF1lFbOG1M3JbNBMyrcybkvpyvU+VlRjf8cDr71HJemLuBJPku5C2BANIAu + Syf62j0gT5vinz99UsqvdA/JGS6ihR8CmDxBRYc0DT/wilkWp43kKzfnvtnM4+TG0cLFrM7Zg0Az + 4cReCpTOgtKzJCyXfgDUcTgYy0CczuXl2HJNzXVV25XGEggozQHbzjPNmkpCulp4667q0+RkXzEN + iz+dYrr4psxO2Zsc0PxNkr/hyTt4jHAhmTs00VaL1DfHQdAmqZETx9G1QFFHfwuJ/4vH+mnKcrSR + Vn49Zj5HJPAT5L3sL7RKs6JQZGJY9XVak2WQtcLLYoZsmvuDU3TgbxSgEd/Z/PCZ8YHYPhwOBLn8 + msb+BpINqrHu86ZS70NaxNWzUeCvM9N06duo7RpFz9auny9Xt9R1pkG2dv8tLDtvqf/cSN0KAEby + 3gTg2WgZbKQAr+SneVIjON57cUoz6oBgjIIAxT8msmmbn2m5wOjqiZcIKLiYBExqJQH+aCFEKLjp + OHJX1RRH+C5ngq0pnPD7Rgoot/j9syxzcKHKKHW7tLB7ze3Kcfd2Sxnf587Uu0D5xFIkoWx1winU + msaeF5Zyxu/U2RsH1Bz8P9ACMOTu7XwOJkin2mTmu64XlhoBmVHbRjH71d4UwzMG3wfF5IbeAyGO + HxdgGrkoJqJ9qUOSHgp1KI/y5KOQJ48Uc2PNeq/GHVDKqldCQVNsD0qRtf0phZuMv2tqKTmWnPni + a6CaT45W9EdaaVOz7OP71HSPbD0YW/beKGSri/XTEyv7E8ynIFYqO1ufHJloxoMiE/2eyKS8ifyp + aR9FeTgrn/uikMIW/m2TR+y5vRIH+n33kSE9UEfmoL4LEsEjLPdCIqVzMp+aDFEfjvcET2Q9WrC7 + 9dWjaaI+qIUxHjW8F6L5ZjmdskBrD0LvvIyuw7UPfz/V87DUzn0RyKOX7VGYtK35yS+HHxSZSPe2 + HKZ3ED41E1bpwVV/RyasdG/WSPHuzCdHINLD8NLza1nKvYiOT9uT9kBECL9beC8UUrnAeHcW60dG + Kd2Qf5+Uot4TpTyeTyoZr+bD0T7KPVFM9bL5p0YjygOiEfmeaKQULuHTI5CHY6BI90QgWYCUO6CN + j8o70scB2LvyjtwnfVTj1HxqMkQ2zUcZ8uhBq5MgD+QYI8buui8HSSlvEl/8ine1oOlB35Q3+PZT + OQ9J3WCwt3shmE/b5ao+EIki35+b5EFu6hVv+u27q2c+nEOuPPDmo2u+NdC9HVDr4YjAnUgS6dFq + bQfsI2U8Os3uhkB6uGzzeyeQx72aEsU8kNPQ90kxNXHaPzkyeSAhTHhehXsSLI/HW5UHdLyVpwl5 + XOG0Bro/M+XBKB0MofW4wrk7yngou74sL9Ojm7VlZz0ugbudJr63o8/2/Z1s/bSvd/Zwc+KOCES+ + L93ySftItAey4kUCuS8H/FvPc2fOPF/GPLQIav3t2exNKnd2C8u+P1v1QYbE6i+Cp2o8EIli3Z9N + 8kmrHPWBHHbmCZgf3fK7dtYfxTwgkfJ4SuBeKKSSG/6RXG6oeZcRxj8un7z9cHzyQCDSvV3JutvT + aR9Z8DRN0famkruLYmPe362sx8ji8N/nZg9+2DsTKvdJLZ/4PU9LfiBLn3ukkceDA0AnD+jgwH3S + ymNkxnVDn9t3lLIA/mJqLniEJ2yMPvGEY0+ePMmeC410zLlYrjFLA8cVWOkLH6YhStILx73IMtnR + 7zCueOnxN8vQTw8H/HCa8DpMfNeLR2dpNLlKRn9dRmlWcBF7Y8zYKH2QDoqfR3PPjb3JBeZY/Ldp + WPB5O3Sul1yl0eIWweNg1ED5b1kx5OonQFYwjpzYpWNYA1+bzzIjgRYpKk+SFDgi9ZI/ET8kP569 + rM1RCWQhsByd69Rw/MXh4BIaTYSFF19G8dwJJ16WIDN0ahq4uIDX/tRJ/RzOSk/rLprb/qyYRhFK + NqdQpB8LefwQEvx5OFh5ThwAx/+D/i3mAmxV/V9LJ0492sJfs8cb8/nB2Fk2yXrssnSSKD3WCS9b + ZPwj1Ra8MN2YqCyb6HrgOMmU4UuipEOewOzrJAoERcDshoIT+NNQiDF5avGruvWrtvWrvvWrsfGV + U1+KJF8Vkvguo7otihIqb9QcHP0xHCeLr0FIznIRW1eQUIA4KJgct3MNrXMNvXMNo3MNk9V4UtQd + +IA45SjfK+1jrU4bB87kakCufTedHQ4U/YvB0TvvvRcuPabJcqWEtXkpGUpttlQczVqVyZI61GRr + 3UxJz+7aqKoPVV3uuVHDGuqS2XOjtjY0jJ4hVWx9KBt2XaNtDdW2tPDSfw/KNnT7IQZJLJye6AUV + D6FF4Q6au615J//wvcAlX4Lt8sVX/RDBXaDjk2qu77k/OT3rZ6Y1sW9xqouG3m+Lhmj3LJ110VL7 + htG6E2F/Ojoh73Dh0NP026LWMyZUIACt3yYVW5R7pgBVE/VbgPJOaODk+evzniw/bajrPXOrIg9t + U+m5TXNoan1TgDSUlJ7HrqlDU6olq9uggZfHPVGBPdTrKXevGbONnqlAtYZS4RhEP23eBmXhIuVO + qOCtl5LTOLr0054oQR0afesDoC61d3kAfGb0vGAFudU/nPrQkmqp6/YogRy7Py+T1OtpPYgkYd4C + SfQ+fUASet/T96BJ4jT2hHPnQ78CQhtqvdtNylBS+27TGtpq3zNnD7V6sb57m6o8NK3aBdgtCogv + MwnRk7cAiEI1ehYRQBSq3f8EWmbfYudBE8XJ6Rn58m0UCt8cH59+RbwPIjn74eTRs7APjA/Vs0Bp + gdJBP/Ovilrv89/3es0Q+9720MW+DQajwVHR9/x/E0cJvYoRzXvaVwKTTJZ7njFVGWp9rwE1aWhb + fa9VYf2r9dym1uhZ6ZsWXjjJjLwKomtyGUdzIIr3XpL64bSn3QXAjdozXQigh/ueQ0GWhkq9rb5P + o8rQsPpu9B6I4oeFF9MTLEl/XiilbytSG8rKLXi2+qZeBX0kfXu2YKFp1sJ5y4Txyg+dcNKftND6 + ttkFS+55+1h+3I8ukgENTjJz4p7sCE3seyVoimrPUgFWEj2vAh8iCbyKPY/kdNDX6aSh3reHHj0W + fbOsrIOx0Lej1BpqUt+6Rhnqd+OALFND31JBEa2eOU4T+94L1cS+z5XBivUuVE3fpPA8iq7IT06w + 9PomA9ns/TCYooh6301aot1zk6rcuzvkTkgBHdEvvXFPGxICmPiW3LfqVQ1Yi/SNXF0ZSn3vUD1E + IyGLIsRvwr303ntBtJh7YUpOPizEnryP/R9sAZuh9/MiQ0vredmnD2WzZ9sWTzLfkduJXaUFMvBC + 10+XfSkJc2j07Za1QEL03qTdt96xh1bf26B3IiMw1IEfTofkGy/0YiAJJiuO3bkfij2KCW1o9L4D + MNR73wIAi71nwjCAp3tuUh+a9SK3d+JA63EWBa4X/89//9+EnPxr6aerfghCN4dW31LeNIaS0jOV + 2eZQU3teW9ggLfres70TaXEeodY4ThIv7ckZDagw+3bSywowndz35SVdHZp233e3dH2oyD0bJtsp + 4Vl+ARsfsamjwg1sLND/HdDC7VUGzRO8S0w7Lw0KPz+bRAENHUQhhB9ZCYoJQZaE99cMK4KzTCOB + XtidYP+NFXiBT67Ws1EJlzfcUd0iIGpvqJIkXQUeB+eAIIVuXFq98aYnI4fT2HvvR8uE8LvN5Nk4 + PiJomQKpycpIzUNEdG76xTKOEUd1LUsqbVndreW38Ka+WWOkSjs3mwGMF8Xr8LAPtE1tauU2n61j + NWTXbW+8bVtHPSR/EoQwWouRcRS7hTvnqHuiIFk44eHA2JRyg6MTJw4B2mQdP2At2DiszzjEdXrs + EsQQ3t6OYsH158I0dgDot5FIY3+ETrBK0rzB2gaq2KTxVTrVUMzONfSuNeghiG7jMNY1dsXjMcyq + M/XyqekEgCzKio1xILpCLomWYe1W0ZZ36hEP2cm7VMRLWma54q64pux7PI26oqraf9vJkUx1Nxyb + +k49Io6lniZnVxyfLseBn8zIy860TMNxSlQ+dwMe5O9OFc1dKwp7VFjbkbsK/JvlPY+5UCPu1+uZ + OxX41HPRrUZn8S13F9/yPkrlXgS+ggEgdpK/ijq0dxP5uG9h7yS6ebiOnaqyoBz3KPdlc2jupupk + a2jqxk41jaGtarvU7CySaufmUejfptB/UnAe5K6DLEJPIY5dKdoUNHpBGyUhhibLynz2J1IVJAnG + BHvjB4EfheRLT5yKhCuCr0gU093sBP2R/FsWlOMrkZxFS1jzHpBXziQ987IAl8VIWPyJP+wdY+7Z + fwjCH07enJ7/493J2ekPb89O/iAIR7u2VhtVDVbX/uRqxWJfLSIfqHbm0ckhGNTNZ3HfLmDEgIeU + r8JahGDjAePIMa/II7A9aeX2oU6FTbdNhyBeaycRlLn0A1g7sx+HAz6EosuC/+7psewOudEfcpPH + tC5o15O3ztzLyG8d8ap5jY6uGmoZoejZqSLliYSkNAHLPi3MvGC3+qcY2XKnmulqAeyceEEwGi9X + X+3URrTAo9HVqkV3Z2sfxvYZ/6xebha/f/bMIbPYuzwcjBIWT5HzacamdJbi0diZAk9EghO6sXct + uIIs2ZamWQMShZMA+B7kBsiKMD2Hmlenr7/8CoRFksZROD16zuqCIYt1yctnI/7h2cgpglryJG+R + +Dki16OQQK0ZmZLpoT08WCiVQknt1ZyhDRXdFrumUW6EziidQtqrLdxNJYT009jb+sjFbfd0Grp7 + pNe7pldTA3u6P3pVpOJdyr2aer5c/Q6p9cqPr2ZRHAq/OBNQbSugVkuyDbUNtf6F1yX/m9W9HWqV + 1B6plYZj6JG6JFhXyf1JV8kW1Z7I9XcqXT9yepWVkWIivXYL0t7cnjpU9f6kqyJpQ9lQe1PhsiVq + HdO3PIrX3ze59mkMILmq8iO5PtqufP4Ubrv2Ra4f91pLNh/XWo/0+rjW+r1Q63yZXAleEIWCpcqG + obeh0jdQh5xAndshT1ntkTw1PP2r9EaemqIODUXptU3ZkEW9p+H+TmXq751KdcMaKkAGvZKpYQ8t + Q+uRTEGW9uTyeCTTB0mm8lCV7KEkqT0SKtinqjWUFb0/QgX7wbIeCfXTJlRbHsq9Eqo+1MGYkBWr + R0K1HiXqp06osm4OJaM/6acpeIYLzAm1P3MCYBTtR4n6aROqaSlDybJ7JFRrKNn6ULGNHgnVEO2e + XAePhPogCdWQ9aGkmT2SKcbCNoeq1aeFaohyT07/RzK9MzJV+vRLoTnZo9ZXpaFm9+yXMg3RfPT1 + f8pUircgTKk/HxKSqY4Bn83+BLRsasVo949k+umRqalIQ7tHMxLJ1DBVjD7dI5mqom4/kuknTKaq + CSseuUfTVEU/lwak2qc0VUTjcTPqUyZTHbM8qH3apvLQNJWhYfe4GWXKovHokfqUyVTW8JprfydP + YOmEB/uGhtKfj0s2JVF/9PB/0mSq2EPT7lOaYl4fIFWzRzI1bFF7XEJ9ymSqgNJXjB69+0Cmuq0M + cXXeH5laj9L00yZTDVblutzfFjySqYUSVevvqIBsmI8r/U+bTFULllA9nr3TVHWoSjq02aNDyjAe + j51+2mRq2UPN7PHUqYouU2No93mYz9BF8wHq/Gd5lN5nm0F6MXjuviFsdgk64/KQOxcTB6jXdfiI + WkSdyYL1kBe85u8y7MztB5jJw0FLXwy6xIwpR/HeNUpN1ohNO+dTukc7bBR/Wvm7RZ4pxsY+esLi + 5k5W1Yae3V0kmBzJdFxrXu8scUu1KQf1I3SEj6oZFrpts6F9NfWWaeh2l+txGm5pGrop98dpuJ1p + kLstMh+n4Zamods+1OM03NI0mI/T8DFMg/E4DR/DNHTbznmchluahm6nvB+n4ZamoZsH6XEabmka + HlfRH8U0PK6iP4ppeFxFfwzTID2uortPw03bG1i0sDGAqQPI3Imnfiik0UIQAnj2Bkd/+ge6jjGt + SzrzyDsvWXi4A+fxRA79BPuv3Smho9268fEkm998z4OOG7c9Wm96bNv1aNjooP3utfNQH9n+5D3M + csE9Xyy1JgJSrLFOENO+0uvwMupU4WWphzpn/77e/nouznPSvfMWUZwWmaVYoQZoqJynLyPd6v1V + Jhg3mBQ6Dzwn8Tr2nicf2UFA3go2eE62rthQesGG+XFhQ+iKBbUPLMjSSLYeMha0XmhB5rSg1WHh + 2ZbMucUEursomSYd02Jv/dRJUkLFc/IpqZfjSbp0AnJyevZJKJiO/CCLsk1TKe7CRkoPbKT3K0wK + meiTp+SN56WYvrajTLnV4pI1kqjsUO5adjzhNfCIDjQdRYEjzP38M3wFrvcXKXG9Sy/OTrxc+6Eb + XYu8wosovPSny9jBZDrkkGz7+u9/k19/+5rC+96JycKZeueYyGdrrezlKS/9dV49SqDm06esQf+S + fAlvRLCYp8Ca/3F4SKSv8MOvFD2FvqAUbeO3JzeMJu85AMTNUUyKi2Uy+zJr66uvuzbw2uVNPF2j + ++lXX7MEaQzXRzvPJuYWa9AGhDxz8Jxh3ho8Cwsn9AKWt41UX9AsnGuRzw5BrQsdDhzXfR0mabzE + YfHv8E7w85fCZRTPqwWPBAGhIYT+QyFm2imDbJnAoLL88OzTgNWA4oXBlcpNgigBRbNMs9Hy8uxN + VoX/Yn8EwZ/Av1EYrPI3LNmoMPNd11uXm0WgA4VxsPTyV4n/i8dXklXMCBSWjWHnMAFUmL80gwmB + IBQSWg/nEj8XxjBineY4YHN9E0IcmtuqiIvs4Okf8rypQTSNCP4jwAL5yksTgR9GZS/ZKDE3/ICk + foon3ngKONSF9IEeMN2AC2wJOm90riuTPXf8sB7iCMRiAVONQ5vGTIQXx7aocMkvXhyVD3zV0ScW + ODp2XbJ+R9KIXDvpZBb4CRgVi25Yx0O8UZgQtJzoPxcXcXRdBLRiXuGwA0GQlTKBFNketOgE2JP+ + gSlJYw+gqx8PK1RsCYmNNVBq7uICh0dpbaNKaaSbP5sGUPKxJHMnCMpD2sqJfFT574UfBLvzWj1u + luO5T2e7ylB3NMY4QvPbbRxzd5HywgknXnDDcEpsicxHdcGIKoPMfgA2ZQ8lwvNTT+BMSTFANt4I + 65yfyNtkEoGCuYyjOTmN/bmfEIG8YYIlzx2Zv8lfCJVVxywNYO6EBWpMZrWRzVdAIrSHn6CNiPeG + PjxgPv+9d5auggyLrJyAB04jbDxdJAejUQClRPZJTL3JjL6g//zke9fiYrb4c3KIyUlMbbC2tna3 + sp6tVftoRL4H5vNCGBgoSOK4CQqdBI84oytyEUeYr5QdniZjsKBdAmR0GdDfE6CiVCQ/JpjzFJRU + 6l/6UPraYw2AbPUvY2fukWUcELAwYJhPcgsF6IculrL+v3w6h2Ua2DJPh+RL7ytyeER+zSwpT0T0 + fcVefAazDivFhCZLBQOKfRTZ768LJUDs5V99t/iFjyf/yn6zEtgfb/uPf8Q24F9klktYPbrUkGOl + M2h4k5MZcEX4ig73kLjRhPKF+K+lF6/OqFyL4i//D8PHf1FioBg7HHz+q+/+Nvjn/2HmG+u/0Fje + zWeFl2KCRCXmw+AP/0meLj485e389iT/F//5bUgunSChVmLBuKuwyjfRcye8An32jiayvUWWmZZ6 + Yqt4Ti1JPFkzB1Cgu5yArTuNxqxCjBXESTQfLZbjkeJpkueNNUExPV2QZc8VbEfTBMmxZXusmhPX + VP8ceyjQDuU/OvPF1wzvsqLqA5LblOibHxDaP7O7DgdS4Xy39AWURZwfDuYgc+n7A1WSFh++njsf + +G/d1vlvNh8HmokvOO/z6ab9Ai+yse7Pzp81evVvcOtzp0scXULDJSXNrjRUQClkPaY1h0ASE5HA + 8gZYlqY8RnnhAqKn4RD+vveCaIEsMARFFS4vwRpZxt6QOCAYEgd6jC7J5TIAe5cyR+xPyHtv5k8C + aAzLoEiYrsgU/7LlGq0JXIS5lpMVCI15IpJz6BRIATTTiqCoomSbzkC/TWcUokuY3+gaF9mJN6Wr + pgNyDFbkPKIbLNjoCevqm3JXZ6wr1kWhBm8GRjwJli7trc24ccxs9NQRgcOvGXgCwhOsDfiL5RMs + 5ay7BstuGTgA14pMYs/1U46ArQPYDnAJRrRQwI6gTdRCDJ/xmwMA8PnJJiRjUzZLsJSM8xJsqsoz + X1cCAUQd8t6rzOs1oOMyM1hW5Dvv8jL2VuS5CEOMneXYC4b0phl55wH6Cd48G6KYArucnIy9eObE + LuseXk7IuRMvvBDdQqjKvlsCCcpDokiSSsv4mEHYcf+1hAY8wDKS9vEygcaG5PzvIhEJ84Nwrt2f + gXflYETRt35CyQFQSrnyf/77/yY48ZMrQrMZI74qIpyxd4nhC/0XjFQgEaolkJ7qX6PhQ8VH5i5d + cFdu4CzDyQxxh/2e/sAQHXpEsRHTsgQTHDsunwKkx7fHZy+P/zpko4DhXFLUy6qokjlPpc7TOzsp + cYDccHAw6s9lc51RXSSvUxI7PlopDtgxQMxY5meYPYLLZvK5ohhZe2Jl6Vs7QlgyAd6FeRTDGL/E + Z4LPX/F1Ma7JbsbexQVbwwOWMgzReaJ3LvlQkMbRSw3076BQvAQupBZZ4mF6+YCsPCeGwV9Sbx0g + DLGIqKWsEtMGQmgVkRNEKZlGDLmAkhBp2xrSWhMnnjtXqJIdFLHuMqaoCqFxgNhJZ8T7AOj0PWBF + xjJcsAeIhuUCjUP8iXOKXyk818BfwN9+SkeBWbBZ3zCMKKR9ss4jMND9EMbyLgI7EyoyNh8vE6Dn + JCFgH4Q4vekMeJDe2uOsH5MXJz8wFqfXSrFnLJN6zhwhuvK8BYMS1W6CNAVlpzBAqgZgCFSlY1FU + 9yRapozQnkLrfgzUMV76QSqgCYRuJlxyr8F+A9ZRQM5gYK4D+PwbyGpAE5quSzpVM2cB8oQJCjzb + JdKVOsw0xbysQgPAo0DwaBOHoPTdUrPIIi9AGPLN/3/AVFA6xqGXkDEk9aSTRtEVcgzUuiSraEnG + OGo+Ql6SudXW1JPN34weOwDJBvWegnQFFEJrOH3IZiEIRglsH7DclgloH9ASQA2R66zyMZ6h0KZN + Jkvqp5rG0XU647hQczQXAZ5xoTWDEY49qINUgvrwA6xCwf57H6EKCvx0JZLjIJ3RScSR0H2dCteE + T1NKs5MUlyYxmmweTMoC9SO+QwYAjYIzMEFnqIukTa6jZYB6is8xSAGgC2eRANFf+wA+ZUTmglnG + mapATlwBr1wnbPqHmdYDTAGlhcypikAACM5qSLgOQRsn24eI6SYIYy3m5wLh8h47SK89L0V5vabz + DMnHKUURpwL6aebPgdFAkTo+khMllFxj1koXFIDvnWCJKPkxDPwrNm0gUtOM9mBhBqIyXnEkIGUm + YDwNM2xu4H/szUBdRWztCNZ4AGbMitBb424ua7jqz5ukrJkz/WSFlk8m+K8pSWCjUAYlLaGAOuQM + 6AGdkz8By4Hux2VyNto1LVLIHJAwYFAgZ39wUrZepUtZh2kMmBswMUDEUXMLv4wRBTB2f4K2FZaZ + RdcNzIYAgoBEZzKACNIgQoWM0PoUh0wSAzNcXvoTH8nB9eY4fMQQG2Nu9gGCUGyCzOOkwCYywmau + QYkR13eRvBPPmzNXYEineT7EX6jV/MsVHcE8CqnWAdmKU8y8/WwvlcqoDL1jbwpMzWgQYV4TKDU3 + 0Rz0UWxD81M0OLHtv0EDaGghdea45bY4oMXn3AVjjuYeSCskZageofIpS0KdyVOcf8G5dnycHCYF + /07OfvyJSTwXSYdrGbRzloshyPGZE06oDe9k6wOAD1+wcTmZCY01lwn9g9ThpV6mwMZRhDMGhnOU + JNQYYDgXyfNlWhDIfydOHMPYkatgzFzvgkDi+hBLUjEVuhEIRewl8D6ArMqYCGaVzD02VU4uhRce + mzuub2YO8C5UwHsigDg2G6iW5qvYB9XMBQs1vZJkCRbxGWCXjSlMuI3P4eQDRJLMYIhBPjHbZz0w + MKTARuHO9QxfIofvX0t/cgUTj3EcoElQ9KqpouxH1heo9mSKGNvjdYeoIz6nGgItsmiRT/dLL1n4 + iPo5jOM926tCMPmoxBJN4awHUULpBnE7QytriCyI8lbEHJBNvEiFOCz1AHso85HpqFGRiRJkHvTO + oNkLo6LiexpFLpuj9conznCIksunK5wAVQCqA1ofPVQp9DCHyaC8DuMpyJHp0nedgsWUcQRjN6BF + ugfnMV0O4Iq1anGt9qh1NoamL9ESRGE6ZOYUGjBUCzO0LEFJBQxwBx7QvuA0VZSn1MyjKp1K81Kf + SQC4EckroMBlCDSO/cEssyVUQaMwe/GDN1mm1GCcODDBgCRqbMM4L6OYWwFXHti8Ez+ZAwJBqqY5 + UXzLJpQkmcUQNkwrmgXo2PXDJRUFaF5SjAPRCKiVQflNZ0wKjOPoChr6XKU0SBcBOZkyTqMV+PTy + wQDeuQkCaExZOWq+ou0DXXKVg+xCF/wzPLaQ/Gvpeb945H/++//huEU4neDaWXErhvLAEte1bDTo + R8WqTMIUrTEgJ6AzwDUSVwRTQPUUDnEK8jfJUbbuB9Hv0HFDLU4oZVXMpM2MG40waIDKubwMvAJB + FtYKa/Mczf2IEk4yj0D38PaYHccpGtYTznusR0nQASIMfGoMLJwU9EeItQvkS0EBwTHxXEotGSbd + GIREbtX8uCgQMBYOcfEGiwePLSYmlL3BJHXiKVJCUWggPhhTuVRsFkQt1SKUw/l0D0kY4bKoaDOw + cWErYP9QOk24FemwTTNnjM1yYzajqAK+6RytV/7rtfy+zvmqLxVXn7B6DLwLoHHuSqSvDwew1Mw2 + Fpahnx4O+Epf4Cv90RkL6vPXJfrush0Ib4znK6QP0sG/bQP/FEuNUDHhgYILoJwPa7B2O9f1BmQt + M0ZfOkjuL6jTirzzwYA9m4DIWPsiiu3PsVoioCsLpNY3x7SZ5B39+Tz68OY1hfilh0KvuJ1ZXM6X + 2ri4CJyxB2Wx540t7WXQVMmbguwcVJwlgd9QnJrng6Pvo+tno8DvVIvUvRQEWCr6yzngkf7tq1WU + IoOjb+HfcovPRsugft+x0g4KZ5BThaMQN1cByZySwhZ/8XOlmQ2XVH4ooVRLAPsHtFmlo+aXQlZP + y8m67PXefLWFoDLItc0TEo3bt5UmCh6iCgZu5Bt0UjpEFmSJJPQ3+olLqz/mH4Z1PyryfN8O/SvU + hPeTzDgE7TfHGiUAxk6A9gzoL2b1ow5ZLHFNhZtEa1GYmdDoxmGAoMHno5PZQamJqlvgHgU0YTwB + zWk0j5Mlg5rBWeqcwexkbvu1OQWqBxeUoBJQ6YABBmYGE9Z0NQJ24xzlDsLnx2AdxlckuobF7oLp + VxdxSTVLCq2gQZpGpY6dCd3QhIIrIY0E+AOEVDBfM9MZF9EM2TFbq5RRj6qR6UF0BlIDU6wjj97u + ddTpDECeF9+nymj2arc4Clw4JZnvRtWc8eVO8nLIK/yZlaL7coIlCWDkTNh5nqwIV9v9BbqqD/PE + DiFTYhauY2cxOPrhOsRR1Z7cXdfD00L8PANQ3hd3f3C3DvRXYHtdBpHTeAa+fJ+Kw48Jw4yeDtDW + QVXZ+ukEm6yKWu05155g+8kJp0tcv3yDRMb3TL88W45x1yT+qhOshqhJtwjqtx5znr/gS7s3oCmm + HtvJRLA7waqKunmLsJ6Bme5la4EXUbzoCp18mwSZYfAdLC2cGBYd9NhBAaEAMlDB36IY1jeZdzrp + RgyKaN3mEHK6PaebaMyzynQFQOx6H8grENodIdbtu4BYl6RdQZRF+zaR+jxwJlfvEJN4cM5Pl9zT + dB7jBiXSxNvjjvCadwMvYpIcu+/9BAi1K4j6LYL4jYdHlzZFFvn++xcd4dRvU7iei+QdWGR8U/44 + SaKJj0dVMo3ApACFfD2IbhJBFrXb5K/vneuYbp19FyXeYkZOggCoIewKYl83aepApEicROSvf/0r + 46mOwEm3SQFnpy/fkTOqCU6pjDo5f7UDlJJo1/JT7b2c7XfPszPDULtgbxNuKuOhHnqCAtaSuGx0 + 6Upnzvy8uDMsSV/ASmzi4HYLbv0QfquJoA+WnzVab07gVgo1HcWCk6xuqbDXkbcWS4s1e21bWZRC + 8rZcZugf1TIDQ+N2WWV8F40/jkXGu2i8Cskbkbz0QiCpeSf2eDFz/HjuhLfIyD85/njmvCfnTuj9 + 7HSEzvcuyTFzM6Bv4wfcJaarwY9xccTAPaG7Pui+59Byc/alH9NDzbcIPFc133jjsd8N03cA3F/8 + +Rhtjq5YvQvQvPc+HiG5cvydQBNO19vPJyEoDI8ecRiSb/ypg3umUby6zZWoH0dzfzLzyRv/l2UY + dRoDmv4LL8TD1ndBoq9jB2XVyQyMo0Xs+d2s4zsG9jsHtwXfLUFNgZ25jN1oMvuY4f2Lk84C3Fr9 + mx+ArSmcz6L5oqvReccw/28H9zpW5DuR/MWPr2ZR3A3cNw6ejETX+IsImJjJWiaJX/khHoMBoXP7 + eoMfn/7J92A4nQZw5oV4IiyXJfnxu3ceO1me3CLYx6Ebe9fkuTMFm6+b4OBw/3QqnKLvPI2dHP38 + fH1BFN7iEN46qZPM0FCdzaOl6zvd6OfHv5Bv8azuKT2NR04j+HfFx/E8O3T3cn1Noeslfw7HZ5/9 + PrYpCvv1xa37mrHJ0kal7A4jv0ZVvSuFIQLIqyhK82tSWIoeAsL9oihEkcQvuND9uQMa+0WQFEEy + aI1n9ESLK6yLZzBNEoDmkrXN3wUC/z33+RPHjKd4Ap47PhyUXqd42EtAlAjQiXM4eFX8mncppKsF + XnsqX8oelZpiP2hEBnq9+lniMeuBg1bdMS6iNwNbyAuReUAvepN5zC5859dHK83SrV5fqNxq3ug7 + wNuy+CeYYnG6AU0dMIFQv5Vd2rQtV0AeXWzufGfX2bNbaxxbYnbMlWON3lrLb7yPYyd0E9o8wkV/ + 0l8DWEP7jkAPLRwOXmCmJVx8v/cTP61SGZQN0psL1RJCNpnZJWXcGk79SSLMwfzDu26z+b+bivCz + fHnREj1sFmMUxtFyw4Z/4PyyErg3oviMp5uQasMEd17DySq/k7dwXLxMgn6NAzJxgsmX1D3xJ6Kq + ZISuiq82utz+85k/n5bA8efIzDUA4F1FHPMBLTFK3k//88M8GH6hvoBHAo9hcvgUyQKo4vr6WrxW + xSiejhRJkrDwU5gw7/p59OHwqUQkIhP56RfqCdTGj/CUXb/GbkbshPUIoAIkjt68HiGx4AMjURew + K4TetQhVOVnkIRJuGG4YZTdCqxPDcbEDAKNqY89GTd1sFET5Wn7jbDmbwRmJsSiyEmdWPDSySWxV + AcGrZQyOcUlogBkQvBt8wwuBcXFVS8U1UPGjR6/oPUSyxJN3B9VDI3l9pwwU7YjAissb4yWPNVhl + aYN0lRWiIuZNfJXf0B0QLkeyEnn8jFf5iy3ihgJM1kW5vKcGC0IHQr+oTujFCGGjx6r4WYOCMQaA + UC/GgRPC7xjrhVG0oFfSw4j1VINqjrDbERvwB+SGLov2puCopdgtLx+CKHn+evTj2YiyyygnpLUU + 6YKDraKkgJFuYGzIEt5So0TZBiGXLhsyhX6s5cD02udHjhgDEiCwJCVVPuTFKAvOgQP9Cgfy7zkD + nme/b+S/vGQX9ktLldbctwbjo2Q+SxaVJq29ZVp/F8yXUdD98l4JijtivQ0TgGvpihlQ1ZsbAYKy + ak/aLwgSD1ZAQRK1Ww3kpflSgN4Wp6El2fX8qrF15nkEyx/UrxgYNRXWB7XLg8IzGu1bJcbWhcGO + C4KdrH2/wfh8tPZ3kBKv3569fnnyDpmUGd18JguWd7RI78r07wLN7a4DynxUs96+kZ/G/lZ2yp1m + W/lqs1Q9gzWU25PTsmHfOaspv0dWY6qQ0fXz140MtjGVt8JpNwNzi/zVrF03XjS46HLv3jhKU4yu + MPPnBP12k2ix2qY684Y6eCP5unyt6BcrAWML0qMXlelpvLnC3MO1fNRgCa3jPTKQBWrerwc3Bi4b + HK1XFhwuWFfkEPqpN89WEyN6FR1jYr1mDzgpheiPld7X15doYD6X8OoCi6rYbAkGfjeYvA8jN7q+ + +BnmAWB7CXP4HT4y6KpXk/btKaE7FosLXZIGR4VjU7fTW+gkrvOvCxk7e0uf0Yxo1VfheBVQo4BC + h7hCGOGJox3BoMSO8Q1zYF5kb+4cpMs08RheXp2fndwPVvyrK8+/UHAv6S19JvB852C4DoiFl8d/ + J9rdo2DmhNOLxMMLZ9/CI6wpcAvyjoG4ipKFPzj6C/65884zeZAmH9b8cUG/reXD+dnf16zCDoHf + OaBOAgAdn/39zjteJoKLQWxwEUex8uMZeUlf3AcmmAJj9wZKauxOALhc4q27BPWVwJ+ZxnrFftwh + KOsgP16c1CrQ7F7FG1pkO2jlO7rrt7XGyR2aLMCSsGDxUx9H9mL9o5vp0pu1UgBnNIWhCPTO/uDo + G3hmZ/D7tiOKPUZ+wDr8MzVWr1OQmj/4we13nPgBBofngz2jvzr02g/BFwGCEos1QC/or3sGiMa3 + zgGiQbHvEyDgOHopPacZvIjHAh90I5n+QXOC5dwPl/MMrmP++16BCh0Q4E4gTJ0kg+ste0W+cZJ7 + BS32nAADr/CF1zv+k2TicMWge6gSfhmjZ4YJ+Pz5vuR7DsDIW8bCMsE8Qz+++4G8/OH774/f9S5j + 192N0wnr7rmfTvCA5u0I9cL40hkf3/m3LA/S3RH3GgqAQJjg/ZQXxy/vF4r5h3BwdHpy9sP9gTEd + L9iUnP7w49t7RocfxmjpvySv37Yj+16hWOGWGeZkQ+8nlworEHj83b2gBTjlwwQXzB6hoeUfpril + 8VWyFdR9iVkGBFjQ0VSgz2hBR9PAYxfC+5Z6vLu5h3551t0beL7Vzpz5L2HW2fHc+QXMwfbd9UO6 + HJI0CfJh8ygo9wMILpo5HLBGFe4aikXssTUzyNfs8Q67x/C+meOCL8zfwquepEjdq3KiHr6TkUbT + aVF+1J8toPWLZw7d2LmG2hhoD6YxzJOQNR44HB/h+YRKpCsa8lMI6IbinIZp49HOx0eb5zoEcvJh + EeC94HatkAlI5WmEgTMrqNlMHVQ9A1J/lKN8WqMgDUv7P3jSA9+whHJN20IFOqLVaXDINZVsbLXV + 0BWtUndEk29WAXFO6BExmOd5Mhoc/en/+38JSzLpHOWXB2hcAAxsVddFfquZ/qw9Dop3hX7xQjH0 + 0hHGYYVpwFByxfe0v2/m42/Jl6+hhB+mX2HA7YBFC09oaMT4veeKlUG/86aAD55pAv8X05i7NKCY + n9DMTDSyJQ1xgaHAJhNvkdKAYlAmWsZ/DMfJ4uubUdkBiQ3b7fSrEF0KOBJcLZZ2+s/xK8J0xr8e + Vd/UHFQqUTkF1cEggXSqarlsx4HBhLx3wMJa0Bs8TQOslioO75R943eAUJwWf4sbe72oP6kga8NO + N/BNJ46hT2wErdHj+gm0A+vryvHJl4X39Sgr1Dxal948WbIt3OLdjBEZJ0l8Fk2uPMzj4idC4yEV + 0s1tjLnc0lFD7Y8RB+hhwagKAtjUhW+jypkdXiqj9QY05I1xdjmq1PsYEeC4NOk1xp1mVXGNU6aF + dQk2DlqigRDqWjuqa+FOcOF9wPDB/Jh1FRvhKp3TMz1zx61I7jfwBmOLvP3Hi6aRYiW0A8PVpPlk + c6GdPsfLzMd/4b3DhIxXLbCB58mFtsip0/Fl/Lzin5qQU6zajB1StRba4ye/g0iRxLKuD46Y4m+P + WIYHeix3snmWpZti5dKENVURHvQl+ZHuXWXPbca7YQqs1SPNYj1xcgXNbKo6S7wSd3fLGSv2k68g + Cu+f8fNP2HjlwiheToX/o//Anyf0n2IGSExdzZwn6bsIzDXMYc1vD45oWCA82+l6LK0gFr72xqfL + ZHbmpSnNaHLIEhSC1ethnNnzaOFPDsgA11ECJpcWoMJgiEX8BNa16btleMBSEeK7JzQ74TohYZbi + 20lW4eRwQP/ww4CjFyzb5+iMFklG0DDtYeSHfirO/VD8Ofnz+0M0pSVFsmVFtii+edM73b8tQTMg + 7PQZ3rce/ey8d9jnDL6Ew7WkYdBBhIry2LUmBiYhVHTXtcaSokkAJOfKIHJcgRrS3nQFrEmXGgsn + Rg3diJGWMLzH/BRxIpqeZeuXsnFpSIYpy4Y68by7gQCvRou648q2bCuebiiX7sSRPcns2v0ToG96 + Hf3ZaJbOg6P/HzaEA+q8RQUA headers: Accept-Ranges: - bytes Age: - - '100' + - '1482' Cache-Control: - public, max-age=120 Connection: @@ -1406,15 +1824,15 @@ interactions: Content-Encoding: - gzip Content-Length: - - '50495' + - '70983' Content-Type: - text/html; charset=utf-8 Date: - - Wed, 08 Feb 2023 14:33:51 GMT + - Thu, 09 Feb 2023 11:43:18 GMT ETag: - - W/"44eef-DrDLQ4i0hxX3UcMHFYJVdjG9Kk8" + - W/"545bc-BrSLN/o4h6f6QgzXBR2obTA3V3k" Last-Modified: - - Wed, 08 Feb 2023 14:32:11 GMT + - Thu, 09 Feb 2023 11:18:36 GMT Vary: - Accept-Encoding, Authorization, Fastly-SSL, X-Device, X-Continent-Code, X-Piano-Disabled, X-User-Logged-In, X-User-Has-Access @@ -1423,7 +1841,7 @@ interactions: X-Cache: - HIT, HIT X-Cache-Hits: - - 4, 1 + - 2, 1 X-Country-Code: - PT X-Forwarded-For: @@ -1433,9 +1851,9 @@ interactions: X-Powered-By: - Fenrir X-Served-By: - - cache-iad-kcgs7200148-IAD, cache-lis1490039-LIS + - cache-iad-kcgs7200148-IAD, cache-lis1490030-LIS X-Timer: - - S1675866832.500786,VS0,VE1 + - S1675942999.753412,VS0,VE1 status: code: 200 message: OK diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_business_insider_view/test_price_target_from_analysts_raw.txt b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_business_insider_view/test_price_target_from_analysts_raw.txt index 429c10bb8ab..623a2f6e001 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_business_insider_view/test_price_target_from_analysts_raw.txt +++ b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_business_insider_view/test_price_target_from_analysts_raw.txt @@ -1,4 +1,4 @@ -Loading Daily data for TSLA with starting period 2020-02-04. +Loading Daily data for TSLA with starting period 2020-02-05. Company Rating Price Target Date 2023-02-06 Wedbush Morgan Securities Inc. BUY 225.000000 From 28a5ee408c7d1e476373adffc07d74f08da94b67 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 12:21:46 +0000 Subject: [PATCH 223/230] try passing git action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 724bfbffe97..c6989a3474b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,7 +81,7 @@ jobs: - name: Start Terminal and exit run: | source $VENV - python terminal.py + yes "" | python terminal.py markdown-link-check: name: Markdown Linting runs-on: ubuntu-latest From a0afedcbfc683d1451436f3a689eaba02ff98fc4 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 12:52:32 +0000 Subject: [PATCH 224/230] change .yml everywhere --- .github/workflows/mac.yml | 2 +- .github/workflows/test.yml | 4 ++-- .github/workflows/unit-test.yml | 8 ++++---- .github/workflows/windows.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 2bca5fa0910..8483930a7b8 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -90,4 +90,4 @@ jobs: - name: Start Terminal and exit shell: bash -l {0} - run: python terminal.py + run: yes "" | python terminal.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6989a3474b..d446e708c79 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -150,7 +150,7 @@ jobs: - name: Start Terminal and exit run: | source $VENV - python terminal.py + yes "" | python terminal.py tests-conda: name: Anaconda Python Tests - (${{ matrix.python-version }}, ${{ matrix.os }}) needs: test @@ -254,4 +254,4 @@ jobs: - name: Start Terminal and exit shell: bash -l {0} - run: python terminal.py + run: yes "" | python terminal.py diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c0aaa4e260f..dfdd059041f 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -101,7 +101,7 @@ jobs: - name: Start Terminal and exit run: | source $VENV - python terminal.py + yes "" | python terminal.py tests-python: name: Vanilla Python Tests - ${{ matrix.python-version }} @@ -159,7 +159,7 @@ jobs: - name: Start Terminal and exit run: | source $VENV - python terminal.py + yes "" | python terminal.py full-test: name: Full Test Suite - (${{ matrix.python-version }}, ${{ matrix.os }}) @@ -218,7 +218,7 @@ jobs: - name: Start Terminal and exit run: | source $VENV - python terminal.py + yes "" | python terminal.py tests-conda: name: Anaconda Python Tests - (${{ matrix.python-version }}, ${{ matrix.os }}) @@ -323,4 +323,4 @@ jobs: - name: Start Terminal and exit shell: bash -l {0} - run: python terminal.py + run: yes "" | python terminal.py diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 6c5763f28b4..830a78fd3cb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -104,4 +104,4 @@ jobs: - name: Start Terminal and exit shell: bash -l {0} - run: python terminal.py + run: yes "" | python terminal.py From e568f7a60fe980b0fc5a5bdfb4c327cd4e86298d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 14:08:41 +0000 Subject: [PATCH 225/230] change register link to dev --- openbb_terminal/session/hub_model.py | 1 + openbb_terminal/session/session_controller.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/session/hub_model.py b/openbb_terminal/session/hub_model.py index 298ee23246e..9d00fd6f152 100644 --- a/openbb_terminal/session/hub_model.py +++ b/openbb_terminal/session/hub_model.py @@ -4,6 +4,7 @@ import requests from openbb_terminal.rich_config import console +REGISTER_LINK = "https://my.openbb.dev/register" BASE_URL = "https://payments.openbb.dev/" TIMEOUT = 15 diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 31619e76cb0..5f8e9c37f8d 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,6 +1,7 @@ from typing import Tuple from prompt_toolkit import PromptSession +from openbb_terminal.session.hub_model import REGISTER_LINK import openbb_terminal.session.local_model as Local from openbb_terminal import terminal_controller @@ -17,7 +18,7 @@ def display_welcome_message(): """Display welcome message""" with open(PACKAGE_DIRECTORY / "session" / "banner.txt") as f: console.print(f"[menu]{f.read()}[/menu]\n") - console.print("Register : [cmds]https://my.openbb.co/register[/cmds]") + console.print(f"Register : [cmds]{REGISTER_LINK}[/cmds]") console.print("Ask support : [cmds]https://openbb.co/support[/cmds]") From 40b95c40a90fc00c4d786dfb0417282e5f76aeaf Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 14:19:36 +0000 Subject: [PATCH 226/230] ruff --- openbb_terminal/session/session_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index 5f8e9c37f8d..f6e73caf8dc 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -1,12 +1,12 @@ from typing import Tuple from prompt_toolkit import PromptSession -from openbb_terminal.session.hub_model import REGISTER_LINK import openbb_terminal.session.local_model as Local from openbb_terminal import terminal_controller from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console +from openbb_terminal.session.hub_model import REGISTER_LINK from openbb_terminal.session.session_model import ( LoginStatus, create_session, From c06c5c8b916696faca6743f130d215f6adbbc7e3 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 16:51:21 +0000 Subject: [PATCH 227/230] update msg --- openbb_terminal/session/session_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index f6e73caf8dc..ba9326edb11 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -20,6 +20,10 @@ def display_welcome_message(): console.print(f"[menu]{f.read()}[/menu]\n") console.print(f"Register : [cmds]{REGISTER_LINK}[/cmds]") console.print("Ask support : [cmds]https://openbb.co/support[/cmds]") + console.print( + "[yellow]\nWARNING: This is a preliminary version published for testing.[/yellow]" + "[yellow]\nBeware that your account might be deleted or changed without notice.[/yellow]" + ) def get_user_input() -> Tuple[str, str, bool]: From c39358eefdaad43a95bb1cf465e7fb8caa728a44 Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 16:53:19 +0000 Subject: [PATCH 228/230] new msg --- openbb_terminal/session/session_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbb_terminal/session/session_controller.py b/openbb_terminal/session/session_controller.py index ba9326edb11..e9631fa9b85 100644 --- a/openbb_terminal/session/session_controller.py +++ b/openbb_terminal/session/session_controller.py @@ -21,8 +21,8 @@ def display_welcome_message(): console.print(f"Register : [cmds]{REGISTER_LINK}[/cmds]") console.print("Ask support : [cmds]https://openbb.co/support[/cmds]") console.print( - "[yellow]\nWARNING: This is a preliminary version published for testing.[/yellow]" - "[yellow]\nBeware that your account might be deleted or changed without notice.[/yellow]" + "[yellow]\nWARNING: This is a pre-release version published for testing.[/yellow]" + "[yellow]\nBeware that your account will be deleted without notice.[/yellow]" ) From a8e2d2d00513f1261ce14e499362a83594e5611d Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 16:54:31 +0000 Subject: [PATCH 229/230] tests --- .../test_session_controller/test_display_welcome_message.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt index 07014fb7a25..591c10ff993 100644 --- a/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt +++ b/tests/openbb_terminal/session/txt/test_session_controller/test_display_welcome_message.txt @@ -18,5 +18,8 @@ Investment research for Everyone, Anywhere. [/menu] -Register : [cmds]https://my.openbb.co/register[/cmds] +Register : [cmds]https://my.openbb.dev/register[/cmds] Ask support : [cmds]https://openbb.co/support[/cmds] +[yellow] +WARNING: This is a pre-release version published for testing.[/yellow][yellow] +Beware that your account will be deleted without notice.[/yellow] From df10cb2cbe6d5ed4f37e8aad2a16e45ab707d7ac Mon Sep 17 00:00:00 2001 From: Diogo Sousa Date: Thu, 9 Feb 2023 17:02:41 +0000 Subject: [PATCH 230/230] change register link --- openbb_terminal/session/user.py | 3 ++- openbb_terminal/terminal_controller.py | 3 ++- .../session/txt/test_user/test_whoami[True-True].txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openbb_terminal/session/user.py b/openbb_terminal/session/user.py index 401070db547..ad50700684b 100644 --- a/openbb_terminal/session/user.py +++ b/openbb_terminal/session/user.py @@ -1,5 +1,6 @@ import openbb_terminal.feature_flags as obbff from openbb_terminal.rich_config import console +from openbb_terminal.session.hub_model import REGISTER_LINK class User: @@ -59,7 +60,7 @@ class User: else: console.print( "[info]You are currently logged as a guest.\n" - "Create an account here https://my.openbb.co/register.[/info]" + f"Create an account here {REGISTER_LINK}.[/info]" ) @classmethod diff --git a/openbb_terminal/terminal_controller.py b/openbb_terminal/terminal_controller.py index f8742c936b0..bf5fe26379e 100644 --- a/openbb_terminal/terminal_controller.py +++ b/openbb_terminal/terminal_controller.py @@ -48,6 +48,7 @@ from openbb_terminal.parent_classes import BaseController from openbb_terminal.reports.reports_model import ipykernel_launcher from openbb_terminal.rich_config import MenuText, console from openbb_terminal.session import session_controller +from openbb_terminal.session.hub_model import REGISTER_LINK from openbb_terminal.session.user import User from openbb_terminal.terminal_helper import ( bootup, @@ -370,7 +371,7 @@ class TerminalController(BaseController): if User.is_guest(): console.print( "[info]You need to be logged in to use this menu.\n" - "Create an account here https://my.openbb.co/register.[/info]\n" + f"Create an account here {REGISTER_LINK}.[/info]\n" ) return self.queue = self.load_class(AccountController, self.queue) diff --git a/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt index f9e689b7db0..805bc8bd83b 100644 --- a/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt +++ b/tests/openbb_terminal/session/txt/test_user/test_whoami[True-True].txt @@ -1,2 +1,2 @@ [info]You are currently logged as a guest. -Create an account here https://my.openbb.co/register.[/info] +Create an account here https://my.openbb.dev/register.[/info]