diff --git a/sdk/atriumdb/atrium_sdk.py b/sdk/atriumdb/atrium_sdk.py index 68e4ae1b..538586a2 100644 --- a/sdk/atriumdb/atrium_sdk.py +++ b/sdk/atriumdb/atrium_sdk.py @@ -114,7 +114,7 @@ class AtriumSDK: >>> sdk = AtriumSDK(dataset_location="./example_dataset", metadata_connection_type=metadata_connection_type, connection_params=connection_params) >>> # Remote API Mode - >>> api_url = "http://example.com/api/v1" + >>> api_url = "http://example.com/v1" >>> token = "4e78a93749ead7893" >>> metadata_connection_type = "api" >>> sdk = AtriumSDK(api_url=api_url, token=token, metadata_connection_type=metadata_connection_type) diff --git a/sdk/atriumdb/cli/atriumdb_cli.py b/sdk/atriumdb/cli/atriumdb_cli.py index ed237fd6..b0fb2915 100644 --- a/sdk/atriumdb/cli/atriumdb_cli.py +++ b/sdk/atriumdb/cli/atriumdb_cli.py @@ -24,10 +24,11 @@ import urllib3 import os import time +from urllib.parse import urlparse from auth0.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier -from dotenv import load_dotenv +from dotenv import load_dotenv, set_key, get_key from atriumdb import AtriumSDK from atriumdb.adb_functions import parse_metadata_uri @@ -43,7 +44,8 @@ _LOGGER = logging.getLogger(__name__) -load_dotenv(dotenv_path="./.env", override=True) +dotenv_path = "./.env" +load_dotenv(dotenv_path=dotenv_path, override=True) cli_help_text = """ The atriumdb command is a command line interface for the Atrium database, @@ -95,11 +97,26 @@ def cli(ctx, dataset_location, metadata_uri, database_type, endpoint_url, api_to @click.command(help="Endpoint to login with QR code.") +@click.option("--endpoint-url", type=str, required=True, help="The endpoint to connect to for a remote AtriumDB server") @click.pass_context -def login(ctx): - endpoint_url = ctx.obj["endpoint_url"] +def login(ctx, endpoint_url): + parsed_url = urlparse(endpoint_url) + + # Check if a port is included in the URL + if parsed_url.port: + base_url = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}" + else: + base_url = f"{parsed_url.scheme}://{parsed_url.hostname}" - endpoint_url = endpoint_url.rstrip('/') + path = parsed_url.path.rstrip('/') + + # Construct the final endpoint URL + endpoint_url = f"{base_url}{path}" + + # Write endpoint URL to .env file + load_dotenv(dotenv_path=dotenv_path) + set_key(dotenv_path, "ATRIUMDB_ENDPOINT_URL", endpoint_url) + load_dotenv(dotenv_path=dotenv_path, override=True) auth_conf_res = requests.get(f'{endpoint_url}/auth/cli/code') @@ -148,8 +165,12 @@ def validate_token(id_token): } authenticated = False - while not authenticated: - # click.echo('Checking if the user completed the flow...') + + # Calculate absolute expiration time + expires_in = time.time() + device_code_data['expires_in'] + expiration_time = time.time() + int(device_code_data['expires_in']) + + while not authenticated and time.time() < expiration_time: token_response = requests.post(f'https://{auth0_domain}/oauth/token', data=token_payload) token_data = token_response.json() @@ -160,8 +181,11 @@ def validate_token(id_token): authenticated = True - set_env_var_in_dotenv("ATRIUMDB_API_TOKEN", token_data['access_token']) - set_env_var_in_dotenv("ATRIUMDB_DATABASE_TYPE", "api") + load_dotenv(dotenv_path=dotenv_path) + set_key(dotenv_path, "ATRIUMDB_API_TOKEN", token_data['access_token']) + set_key(dotenv_path, "ATRIUMDB_DATABASE_TYPE", "api") + load_dotenv(dotenv_path=dotenv_path, override=True) + click.echo("Your API Token is:\n") click.echo(token_data['access_token']) click.echo("The variable ATRIUMDB_API_TOKEN has been set in your .env file and") @@ -173,6 +197,58 @@ def validate_token(id_token): else: time.sleep(device_code_data['interval']) + # Check if the authentication process timed out + if not authenticated: + click.echo("Authentication Request Timed-Out") + + +@click.command(help="Refresh the API token using the stored endpoint URL.") +@click.pass_context +def refresh_token(ctx): + # Load .env file + load_dotenv(dotenv_path=dotenv_path) + + # Check if endpoint URL is set + endpoint_url = get_key(dotenv_path, "ATRIUMDB_ENDPOINT_URL") + + if not endpoint_url: + click.echo("Endpoint URL not set. Please use 'atriumdb login --endpoint-url MY_URL'.") + exit(1) + + # Call the login function with the stored endpoint URL + ctx.invoke(login, endpoint_url=endpoint_url) + + +@click.command(help="Displays the current CLI configuration.") +@click.pass_context +def config(ctx): + # Retrieve the configuration from the context + config_data = { + "Endpoint URL": "Not Set" if not ctx.obj.get("endpoint_url") else ctx.obj.get("endpoint_url"), + "API Token": "Not Set" if not ctx.obj.get("api_token") else ctx.obj.get("api_token"), + "Dataset Location": "Not Set" if not ctx.obj.get("dataset_location") else ctx.obj.get("dataset_location"), + "Metadata URI": "Not Set" if not ctx.obj.get("metadata_uri") else ctx.obj.get("metadata_uri"), + "Database Type": "Not Set" if not ctx.obj.get("database_type") else ctx.obj.get("database_type"), + } + + if config_data["API Token"] != "Not Set": + config_data["API Token"] = config_data["API Token"][:15] + "..." + + # Determine the mode (remote or local) + mode = "Remote" if config_data["Database Type"] == "api" else "Local" + + # Prepare data for tabulation + data = [ + ["Mode", mode], + *config_data.items() + ] + + # Print the tabulated data + click.echo(tabulate(data, headers=["Configuration", "Value"], tablefmt="grid")) + if config_data["API Token"] != "Not Set": + click.echo("Full API Token:") + click.echo(ctx.obj.get("api_token", "Not Set")) + @click.command() @click.pass_context @@ -459,6 +535,8 @@ def patient_ls(ctx, skip, limit, age_years_min, age_years_max, gender, source_id cli.add_command(atriumdb_measure) cli.add_command(atriumdb_device) cli.add_command(login) +cli.add_command(refresh_token) +cli.add_command(config) def set_env_var_in_dotenv(name, value): diff --git a/sdk/docs/source/cli.rst b/sdk/docs/source/cli.rst index 17bedc64..8b779705 100644 --- a/sdk/docs/source/cli.rst +++ b/sdk/docs/source/cli.rst @@ -42,41 +42,52 @@ Once installed, you can access the CLI by running the `atriumdb` command. For he Authentication ========================= -To use the CLI for authentication and remote access, you will need to install the `atriumdb` package with the `cli` and `remote` optional dependency. +To use the AtriumDB CLI for authentication and remote access, you only need to specify the `endpoint-url` when logging in. The CLI will now automatically detect the URL from the last login for all subsequent commands. + +To log in and set the endpoint URL, simply use the `atriumdb login` command with the `--endpoint-url` option. This option now supports login with an optional port: .. code-block:: bash - pip install atriumdb[cli,remote] + # Login with a port + atriumdb login --endpoint-url "https://example.com:443/v1" + + # Login without specifying a port + atriumdb login --endpoint-url "https://example.com/v1" -You can then use the `atriumdb` CLI to set the endpoint URL and log in to the remote API. +After logging in, the authentication credentials, including the API token and endpoint URL, are securely stored. If you need to refresh your token, you can now do so with the `atriumdb refresh-token` command, which uses the stored endpoint URL: .. code-block:: bash - atriumdb --endpoint-url http://example.com/api/v1 login + atriumdb refresh-token + +To view your current CLI configuration, including the endpoint URL and API token, use the `atriumdb config` command: -If the endpoint URL is already set in the .env file or as an environment variable, you can simply log in like this: +.. code-block:: bash -Create a file named `.env` in the same directory as your script and add the following content: + atriumdb config -.. code-block:: ini +Authentication Timeout +----------------------- - ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1 +If a certain period of inactivity passes or the CLI detects that the authentication has timed out, the CLI will prompt you to reauthenticate using the `atriumdb refresh-token` or re-run the `atriumdb login` command with your endpoint URL. -Now, you can log in using the CLI: +Modifying the `.env` File +--------------------------- -.. code-block:: bash +Directly editing the `.env` file is no longer recommended. The AtriumDB CLI will manage all necessary environment variables for you, ensuring secure and effective handling of authentication credentials. - atriumdb login +Connecting to an Existing Dataset +---------------------------------- -After logging in, the `atriumdb` CLI will store the API token in the `.env` file. You can update your `.env` file to include the API token as well: +After successfully logging in using the updated methods described above, you can continue to use the AtriumSDK in remote mode with any Python scripts. The synchronization with your current login state occurs automatically. -.. code-block:: ini +Remember that while instantiating the `AtriumSDK`, there's no need to explicitly provide the API token, as it will be read from the stored credentials: - ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1 - ATRIUMDB_API_TOKEN=4e78a93749ead7893 +.. code-block:: python -Now, you can access the remote dataset using the AtriumSDK object, as shown in the "Connecting to an Existing Dataset" section. + sdk = AtriumSDK(metadata_connection_type="api", api_url="https://example.com/v1") + # The SDK will now use the stored API token from the last successful login. ======================================================== Using AtriumSDK in Remote Mode with CLI Authentication @@ -100,7 +111,7 @@ To use the AtriumSDK in remote mode, follow these steps: .. code-block:: python - sdk = AtriumSDK(metadata_connection_type="api", api_url="http://example.com/api/v1") + sdk = AtriumSDK(metadata_connection_type="api", api_url="http://example.com/v1") By setting `metadata_connection_type` to `"api"`, the AtriumSDK will automatically detect and use the API token stored in the `.env` file for remote API calls (alternatively you can specify the token in the `token` parameter). diff --git a/sdk/docs/source/quickstart.rst b/sdk/docs/source/quickstart.rst index 3cbc2a5e..052e6a6f 100644 --- a/sdk/docs/source/quickstart.rst +++ b/sdk/docs/source/quickstart.rst @@ -53,7 +53,7 @@ If it's a MariaDB dataset you will also have to specify the connection parameter sdk = AtriumSDK(dataset_location=dataset_location, metadata_connection_type="mysql", connection_params=connection_params) # Connect to a remote dataset using the API - api_url = "http://example.com/api/v1" + api_url = "http://example.com/v1" token = "4e78a93749ead7893" sdk = AtriumSDK(api_url=api_url, token=token, metadata_connection_type="api") @@ -130,28 +130,22 @@ You can then use the `atriumdb` CLI to set the endpoint URL and log in to the re .. code-block:: bash - atriumdb --endpoint-url http://example.com/api/v1 login + atriumdb login --endpoint-url "https://example.com/v1" -If the endpoint URL is already set in the .env file or as an environment variable, you can simply log in like this: - -Create a file named `.env` in the same directory as your script and add the following content: +This command, after authenticating your API connection, will save your URL, token, auth expiration time, and connection mode in the `.env`: .. code-block:: ini - ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1 + ATRIUMDB_ENDPOINT_URL=https://example.com/v1 + ATRIUMDB_API_TOKEN='aBcD012345eFgHI' + ATRIUMDB_AUTH_EXPIRATION_TIME=1234567890.1234567 + ATRIUMDB_DATABASE_TYPE='api' -Now, you can log in using the CLI: +Once these variables have been set after running `login`, you can refresh the token using: .. code-block:: bash - atriumdb login - -After logging in, the `atriumdb` CLI will store the API token in the `.env` file. You can update your `.env` file to include the API token as well: - -.. code-block:: ini - - ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1 - ATRIUMDB_API_TOKEN=4e78a93749ead7893 + atriumdb refresh-token Now, you can access the remote dataset using the AtriumSDK object, as shown in the "Connecting to an Existing Dataset" section. diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 2579cea2..24bfe789 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -12,7 +12,7 @@ mypkg = ["*.so", "*.dll"] [project] name = "atriumdb" -version = "2.0.0" +version = "2.1.1" description = "Timeseries Database" readme = "README.md" authors = [{name = "Robert Greer, William Dixon, Spencer Vecile"}, { name = "Robert Greer", email = "robert.greer@sickkids.ca"}, { name = "William Dixon", email = "will.dixon@sickkids.ca" }, { name = "Spencer Vecile", email = "spencer.vecile@sickkids.ca"}]