diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a69ff4..e21a44c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,29 +1,29 @@ # find all other possible hooks at https://pre-commit.com/hooks.html -default_stages: [commit] +default_stages: [pre-commit] fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-toml - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 6.0.0 hooks: - id: isort name: isort (python) - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v2.3.1 hooks: - id: autoflake args: [ @@ -33,7 +33,7 @@ repos: "--ignore-init-module-imports" ] - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 7.1.2 hooks: - id: flake8 additional_dependencies: [flake8-isort] diff --git a/docs/conf.py b/docs/conf.py index 9cea3d3..89d7fdd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,28 +54,6 @@ except Exception as e: print("Running `sphinx-apidoc` failed!\n{}".format(e)) -# This dummy-keyring must be set on order to -# avoid keyring.errors.NoKeyringError during build process -try: - from keyring import backend - - # this class prevents the "keyring.errors.NoKeyringError" - class TestKeyring(backend.KeyringBackend): - - priority = 1 - - def set_password(self, servicename, username, password): - return "None" - - def get_password(self, servicename, username): - return "None" - - def delete_password(self, servicename, username): - return "None" - -except Exception as e: - print(e) - # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. diff --git a/setup.cfg b/setup.cfg index 4af6729..63f0921 100644 --- a/setup.cfg +++ b/setup.cfg @@ -110,7 +110,7 @@ formats = bdist_wheel [flake8] # Some sane defaults for the code style checker flake8 -max_line_length = 88 +max_line_length = 100 extend_ignore = E203, W503 # ^ Black-compatible # E203 and W503 have edge cases handled by black @@ -120,6 +120,8 @@ exclude = dist .eggs docs/conf.py +per-file-ignores = + */__init__.py: F401 [isort] # configurations for isort import style checker diff --git a/src/sxapi/base.py b/src/sxapi/base.py index 29a6053..ab45cd0 100644 --- a/src/sxapi/base.py +++ b/src/sxapi/base.py @@ -5,12 +5,34 @@ import requests +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiUnprocessableContentError, +) + class ApiTypes(Enum): PUBLIC = 1 INTEGRATION = 2 +def check_response(func): + """Decorator to handle response status codes.""" + + def wrapper(*args, **kwargs): + response = func(*args, **kwargs) + if response.status_code in [401, 403]: + raise SxapiAuthorizationError() + elif response.status_code == 422: + raise SxapiUnprocessableContentError() + else: + response.raise_for_status() + + return response.json() + + return wrapper + + class BaseAPI(object): def __init__( self, @@ -36,7 +58,7 @@ def get_token(self): @property def session(self): """ - Geneates a new HTTP session on the fly and logs in if no session exists. + Generates a new HTTP session on the fly and logs in if no session exists. """ if self._session is None: self._session = requests.Session() @@ -69,22 +91,26 @@ def _login(self): raise requests.HTTPError(response.status_code, response.reason) self._session.headers.update({"Authorization": f"Bearer {self.api_token}"}) + @check_response def get(self, path, *args, **kwargs): url = self.to_url(path) r = self.session.get(url, *args, **kwargs) - return r.json() + return r + @check_response def post(self, path, *args, **kwargs): url = self.to_url(path) r = self.session.post(url, *args, allow_redirects=False, **kwargs) - return r.json() + return r + @check_response def put(self, path, *args, **kwargs): url = self.to_url(path) r = self.session.put(url, *args, allow_redirects=False, **kwargs) - return r.json() + return r + @check_response def delete(self, path, *args, **kwargs): url = self.to_url(path) r = self.session.delete(url, *args, **kwargs) - return r.json() + return r diff --git a/src/sxapi/cli/__init__.py b/src/sxapi/cli/__init__.py index feb1785..c1a2222 100644 --- a/src/sxapi/cli/__init__.py +++ b/src/sxapi/cli/__init__.py @@ -1,3 +1,119 @@ -from .cli_user import CliUser +import os + +import keyring + +from sxapi.integrationV2 import IntegrationAPIV2 +from sxapi.publicV2 import PublicAPIV2 + + +class CliUser: + """ + CliUser class used for initializing, storing, retrieving and deleting + credentials and creating/holding Instances of supported API + Client. + + This class should only be used in the cli package. + """ + + def __init__(self): + """ + Basic User Credentials Constructor + + calls self._init_creds() to set available credentials on startup. + """ + self.organisation_id = None + self.api_access_token = None + self.public_v2_api = None + self.integration_v2_api = None + + @staticmethod + def get_token_environment(): + """ + Gets token named 'SMAXTEC_API_ACCESS_TOKEN' from the systems' environment. + """ + + return os.environ.get("SMAXTEC_API_ACCESS_TOKEN", None) + + def set_token_keyring(self, token): + """ + Store the given token in keyring. + """ + keyring.set_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN", token) + self.api_access_token = token + + @staticmethod + def get_token_keyring(): + """ + Gets the token stored in the keyring. + """ + return keyring.get_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN") + + @staticmethod + def clear_token_keyring(): + """ + Deletes the token from the keyring. + """ + keyring.delete_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN") + + # general functions + def check_credentials_set(self): + """ + Checks if token is already set. + """ + if self.api_access_token is not None: + return True + return False + + def init_user(self, config, args_token, args_keyring, args_orga_id): + """ + This function retrieves the token from the specified resource + (keyring, environment or args) and initializes clients + of the supported APIs (PublicV2, IntegrationV2). + + If no token can be found the token is retrieved via + the username and password. + + If username and password are also missing, no credentials get + stored and not API clients are created. + """ + if args_orga_id: + self.organisation_id = args_orga_id + elif config.orga: + self.organisation_id = config.orga + + if args_token: + self.api_access_token = args_token + elif args_keyring: + self.api_access_token = self.get_token_keyring() + if self.api_access_token is None: + print("No token found in keyring. Use values from config file.\n") + else: + self.api_access_token = self.get_token_environment() + + if self.api_access_token is None and config.user and config.password: + self.public_v2_api = PublicAPIV2( + base_url=config.api_public_v2_path, + email=config.user, + password=config.password, + ) + self.integration_v2_api = IntegrationAPIV2( + base_url=config.api_integration_v2_path, + email=config.user, + password=config.password, + ) + + self.api_access_token = self.public_v2_api.get_token() + + elif self.api_access_token: + self.public_v2_api = PublicAPIV2( + base_url=config.api_public_v2_path, + api_token=self.api_access_token, + ) + + self.integration_v2_api = IntegrationAPIV2( + base_url=config.api_integration_v2_path, + api_token=self.api_access_token, + ) + cli_user = CliUser() diff --git a/src/sxapi/cli/cli.py b/src/sxapi/cli/cli.py index 16ea253..81448e9 100644 --- a/src/sxapi/cli/cli.py +++ b/src/sxapi/cli/cli.py @@ -1,199 +1,76 @@ -import argparse -import configparser -import os import sys -from os.path import ( - abspath, - expanduser, -) - -from setuptools import setup - -from sxapi.cli import cli_user -from sxapi.cli.subparser.token import create_token_parser +from requests.exceptions import HTTPError + +from sxapi.cli.parser.main_parser import SxApiMainParser +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiCliArgumentError, + SxapiConfigurationFileError, + SxapiFileNotFoundError, + SxapiInvalidJsonError, + SxapiMissingOrgaIDError, + SxapiUnprocessableContentError, +) -class Cli: - """CLI class for handling arguments and calling the API.""" - def __init__(self): - self.config_file_paths = ["/etc/sxapi.conf", "~/.config/sxapi.conf"] - - @staticmethod - def update_config_with_env(config_dict): - config_dict["user"] = os.getenv("SXAPI_USER", config_dict["user"]) - config_dict["pwd"] = os.getenv("SXAPI_PASSWORD", config_dict["pwd"]) - config_dict["orga"] = os.getenv("SXAPI_ORGA", config_dict["orga"]) - config_dict["api_public_v2_path"] = os.getenv( - "SXAPI_API_PUBLIC_V2_PATH", config_dict["api_public_v2_path"] - ) - config_dict["api_integration_v2_path"] = os.getenv( - "SXAPI_API_INTEGRATION_V2_PATH", config_dict["api_integration_v2_path"] - ) - - def read_config_from_file(self, config_file_path): - config_dict = { - "user": None, - "pwd": None, - "orga": None, - "api_public_v2_path": None, - "api_integration_v2_path": None, - } - - if config_file_path: - self.config_file_paths.append(config_file_path) - - parsable_files = [] - for config_file in self.config_file_paths: - config_file = expanduser(config_file) - config_file = abspath(config_file) - parsable_files.append(config_file) +def handle_cli_return_values(func): + def wrapper(*args, **kwargs): try: - config = configparser.ConfigParser(interpolation=None) - config.read(parsable_files) - - config_dict["user"] = config.get("SXAPI", "USER") - config_dict["pwd"] = config.get("SXAPI", "PASSWORD") - config_dict["orga"] = config.get("SXAPI", "ORGA") - config_dict["api_public_v2_path"] = config.get( - "SXAPI", "API_PUBLIC_V2_PATH" - ) - config_dict["api_integration_v2_path"] = config.get( - "SXAPI", "API_INTEGRATION_V2_PATH" - ) - except ( - KeyError, - configparser.NoSectionError, - configparser.MissingSectionHeaderError, - ) as e: - if config_file_path: - print(f"Error while reading config file: {e}") - return - # we should raise custom exception here - - return config_dict - - @staticmethod - def api_status(): - """ - Print online status of api/v2 and integration/v2 - """ - - if not cli_user.api_access_token: - print("No credentials set. Use --help for more information.") - return - - pub_resp = cli_user.public_v2_api.get("/service/status") - int_resp = cli_user.integration_v2_api.get("/service/status") - - exit_code = 0 - - if not (pub_resp["result"] == "ok" and int_resp["result"] == "ok"): + return func(*args, **kwargs) + except SxapiAuthorizationError as e: + error_msg = e exit_code = 1 - - print(f"PublicV2 status: {pub_resp['result']}") - print(f"IntegrationV2 status: {int_resp['result']}") + except SxapiUnprocessableContentError as e: + error_msg = e + exit_code = 2 + except SxapiConfigurationFileError as e: + error_msg = e + exit_code = 3 + except SxapiInvalidJsonError as e: + error_msg = e + exit_code = 4 + except SxapiFileNotFoundError as e: + error_msg = e + exit_code = 5 + except SxapiMissingOrgaIDError as e: + error_msg = e + exit_code = 6 + except SxapiCliArgumentError as e: + error_msg = e + exit_code = 7 + except HTTPError as e: + error_msg = e + exit_code = 98 + except Exception as e: + error_msg = e + exit_code = 99 + + if error_msg: + print(error_msg) exit(exit_code) - @staticmethod - def version_info(): - """Print version info.""" - setup(use_scm_version={"version_scheme": "no-guess-dev"}) - - @staticmethod - def parse_args(args): - """ - Parse arguments from the CLI and initializes subparsers. - """ - main_parser = argparse.ArgumentParser( - description=( - "Issue calls to the smaXtec system API to import and export data." - ), - usage="%(prog)s [options] [sub_command_options] []", - ) - main_parser.add_argument( - "--version", - action="store_true", - default=False, - help="print version info and exit.", - ) - main_parser.add_argument( - "--status", - action="store_true", - default=False, - help="prints status of api/V2 and integration/v2", - ) - main_parser.add_argument( - "-t", - "--access_token", - type=str, - help="Access Token", - ) - main_parser.add_argument( - "-k", - "--use_keyring", - action="store_true", - help="Use keyring as token source!", - ) - main_parser.add_argument( - "-c", "--configfile", type=str, help="Path to config file" - ) - main_parser.add_argument( - "--print-configfile", - action="store_true", - help="Print example config file and exits", - ) - - subparsers = main_parser.add_subparsers(title="sub_commands") - # create_gsd_parser(subparsers) - create_token_parser(subparsers) - - if not args: - main_parser.print_help() - return - - # check if subparser is called with arguments - # otherwise print help for subparser - elif args[0] in subparsers.choices.keys() and len(args) == 1: - subparsers.choices[args[0]].print_help() - return - - return main_parser.parse_args(args) - - def run(self): - """Call sxapi functions based on passed arguments.""" - args = self.parse_args(sys.argv[1:]) - if not args: - return 0 + return wrapper - if args.print_configfile: - with open("./src/sxapi/cli/example-config.conf", "r") as f: - print(f.read()) - return - config_dict = self.read_config_from_file(args.configfile or None) - - self.update_config_with_env(config_dict) - - cli_user.init_user(config_dict, args.access_token, args.use_keyring) - - if args.status: - self.api_status() +class Cli: + """CLI class for handling arguments and calling the API.""" - if args.version: - self.version_info() + sx_main_parser = SxApiMainParser(subparsers=True) - if args.use_keyring and args.access_token: - print("Choose either -k (keyring), -t (argument) or no flag (environment)!") - return + @handle_cli_return_values + def run(self): + """Call sxapi functions based on passed arguments.""" - # run set_defaults for subparser - if hasattr(args, "func"): - args.func(args) + return self.sx_main_parser.parse_args(sys.argv[1:]) def cli_run(): """Start CLI""" Cli().run() + + +if __name__ == "__main__": + cli_run() diff --git a/src/sxapi/cli/cli_user.py b/src/sxapi/cli/cli_user.py deleted file mode 100644 index 28adee6..0000000 --- a/src/sxapi/cli/cli_user.py +++ /dev/null @@ -1,109 +0,0 @@ -import os - -import keyring - -from sxapi.integrationV2 import IntegrationAPIV2 -from sxapi.publicV2 import PublicAPIV2 - - -class CliUser: - """ - CliUser class used for initializing, storing, retrieving and deleting - credentials and creating/holding Instances of supported API - Client. - - This class should only be used in the cli package. - """ - - def __init__(self): - """ - Basic User Credentials Constructor - - calls self._init_creds() to set available credentials on startup. - """ - - self.api_access_token = None - self.public_v2_api = None - self.integration_v2_api = None - - @staticmethod - def get_token_environment(): - """ - Gets token named 'SMAXTEC_API_ACCESS_TOKEN' from the systems' environment. - """ - - return os.environ.get("SMAXTEC_API_ACCESS_TOKEN", None) - - def set_token_keyring(self, token): - """ - Store the given token in keyring. - """ - keyring.set_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN", token) - self.api_access_token = token - - @staticmethod - def get_token_keyring(): - """ - Gets the token stored in the keyring. - """ - return keyring.get_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN") - - @staticmethod - def clear_token_keyring(): - """ - Deletes the token from the keyring. - """ - keyring.delete_password("sxapi", "SMAXTEC_API_ACCESS_TOKEN") - - # general functions - def check_credentials_set(self): - """ - Checks if token is already set. - """ - if self.api_access_token is not None: - return True - return False - - def init_user(self, config_dict, args_token, args_keyring): - """ - This function retrieves the token from the specified resource - (keyring, environment or args) and initializes clients - of the supported APIs (PublicV2, IntegrationV2). - - If no token can be found the token is retrieved via - the username and password. - - If username and password are also missing, no credentials get - stored and not API clients are created. - """ - if args_token: - self.api_access_token = args_token - elif args_keyring: - self.api_access_token = self.get_token_keyring() - else: - self.api_access_token = self.get_token_environment() - - if self.api_access_token is None and config_dict["user"] and config_dict["pwd"]: - self.public_v2_api = PublicAPIV2( - base_url=config_dict["api_public_v2_path"], - email=config_dict["user"], - password=config_dict["pwd"], - ) - self.integration_v2_api = IntegrationAPIV2( - base_url=config_dict["api_integration_v2_path"], - email=config_dict["user"], - password=config_dict["pwd"], - ) - - self.api_access_token = self.public_v2_api.get_token() - - elif self.api_access_token: - self.public_v2_api = PublicAPIV2( - base_url=config_dict["api_public_v2_path"], - api_token=self.api_access_token, - ) - - self.integration_v2_api = IntegrationAPIV2( - base_url=config_dict["api_integration_v2_path"], - api_token=self.api_access_token, - ) diff --git a/src/sxapi/cli/configuration.py b/src/sxapi/cli/configuration.py new file mode 100644 index 0000000..d29ece0 --- /dev/null +++ b/src/sxapi/cli/configuration.py @@ -0,0 +1,80 @@ +import configparser +import os +from os.path import ( + abspath, + expanduser, +) + +from sxapi.errors import ( + SxapiConfigurationFileError, + SxapiFileNotFoundError, +) + + +class Config: + + _config_file_paths = ["/etc/sxapi.conf", "~/.config/sxapi.conf"] + + def __init__(self, configfile=None): + + self.user = None + self.password = None + self.orga = None + self.api_public_v2_path = None + self.api_integration_v2_path = None + + self._read_config_from_file(configfile) + self._update_config_with_env() + + def to_dict(self): + return { + "user": self.user, + "password": self.password, + "orga": self.orga, + "api_public_v2_path": self.api_public_v2_path, + "api_integration_v2_path": self.api_integration_v2_path, + } + + def _update_config_with_env(self): + self.user = os.getenv("SXAPI_USER", self.user) + self.password = os.getenv("SXAPI_PASSWORD", self.password) + self.orga = os.getenv("SXAPI_ORGA", self.orga) + self.api_public_v2_path = os.getenv( + "SXAPI_API_PUBLIC_V2_PATH", self.api_public_v2_path + ) + self.api_integration_v2_path = os.getenv( + "SXAPI_API_INTEGRATION_V2_PATH", self.api_integration_v2_path + ) + + def _read_config_from_file(self, config_file_path): + if config_file_path: + if not os.path.isfile(config_file_path): + raise SxapiFileNotFoundError( + f"Config file {config_file_path} does not exist." + ) + self._config_file_paths.append(config_file_path) + + parsable_files = [] + for config_file in self._config_file_paths: + config_file = expanduser(config_file) + config_file = abspath(config_file) + parsable_files.append(config_file) + + config = configparser.ConfigParser(interpolation=None) + + # if no configfile was read return empty config_dict + if len(config.read(parsable_files)) == 0: + return self + + try: + self.user = config.get("SXAPI", "USER") + self.password = config.get("SXAPI", "PASSWORD") + self.orga = config.get("SXAPI", "ORGA") + self.api_public_v2_path = config.get("SXAPI", "API_PUBLIC_V2_PATH") + self.api_integration_v2_path = config.get( + "SXAPI", "API_INTEGRATION_V2_PATH" + ) + except configparser.Error as e: + raise SxapiConfigurationFileError(e) + + return self diff --git a/src/sxapi/cli/example-config.conf b/src/sxapi/cli/example-config.conf index 954eba8..ad15fe2 100644 --- a/src/sxapi/cli/example-config.conf +++ b/src/sxapi/cli/example-config.conf @@ -4,13 +4,13 @@ USER = user.name@example.com # smaXtec password -PASSWORD = "userSecret" +PASSWORD = userSecret # organisation to retrieve data from ORGA = smaxtec_organisation_id # API url for PublicV2 -API_PUBLIC_V2_PATH = https://api.smaxtec.com/v2/public +API_PUBLIC_V2_PATH = https://api.smaxtec.com/api/v2 # API url for IntegrationV2 -API_INTEGRATION_V2_PATH = https://api.smaxtec.com/v2/integration +API_INTEGRATION_V2_PATH = https://api.smaxtec.com/integration/v2 diff --git a/base_api.py b/src/sxapi/cli/parser/__init__.py similarity index 100% rename from base_api.py rename to src/sxapi/cli/parser/__init__.py diff --git a/src/sxapi/cli/parser/main_parser.py b/src/sxapi/cli/parser/main_parser.py new file mode 100644 index 0000000..d160fb6 --- /dev/null +++ b/src/sxapi/cli/parser/main_parser.py @@ -0,0 +1,146 @@ +import argparse + +from setuptools import setup + +from sxapi.cli import cli_user +from sxapi.cli.configuration import Config +from sxapi.cli.parser.subparser import ( + SxApiAbortsSubparser, + SxApiAnimalsSubparser, + SxApiTokenSubparser, +) + + +def version_info(): + """Print version info.""" + setup(use_scm_version={"version_scheme": "no-guess-dev"}) + + +def api_status(): + """ + Print online status of api/v2 and integration/v2 + """ + + if not cli_user.api_access_token: + print("No credentials set. Use --help for more information.") + return + + pub_resp = cli_user.public_v2_api.get("/service/status") + int_resp = cli_user.integration_v2_api.get("/service/status") + + print(f"PublicV2 status: {pub_resp['result']}") + print(f"IntegrationV2 status: {int_resp['result']}") + + +class SxApiMainParser: + def __init__(self, subparsers=False): + + self.config = Config() + + self._parser = argparse.ArgumentParser( + description=( + "Issue calls to the smaXtec system API to import and export data." + ), + usage="%(prog)s [options] [sub_command_options] []", + ) + + self._add_arguments() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="main_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + # add arguments to the parser + self._parser.add_argument( + "--version", + action="store_true", + default=False, + help="print version info and exit.", + ) + self._parser.add_argument( + "--status", + action="store_true", + default=False, + help="prints status of api/V2 and integration/v2", + ) + self._parser.add_argument( + "-t", + "--access_token", + type=str, + help="Access Token", + ) + self._parser.add_argument( + "-k", + "--use_keyring", + action="store_true", + help="Use keyring as token source!", + ) + self._parser.add_argument( + "-c", "--configfile", type=str, help="Path to config file" + ) + self._parser.add_argument( + "--print-configfile", + action="store_true", + help="Print example config file and exits", + ) + self._parser.add_argument( + "-o", + "--organisation_id", + type=str, + default=None, + help="ID of working organisation", + ) + + def _add_subparser(self): + # Initiate other subparsers here + SxApiTokenSubparser.register_as_subparser(self._subparsers) + SxApiAnimalsSubparser.register_as_subparser(self._subparsers) + SxApiAbortsSubparser.register_as_subparser(self._subparsers) + + def parse_args(self, args): + if len(args) == 0: + self._parser.print_help() + return + + args = self._parser.parse_args(args) + + if not args: + return + + if args.print_configfile: + with open("./src/sxapi/cli/example-config.conf", "r") as f: + print(f.read()) + return + + if args.configfile: + self.config = Config(args.configfile) + + cli_user.init_user( + self.config, args.access_token, args.use_keyring, args.organisation_id + ) + + if args.status: + api_status() + + if args.version: + version_info() + + if args.use_keyring and args.access_token: + print( + "Choose either -k (keyring), -t (argument) or" + " no flag (environment/config)!" + ) + return + + # run set_defaults for subparser + if hasattr(args, "func"): + args.func(args) + + return args diff --git a/src/sxapi/cli/parser/subparser/__init__.py b/src/sxapi/cli/parser/subparser/__init__.py new file mode 100644 index 0000000..c38ff55 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/__init__.py @@ -0,0 +1,3 @@ +from sxapi.cli.parser.subparser.aborts import SxApiAbortsSubparser +from sxapi.cli.parser.subparser.animals import SxApiAnimalsSubparser +from sxapi.cli.parser.subparser.token.__init__ import SxApiTokenSubparser diff --git a/src/sxapi/cli/parser/subparser/aborts/__init__.py b/src/sxapi/cli/parser/subparser/aborts/__init__.py new file mode 100644 index 0000000..82a3bae --- /dev/null +++ b/src/sxapi/cli/parser/subparser/aborts/__init__.py @@ -0,0 +1,41 @@ +from sxapi.cli.parser.subparser.aborts.create import SxApiAbortsCreateSubparser +from sxapi.cli.parser.subparser.aborts.delete import SxApiAbortsDeleteSubparser +from sxapi.cli.parser.subparser.aborts.update import SxApiAbortsUpdateSubparser + + +class SxApiAbortsSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=True): + self._parser = parent_subparser.add_parser( + "aborts", + help="Working on aborts", + usage="sxapi [base_options] aborts [abort_sub_commands]", + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="abort_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + pass + + def _add_subparser(self): + SxApiAbortsUpdateSubparser.register_as_subparser(self._subparsers) + SxApiAbortsCreateSubparser.register_as_subparser(self._subparsers) + SxApiAbortsDeleteSubparser.register_as_subparser(self._subparsers) + + def _set_default_func(self): + + self._parser.set_defaults(func=lambda args: self._parser.print_help()) diff --git a/src/sxapi/cli/parser/subparser/aborts/create.py b/src/sxapi/cli/parser/subparser/aborts/create.py new file mode 100644 index 0000000..e6336d5 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/aborts/create.py @@ -0,0 +1,79 @@ +import argparse + +from sxapi.cli import cli_user +from sxapi.errors import SxapiAuthorizationError + +DESCRIPTION = """ + Create abort for the given animal. + + All available fields can be found here: https://api.smaxtec.com/api/v2/animals + + If the call was successful the whole animal, containing the created abort, is returned. +""" + + +class SxApiAbortsCreateSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "create", + help="create abort", + usage="sxapi [base_options] aborts create ANIMAL_ID EVENT_DATE [options]", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="abort_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "animal_id", + nargs="?", + type=str, + help="ID of the animal to create the abort for", + metavar="ANIMAL_ID", + ) + self._parser.add_argument( + "event_ts", + nargs="?", + type=str, + help="Date of the event as unix timestamp", + metavar="EVENT_DATE", + ) + self._parser.add_argument( + "--late_abort", + action="store_true", + help="Set the abort as late", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + res = cli_user.public_v2_api.animals.post_aborts( + animal_id=args.animal_id, + event_ts=args.event_ts, + late_abort=args.late_abort, + ) + + print(res) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/aborts/delete.py b/src/sxapi/cli/parser/subparser/aborts/delete.py new file mode 100644 index 0000000..2d6eb22 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/aborts/delete.py @@ -0,0 +1,73 @@ +import argparse + +from sxapi.cli import cli_user +from sxapi.errors import SxapiAuthorizationError + +DESCRIPTION = """ + Delete abort for the given animal. + + All available fields can be found here: https://api.smaxtec.com/api/v2/ + + If the call was successful the updated animal is returned. +""" + + +class SxApiAbortsDeleteSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "delete", + help="delete abort", + usage="sxapi [base_options] aborts update ANIMAL_ID ABORT_ID", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="abort_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "animal_id", + nargs="?", + type=str, + help="ID of the animal to delete the abort for", + metavar="ANIMAL_ID", + ) + self._parser.add_argument( + "abort_id", + nargs="?", + type=str, + help="ID of the abort to delete", + metavar="ABORT_ID", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + res = cli_user.public_v2_api.animals.delete_aborts( + animal_id=args.animal_id, + abort_id=args.abort_id, + ) + + print(res) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/aborts/update.py b/src/sxapi/cli/parser/subparser/aborts/update.py new file mode 100644 index 0000000..1da3c56 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/aborts/update.py @@ -0,0 +1,87 @@ +import argparse + +from sxapi.cli import cli_user +from sxapi.errors import SxapiAuthorizationError + +DESCRIPTION = """ + Update abort for the given animal. + + All available fields can be found here: https://api.smaxtec.com/api/v2/ + + If the call was successful the whole animal, containing the updated abort,is returned. +""" + + +class SxApiAbortsUpdateSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "update", + help="update abort", + usage="sxapi [base_options] aborts update ANIMAL_ID EVENT_TS ABORT_ID [options]", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="abort_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "animal_id", + nargs="?", + type=str, + help="ID of the animal to create the abort for", + metavar="ANIMAL_ID", + ) + self._parser.add_argument( + "event_ts", + nargs="?", + type=str, + help="Date of the event as unix timestamp", + metavar="EVENT_DATE", + ) + self._parser.add_argument( + "abort_id", + nargs="?", + type=str, + help="ID of the abort to update", + metavar="ABORT_ID", + ) + self._parser.add_argument( + "--late_abort", + action="store_true", + help="Set the abort as late", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + res = cli_user.public_v2_api.animals.put_aborts( + animal_id=args.animal_id, + event_ts=args.event_ts, + abort_id=args.abort_id, + late_abort=args.late_abort, + ) + + print(res) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/animals/__init__.py b/src/sxapi/cli/parser/subparser/animals/__init__.py new file mode 100644 index 0000000..151ba99 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/animals/__init__.py @@ -0,0 +1,41 @@ +from sxapi.cli.parser.subparser.animals.create import SxApiAnimalsCreateSubparser +from sxapi.cli.parser.subparser.animals.get import SxApiAnimalsGetSubparser +from sxapi.cli.parser.subparser.animals.update import SxApiAnimalsUpdateSubparser + + +class SxApiAnimalsSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=True): + self._parser = parent_subparser.add_parser( + "animals", + help="Working on animals", + usage="sxapi [base_options] animals [animals_sub_commands]", + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="animals_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + pass + + def _add_subparser(self): + SxApiAnimalsGetSubparser.register_as_subparser(self._subparsers) + SxApiAnimalsCreateSubparser.register_as_subparser(self._subparsers) + SxApiAnimalsUpdateSubparser.register_as_subparser(self._subparsers) + + def _set_default_func(self): + + self._parser.set_defaults(func=lambda args: self._parser.print_help()) diff --git a/src/sxapi/cli/parser/subparser/animals/create.py b/src/sxapi/cli/parser/subparser/animals/create.py new file mode 100644 index 0000000..9dff76f --- /dev/null +++ b/src/sxapi/cli/parser/subparser/animals/create.py @@ -0,0 +1,106 @@ +import argparse +import json +import sys + +from sxapi.cli import cli_user +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiFileNotFoundError, + SxapiInvalidJsonError, + SxapiMissingOrgaIDError, +) + +DESCRIPTION = """ + Create animals for the given organisation. + + Basic Example: + { + "mark": "123456", + "organisation_id": "123456", + "official_id": "123456", + "birthday": "2018-01-01", + "name": "Bella", + } + + All available fields can be found here: https://api.smaxtec.com/api/v2/animals + + If the call was successful the whole animal is returned. +""" + + +class SxApiAnimalsCreateSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "create", + help="create animals", + usage="sxapi [base_options] animals create [options] ANIMAL_JSON_FILE", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="animals_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "animal_json", + nargs="?", + type=argparse.FileType("r"), + default=sys.stdin, + help="Path to json file containing animal data. (default: stdin)", + metavar="ANIMAL_JSON_FILE", + ) + self._parser.add_argument( + "--organisation_id", + nargs="?", + type=str, + help="ID of the organisation to retrieve animals from", + metavar="ORGANISATION_ID", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + organisation_id = cli_user.organisation_id + + if args.organisation_id: + organisation_id = args.organisation_id + + if organisation_id is None: + raise SxapiMissingOrgaIDError() + + if args.animal_json.isatty(): + raise SxapiFileNotFoundError() + + try: + animal_json = json.load(args.animal_json) + + res = cli_user.public_v2_api.animals.post( + organisation_id, **animal_json + ) + + except json.JSONDecodeError: + raise SxapiInvalidJsonError() + + print(res) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/animals/get.py b/src/sxapi/cli/parser/subparser/animals/get.py new file mode 100644 index 0000000..9caef83 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/animals/get.py @@ -0,0 +1,128 @@ +import json + +from sxapi.cli import cli_user +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiMissingOrgaIDError, +) + +DESCRIPTION = """ + Get animals from the smaXtec system. + + If no optional flags are set, get all animals from the specified organisation. + + If the call was successful the whole animal is returned. +""" + + +class SxApiAnimalsGetSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "get", + help="Get animals", + usage="sxapi [base_options] animals get [options]", + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="animals_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "--ids", + nargs="+", + help="ID's of the animals to retrieve", + metavar="ANIMAL_IDS", + ) + self._parser.add_argument( + "--official-ids", + action="store_true", + help="The given ANIMAL_IDS are animals official_ids\ + instead of internal_ids", + ) + self._parser.add_argument( + "-o", + "--organisation_id", + nargs="?", + type=str, + help="ID of the organisation to retrieve animals from", + metavar="ORGANISATION_ID", + ) + self._parser.add_argument( + "--limit", + "-l", + default=0, + type=int, + help="Limit the number of animals to retrieve.", + metavar="NUMBER_OF_ANIMALS", + ) + self._parser.add_argument( + "--archived", + action="store_true", + default=False, + help="Include archived animals. default = False.\ + (only active if --all is set)", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + organisation_id = cli_user.organisation_id + + if args.organisation_id: + organisation_id = args.organisation_id + + if organisation_id is None: + raise SxapiMissingOrgaIDError() + + animals = [] + + if args.ids: + if args.official_ids: + # there is no endpoint to get a list of animals by official id + for official_id in args.ids: + animal = cli_user.public_v2_api.animals.get_by_official_id( + organisation_id, official_id + ) + + if ( + not animal["archived"] + or animal["archived"] + and args.archived + ): + animals.append(animal) + else: + for animal in cli_user.public_v2_api.animals.get_by_ids(args.ids): + if animal["archived"] and args.archived: + animals.append(animal) + else: + animals = cli_user.integration_v2_api.organisations.get_animals( + organisation_id, + include_archived=args.archived, + ) + + if args.limit > 0: + animals = animals[: args.limit] + + print(json.dumps(animals)) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/animals/update.py b/src/sxapi/cli/parser/subparser/animals/update.py new file mode 100644 index 0000000..d976fb6 --- /dev/null +++ b/src/sxapi/cli/parser/subparser/animals/update.py @@ -0,0 +1,116 @@ +import argparse +import json +import sys + +from sxapi.cli import cli_user +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiFileNotFoundError, + SxapiInvalidJsonError, + SxapiMissingOrgaIDError, +) + +DESCRIPTION = """ + Update an animal for the given organisation. + + Provide an dict containing the fields to update. + 1. The animal_id must be contained in the update dict. + 2. If you want to move the animal to another organisation, + the mark needs to be unique in the new organisation. + + Basic Example: + { + "animal_id": "123456", # REQUIRED + ... + "organisation_id": "1234bdc234d + "mark": "123456", + "official_id": "AT000123456", + "official_id_rule": "AT", + "birthday": "2018-01-01", + "name": "Bella", + ... + } + + All available fields can be found here: + POST https://api.smaxtec.com/api/v2/animals/{animal_id} + + If the call was successful the whole animal is returned. +""" + + +class SxApiAnimalsUpdateSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "update", + help="Update animal", + usage="sxapi [base_options] animals update [options]", + formatter_class=argparse.RawDescriptionHelpFormatter, + description=DESCRIPTION, + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="animals_sub_commands", + ) + + self._add_subparser() + + def _add_arguments(self): + self._parser.add_argument( + "update_dict", + nargs="?", + type=argparse.FileType("r"), + default=sys.stdin, + help="Path to file containing animal update dict. (default: stdin)", + metavar="ANIMAL_UPDATE_DICT", + ) + self._parser.add_argument( + "-o", + "--organisation_id", + nargs="?", + type=str, + help="ID of the organisation to retrieve animals from", + metavar="ORGANISATION_ID", + ) + + def _add_subparser(self): + pass + + def _set_default_func(self): + def animals_sub_function(args): + if not cli_user.check_credentials_set(): + raise SxapiAuthorizationError() + + organisation_id = cli_user.organisation_id + + if args.organisation_id: + organisation_id = args.organisation_id + + if organisation_id is None: + raise SxapiMissingOrgaIDError() + + if args.update_dict.isatty(): + raise SxapiFileNotFoundError() + + try: + update_dict = json.load(args.update_dict) + animal_id = update_dict.pop("animal_id", None) + print(update_dict) + res = cli_user.public_v2_api.animals.put(animal_id, **update_dict) + + except json.JSONDecodeError: + raise SxapiInvalidJsonError() + + print(res) + + self._parser.set_defaults(func=animals_sub_function) diff --git a/src/sxapi/cli/parser/subparser/token/__init__.py b/src/sxapi/cli/parser/subparser/token/__init__.py new file mode 100644 index 0000000..68dd57e --- /dev/null +++ b/src/sxapi/cli/parser/subparser/token/__init__.py @@ -0,0 +1,183 @@ +import getpass + +import requests + +from sxapi.cli import cli_user +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiCliArgumentError, +) +from sxapi.publicV2 import PublicAPIV2 + + +def handle_print_token(args): + """ + Logic behind the token subparser --print_token flag. + + Prints the token from the desired source (environment or keyring) to stdout. + """ + keyring = str(cli_user.get_token_keyring()) + env = str(cli_user.get_token_environment()) + + if args.print_token == "ek": + print(f"\nKeyring: {keyring}\n\nEnvironment: {env}") + return 0 + elif len(args.print_token) > 2: + raise SxapiCliArgumentError( + "Invalid number of arguments. Use --help for usage information." + ) + + if "e" != args.print_token and "k" != args.print_token: + raise SxapiCliArgumentError( + "Invalid arguments. Only use 'e' for environment, 'k' for keyring or 'ek' for both." + ) + + if "e" == args.print_token: + print(f"\nEnvironment Token: {env}\n") + return 0 + elif "k" == args.print_token: + print(f"\nKeyring Token: {keyring}\n") + return 0 + + +def handle_set_token(args): + """ + Logic behind the token subparser --set_keyring flag. + + Parses the args and stores the token in the keyring. + """ + token = args.set_keyring[0] + cli_user.set_token_keyring(token=token) + print("Token is stored in keyring!") + + +def handle_clear_token(): + """ + Logic behind the token subparser --clear_keyring flag. + + Deletes the token from the keyring. + """ + cli_user.clear_token_keyring() + print("Token was deleted from keyring!") + + +def handle_new_token(): + """ + Logic behind the token subparser --new_token flag. + + Parses the args, creates an PublicAPIV2 instance to get new token and + print the new token to stdout. + """ + + username = input("Username: ") + + if "@" not in username: + raise SxapiCliArgumentError("Username must be a email!") + + pwd = getpass.getpass() + + try: + token = str(PublicAPIV2(email=username, password=pwd).get_token()) + print("SMAXTEC_API_ACCESS_TOKEN=" + token) + return 0 + except requests.HTTPError as e: + if "401" in str(e) or "422" in str(e): + raise SxapiAuthorizationError("Username or Password is wrong!") + + +class SxApiTokenSubparser: + @classmethod + def register_as_subparser(cls, parent_subparser): + return cls(parent_subparser) + + def __init__(self, parent_subparser, subparsers=False): + self._parser = parent_subparser.add_parser( + "token", + help="Get/Set credentials aka 'SMAXTEC_API_ACCESS_TOKEN' from/to specified " + "storage location or create new one", + usage="sxapi [base_options] token [options]", + ) + + self._add_arguments() + self._set_default_func() + + if subparsers: + self.reg_subparsers = [] + self._subparsers = self._parser.add_subparsers( + title="Sub Commands", + description="Available subcommands:", + dest="main_sub_commands", + ) + + self._add_subparser("naf") + + def _add_arguments(self): + self._parser.add_argument( + "--print_token", + "-p", + nargs="?", + const="ek", + help="Print the current token stored in keyring/environment to stdout. " + "One argument required. Possible args 'e' environment | " + "'k' keyring | ek for printing both.", + metavar="SOURCE", + ) + self._parser.add_argument( + "--set_keyring", + "-s", + nargs=1, + help="Store the given token in keyring! Requires one argument .", + metavar="TOKEN", + ) + self._parser.add_argument( + "--new_token", + "-n", + action="store_true", + help="Reqeust new token", + ) + self._parser.add_argument( + "--clear_keyring", + "-c", + action="store_true", + default=False, + help="Remove the token from keyring!", + ) + + def _add_subparser(self, name, **kwargs): + raise NotImplementedError + + def _set_default_func(self): + def token_sub_function(args): + """ + The token subparser default function. + This function gets called if token subparser is used. + + Checks args and calls the specific helper function (see below) according to + the present flag. + """ + number_op = ( + bool(args.print_token) + + bool(args.set_keyring) + + bool(args.new_token) + + bool(args.clear_keyring) + ) + + if number_op > 1: + print(number_op) + raise SxapiCliArgumentError( + "Invalid Combination! Please use just one out of these parameters " + "[--print_token, --set_keyring, --new_token, --clear_keyring]" + ) + + if args.print_token: + return handle_print_token(args) + elif args.set_keyring: + return handle_set_token(args) + elif args.clear_keyring: + return handle_clear_token() + elif args.new_token: + return handle_new_token() + + self._parser.print_help() + + self._parser.set_defaults(func=token_sub_function) diff --git a/src/sxapi/cli/subparser/get_sensor_data.py b/src/sxapi/cli/subparser/get_sensor_data.py deleted file mode 100644 index 17ffae5..0000000 --- a/src/sxapi/cli/subparser/get_sensor_data.py +++ /dev/null @@ -1,83 +0,0 @@ -import json - -from sxapi.cli import cli_user -from sxapi.publicV2.sensordata import get_sensor_data_from_animal - - -def create_gsd_parser(subparsers): - """ - get_sensor_data subparser for cli_tests. - Responsible for performing api-call to get the sensor data for a given animal. - - The 'animals_id' is mandatory argument 'animal_id'. - It represents the animal you want to get data from. - - The following flag are only optional. - The --metrics/-m Flag defines the metrics you want to get from the animal sensor. - It expects at most two arguments 'temp', 'act' or both together. Where 'temp' means - getting the temperature metric and 'act' means the activity metric. - - - The --from_date Flag defines the start-date of window you want to get data from. - It expects exactly one argument, a datetime in the format 'YYYY-MM-DD' - (e.g. 2022.01.12). - - The --to_date Flag defines the end-date of window you want to get data from. - It expects exactly one argument, a datetime in the format 'YYYY-MM-DD' - (e.g. 2022.01.12). - """ - gsd_parser = subparsers.add_parser( - "get_sensor_data", - aliases=["gsd"], - help="Get sensor data from animal(by its ID)", - ) - gsd_parser.add_argument( - "animal_id", - help="Animal you want get data from", - ) - gsd_parser.add_argument( - "--metrics", - "-m", - nargs="*", - default=None, - help="metrics for sensordata", - ) - gsd_parser.add_argument( - "--from_date", - default=None, - nargs=1, - help="from_date format: YYYY-MM-DD", - ) - gsd_parser.add_argument( - "--to_date", - default=None, - nargs=1, - help="to_date format: YYYY-MM-DD", - ) - - gsd_parser.set_defaults(func=gsd_sub_function) - - -def gsd_sub_function(args): - """ - The get_sensor_data subparser default function. - This function gets called if you get_sensor_data subparser is used. - - Pares the given arguments and calls a function which - performs the desired api call. - """ - if not cli_user.check_credentials_set(): - print("No credentials set. Use --help for more information.") - return - - api = cli_user.public_v2_api - - id = args.animal_id - metrics = args.metrics - from_date = args.from_date - to_date = args.to_date - resp = get_sensor_data_from_animal( - api=api, animal_id=id, metrics=metrics, from_date=from_date, to_date=to_date - ) - if resp is not None: - print(json.dumps(resp, indent=0)) diff --git a/src/sxapi/cli/subparser/token.py b/src/sxapi/cli/subparser/token.py deleted file mode 100644 index 745dc26..0000000 --- a/src/sxapi/cli/subparser/token.py +++ /dev/null @@ -1,183 +0,0 @@ -import getpass - -import requests - -from sxapi.cli import cli_user -from sxapi.publicV2 import PublicAPIV2 - - -def create_token_parser(subparsers): - """ - Token subparser for cli_tests. - Responsible for managing tokens/credentials. - - The --print_token/-p Flag prints the token, stored in keyring or in - environment, to stdout. It expects exactly only one argument -> 'e' - for printing the token stored in the environment, 'k' for printing - the token stored in keyring and 'ek' for printing both at the same time. - - The --set_keyring/-s flag stores the token (given as argument) in the keyring. - It expects exactly one argument -> the token you want to store. - - The --clear_keyring/-c Flag deletes the token from the keyring. - This flag doesn't expect any argument. - - The --new_token/-n Flag calls api to get new token and prints it to stdout. - It expects exactly two arguments -> smaxtec-username and smaxtec-password. - - It is not possible to use more than one of those flags at the same time - """ - token_parser = subparsers.add_parser( - "token", - help="Get/Set credentials aka 'SMAXTEC_API_ACCESS_TOKEN' from/to specified " - "storage location or create new one", - usage="sxapi [base_options] token [options]", - ) - - token_parser.add_argument( - "--print_token", - "-p", - nargs="?", - const="ek", - help="Print the current token stored in keyring/environment to stdout. " - "One argument required. Possible args 'e' environment | " - "'k' keyring | ek for printing both.", - metavar="SOURCE", - ) - token_parser.add_argument( - "--set_keyring", - "-s", - nargs=1, - help="Store the given token in keyring! Requires one argument .", - metavar="TOKEN", - ) - token_parser.add_argument( - "--new_token", - "-n", - action="store_true", - help="Reqeust new token", - ) - token_parser.add_argument( - "--clear_keyring", - "-c", - action="store_true", - default=False, - help="Remove the token from keyring!", - ) - - token_parser.set_defaults(func=token_sub_function) - - -def token_sub_function(args): - """ - The token subparser default function. - This function gets called if token subparser is used. - - Checks args and calls the specific helper function (see below) according to - the present flag. - """ - number_op = ( - bool(args.print_token) - + bool(args.set_keyring) - + bool(args.new_token) - + bool(args.clear_keyring) - ) - - if number_op > 1: - print( - "Invalid Combination! Please use just one out of these parameters " - "[--print_token, --set_keyring, --new_token, --clear_keyring]" - ) - return 1 - - if args.print_token: - return handle_print_token(args) - elif args.set_keyring: - return handle_set_token(args) - elif args.clear_keyring: - return handle_clear_token() - elif args.new_token: - return handle_new_token(args) - - -# Flag helper functions -def handle_print_token(args): - """ - Logic behind the token subparser --print_token flag. - - Prints the token from the desired source (environment or keyring) to stdout. - """ - keyring = str(cli_user.get_token_keyring()) - env = str(cli_user.get_token_environment()) - - if args.print_token == "ek": - print(f"\nKeyring: {keyring}\n\nEnvironment: {env}") - return 0 - elif len(args.print_token) > 2: - print("Invalid number of arguments. Use --help for usage information.") - return 0 - - if "e" != args.print_token and "k" != args.print_token: - print( - "Invalid arguments. Only use 'e' for environment, 'k' for keyring " - "or 'ek' for both." - ) - return 1 - - if "e" == args.print_token: - print(f"\nEnvironment Token: {env}\n") - return 0 - elif "k" == args.print_token: - print(f"\nKeyring Token: {keyring}\n") - return 0 - - -def handle_set_token(args): - """ - Logic behind the token subparser --set_keyring flag. - - Parses the args and stores the token in the keyring. - """ - token = args.set_keyring[0] - cli_user.set_token_keyring(token=token) - print("Token is stored in keyring!") - - return 0 - - -def handle_clear_token(): - """ - Logic behind the token subparser --clear_keyring flag. - - Deletes the token from the keyring. - """ - cli_user.clear_token_keyring() - print("Token was deleted from keyring!") - - return 0 - - -def handle_new_token(args): - """ - Logic behind the token subparser --new_token flag. - - Parses the args, creates an PublicAPIV2 instance to get new token and - print the new token to stdout. - """ - - username = input("Username: ") - - if "@" not in username: - print("Username must be a email!") - return 1 - - pwd = getpass.getpass() - - try: - token = str(PublicAPIV2(email=username, password=pwd).get_token()) - print("SMAXTEC_API_ACCESS_TOKEN=" + token) - return 0 - except requests.HTTPError as e: - if "401" in str(e) or "422" in str(e): - print("Username or Password is wrong!") - return 1 diff --git a/src/sxapi/errors.py b/src/sxapi/errors.py new file mode 100644 index 0000000..0ea0170 --- /dev/null +++ b/src/sxapi/errors.py @@ -0,0 +1,93 @@ +class SxapiCliArgumentError(Exception): + """Raised when arguments are valid for argparse + but make no sense semantically.""" + + ARGUMENT_ERROR_MSG = """ + Invalid arguments provided. Please check the help for the correct usage.""" + + def __init__(self, message=ARGUMENT_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" + + +class SxapiAuthorizationError(Exception): + """Raised when authorization fails 401, 403.""" + + AUTHORIZATION_ERROR_MSG = """ + Authorization failed: Access to the requested resource is denied. + Please check your if your credentials are set and ensure you have the necessary permissions.""" + + def __init__(self, message=AUTHORIZATION_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" + + +class SxapiMissingOrgaIDError(Exception): + """Raised when no organisation_id can be found""" + + ORGANISATION_ID_ERROR_MSG = """ + No organisation_id was set. + Provide you organisation_id as env var, as cli parameter or inside the config File""" + + def __init__(self, message=ORGANISATION_ID_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" + + +class SxapiUnprocessableContentError(Exception): + """Raised when content is unprocessable 422.""" + + UNPROCESSABLE_CONTENT_ERROR_MSG = """ + The request was well-formed but was unable to be followed due to semantic errors.""" + + def __init__(self, message=UNPROCESSABLE_CONTENT_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" + + +class SxapiConfigurationFileError(Exception): + """Raised when configuration file is not valid.""" + + CONFIGURATION_FILE_ERROR_MSG = "Configuration File Error" + + def __init__(self, parent, info=CONFIGURATION_FILE_ERROR_MSG): + self.parent_name = parent.__class__.__name__ + self.message = parent.message + self.info = info + + def __str__(self): + return f"{self.info} -> {self.parent_name} -> {self.message}" + + +class SxapiInvalidJsonError(Exception): + """Raised when JSON object is not valid.""" + + AUTHORIZATION_ERROR_MSG = """ + Object is not a valid JSON object.""" + + def __init__(self, message=AUTHORIZATION_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" + + +class SxapiFileNotFoundError(Exception): + """Raised when the path is not valid or the file does not exist.""" + + AUTHORIZATION_ERROR_MSG = """ + Given Path is not correct or file does not exist.""" + + def __init__(self, message=AUTHORIZATION_ERROR_MSG): + self.message = message + + def __str__(self): + return f"{self.__class__.__name__}: {self.message}" diff --git a/src/sxapi/publicV2/data.py b/src/sxapi/publicV2/data.py index 3993207..3ae8b12 100644 --- a/src/sxapi/publicV2/data.py +++ b/src/sxapi/publicV2/data.py @@ -55,7 +55,7 @@ def get_metrics_animals(self, animal_id, **kwargs): params[k] = v url_suffix = self.path_suffix + f"/animals/{animal_id}/metrics" - self.api.get(url_suffix, json=params) + return self.api.get(url_suffix, json=params) def get_data_devices(self, device_id, metrics, from_date, to_date, **kwargs): """Query sensordata for a device. diff --git a/src/sxapi/publicV2/sensordata.py b/src/sxapi/publicV2/sensordata.py index daa39a5..a861b09 100644 --- a/src/sxapi/publicV2/sensordata.py +++ b/src/sxapi/publicV2/sensordata.py @@ -1,7 +1,4 @@ -from datetime import ( - datetime, - timedelta, -) +import datetime from sxapi.publicV2 import PublicAPIV2 @@ -32,19 +29,19 @@ def get_sensor_data_from_animal(api, animal_id, *args, **kwargs): from_date_string = kwargs.get("from_date") to_date_string = kwargs.get("to_date") - to_date = datetime.utcnow() - from_date = to_date - timedelta(days=2) + to_date = datetime.datetime.now(datetime.UTC) + from_date = to_date - datetime.timedelta(days=2) if to_date_string: try: - to_date = datetime.strptime(*to_date_string, "%Y-%m-%d") + to_date = datetime.datetime.strptime(to_date_string, "%Y-%m-%d") except ValueError: print("to_date has not the right format YYYY-MM-DD!") return None if from_date_string: try: - from_date = datetime.strptime(*from_date_string, "%Y-%m-%d") + from_date = datetime.datetime.strptime(from_date_string, "%Y-%m-%d") except ValueError: print("from_date has not the right format YYYY-MM-DD!") return None diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..88f6baa --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,236 @@ +from requests import Response + +from sxapi.base import check_response +from sxapi.cli import CliUser +from sxapi.cli.cli import Cli +from sxapi.cli.configuration import Config +from sxapi.cli.parser.main_parser import SxApiMainParser +from sxapi.integrationV2 import IntegrationAPIV2 +from sxapi.publicV2 import PublicAPIV2 + + +class SxMainTestParser(SxApiMainParser): + def __init__(self, subparsers=True): + self.config = Config(configfile="./tests/cli_tests/test-config.conf") + + super().__init__(subparsers=subparsers) + + +class CliTest(Cli): + sx_main_parser = SxMainTestParser(subparsers=True) + + +class ResponseMock(Response): + """ + Object to mock a response object from the requests library. + """ + + def __init__(self, value, status_code, exception=None, iterator=False): + """ + Initialize a new response object. + + Args: + value (Any): Desired return value of the response object. + status_code (int): Desired status code of the response object. + exception (Exception): Desired exception to be raised by the response object. + iterator (bool): If value is an iterator and mocked call is performed several types + set this to True in order to get the next value of the iterator on each call. + """ + + super().__init__() + + self.value = value + self.status_code = status_code + self.exception = exception + self.iterator = iterator + + def json(self, **kwargs): + """ + Mock the json method of the response object. + """ + return self.value + + +class _APIMock: + """ + Base Mock object for the API classes. + """ + + def __init__(self): + super().__init__() + + self.get_iterator = None + self.post_iterator = None + self.put_iterator = None + self.delete_iterator = None + + self.get_return_value = None + self.post_return_value = None + self.put_return_value = None + self.delete_return_value = None + + self.get_called_with = [] + self.post_called_with = [] + self.put_called_with = [] + self.delete_called_with = [] + + def mock_get_return_value(self, return_value): + """ + Set the return value of the mock object. + Args: + return_value: + + Returns: + + """ + if not isinstance(return_value, ResponseMock): + raise ValueError("return_value is not of type ResponseMock") + + self.get_return_value = return_value + + if return_value.iterator: + self.get_iterator = iter(self.get_return_value.value) + + def mock_post_return_value(self, return_value): + """ + Set the return value of the mock object. + Args: + return_value: + + Returns: + + """ + if not isinstance(return_value, ResponseMock): + raise ValueError("return_value is not of type ResponseMock") + + self.post_return_value = return_value + + if return_value.iterator: + self.post_iterator = iter(self.post_return_value.value) + + def mock_put_return_value(self, return_value): + """ + Set the return value of the mock object. + Args: + return_value: + + Returns: + + """ + if not isinstance(return_value, ResponseMock): + raise ValueError("return_value is not of type ResponseMock") + + self.put_return_value = return_value + + if return_value.iterator: + self.put_iterator = iter(self.put_return_value.value) + + def mock_delete_return_value(self, return_value): + """ + Set the return value of the mock object. + Args: + return_value: + + Returns: + + """ + if not isinstance(return_value, ResponseMock): + raise ValueError("return_value is not of type ResponseMock") + + self.delete_return_value = return_value + + if return_value.iterator: + self.delete_iterator = iter(self.delete_return_value.value) + + def reset_mock(self, get=False, post=False, put=False, delete=False): + + # if nothing is set reset all + if not get and not post and not put and not delete: + get = post = put = delete = True + + if get: + self.get_return_value = [] + self.get_called_with = [] + if post: + self.post_return_value = [] + self.post_called_with = [] + if put: + self.put_return_value = [] + self.put_called_with = [] + if delete: + self.delete_return_value = [] + self.delete_called_with = [] + + @check_response + def get(self, path, *args, **kwargs): + self.get_called_with.append({"path": path, "kwargs": kwargs}) + + if self.get_return_value.exception: + raise self.get_return_value.exception + + if self.get_iterator: + self.get_return_value.value = next(self.get_iterator) + + return self.get_return_value + + @check_response + def post(self, path, *args, **kwargs): + self.post_called_with.append({"path": path, "kwargs": kwargs}) + + if self.post_return_value.exception: + raise self.post_return_value.exception + + if self.post_iterator: + self.post_return_value.value = next(self.post_iterator) + + return self.post_return_value + + @check_response + def put(self, path, *args, **kwargs): + self.put_called_with.append({"path": path, "kwargs": kwargs}) + + if self.put_return_value.exception: + raise self.put_return_value.exception + + if self.put_iterator: + self.put_return_value.value = next(self.put_iterator) + + return self.put_return_value + + @check_response + def delete(self, path, *args, **kwargs): + self.delete_called_with.append({"path": path, "kwargs": kwargs}) + + if self.delete_return_value.exception: + raise self.delete_return_value.exception + + if self.delete_iterator: + self.delete_return_value.value = next(self.delete_iterator) + + return self.delete_return_value + + +class PublicAPIV2Test(_APIMock, PublicAPIV2): + def __init__(self): + super().__init__() + + +class IntegrationAPIV2Test(_APIMock, IntegrationAPIV2): + def __init__(self): + super().__init__() + + +class CliUserTest(CliUser): + def __init__(self): + super().__init__() + + self.organisation_id = "test_organisation_id" + self.api_access_token = "test_api_token" + self.public_v2_api = PublicAPIV2Test() + self.integration_v2_api = IntegrationAPIV2Test() + + def check_credentials_set(self): + return True + + def init_user(self, config, args_token, args_keyring, args_orga_id): + pass diff --git a/tests/cli_tests/test-config.conf b/tests/cli_tests/test-config.conf index bcd9c90..621a7d5 100644 --- a/tests/cli_tests/test-config.conf +++ b/tests/cli_tests/test-config.conf @@ -4,5 +4,5 @@ USER = test@example.com PASSWORD = smaxtec_test_user_pwd ORGA = smaxtec_test_organisation_id -API_PUBLIC_V2_PATH = test_path_test -API_INTEGRATION_V2_PATH = test_path_integration +API_PUBLIC_V2_PATH = https://test_path_test +API_INTEGRATION_V2_PATH = https://test_path_integration diff --git a/tests/cli_tests/test_cli.py b/tests/cli_tests/test_cli.py index d3fed55..0cd9fe5 100644 --- a/tests/cli_tests/test_cli.py +++ b/tests/cli_tests/test_cli.py @@ -2,95 +2,113 @@ import mock -from sxapi.cli import cli_user -from sxapi.cli.cli import Cli +from sxapi.cli import ( + cli_user, + configuration, +) +from tests import CliTest -cli = Cli() +obj = mock.MagicMock +ConfigTest = configuration.Config -@mock.patch("sxapi.cli.cli.Cli.version_info") + +@mock.patch("sxapi.cli.parser.main_parser.version_info") @mock.patch("builtins.print") @mock.patch("sxapi.cli.cli_user.get_token_keyring", return_value="keyring-token") @mock.patch("sxapi.publicV2.PublicAPIV2.get_token", return_value="keyring-token") def test_func(get_token_mock, keyring_mock, print_mock, version_mock): - with mock.patch("sys.argv", ["sx_api", "--version"]): + cli = CliTest() + with mock.patch("sys.argv", ["sxapi", "--version"]): assert version_mock.call_count == 0 - cli.run() + CliTest().run() assert version_mock.call_count == 1 print_mock.reset_mock() - with mock.patch("sys.argv", ["sx_api", "-k", "-t", "test"]): + with mock.patch("sys.argv", ["sxapi", "-k", "-t", "test"]): cli.run() call_args = print_mock.call_args_list[0] assert ( - call_args.args[0] - == "Choose either -k (keyring), -t (argument) or no flag (environment)!" + call_args.args[0] == "Choose either -k (keyring), -t (argument)" + " or no flag (environment/config)!" ) print_mock.reset_mock() - with mock.patch("sys.argv", ["sx_api", "-k"]): + with mock.patch("sys.argv", ["sxapi", "-k"]): cli.run() assert cli_user.api_access_token == "keyring-token" - with mock.patch("sys.argv", ["sx_api", "-t", "args_token"]): + with mock.patch("sys.argv", ["sxapi", "-t", "args_token"]): cli.run() assert cli_user.api_access_token == "args_token" -@mock.patch("sxapi.cli.cli.Cli.version_info") +@mock.patch("sxapi.cli.parser.main_parser.version_info") @mock.patch("sxapi.cli.cli_user.get_token_keyring", return_value="keyring-token") def test_config(keyring_mock, version_mock): # test config file default - cli.config_file_paths = ["./tests/cli_tests/test-config.conf"] - with mock.patch("sys.argv", ["sx_api", "--version"]): + cli = CliTest() + ConfigTest._config_file_paths = ["./tests/cli_tests/test-config.conf"] + cli.sx_main_parser.config = ConfigTest() + with mock.patch("sys.argv", ["sxapi", "--version"]): with mock.patch("sxapi.cli.cli_user.init_user") as iu_mock: cli.run() res = iu_mock.call_args_list[0][0][0] - assert res["user"] == "test@example.com" - assert res["pwd"] == "smaxtec_test_user_pwd" - assert res["orga"] == "smaxtec_test_organisation_id" - assert res["api_public_v2_path"] == "test_path_test" - assert res["api_integration_v2_path"] == "test_path_integration" + assert res.user == "test@example.com" + assert res.password == "smaxtec_test_user_pwd" + assert res.orga == "smaxtec_test_organisation_id" + assert res.api_public_v2_path == "https://test_path_test" + assert res.api_integration_v2_path == "https://test_path_integration" # test config_file_path override with mock.patch( "sys.argv", - ["sx_api", "--version", "-c", "./tests/cli_tests/test-config_param.conf"], + ["sxapi", "--version", "-c", "./tests/cli_tests/test-config_param.conf"], ): with mock.patch("sxapi.cli.cli_user.init_user") as iu_mock: cli.run() res = iu_mock.call_args_list[0][0][0] - assert res["user"] == "test2@example.com" - assert res["pwd"] == "smaxtec_test2_user_pwd" - assert res["orga"] == "smaxtec_test2_organisation_id" - assert res["api_public_v2_path"] == "test2_path_test" - assert res["api_integration_v2_path"] == "test2_path_integration" + assert res.user == "test2@example.com" + assert res.password == "smaxtec_test2_user_pwd" + assert res.orga == "smaxtec_test2_organisation_id" + assert res.api_public_v2_path == "test2_path_test" + assert res.api_integration_v2_path == "test2_path_integration" # test override with env_vars os.environ["SXAPI_USER"] = "testenv@example.com" + os.path.abspath("") with mock.patch( - "sys.argv", ["sx_api", "--version", "-c", "./test-config_param.conf"] + "sys.argv", + ["sxapi", "--version", "-c", "./tests/cli_tests/test-config_param.conf"], ): with mock.patch("sxapi.cli.cli_user.init_user") as iu_mock: cli.run() res = iu_mock.call_args_list[0][0][0] - assert res["user"] == "testenv@example.com" - assert res["pwd"] == "smaxtec_test2_user_pwd" - assert res["orga"] == "smaxtec_test2_organisation_id" - assert res["api_public_v2_path"] == "test2_path_test" - assert res["api_integration_v2_path"] == "test2_path_integration" + assert res.user == "testenv@example.com" + assert res.password == "smaxtec_test2_user_pwd" + assert res.orga == "smaxtec_test2_organisation_id" + assert res.api_public_v2_path == "test2_path_test" + assert res.api_integration_v2_path == "test2_path_integration" -@mock.patch("sxapi.cli.cli.Cli.version_info") +@mock.patch("sxapi.cli.parser.main_parser.version_info") @mock.patch("sxapi.cli.cli_user.get_token_keyring", return_value="keyring-token") @mock.patch("sxapi.publicV2.PublicAPIV2.get_token", return_value="api-token") def test_init_user(api_mock, k_mock, version_mock): + cli = CliTest() with mock.patch( "sys.argv", - ["sx_api", "--version", "-c", "test-config_param.conf", "-t", "atoken"], + [ + "sxapi", + "--version", + "-c", + "./tests/cli_tests/test-config_param.conf", + "-t", + "atoken", + ], ): cli.run() assert cli_user.api_access_token == "atoken" @@ -98,7 +116,8 @@ def test_init_user(api_mock, k_mock, version_mock): cli_user.public_v2_api = cli_user.integration_v2_api = None with mock.patch( - "sys.argv", ["sx_api", "--version", "-c", "test-config_param.conf", "-k"] + "sys.argv", + ["sxapi", "--version", "-c", "./tests/cli_tests/test-config_param.conf", "-k"], ): cli.run() assert cli_user.api_access_token == "keyring-token" @@ -106,7 +125,8 @@ def test_init_user(api_mock, k_mock, version_mock): cli_user.public_v2_api = cli_user.integration_v2_api = None with mock.patch( - "sys.argv", ["sx_api", "--version", "-c", "test-config_param.conf"] + "sys.argv", + ["sxapi", "--version", "-c", "./tests/cli_tests/test-config_param.conf"], ): os.environ["SMAXTEC_API_ACCESS_TOKEN"] = "env_token" cli.run() @@ -116,7 +136,8 @@ def test_init_user(api_mock, k_mock, version_mock): cli_user.public_v2_api = cli_user.integration_v2_api = None with mock.patch( - "sys.argv", ["sx_api", "--version", "-c", "test-config_param.conf"] + "sys.argv", + ["sxapi", "--version", "-c", "./tests/cli_tests/test-config_param.conf"], ): cli.run() assert cli_user.api_access_token == "api-token" diff --git a/tests/cli_tests/test_cli_animals.py b/tests/cli_tests/test_cli_animals.py new file mode 100644 index 0000000..835c6b8 --- /dev/null +++ b/tests/cli_tests/test_cli_animals.py @@ -0,0 +1,107 @@ +import mock + +from tests import ( + CliUserTest, + ResponseMock, + SxMainTestParser, +) + +test_paser = SxMainTestParser(True) +args_parser = test_paser.parse_args +test_cli_user = CliUserTest() + + +@mock.patch("builtins.print") +@mock.patch("sxapi.cli.parser.main_parser.cli_user", test_cli_user) +@mock.patch("sxapi.cli.parser.subparser.animals.get.cli_user", test_cli_user) +def test_cli_animals_get(print_mock): + + # test output for all animals and organisation_id + test_cli_user.integration_v2_api.mock_get_return_value( + ResponseMock([{"all": "animals"}], 200) + ) + + # check for correct argument parsing + namespace = args_parser(["animals", "get", "--organisation_id", "parent_orga_id"]) + assert namespace.animals_sub_commands == "get" + assert namespace.ids is None + assert namespace.official_ids is False + assert namespace.organisation_id == "parent_orga_id" + assert namespace.limit == 0 + assert namespace.archived is False + + # check if output was correctly printed to stdout + assert print_mock.call_args[0][0] == '[{"all": "animals"}]' + assert test_cli_user.integration_v2_api.get_called_with[0] == { + "kwargs": {"json": {"include_archived": False}}, + "path": "/organisations/parent_orga_id/animals", + } + + test_cli_user.integration_v2_api.reset_mock() + + # test output for multiple animals with id + test_cli_user.public_v2_api.mock_get_return_value( + ResponseMock( + [ + {"animal_id": "1", "archived": True}, + {"animal_id": "2", "archived": False}, + {"animal_id": "3", "archived": True}, + {"animal_id": "4", "archived": True}, + ], + 200, + ) + ) + + # check for correct argument parsing + namespace = args_parser( + ["animals", "get", "--ids", "1", "2", "3", "4", "--limit", "2", "--archived"] + ) + assert namespace.animals_sub_commands == "get" + assert namespace.ids == ["1", "2", "3", "4"] + assert namespace.official_ids is False + assert namespace.organisation_id is None + assert namespace.limit == 2 + assert namespace.archived is True + + # check if output was correctly printed to stdout + assert ( + print_mock.call_args[0][0] + == '[{"animal_id": "1", "archived": true}, {"animal_id": "3", "archived": true}]' + ) + assert test_cli_user.public_v2_api.get_called_with[0] == { + "kwargs": {"json": {"animal_ids": ["1", "2", "3", "4"]}}, + "path": "/animals/by_ids", + } + test_cli_user.public_v2_api.reset_mock() + + # test output for multiple animals with official id and archived + animals_list = [ + {"animal_id": "1", "archived": True}, + {"animal_id": "2", "archived": False}, + {"animal_id": "3", "archived": True}, + {"animal_id": "4", "archived": True}, + ] + test_cli_user.public_v2_api.mock_get_return_value( + ResponseMock(animals_list, 200, iterator=True) + ) + + # check for correct argument parsing + namespace = args_parser( + ["animals", "get", "--ids", "1", "2", "3", "4", "--official-ids"] + ) + assert namespace.animals_sub_commands == "get" + assert namespace.ids == ["1", "2", "3", "4"] + assert namespace.official_ids is True + assert namespace.organisation_id is None + assert namespace.limit == 0 + assert namespace.archived is False + + assert len(test_cli_user.public_v2_api.get_called_with) == 4 + for idx, a in enumerate(animals_list): + assert test_cli_user.public_v2_api.get_called_with[idx] == { + "kwargs": {"json": {}}, + "path": f"/animals/by_official_id/{test_cli_user.organisation_id}/{a['animal_id']}", + } + + # check if output was correctly printed to stdout + assert print_mock.call_args[0][0] == '[{"animal_id": "2", "archived": false}]' diff --git a/tests/cli_tests/test_gsd_subparser.py b/tests/cli_tests/test_gsd_subparser.py deleted file mode 100644 index 7d7aaad..0000000 --- a/tests/cli_tests/test_gsd_subparser.py +++ /dev/null @@ -1,60 +0,0 @@ -import mock -import pytest - -from sxapi.cli.cli import Cli -from sxapi.publicV2 import PublicAPIV2 - -args_parser = Cli.parse_args - - -@mock.patch( - "sxapi.cli.subparser.get_sensor_data.get_sensor_data_from_animal", return_value={} -) -@mock.patch("sxapi.cli.cli_user.check_credentials_set", return_value=True) -@mock.patch("sxapi.cli.cli_user.public_v2_api", PublicAPIV2()) -@pytest.mark.skip() -def test_get_sensor_data_parser(creds_mock, get_data_mock): - namespace = args_parser( - [ - "get_sensor_data", - "12378479238", - "-m", - "act", - "--from_date", - "2012-12-12", - "--to_date", - "2012-12-13", - ] - ) - assert namespace.animal_id == "12378479238" - assert namespace.metrics == ["act"] - assert namespace.from_date == ["2012-12-12"] - assert namespace.to_date == ["2012-12-13"] - - namespace.func(namespace) - assert get_data_mock.call_count == 1 - call_args = get_data_mock.call_args_list[0].kwargs - assert len(call_args) == 5 - assert call_args["animal_id"] == "12378479238" - assert call_args["metrics"] == ["act"] - assert call_args["from_date"] == ["2012-12-12"] - assert call_args["to_date"] == ["2012-12-13"] - assert isinstance(call_args["api"], PublicAPIV2) - - namespace = args_parser( - [ - "get_sensor_data", - "23454", - ] - ) - assert namespace.animal_id == "23454" - - namespace.func(namespace) - assert get_data_mock.call_count == 2 - call_args = get_data_mock.call_args_list[1].kwargs - assert len(call_args) == 5 - assert call_args["animal_id"] == "23454" - assert call_args["metrics"] is None - assert call_args["from_date"] is None - assert call_args["to_date"] is None - assert isinstance(call_args["api"], PublicAPIV2) diff --git a/tests/cli_tests/test_token_subparser.py b/tests/cli_tests/test_token_subparser.py index 53fb900..336e721 100644 --- a/tests/cli_tests/test_token_subparser.py +++ b/tests/cli_tests/test_token_subparser.py @@ -1,14 +1,20 @@ import mock +import pytest +from requests import HTTPError -from sxapi.cli.cli import Cli -from sxapi.cli.subparser.token import ( - handle_clear_token, - handle_new_token, - handle_print_token, - handle_set_token, +from sxapi.cli.parser import main_parser +from sxapi.errors import ( + SxapiAuthorizationError, + SxapiCliArgumentError, ) -args_parser = Cli.parse_args +test_parser = main_parser.SxApiMainParser(True) +args_parser = test_parser.parse_args + + +class MockHTTPError(HTTPError): + def __str__(self): + return "401" @mock.patch("builtins.print") @@ -16,7 +22,6 @@ def test_handle_print_token(_, print_mock): namespace = args_parser(["token", "-p"]) assert namespace.print_token == "ek" - handle_print_token(namespace) assert print_mock.call_count == 1 call_args = print_mock.call_args_list[0] assert call_args.args[0] == "\nKeyring: None\n\nEnvironment: None" @@ -24,7 +29,6 @@ def test_handle_print_token(_, print_mock): namespace = args_parser(["token", "-p", "ek"]) assert namespace.print_token == "ek" - handle_print_token(namespace) assert print_mock.call_count == 1 call_args = print_mock.call_args_list[0] assert call_args.args[0] == "\nKeyring: None\n\nEnvironment: None" @@ -32,7 +36,6 @@ def test_handle_print_token(_, print_mock): namespace = args_parser(["token", "-p", "k"]) assert namespace.print_token == "k" - handle_print_token(namespace) call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 assert call_args.args[0] == "\nKeyring Token: None\n" @@ -40,30 +43,23 @@ def test_handle_print_token(_, print_mock): namespace = args_parser(["token", "-p", "e"]) assert namespace.print_token == "e" - handle_print_token(namespace) call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 assert call_args.args[0] == "\nEnvironment Token: None\n" print_mock.reset_mock() - namespace = args_parser(["token", "-p", "a"]) - assert namespace.print_token == "a" - handle_print_token(namespace) - call_args = print_mock.call_args_list[0] - assert print_mock.call_count == 1 + with pytest.raises(SxapiCliArgumentError) as e: + args_parser(["token", "-p", "a"]) assert ( - call_args.args[0] == "Invalid arguments. Only use 'e' for environment, " - "'k' for keyring or 'ek' for both." + e.value.message + == "Invalid arguments. Only use 'e' for environment, 'k' for keyring or 'ek' for both." ) print_mock.reset_mock() - namespace = args_parser(["token", "-p", "notvalid"]) - assert namespace.print_token == "notvalid" - handle_print_token(namespace) - call_args = print_mock.call_args_list[0] - assert print_mock.call_count == 1 + with pytest.raises(SxapiCliArgumentError) as e: + args_parser(["token", "-p", "notvalid"]) assert ( - call_args.args[0] + e.value.message == "Invalid number of arguments. Use --help for usage information." ) print_mock.reset_mock() @@ -71,10 +67,10 @@ def test_handle_print_token(_, print_mock): @mock.patch("builtins.print") @mock.patch("sxapi.cli.cli_user.set_token_keyring", return_value="api_token") -def test_handle_set_token(cred_mock, print_mock): +@mock.patch("sxapi.publicV2.PublicAPIV2.get_token") +def test_handle_set_token(get_mock, cred_mock, print_mock): namespace = args_parser(["token", "-s", "api_token"]) assert namespace.set_keyring == ["api_token"] - handle_set_token(namespace) call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 assert call_args.args[0] == "Token is stored in keyring!" @@ -84,10 +80,10 @@ def test_handle_set_token(cred_mock, print_mock): @mock.patch("builtins.print") @mock.patch("sxapi.cli.cli_user.clear_token_keyring", return_value="api_token") -def test_handle_clear_token(_, print_mock): +@mock.patch("sxapi.publicV2.PublicAPIV2.get_token") +def test_handle_clear_token(get_mock, user_mock, print_mock): namespace = args_parser(["token", "-c"]) assert namespace.clear_keyring is True - handle_clear_token() call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 assert call_args.args[0] == "Token was deleted from keyring!" @@ -95,36 +91,32 @@ def test_handle_clear_token(_, print_mock): @mock.patch("builtins.print") -@mock.patch("sxapi.cli.subparser.token.getpass.getpass", return_value=None) -@mock.patch("sxapi.cli.cli_user") -def test_handle_new_token(_, getpass_mock, print_mock): +@mock.patch("sxapi.cli.parser.subparser.token.getpass.getpass", return_value=None) +@mock.patch("sxapi.cli.parser.main_parser.cli_user") +@mock.patch( + "sxapi.cli.parser.subparser.token.PublicAPIV2.get_token", + side_effect=MockHTTPError(), +) +def test_handle_new_token(a, user_mock, getpass_mock, print_mock): print_mock.reset_mock() - namespace = args_parser(["token", "-n"]) - assert namespace.new_token is True with mock.patch("builtins.input", lambda _: "marco_no_at_test"): - handle_new_token(namespace) - assert getpass_mock.call_count == 0 - call_args = print_mock.call_args_list[0] - assert print_mock.call_count == 1 - assert call_args.args[0] == "Username must be a email!" + + with pytest.raises(SxapiCliArgumentError) as e: + args_parser(["token", "-n"]) + assert e.value.message == "Username must be a email!" print_mock.reset_mock() - namespace = args_parser(["token", "-n"]) - assert namespace.new_token is True with mock.patch("builtins.input", lambda _: "marco@test"): - handle_new_token(namespace) - assert getpass_mock.call_count == 1 - call_args = print_mock.call_args_list[0] - assert print_mock.call_count == 1 - assert call_args.args[0] == "Username or Password is wrong!" + with pytest.raises(SxapiAuthorizationError) as e: + args_parser(["token", "-n"]) + assert e.value.message == "Username or Password is wrong!" print_mock.reset_mock() with mock.patch("sxapi.publicV2.PublicAPIV2.get_token", return_value="api_token"): - namespace = args_parser(["token", "-n"]) - assert namespace.new_token is True with mock.patch("builtins.input", lambda _: "marco@test"): - handle_new_token(namespace) + namespace = args_parser(["token", "-n"]) + assert namespace.new_token is True assert getpass_mock.call_count == 2 call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 @@ -133,14 +125,13 @@ def test_handle_new_token(_, getpass_mock, print_mock): @mock.patch("builtins.print") -def test_token_subfunc(print_mock): - namespace = args_parser(["token", "-c", "-s", "api_token"]) +@mock.patch("sxapi.publicV2.PublicAPIV2.get_token", return_value="api_token") +def test_token_sub_func(_, print_mock): + with pytest.raises(SxapiCliArgumentError) as e: + args_parser(["token", "-c", "-s", "api_token"]) - namespace.func(namespace) - call_args = print_mock.call_args_list[0] - assert print_mock.call_count == 1 assert ( - call_args.args[0] + e.value.message == "Invalid Combination! Please use just one out of these parameters " "[--print_token, --set_keyring, --new_token, --clear_keyring]" ) diff --git a/tests/test_publicV2/test_get_sensor_data.py b/tests/test_publicV2/test_get_sensor_data.py index fd7bab0..875861d 100644 --- a/tests/test_publicV2/test_get_sensor_data.py +++ b/tests/test_publicV2/test_get_sensor_data.py @@ -17,7 +17,7 @@ def test_get_sensor_data(get_mock, print_mock): print_mock.reset_mock() get_sensor_data_from_animal( - PublicAPIV2(), "1233455", metrics="act", to_date=["21-12-2"] + PublicAPIV2(), "1233455", metrics="act", to_date="21-12-2" ) call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 @@ -28,8 +28,8 @@ def test_get_sensor_data(get_mock, print_mock): PublicAPIV2(), "1233455", metrics="act", - to_date=["2001-12-2"], - from_date=["0334"], + to_date="2001-12-2", + from_date="0334", ) call_args = print_mock.call_args_list[0] assert print_mock.call_count == 1 @@ -40,8 +40,8 @@ def test_get_sensor_data(get_mock, print_mock): PublicAPIV2(), "1233455", metrics="act", - to_date=["2001-12-02"], - from_date=["2001-11-02"], + to_date="2001-12-02", + from_date="2001-11-02", ) get_mock.assert_called_once_with(