From 7ebf815681b8c927bb2ad533e08e5d1e7abfc11d Mon Sep 17 00:00:00 2001 From: Charles Graham SWT Date: Thu, 22 Jan 2026 17:14:52 -0600 Subject: [PATCH 1/4] First attempt at wrapping cli in an friendly error handler --- cwmscli/__main__.py | 39 +++++++++++++++++++ cwmscli/utils/ssl_errors.py | 77 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 cwmscli/utils/ssl_errors.py diff --git a/cwmscli/__main__.py b/cwmscli/__main__.py index 84ef976..e3d277d 100644 --- a/cwmscli/__main__.py +++ b/cwmscli/__main__.py @@ -1,8 +1,15 @@ +from __future__ import annotations + +import logging +import os +import sys + import click from cwmscli.commands import commands_cwms from cwmscli.load import __main__ as load from cwmscli.usgs import usgs_group +from cwmscli.utils.ssl_errors import is_cert_verify_error, ssl_help_text @click.group() @@ -15,3 +22,35 @@ def cli(): cli.add_command(commands_cwms.csv2cwms_cmd) cli.add_command(commands_cwms.blob_group) cli.add_command(load.load_group) + + +def main() -> None: + """ + Entrypoint wrapper so we can print friendly guidance without a traceback + for known TLS/cert issues. + """ + debug = os.getenv("CWMS_CLI_DEBUG", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + try: + cli(standalone_mode=False) + except SystemExit: + raise + except Exception as e: + if is_cert_verify_error(e) and not debug: + # Keep this short, no stack trace. + logging.error( + "SSL certificate verification failed while connecting to the server." + ) + click.echo(ssl_help_text(), err=True) + raise SystemExit(2) + + # If debug is enabled (or it's not a cert verify error), keep the normal failure behavior. + raise + + +if __name__ == "__main__": + main() diff --git a/cwmscli/utils/ssl_errors.py b/cwmscli/utils/ssl_errors.py new file mode 100644 index 0000000..ea64b84 --- /dev/null +++ b/cwmscli/utils/ssl_errors.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import os +import ssl +import sys +from typing import Iterable + + +def _walk_exception_chain(exc: BaseException) -> Iterable[BaseException]: + seen: set[int] = set() + cur: BaseException | None = exc + while cur is not None and id(cur) not in seen: + seen.add(id(cur)) + yield cur + cur = cur.__cause__ or cur.__context__ + + +def is_cert_verify_error(exc: BaseException) -> bool: + try: + import requests + except Exception: + requests = None + + try: + import urllib3 + except Exception: + urllib3 = None + + for e in _walk_exception_chain(exc): + if isinstance(e, ssl.SSLCertVerificationError): + return True + if isinstance(e, ssl.SSLError) and "CERTIFICATE_VERIFY_FAILED" in str(e): + return True + if requests is not None: + if isinstance(e, getattr(requests.exceptions, "SSLError", ())): + if ( + "CERTIFICATE_VERIFY_FAILED" in str(e) + or "certificate verify failed" in str(e).lower() + ): + return True + if urllib3 is not None: + if isinstance(e, getattr(urllib3.exceptions, "SSLError", ())): + if ( + "CERTIFICATE_VERIFY_FAILED" in str(e) + or "certificate verify failed" in str(e).lower() + ): + return True + return False + + +def ssl_help_text() -> str: + if os.name == "nt" or sys.platform.startswith("win"): + return ( + "TLS certificate verification failed.\n\n" + "Windows fix (recommended):\n" + " python -m pip install --upgrade pip-system-certs\n\n" + "Then re-run your command.\n" + ) + + if sys.platform.startswith(("sunos", "sunos5", "solaris")): + return ( + "TLS certificate verification failed.\n\n" + "Solaris fix: configure Python/requests to use your system/DoD trust bundle.\n" + "Add one of these to your shell profile (e.g., ~/.bashrc) and start a new shell:\n\n" + " export SSL_CERT_FILE=/path/to/your/dod_ca_bundle.pem\n" + " # or\n" + " export REQUESTS_CA_BUNDLE=/path/to/your/dod_ca_bundle.pem\n\n" + "Use the bundle path required by your environment.\n" + ) + + return ( + "TLS certificate verification failed.\n\n" + "Fix: configure Python/requests to use your organization trust bundle.\n" + "Common options:\n" + " export SSL_CERT_FILE=/path/to/ca-bundle.pem\n" + " export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.pem\n" + ) diff --git a/pyproject.toml b/pyproject.toml index 3637a1a..cb28636 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,4 +44,4 @@ explicit_start = false preserve_quotes = true [tool.poetry.scripts] -cwms-cli = "cwmscli.__main__:cli" +cwms-cli = "cwmscli.__main__:main" From c0341b1c37e267d14f77e61cccf6e2d7fea8d1dd Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Tue, 3 Feb 2026 20:14:01 +0000 Subject: [PATCH 2/4] Update black code check version and run formatter for all files --- .github/workflows/code-check.yml | 2 +- .github/workflows/docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index f28ba95..eb66b54 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -15,5 +15,5 @@ jobs: # below simply check the source code and fail if they find any files that need to be # formatted. The code is not automatically reformatted like it is when running the # pre-commit hooks. - - uses: psf/black@stable + - uses: psf/black@24.2.0 - uses: isort/isort-action@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 455585f..0b9ba02 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install deps From c89d4b59f9dcb8ed79ccc551f8baaa4b02296e54 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Tue, 3 Feb 2026 20:51:58 +0000 Subject: [PATCH 3/4] Update black dependency version and change code check to use the same local pre commit code check --- .github/workflows/code-check.yml | 15 ++++++++------- poetry.lock | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 74eaaf2..e8624da 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -7,13 +7,14 @@ jobs: # Run basic code quality checks. check-code: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - # Verify that python files are formatted using black and isort. Both of the actions - # below simply check the source code and fail if they find any files that need to be - # formatted. The code is not automatically reformatted like it is when running the - # pre-commit hooks. - - uses: psf/black@26.1.0 - - uses: isort/isort-action@v1 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Poetry + run: pipx install poetry + + - uses: pre-commit/action@v3.0.1 diff --git a/poetry.lock b/poetry.lock index 25305e7..1b33e5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -264,7 +264,7 @@ 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 = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\" and python_version >= \"3.11\""} +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" and python_version >= \"3.10\" or platform_system == \"Windows\""} [[package]] name = "cwms-python" @@ -1263,7 +1263,7 @@ description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version >= \"3.11\"" +markers = "python_version >= \"3.10\"" files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, From 12aa8cc5a54c7373c89895676dde60cf67d9e2d5 Mon Sep 17 00:00:00 2001 From: "Charles Graham, SWT" Date: Tue, 3 Feb 2026 20:54:49 +0000 Subject: [PATCH 4/4] Disable venv for poetry in CI/CD --- .github/workflows/code-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index e8624da..2d9defc 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -17,4 +17,9 @@ jobs: - name: Install Poetry run: pipx install poetry + - name: Configure Poetry (no venv) + install deps + run: | + poetry config virtualenvs.create false + poetry install --no-interaction + - uses: pre-commit/action@v3.0.1