helper-cli 0.1.11__py3-none-any.whl → 0.1.21__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.
helper/commands/docker.py CHANGED
@@ -1,21 +1,31 @@
1
- import click
2
- import subprocess
1
+ """
2
+ Docker management commands for the helper CLI.
3
+
4
+ This module provides commands to manage Docker containers and images,
5
+ including listing, running, and removing containers and images.
6
+ """
7
+
3
8
  import json
4
- import re
5
9
  import logging
10
+ import subprocess
6
11
  import sys
7
- from typing import Dict, List, Tuple, Optional, Any
12
+ from typing import Dict, List
13
+
14
+ import click
15
+ from helper import __version__
8
16
 
9
17
  # Configure logging
10
18
  logging.basicConfig(
11
19
  level=logging.WARNING,
12
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
13
- stream=sys.stderr
20
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
21
+ stream=sys.stderr,
14
22
  )
15
- logger = logging.getLogger('docker-helper')
23
+ logger = logging.getLogger("docker-helper")
24
+
16
25
 
17
26
  class Verbosity:
18
27
  """Handle verbosity levels for logging."""
28
+
19
29
  def __init__(self, verbosity: int = 0):
20
30
  self.verbosity = verbosity
21
31
  self.set_level()
@@ -50,68 +60,97 @@ class Verbosity:
50
60
  """Log error message regardless of verbosity."""
51
61
  logger.error(msg, *args, **kwargs)
52
62
 
63
+
53
64
  def get_container_ports(container_id: str, verbosity: Verbosity) -> List[Dict]:
54
- """Get exposed ports and IPs for a container."""
55
- verbosity.debug(f"Getting ports for container {container_id}")
65
+ """Get exposed ports and IPs for a container.
66
+
67
+ Args:
68
+ container_id: The ID of the container to inspect
69
+ verbosity: Verbosity level for logging
70
+
71
+ Returns:
72
+ List of dictionaries containing port mappings
73
+ """
74
+ verbosity.debug("Getting ports for container %s", container_id)
56
75
  try:
57
76
  result = subprocess.run(
58
- ['docker', 'inspect', '--format',
59
- '{{range $p, $conf := .NetworkSettings.Ports}}' # noqa: E501
60
- '{{range $h, $hosts := $conf}}'
61
- '{{$p}}|{{$hosts.HostIp}}|{{$hosts.HostPort}};'
62
- '{{end}}{{end}}',
63
- container_id],
77
+ [
78
+ "docker",
79
+ "inspect",
80
+ "--format",
81
+ "{{range $p, $conf := .NetworkSettings.Ports}}" # noqa: E501
82
+ "{{range $h, $hosts := $conf}}"
83
+ "{{$p}}|{{$hosts.HostIp}}|{{$hosts.HostPort}};"
84
+ "{{end}}{{end}}",
85
+ container_id,
86
+ ],
64
87
  capture_output=True,
65
- text=True
88
+ text=True,
89
+ check=True,
66
90
  )
67
91
 
68
- if result.returncode != 0:
69
- verbosity.error(f"Failed to get container info: {result.stderr}")
70
- return []
71
-
72
92
  ports = []
73
93
  raw_output = result.stdout.strip()
74
- verbosity.debug(f"Raw port mappings: {raw_output}")
94
+ verbosity.debug("Raw port mappings: %s", raw_output)
75
95
 
76
- if raw_output:
77
- for mapping in raw_output.split(';'):
78
- if not mapping:
79
- continue
80
- try:
81
- container_port, host_ip, host_port = mapping.split('|')
82
- verbosity.debug(f"Processing mapping: container={container_port}, host_ip={host_ip}, host_port={host_port}")
83
-
84
- if container_port and host_port:
85
- port_info = {
86
- 'container_port': container_port.split('/')[0], # Remove /tcp or /udp
87
- 'host_ip': host_ip if host_ip not in ('0.0.0.0', '') else 'localhost',
88
- 'host_port': host_port
89
- }
90
- verbosity.info(f"Added port mapping: {port_info}")
91
- ports.append(port_info)
92
- else:
93
- verbosity.debug(f"Skipping incomplete mapping: {mapping}")
94
- except ValueError as e:
95
- verbosity.warning(f"Failed to parse mapping '{mapping}': {e}")
96
- continue
97
- verbosity.debug(f"Final port mappings: {ports}")
96
+ if not raw_output:
97
+ return ports
98
+
99
+ for mapping in raw_output.split(";"):
100
+ if not mapping:
101
+ continue
102
+ try:
103
+ container_port, host_ip, host_port = mapping.split("|")
104
+ verbosity.debug(
105
+ "Processing mapping: container=%s, host_ip=%s, host_port=%s",
106
+ container_port,
107
+ host_ip,
108
+ host_port,
109
+ )
110
+
111
+ if container_port and host_port:
112
+ port_info = {
113
+ "container_port": container_port.split("/")[
114
+ 0
115
+ ], # Remove /tcp or /udp
116
+ "host_ip": (
117
+ host_ip if host_ip not in ("0.0.0.0", "") else "localhost"
118
+ ),
119
+ "host_port": host_port,
120
+ }
121
+ verbosity.info("Added port mapping: %s", port_info)
122
+ ports.append(port_info)
123
+ else:
124
+ verbosity.debug("Skipping incomplete mapping: %s", mapping)
125
+ except ValueError as e:
126
+ verbosity.warning("Failed to parse mapping '%s': %s", mapping, e)
127
+
128
+ verbosity.debug("Final port mappings: %s", ports)
98
129
  return ports
99
- except Exception as e:
100
- verbosity.error(f"Unexpected error in get_container_ports: {str(e)}", exc_info=verbosity.verbosity >= 3)
101
- return []
130
+
131
+ except subprocess.CalledProcessError as e:
132
+ verbosity.error("Failed to get container info: %s", e.stderr)
133
+ except Exception as e: # pylint: disable=broad-except
134
+ verbosity.error(
135
+ "Unexpected error in get_container_ports: %s",
136
+ str(e),
137
+ exc_info=verbosity.verbosity >= 3,
138
+ )
139
+ return []
140
+
102
141
 
103
142
  def check_docker(verbosity: Verbosity) -> bool:
104
143
  """Check if Docker is installed and running."""
105
144
  verbosity.info("Checking if Docker is installed and running...")
106
145
  try:
107
- result = subprocess.run(['docker', 'info'],
108
- capture_output=True,
109
- text=True)
146
+ result = subprocess.run(["docker", "info"], capture_output=True, text=True)
110
147
 
111
148
  verbosity.debug(f"Docker info command output:\n{result.stdout}")
112
149
 
113
150
  if result.returncode != 0:
114
- verbosity.error(f"Docker is not running or not accessible. Error: {result.stderr}")
151
+ verbosity.error(
152
+ f"Docker is not running or not accessible. Error: {result.stderr}"
153
+ )
115
154
  return False
116
155
 
117
156
  verbosity.info("Docker is running and accessible")
@@ -121,77 +160,119 @@ def check_docker(verbosity: Verbosity) -> bool:
121
160
  verbosity.error("Docker command not found. Is Docker installed?")
122
161
  return False
123
162
  except Exception as e:
124
- verbosity.error(f"Unexpected error checking Docker: {str(e)}", exc_info=verbosity.verbosity >= 3)
163
+ verbosity.error(
164
+ f"Unexpected error checking Docker: {str(e)}",
165
+ exc_info=verbosity.verbosity >= 3,
166
+ )
125
167
  return False
126
168
 
127
- def format_output(output, output_format='table'):
169
+
170
+ def format_output(output, output_format="table"):
128
171
  """Format command output based on the specified format."""
129
- if output_format == 'json':
172
+ if output_format == "json":
130
173
  try:
131
174
  return json.dumps(json.loads(output), indent=2)
132
175
  except json.JSONDecodeError:
133
176
  return output
134
177
  return output
135
178
 
179
+
136
180
  def get_verbosity(ctx: click.Context) -> Verbosity:
137
181
  """Get verbosity level from context."""
138
182
  # Count the number of 'v's in the --verbose flag
139
- verbose = ctx.params.get('verbose', 0)
183
+ verbose = ctx.params.get("verbose", 0)
140
184
  verbosity = Verbosity(verbosity=verbose)
141
185
  verbosity.info(f"Verbosity level set to {verbose}")
142
186
  return verbosity
143
187
 
188
+
144
189
  @click.group()
145
- @click.option('-v', '--verbose', count=True, help='Increase verbosity (can be used multiple times)')
190
+ @click.option(
191
+ "-v",
192
+ "--verbose",
193
+ count=True,
194
+ help="Increase verbosity (can be used multiple times)",
195
+ )
146
196
  @click.pass_context
147
197
  def docker(ctx, verbose):
148
- """Docker management commands."""
149
- ctx.ensure_object(dict)
198
+ """Docker management commands (v{}).
150
199
 
200
+ This command provides various Docker management subcommands including:
201
+ - ps: List containers
202
+ - run: Run a command in a new container
203
+ - rm: Remove one or more containers
204
+ - rmi: Remove one or more images
205
+ - url: Show containers with their HTTP/HTTPS URLs
206
+
207
+ Use --help with any subcommand for more information.
208
+ """.format(__version__)
209
+ ctx.ensure_object(dict)
210
+
151
211
  # Get verbosity from parent context if it exists, otherwise use the flag value
152
- parent_verbosity = ctx.obj.get('verbosity', 0) if hasattr(ctx, 'obj') else 0
212
+ parent_verbosity = ctx.obj.get("verbosity", 0) if hasattr(ctx, "obj") else 0
153
213
  verbosity_level = max(verbose, parent_verbosity)
154
-
214
+
155
215
  # Initialize verbosity
156
216
  verbosity = Verbosity(verbosity=verbosity_level)
157
- ctx.obj['verbosity'] = verbosity
158
-
159
- # Set up logging
160
- logger = logging.getLogger('docker-helper')
161
- if verbosity_level >= 3:
162
- logger.setLevel(logging.DEBUG)
163
- elif verbosity_level == 2:
164
- logger.setLevel(logging.INFO)
165
- elif verbosity_level == 1:
166
- logger.setLevel(logging.WARNING)
167
- else:
168
- logger.setLevel(logging.ERROR)
169
-
170
- logger.debug(f"Docker command group initialized with verbosity level: {verbosity_level}")
217
+ ctx.obj["verbosity"] = verbosity
218
+
219
+ # Configure logger with verbosity level
220
+ logger.setLevel(
221
+ logging.DEBUG
222
+ if verbosity_level >= 3
223
+ else (
224
+ logging.INFO
225
+ if verbosity_level == 2
226
+ else logging.WARNING if verbosity_level == 1 else logging.ERROR
227
+ )
228
+ )
229
+
230
+ logger.debug(
231
+ "Docker command group initialized with verbosity level: %s", verbosity_level
232
+ )
171
233
 
172
234
  verbosity.debug("Initializing Docker command group")
173
235
  if not check_docker(verbosity):
174
- click.echo("Error: Docker is not installed or not running. Please start Docker and try again.", err=True)
236
+ click.echo(
237
+ "Error: Docker is not installed or not running. Please start Docker and try again.",
238
+ err=True,
239
+ )
175
240
  ctx.exit(1)
176
241
 
177
- @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
178
- @click.option('--all', '-a', is_flag=True, help='Show all containers (default shows just running)')
179
- @click.option('--format', type=click.Choice(['table', 'json'], case_sensitive=False),
180
- default='table', help='Output format')
242
+
243
+ @docker.command(
244
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
245
+ )
246
+ @click.option(
247
+ "--all", "-a", is_flag=True, help="Show all containers (default shows just running)"
248
+ )
249
+ @click.option(
250
+ "--all-containers",
251
+ is_flag=True,
252
+ help="Show all containers (default shows just running)",
253
+ )
254
+ @click.option(
255
+ "--format",
256
+ "-f",
257
+ type=click.Choice(["table", "json"], case_sensitive=False),
258
+ default="table",
259
+ help="Output format",
260
+ )
261
+ @click.help_option("--help", "-h")
181
262
  @click.pass_context
182
- def ps(ctx, all, format):
263
+ def ps(ctx, all_containers, output_format): # pylint: disable=redefined-builtin
183
264
  """List containers."""
184
- verbosity = ctx.obj['verbosity']
185
- cmd = ['docker', 'ps']
186
- if all:
187
- cmd.append('-a')
265
+ verbosity = ctx.obj["verbosity"]
266
+ cmd = ["docker", "ps"]
267
+ if all_containers:
268
+ cmd.append("-a")
188
269
 
189
270
  try:
190
271
  verbosity.debug(f"Running command: {' '.join(cmd)}")
191
272
  result = subprocess.run(cmd, capture_output=True, text=True)
192
-
273
+
193
274
  if result.returncode == 0:
194
- if format == 'json':
275
+ if output_format == "json":
195
276
  # Try to parse and pretty-print JSON output
196
277
  try:
197
278
  data = json.loads(result.stdout)
@@ -201,27 +282,42 @@ def ps(ctx, all, format):
201
282
  click.echo(result.stdout)
202
283
  else:
203
284
  # For table format, try to align columns
204
- lines = result.stdout.strip().split('\n')
285
+ lines = result.stdout.strip().split("\n")
205
286
  if len(lines) > 1:
206
287
  # Parse as JSON to handle special characters in values
207
288
  try:
208
289
  data = [json.loads(line) for line in lines[1:]]
209
290
  headers = data[0].keys()
210
- rows = [[item.get(header, '') for header in headers] for item in data]
291
+ rows = [
292
+ [item.get(header, "") for header in headers]
293
+ for item in data
294
+ ]
211
295
 
212
296
  # Calculate column widths
213
- col_widths = [max(len(str(header)),
214
- max((len(str(row[i])) for row in rows), default=0))
215
- for i, header in enumerate(headers)]
297
+ col_widths = [
298
+ max(
299
+ len(str(header)),
300
+ max((len(str(row[i])) for row in rows), default=0),
301
+ )
302
+ for i, header in enumerate(headers)
303
+ ]
216
304
 
217
305
  # Print header
218
- header_row = " ".join(header.ljust(width) for header, width in zip(headers, col_widths))
306
+ header_row = " ".join(
307
+ header.ljust(width)
308
+ for header, width in zip(headers, col_widths)
309
+ )
219
310
  click.echo(header_row)
220
311
  click.echo("-" * len(header_row))
221
312
 
222
313
  # Print rows
223
314
  for row in rows:
224
- click.echo(" ".join(str(cell).ljust(width) for cell, width in zip(row, col_widths)))
315
+ click.echo(
316
+ " ".join(
317
+ str(cell).ljust(width)
318
+ for cell, width in zip(row, col_widths)
319
+ )
320
+ )
225
321
  except Exception as e:
226
322
  verbosity.debug(f"Error formatting table: {str(e)}")
227
323
  # Fall back to raw output if processing fails
@@ -241,52 +337,62 @@ def ps(ctx, all, format):
241
337
  verbosity.error(error_msg)
242
338
  click.echo(error_msg, err=True)
243
339
 
244
- @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
245
- @click.argument('image', required=False)
246
- @click.option('--name', help='Assign a name to the container')
247
- @click.option('--port', '-p', multiple=True, help='Publish a container\'s port(s) to the host')
248
- @click.option('--detach', '-d', is_flag=True, help='Run container in background and print container ID')
249
- @click.option('--env', '-e', multiple=True, help='Set environment variables')
250
- @click.option('--volume', '-v', multiple=True, help='Bind mount a volume')
340
+
341
+ @docker.command(
342
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
343
+ )
344
+ @click.argument("image", required=False)
345
+ @click.option("--name", help="Assign a name to the container")
346
+ @click.option(
347
+ "--port", "-p", multiple=True, help="Publish a container's port(s) to the host"
348
+ )
349
+ @click.option(
350
+ "--detach",
351
+ "-d",
352
+ is_flag=True,
353
+ help="Run container in background and print container ID",
354
+ )
355
+ @click.option("--env", "-e", multiple=True, help="Set environment variables")
356
+ @click.option("--volume", "-v", multiple=True, help="Bind mount a volume")
251
357
  @click.pass_context
252
358
  def run(ctx, image, name, port, detach, env, volume):
253
359
  """Run a command in a new container."""
254
- verbosity = ctx.obj['verbosity']
255
- cmd = ['docker', 'run']
360
+ verbosity = ctx.obj["verbosity"]
361
+ cmd = ["docker", "run"]
256
362
 
257
363
  if name:
258
- cmd.extend(['--name', name])
364
+ cmd.extend(["--name", name])
259
365
  verbosity.debug(f"Setting container name: {name}")
260
366
 
261
367
  for p in port:
262
- cmd.extend(['-p', p])
368
+ cmd.extend(["-p", p])
263
369
  verbosity.debug(f"Adding port mapping: {p}")
264
370
 
265
371
  if detach:
266
- cmd.append('-d')
372
+ cmd.append("-d")
267
373
  verbosity.debug("Running container in detached mode")
268
374
 
269
375
  for e in env:
270
- cmd.extend(['-e', e])
376
+ cmd.extend(["-e", e])
271
377
  verbosity.debug(f"Setting environment variable: {e}")
272
378
 
273
379
  for v in volume:
274
- cmd.extend(['-v', v])
380
+ cmd.extend(["-v", v])
275
381
  verbosity.debug(f"Mounting volume: {v}")
276
382
 
277
383
  if image:
278
384
  cmd.append(image)
279
385
  verbosity.debug(f"Using image: {image}")
280
-
386
+
281
387
  # Add any remaining arguments
282
- if hasattr(ctx, 'args') and ctx.args:
388
+ if hasattr(ctx, "args") and ctx.args:
283
389
  cmd.extend(ctx.args)
284
390
  verbosity.debug(f"Additional arguments: {' '.join(ctx.args)}")
285
391
 
286
392
  try:
287
393
  verbosity.debug(f"Running command: {' '.join(cmd)}")
288
394
  result = subprocess.run(cmd, capture_output=True, text=True)
289
-
395
+
290
396
  if result.returncode == 0:
291
397
  if result.stdout:
292
398
  click.echo(result.stdout.strip())
@@ -296,35 +402,48 @@ def run(ctx, image, name, port, detach, env, volume):
296
402
  verbosity.error(error_msg)
297
403
  click.echo(error_msg, err=True)
298
404
  ctx.exit(1)
299
-
405
+
300
406
  except Exception as e:
301
407
  error_msg = f"Failed to run container: {str(e)}"
302
408
  verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
303
409
  click.echo(error_msg, err=True)
304
- ctx.exit(1)
410
+ ctx.exit(1)
411
+
305
412
 
306
- @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
307
- @click.argument('containers', nargs=-1, required=False)
308
- @click.option('--force', '-f', is_flag=True, help='Force the removal of a running container (uses SIGKILL)')
309
- @click.option('--volumes', '-v', is_flag=True, help='Remove anonymous volumes associated with the container')
413
+ @docker.command(
414
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
415
+ )
416
+ @click.argument("containers", nargs=-1, required=False)
417
+ @click.option(
418
+ "--force",
419
+ "-f",
420
+ is_flag=True,
421
+ help="Force the removal of a running container (uses SIGKILL)",
422
+ )
423
+ @click.option(
424
+ "--volumes",
425
+ "-v",
426
+ is_flag=True,
427
+ help="Remove anonymous volumes associated with the container",
428
+ )
310
429
  @click.pass_context
311
430
  def rm(ctx, containers, force, volumes):
312
431
  """Remove one or more containers."""
313
- verbosity = ctx.obj['verbosity']
314
- cmd = ['docker', 'rm']
432
+ verbosity = ctx.obj["verbosity"]
433
+ cmd = ["docker", "rm"]
315
434
 
316
435
  if force:
317
- cmd.append('-f')
436
+ cmd.append("-f")
318
437
  verbosity.debug("Force removal enabled")
319
438
  if volumes:
320
- cmd.append('-v')
439
+ cmd.append("-v")
321
440
  verbosity.debug("Volume removal enabled")
322
441
 
323
442
  # Get containers from both the containers argument and any remaining args
324
443
  all_containers = list(containers)
325
- if hasattr(ctx, 'args') and ctx.args:
444
+ if hasattr(ctx, "args") and ctx.args:
326
445
  all_containers.extend(ctx.args)
327
-
446
+
328
447
  if not all_containers:
329
448
  error_msg = "Error: You must specify at least one container"
330
449
  verbosity.error(error_msg)
@@ -337,7 +456,7 @@ def rm(ctx, containers, force, volumes):
337
456
  try:
338
457
  verbosity.debug(f"Running command: {' '.join(cmd)}")
339
458
  result = subprocess.run(cmd, capture_output=True, text=True)
340
-
459
+
341
460
  if result.returncode == 0:
342
461
  if result.stdout.strip():
343
462
  click.echo(result.stdout.strip())
@@ -347,27 +466,39 @@ def rm(ctx, containers, force, volumes):
347
466
  verbosity.error(error_msg)
348
467
  click.echo(error_msg, err=True)
349
468
  ctx.exit(1)
350
-
469
+
351
470
  except Exception as e:
352
471
  error_msg = f"Failed to remove containers: {str(e)}"
353
472
  verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
354
473
  click.echo(error_msg, err=True)
355
- ctx.exit(1)
474
+ ctx.exit(1)
356
475
 
357
- @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
358
- @click.option('--show-all', '-a', is_flag=True, help='Show all containers (default shows just running)')
359
- @click.option('--http-only', is_flag=True, help='Show only containers with HTTP/HTTPS ports')
476
+
477
+ @docker.command(
478
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
479
+ )
480
+ @click.option(
481
+ "--show-all",
482
+ "-a",
483
+ is_flag=True,
484
+ help="Show all containers (default shows just running)",
485
+ )
486
+ @click.option(
487
+ "--http-only", "-h", is_flag=True, help="Show only containers with HTTP/HTTPS ports"
488
+ )
360
489
  @click.pass_context
361
490
  def url(ctx, show_all, http_only):
362
491
  """Show containers with their HTTP/HTTPS URLs."""
363
- verbosity = ctx.obj['verbosity']
364
- verbosity.info(f"Starting url command with show_all={show_all}, http_only={http_only}")
492
+ verbosity = ctx.obj["verbosity"]
493
+ verbosity.info(
494
+ f"Starting url command with show_all={show_all}, http_only={http_only}"
495
+ )
365
496
 
366
497
  try:
367
498
  # Get all containers
368
- cmd = ['docker', 'ps', '--format', '{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}']
499
+ cmd = ["docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}"]
369
500
  if show_all:
370
- cmd.append('-a')
501
+ cmd.append("-a")
371
502
 
372
503
  verbosity.debug(f"Running command: {' '.join(cmd)}")
373
504
  result = subprocess.run(cmd, capture_output=True, text=True)
@@ -383,7 +514,7 @@ def url(ctx, show_all, http_only):
383
514
  running_containers = []
384
515
  stopped_containers = []
385
516
 
386
- container_lines = result.stdout.strip().split('\n')
517
+ container_lines = result.stdout.strip().split("\n")
387
518
  verbosity.info(f"Found {len(container_lines)} container(s)")
388
519
 
389
520
  for line in container_lines:
@@ -393,16 +524,18 @@ def url(ctx, show_all, http_only):
393
524
 
394
525
  try:
395
526
  verbosity.debug(f"Processing container line: {line}")
396
- container_id, name, status, ports = line.split('|', 3)
397
- is_running = 'Up' in status
398
- verbosity.info(f"Container: ID={container_id[:12]}, Name={name}, Status={status}, Running={is_running}")
527
+ container_id, name, status, ports = line.split("|", 3)
528
+ is_running = "Up" in status
529
+ verbosity.info(
530
+ f"Container: ID={container_id[:12]}, Name={name}, Status={status}, Running={is_running}"
531
+ )
399
532
 
400
533
  # Get container details
401
534
  container_info = {
402
- 'id': container_id[:12], # Short ID
403
- 'name': name,
404
- 'status': status,
405
- 'urls': []
535
+ "id": container_id[:12], # Short ID
536
+ "name": name,
537
+ "status": status,
538
+ "urls": [],
406
539
  }
407
540
 
408
541
  # Get exposed ports and their mappings
@@ -410,57 +543,63 @@ def url(ctx, show_all, http_only):
410
543
  verbosity.debug(f"Found {len(port_mappings)} port mappings for {name}")
411
544
 
412
545
  for port in port_mappings:
413
- if not port.get('host_port') or not port.get('container_port'):
546
+ if not port.get("host_port") or not port.get("container_port"):
414
547
  verbosity.debug(f"Skipping incomplete port mapping: {port}")
415
548
  continue
416
-
549
+
417
550
  verbosity.debug(f"Checking port mapping: {port}")
418
- verbosity.debug(f"Container name: {name}, Port: {port['container_port']}")
419
-
551
+ verbosity.debug(
552
+ f"Container name: {name}, Port: {port['container_port']}"
553
+ )
554
+
420
555
  # Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
421
- port_str = port['container_port']
422
-
556
+ port_str = port["container_port"]
557
+
423
558
  # Extract port number and protocol
424
559
  port_num = None
425
- protocol = 'tcp' # default protocol
426
-
560
+ protocol = "tcp" # default protocol
561
+
427
562
  # Handle format like '8069/tcp' or '80/http'
428
- if '/' in port_str:
429
- port_num, protocol = port_str.split('/', 1)
563
+ if "/" in port_str:
564
+ port_num, protocol = port_str.split("/", 1)
430
565
  # Handle format like '0.0.0.0:8080->80/tcp'
431
- elif '->' in port_str:
432
- _, port_mapping = port_str.split('->', 1)
433
- if '/' in port_mapping:
434
- port_num, protocol = port_mapping.split('/', 1)
566
+ elif "->" in port_str:
567
+ _, port_mapping = port_str.split("->", 1)
568
+ if "/" in port_mapping:
569
+ port_num, protocol = port_mapping.split("/", 1)
435
570
  else:
436
571
  port_num = port_mapping
437
572
  else:
438
573
  port_num = port_str
439
-
574
+
440
575
  # Clean up port number (remove any non-numeric characters)
441
- port_num = ''.join(c for c in port_num if c.isdigit())
442
-
576
+ port_num = "".join(c for c in port_num if c.isdigit())
577
+
443
578
  # Map all ports to HTTP URLs
444
579
  if port_num: # Process all ports regardless of protocol
445
- scheme = 'http'
446
-
580
+ scheme = "http"
581
+
447
582
  # Handle IPv6 addresses (add brackets if needed)
448
- host = port['host_ip']
449
- if ':' in host and not host.startswith('['):
450
- host = f'[{host}]'
451
-
583
+ host = port["host_ip"]
584
+ if ":" in host and not host.startswith("["):
585
+ host = f"[{host}]"
586
+ verbosity.info(
587
+ f"Skipping non-HTTP port: {port['container_port']}"
588
+ )
589
+ continue
590
+
452
591
  url = f"{scheme}://{host}:{port['host_port']}"
453
-
454
- container_info['urls'].append({
455
- 'url': url,
456
- 'port': port_num,
457
- 'protocol': protocol
458
- })
459
- verbosity.info(f"Added URL for {name}: {url} (port {port_num}/{protocol})")
592
+
593
+ container_info["urls"].append(
594
+ {"url": url, "port": port_num, "protocol": protocol}
595
+ )
596
+ verbosity.info(
597
+ f"Added URL for {name}: {url} (port {port_num}/{protocol})"
598
+ )
460
599
  verbosity.info(f"Added URL for {name}: {url} (port {port_num})")
461
600
 
462
601
  # If http_only is True and no HTTP URLs, skip this container
463
- if http_only and not container_info['urls']:
602
+ if http_only and not container_info["urls"]:
464
603
  continue
465
604
 
466
605
  if is_running:
@@ -474,71 +613,97 @@ def url(ctx, show_all, http_only):
474
613
 
475
614
  # Display running containers
476
615
  if running_containers:
477
- click.secho("\n🚀 Running Containers:", fg='green', bold=True)
616
+ click.secho("\n🚀 Running Containers:", fg="green", bold=True)
478
617
  for container in running_containers:
479
618
  verbosity.debug(f"Displaying running container: {container['name']}")
480
- click.echo(f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})")
619
+ click.echo(
620
+ f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})"
621
+ )
481
622
 
482
- if container['urls']:
483
- verbosity.info(f"Found {len(container['urls'])} URLs for {container['name']}")
484
- for url_info in container['urls']:
623
+ if container["urls"]:
624
+ verbosity.info(
625
+ f"Found {len(container['urls'])} URLs for {container['name']}"
626
+ )
627
+ for url_info in container["urls"]:
485
628
  verbosity.debug(f"Displaying URL: {url_info['url']}")
486
- click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}")
629
+ click.echo(
630
+ f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}"
631
+ )
487
632
  else:
488
633
  verbosity.debug(f"No URLs found for {container['name']}")
489
634
 
490
635
  # Display stopped containers
491
636
  if stopped_containers and (show_all or not http_only):
492
- click.secho("\n⏸️ Stopped Containers:", fg='yellow', bold=True)
637
+ click.secho("\n⏸️ Stopped Containers:", fg="yellow", bold=True)
493
638
  for container in stopped_containers:
494
639
  verbosity.debug(f"Displaying stopped container: {container['name']}")
495
- click.echo(f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})")
640
+ click.echo(
641
+ f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})"
642
+ )
496
643
 
497
- if container['urls']:
498
- verbosity.info(f"Found {len(container['urls'])} URLs for stopped container {container['name']}")
499
- for url_info in container['urls']:
500
- verbosity.debug(f"Displaying URL for stopped container: {url_info['url']}")
501
- click.echo(f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True, dim=True)}")
644
+ if container["urls"]:
645
+ verbosity.info(
646
+ f"Found {len(container['urls'])} URLs for stopped container {container['name']}"
647
+ )
648
+ for url_info in container["urls"]:
649
+ verbosity.debug(
650
+ f"Displaying URL for stopped container: {url_info['url']}"
651
+ )
652
+ click.echo(
653
+ f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True, dim=True)}"
654
+ )
502
655
  else:
503
- verbosity.debug(f"No URLs found for stopped container {container['name']}")
656
+ verbosity.debug(
657
+ f"No URLs found for stopped container {container['name']}"
658
+ )
504
659
 
505
660
  if not running_containers and not stopped_containers:
506
661
  msg = "No containers found."
507
662
  verbosity.info(msg)
508
663
  click.echo(msg)
509
664
  else:
510
- verbosity.info(f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers")
665
+ verbosity.info(
666
+ f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers"
667
+ )
511
668
 
512
669
  except Exception as e:
513
670
  error_msg = f"Error in url command: {str(e)}"
514
671
  verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
515
672
  click.echo(error_msg, err=True)
516
673
 
517
- @docker.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
518
- @click.argument('image', required=False)
519
- @click.option('--all-tags', '-a', is_flag=True, help='Remove all versions of the image with the given name')
520
- @click.option('--force', '-f', is_flag=True, help='Force removal of the image')
521
- @click.option('--no-prune', is_flag=True, help='Do not delete untagged parents')
674
+
675
+ @docker.command(
676
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
677
+ )
678
+ @click.argument("image", required=False)
679
+ @click.option(
680
+ "--all-tags",
681
+ "-a",
682
+ is_flag=True,
683
+ help="Remove all versions of the image with the given name",
684
+ )
685
+ @click.option("--force", "-f", is_flag=True, help="Force removal of the image")
686
+ @click.option("--no-prune", is_flag=True, help="Do not delete untagged parents")
522
687
  @click.pass_context
523
688
  def rmi(ctx, image, all_tags, force, no_prune):
524
689
  """Remove one or more images."""
525
- verbosity = ctx.obj['verbosity']
526
- cmd = ['docker', 'rmi']
690
+ verbosity = ctx.obj["verbosity"]
691
+ cmd = ["docker", "rmi"]
527
692
 
528
693
  if force:
529
- cmd.append('-f')
694
+ cmd.append("-f")
530
695
  verbosity.debug("Force removal enabled")
531
696
  if no_prune:
532
- cmd.append('--no-prune')
697
+ cmd.append("--no-prune")
533
698
  verbosity.debug("Pruning of untagged parents disabled")
534
699
 
535
700
  # Get images from both the image argument and any remaining args
536
701
  images = []
537
702
  if image:
538
703
  images.append(image)
539
- if hasattr(ctx, 'args') and ctx.args:
704
+ if hasattr(ctx, "args") and ctx.args:
540
705
  images.extend(ctx.args)
541
-
706
+
542
707
  if not images and not all_tags:
543
708
  error_msg = "Error: You must specify at least one image"
544
709
  verbosity.error(error_msg)
@@ -551,38 +716,41 @@ def rmi(ctx, image, all_tags, force, no_prune):
551
716
  verbosity.error(error_msg)
552
717
  click.echo(error_msg, err=True)
553
718
  ctx.exit(1)
554
-
719
+
555
720
  # Get all tags for the specified images
556
721
  all_tags_to_remove = []
557
722
  for img in images:
558
723
  verbosity.debug(f"Finding all tags for image: {img}")
559
724
  try:
560
725
  result = subprocess.run(
561
- ['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}', img],
562
- capture_output=True,
563
- text=True
726
+ ["docker", "images", "--format", "{{.Repository}}:{{.Tag}}", img],
727
+ capture_output=True,
728
+ text=True,
564
729
  )
565
-
730
+
566
731
  if result.returncode == 0 and result.stdout.strip():
567
- tags = [line for line in result.stdout.split('\n') if line]
732
+ tags = [line for line in result.stdout.split("\n") if line]
568
733
  verbosity.debug(f"Found {len(tags)} tags for {img}")
569
734
  all_tags_to_remove.extend(tags)
570
735
  else:
571
736
  verbosity.warning(f"No images found matching '{img}'")
572
-
737
+
573
738
  except Exception as e:
574
- verbosity.error(f"Error finding tags for {img}: {str(e)}", exc_info=verbosity.verbosity >= 3)
739
+ verbosity.error(
740
+ f"Error finding tags for {img}: {str(e)}",
741
+ exc_info=verbosity.verbosity >= 3,
742
+ )
575
743
  continue
576
-
744
+
577
745
  if not all_tags_to_remove:
578
746
  error_msg = "No matching images found to remove"
579
747
  verbosity.error(error_msg)
580
748
  click.echo(error_msg, err=True)
581
749
  ctx.exit(1)
582
-
750
+
583
751
  cmd.extend(all_tags_to_remove)
584
752
  verbosity.info(f"Removing {len(all_tags_to_remove)} image(s) with all tags")
585
-
753
+
586
754
  else:
587
755
  cmd.extend(images)
588
756
  verbosity.info(f"Removing {len(images)} image(s)")
@@ -590,7 +758,7 @@ def rmi(ctx, image, all_tags, force, no_prune):
590
758
  try:
591
759
  verbosity.debug(f"Running command: {' '.join(cmd)}")
592
760
  result = subprocess.run(cmd, capture_output=True, text=True)
593
-
761
+
594
762
  if result.returncode == 0:
595
763
  if result.stdout.strip():
596
764
  click.echo(result.stdout.strip())
@@ -601,9 +769,9 @@ def rmi(ctx, image, all_tags, force, no_prune):
601
769
  verbosity.error(error_msg)
602
770
  click.echo(error_msg, err=True)
603
771
  ctx.exit(1)
604
-
772
+
605
773
  except Exception as e:
606
774
  error_msg = f"Failed to remove images: {str(e)}"
607
775
  verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
608
776
  click.echo(error_msg, err=True)
609
- ctx.exit(1)
777
+ ctx.exit(1)