Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
705f820
Add yaml for config files
krowvin Sep 16, 2025
d0684f3
Add initial templates
krowvin Sep 16, 2025
e66c7a7
Update report yaml to extend to columns/rows
krowvin Sep 16, 2025
d3ba838
Add initial reporting cmd/attempt render/scaffold design
krowvin Sep 16, 2025
661142e
Change to a default template given a config, update internal daily te…
krowvin Sep 17, 2025
3b0b54a
Update to allow for levels and per column href with specific begin/end
krowvin Sep 17, 2025
84820cc
Make the header dynamic
krowvin Sep 18, 2025
d724fc1
Add reporting diagram
krowvin Sep 18, 2025
713e14c
Add additional location placeholders for report, test target_time
krowvin Sep 22, 2025
2874fc1
Add dateparser/dateutil for extensive date options/strings in cwms-cli
krowvin Sep 23, 2025
c745f25
Restructure from single file
krowvin Sep 23, 2025
39fe299
Create reporting docs dir
krowvin Sep 23, 2025
e56dc6a
Add per column item begin/end times
krowvin Sep 23, 2025
7a24ade
Remove time delta idea
krowvin Sep 30, 2025
84f30f2
20 setup read the docs documentation (#31)
krowvin Sep 19, 2025
b1da09f
29 blob script needs lazy import for imghdr (#30)
krowvin Sep 19, 2025
e0b5160
18 overhaul csv2cwms (#34)
krowvin Sep 19, 2025
de5140f
Bump actions/checkout from 4 to 5 (#36)
dependabot[bot] Sep 23, 2025
bc61b0d
Bump actions/setup-python from 5 to 6 (#37)
dependabot[bot] Sep 23, 2025
c5cc51d
Combine USGS Scripts into Group (#40)
krowvin Sep 23, 2025
f337d3f
Update .readthedocs.yaml (#43)
krowvin Sep 23, 2025
d5441e4
revert back to 3.9 (#42)
Enovotny Sep 24, 2025
1ba1a9c
Fix global imports for lazy loading, fixes #47
krowvin Oct 6, 2025
41b596f
Add requirement for yaml library for configs
krowvin Dec 12, 2025
21dde5a
Add all locations in
krowvin Jan 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: CWMS CLI Script Issues
name: CWMS-CLI Script Issues
description: File a bug report CWMS-CLI scripts
labels: ["bug"]
body:

- type: dropdown
id: script
attributes:
label: CLI Script
description: Select the script this pertains to
options:
- blob
- cwms-cli
- csv2cwms
- getusgs-measurements
Expand Down
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/feature-request.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CWMS-CLI Script Issue or Feature Request
name: CWMS-CLI Feature Request
description: Request a feature related to CWMS-CLI scripts
labels: ["enhancement"]
body:
Expand All @@ -8,6 +8,7 @@ body:
label: CLI Script
description: Select the script this pertains to
options:
- blob
- cwms-cli
- csv2cwms
- getusgs-measurements
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Confirm the docs build works on PRs touching docs or code
name: ReadTheDocs/Sphinx Validation
on:
pull_request:
paths: ["docs/**", "cwmscli/**", "pyproject.toml"]
push:
branches: [main]
paths: ["docs/**", "cwmscli/**", "pyproject.toml"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install deps
run: |
python -m pip install -U pip
pip install -r docs/requirements.txt
pip install .
- name: Sphinx build (treat warnings as errors)
run: sphinx-build -nW -b html docs docs/_build/html
- name: Link check (optional)
run: sphinx-build -b linkcheck docs docs/_build/linkcheck
16 changes: 16 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 2

build:
os: ubuntu-24.04
tools:
python: "3.12"

sphinx:
configuration: docs/conf.py

python:
install:
- method: pip
path: .
- requirements: docs/requirements.txt
formats: [pdf]
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# cwms-cli

command line utilities used for Corps Water Management Systems (CWMS) processes

[![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](https://cwms-cli.readthedocs.io/en/latest/)

## Install

```sh
pip install git+https://github.com/HydrologicEngineeringCenter/cwms-cli.git@main
```

## Command line implementation

```sh
cwms-cli --help
```

9 changes: 4 additions & 5 deletions cwmscli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import click

from cwmscli.commands import commands_cwms
from cwmscli.getusgs import commands_getusgs
from cwmscli.reporting import reporting_cli
from cwmscli.usgs import usgs_group


@click.group()
def cli():
pass


cli.add_command(commands_getusgs.getusgs_timeseries)
cli.add_command(commands_getusgs.getusgs_ratings)
cli.add_command(commands_getusgs.ratingsinifileimport)
cli.add_command(commands_getusgs.getusgs_measurements)
cli.add_command(usgs_group, name="usgs")
cli.add_command(commands_cwms.shefcritimport)
cli.add_command(commands_cwms.csv2cwms_cmd)
cli.add_command(commands_cwms.blob_group)
cli.add_command(reporting_cli)
39 changes: 34 additions & 5 deletions cwmscli/commands/blob.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import base64
import imghdr
import json
import logging
import mimetypes
Expand All @@ -13,11 +12,42 @@
import requests

from cwmscli.utils import get_api_key
from cwmscli.utils.deps import requires

# used to rebuild data URL for images
DATA_URL_RE = re.compile(r"^data:(?P<mime>[^;]+);base64,(?P<data>.+)$", re.I | re.S)


@requires(
{
"module": "imghdr",
"package": "standard-imghdr",
"version": "3.0.0",
"desc": "Package to help detect image types",
"link": "https://docs.python.org/3/library/imghdr.html",
}
)
def _determine_ext(data: bytes | str, write_type: str) -> str:
"""
Attempt to determine the file extension from the data itself.
Requires the imghdr module (lazy import) to inspect the bytes for image types.
If not an image, defaults to .bin

Args:
data: The binary data or base64 string to inspect.
write_type: The mode in which the data will be written ('wb' for binary, 'w' for text).

Returns:
The determined file extension, including the leading dot (e.g., '.png', '.jpg').
"""
import imghdr

kind = imghdr.what(None, data)
if kind == "jpeg":
kind = "jpg"
return f".{kind}" if kind else ".bin"


def _save_base64(
b64_or_dataurl: str,
dest: str,
Expand Down Expand Up @@ -48,11 +78,10 @@ def _save_base64(
ext = mimetypes.guess_extension(media_type.split(";")[0].lower()) or ""
if ext == ".jpe":
ext = ".jpg"
# last resort, try to determine from the data itself
# requires imghdr to dig into the bytes to determine image type
if not ext:
kind = imghdr.what(None, data)
if kind == "jpeg":
kind = "jpg"
ext = f".{kind}" if kind else ".bin"
ext = _determine_ext(data, write_type)
dest = base + ext

os.makedirs(os.path.dirname(dest) or ".", exist_ok=True)
Expand Down
30 changes: 16 additions & 14 deletions cwmscli/commands/commands_cwms.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ def shefcritimport(filename, office, api_root, api_key, api_key_loc):
@click.command("csv2cwms", help="Store CSV TimeSeries data to CWMS using a config file")
@common_api_options
@click.option(
"-l",
"--location",
"--input-keys",
"input_keys",
default="all",
show_default=True,
help='Location ID. Use "-p=all" for all locations.',
help='Input keys. Defaults to all keys/files with --input-keys=all. These are the keys under "input_files" in a given config file. This option lets you run a single file from a config that contains multiple files. Example: --input-keys=file1',
)
@click.option(
"-lb",
Expand All @@ -67,15 +67,6 @@ def shefcritimport(filename, office, api_root, api_key, api_key_loc):
help="Override CSV file (else use config)",
)
@click.option("--log", show_default=True, help="Path to the log file.")
@click.option(
"-dp",
"--data-path",
"data_path",
default=".",
show_default=True,
type=click.Path(exists=True, file_okay=False),
help="Directory where csv files are stored",
)
@click.option("--dry-run", is_flag=True, help="Log only (no HTTP calls)")
@click.option("--begin", type=str, help="YYYY-MM-DDTHH:MM (local to --tz)")
@click.option("-tz", "--timezone", "tz", default="GMT", show_default=True)
Expand All @@ -100,7 +91,16 @@ def csv2cwms_cmd(**kwargs):
# ================================================================================
# BLOB
# ================================================================================
@click.group("blob", help="Manage CWMS Blobs (upload, download, delete, update, list)")
@click.group(
"blob",
help="Manage CWMS Blobs (upload, download, delete, update, list)",
epilog="""
* Store a PDF/image as a CWMS blob with optional description
* Download a blob by id to your local filesystem
* Update a blob's name/description
* Bulk list blobs for an office
""",
)
@requires(reqs.cwms)
def blob_group():
pass
Expand Down Expand Up @@ -190,7 +190,9 @@ def update_cmd(**kwargs):
# ================================================================================
@blob_group.command("list", help="List blobs with optional filters and sorting")
# TODO: Add link to regex docs when new CWMS-DATA site is deployed to PROD
@click.option("--blob-id-like", help="LIKE filter for blob ID (e.g., '*PNG').")
@click.option(
"--blob-id-like", help="LIKE filter for blob ID (e.g., ``*PNG``)."
) # Escape the wildcard/asterisk for RTD generation with double backticks
@click.option(
"--columns",
multiple=True,
Expand Down
68 changes: 38 additions & 30 deletions cwmscli/commands/csv2cwms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,44 @@ To View the Help: `cwms-cli csv2cwms --help`

Usage: cwms-cli csv2cwms [OPTIONS]

Store CSV TimeSeries data to CWMS using a config file
Store CSV TimeSeries data to CWMS using a config file

Options:
-o, --office TEXT Office to grab data for [required]
-a, --api_root TEXT Api Root for CDA. Can be user defined or placed
in a env variable CDA_API_ROOT [required]
-k, --api_key TEXT api key for CDA. Can be user defined or place in
env variable CDA_API_KEY. one of api_key or
api_key_loc are required
-l, --location TEXT Location ID. Use "-p=all" for all locations.
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
-c, --config PATH Path to JSON config file [required]
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
[default: all]
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
-c, --config PATH Path to JSON config file [required]
-df, --data-file TEXT Override CSV file (else use config)
--log TEXT Path to the log file.
-dp, --data-path DIRECTORY Directory where csv files are stored [default:
.]
--dry-run Log only (no HTTP calls)
--begin TEXT YYYY-MM-DDTHH:MM (local to --tz)
-tz, --timezone TEXT [default: GMT]
--ignore-ssl-errors Ignore TLS errors (testing only)
--version Show the version and exit.
--help Show this message and exit.
-o, --office TEXT Office to grab data for [required]
-a, --api_root TEXT Api Root for CDA. Can be user defined or placed
in a env variable CDA_API_ROOT [required]
-k, --api_key TEXT api key for CDA. Can be user defined or place in
env variable CDA_API_KEY. one of api_key or
api_key_loc are required
-l, --location TEXT Location ID. Use "-p=all" for all locations.
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
-c, --config PATH Path to JSON config file [required]
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
[default: all]
[default: all]
-lb, --lookback INTEGER Lookback period in HOURS [default: 120]
-v, --verbose Verbose logging
-c, --config PATH Path to JSON config file [required]
-df, --data-file TEXT Override CSV file (else use config)
--log TEXT Path to the log file.
-dp, --data-path DIRECTORY Directory where csv files are stored [default:
.]
--dry-run Log only (no HTTP calls)
--begin TEXT YYYY-MM-DDTHH:MM (local to --tz)
-tz, --timezone TEXT [default: GMT]
--ignore-ssl-errors Ignore TLS errors (testing only)
--version Show the version and exit.
--help Show this message and exit.

## Features

- Allow for specifying one or more date formats that might be seen per input csv file
- Allow mathematical operations across multiple columns and storing into one timeseries
- Store one column of data with a user-specified precision and units to a timeseries identifier
- Dry runs to test what data might look like prior to database storage
- Verbose logging via the -v flag
- Colored terminal output for user readability
Loading
Loading