diff --git a/cwmscli/__main__.py b/cwmscli/__main__.py index e97e6b9..99811ad 100644 --- a/cwmscli/__main__.py +++ b/cwmscli/__main__.py @@ -7,6 +7,7 @@ from click.core import ParameterSource from cwmscli.commands import commands_cwms +from cwmscli.commands.env import env_group from cwmscli.load import __main__ as load from cwmscli.usgs import usgs_group from cwmscli.utils.click_help import add_version_to_help_tree @@ -88,6 +89,7 @@ def cli( cli.add_command(commands_cwms.blob_group) cli.add_command(commands_cwms.clob_group) cli.add_command(commands_cwms.users_group) +cli.add_command(env_group) cli.add_command(load.load_group) add_version_to_help_tree(cli) diff --git a/cwmscli/commands/env.py b/cwmscli/commands/env.py new file mode 100644 index 0000000..7883a56 --- /dev/null +++ b/cwmscli/commands/env.py @@ -0,0 +1,268 @@ +import os +import sys +from pathlib import Path +from typing import Dict, Optional + +import click + +from cwmscli.utils.credentials import ( + CredentialStorageError, + add_to_environment_index, + delete_environment, + get_environment, + get_environment_from_os_environ, + get_environment_index, + is_keyring_available, + remove_from_environment_index, + store_environment, +) + + +def get_envs_dir() -> Path: + """Get the directory where environment files are stored.""" + if sys.platform == "win32": + base_dir = Path(os.environ.get("APPDATA", "~/.config")).expanduser() + else: + base_dir = Path("~/.config").expanduser() + + envs_dir = base_dir / "cwms-cli" / "envs" + return envs_dir + + +ENV_DEFAULTS = { + "cwbi-prod": "https://cwms-data.usace.army.mil/cwms-data", +} + + +@click.group("env", help="Manage CDA environments and API keys") +def env_group(): + """Environment management commands for cwms-cli.""" + pass + + +@env_group.command("setup", help="Create or update an environment configuration") +@click.argument("env_name") +@click.option( + "--api-root", + help="CDA API root URL (e.g., https://example.mil/cwms-data)", +) +@click.option( + "--api-key", + help="API key for authentication", +) +@click.option( + "--office", + help="Default office code (e.g., SWT)", +) +def setup_cmd( + env_name: str, + api_root: Optional[str], + api_key: Optional[str], + office: Optional[str], +): + """ + Create or update environment configuration. + + ENV_NAME can be: cwbi-dev, cwbi-test, cwbi-prod, onsite, localhost, or custom + """ + # Get existing config from keyring, if any + existing_vars = get_environment(env_name) or {} + + env_vars = existing_vars.copy() + env_vars["ENVIRONMENT"] = env_name + + if api_root: + env_vars["CDA_API_ROOT"] = api_root + elif "CDA_API_ROOT" not in env_vars and env_name in ENV_DEFAULTS: + env_vars["CDA_API_ROOT"] = ENV_DEFAULTS[env_name] + + if api_key: + env_vars["CDA_API_KEY"] = api_key + + if office: + env_vars["OFFICE"] = office.upper() + + if "CDA_API_ROOT" not in env_vars: + click.echo( + f"Error: --api-root is required for '{env_name}' (not a default environment)", + err=True, + ) + click.echo(f"Available defaults: {', '.join(ENV_DEFAULTS.keys())}", err=True) + sys.exit(1) + + # Store in keyring + try: + store_environment(env_name, env_vars) + add_to_environment_index(env_name) + click.echo(f"Environment '{env_name}' configured securely in system keyring") + except CredentialStorageError as e: + click.echo(f"Error: {e}", err=True) + sys.exit(1) + + +@env_group.command("show", help="Show current environment and available configurations") +def show_cmd(): + """ + Display current environment and list all configured environments. + + Lists all environments with API root, office, and key status. + """ + current_env = os.environ.get("ENVIRONMENT") + + # List all environments + if current_env: + click.echo( + f"Current environment: {click.style(current_env, fg='green', bold=True)}\n" + ) + else: + click.echo("No environment currently active\n") + + environments = get_environment_index() + + if environments: + click.echo("Available environments:") + for env_name in environments: + env_config = get_environment(env_name) + if env_config: + is_active = env_name == current_env + marker = "* " if is_active else " " + + api_root = env_config.get("CDA_API_ROOT", "not set") + office = env_config.get("OFFICE", "not set") + has_key = ( + "has API key" if env_config.get("CDA_API_KEY") else "no API key" + ) + + click.echo(f"{marker}{env_name}") + click.echo(f" API Root: {api_root}") + click.echo(f" Office: {office}") + click.echo(f" Status: {has_key}") + else: + click.echo("No environments configured") + click.echo("Run 'cwms-cli env setup ' to create one") + + # Check for old .env files (for migration purposes) + envs_dir = get_envs_dir() + env_files = [] + if envs_dir.exists(): + env_files = sorted(envs_dir.glob("*.env")) + + if env_files: + click.echo("\nOld .env files found (not migrated to keyring):") + for env_file in env_files: + env_name = env_file.stem + click.echo(f" - {env_name}") + + +@env_group.command("delete", help="Delete an environment configuration") +@click.argument("env_name") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt") +def delete_cmd(env_name: str, yes: bool): + """ + Delete an environment configuration from keyring. + + Examples: + cwms-cli env delete myenv + cwms-cli env delete myenv --yes + """ + if not yes: + if not click.confirm(f"Delete environment '{env_name}'?"): + click.echo("Cancelled") + return + + try: + delete_environment(env_name) + remove_from_environment_index(env_name) + click.echo(f"Environment '{env_name}' deleted") + except CredentialStorageError as e: + click.echo(f"Error: {e}", err=True) + sys.exit(1) + + +def spawn_shell_with_env(env_vars: Dict[str, str], env_name: str): + """Spawn a new shell with environment variables set.""" + import subprocess + + # Detect user's shell + user_shell = os.environ.get("SHELL") + if not user_shell: + if sys.platform == "win32": + user_shell = os.environ.get("COMSPEC", "cmd.exe") + else: + user_shell = "/bin/bash" + + # Create environment dict + new_env = os.environ.copy() + new_env.update(env_vars) + + # Show activation message + click.echo( + f"Activating environment: {click.style(env_name, fg='green', bold=True)}" + ) + click.echo(f"Shell: {user_shell}") + click.echo("Type 'exit' or press Ctrl+D to return to your original environment\n") + + # Spawn shell with modified environment + try: + result = subprocess.run([user_shell], env=new_env) + sys.exit(result.returncode) + except Exception as e: + click.echo(f"Error spawning shell: {e}", err=True) + sys.exit(1) + + +@env_group.command("activate", help="Activate an environment in a new shell") +@click.argument("env_name") +def activate_cmd(env_name: str): + """ + Activate an environment in a new shell session. + + The environment variables will be set in the new shell and persist + until you exit the shell. Type 'exit' to return to your original environment. + + Examples: + cwms-cli env activate cwbi-prod + cwms-cli env activate localhost + """ + # Try to get environment from keyring + env_vars = get_environment(env_name) + + if not env_vars: + # If not in keyring and keyring not available, try OS environment as fallback + if not is_keyring_available(): + fallback_vars = get_environment_from_os_environ() + if fallback_vars: + click.echo( + f"Using environment variables from current shell (keyring not available)", + err=True, + ) + env_vars = fallback_vars + env_vars["ENVIRONMENT"] = env_name + else: + click.echo( + "Error: Keyring not available and no environment variables found.", + err=True, + ) + click.echo( + "Set CDA_API_ROOT, CDA_API_KEY, and OFFICE in your environment,", + err=True, + ) + click.echo( + "or run 'cwms-cli env setup ' on a system with keyring support.", + err=True, + ) + sys.exit(1) + else: + click.echo( + f"Error: Environment '{env_name}' not found in keyring", + err=True, + ) + click.echo( + "Run 'cwms-cli env show --name ' to see if it exists", + err=True, + ) + click.echo(f"Or run 'cwms-cli env setup {env_name}' to create it", err=True) + sys.exit(1) + + # Always spawn a new shell + spawn_shell_with_env(env_vars, env_name) diff --git a/cwmscli/utils/credentials.py b/cwmscli/utils/credentials.py new file mode 100644 index 0000000..9205beb --- /dev/null +++ b/cwmscli/utils/credentials.py @@ -0,0 +1,285 @@ +"""Credential storage using keyring for secure cross-platform credential management.""" + +import json +import os +from typing import Dict, List, Optional + +import keyring +from keyring.errors import KeyringError + + +class CredentialStorageError(Exception): + """Raised when credential storage operations fail.""" + + pass + + +def is_keyring_available() -> bool: + """ + Check if keyring backend is available and functional. + + Returns: + True if keyring can be used, False otherwise + """ + try: + backend = keyring.get_keyring() + # Test write/read/delete to ensure it actually works + test_service = "cwms-cli-test" + test_key = "__availability_test__" + test_value = "test" + + keyring.set_password(test_service, test_key, test_value) + result = keyring.get_password(test_service, test_key) + keyring.delete_password(test_service, test_key) + + return result == test_value + except (KeyringError, Exception): + return False + + +def store_credential(service: str, key: str, value: str) -> None: + """ + Store a credential in the system keyring. + + Args: + service: Service name (e.g., "cwms-cli-env") + key: Key/username for the credential + value: Value/password to store + + Raises: + CredentialStorageError: If keyring is not available + """ + if not is_keyring_available(): + raise CredentialStorageError( + "Secure credential storage (keyring) is not available.\n\n" + "For headless/CI environments, set these environment variables instead:\n" + ' export CDA_API_ROOT="https://..."\n' + ' export CDA_API_KEY="your_key"\n' + ' export OFFICE="SWT"\n\n' + "For interactive systems, install a keyring backend:\n" + " Linux: Install gnome-keyring, kwallet, or python3-secretstorage\n" + " macOS: Uses Keychain (built-in)\n" + " Windows: Uses Credential Manager (built-in)" + ) + + try: + keyring.set_password(service, key, value) + except KeyringError as e: + raise CredentialStorageError(f"Failed to store credential: {e}") from e + + +def get_credential(service: str, key: str) -> Optional[str]: + """ + Retrieve a credential from the system keyring. + + Args: + service: Service name (e.g., "cwms-cli-env") + key: Key/username for the credential + + Returns: + The credential value, or None if not found + """ + if not is_keyring_available(): + return None + + try: + return keyring.get_password(service, key) + except KeyringError: + return None + + +def delete_credential(service: str, key: str) -> None: + """ + Delete a credential from the system keyring. + + Args: + service: Service name (e.g., "cwms-cli-env") + key: Key/username for the credential + + Raises: + CredentialStorageError: If deletion fails + """ + if not is_keyring_available(): + raise CredentialStorageError("Keyring is not available") + + try: + keyring.delete_password(service, key) + except KeyringError as e: + raise CredentialStorageError(f"Failed to delete credential: {e}") from e + + +def list_stored_credentials(service_prefix: str) -> List[str]: + """ + List credential keys for a given service prefix. + + Note: This functionality is limited by keyring backend capabilities. + Some backends don't support enumeration, so this may return an empty list + even if credentials exist. + + Args: + service_prefix: Service name prefix (e.g., "cwms-cli-env") + + Returns: + List of credential keys matching the service prefix + """ + # Most keyring backends don't support enumeration + # This is a limitation we document and work around + # by having users explicitly name environments + return [] + + +# Environment-specific functions + + +def store_environment(env_name: str, config: Dict[str, str]) -> None: + """ + Store environment configuration in keyring. + + Args: + env_name: Name of the environment + config: Dictionary of environment variables to store + + Raises: + CredentialStorageError: If keyring is not available + """ + service = f"cwms-cli-env:{env_name}" + # Store the entire config as a JSON string + config_json = json.dumps(config, sort_keys=True) + store_credential(service, "config", config_json) + + +def get_environment(env_name: str) -> Optional[Dict[str, str]]: + """ + Retrieve environment configuration from keyring. + + Args: + env_name: Name of the environment + + Returns: + Dictionary of environment variables, or None if not found + """ + service = f"cwms-cli-env:{env_name}" + config_json = get_credential(service, "config") + + if config_json: + try: + return json.loads(config_json) + except json.JSONDecodeError: + return None + + return None + + +def delete_environment(env_name: str) -> None: + """ + Delete environment configuration from keyring. + + Args: + env_name: Name of the environment + + Raises: + CredentialStorageError: If deletion fails + """ + service = f"cwms-cli-env:{env_name}" + delete_credential(service, "config") + + +def get_environment_from_os_environ() -> Dict[str, str]: + """ + Build environment config from OS environment variables. + + This is used as a fallback for headless/CI environments where keyring + is not available. Only returns variables that are actually set. + + Returns: + Dictionary of environment variables found in os.environ + """ + env_vars = {} + + # Check for known environment variables + if "CDA_API_ROOT" in os.environ: + env_vars["CDA_API_ROOT"] = os.environ["CDA_API_ROOT"] + + if "CDA_API_KEY" in os.environ: + env_vars["CDA_API_KEY"] = os.environ["CDA_API_KEY"] + + if "OFFICE" in os.environ: + env_vars["OFFICE"] = os.environ["OFFICE"] + + if "ENVIRONMENT" in os.environ: + env_vars["ENVIRONMENT"] = os.environ["ENVIRONMENT"] + + return env_vars + + +# Environment index management + + +def get_environment_index() -> List[str]: + """ + Get list of all environment names from the index. + + Returns: + List of environment names, or empty list if index doesn't exist + """ + if not is_keyring_available(): + return [] + + service = "cwms-cli-meta" + index_json = get_credential(service, "environments") + + if index_json: + try: + return json.loads(index_json) + except json.JSONDecodeError: + return [] + + return [] + + +def add_to_environment_index(env_name: str) -> None: + """ + Add an environment name to the index. + + Args: + env_name: Name of the environment to add + + Raises: + CredentialStorageError: If keyring is not available + """ + if not is_keyring_available(): + raise CredentialStorageError("Keyring is not available") + + index = get_environment_index() + if env_name not in index: + index.append(env_name) + index.sort() + service = "cwms-cli-meta" + store_credential(service, "environments", json.dumps(index)) + + +def remove_from_environment_index(env_name: str) -> None: + """ + Remove an environment name from the index. + + Args: + env_name: Name of the environment to remove + + Raises: + CredentialStorageError: If keyring is not available + """ + if not is_keyring_available(): + raise CredentialStorageError("Keyring is not available") + + index = get_environment_index() + if env_name in index: + index.remove(env_name) + service = "cwms-cli-meta" + if index: + store_credential(service, "environments", json.dumps(index)) + else: + # If index is empty, delete it + try: + delete_credential(service, "environments") + except CredentialStorageError: + pass # It's okay if it doesn't exist diff --git a/docs/cli/env.rst b/docs/cli/env.rst new file mode 100644 index 0000000..86abf47 --- /dev/null +++ b/docs/cli/env.rst @@ -0,0 +1,207 @@ +Environment Manager +=================== + +Secure environment management for CDA environments using system keyring storage. + +Overview +-------- + +The environment manager stores your CDA API credentials and configuration securely using your system's keyring: + +- **Linux**: gnome-keyring, kwallet, or secretstorage +- **macOS**: Keychain (built-in) +- **Windows**: Credential Manager (built-in) +- **Solaris**: Keyring not available - use environment variables (see Headless/CI Usage below) + +This keeps API keys out of plaintext files and provides a consistent, secure experience across platforms. + +Suggested Environments +---------------------- + +**Pre-configured (have default URLs):** + +- ``cwbi-prod`` - Production CWBI (https://cwms-data.usace.army.mil/cwms-data) + +**Need --api-root:** + +- ``cwbi-dev`` - Development CWBI +- ``cwbi-test`` - Test CWBI +- ``localhost`` - Local development server (port varies: 8081, 8082, etc.) +- ``onsite`` - Local non-cloud server +- Custom environment names + +Quick Start +----------- + +**1. Setup environments:** + +.. code-block:: bash + + # Pre-configured environment (just add key/office) + cwms-cli env setup cwbi-prod --office SWT --api-key YOUR_KEY + + # Custom environments (need --api-root) + cwms-cli env setup cwbi-dev --api-root https://cwms-data-dev.example.mil/cwms-data --office SWT --api-key YOUR_KEY + cwms-cli env setup localhost --api-root http://localhost:8082/cwms-data --office DEV + +**2. Activate an environment:** + +.. code-block:: bash + + cwms-cli env activate cwbi-dev + +This spawns a new shell with the environment variables set. When you're done, type ``exit`` to return to your original shell. + +**3. View environments:** + +.. code-block:: bash + + # List all environments with their API roots + cwms-cli env show + +Commands +-------- + +cwms-cli env setup +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create or update an environment configuration. + +.. code-block:: bash + + # Setup with all options + cwms-cli env setup myenv --api-root https://cwms-data-dev.example.mil/cwms-data --api-key YOUR_KEY --office SWT + + # Update just the API key + cwms-cli env setup myenv --api-key NEW_KEY + + # Update just the office + cwms-cli env setup myenv --office LRD + +cwms-cli env show +~~~~~~~~~~~~~~~~~~ + +List all configured environments. + +.. code-block:: bash + + # List all environments with API roots and key status + cwms-cli env show + +**Output:** + +.. code-block:: text + + Current environment: cwbi-prod + + Available environments: + * cwbi-prod + API Root: https://cwms-data.usace.army.mil/cwms-data + Office: SWT + Status: has API key + cwbi-dev + API Root: https://cwms-data-dev.example.mil/cwms-data + Office: SWT + Status: no API key + +The ``*`` marks the currently active environment. + +cwms-cli env activate +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Activate an environment in a new shell session. + +.. code-block:: bash + + cwms-cli env activate cwbi-prod + +The environment variables will be set in the new shell and persist until you exit: + +.. code-block:: bash + + # Now in the activated environment + echo $CDA_API_ROOT # Shows the API root + cwms-cli blob list # Uses environment config + + # Exit to return to original shell + exit + +**Benefits:** + +- Clean separation between environments +- Original shell remains unchanged +- Type ``exit`` to immediately return to original state + +cwms-cli env delete +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Delete an environment configuration from keyring. + +.. code-block:: bash + + # Delete with confirmation prompt + cwms-cli env delete myenv + + # Delete without confirmation + cwms-cli env delete myenv --yes + +How It Works +------------ + +**Secure storage:** Configuration is stored in your system's keyring: + +- ``~/.local/share/keyrings/`` (Linux with GNOME Keyring) +- ``~/Library/Keychains/`` (macOS Keychain) +- Windows Credential Manager (Windows) + +**Environment variables set when activated:** + +- ``ENVIRONMENT`` - Environment name +- ``CDA_API_ROOT`` - API root URL +- ``CDA_API_KEY`` - API key (if provided) +- ``OFFICE`` - Default office (if provided) + +**Usage with other commands:** + +.. code-block:: bash + + # Activate environment (spawns new shell) + cwms-cli env activate cwbi-prod + + # Now run commands (uses environment variables automatically) + cwms-cli blob list + cwms-cli users list + + # Command flags override environment variables + cwms-cli blob list --api-root https://cwms-data.usace.army.mil/cwms-data + + # Exit the environment shell + exit + +**Variable persistence:** + +- Variables persist until you ``exit`` the spawned shell +- Variables do NOT affect your original shell +- Variables do NOT persist across terminal restarts (activate again when needed) + +Headless/CI Usage (and Solaris) +-------------------------------- + +For headless, CI, or Solaris environments where keyring is not available, set environment variables directly: + +.. code-block:: bash + + export CDA_API_ROOT="https://cwms-data.usace.army.mil/cwms-data" + export CDA_API_KEY="your_key" + export OFFICE="SWT" + + # Commands will use these variables + cwms-cli blob list + +The CLI will automatically fall back to reading from ``os.environ`` when keyring is unavailable. + +**Note for Solaris users:** Since system keyring backends are not available on Solaris, you must use this environment variable approach. The ``cwms-cli env setup`` and ``cwms-cli env activate`` commands will not work without a keyring backend. Instead, set the variables directly in your shell profile (e.g., ``~/.bashrc`` or ``~/.profile``). + +.. click:: cwmscli.commands.env:env_group + :prog: cwms-cli env + :nested: full diff --git a/docs/index.rst b/docs/index.rst index dea66f7..9e83a2d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,6 +59,7 @@ Contents cli/csv2cwms cli/blob + cli/env cli/login cli/clob cli/users diff --git a/poetry.lock b/poetry.lock index e0e4377..a832286 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,21 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\"" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] [[package]] name = "black" @@ -59,6 +76,104 @@ files = [ {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "cfgv" version = "3.4.0" @@ -236,7 +351,124 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.9\" and sys_platform == \"linux\"" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cryptography" +version = "47.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and sys_platform == \"linux\"" +files = [ + {file = "cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8"}, + {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318"}, + {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001"}, + {file = "cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203"}, + {file = "cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa"}, + {file = "cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7"}, + {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52"}, + {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd"}, + {file = "cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63"}, + {file = "cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b"}, + {file = "cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe"}, + {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31"}, + {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7"}, + {file = "cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310"}, + {file = "cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8"}, + {file = "cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +ssh = ["bcrypt (>=3.1.5)"] [[package]] name = "cwms-python" @@ -370,6 +602,56 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and python_version < \"3.12\"" +files = [ + {file = "importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7"}, + {file = "importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "iniconfig" version = "2.1.0" @@ -397,6 +679,143 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808"}, + {file = "jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535"}, + {file = "jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + [[package]] name = "librt" version = "0.8.1" @@ -515,6 +934,32 @@ click = ">=8.0.1,<9.0.0" pydantic = ">=1.10.13,<2.0.0" toml = ">=0.10.2,<0.11.0" +[[package]] +name = "more-itertools" +version = "10.8.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.9\"" +files = [ + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, +] + +[[package]] +name = "more-itertools" +version = "11.0.2" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\"" +files = [ + {file = "more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4"}, + {file = "more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804"}, +] + [[package]] name = "mypy" version = "1.19.1" @@ -922,6 +1367,32 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\" and python_version == \"3.9\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and platform_python_implementation != \"PyPy\" and sys_platform == \"linux\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + [[package]] name = "pydantic" version = "1.10.26" @@ -1062,6 +1533,19 @@ files = [ {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1201,6 +1685,40 @@ setuptools = ">=39.0" [package.extras] docs = ["Sphinx"] +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "python_version == \"3.9\" and sys_platform == \"linux\"" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and sys_platform == \"linux\"" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "setuptools" version = "82.0.1" @@ -1310,11 +1828,12 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {main = "sys_platform == \"linux\" and python_version == \"3.10\""} [[package]] name = "tzdata" @@ -1385,7 +1904,28 @@ click = ">=8.1.3" maison = ">=1.4.0,<1.4.3" ruyaml = ">=0.91.0" +[[package]] +name = "zipp" +version = "3.23.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\"" +files = [ + {file = "zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc"}, + {file = "zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "056bcf91885aac3e6fe947610dbf8925f3ab359f94382b8dcb12035db4610472" +content-hash = "3cb5794dfc58e421338f2f7f2387afec10ee79890ea19f25efdb1f9ef974a950" diff --git a/pyproject.toml b/pyproject.toml index 1589a58..6553cdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ requests = "^2.30.0" hecdss = { version = ">=0.1.24", optional = true } # Via https://github.com/HydrologicEngineeringCenter/hec-python-library/blob/main/hec/shared.py#L9-10 cwms-python = { version = ">=1.0.7", optional = true} colorama = "^0.4.6" +keyring = "^25.0.0" [tool.poetry.group.dev.dependencies] black = "^24.2.0" diff --git a/tests/commands/test_env.py b/tests/commands/test_env.py new file mode 100644 index 0000000..d1b9407 --- /dev/null +++ b/tests/commands/test_env.py @@ -0,0 +1,229 @@ +import os +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from cwmscli.commands.env import get_envs_dir + + +def test_get_envs_dir_returns_path(): + envs_dir = get_envs_dir() + assert isinstance(envs_dir, Path) + assert "cwms-cli" in str(envs_dir) + assert "envs" in str(envs_dir) + + +# Test keyring-based environment management +@pytest.fixture +def mock_keyring(): + """Mock keyring module for testing.""" + with patch("cwmscli.utils.credentials.keyring") as mock: + # Mock successful keyring operations + mock.get_keyring.return_value = MagicMock() + mock.get_password.return_value = None + mock.set_password.return_value = None + mock.delete_password.return_value = None + yield mock + + +def test_store_and_get_environment(mock_keyring): + """Test storing and retrieving environment configuration.""" + from cwmscli.utils.credentials import get_environment, store_environment + + env_name = "test-env" + env_vars = { + "ENVIRONMENT": "test", + "CDA_API_ROOT": "https://example.com/cwms-data", + "CDA_API_KEY": "secret123", + "OFFICE": "SWT", + } + + # Mock storage + stored_data = {} + + def set_password(service, key, value): + stored_data[f"{service}:{key}"] = value + + def get_password(service, key): + return stored_data.get(f"{service}:{key}") + + mock_keyring.set_password.side_effect = set_password + mock_keyring.get_password.side_effect = get_password + + # Store environment + store_environment(env_name, env_vars) + + # Verify it was stored + assert f"cwms-cli-env:{env_name}:config" in stored_data + + # Retrieve environment + retrieved_vars = get_environment(env_name) + assert retrieved_vars == env_vars + + +def test_delete_environment(mock_keyring): + """Test deleting environment configuration.""" + from cwmscli.utils.credentials import ( + delete_environment, + get_environment, + store_environment, + ) + + env_name = "test-env" + env_vars = {"ENVIRONMENT": "test", "CDA_API_ROOT": "https://example.com"} + + # Mock storage + stored_data = {} + + def set_password(service, key, value): + stored_data[f"{service}:{key}"] = value + + def get_password(service, key): + return stored_data.get(f"{service}:{key}") + + def delete_password(service, key): + stored_data.pop(f"{service}:{key}", None) + + mock_keyring.set_password.side_effect = set_password + mock_keyring.get_password.side_effect = get_password + mock_keyring.delete_password.side_effect = delete_password + + # Store and then delete + store_environment(env_name, env_vars) + delete_environment(env_name) + + # Verify it was deleted + assert get_environment(env_name) is None + + +def test_get_environment_nonexistent_returns_none(mock_keyring): + """Test retrieving non-existent environment returns None.""" + from cwmscli.utils.credentials import get_environment + + mock_keyring.get_password.return_value = None + result = get_environment("nonexistent") + assert result is None + + +def test_environment_index_management(mock_keyring): + """Test adding and removing environments from the index.""" + from cwmscli.utils.credentials import ( + add_to_environment_index, + get_environment_index, + remove_from_environment_index, + ) + + # Mock storage + stored_data = {} + + def set_password(service, key, value): + stored_data[f"{service}:{key}"] = value + + def get_password(service, key): + return stored_data.get(f"{service}:{key}") + + def delete_password(service, key): + stored_data.pop(f"{service}:{key}", None) + + mock_keyring.set_password.side_effect = set_password + mock_keyring.get_password.side_effect = get_password + mock_keyring.delete_password.side_effect = delete_password + + # Initially empty + assert get_environment_index() == [] + + # Add environments + add_to_environment_index("env1") + add_to_environment_index("env2") + assert sorted(get_environment_index()) == ["env1", "env2"] + + # Remove environment + remove_from_environment_index("env1") + assert get_environment_index() == ["env2"] + + +def test_get_environment_from_os_environ(): + """Test building environment config from OS environment variables.""" + from cwmscli.utils.credentials import get_environment_from_os_environ + + with patch.dict( + os.environ, + { + "CDA_API_ROOT": "https://example.com/cwms-data", + "CDA_API_KEY": "test-key", + "OFFICE": "SWT", + "ENVIRONMENT": "test", + }, + clear=False, + ): + result = get_environment_from_os_environ() + assert result == { + "CDA_API_ROOT": "https://example.com/cwms-data", + "CDA_API_KEY": "test-key", + "OFFICE": "SWT", + "ENVIRONMENT": "test", + } + + +def test_get_environment_from_os_environ_partial(): + """Test building config with only some variables set.""" + from cwmscli.utils.credentials import get_environment_from_os_environ + + with patch.dict( + os.environ, + { + "CDA_API_ROOT": "https://example.com/cwms-data", + "OFFICE": "SWT", + }, + clear=True, + ): + result = get_environment_from_os_environ() + assert result == { + "CDA_API_ROOT": "https://example.com/cwms-data", + "OFFICE": "SWT", + } + assert "CDA_API_KEY" not in result + assert "ENVIRONMENT" not in result + + +def test_is_keyring_available_success(mock_keyring): + """Test keyring availability check when keyring works.""" + from cwmscli.utils.credentials import is_keyring_available + + # Mock successful keyring operations + stored_value = None + + def set_password(service, key, value): + nonlocal stored_value + stored_value = value + + def get_password(service, key): + return stored_value + + mock_keyring.set_password.side_effect = set_password + mock_keyring.get_password.side_effect = get_password + mock_keyring.delete_password.return_value = None + + assert is_keyring_available() is True + + +def test_is_keyring_available_failure(mock_keyring): + """Test keyring availability check when keyring fails.""" + from keyring.errors import KeyringError + + from cwmscli.utils.credentials import is_keyring_available + + # Mock keyring failure + mock_keyring.get_keyring.side_effect = KeyringError("No keyring available") + + assert is_keyring_available() is False + + +def test_store_environment_no_keyring(): + """Test storing environment when keyring is not available.""" + from cwmscli.utils.credentials import CredentialStorageError, store_environment + + with patch("cwmscli.utils.credentials.is_keyring_available", return_value=False): + with pytest.raises(CredentialStorageError, match="Secure credential storage"): + store_environment("test", {"CDA_API_ROOT": "https://example.com"})