diff --git a/fedn/cli/__init__.py b/fedn/cli/__init__.py index be680eb23..f00bb351b 100644 --- a/fedn/cli/__init__.py +++ b/fedn/cli/__init__.py @@ -1,14 +1,15 @@ from .client_cmd import client_cmd # noqa: F401 from .combiner_cmd import combiner_cmd # noqa: F401 from .config_cmd import config_cmd # noqa: F401 +from .controller_cmd import controller_cmd # noqa: F401 from .hooks_cmd import hooks_cmd # noqa: F401 +from .login_cmd import login_cmd # noqa: F401 from .main import main # noqa: F401 from .model_cmd import model_cmd # noqa: F401 from .package_cmd import package_cmd # noqa: F401 +from .project_cmd import project_cmd # noqa: F401 from .round_cmd import round_cmd # noqa: F401 from .run_cmd import run_cmd # noqa: F401 from .session_cmd import session_cmd # noqa: F401 from .status_cmd import status_cmd # noqa: F401 from .validation_cmd import validation_cmd # noqa: F401 -from .controller_cmd import controller_cmd # noqa: F401 -from .login_cmd import login_cmd # noqa: F401 diff --git a/fedn/cli/client_cmd.py b/fedn/cli/client_cmd.py index 7c9ffc1e7..762fbe818 100644 --- a/fedn/cli/client_cmd.py +++ b/fedn/cli/client_cmd.py @@ -1,10 +1,9 @@ import uuid import click -import requests from fedn.cli.main import main -from fedn.cli.shared import CONTROLLER_DEFAULTS, apply_config, get_api_url, get_token, print_response +from fedn.cli.shared import CONTROLLER_DEFAULTS, apply_config, get_response, print_response from fedn.common.exceptions import InvalidClientConfig from fedn.network.clients.client import Client from fedn.network.clients.client_v2 import Client as ClientV2 @@ -49,23 +48,14 @@ def list_clients(ctx, protocol: str, host: str, port: str, token: str = None, n_ - result: list of clients """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="clients") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) + response = get_response(protocol=protocol, host=host, port=port, endpoint="clients", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "clients", None) - if _token: - headers["Authorization"] = _token - - - try: - response = requests.get(url, headers=headers) - print_response(response, "clients", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @click.option("-H", "--host", required=False, default=CONTROLLER_DEFAULTS["host"], help="Hostname of controller (api)") @@ -80,24 +70,8 @@ def get_client(ctx, protocol: str, host: str, port: str, token: str = None, id: - result: client with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="clients") - headers = {} - - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "client", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"clients/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "client", id) @client_cmd.command("start-v1") diff --git a/fedn/cli/combiner_cmd.py b/fedn/cli/combiner_cmd.py index 0a6403587..4df5846a4 100644 --- a/fedn/cli/combiner_cmd.py +++ b/fedn/cli/combiner_cmd.py @@ -1,10 +1,9 @@ import uuid import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, apply_config, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, apply_config, get_response, print_response @main.group("combiner") @@ -77,22 +76,13 @@ def list_combiners(ctx, protocol: str, host: str, port: str, token: str = None, - result: list of combiners """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="combiners") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - try: - response = requests.get(url, headers=headers) - print_response(response, "combiners", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint="combiners", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "combiners", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -108,19 +98,5 @@ def get_combiner(ctx, protocol: str, host: str, port: str, token: str = None, id - result: combiner with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="combiners") - headers = {} - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - try: - response = requests.get(url, headers=headers) - print_response(response, "combiner", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"combiners/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "combiner", id) diff --git a/fedn/cli/login_cmd.py b/fedn/cli/login_cmd.py index d2ce8ac90..91cb49ddf 100644 --- a/fedn/cli/login_cmd.py +++ b/fedn/cli/login_cmd.py @@ -3,9 +3,9 @@ import click import requests -import yaml from .main import main +from .shared import STUDIO_DEFAULTS, get_response, set_context # Replace this with the platform's actual login endpoint home_dir = os.path.expanduser("~") @@ -19,19 +19,26 @@ def login_cmd(ctx): @login_cmd.command("login") -@click.option("-p", "--protocol", required=False, default="https", help="Communication protocol") -@click.option("-H", "--host", required=False, default="fedn.scaleoutsystems.com", help="Hostname of controller (api)") +@click.option("-u", "--username", required=False, default=None, help="username in studio") +@click.option("-P", "--password", required=False, default=None, help="password in studio") +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") @click.pass_context -def login_cmd(ctx, protocol: str, host: str): - """Logging into FEDn Studio""" +def login_cmd(ctx, protocol: str, host: str, username: str, password: str): + """Login to FEDn Studio""" # Step 1: Display welcome message click.secho("Welcome to Scaleout FEDn!", fg="green") url = f"{protocol}://{host}/api/token/" # Step 3: Prompt for username and password - username = input("Please enter your username: ") - password = getpass("Please enter your password: ") + if username is None and password is None: + username = input("Please enter your username: ") + password = getpass("Please enter your password: ") + elif password is None: + password = getpass("Please enter your password: ") + else: + username = input("Please enter your username: ") # Call the authentication API try: @@ -44,18 +51,68 @@ def login_cmd(ctx, protocol: str, host: str): # Handle the response if response.status_code == 200: - data = response.json() - if data.get("access"): - click.secho("Login successful!", fg="green") - context_path = os.path.join(home_dir, ".fedn") - if not os.path.exists(context_path): - os.makedirs(context_path) - try: - with open(f"{context_path}/context.yaml", "w") as yaml_file: - yaml.dump(data, yaml_file, default_flow_style=False) # Add access and refresh tokens to context yaml file - except Exception as e: - print(f"Error: Failed to write to YAML file. Details: {e}") + context_data = get_context(response, protocol, host) + + context_path = os.path.join(home_dir, ".fedn") + if not os.path.exists(context_path): + os.makedirs(context_path) + set_context(context_path, context_data) + else: + click.secho(f"Unexpected error: {response.status_code}", fg="red") + + +# Sets the context for a given user +def get_context(response, protocol: str, host: str): + """Generates content for context file with the following data: + User tokens: access and refresh token to authenticate user towards Studio + Active project tokens: access and refresh token to authenticate user towards controller + Active project id: slug of active project + Active project url: controller url of active project + """ + context_data = {"User tokens": {}, "Active project tokens": {}, "Active project id": {}, "Active project url": {}} + user_token_data = response.json() + if user_token_data.get("access"): + context_data["User tokens"] = user_token_data + studio_api = True + headers_projects = {} + user_access_token = user_token_data.get("access") + response_projects = get_response( + protocol=protocol, + host=host, + port=None, + endpoint="projects", + token=user_access_token, + headers=headers_projects, + usr_api=studio_api, + usr_token=True, + ) + if response_projects.status_code == 200: + projects_response_json = response_projects.json() + if len(projects_response_json) > 0: + id = projects_response_json[0].get("slug") + context_data["Active project id"] = id + headers_projects["X-Project-Slug"] = id + response_project_tokens = get_response( + protocol=protocol, + host=host, + port=None, + endpoint="admin-token", + token=user_access_token, + headers=headers_projects, + usr_api=studio_api, + usr_token=False, + ) + if response_project_tokens.status_code == 200: + project_tokens = response_project_tokens.json() + context_data["Active project tokens"] = project_tokens + controller_url = f"{protocol}://{host}/{id}-fedn-reducer" + context_data["Active project url"] = controller_url + click.secho("Login successful!", fg="green") + else: + click.secho(f"Unexpected error: {response_project_tokens.status_code}", fg="red") else: - click.secho("Login failed. Please check your credentials.", fg="red") + click.secho(f"Unexpected error: {response_projects.status_code}", fg="red") else: - click.secho(f"Unexpected error: {response.text}", fg="red") + click.secho("Login failed. Please check your credentials.", fg="red") + + return context_data diff --git a/fedn/cli/model_cmd.py b/fedn/cli/model_cmd.py index 2e522e5a1..ae1dafe32 100644 --- a/fedn/cli/model_cmd.py +++ b/fedn/cli/model_cmd.py @@ -1,15 +1,13 @@ import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("model") @click.pass_context def model_cmd(ctx): - """:param ctx: - """ + """:param ctx:""" pass @@ -17,7 +15,7 @@ def model_cmd(ctx): @click.option("-H", "--host", required=False, default=CONTROLLER_DEFAULTS["host"], help="Hostname of controller (api)") @click.option("-P", "--port", required=False, default=CONTROLLER_DEFAULTS["port"], help="Port of controller (api)") @click.option("-t", "--token", required=False, help="Authentication token") -@click.option("-session_id", "--session_id", required=False, help="models in session with given session id") +@click.option("-s", "--session_id", required=False, help="models in session with given session id") @click.option("--n_max", required=False, help="Number of items to list") @model_cmd.command("list") @click.pass_context @@ -28,28 +26,18 @@ def list_models(ctx, protocol: str, host: str, port: str, token: str = None, ses - result: list of models """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="models") - - headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - if session_id: - url = f"{url}?session_id={session_id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "models", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response( + protocol=protocol, host=host, port=port, endpoint=f"models/?session_id={session_id}", token=token, headers=headers, usr_api=False, usr_token=False + ) + else: + response = get_response(protocol=protocol, host=host, port=port, endpoint="models", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "models", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -65,23 +53,5 @@ def get_model(ctx, protocol: str, host: str, port: str, token: str = None, id: s - result: model with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="models") - - - headers = {} - - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "model", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"models/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "model", id) diff --git a/fedn/cli/package_cmd.py b/fedn/cli/package_cmd.py index b8a130f68..e6ec14329 100644 --- a/fedn/cli/package_cmd.py +++ b/fedn/cli/package_cmd.py @@ -2,19 +2,17 @@ import tarfile import click -import requests from fedn.common.log_config import logger from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("package") @click.pass_context def package_cmd(ctx): - """:param ctx: - """ + """:param ctx:""" pass @@ -55,23 +53,13 @@ def list_packages(ctx, protocol: str, host: str, port: str, token: str = None, n - result: list of packages """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="packages") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - - try: - response = requests.get(url, headers=headers) - print_response(response, "packages", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint="packages", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "packages", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -87,21 +75,5 @@ def get_package(ctx, protocol: str, host: str, port: str, token: str = None, id: - result: package with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="packages") - headers = {} - - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "package", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"packages/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "package", id) diff --git a/fedn/cli/project_cmd.py b/fedn/cli/project_cmd.py new file mode 100644 index 000000000..111f226a4 --- /dev/null +++ b/fedn/cli/project_cmd.py @@ -0,0 +1,220 @@ +import os + +import click +import requests + +from .main import main +from .shared import STUDIO_DEFAULTS, get_api_url, get_context, get_response, get_token, print_response, set_context + +home_dir = os.path.expanduser("~") + + +@main.group("project") +@click.pass_context +def project_cmd(ctx): + """:param ctx:""" + pass + + +@click.option("-id", "--id", required=True, help="ID of project.") +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") +@project_cmd.command("delete") +@click.pass_context +def delete_project(ctx, id: str = None, protocol: str = None, host: str = None): + """Delete project with given ID.""" + # Check if project with given id exists + studio_api = True + + response = get_response(protocol=protocol, host=host, port=None, endpoint=f"projects/{id}", token=None, headers={}, usr_api=studio_api, usr_token=False) + if response.status_code == 200: + if response.json().get("error"): + click.secho(f"No project with id '{id}' exists.", fg="red") + else: + # Check if user wants to delete project with given id + user_input = input(f"Are you sure you want to delete project with id {id} (y/n)?: ") + if user_input == "y": + url = get_api_url(protocol=protocol, host=host, port=None, endpoint=f"projects/delete/{id}", usr_api=studio_api) + headers = {} + + _token = get_token(None, True) + + if _token: + headers["Authorization"] = _token + # Call the authentication API + try: + requests.delete(url, headers=headers) + click.secho(f"Project with slug {id} has been removed.", fg="green") + except requests.exceptions.RequestException as e: + click.echo(str(e), fg="red") + activate_project(None, protocol, host) + else: + click.secho(f"Unexpected error: {response.status_code}", fg="red") + + +@click.option("-n", "--name", required=False, default=None, help="Name of new projec.") +@click.option("-d", "--description", required=False, default=None, help="Description of new projec.") +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") +@project_cmd.command("create") +@click.pass_context +def create_project(ctx, name: str = None, description: str = None, protocol: str = None, host: str = None): + """Create project. + :param ctx: + """ + # Check if user can create project + studio_api = True + url = get_api_url(protocol=protocol, host=host, port=None, endpoint="projects/create", usr_api=studio_api) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + _token = get_token(None, True) + + if _token: + headers["Authorization"] = _token + if name is None: + name = input("Please enter a project name: ") + if description is None: + description = input("Please enter a project description (optional): ") + if len(name) > 46 or len(description) >= 255: + click.secho("Project name or description too long.", fg="red") + else: + # Call the authentication API + try: + requests.post(url, data={"name": name, "description": description}, headers=headers) + except requests.exceptions.RequestException as e: + click.secho(str(e), fg="red") + click.secho("Project created.", fg="green") + + +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") +@project_cmd.command("list") +@click.pass_context +def list_projects(ctx, protocol: str = None, host: str = None): + """Return: + ------ + - result: list of projects + + """ + studio_api = True + + response = get_response(protocol=protocol, host=host, port=None, endpoint="projects", token=None, headers={}, usr_api=studio_api, usr_token=True) + + if response.status_code == 200: + response_json = response.json() + if len(response_json) > 0: + context_path = os.path.join(home_dir, ".fedn") + context_data = get_context(context_path) + active_project = context_data.get("Active project id") + + for i in response_json: + project_name = i.get("slug") + if project_name == active_project: + click.secho(f"{project_name} (active)", fg="green") + else: + click.secho(project_name) + else: + click.secho(f"Unexpected error: {response.status_code}", fg="red") + + +@click.option("-id", "--id", required=True, help="ID of project.") +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") +@project_cmd.command("get") +@click.pass_context +def get_project(ctx, id: str = None, protocol: str = None, host: str = None): + """Return: + ------ + - result: project with given id + + """ + studio_api = True + + response = get_response(protocol=protocol, host=host, port=None, endpoint=f"projects/{id}", token=None, headers={}, usr_api=studio_api, usr_token=False) + + if response.status_code == 200: + response_json = response.json() + + if response_json.get("error"): + click.secho(f"No project with id '{id}' exists.", fg="red") + else: + print_response(response, "project", True) + else: + click.secho(f"Unexpected error: {response.status_code}", fg="red") + + +@click.option("-id", "--id", required=True, help="id name of project.") +@click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") +@click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") +@project_cmd.command("set-context") +@click.pass_context +def set_active_project(ctx, id: str = None, protocol: str = None, host: str = None): + """Set active project. + + :param ctx: + :param id: + """ + activate_project(id, protocol, host) + + +def activate_project(id: str = None, protocol: str = None, host: str = None): + """Sets project with give ID as active by updating context file.""" + studio_api = True + headers_projects = {} + context_path = os.path.join(home_dir, ".fedn") + context_data = get_context(context_path) + + user_access_token = context_data.get("User tokens").get("access") + + response_projects = get_response( + protocol=protocol, host=host, port=None, endpoint="projects", token=user_access_token, headers=headers_projects, usr_api=studio_api, usr_token=False + ) + if response_projects.status_code == 200: + projects_response_json = response_projects.json() + if len(projects_response_json) > 0: + if id is None: + headers_projects["X-Project-Slug"] = projects_response_json[0].get("slug") + id = projects_response_json[0].get("slug") + else: + for i in projects_response_json: + if i.get("slug") == id: + headers_projects["X-Project-Slug"] = i.get("slug") + + controller_url = f"{protocol}://{host}/{id}-fedn-reducer" + + response_project_tokens = get_response( + protocol=protocol, + host=host, + port=None, + endpoint="admin-token", + token=user_access_token, + headers=headers_projects, + usr_api=studio_api, + usr_token=False, + ) + if response_project_tokens.status_code == 200: + project_tokens = response_project_tokens.json() + context_data["Active project tokens"] = project_tokens + context_data["Active project id"] = id + context_data["Active project url"] = controller_url + + set_context(context_path, context_data) + + click.secho(f"Project with slug {id} is now active.", fg="green") + else: + click.secho(f"Unexpected error: {response_project_tokens.status_code}", fg="red") + else: + click.echo("Set current context.") + else: + click.secho(f"Unexpected error: {response_projects.status_code}", fg="red") + + +def no_project_exists(response) -> bool: + """Returns true if no project exists.""" + response_json = response.json() + print(response_json) + if type(response_json) is list: + return False + elif response_json.get("error"): + return True + return False diff --git a/fedn/cli/round_cmd.py b/fedn/cli/round_cmd.py index 2f889fef3..71c68df69 100644 --- a/fedn/cli/round_cmd.py +++ b/fedn/cli/round_cmd.py @@ -1,22 +1,20 @@ import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("round") @click.pass_context def round_cmd(ctx): - """:param ctx: - """ + """:param ctx:""" pass @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @click.option("-H", "--host", required=False, default=CONTROLLER_DEFAULTS["host"], help="Hostname of controller (api)") @click.option("-P", "--port", required=False, default=CONTROLLER_DEFAULTS["port"], help="Port of controller (api)") -@click.option("-session_id", "--session_id", required=False, help="Rounds in session with given session id") +@click.option("-s", "--session_id", required=False, help="Rounds in session with given session id") @click.option("-t", "--token", required=False, help="Authentication token") @click.option("--n_max", required=False, help="Number of items to list") @round_cmd.command("list") @@ -28,28 +26,34 @@ def list_rounds(ctx, protocol: str, host: str, port: str, token: str = None, ses - result: list of rounds """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="rounds") - headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - if session_id: - url = f"{url}?round_config.session_id={session_id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "rounds", None) - - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response( + protocol=protocol, + host=host, + port=port, + endpoint=f"rounds/?round_config.session_id={session_id}", + token=token, + headers=headers, + usr_api=False, + usr_token=False, + ) + else: + response = get_response( + protocol=protocol, + host=host, + port=port, + endpoint="rounds", + token=token, + headers=headers, + usr_api=False, + usr_token=False, + ) + print_response(response, "rounds", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -65,23 +69,5 @@ def get_round(ctx, protocol: str, host: str, port: str, token: str = None, id: s - result: round with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="rounds") - - headers = {} - - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "round", id) - - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"rounds/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "round", id) diff --git a/fedn/cli/session_cmd.py b/fedn/cli/session_cmd.py index a0f1e64c3..4a1624f4d 100644 --- a/fedn/cli/session_cmd.py +++ b/fedn/cli/session_cmd.py @@ -1,15 +1,13 @@ import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("session") @click.pass_context def session_cmd(ctx): - """:param ctx: - """ + """:param ctx:""" pass @@ -27,23 +25,13 @@ def list_sessions(ctx, protocol: str, host: str, port: str, token: str = None, n - result: list of sessions """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="sessions") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - - try: - response = requests.get(url, headers=headers) - print_response(response, "sessions", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint="sessions", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "sessions", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -59,20 +47,5 @@ def get_session(ctx, protocol: str, host: str, port: str, token: str = None, id: - result: session with given session id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="sessions") - headers = {} - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "session", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"sessions/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "session", id) diff --git a/fedn/cli/shared.py b/fedn/cli/shared.py index 21fa2b072..f463b5554 100644 --- a/fedn/cli/shared.py +++ b/fedn/cli/shared.py @@ -1,12 +1,15 @@ import os import click +import requests import yaml from fedn.common.log_config import logger CONTROLLER_DEFAULTS = {"protocol": "http", "host": "localhost", "port": 8092, "debug": False} +STUDIO_DEFAULTS = {"protocol": "https", "host": "fedn.scaleoutstudio.com"} + COMBINER_DEFAULTS = {"discover_host": "localhost", "discover_port": 8092, "host": "localhost", "port": 12080, "name": "combiner", "max_clients": 30} CLIENT_DEFAULTS = { @@ -16,6 +19,8 @@ API_VERSION = "v1" +home_dir = os.path.expanduser("~") + def apply_config(path: str, config: dict): """Parse client config from file. @@ -35,24 +40,47 @@ def apply_config(path: str, config: dict): config[key] = val -def get_api_url(protocol: str, host: str, port: str, endpoint: str) -> str: - _url = os.environ.get("FEDN_CONTROLLER_URL") +def get_api_url(protocol: str, host: str, port: str, endpoint: str, usr_api: bool) -> str: + if usr_api: + _url = os.environ.get("FEDN_STUDIO_URL") + _protocol = protocol or os.environ.get("FEDN_STUDIO_PROTOCOL") or STUDIO_DEFAULTS["protocol"] + _host = host or os.environ.get("FEDN_STUDIO_HOST") or STUDIO_DEFAULTS["host"] - if _url: - return f"{_url}/api/{API_VERSION}/{endpoint}/" + if _url is None: + return f"{_protocol}://{_host}/api/{API_VERSION}/{endpoint}" + + return f"{_url}/api/{API_VERSION}/{endpoint}" + else: + _url = os.environ.get("FEDN_CONTROLLER_URL") + _protocol = protocol or os.environ.get("FEDN_CONTROLLER_PROTOCOL") or CONTROLLER_DEFAULTS["protocol"] + _host = host or os.environ.get("FEDN_CONTROLLER_HOST") or CONTROLLER_DEFAULTS["host"] + _port = port or os.environ.get("FEDN_CONTROLLER_PORT") or CONTROLLER_DEFAULTS["port"] - _protocol = protocol or os.environ.get("FEDN_CONTROLLER_PROTOCOL") or CONTROLLER_DEFAULTS["protocol"] - _host = host or os.environ.get("FEDN_CONTROLLER_HOST") or CONTROLLER_DEFAULTS["host"] - _port = port or os.environ.get("FEDN_CONTROLLER_PORT") or CONTROLLER_DEFAULTS["port"] + if _url is None: + context_path = os.path.join(home_dir, ".fedn") + try: + context_data = get_context(context_path) + _url = context_data.get("Active project url") + except Exception as e: + click.echo(f"Encountered error {e}. Make sure you are logged in and have activated a project. Using controller defaults instead.", fg="red") + _url = f"{_protocol}://{_host}:{_port}" - return f"{_protocol}://{_host}:{_port}/api/{API_VERSION}/{endpoint}/" + return f"{_url}/api/{API_VERSION}/{endpoint}" -def get_token(token: str) -> str: +def get_token(token: str, usr_token: bool) -> str: _token = token or os.environ.get("FEDN_AUTH_TOKEN", None) if _token is None: - return None + context_path = os.path.join(home_dir, ".fedn") + try: + context_data = get_context(context_path) + if usr_token: + _token = context_data.get("User tokens").get("access") + else: + _token = context_data.get("Active project tokens").get("access") + except Exception as e: + click.secho(f"Encountered error {e}. Make sure you are logged in and have activated a project.", fg="red") scheme = os.environ.get("FEDN_AUTH_SCHEME", "Bearer") @@ -99,3 +127,36 @@ def print_response(response, entity_name: str, so): click.echo(f'Error: {json_data["message"]}') else: click.echo(f"Error: {response.status_code}") + + +def set_context(context_path, context_data): + """Saves context data as yaml file in given path""" + try: + with open(f"{context_path}/context.yaml", "w") as yaml_file: + yaml.dump(context_data, yaml_file, default_flow_style=False) + except Exception as e: + print(f"Error: Failed to write to YAML file. Details: {e}") + + +def get_context(context_path): + """Retrieves context data from yaml file in given path""" + try: + with open(f"{context_path}/context.yaml", "r") as yaml_file: + context_data = yaml.safe_load(yaml_file) + except Exception as e: + print(f"Error: Failed to write to YAML file. Details: {e}") + return context_data + + +def get_response(protocol: str, host: str, port: str, endpoint: str, token: str, headers: dict, usr_api: bool, usr_token: str): + """Utility function to retrieve response from get request based on provided information.""" + url = get_api_url(protocol=protocol, host=host, port=port, endpoint=endpoint, usr_api=usr_api) + + _token = get_token(token=token, usr_token=usr_token) + + if _token: + headers["Authorization"] = _token + + response = requests.get(url, headers=headers) + + return response diff --git a/fedn/cli/status_cmd.py b/fedn/cli/status_cmd.py index 9b751f65b..56fd22801 100644 --- a/fedn/cli/status_cmd.py +++ b/fedn/cli/status_cmd.py @@ -1,8 +1,7 @@ import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("status") @@ -16,7 +15,7 @@ def status_cmd(ctx): @click.option("-H", "--host", required=False, default=CONTROLLER_DEFAULTS["host"], help="Hostname of controller (api)") @click.option("-P", "--port", required=False, default=CONTROLLER_DEFAULTS["port"], help="Port of controller (api)") @click.option("-t", "--token", required=False, help="Authentication token") -@click.option("-session_id", "--session_id", required=False, help="statuses with given session id") +@click.option("-s", "--session_id", required=False, help="statuses with given session id") @click.option("--n_max", required=False, help="Number of items to list") @status_cmd.command("list") @click.pass_context @@ -27,26 +26,18 @@ def list_statuses(ctx, protocol: str, host: str, port: str, token: str = None, s - result: list of statuses """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="statuses") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - if session_id: - url = f"{url}?sessionId={session_id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "statuses", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response( + protocol=protocol, host=host, port=port, endpoint=f"statuses/?sessionId={session_id}", token=token, headers=headers, usr_api=False, usr_token=False + ) + else: + response = get_response(protocol=protocol, host=host, port=port, endpoint="statuses", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "statuses", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -62,21 +53,5 @@ def get_status(ctx, protocol: str, host: str, port: str, token: str = None, id: - result: status with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="statuses") - headers = {} - - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "status", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"statuses{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "status", id) diff --git a/fedn/cli/validation_cmd.py b/fedn/cli/validation_cmd.py index b7417af5e..37600deb4 100644 --- a/fedn/cli/validation_cmd.py +++ b/fedn/cli/validation_cmd.py @@ -1,15 +1,13 @@ import click -import requests from .main import main -from .shared import CONTROLLER_DEFAULTS, get_api_url, get_token, print_response +from .shared import CONTROLLER_DEFAULTS, get_response, print_response @main.group("validation") @click.pass_context def validation_cmd(ctx): - """:param ctx: - """ + """:param ctx:""" pass @@ -17,7 +15,7 @@ def validation_cmd(ctx): @click.option("-H", "--host", required=False, default=CONTROLLER_DEFAULTS["host"], help="Hostname of controller (api)") @click.option("-P", "--port", required=False, default=CONTROLLER_DEFAULTS["port"], help="Port of controller (api)") @click.option("-t", "--token", required=False, help="Authentication token") -@click.option("-session_id", "--session_id", required=False, help="validations in session with given session id") +@click.option("-s", "--session_id", required=False, help="validations in session with given session id") @click.option("--n_max", required=False, help="Number of items to list") @validation_cmd.command("list") @click.pass_context @@ -28,26 +26,25 @@ def list_validations(ctx, protocol: str, host: str, port: str, token: str = None - result: list of validations """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="validations") headers = {} if n_max: headers["X-Limit"] = n_max - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - if session_id: - url = f"{url}?sessionId={session_id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "validations", None) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response( + protocol=protocol, + host=host, + port=port, + endpoint=f"validations/?sessionId={session_id}", + token=token, + headers=headers, + usr_api=False, + usr_token=False, + ) + else: + response = get_response(protocol=protocol, host=host, port=port, endpoint="validations", token=token, headers=headers, usr_api=False, usr_token=False) + print_response(response, "validations", None) @click.option("-p", "--protocol", required=False, default=CONTROLLER_DEFAULTS["protocol"], help="Communication protocol of controller (api)") @@ -63,20 +60,5 @@ def get_validation(ctx, protocol: str, host: str, port: str, token: str = None, - result: validation with given id """ - url = get_api_url(protocol=protocol, host=host, port=port, endpoint="validations") - headers = {} - - _token = get_token(token) - - if _token: - headers["Authorization"] = _token - - if id: - url = f"{url}{id}" - - - try: - response = requests.get(url, headers=headers) - print_response(response, "validation", id) - except requests.exceptions.ConnectionError: - click.echo(f"Error: Could not connect to {url}") + response = get_response(protocol=protocol, host=host, port=port, endpoint=f"validations/{id}", token=token, headers={}, usr_api=False, usr_token=False) + print_response(response, "validation", id)