Skip to content

Commit 11ba1ea

Browse files
committed
cd '/home/vm/code/cwms-cli'
Merge remote-tracking branch 'origin/main' into feature/env-man
2 parents 7614d41 + 500435c commit 11ba1ea

39 files changed

Lines changed: 2736 additions & 65 deletions

.devcontainer/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm
2+
3+
ARG POETRY_VERSION=2.0.1
4+
5+
ENV TZ=Etc/UTC \
6+
POETRY_HOME=/opt/poetry \
7+
VIRTUAL_ENV=/home/vscode/.venv \
8+
POETRY_VIRTUALENVS_CREATE=false
9+
ENV PATH="${VIRTUAL_ENV}/bin:${POETRY_HOME}/bin:${PATH}"
10+
11+
RUN python -m venv "${POETRY_HOME}" \
12+
&& "${POETRY_HOME}/bin/python" -m pip install --no-cache-dir \
13+
"poetry==${POETRY_VERSION}" \
14+
&& python -m venv "${VIRTUAL_ENV}" \
15+
&& chown -R vscode:vscode "${VIRTUAL_ENV}"

.devcontainer/devcontainer.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "cwms-cli",
3+
"build": {
4+
"dockerfile": "Dockerfile",
5+
"args": {
6+
"POETRY_VERSION": "2.0.1"
7+
}
8+
},
9+
"remoteUser": "vscode",
10+
"containerEnv": {
11+
"TZ": "Etc/UTC",
12+
"POETRY_HOME": "/opt/poetry",
13+
"VIRTUAL_ENV": "/home/vscode/.venv",
14+
"POETRY_VIRTUALENVS_CREATE": "false"
15+
},
16+
"postCreateCommand": "git config --global --add safe.directory \"$(pwd)\" && poetry install --with dev --no-interaction",
17+
"customizations": {
18+
"vscode": {
19+
"extensions": [
20+
"ms-python.python"
21+
],
22+
"settings": {
23+
"python.defaultInterpreterPath": "/home/vscode/.venv/bin/python"
24+
}
25+
}
26+
}
27+
}

.github/ISSUE_TEMPLATE/bug-report.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ body:
88
label: CLI Script
99
description: Select the script this pertains to
1010
options:
11-
- blob
12-
- cwms-cli
13-
- csv2cwms
14-
- getusgs-measurements
11+
- blob
12+
- cwms-cli
13+
- csv2cwms
14+
- dss
15+
- getusgs-measurements
1516
- getusgs-ratings
1617
- getusgs-timeseries
1718
- shefcritimport

.github/ISSUE_TEMPLATE/feature-request.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ body:
88
label: CLI Script
99
description: Select the script this pertains to
1010
options:
11-
- blob
12-
- cwms-cli
13-
- csv2cwms
14-
- getusgs-measurements
11+
- blob
12+
- cwms-cli
13+
- csv2cwms
14+
- dss
15+
- getusgs-measurements
1516
- getusgs-ratings
1617
- getusgs-timeseries
1718
- shefcritimport

.github/workflows/cli-tests.yml

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ on:
66

77
jobs:
88
test:
9-
name: Test CLI on ${{ matrix.python-version }}
10-
runs-on: ubuntu-latest
9+
name: Test CLI on ${{ matrix.name }} with Python ${{ matrix.python-version }}
10+
runs-on: ${{ matrix.os }}
1111

1212
strategy:
13+
fail-fast: false
1314
matrix:
14-
# Test 3.9 for T7, 3.12 for general/cloud use
15-
python-version: ["3.9", "3.12"]
15+
include:
16+
# Test the supported Python range on Linux and exercise the primary
17+
# Python version on each supported desktop operating system.
18+
- name: Linux
19+
os: ubuntu-latest
20+
python-version: "3.9"
21+
- name: Linux
22+
os: ubuntu-latest
23+
python-version: "3.12"
24+
- name: Windows
25+
os: windows-latest
26+
python-version: "3.12"
27+
- name: macOS
28+
os: macos-latest
29+
python-version: "3.12"
1630

1731
steps:
1832
- name: Checkout repository
@@ -38,3 +52,17 @@ jobs:
3852

3953
- name: Run full test suite
4054
run: poetry run pytest -q
55+
56+
test-devcontainer:
57+
name: Test dev container on 3.12
58+
runs-on: ubuntu-latest
59+
60+
steps:
61+
- name: Checkout repository
62+
uses: actions/checkout@v7
63+
64+
- name: Build dev container and run tests
65+
uses: devcontainers/ci@v0.3
66+
with:
67+
push: never
68+
runCmd: poetry run pytest -q

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
uses: pypa/gh-action-pypi-publish@release/v1
7171

7272
- name: Sign Distribution
73-
uses: sigstore/gh-action-sigstore-python@v3.4.0
73+
uses: sigstore/gh-action-sigstore-python@v3.5.0
7474
with:
7575
inputs: |
7676
./dist/*.tar.gz

AGENTS.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Repository instructions
2+
3+
Guidance for coding agents working in `HydrologicEngineeringCenter/cwms-cli`.
4+
5+
- Never push to `origin` unless the user explicitly says they are ready for
6+
that push.
7+
- Use JDK 21 or newer for new work that is not intended to run on T7 systems.
8+
On Windows, use JDK 21 at `C:\Program Files\Java\jdk-21`. On Linux or other
9+
Unix-like systems, select an installed JDK 21 or newer through `JAVA_HOME`.
10+
Use another Java version only when the target or task requires it.
11+
- Use the `.devcontainer` Linux/Python 3.12 environment for changes involving
12+
time zones, paths, native libraries, HEC-DSS, or other operating-system-
13+
dependent behavior.
14+
- Run the full dev-container test suite with
15+
`devcontainer exec --workspace-folder . poetry run pytest -q` when the dev
16+
container is available. The standard CI matrix separately covers Python 3.9
17+
and Python 3.12 package compatibility.
18+
19+
## Terminal colors
20+
21+
- Use the shared helpers in `cwmscli.utils.colors` for user-facing terminal
22+
color. Prefer `colors.ok`, `colors.warn`, `colors.err`, and `colors.dim` for
23+
their semantic cases, or `colors.c(text, color, bright=...)` when a specific
24+
color is needed.
25+
- Do not embed ANSI escape sequences or initialize Colorama in individual
26+
commands. Global logging setup owns Colorama initialization and calls
27+
`colors.set_enabled(...)` so `--no-color`, `--log-file`, and non-TTY output
28+
remain consistent.
29+
- Keep the text meaningful without color. Color should clarify status or
30+
structure, not carry information that disappears when color is disabled.
31+
- Reuse the existing conventions: green for success, yellow for warnings,
32+
red for errors, cyan or blue for identifiers and commands, and dim text for
33+
secondary detail.
34+
- When testing colored output, cover the plain-text behavior first. Enable the
35+
shared color helper explicitly only in tests that need to assert escape codes,
36+
and restore its state afterward.

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@ Note: You may need to run `python -m pip install cwms-cli` if PIP is not in your
1414

1515
### Update
1616
```sh
17-
pip install cwms-cli --upgrade
17+
python -m pip install --upgrade cwms-cli
1818
```
1919

2020
Or as of version `0.3.0+`
2121
```sh
2222
cwms-cli update
2323
```
2424

25+
`cwms-cli update` displays the Python executable, environment, package metadata
26+
location, and editable project location when applicable before asking for
27+
confirmation. It runs pip through the same Python interpreter that is running
28+
`cwms-cli`, avoiding accidental updates to a different Python installation or
29+
virtual environment.
30+
2531
To install a specific version:
2632
```sh
2733
cwms-cli update --target-version 0.7.1 --yes
@@ -40,3 +46,21 @@ from cwmscli.usgs.getusgs_cda import getusgs_cda
4046
from cwmscli.usgs.getusgs_measurements_cda import getusgs_measurements_cda
4147
from cwmscli.usgs.getUSGS_ratings_cda import getusgs_rating_cda
4248
```
49+
50+
## Development environment
51+
52+
The repository includes a Linux/Python 3.12 development container exercised by
53+
a dedicated CI job. Open the repository with the VS Code Dev Containers
54+
extension, or use the Dev Container CLI:
55+
56+
```sh
57+
devcontainer up --workspace-folder .
58+
devcontainer exec --workspace-folder . poetry run pytest -q
59+
```
60+
61+
The container installs project dependencies with Poetry and uses
62+
`/home/vscode/.venv`, so it does not reuse a host operating system's `.venv`.
63+
Poetry itself is kept in a separate `/opt/poetry` environment. Use the container
64+
for changes involving time zones, paths, native libraries, or other
65+
operating-system-dependent behavior. Standard Python 3.9 and Python 3.12
66+
installations remain covered by the existing CI matrix.

cwmscli/__main__.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88

99
from cwmscli.commands import commands_cwms
1010
from cwmscli.commands.env import env_group
11+
from cwmscli.dss import dss_group
1112
from cwmscli.load import __main__ as load
1213
from cwmscli.usgs import usgs_group
1314
from cwmscli.utils.click_help import add_version_to_help_tree
14-
from cwmscli.utils.friendly_errors import to_user_facing_error
15+
from cwmscli.utils.friendly_errors import (
16+
cda_stack_trace,
17+
format_cda_stack_trace,
18+
to_user_facing_error,
19+
)
20+
from cwmscli.utils.links import BUG_REPORT_URL
1521
from cwmscli.utils.logging import (
1622
LoggingConfig,
1723
apply_logging_policies,
@@ -91,6 +97,7 @@ def cli(
9197
cli.add_command(commands_cwms.users_group)
9298
cli.add_command(env_group)
9399
cli.add_command(load.load_group)
100+
cli.add_command(dss_group)
94101
add_version_to_help_tree(cli)
95102

96103

@@ -113,9 +120,19 @@ def main() -> None:
113120
except SystemExit:
114121
raise
115122
except click.ClickException as e:
123+
debug = debug or logging.getLogger().isEnabledFor(logging.DEBUG)
124+
if debug:
125+
server_stack_trace = cda_stack_trace(e)
126+
if server_stack_trace is not None:
127+
click.echo(format_cda_stack_trace(server_stack_trace), err=True)
128+
raise SystemExit(e.exit_code)
116129
e.show()
117130
raise SystemExit(e.exit_code)
118131
except Exception as e:
132+
# The environment switch supports failures before Click configures logging.
133+
# Once CLI setup has run, --log-level DEBUG enables the same behavior.
134+
debug = debug or logging.getLogger().isEnabledFor(logging.DEBUG)
135+
119136
if is_cert_verify_error(e) and not debug:
120137
# Keep this short, no stack trace.
121138
logging.error(
@@ -124,14 +141,21 @@ def main() -> None:
124141
click.echo(ssl_help_text(), err=True)
125142
raise SystemExit(2)
126143

127-
if not debug:
144+
if debug:
145+
server_stack_trace = cda_stack_trace(e)
146+
if server_stack_trace is not None:
147+
click.echo(format_cda_stack_trace(server_stack_trace), err=True)
148+
raise SystemExit(1)
149+
else:
128150
friendly_error = to_user_facing_error(e)
129151
if friendly_error is not None:
130152
logging.debug("Suppressed traceback for CLI exception", exc_info=e)
131153
friendly_error.show()
132154
raise SystemExit(friendly_error.exit_code)
133155

134-
# If debug is enabled (or it's not a cert verify error), keep the normal failure behavior.
156+
click.echo(f"Unexpected error. Report it at {BUG_REPORT_URL}", err=True)
157+
# Preserve raw exception behavior when CDA did not provide a server stack
158+
# trace so an issue report includes useful diagnostic details.
135159
raise
136160

137161

cwmscli/commands/commands_cwms.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
import os
33
import subprocess
4-
import sys
54
import textwrap
65
from pathlib import Path
76
from typing import Optional
@@ -26,7 +25,9 @@
2625
from cwmscli.utils.deps import requires
2726
from cwmscli.utils.update import (
2827
build_update_package_spec,
28+
get_update_environment,
2929
launch_windows_update,
30+
looks_like_externally_managed_environment,
3031
looks_like_missing_version,
3132
)
3233
from cwmscli.utils.version import get_cwms_cli_version
@@ -416,6 +417,7 @@ def csv2cwms_cmd(**kwargs):
416417
def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
417418
current_version = get_cwms_cli_version()
418419
package_spec = build_update_package_spec(target_version)
420+
update_environment = get_update_environment()
419421

420422
click.echo(
421423
"Current cwms-cli version: " f"{colors.c(current_version, 'cyan', bright=True)}"
@@ -428,12 +430,35 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
428430
else:
429431
click.echo("Requested cwms-cli version: latest available release")
430432

431-
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", package_spec]
433+
click.echo("Update environment:")
434+
click.echo(f" Python executable: {update_environment.python_executable}")
435+
click.echo(
436+
f" Environment: {update_environment.environment_prefix} "
437+
f"({update_environment.environment_type})"
438+
)
439+
click.echo(f" Package metadata location: {update_environment.package_location}")
440+
if update_environment.editable_project_location:
441+
click.echo(
442+
" Editable project location: "
443+
f"{update_environment.editable_project_location}"
444+
)
445+
446+
cmd = [
447+
update_environment.python_executable,
448+
"-m",
449+
"pip",
450+
"install",
451+
"--upgrade",
452+
package_spec,
453+
]
432454
if pre:
433455
cmd.append("--pre")
434456

435457
if not yes:
436-
proceed = click.confirm("Proceed with updating cwms-cli via pip?", default=True)
458+
proceed = click.confirm(
459+
"Proceed with updating cwms-cli in this environment via pip?",
460+
default=True,
461+
)
437462
if not proceed:
438463
click.echo(colors.warn("Update canceled."))
439464
return
@@ -472,6 +497,16 @@ def update_cli_cmd(target_version: Optional[str], pre: bool, yes: bool) -> None:
472497

473498
if result.returncode != 0:
474499
pip_output = "\n".join(part for part in [result.stdout, result.stderr] if part)
500+
if looks_like_externally_managed_environment(pip_output):
501+
raise click.ClickException(
502+
colors.err(
503+
"The selected Python installation is externally managed, so pip "
504+
"refused to update cwms-cli. Install cwms-cli in a virtual "
505+
"environment or with pipx, then run that installation's "
506+
"cwms-cli update command. cwms-cli will not use "
507+
"--break-system-packages automatically."
508+
)
509+
)
475510
if target_version and looks_like_missing_version(pip_output, package_spec):
476511
raise click.ClickException(
477512
colors.err(

0 commit comments

Comments
 (0)