cwms-cli 0.3.8__tar.gz → 0.4.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.3.8 → cwms_cli-0.4.0}/PKG-INFO +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/__main__.py +1 -0
- cwms_cli-0.4.0/cwmscli/callbacks/__init__.py +27 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/commands_cwms.py +154 -1
- cwms_cli-0.4.0/cwmscli/commands/users.py +420 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/requirements.py +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/__init__.py +12 -1
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/click_help.py +3 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/colors.py +0 -2
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/deps.py +1 -1
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/pyproject.toml +6 -2
- cwms_cli-0.3.8/cwmscli/callbacks/__init__.py +0 -18
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/LICENSE +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/blob.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/.gitignore +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/config.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/doclinks.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/examples/complete_config.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/parser.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/data/.gitignore +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/data/sample_brok.csv +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/data/sample_config.json +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/skip_test_integration_pipeline.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/test_dateutils.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/test_expressions.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/test_fileio.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/test_main.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/transform.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/utils/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/utils/dateutils.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/utils/expression.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/utils/fileio.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/utils/logging.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/writer.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/shef_critfile_import.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/README.md +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/location/location.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/location/location_ids.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/location/location_ids_bygroup.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/root.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/timeseries/timeseries.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/load/timeseries/timeseries_ids.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/__main__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/getUSGS_ratings_cda.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/getusgs_cda.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/getusgs_measurements_cda.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/usgs/rating_ini_file_import.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/friendly_errors.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/intervals.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/io.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/links.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/logging/__init__.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/logging/formatters.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/ssl_errors.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/update.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/version.py +0 -0
- {cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/utils/version_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cwms-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -85,6 +85,7 @@ cli.add_command(commands_cwms.shefcritimport)
|
|
|
85
85
|
cli.add_command(commands_cwms.csv2cwms_cmd)
|
|
86
86
|
cli.add_command(commands_cwms.update_cli_cmd)
|
|
87
87
|
cli.add_command(commands_cwms.blob_group)
|
|
88
|
+
cli.add_command(commands_cwms.users_group)
|
|
88
89
|
cli.add_command(load.load_group)
|
|
89
90
|
add_version_to_help_tree(cli)
|
|
90
91
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Click callbacks for click
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def csv_to_list(ctx, param, value):
|
|
7
|
+
"""Accept multiple values either via repeated flags or a single delimiter-separated string.
|
|
8
|
+
|
|
9
|
+
Supported delimiters are comma (,) and pipe (|) to make CLI usage easier and avoid
|
|
10
|
+
shell pipe interpretation issues when users type role shortcuts.
|
|
11
|
+
"""
|
|
12
|
+
if value is None:
|
|
13
|
+
return None
|
|
14
|
+
if isinstance(value, (list, tuple)):
|
|
15
|
+
out = []
|
|
16
|
+
for v in value:
|
|
17
|
+
if isinstance(v, str) and ("," in v or "|" in v):
|
|
18
|
+
for part in re.split(r"[,|]", v):
|
|
19
|
+
part = part.strip()
|
|
20
|
+
if part:
|
|
21
|
+
out.append(part)
|
|
22
|
+
else:
|
|
23
|
+
out.append(v)
|
|
24
|
+
return tuple(out)
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return tuple(p.strip() for p in re.split(r"[,|]", value) if p.strip())
|
|
27
|
+
return value
|
|
@@ -9,7 +9,17 @@ import click
|
|
|
9
9
|
from cwmscli import requirements as reqs
|
|
10
10
|
from cwmscli.callbacks import csv_to_list
|
|
11
11
|
from cwmscli.commands import csv2cwms
|
|
12
|
-
from cwmscli.utils import
|
|
12
|
+
from cwmscli.utils import (
|
|
13
|
+
api_key_loc_option,
|
|
14
|
+
api_key_option,
|
|
15
|
+
api_root_option,
|
|
16
|
+
colors,
|
|
17
|
+
common_api_options,
|
|
18
|
+
get_api_key,
|
|
19
|
+
office_option,
|
|
20
|
+
office_option_notrequired,
|
|
21
|
+
to_uppercase,
|
|
22
|
+
)
|
|
13
23
|
from cwmscli.utils.deps import requires
|
|
14
24
|
from cwmscli.utils.update import (
|
|
15
25
|
build_update_package_spec,
|
|
@@ -393,3 +403,146 @@ def list_cmd(**kwargs):
|
|
|
393
403
|
from cwmscli.commands.blob import list_cmd
|
|
394
404
|
|
|
395
405
|
list_cmd(**kwargs)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ================================================================================
|
|
409
|
+
# USERS
|
|
410
|
+
# ================================================================================
|
|
411
|
+
user_name_option = click.option(
|
|
412
|
+
"-u",
|
|
413
|
+
"--user-name",
|
|
414
|
+
type=str,
|
|
415
|
+
default=None,
|
|
416
|
+
required=True,
|
|
417
|
+
help="Existing user name.",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@click.group(
|
|
422
|
+
"users",
|
|
423
|
+
help="Manage CWMS users and user-management roles",
|
|
424
|
+
)
|
|
425
|
+
def users_group():
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@users_group.command(
|
|
430
|
+
"user-ids",
|
|
431
|
+
help="List all available user IDs for an office or lookup using like filter",
|
|
432
|
+
)
|
|
433
|
+
@office_option_notrequired
|
|
434
|
+
@api_root_option
|
|
435
|
+
@api_key_option
|
|
436
|
+
@api_key_loc_option
|
|
437
|
+
@click.option(
|
|
438
|
+
"-ul",
|
|
439
|
+
"--username-like",
|
|
440
|
+
type=str,
|
|
441
|
+
default=None,
|
|
442
|
+
help="Enter any part of a user name to filter user id listing. Case-insensitive.",
|
|
443
|
+
)
|
|
444
|
+
@requires(reqs.cwms)
|
|
445
|
+
def users_user_ids(office, api_root, api_key, api_key_loc, username_like):
|
|
446
|
+
from cwmscli.commands.users import list_user_ids
|
|
447
|
+
|
|
448
|
+
list_user_ids(
|
|
449
|
+
office=office,
|
|
450
|
+
api_root=api_root,
|
|
451
|
+
api_key=api_key,
|
|
452
|
+
api_key_loc=api_key_loc,
|
|
453
|
+
username_like=username_like,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@click.group(
|
|
458
|
+
"roles",
|
|
459
|
+
help="Manage CWMS users and user-management roles",
|
|
460
|
+
)
|
|
461
|
+
def roles_group():
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@roles_group.command(
|
|
466
|
+
"list-all",
|
|
467
|
+
help="List assignable CWMS user-management roles",
|
|
468
|
+
)
|
|
469
|
+
@api_root_option
|
|
470
|
+
@api_key_option
|
|
471
|
+
@api_key_loc_option
|
|
472
|
+
@requires(reqs.cwms)
|
|
473
|
+
def users_roles_list_all(api_root, api_key, api_key_loc):
|
|
474
|
+
from cwmscli.commands.users import list_roles
|
|
475
|
+
|
|
476
|
+
list_roles(api_root=api_root, api_key=api_key, api_key_loc=api_key_loc)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@roles_group.command("list-user", help="List roles for a specific user and office")
|
|
480
|
+
@user_name_option
|
|
481
|
+
@office_option_notrequired
|
|
482
|
+
@api_root_option
|
|
483
|
+
@api_key_option
|
|
484
|
+
@api_key_loc_option
|
|
485
|
+
@requires(reqs.cwms)
|
|
486
|
+
def users_roles_list_user(user_name, office, api_root, api_key, api_key_loc):
|
|
487
|
+
from cwmscli.commands.users import list_user_roles
|
|
488
|
+
|
|
489
|
+
list_user_roles(
|
|
490
|
+
user_name=user_name,
|
|
491
|
+
office=office,
|
|
492
|
+
api_root=api_root,
|
|
493
|
+
api_key=api_key,
|
|
494
|
+
api_key_loc=api_key_loc,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@roles_group.command("add", help="Add one or more roles to an existing user")
|
|
499
|
+
@common_api_options
|
|
500
|
+
@api_key_loc_option
|
|
501
|
+
@user_name_option
|
|
502
|
+
@click.option(
|
|
503
|
+
"--roles",
|
|
504
|
+
multiple=True,
|
|
505
|
+
default=None,
|
|
506
|
+
callback=csv_to_list,
|
|
507
|
+
help="enter admin, readonly, readwrite, or individual role name(s) to add. Repeat the option or pass a comma/pipe-separated list.",
|
|
508
|
+
)
|
|
509
|
+
@requires(reqs.cwms)
|
|
510
|
+
def users_roles_add(office, api_root, api_key, api_key_loc, user_name, roles):
|
|
511
|
+
from cwmscli.commands.users import add_roles
|
|
512
|
+
|
|
513
|
+
add_roles(
|
|
514
|
+
user_name=user_name,
|
|
515
|
+
roles=roles,
|
|
516
|
+
office=office,
|
|
517
|
+
api_root=api_root,
|
|
518
|
+
api_key=api_key,
|
|
519
|
+
api_key_loc=api_key_loc,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@roles_group.command("delete", help="Remove one or more roles from an existing user")
|
|
524
|
+
@common_api_options
|
|
525
|
+
@api_key_loc_option
|
|
526
|
+
@user_name_option
|
|
527
|
+
@click.option(
|
|
528
|
+
"--roles",
|
|
529
|
+
multiple=True,
|
|
530
|
+
default=None,
|
|
531
|
+
callback=csv_to_list,
|
|
532
|
+
help="enter 'all' to delete all roles, or admin, readonly, readwrite, or individual role name(s) to delete. Repeat the option or pass a comma/pipe-separated list.",
|
|
533
|
+
)
|
|
534
|
+
@requires(reqs.cwms)
|
|
535
|
+
def users_roles_delete(office, api_root, api_key, api_key_loc, user_name, roles):
|
|
536
|
+
from cwmscli.commands.users import delete_roles
|
|
537
|
+
|
|
538
|
+
delete_roles(
|
|
539
|
+
user_name=user_name,
|
|
540
|
+
roles=roles,
|
|
541
|
+
office=office,
|
|
542
|
+
api_root=api_root,
|
|
543
|
+
api_key=api_key,
|
|
544
|
+
api_key_loc=api_key_loc,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
users_group.add_command(roles_group)
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from cwmscli.utils import colors, get_api_key
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _format_table(headers: list[str], rows: list[list[str]]) -> str:
|
|
11
|
+
widths = [len(header) for header in headers]
|
|
12
|
+
for row in rows:
|
|
13
|
+
for idx, cell in enumerate(row):
|
|
14
|
+
widths[idx] = max(widths[idx], len(cell))
|
|
15
|
+
|
|
16
|
+
def render_row(row: list[str]) -> str:
|
|
17
|
+
return " ".join(cell.ljust(widths[idx]) for idx, cell in enumerate(row))
|
|
18
|
+
|
|
19
|
+
divider = " ".join("-" * width for width in widths)
|
|
20
|
+
lines = [render_row(headers), divider]
|
|
21
|
+
lines.extend(render_row(row) for row in rows)
|
|
22
|
+
return "\n".join(lines)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _cmd(text: str) -> str:
|
|
26
|
+
return colors.c(text, "blue", bright=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_api_error(error: Exception, cwms_module) -> str:
|
|
30
|
+
message = str(error)
|
|
31
|
+
if isinstance(error, cwms_module.api.PermissionError):
|
|
32
|
+
status_text = "CDA responded with 403 Forbidden."
|
|
33
|
+
if status_text in message:
|
|
34
|
+
message = message.replace(status_text, colors.err(status_text))
|
|
35
|
+
message = (
|
|
36
|
+
f"{message} Use an admin API key or sign in as a user with user-management "
|
|
37
|
+
"admin roles. I.e. 'CWMS User Admins'"
|
|
38
|
+
)
|
|
39
|
+
return message
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _handle_api_error(error: Exception, cwms_module) -> None:
|
|
43
|
+
raise click.ClickException(_format_api_error(error, cwms_module)) from None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _init_cwms(api_root: str, api_key: str, api_key_loc: str) -> object:
|
|
47
|
+
import cwms
|
|
48
|
+
|
|
49
|
+
resolved_api_key = get_api_key(api_key, api_key_loc)
|
|
50
|
+
cwms.init_session(api_root=api_root, api_key=resolved_api_key)
|
|
51
|
+
return cwms
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fetch_roles(cwms_module) -> list[str]:
|
|
55
|
+
try:
|
|
56
|
+
return sorted(cwms_module.get_roles(), key=str.casefold)
|
|
57
|
+
except cwms_module.api.ApiError as error:
|
|
58
|
+
_handle_api_error(error, cwms_module)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _fetch_users(
|
|
62
|
+
cwms_module, office=None, username_like=None, page_size: int = 5000
|
|
63
|
+
) -> list[dict]:
|
|
64
|
+
users: list[dict] = []
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
response = cwms_module.get_users(
|
|
68
|
+
office_id=office, username_like=username_like, page_size=page_size
|
|
69
|
+
)
|
|
70
|
+
except cwms_module.api.ApiError as error:
|
|
71
|
+
_handle_api_error(error, cwms_module)
|
|
72
|
+
|
|
73
|
+
payload = getattr(response, "json", {}) or {}
|
|
74
|
+
page_users = payload.get("users", [])
|
|
75
|
+
users.extend(page_users)
|
|
76
|
+
|
|
77
|
+
return users
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _fetch_user_roles(
|
|
81
|
+
cwms_module, user_name: str, office: Optional[str] = None
|
|
82
|
+
) -> list[str]:
|
|
83
|
+
roles_payload = _get_user_roles_payload(cwms_module, user_name)
|
|
84
|
+
return _extract_roles_from_payload(roles_payload, office)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_user_roles_payload(cwms_module, user_name: str) -> dict:
|
|
88
|
+
try:
|
|
89
|
+
payload = cwms_module.get_user(user_name=user_name)
|
|
90
|
+
return payload.get("roles", {})
|
|
91
|
+
except cwms_module.api.ApiError as error:
|
|
92
|
+
_handle_api_error(error, cwms_module)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_roles_from_payload(
|
|
96
|
+
roles_payload: dict, office: Optional[str] = None
|
|
97
|
+
) -> list[str]:
|
|
98
|
+
if isinstance(roles_payload, dict):
|
|
99
|
+
if office:
|
|
100
|
+
office_key = office.strip().upper()
|
|
101
|
+
office_roles = roles_payload.get(office_key, [])
|
|
102
|
+
if office_roles is None:
|
|
103
|
+
office_roles = []
|
|
104
|
+
return [r.strip() for r in office_roles if r and r.strip()]
|
|
105
|
+
all_roles = []
|
|
106
|
+
for rlist in roles_payload.values():
|
|
107
|
+
if rlist:
|
|
108
|
+
all_roles.extend(rlist)
|
|
109
|
+
return sorted(
|
|
110
|
+
{r.strip() for r in all_roles if r and r.strip()}, key=str.casefold
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if isinstance(roles_payload, list):
|
|
114
|
+
return [r.strip() for r in roles_payload if r and r.strip()]
|
|
115
|
+
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _existing_user_name(users: list[dict], user_name: str) -> Optional[str]:
|
|
120
|
+
|
|
121
|
+
requested = user_name.strip().casefold()
|
|
122
|
+
for user in users:
|
|
123
|
+
existing = str(user.get("user-name", "")).strip()
|
|
124
|
+
if existing and existing.casefold() == requested:
|
|
125
|
+
return existing
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _split_roles(
|
|
130
|
+
raw_roles: Optional[Union[tuple[str, ...], list[str]]] = None
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
if not raw_roles:
|
|
133
|
+
return []
|
|
134
|
+
return [role.strip() for role in raw_roles if role and role.strip()]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _expand_role_shortcuts(roles: list[str]) -> list[str]:
|
|
138
|
+
emap = {
|
|
139
|
+
"admin": ["All Users", "CWMS Users", "TS ID Creator", "CWMS User Admins"],
|
|
140
|
+
"readonly": ["All Users", "CWMS Users"],
|
|
141
|
+
"readwrite": ["All Users", "CWMS Users", "TS ID Creator"],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
expanded_roles: list[str] = []
|
|
145
|
+
for role in roles:
|
|
146
|
+
key = role.strip().casefold()
|
|
147
|
+
if key == "all":
|
|
148
|
+
expanded_roles.append(role) # Keep "all" as is, handled elsewhere
|
|
149
|
+
elif key in emap:
|
|
150
|
+
expanded_roles.extend(emap[key])
|
|
151
|
+
else:
|
|
152
|
+
expanded_roles.append(role)
|
|
153
|
+
return expanded_roles
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_role_inputs(
|
|
157
|
+
users: list[dict], available_roles: list[str], user_name: str, roles: list[str]
|
|
158
|
+
) -> tuple[str, list[str]]:
|
|
159
|
+
existing_user = _existing_user_name(users, user_name)
|
|
160
|
+
if not existing_user:
|
|
161
|
+
raise click.ClickException(f"User '{user_name}' was not found in CDA /users.")
|
|
162
|
+
|
|
163
|
+
roles_lookup = {role.casefold(): role for role in available_roles}
|
|
164
|
+
normalized_roles: list[str] = []
|
|
165
|
+
invalid_roles: list[str] = []
|
|
166
|
+
|
|
167
|
+
for role in roles:
|
|
168
|
+
normalized = roles_lookup.get(role.casefold())
|
|
169
|
+
if normalized:
|
|
170
|
+
normalized_roles.append(normalized)
|
|
171
|
+
else:
|
|
172
|
+
invalid_roles.append(role)
|
|
173
|
+
|
|
174
|
+
if invalid_roles:
|
|
175
|
+
invalid = ", ".join(colors.err(role) for role in invalid_roles)
|
|
176
|
+
raise click.ClickException(
|
|
177
|
+
f"Unknown role value(s): {invalid}. Run {_cmd('cwms-cli users roles')} to see the valid role catalog."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
deduped_roles = []
|
|
181
|
+
seen_roles = set()
|
|
182
|
+
for role in normalized_roles:
|
|
183
|
+
if role not in seen_roles:
|
|
184
|
+
seen_roles.add(role)
|
|
185
|
+
deduped_roles.append(role)
|
|
186
|
+
|
|
187
|
+
if not deduped_roles:
|
|
188
|
+
raise click.ClickException("At least one valid role is required.")
|
|
189
|
+
|
|
190
|
+
return existing_user, deduped_roles
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _render_roles_table(roles: list[str]) -> str:
|
|
194
|
+
rows = [[colors.c(role, "green")] for role in roles]
|
|
195
|
+
return _format_table(["Role"], rows)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _render_user_roles_table(user_roles: dict[str, list[str]]) -> str:
|
|
199
|
+
rows = []
|
|
200
|
+
for office, roles in sorted(user_roles.items()):
|
|
201
|
+
roles_str = ", ".join(sorted(roles))
|
|
202
|
+
rows.append([colors.c(office, "cyan"), colors.c(roles_str, "green")])
|
|
203
|
+
return _format_table(["Office", "Roles"], rows)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _render_users_table(users: list[dict]) -> str:
|
|
207
|
+
rows = [[colors.c(user.get("user-name", ""), "green")] for user in users]
|
|
208
|
+
return _format_table(["User Name"], rows)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _prompt_for_role_inputs(
|
|
212
|
+
action: str, available_roles: list[str]
|
|
213
|
+
) -> tuple[str, list[str]]:
|
|
214
|
+
click.echo(f"Enter the target user and one or more roles to {action}.")
|
|
215
|
+
click.echo("")
|
|
216
|
+
user_name = click.prompt("User name", type=str).strip()
|
|
217
|
+
click.echo("")
|
|
218
|
+
click.echo("Available roles:")
|
|
219
|
+
click.echo(_render_roles_table(available_roles))
|
|
220
|
+
click.echo("")
|
|
221
|
+
roles_raw = click.prompt(
|
|
222
|
+
"Roles (comma-separated; use names exactly as listed; or shortcuts admin/readwrite/readonly)",
|
|
223
|
+
type=str,
|
|
224
|
+
).strip()
|
|
225
|
+
roles = [role.strip() for role in roles_raw.split(",") if role.strip()]
|
|
226
|
+
return user_name, roles
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _prompt_for_office(office: str) -> str:
|
|
230
|
+
colored_office = colors.c(office, "cyan", bright=True)
|
|
231
|
+
change_office = click.confirm(
|
|
232
|
+
f"You have office set to {colored_office}, would you like to change this?",
|
|
233
|
+
default=False,
|
|
234
|
+
)
|
|
235
|
+
if not change_office:
|
|
236
|
+
return office
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
click.prompt("Office", type=str, default=office, show_default=True)
|
|
240
|
+
.strip()
|
|
241
|
+
.upper()
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def list_roles(api_root: str, api_key: str, api_key_loc: str) -> None:
|
|
246
|
+
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
247
|
+
roles = _fetch_roles(cwms)
|
|
248
|
+
header_count = colors.c(str(len(roles)), "cyan", bright=True)
|
|
249
|
+
click.echo(f"Available roles for user management: {header_count}")
|
|
250
|
+
if not roles:
|
|
251
|
+
click.echo(colors.warn("No roles were returned."))
|
|
252
|
+
return
|
|
253
|
+
click.echo(_render_roles_table(roles))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def list_user_roles(
|
|
257
|
+
user_name: str, office: Optional[str], api_root: str, api_key: str, api_key_loc: str
|
|
258
|
+
) -> None:
|
|
259
|
+
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
260
|
+
roles_payload = _get_user_roles_payload(cwms, user_name)
|
|
261
|
+
if isinstance(roles_payload, dict):
|
|
262
|
+
if office:
|
|
263
|
+
office_key = office.strip().upper()
|
|
264
|
+
office_roles = roles_payload.get(office_key, [])
|
|
265
|
+
if office_roles is None:
|
|
266
|
+
office_roles = []
|
|
267
|
+
user_roles = [r.strip() for r in office_roles if r and r.strip()]
|
|
268
|
+
header = (
|
|
269
|
+
f"Roles for user '{user_name}' in office '{office}': {len(user_roles)}"
|
|
270
|
+
)
|
|
271
|
+
click.echo(colors.c(header, "cyan", bright=True))
|
|
272
|
+
if not user_roles:
|
|
273
|
+
click.echo(colors.warn("No roles found."))
|
|
274
|
+
return
|
|
275
|
+
click.echo(_render_roles_table(user_roles))
|
|
276
|
+
else:
|
|
277
|
+
# List roles for each office
|
|
278
|
+
header = f"Roles for user '{user_name}' across all offices:"
|
|
279
|
+
click.echo(colors.c(header, "cyan", bright=True))
|
|
280
|
+
for off, roles in roles_payload.items():
|
|
281
|
+
if roles:
|
|
282
|
+
cleaned_roles = [r.strip() for r in roles if r and r.strip()]
|
|
283
|
+
if cleaned_roles:
|
|
284
|
+
click.echo(
|
|
285
|
+
colors.c(
|
|
286
|
+
f"Office '{off}': {len(cleaned_roles)} roles", "yellow"
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
click.echo(_render_roles_table(cleaned_roles))
|
|
290
|
+
click.echo("")
|
|
291
|
+
else:
|
|
292
|
+
click.echo(colors.c(f"Office '{off}': No roles", "yellow"))
|
|
293
|
+
click.echo("")
|
|
294
|
+
else:
|
|
295
|
+
click.echo(colors.warn("Unexpected roles format."))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def list_user_ids(
|
|
299
|
+
office: str,
|
|
300
|
+
api_root: str,
|
|
301
|
+
api_key: str,
|
|
302
|
+
api_key_loc: str,
|
|
303
|
+
username_like: Optional[str] = None,
|
|
304
|
+
) -> None:
|
|
305
|
+
# This command is wired from user roles group default action. Users can
|
|
306
|
+
# pass an optional name filter but for now roles view is what should be shown.
|
|
307
|
+
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
308
|
+
users = _fetch_users(cwms, office, username_like=username_like)
|
|
309
|
+
click.echo(_render_users_table(users))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def add_roles(
|
|
313
|
+
office: str,
|
|
314
|
+
api_root: str,
|
|
315
|
+
api_key: str,
|
|
316
|
+
api_key_loc: str,
|
|
317
|
+
user_name: Optional[str],
|
|
318
|
+
roles: Optional[Union[tuple[str, ...], list[str]]],
|
|
319
|
+
) -> None:
|
|
320
|
+
provided_user_name = (user_name or "").strip()
|
|
321
|
+
provided_roles = _split_roles(roles)
|
|
322
|
+
|
|
323
|
+
if bool(provided_user_name) ^ bool(provided_roles):
|
|
324
|
+
raise click.ClickException(
|
|
325
|
+
"Either specify all add arguments (--user-name and --roles) or run "
|
|
326
|
+
f"{_cmd('cwms-cli users roles add')} interactively with no add-specific args."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
330
|
+
users = _fetch_users(cwms)
|
|
331
|
+
available_roles = _fetch_roles(cwms)
|
|
332
|
+
|
|
333
|
+
if not provided_user_name and not provided_roles:
|
|
334
|
+
office = _prompt_for_office(office)
|
|
335
|
+
provided_user_name, provided_roles = _prompt_for_role_inputs(
|
|
336
|
+
"add", available_roles
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
provided_roles = _expand_role_shortcuts(provided_roles)
|
|
340
|
+
existing_user, validated_roles = _validate_role_inputs(
|
|
341
|
+
users, available_roles, provided_user_name, provided_roles
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
cwms.store_user(
|
|
346
|
+
user_name=existing_user, office_id=office, roles=validated_roles
|
|
347
|
+
)
|
|
348
|
+
except cwms.api.ApiError as error:
|
|
349
|
+
_handle_api_error(error, cwms)
|
|
350
|
+
|
|
351
|
+
click.echo(
|
|
352
|
+
f"Added {len(validated_roles)} role(s) to user "
|
|
353
|
+
f"{colors.c(existing_user, 'cyan', bright=True)} for office "
|
|
354
|
+
f"{colors.c(office, 'cyan', bright=True)}."
|
|
355
|
+
)
|
|
356
|
+
click.echo(_render_roles_table(validated_roles))
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def delete_roles(
|
|
360
|
+
office: str,
|
|
361
|
+
api_root: str,
|
|
362
|
+
api_key: str,
|
|
363
|
+
api_key_loc: str,
|
|
364
|
+
user_name: Optional[str],
|
|
365
|
+
roles: Optional[Union[tuple[str, ...], list[str]]],
|
|
366
|
+
) -> None:
|
|
367
|
+
provided_user_name = (user_name or "").strip()
|
|
368
|
+
provided_roles = _split_roles(roles)
|
|
369
|
+
provided_roles = _expand_role_shortcuts(provided_roles)
|
|
370
|
+
|
|
371
|
+
if bool(provided_user_name) ^ bool(provided_roles):
|
|
372
|
+
raise click.ClickException(
|
|
373
|
+
"Either specify all delete arguments (--user-name and --roles) or run "
|
|
374
|
+
f"{_cmd('cwms-cli users roles delete')} interactively with no delete-specific args."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
cwms = _init_cwms(api_root, api_key, api_key_loc)
|
|
378
|
+
users = _fetch_users(cwms, office)
|
|
379
|
+
available_roles = _fetch_roles(cwms)
|
|
380
|
+
|
|
381
|
+
if not provided_user_name and not provided_roles:
|
|
382
|
+
office = _prompt_for_office(office)
|
|
383
|
+
provided_user_name, provided_roles = _prompt_for_role_inputs(
|
|
384
|
+
"delete", available_roles
|
|
385
|
+
)
|
|
386
|
+
provided_roles = _expand_role_shortcuts(provided_roles)
|
|
387
|
+
|
|
388
|
+
if provided_roles and any(r.strip().casefold() == "all" for r in provided_roles):
|
|
389
|
+
if len(provided_roles) > 1:
|
|
390
|
+
raise click.ClickException("'all' cannot be specified with other roles.")
|
|
391
|
+
existing_user = _existing_user_name(users, provided_user_name)
|
|
392
|
+
if not existing_user:
|
|
393
|
+
raise click.ClickException(f"User '{provided_user_name}' not found.")
|
|
394
|
+
fetched_roles = _fetch_user_roles(cwms, existing_user, office)
|
|
395
|
+
provided_roles = fetched_roles
|
|
396
|
+
|
|
397
|
+
existing_user, validated_roles = _validate_role_inputs(
|
|
398
|
+
users, available_roles, provided_user_name, provided_roles
|
|
399
|
+
)
|
|
400
|
+
if "All Users" in validated_roles:
|
|
401
|
+
validated_roles.remove("All Users")
|
|
402
|
+
click.echo(
|
|
403
|
+
colors.warn(
|
|
404
|
+
"Warning: 'All Users' role cannot be deleted directly and will remain assigned. "
|
|
405
|
+
"Any other specified roles will be deleted as requested."
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
try:
|
|
409
|
+
cwms.delete_user_roles(
|
|
410
|
+
user_name=existing_user, office_id=office, roles=validated_roles
|
|
411
|
+
)
|
|
412
|
+
except cwms.api.ApiError as error:
|
|
413
|
+
_handle_api_error(error, cwms)
|
|
414
|
+
|
|
415
|
+
click.echo(
|
|
416
|
+
f"Deleted {len(validated_roles)} role(s) from user "
|
|
417
|
+
f"{colors.c(existing_user, 'cyan', bright=True)} for office "
|
|
418
|
+
f"{colors.c(office, 'cyan', bright=True)}."
|
|
419
|
+
)
|
|
420
|
+
click.echo(_render_roles_table(validated_roles))
|
|
@@ -42,6 +42,16 @@ office_option = click.option(
|
|
|
42
42
|
callback=to_uppercase,
|
|
43
43
|
help="Office to grab data for",
|
|
44
44
|
)
|
|
45
|
+
office_option_notrequired = click.option(
|
|
46
|
+
"-o",
|
|
47
|
+
"--office",
|
|
48
|
+
default=None,
|
|
49
|
+
required=False,
|
|
50
|
+
envvar="OFFICE",
|
|
51
|
+
type=str,
|
|
52
|
+
callback=to_uppercase,
|
|
53
|
+
help="Office to grab data for",
|
|
54
|
+
)
|
|
45
55
|
api_root_option = click.option(
|
|
46
56
|
"-a",
|
|
47
57
|
"--api-root",
|
|
@@ -82,7 +92,8 @@ log_level_option = click.option(
|
|
|
82
92
|
envvar="LOG_LEVEL",
|
|
83
93
|
callback=_set_log_level,
|
|
84
94
|
expose_value=False, # Callback will set the log level of all methods
|
|
85
|
-
|
|
95
|
+
# Run before other commands (to cover any logging statements)
|
|
96
|
+
is_eager=True,
|
|
86
97
|
help="Set logging verbosity (overrides default INFO).",
|
|
87
98
|
)
|
|
88
99
|
|
|
@@ -41,6 +41,9 @@ def _docs_url_for_context(ctx: click.Context) -> Optional[str]:
|
|
|
41
41
|
command = (ctx.info_name or getattr(ctx.command, "name", None) or "").strip()
|
|
42
42
|
page_map = {
|
|
43
43
|
"blob": "blob",
|
|
44
|
+
"update": "update",
|
|
45
|
+
"users": "users",
|
|
46
|
+
"version": "version",
|
|
44
47
|
}
|
|
45
48
|
# Link to dedicated pages that are created in \docs
|
|
46
49
|
if command in page_map:
|
|
@@ -17,8 +17,6 @@ def c(text: str, color: str, bright: bool = False) -> str:
|
|
|
17
17
|
# Find the color in Fore and apply it to the text, then reset the style at the end
|
|
18
18
|
if hasattr(Fore, color.upper()):
|
|
19
19
|
color = getattr(Fore, color.upper())
|
|
20
|
-
else:
|
|
21
|
-
color = ""
|
|
22
20
|
return f"{color}{b}{text}{Style.RESET_ALL}"
|
|
23
21
|
|
|
24
22
|
|
|
@@ -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.4.0"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
packages = [
|
|
@@ -18,7 +18,7 @@ authors = ["Eric Novotny <eric.v.novotny@usace.army.mil>", "Charles Graham <char
|
|
|
18
18
|
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
|
-
cwms-python = { version = ">=0.
|
|
21
|
+
cwms-python = { version = ">=1.0.7", optional = true}
|
|
22
22
|
colorama = "^0.4.6"
|
|
23
23
|
|
|
24
24
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -30,6 +30,10 @@ pytest = "^8.3.5"
|
|
|
30
30
|
#pytest-cov = "^4.1.0"
|
|
31
31
|
#pandas-stubs = "^2.2.1.240316"
|
|
32
32
|
yamlfix = "^1.16.0"
|
|
33
|
+
pandas = "^2.1.3"
|
|
34
|
+
cwms-python = "^1.0.7"
|
|
35
|
+
hecdss = "^0.1.24"
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
[build-system]
|
|
35
39
|
requires = ["poetry-core"]
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Click callbacks for click
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def csv_to_list(ctx, param, value):
|
|
5
|
-
"""Accept multiple values either via repeated flags or a single comma-delimited string."""
|
|
6
|
-
if value is None:
|
|
7
|
-
return None
|
|
8
|
-
if isinstance(value, (list, tuple)):
|
|
9
|
-
out = []
|
|
10
|
-
for v in value:
|
|
11
|
-
if isinstance(v, str) and "," in v:
|
|
12
|
-
out.extend([p.strip() for p in v.split(",") if p.strip()])
|
|
13
|
-
else:
|
|
14
|
-
out.append(v)
|
|
15
|
-
return tuple(out)
|
|
16
|
-
if isinstance(value, str):
|
|
17
|
-
return tuple([p.strip() for p in value.split(",") if p.strip()])
|
|
18
|
-
return value
|
|
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
|
{cwms_cli-0.3.8 → cwms_cli-0.4.0}/cwmscli/commands/csv2cwms/tests/data/expected_brok_output.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cwms_cli-0.3.8 → cwms_cli-0.4.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
|
|
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
|