bt-cli 0.4.13__py3-none-any.whl

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 (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,415 @@
1
+ """CLI commands for managing users and user groups."""
2
+
3
+ from typing import Optional
4
+ import json
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...core.output import print_api_error
12
+ from ..client.base import get_client
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="Manage users, user groups, and roles")
15
+ console = Console()
16
+
17
+
18
+ def print_users_table(users: list[dict], title: str = "Users") -> None:
19
+ """Print users in a formatted table."""
20
+ table = Table(title=title)
21
+ table.add_column("ID", style="cyan", no_wrap=True)
22
+ table.add_column("Username", style="green")
23
+ table.add_column("Name", style="yellow")
24
+ table.add_column("Email", style="blue")
25
+ table.add_column("Active", style="magenta", no_wrap=True)
26
+ table.add_column("Last Login", style="dim")
27
+
28
+ for user in users:
29
+ # Build display name from first/last
30
+ first = user.get("FirstName") or ""
31
+ last = user.get("LastName") or ""
32
+ display_name = f"{first} {last}".strip() or "-"
33
+
34
+ # Format last login
35
+ last_login = user.get("LastLoginDate")
36
+ if last_login:
37
+ # Just show date part
38
+ last_login = str(last_login)[:10]
39
+ else:
40
+ last_login = "-"
41
+
42
+ # Check if API user
43
+ is_api = "ClientID" in user and user.get("ClientID")
44
+ username = user.get("UserName", "")
45
+ if is_api:
46
+ username = f"{username} [dim](API)[/dim]"
47
+
48
+ table.add_row(
49
+ str(user.get("UserID", "")),
50
+ username,
51
+ display_name,
52
+ user.get("EmailAddress") or "-",
53
+ "Yes" if user.get("IsActive") else "No",
54
+ last_login,
55
+ )
56
+
57
+ console.print(table)
58
+
59
+
60
+ def print_user_detail(user: dict) -> None:
61
+ """Print detailed user information."""
62
+ console.print(f"\n[bold cyan]User: {user.get('UserName', 'Unknown')}[/bold cyan]\n")
63
+
64
+ info_table = Table(show_header=False, box=None)
65
+ info_table.add_column("Field", style="dim", width=25)
66
+ info_table.add_column("Value")
67
+
68
+ fields = [
69
+ ("ID", "UserID"),
70
+ ("Username", "UserName"),
71
+ ("First Name", "FirstName"),
72
+ ("Last Name", "LastName"),
73
+ ("Email", "EmailAddress"),
74
+ ("Domain", "DomainName"),
75
+ ("Distinguished Name", "DistinguishedName"),
76
+ ("Active", "IsActive"),
77
+ ("Quarantined", "IsQuarantined"),
78
+ ("Last Login", "LastLoginDate"),
79
+ ("Auth Type", "LastLoginAuthenticationType"),
80
+ ("SSO URL", "LastLoginSSOURL"),
81
+ ("SAML IDP URL", "LastLoginSAMLIDPURL"),
82
+ ("Config Name", "LastLoginConfigurationName"),
83
+ ]
84
+
85
+ for label, key in fields:
86
+ value = user.get(key)
87
+ if value is not None:
88
+ if isinstance(value, bool):
89
+ value = "Yes" if value else "No"
90
+ info_table.add_row(label, str(value))
91
+
92
+ console.print(info_table)
93
+
94
+ # Show API user details if present
95
+ if user.get("ClientID"):
96
+ console.print("\n[bold yellow]API User Details:[/bold yellow]")
97
+ api_table = Table(show_header=False, box=None)
98
+ api_table.add_column("Field", style="dim", width=25)
99
+ api_table.add_column("Value")
100
+ api_table.add_row("Client ID", user.get("ClientID", "-"))
101
+ api_table.add_row("Access Policy ID", str(user.get("AccessPolicyID", "-")))
102
+ console.print(api_table)
103
+
104
+
105
+ def print_groups_table(groups: list[dict], title: str = "User Groups") -> None:
106
+ """Print user groups in a formatted table."""
107
+ table = Table(title=title)
108
+ table.add_column("ID", style="cyan", no_wrap=True)
109
+ table.add_column("Name", style="green")
110
+ table.add_column("Description", style="yellow")
111
+ table.add_column("Type", style="blue")
112
+ table.add_column("Active", style="magenta", no_wrap=True)
113
+
114
+ for group in groups:
115
+ table.add_row(
116
+ str(group.get("GroupID", "")),
117
+ group.get("Name", ""),
118
+ group.get("Description") or "-",
119
+ group.get("GroupType") or "-",
120
+ "Yes" if group.get("IsActive") else "No",
121
+ )
122
+
123
+ console.print(table)
124
+
125
+
126
+ def print_group_detail(group: dict) -> None:
127
+ """Print detailed user group information."""
128
+ console.print(f"\n[bold cyan]User Group: {group.get('Name', 'Unknown')}[/bold cyan]\n")
129
+
130
+ info_table = Table(show_header=False, box=None)
131
+ info_table.add_column("Field", style="dim", width=30)
132
+ info_table.add_column("Value")
133
+
134
+ fields = [
135
+ ("ID", "GroupID"),
136
+ ("Name", "Name"),
137
+ ("Description", "Description"),
138
+ ("Type", "GroupType"),
139
+ ("Active", "IsActive"),
140
+ ("Role Type", "RoleType"),
141
+ ("Distinguished Name", "DistinguishedName"),
142
+ ("Excluded From Global Sync", "ExcludedFromGlobalSync"),
143
+ ("Override Global Sync Settings", "OverrideGlobalSyncSettings"),
144
+ ("Membership Attribute", "MembershipAttribute"),
145
+ ("Account Attribute", "AccountAttribute"),
146
+ ("Base Distinguished Name", "BaseDistinguishedName"),
147
+ ("Application Registration IDs", "ApplicationRegistrationIDs"),
148
+ ]
149
+
150
+ for label, key in fields:
151
+ value = group.get(key)
152
+ if value is not None:
153
+ if isinstance(value, bool):
154
+ value = "Yes" if value else "No"
155
+ info_table.add_row(label, str(value))
156
+
157
+ console.print(info_table)
158
+
159
+
160
+ def print_roles_table(roles: list[dict], title: str = "Roles") -> None:
161
+ """Print roles in a formatted table."""
162
+ table = Table(title=title)
163
+ table.add_column("ID", style="cyan", no_wrap=True)
164
+ table.add_column("Name", style="green")
165
+
166
+ for role in roles:
167
+ table.add_row(
168
+ str(role.get("RoleID", "")),
169
+ role.get("Name", ""),
170
+ )
171
+
172
+ console.print(table)
173
+
174
+
175
+ # =============================================================================
176
+ # Users Commands
177
+ # =============================================================================
178
+
179
+
180
+ @app.command("list")
181
+ def list_users(
182
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username"),
183
+ limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
184
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
185
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
186
+ ) -> None:
187
+ """List all users.
188
+
189
+ Examples:
190
+ bt pws users list # First 50 users
191
+ bt pws users list --all # All users
192
+ bt pws users list -s "admin" # Search by username
193
+ bt pws users list -o json # JSON output
194
+ """
195
+ try:
196
+ with get_client() as client:
197
+ client.authenticate()
198
+ users = client.list_users(search=search, limit=None if fetch_all else limit)
199
+
200
+ if output == "json":
201
+ console.print_json(json.dumps(users, default=str))
202
+ else:
203
+ if users:
204
+ print_users_table(users)
205
+ if not fetch_all and len(users) == limit:
206
+ console.print(
207
+ f"[dim]Showing {len(users)} results. Use --all to fetch all results.[/dim]"
208
+ )
209
+ else:
210
+ console.print("[yellow]No users found.[/yellow]")
211
+
212
+ except httpx.HTTPStatusError as e:
213
+ print_api_error(e, "list users")
214
+ raise typer.Exit(1)
215
+ except httpx.RequestError as e:
216
+ print_api_error(e, "list users")
217
+ raise typer.Exit(1)
218
+ except Exception as e:
219
+ print_api_error(e, "list users")
220
+ raise typer.Exit(1)
221
+
222
+
223
+ @app.command("get")
224
+ def get_user(
225
+ user_id: int = typer.Argument(..., help="User ID"),
226
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
227
+ ) -> None:
228
+ """Get a user by ID.
229
+
230
+ Examples:
231
+ bt pws users get 1 # Get user ID 1
232
+ bt pws users get 4 -o json # JSON output
233
+ """
234
+ try:
235
+ with get_client() as client:
236
+ client.authenticate()
237
+ user = client.get_user(user_id)
238
+
239
+ if output == "json":
240
+ console.print_json(json.dumps(user, default=str))
241
+ else:
242
+ print_user_detail(user)
243
+
244
+ except httpx.HTTPStatusError as e:
245
+ print_api_error(e, "get user")
246
+ raise typer.Exit(1)
247
+ except httpx.RequestError as e:
248
+ print_api_error(e, "get user")
249
+ raise typer.Exit(1)
250
+ except Exception as e:
251
+ print_api_error(e, "get user")
252
+ raise typer.Exit(1)
253
+
254
+
255
+ # =============================================================================
256
+ # User Groups Commands
257
+ # =============================================================================
258
+
259
+
260
+ @app.command("groups")
261
+ def list_groups(
262
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by group name"),
263
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
264
+ ) -> None:
265
+ """List all user groups.
266
+
267
+ Examples:
268
+ bt pws users groups # All user groups
269
+ bt pws users groups -s "admin" # Search groups
270
+ bt pws users groups -o json # JSON output
271
+ """
272
+ try:
273
+ with get_client() as client:
274
+ client.authenticate()
275
+ groups = client.list_user_groups(search=search)
276
+
277
+ if output == "json":
278
+ console.print_json(json.dumps(groups, default=str))
279
+ else:
280
+ if groups:
281
+ print_groups_table(groups)
282
+ else:
283
+ console.print("[yellow]No user groups found.[/yellow]")
284
+
285
+ except httpx.HTTPStatusError as e:
286
+ print_api_error(e, "list user groups")
287
+ raise typer.Exit(1)
288
+ except httpx.RequestError as e:
289
+ print_api_error(e, "list user groups")
290
+ raise typer.Exit(1)
291
+ except Exception as e:
292
+ print_api_error(e, "list user groups")
293
+ raise typer.Exit(1)
294
+
295
+
296
+ @app.command("group")
297
+ def get_group(
298
+ group_id: int = typer.Argument(..., help="User group ID"),
299
+ show_members: bool = typer.Option(False, "--members", "-m", help="Show group members"),
300
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
301
+ ) -> None:
302
+ """Get a user group by ID.
303
+
304
+ Examples:
305
+ bt pws users group 1 # Get group ID 1
306
+ bt pws users group 1 --members # Show group with members
307
+ bt pws users group 1 -o json # JSON output
308
+ """
309
+ try:
310
+ with get_client() as client:
311
+ client.authenticate()
312
+ group = client.get_user_group(group_id)
313
+
314
+ members = []
315
+ if show_members:
316
+ members = client.get_user_group_members(group_id)
317
+
318
+ if output == "json":
319
+ result = {"group": group, "members": members} if show_members else group
320
+ console.print_json(json.dumps(result, default=str))
321
+ else:
322
+ print_group_detail(group)
323
+ if show_members:
324
+ console.print(f"\n[bold]Members ({len(members)}):[/bold]")
325
+ if members:
326
+ print_users_table(members, title="Group Members")
327
+ else:
328
+ console.print("[dim]No members in this group.[/dim]")
329
+
330
+ except httpx.HTTPStatusError as e:
331
+ print_api_error(e, "get user group")
332
+ raise typer.Exit(1)
333
+ except httpx.RequestError as e:
334
+ print_api_error(e, "get user group")
335
+ raise typer.Exit(1)
336
+ except Exception as e:
337
+ print_api_error(e, "get user group")
338
+ raise typer.Exit(1)
339
+
340
+
341
+ @app.command("group-members")
342
+ def list_group_members(
343
+ group_id: int = typer.Argument(..., help="User group ID"),
344
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
345
+ ) -> None:
346
+ """List members of a user group.
347
+
348
+ Examples:
349
+ bt pws users group-members 1 # Members of group 1
350
+ bt pws users group-members 1 -o json # JSON output
351
+ """
352
+ try:
353
+ with get_client() as client:
354
+ client.authenticate()
355
+ group = client.get_user_group(group_id)
356
+ members = client.get_user_group_members(group_id)
357
+
358
+ group_name = group.get("Name", f"Group {group_id}")
359
+
360
+ if output == "json":
361
+ console.print_json(json.dumps(members, default=str))
362
+ else:
363
+ if members:
364
+ print_users_table(members, title=f"Members of '{group_name}'")
365
+ else:
366
+ console.print(f"[yellow]No members in group '{group_name}'.[/yellow]")
367
+
368
+ except httpx.HTTPStatusError as e:
369
+ print_api_error(e, "list group members")
370
+ raise typer.Exit(1)
371
+ except httpx.RequestError as e:
372
+ print_api_error(e, "list group members")
373
+ raise typer.Exit(1)
374
+ except Exception as e:
375
+ print_api_error(e, "list group members")
376
+ raise typer.Exit(1)
377
+
378
+
379
+ # =============================================================================
380
+ # Roles Commands
381
+ # =============================================================================
382
+
383
+
384
+ @app.command("roles")
385
+ def list_roles(
386
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
387
+ ) -> None:
388
+ """List all available roles.
389
+
390
+ Examples:
391
+ bt pws users roles # All roles
392
+ bt pws users roles -o json # JSON output
393
+ """
394
+ try:
395
+ with get_client() as client:
396
+ client.authenticate()
397
+ roles = client.list_roles()
398
+
399
+ if output == "json":
400
+ console.print_json(json.dumps(roles, default=str))
401
+ else:
402
+ if roles:
403
+ print_roles_table(roles)
404
+ else:
405
+ console.print("[yellow]No roles found.[/yellow]")
406
+
407
+ except httpx.HTTPStatusError as e:
408
+ print_api_error(e, "list roles")
409
+ raise typer.Exit(1)
410
+ except httpx.RequestError as e:
411
+ print_api_error(e, "list roles")
412
+ raise typer.Exit(1)
413
+ except Exception as e:
414
+ print_api_error(e, "list roles")
415
+ raise typer.Exit(1)
@@ -0,0 +1,166 @@
1
+ """CLI commands for managing workgroups."""
2
+
3
+ from typing import Optional
4
+ import json
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...core.output import print_api_error
12
+ from ..client.base import get_client
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="Manage workgroups in BeyondInsight")
15
+ console = Console()
16
+
17
+
18
+ def print_workgroups_table(workgroups: list[dict], title: str = "Workgroups") -> None:
19
+ """Print workgroups in a formatted table."""
20
+ table = Table(title=title)
21
+ table.add_column("ID", style="cyan", no_wrap=True)
22
+ table.add_column("Name", style="green")
23
+ table.add_column("Description", style="yellow")
24
+ table.add_column("Created", style="dim")
25
+
26
+ for wg in workgroups:
27
+ table.add_row(
28
+ str(wg.get("WorkgroupID", wg.get("ID", ""))),
29
+ wg.get("Name", ""),
30
+ wg.get("Description", "-"),
31
+ str(wg.get("CreatedDate", "-")),
32
+ )
33
+
34
+ console.print(table)
35
+
36
+
37
+ @app.command("list")
38
+ def list_workgroups(
39
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
40
+ limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
41
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
42
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
43
+ ) -> None:
44
+ """List all workgroups.
45
+
46
+ Examples:
47
+ bt pws workgroups list # First 50 workgroups
48
+ bt pws workgroups list --all # All workgroups
49
+ """
50
+ try:
51
+ with get_client() as client:
52
+ client.authenticate()
53
+ workgroups = client.list_workgroups(search=search, limit=None if fetch_all else limit)
54
+
55
+ if output == "json":
56
+ console.print_json(json.dumps(workgroups, default=str))
57
+ else:
58
+ if workgroups:
59
+ print_workgroups_table(workgroups)
60
+ if not fetch_all and len(workgroups) == limit:
61
+ console.print(f"[dim]Showing {len(workgroups)} results. Use --all to fetch all results.[/dim]")
62
+ else:
63
+ console.print("[yellow]No workgroups found.[/yellow]")
64
+
65
+ except httpx.HTTPStatusError as e:
66
+ print_api_error(e, "manage workgroups")
67
+ raise typer.Exit(1)
68
+ except httpx.RequestError as e:
69
+ print_api_error(e, "manage workgroups")
70
+ raise typer.Exit(1)
71
+ except Exception as e:
72
+ print_api_error(e, "manage workgroups")
73
+ raise typer.Exit(1)
74
+
75
+
76
+ @app.command("get")
77
+ def get_workgroup(
78
+ workgroup_id: int = typer.Argument(..., help="Workgroup ID"),
79
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
80
+ ) -> None:
81
+ """Get a workgroup by ID."""
82
+ try:
83
+ with get_client() as client:
84
+ client.authenticate()
85
+ workgroup = client.get_workgroup(workgroup_id)
86
+
87
+ if output == "json":
88
+ console.print_json(json.dumps(workgroup, default=str))
89
+ else:
90
+ console.print(f"\n[bold cyan]Workgroup: {workgroup.get('Name', 'Unknown')}[/bold cyan]\n")
91
+ console.print(f" ID: {workgroup.get('WorkgroupID', 'N/A')}")
92
+ console.print(f" Description: {workgroup.get('Description', '-')}")
93
+ console.print(f" Created: {workgroup.get('CreatedDate', '-')}")
94
+
95
+ except httpx.HTTPStatusError as e:
96
+ print_api_error(e, "manage workgroups")
97
+ raise typer.Exit(1)
98
+ except httpx.RequestError as e:
99
+ print_api_error(e, "manage workgroups")
100
+ raise typer.Exit(1)
101
+ except Exception as e:
102
+ print_api_error(e, "manage workgroups")
103
+ raise typer.Exit(1)
104
+
105
+
106
+ @app.command("create")
107
+ def create_workgroup(
108
+ name: str = typer.Option(..., "--name", "-n", help="Workgroup name"),
109
+ description: Optional[str] = typer.Option(None, "--description", "-d", help="Description"),
110
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
111
+ ) -> None:
112
+ """Create a new workgroup."""
113
+ try:
114
+ with get_client() as client:
115
+ client.authenticate()
116
+ workgroup = client.create_workgroup(name=name, description=description)
117
+
118
+ if output == "json":
119
+ console.print_json(json.dumps(workgroup, default=str))
120
+ else:
121
+ console.print(f"[green]Created workgroup:[/green] {workgroup.get('Name', 'Unknown')}")
122
+ console.print(f" ID: {workgroup.get('WorkgroupID', 'N/A')}")
123
+
124
+ except httpx.HTTPStatusError as e:
125
+ print_api_error(e, "manage workgroups")
126
+ raise typer.Exit(1)
127
+ except httpx.RequestError as e:
128
+ print_api_error(e, "manage workgroups")
129
+ raise typer.Exit(1)
130
+ except Exception as e:
131
+ print_api_error(e, "manage workgroups")
132
+ raise typer.Exit(1)
133
+
134
+
135
+ @app.command("delete")
136
+ def delete_workgroup(
137
+ workgroup_id: int = typer.Argument(..., help="Workgroup ID to delete"),
138
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
139
+ ) -> None:
140
+ """Delete a workgroup."""
141
+ try:
142
+ with get_client() as client:
143
+ client.authenticate()
144
+
145
+ if not force:
146
+ workgroup = client.get_workgroup(workgroup_id)
147
+ name = workgroup.get("Name", "Unknown")
148
+ confirm = typer.confirm(
149
+ f"Are you sure you want to delete workgroup '{name}' (ID: {workgroup_id})?"
150
+ )
151
+ if not confirm:
152
+ console.print("[yellow]Cancelled.[/yellow]")
153
+ raise typer.Exit(0)
154
+
155
+ client.delete_workgroup(workgroup_id)
156
+ console.print(f"[green]Deleted workgroup ID: {workgroup_id}[/green]")
157
+
158
+ except httpx.HTTPStatusError as e:
159
+ print_api_error(e, "manage workgroups")
160
+ raise typer.Exit(1)
161
+ except httpx.RequestError as e:
162
+ print_api_error(e, "manage workgroups")
163
+ raise typer.Exit(1)
164
+ except Exception as e:
165
+ print_api_error(e, "manage workgroups")
166
+ raise typer.Exit(1)
bt_cli/pws/config.py ADDED
@@ -0,0 +1,18 @@
1
+ """Password Safe configuration wrapper.
2
+
3
+ Provides backward-compatible get_config() for command modules.
4
+ """
5
+
6
+ from ..core.config import PWSConfig, load_pws_config
7
+
8
+
9
+ def get_config() -> PWSConfig:
10
+ """Get Password Safe configuration.
11
+
12
+ Returns:
13
+ PWSConfig instance loaded from environment
14
+ """
15
+ return load_pws_config()
16
+
17
+
18
+ __all__ = ["PWSConfig", "get_config", "load_pws_config"]
@@ -0,0 +1,19 @@
1
+ """Pydantic models for Password Safe API responses."""
2
+
3
+ from .common import (
4
+ BaseAPIModel,
5
+ EntityRef,
6
+ PaginatedResponse,
7
+ ApiError,
8
+ Timestamp,
9
+ normalize_api_response,
10
+ )
11
+
12
+ __all__ = [
13
+ "BaseAPIModel",
14
+ "EntityRef",
15
+ "PaginatedResponse",
16
+ "ApiError",
17
+ "Timestamp",
18
+ "normalize_api_response",
19
+ ]