cwms-cli 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/PKG-INFO +3 -2
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/README.md +1 -1
- cwms_cli-0.3.0/cwmscli/__main__.py +80 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/blob.py +2 -2
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/commands_cwms.py +1 -4
- cwms_cli-0.3.0/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/__init__.py +4 -2
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/utils/__init__.py +25 -0
- cwms_cli-0.3.0/cwmscli/utils/colors.py +38 -0
- cwms_cli-0.3.0/cwmscli/utils/io.py +16 -0
- cwms_cli-0.3.0/cwmscli/utils/logging/__init__.py +82 -0
- cwms_cli-0.3.0/cwmscli/utils/logging/formatters.py +105 -0
- cwms_cli-0.3.0/cwmscli/utils/ssl_errors.py +77 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/pyproject.toml +4 -3
- cwms_cli-0.2.1/cwmscli/__init__.py +0 -14
- cwms_cli-0.2.1/cwmscli/__main__.py +0 -17
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/LICENSE +0 -0
- {cwms_cli-0.2.1/cwmscli/commands/csv2cwms/tests → cwms_cli-0.3.0/cwmscli}/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/callbacks/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/hourly.json +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/minutes.json +0 -0
- {cwms_cli-0.2.1/cwmscli/load → cwms_cli-0.3.0/cwmscli/commands/csv2cwms/tests}/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/terminal.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/location/location_ids.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/requirements.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/getusgs_cda.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/utils/deps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Command line utilities for Corps Water Management Systems (CWMS) python scripts. This is a collection of shared scripts across the enterprise Water Management Enterprise System (WMES) teams.
|
|
5
5
|
License: LICENSE
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
20
|
+
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
20
21
|
Project-URL: Repository, https://github.com/HydrologicEngineeringCenter/cwms-cli
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
|
|
@@ -24,7 +25,7 @@ Description-Content-Type: text/markdown
|
|
|
24
25
|
|
|
25
26
|
A collection of scripts to create, read, update, list, and delete data through CWMS Data API (CDA) and other commonly used API in the US Army Corps of Engineers water management. CWMS-CLI wraps these API in a friendly to use terminal based interface.
|
|
26
27
|
|
|
27
|
-
[](https://cwms-cli.readthedocs.io/en/latest/) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
|
|
28
|
+
[](https://cwms-cli.readthedocs.io/en/latest/cli.html#cwms-cli) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
|
|
28
29
|
|
|
29
30
|
## Install
|
|
30
31
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A collection of scripts to create, read, update, list, and delete data through CWMS Data API (CDA) and other commonly used API in the US Army Corps of Engineers water management. CWMS-CLI wraps these API in a friendly to use terminal based interface.
|
|
4
4
|
|
|
5
|
-
[](https://cwms-cli.readthedocs.io/en/latest/) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
|
|
5
|
+
[](https://cwms-cli.readthedocs.io/en/latest/cli.html#cwms-cli) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from cwmscli.commands import commands_cwms
|
|
9
|
+
from cwmscli.load import __main__ as load
|
|
10
|
+
from cwmscli.usgs import usgs_group
|
|
11
|
+
from cwmscli.utils.logging import LoggingConfig, setup_logging
|
|
12
|
+
from cwmscli.utils.ssl_errors import is_cert_verify_error, ssl_help_text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
16
|
+
@click.option(
|
|
17
|
+
"--log-file",
|
|
18
|
+
type=click.Path(dir_okay=False, writable=True, resolve_path=True),
|
|
19
|
+
default=None,
|
|
20
|
+
help="Write logs to a file. If set, disables color completely.",
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--no-color",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
default=False,
|
|
26
|
+
help="Disable colored output in the terminal.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--log-level",
|
|
30
|
+
type=click.Choice(
|
|
31
|
+
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
|
|
32
|
+
),
|
|
33
|
+
default="INFO",
|
|
34
|
+
)
|
|
35
|
+
def cli(log_file: Optional[str], no_color: bool, log_level: str) -> None:
|
|
36
|
+
level = getattr(logging, log_level.upper(), logging.INFO)
|
|
37
|
+
|
|
38
|
+
# Disable colors if stdout isn't a TTY (piped/redirected)
|
|
39
|
+
tty = sys.stdout.isatty()
|
|
40
|
+
color = (not no_color) and tty
|
|
41
|
+
setup_logging(LoggingConfig(level=level, log_file=log_file, color=color))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
cli.add_command(usgs_group, name="usgs")
|
|
45
|
+
cli.add_command(commands_cwms.shefcritimport)
|
|
46
|
+
cli.add_command(commands_cwms.csv2cwms_cmd)
|
|
47
|
+
cli.add_command(commands_cwms.blob_group)
|
|
48
|
+
cli.add_command(load.load_group)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main() -> None:
|
|
52
|
+
"""
|
|
53
|
+
Entrypoint wrapper so we can print friendly guidance without a traceback
|
|
54
|
+
for known TLS/cert issues.
|
|
55
|
+
"""
|
|
56
|
+
debug = os.getenv("CWMS_CLI_DEBUG", "").strip().lower() in {
|
|
57
|
+
"1",
|
|
58
|
+
"true",
|
|
59
|
+
"yes",
|
|
60
|
+
"on",
|
|
61
|
+
}
|
|
62
|
+
try:
|
|
63
|
+
cli(standalone_mode=False)
|
|
64
|
+
except SystemExit:
|
|
65
|
+
raise
|
|
66
|
+
except Exception as e:
|
|
67
|
+
if is_cert_verify_error(e) and not debug:
|
|
68
|
+
# Keep this short, no stack trace.
|
|
69
|
+
logging.error(
|
|
70
|
+
"SSL certificate verification failed while connecting to the server."
|
|
71
|
+
)
|
|
72
|
+
click.echo(ssl_help_text(), err=True)
|
|
73
|
+
raise SystemExit(2)
|
|
74
|
+
|
|
75
|
+
# If debug is enabled (or it's not a cert verify error), keep the normal failure behavior.
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -326,7 +326,7 @@ def download_cmd(
|
|
|
326
326
|
f"DRY RUN: would GET {api_root} blob with blob-id={blob_id} office={office}."
|
|
327
327
|
)
|
|
328
328
|
return
|
|
329
|
-
cwms.init_session(api_root=api_root
|
|
329
|
+
cwms.init_session(api_root=api_root)
|
|
330
330
|
bid = blob_id.upper()
|
|
331
331
|
logging.debug(f"Office={office} BlobID={bid}")
|
|
332
332
|
|
|
@@ -419,7 +419,7 @@ def list_cmd(
|
|
|
419
419
|
import cwms
|
|
420
420
|
import pandas as pd
|
|
421
421
|
|
|
422
|
-
cwms.init_session(api_root=api_root
|
|
422
|
+
cwms.init_session(api_root=api_root)
|
|
423
423
|
df = list_blobs(
|
|
424
424
|
office=office,
|
|
425
425
|
blob_id_like=blob_id_like,
|
|
@@ -5,7 +5,7 @@ import click
|
|
|
5
5
|
from cwmscli import requirements as reqs
|
|
6
6
|
from cwmscli.callbacks import csv_to_list
|
|
7
7
|
from cwmscli.commands import csv2cwms
|
|
8
|
-
from cwmscli.utils import api_key_loc_option, common_api_options
|
|
8
|
+
from cwmscli.utils import api_key_loc_option, common_api_options, to_uppercase
|
|
9
9
|
from cwmscli.utils.deps import requires
|
|
10
10
|
|
|
11
11
|
|
|
@@ -251,6 +251,3 @@ def list_cmd(**kwargs):
|
|
|
251
251
|
from cwmscli.commands.blob import list_cmd
|
|
252
252
|
|
|
253
253
|
list_cmd(**kwargs)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# endregion
|
|
File without changes
|
|
@@ -118,13 +118,15 @@ def ratingsinifileimport(filename, api_root, api_key, api_key_loc):
|
|
|
118
118
|
@click.option(
|
|
119
119
|
"-d",
|
|
120
120
|
"--days-back-modified",
|
|
121
|
-
default=
|
|
121
|
+
default=2,
|
|
122
|
+
type=int,
|
|
122
123
|
help="Days back from current time measurements have been modified in USGS database. Can be integer value",
|
|
123
124
|
)
|
|
124
125
|
@click.option(
|
|
125
126
|
"-c",
|
|
126
127
|
"--days-back-collected",
|
|
127
|
-
default=
|
|
128
|
+
default=365,
|
|
129
|
+
type=int,
|
|
128
130
|
help="Days back from current time measurements have been collected. Can be integer value",
|
|
129
131
|
)
|
|
130
132
|
@office_option
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
|
|
3
5
|
|
|
@@ -7,6 +9,16 @@ def to_uppercase(ctx, param, value):
|
|
|
7
9
|
return value.upper()
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
def _set_log_level(ctx, param, value):
|
|
13
|
+
if value is None:
|
|
14
|
+
return
|
|
15
|
+
level = getattr(logging, value.upper(), None)
|
|
16
|
+
if level is None:
|
|
17
|
+
raise click.BadParameter(f"Invalid log level: {value}")
|
|
18
|
+
logging.getLogger().setLevel(level)
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
10
22
|
office_option = click.option(
|
|
11
23
|
"-o",
|
|
12
24
|
"--office",
|
|
@@ -47,6 +59,18 @@ api_key_loc_option = click.option(
|
|
|
47
59
|
type=str,
|
|
48
60
|
help="file storing Api Key. One of api-key or api-key-loc are required",
|
|
49
61
|
)
|
|
62
|
+
log_level_option = click.option(
|
|
63
|
+
"--log-level",
|
|
64
|
+
type=click.Choice(
|
|
65
|
+
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
|
|
66
|
+
),
|
|
67
|
+
default="INFO",
|
|
68
|
+
envvar="LOG_LEVEL",
|
|
69
|
+
callback=_set_log_level,
|
|
70
|
+
expose_value=False, # Callback will set the log level of all methods
|
|
71
|
+
is_eager=True, # Run before other commands (to cover any logging statements)
|
|
72
|
+
help="Set logging verbosity (overrides default INFO).",
|
|
73
|
+
)
|
|
50
74
|
|
|
51
75
|
|
|
52
76
|
def get_api_key(api_key: str, api_key_loc: str) -> str:
|
|
@@ -62,6 +86,7 @@ def get_api_key(api_key: str, api_key_loc: str) -> str:
|
|
|
62
86
|
|
|
63
87
|
|
|
64
88
|
def common_api_options(f):
|
|
89
|
+
f = log_level_option(f)
|
|
65
90
|
f = office_option(f)
|
|
66
91
|
f = api_root_option(f)
|
|
67
92
|
f = api_key_option(f)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from colorama import Fore, Style
|
|
4
|
+
|
|
5
|
+
_ENABLED: bool = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def set_enabled(enabled: bool) -> None:
|
|
9
|
+
global _ENABLED
|
|
10
|
+
_ENABLED = enabled
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def c(text: str, color: str, bright: bool = False) -> str:
|
|
14
|
+
if not _ENABLED:
|
|
15
|
+
return text
|
|
16
|
+
b = Style.BRIGHT if bright else ""
|
|
17
|
+
# Find the color in Fore and apply it to the text, then reset the style at the end
|
|
18
|
+
if hasattr(Fore, color.upper()):
|
|
19
|
+
color = getattr(Fore, color.upper())
|
|
20
|
+
else:
|
|
21
|
+
color = ""
|
|
22
|
+
return f"{color}{b}{text}{Style.RESET_ALL}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ok(text: str) -> str:
|
|
26
|
+
return c(text, Fore.GREEN)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def warn(text: str) -> str:
|
|
30
|
+
return c(text, Fore.YELLOW, bright=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def err(text: str) -> str:
|
|
34
|
+
return c(text, Fore.RED, bright=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dim(text: str) -> str:
|
|
38
|
+
return c(text, Fore.WHITE, bright=False)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def write_to_file(file_path: str, data: str, create_dir: bool = False) -> None:
|
|
7
|
+
"""Writes data to a file at the specified path."""
|
|
8
|
+
if not file_path:
|
|
9
|
+
raise ValueError("You must specify a file path to write data to.")
|
|
10
|
+
if not data:
|
|
11
|
+
raise ValueError("No data provided to write to file.")
|
|
12
|
+
if create_dir:
|
|
13
|
+
Path(os.path.dirname(file_path)).mkdir(parents=True, exist_ok=True)
|
|
14
|
+
with open(file_path, "w") as file:
|
|
15
|
+
file.write(data)
|
|
16
|
+
logging.info(f"Data written to file: {file_path}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from colorama import Fore, Style
|
|
9
|
+
from colorama import init as colorama_init
|
|
10
|
+
|
|
11
|
+
from cwmscli.utils import colors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class LoggingConfig:
|
|
16
|
+
level: int = logging.INFO
|
|
17
|
+
log_file: Optional[str] = None
|
|
18
|
+
color: bool = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ColorLevelFormatter(logging.Formatter):
|
|
22
|
+
def __init__(self, fmt: str, datefmt: str, enable_color: bool) -> None:
|
|
23
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
24
|
+
self._enable_color = enable_color
|
|
25
|
+
|
|
26
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
27
|
+
original = record.levelname
|
|
28
|
+
try:
|
|
29
|
+
if self._enable_color:
|
|
30
|
+
record.levelname = self._color_levelname(
|
|
31
|
+
record.levelname, record.levelno
|
|
32
|
+
)
|
|
33
|
+
# Make a copy of the record to avoid mutating the original, since format() can be called multiple times for the same record by multiple handlers
|
|
34
|
+
return super().format(record)
|
|
35
|
+
finally:
|
|
36
|
+
record.levelname = original
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _color_datetime(dt_str: str) -> str:
|
|
40
|
+
return f"{Fore.CYAN}{dt_str}{Style.RESET_ALL}"
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _color_levelname(levelname: str, levelno: int) -> str:
|
|
44
|
+
# Color the LOG LEVEL
|
|
45
|
+
if levelno >= logging.CRITICAL:
|
|
46
|
+
return f"{Fore.MAGENTA}{Style.BRIGHT}{levelname}{Style.RESET_ALL}"
|
|
47
|
+
if levelno >= logging.ERROR:
|
|
48
|
+
return f"{Fore.RED}{Style.BRIGHT}{levelname}{Style.RESET_ALL}"
|
|
49
|
+
if levelno >= logging.WARNING:
|
|
50
|
+
return f"{Fore.YELLOW}{Style.BRIGHT}{levelname}{Style.RESET_ALL}"
|
|
51
|
+
if levelno >= logging.INFO:
|
|
52
|
+
return f"{Fore.GREEN}{levelname}{Style.RESET_ALL}"
|
|
53
|
+
return f"{Fore.CYAN}{levelname}{Style.RESET_ALL}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def setup_logging(cfg: LoggingConfig) -> None:
|
|
57
|
+
root = logging.getLogger()
|
|
58
|
+
# Clear existing handlers
|
|
59
|
+
if root.hasHandlers():
|
|
60
|
+
root.handlers.clear()
|
|
61
|
+
|
|
62
|
+
root.setLevel(cfg.level)
|
|
63
|
+
root.propagate = False
|
|
64
|
+
|
|
65
|
+
# If a log file is specified, disable color completely
|
|
66
|
+
color_enabled = False if cfg.log_file else cfg.color
|
|
67
|
+
|
|
68
|
+
# Initialize colorama once, per the docs. Do NOT intialize colorama in each handler/formatter
|
|
69
|
+
colorama_init(autoreset=True, strip=not color_enabled)
|
|
70
|
+
colors.set_enabled(color_enabled)
|
|
71
|
+
|
|
72
|
+
base_fmt = "%(asctime)s;%(levelname)s;%(message)s"
|
|
73
|
+
date_fmt = "%Y-%m-%d %H:%M:%S"
|
|
74
|
+
stream_handler = logging.StreamHandler(sys.stdout)
|
|
75
|
+
stream_handler.setFormatter(ColorLevelFormatter(base_fmt, date_fmt, color_enabled))
|
|
76
|
+
root.addHandler(stream_handler)
|
|
77
|
+
|
|
78
|
+
if cfg.log_file:
|
|
79
|
+
file_handler = logging.FileHandler(cfg.log_file, encoding="utf-8")
|
|
80
|
+
file_handler.setFormatter(logging.Formatter(base_fmt, date_fmt))
|
|
81
|
+
root.addHandler(file_handler)
|
|
82
|
+
logging.getLogger().info("logger configured")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
ColorFn = Callable[[str, str], str] # e.g. c("text", "BLUE")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_JSON_START = re.compile(r"^\s*[\{\[]")
|
|
11
|
+
# Very lightweight JSON "syntax highlighting" (works on pretty-printed JSON)
|
|
12
|
+
_JSON_TOKENS = re.compile(
|
|
13
|
+
r'(?P<key>"(?:\\.|[^"\\])*")\s*:'
|
|
14
|
+
r"|(?P<string>\"(?:\\.|[^\"\\])*\")"
|
|
15
|
+
r"|(?P<number>-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)"
|
|
16
|
+
r"|(?P<bool>\btrue\b|\bfalse\b)"
|
|
17
|
+
r"|(?P<null>\bnull\b)"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _maybe_parse_json(s: str) -> Optional[Any]:
|
|
22
|
+
if not _JSON_START.match(s):
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return json.loads(s)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _colorize_json(pretty_json: str, c: ColorFn) -> str:
|
|
31
|
+
def repl(m: re.Match[str]) -> str:
|
|
32
|
+
if m.group("key") is not None:
|
|
33
|
+
# key token (still includes quotes)
|
|
34
|
+
return f'{c(m.group("key"), "CYAN")}:'
|
|
35
|
+
if m.group("string") is not None:
|
|
36
|
+
return c(m.group("string"), "GREEN")
|
|
37
|
+
if m.group("number") is not None:
|
|
38
|
+
return c(m.group("number"), "MAGENTA")
|
|
39
|
+
if m.group("bool") is not None:
|
|
40
|
+
return c(m.group("bool"), "YELLOW")
|
|
41
|
+
if m.group("null") is not None:
|
|
42
|
+
return c(m.group("null"), "RED")
|
|
43
|
+
return m.group(0)
|
|
44
|
+
|
|
45
|
+
return _JSON_TOKENS.sub(repl, pretty_json)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_cell(val: Any, *, c: ColorFn, json_color: bool) -> str:
|
|
49
|
+
s = "" if val is None else str(val)
|
|
50
|
+
|
|
51
|
+
if json_color:
|
|
52
|
+
obj = _maybe_parse_json(s)
|
|
53
|
+
if obj is not None:
|
|
54
|
+
pretty = json.dumps(obj, indent=2, sort_keys=True)
|
|
55
|
+
return _colorize_json(pretty, c)
|
|
56
|
+
|
|
57
|
+
return s
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_df_for_log(
|
|
61
|
+
df: pd.DataFrame,
|
|
62
|
+
*,
|
|
63
|
+
c: ColorFn,
|
|
64
|
+
col_colors: Optional[Dict[int, str]] = None,
|
|
65
|
+
json_color: bool = True,
|
|
66
|
+
max_rows: int = 500,
|
|
67
|
+
) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Formats a pandas DataFrame for logging, applying optional colorization to columns and JSON values.
|
|
70
|
+
This function iterates over the rows of the provided DataFrame and formats each cell for logging.
|
|
71
|
+
You can specify colors for particular columns and whether to colorize JSON values. The output is a
|
|
72
|
+
string suitable for logging, with each row on a new line.
|
|
73
|
+
Args:
|
|
74
|
+
df (pd.DataFrame): The DataFrame to format.
|
|
75
|
+
c (ColorFn): A function that applies color to a string, e.g., c(text, color_name).
|
|
76
|
+
col_colors (Optional[Dict[int, str]], optional): A mapping from column indices to color names.
|
|
77
|
+
If None, defaults to {0: "BLUE", 1: "GREEN"}.
|
|
78
|
+
json_color (bool, optional): Whether to apply colorization to JSON values. Defaults to True.
|
|
79
|
+
max_rows (int, optional): Maximum number of rows to display. Defaults to 500.
|
|
80
|
+
Returns:
|
|
81
|
+
str: The formatted string representation of the DataFrame for logging.
|
|
82
|
+
Examples:
|
|
83
|
+
>>> import pandas as pd
|
|
84
|
+
>>> def color_fn(text, color): return f"<{color}>{text}</{color}>"
|
|
85
|
+
>>> df = pd.DataFrame({'A': [1, 2], 'B': ['x', 'y']})
|
|
86
|
+
>>> print(format_df_for_log(df, c=color_fn, col_colors={0: "RED", 1: "GREEN"}))
|
|
87
|
+
<RED>1</RED> <GREEN>x</GREEN>
|
|
88
|
+
<RED>2</RED> <GREEN>y</GREEN>
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
col_colors = col_colors or {0: "BLUE", 1: "GREEN"}
|
|
92
|
+
|
|
93
|
+
lines: List[str] = []
|
|
94
|
+
with pd.option_context("display.max_rows", max_rows, "display.max_columns", None):
|
|
95
|
+
for row in df.itertuples(index=False, name=None):
|
|
96
|
+
parts: List[str] = []
|
|
97
|
+
for idx, val in enumerate(row):
|
|
98
|
+
cell = _format_cell(val, c=c, json_color=json_color)
|
|
99
|
+
if idx in col_colors:
|
|
100
|
+
parts.append(c(cell, col_colors[idx]))
|
|
101
|
+
else:
|
|
102
|
+
parts.append(cell)
|
|
103
|
+
lines.append(" ".join(parts))
|
|
104
|
+
|
|
105
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import ssl
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Iterable, Optional, Set
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _walk_exception_chain(exc: BaseException) -> Iterable[BaseException]:
|
|
10
|
+
seen: Set[int] = set()
|
|
11
|
+
cur: Optional[BaseException] = exc
|
|
12
|
+
while cur is not None and id(cur) not in seen:
|
|
13
|
+
seen.add(id(cur))
|
|
14
|
+
yield cur
|
|
15
|
+
cur = cur.__cause__ or cur.__context__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_cert_verify_error(exc: BaseException) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
import requests
|
|
21
|
+
except Exception:
|
|
22
|
+
requests = None
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import urllib3
|
|
26
|
+
except Exception:
|
|
27
|
+
urllib3 = None
|
|
28
|
+
|
|
29
|
+
for e in _walk_exception_chain(exc):
|
|
30
|
+
if isinstance(e, ssl.SSLCertVerificationError):
|
|
31
|
+
return True
|
|
32
|
+
if isinstance(e, ssl.SSLError) and "CERTIFICATE_VERIFY_FAILED" in str(e):
|
|
33
|
+
return True
|
|
34
|
+
if requests is not None:
|
|
35
|
+
if isinstance(e, getattr(requests.exceptions, "SSLError", ())):
|
|
36
|
+
if (
|
|
37
|
+
"CERTIFICATE_VERIFY_FAILED" in str(e)
|
|
38
|
+
or "certificate verify failed" in str(e).lower()
|
|
39
|
+
):
|
|
40
|
+
return True
|
|
41
|
+
if urllib3 is not None:
|
|
42
|
+
if isinstance(e, getattr(urllib3.exceptions, "SSLError", ())):
|
|
43
|
+
if (
|
|
44
|
+
"CERTIFICATE_VERIFY_FAILED" in str(e)
|
|
45
|
+
or "certificate verify failed" in str(e).lower()
|
|
46
|
+
):
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ssl_help_text() -> str:
|
|
52
|
+
if os.name == "nt" or sys.platform.startswith("win"):
|
|
53
|
+
return (
|
|
54
|
+
"TLS certificate verification failed.\n\n"
|
|
55
|
+
"Windows fix (recommended):\n"
|
|
56
|
+
" python -m pip install --upgrade pip-system-certs\n\n"
|
|
57
|
+
"Then re-run your command.\n"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if sys.platform.startswith(("sunos", "sunos5", "solaris")):
|
|
61
|
+
return (
|
|
62
|
+
"TLS certificate verification failed.\n\n"
|
|
63
|
+
"Solaris fix: configure Python/requests to use your system/DoD trust bundle.\n"
|
|
64
|
+
"Add one of these to your shell profile (e.g., ~/.bashrc) and start a new shell:\n\n"
|
|
65
|
+
" export SSL_CERT_FILE=/path/to/your/dod_ca_bundle.pem\n"
|
|
66
|
+
" # or\n"
|
|
67
|
+
" export REQUESTS_CA_BUNDLE=/path/to/your/dod_ca_bundle.pem\n\n"
|
|
68
|
+
"Use the bundle path required by your environment.\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
"TLS certificate verification failed.\n\n"
|
|
73
|
+
"Fix: configure Python/requests to use your organization trust bundle.\n"
|
|
74
|
+
"Common options:\n"
|
|
75
|
+
" export SSL_CERT_FILE=/path/to/ca-bundle.pem\n"
|
|
76
|
+
" export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.pem\n"
|
|
77
|
+
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "cwms-cli"
|
|
3
3
|
repository = "https://github.com/HydrologicEngineeringCenter/cwms-cli"
|
|
4
4
|
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.3.0"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
packages = [
|
|
@@ -19,13 +19,14 @@ python = "^3.9"
|
|
|
19
19
|
click = "^8.1.8"
|
|
20
20
|
hecdss = { version = ">=0.1.24", optional = true } # Via https://github.com/HydrologicEngineeringCenter/hec-python-library/blob/main/hec/shared.py#L9-10
|
|
21
21
|
cwms-python = { version = ">=0.8.0", optional = true}
|
|
22
|
+
colorama = "^0.4.6"
|
|
22
23
|
|
|
23
24
|
[tool.poetry.group.dev.dependencies]
|
|
24
25
|
black = "^24.2.0"
|
|
25
26
|
isort = "^5.13.2"
|
|
26
27
|
mypy = "^1.9.0"
|
|
27
28
|
pre-commit = "^3.6.2"
|
|
28
|
-
pytest =
|
|
29
|
+
pytest = "^8.3.5"
|
|
29
30
|
#pytest-cov = "^4.1.0"
|
|
30
31
|
#pandas-stubs = "^2.2.1.240316"
|
|
31
32
|
yamlfix = "^1.16.0"
|
|
@@ -44,4 +45,4 @@ explicit_start = false
|
|
|
44
45
|
preserve_quotes = true
|
|
45
46
|
|
|
46
47
|
[tool.poetry.scripts]
|
|
47
|
-
cwms-cli = "cwmscli.__main__:
|
|
48
|
+
cwms-cli = "cwmscli.__main__:main"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import logging as lg
|
|
2
|
-
|
|
3
|
-
from cwmscli import load
|
|
4
|
-
|
|
5
|
-
# create logging for logging
|
|
6
|
-
logging = lg.getLogger()
|
|
7
|
-
if logging.hasHandlers():
|
|
8
|
-
logging.handlers.clear()
|
|
9
|
-
handler = lg.StreamHandler()
|
|
10
|
-
formatter = lg.Formatter("%(asctime)s;%(levelname)s;%(message)s", "%Y-%m-%d %H:%M:%S")
|
|
11
|
-
handler.setFormatter(formatter)
|
|
12
|
-
logging.addHandler(handler)
|
|
13
|
-
logging.setLevel(lg.INFO)
|
|
14
|
-
logging.propagate = False
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
|
|
3
|
-
from cwmscli.commands import commands_cwms
|
|
4
|
-
from cwmscli.load import __main__ as load
|
|
5
|
-
from cwmscli.usgs import usgs_group
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@click.group()
|
|
9
|
-
def cli():
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
cli.add_command(usgs_group, name="usgs")
|
|
14
|
-
cli.add_command(commands_cwms.shefcritimport)
|
|
15
|
-
cli.add_command(commands_cwms.csv2cwms_cmd)
|
|
16
|
-
cli.add_command(commands_cwms.blob_group)
|
|
17
|
-
cli.add_command(load.load_group)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.2.1 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|