cwms-cli 0.2.2__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.
Files changed (57) hide show
  1. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/PKG-INFO +3 -2
  2. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/README.md +1 -1
  3. cwms_cli-0.3.0/cwmscli/__main__.py +80 -0
  4. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/blob.py +2 -2
  5. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/commands_cwms.py +1 -4
  6. cwms_cli-0.3.0/cwmscli/load/__init__.py +0 -0
  7. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/utils/__init__.py +25 -0
  8. cwms_cli-0.3.0/cwmscli/utils/colors.py +38 -0
  9. cwms_cli-0.3.0/cwmscli/utils/io.py +16 -0
  10. cwms_cli-0.3.0/cwmscli/utils/logging/__init__.py +82 -0
  11. cwms_cli-0.3.0/cwmscli/utils/logging/formatters.py +105 -0
  12. cwms_cli-0.3.0/cwmscli/utils/ssl_errors.py +77 -0
  13. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/pyproject.toml +4 -3
  14. cwms_cli-0.2.2/cwmscli/__init__.py +0 -14
  15. cwms_cli-0.2.2/cwmscli/__main__.py +0 -17
  16. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/LICENSE +0 -0
  17. {cwms_cli-0.2.2/cwmscli/commands/csv2cwms/tests → cwms_cli-0.3.0/cwmscli}/__init__.py +0 -0
  18. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/callbacks/__init__.py +0 -0
  19. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
  20. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/README.md +0 -0
  21. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
  22. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
  23. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
  24. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/hourly.json +0 -0
  25. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/examples/minutes.json +0 -0
  26. {cwms_cli-0.2.2/cwmscli/load → cwms_cli-0.3.0/cwmscli/commands/csv2cwms/tests}/__init__.py +0 -0
  27. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
  28. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
  29. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
  30. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
  31. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
  32. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
  33. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
  34. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
  35. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
  36. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
  37. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
  38. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
  39. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
  40. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/csv2cwms/utils/terminal.py +0 -0
  41. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/commands/shef_critfile_import.py +0 -0
  42. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/README.md +0 -0
  43. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/__main__.py +0 -0
  44. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/location/location.py +0 -0
  45. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/location/location_ids.py +0 -0
  46. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
  47. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/root.py +0 -0
  48. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/timeseries/timeseries.py +0 -0
  49. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
  50. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/requirements.py +0 -0
  51. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/__init__.py +0 -0
  52. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/__main__.py +0 -0
  53. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
  54. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/getusgs_cda.py +0 -0
  55. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
  56. {cwms_cli-0.2.2 → cwms_cli-0.3.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
  57. {cwms_cli-0.2.2 → 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.2.2
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
- [![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](https://cwms-cli.readthedocs.io/en/latest/) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
28
+ [![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](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
- [![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](https://cwms-cli.readthedocs.io/en/latest/) - 📖 Read the docs: https://cwms-cli.readthedocs.io/en/latest/
5
+ [![Docs](https://readthedocs.org/projects/cwms-cli/badge/?version=latest)](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, api_key=get_api_key(api_key, ""))
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, api_key=get_api_key(api_key, None))
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
@@ -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.2.2"
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 = { version = "^9.0.2", python = ">=3.10" }
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__:cli"
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