cave-cli 3.5.0__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.
@@ -0,0 +1,530 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ from cave_cli.utils.logger import logger
5
+ from cave_cli.utils.subprocess import run, run_and_log, version_tuple
6
+
7
+
8
+ def check_docker() -> None:
9
+ """
10
+ Usage:
11
+
12
+ - Validates that Docker is installed, running, and meets the minimum version
13
+
14
+ Notes:
15
+
16
+ - Exits with code 1 if Docker is not installed, not running, or too old
17
+ """
18
+ from cave_cli.utils.constants import MIN_DOCKER_VERSION
19
+
20
+ try:
21
+ result = run(["docker", "--version"])
22
+ except FileNotFoundError:
23
+ logger.error(
24
+ f"Docker is not installed. "
25
+ f"Please install Docker version {MIN_DOCKER_VERSION} or greater.\n"
26
+ f"For more information see: https://docs.docker.com/get-docker/"
27
+ )
28
+ sys.exit(1)
29
+
30
+ if result.returncode != 0 or not result.stdout:
31
+ logger.error(
32
+ f"Could not determine Docker version. "
33
+ f"Please install Docker version {MIN_DOCKER_VERSION} or greater."
34
+ )
35
+ sys.exit(1)
36
+
37
+ version_str = result.stdout.strip()
38
+ import re
39
+
40
+ match = re.search(r"(\d+\.\d+\.\d+)", version_str)
41
+ if not match:
42
+ logger.error(f"Could not parse Docker version from: {version_str}")
43
+ sys.exit(1)
44
+
45
+ current = match.group(1)
46
+ if version_tuple(current) < version_tuple(MIN_DOCKER_VERSION):
47
+ logger.error(
48
+ f"Your current Docker version ({current}) is too old.\n"
49
+ f"Please install Docker version {MIN_DOCKER_VERSION} or greater.\n"
50
+ f"For more information see: https://docs.docker.com/get-docker/"
51
+ )
52
+ sys.exit(1)
53
+
54
+ info_result = run(["docker", "info"])
55
+ if info_result.returncode != 0:
56
+ logger.error(
57
+ "Docker not running... Please start Docker and try again!"
58
+ )
59
+ sys.exit(1)
60
+
61
+ logger.debug("Docker Check Passed!")
62
+
63
+
64
+ def build_image(app_name: str, path: str) -> None:
65
+ """
66
+ Usage:
67
+
68
+ - Builds a Docker image for the CAVE app
69
+
70
+ Requires:
71
+
72
+ - ``app_name``:
73
+ - Type: str
74
+ - What: The app name used for tagging the image
75
+
76
+ - ``path``:
77
+ - Type: str
78
+ - What: The build context directory
79
+
80
+ Notes:
81
+
82
+ - Streams build output and checks for ERROR lines
83
+ - Exits with code 1 if an error is detected during the build
84
+ """
85
+ remove_containers(app_name)
86
+ logger.info("Getting Docker setup... (this may take a while)")
87
+ has_error = False
88
+ process = subprocess.Popen(
89
+ ["docker", "build", ".", "--tag", f"cave-app:{app_name}"],
90
+ cwd=path,
91
+ stdout=subprocess.PIPE,
92
+ stderr=subprocess.STDOUT,
93
+ text=True,
94
+ encoding="utf-8",
95
+ errors="replace",
96
+ )
97
+ for line in process.stdout:
98
+ line = line.rstrip()
99
+ if "ERROR" in line:
100
+ has_error = True
101
+ logger.debug(line)
102
+ process.wait()
103
+ if has_error or process.returncode != 0:
104
+ logger.error(
105
+ "An ERROR was returned during the Docker container build process."
106
+ )
107
+ logger.error(
108
+ "The CAVE CLI command is exiting early due to this ERROR."
109
+ )
110
+ logger.error(
111
+ "Consider running your command again in verbose mode "
112
+ "to get more information."
113
+ )
114
+ logger.error("EG: 'cave reset --verbose' or 'cave run --verbose'")
115
+ sys.exit(1)
116
+
117
+
118
+ def create_network(app_name: str) -> None:
119
+ """
120
+ Usage:
121
+
122
+ - Creates a Docker network for the CAVE app
123
+
124
+ Requires:
125
+
126
+ - ``app_name``:
127
+ - Type: str
128
+ - What: The app name used in the network name
129
+ """
130
+ run_and_log(["docker", "network", "create", f"cave-net:{app_name}"])
131
+
132
+
133
+ def remove_network(app_name: str) -> None:
134
+ """
135
+ Usage:
136
+
137
+ - Removes the Docker network for the CAVE app
138
+
139
+ Requires:
140
+
141
+ - ``app_name``:
142
+ - Type: str
143
+ - What: The app name used in the network name
144
+ """
145
+ run_and_log(["docker", "network", "rm", f"cave-net:{app_name}"])
146
+
147
+
148
+ def run_detached(
149
+ name: str,
150
+ image: str,
151
+ network: str | None = None,
152
+ volumes: list[str] | None = None,
153
+ env_vars: dict[str, str] | None = None,
154
+ extra_args: list[str] | None = None,
155
+ command: list[str] | None = None,
156
+ ) -> None:
157
+ """
158
+ Usage:
159
+
160
+ - Runs a Docker container in detached mode
161
+
162
+ Requires:
163
+
164
+ - ``name``:
165
+ - Type: str
166
+ - What: The container name
167
+
168
+ - ``image``:
169
+ - Type: str
170
+ - What: The Docker image to run
171
+
172
+ Optional:
173
+
174
+ - ``network``:
175
+ - Type: str | None
176
+ - What: Docker network to attach to
177
+ - Default: None
178
+
179
+ - ``volumes``:
180
+ - Type: list[str] | None
181
+ - What: Volume mount specifications
182
+ - Default: None
183
+
184
+ - ``env_vars``:
185
+ - Type: dict[str, str] | None
186
+ - What: Environment variables to set
187
+ - Default: None
188
+
189
+ - ``extra_args``:
190
+ - Type: list[str] | None
191
+ - What: Additional docker run arguments
192
+ - Default: None
193
+
194
+ - ``command``:
195
+ - Type: list[str] | None
196
+ - What: Command to run in the container
197
+ - Default: None
198
+ """
199
+ cmd = ["docker", "run", "-d"]
200
+ if extra_args:
201
+ cmd.extend(extra_args)
202
+ if network:
203
+ cmd.extend(["--network", network])
204
+ for vol in volumes or []:
205
+ cmd.extend(["--volume", vol])
206
+ for key, val in (env_vars or {}).items():
207
+ cmd.extend(["-e", f"{key}={val}"])
208
+ cmd.extend(["--name", name])
209
+ cmd.append(image)
210
+ if command:
211
+ cmd.extend(command)
212
+ run_and_log(cmd)
213
+
214
+
215
+ def run_interactive(
216
+ name: str,
217
+ image: str,
218
+ network: str | None = None,
219
+ ports: list[str] | None = None,
220
+ volumes: list[str] | None = None,
221
+ env_vars: dict[str, str] | None = None,
222
+ extra_args: list[str] | None = None,
223
+ command: list[str] | None = None,
224
+ ) -> None:
225
+ """
226
+ Usage:
227
+
228
+ - Runs a Docker container interactively with TTY passthrough
229
+
230
+ Requires:
231
+
232
+ - ``name``:
233
+ - Type: str
234
+ - What: The container name
235
+
236
+ - ``image``:
237
+ - Type: str
238
+ - What: The Docker image to run
239
+
240
+ Optional:
241
+
242
+ - ``network``:
243
+ - Type: str | None
244
+ - What: Docker network to attach to
245
+ - Default: None
246
+
247
+ - ``ports``:
248
+ - Type: list[str] | None
249
+ - What: Port mapping specifications
250
+ - Default: None
251
+
252
+ - ``volumes``:
253
+ - Type: list[str] | None
254
+ - What: Volume mount specifications
255
+ - Default: None
256
+
257
+ - ``env_vars``:
258
+ - Type: dict[str, str] | None
259
+ - What: Environment variables to set
260
+ - Default: None
261
+
262
+ - ``extra_args``:
263
+ - Type: list[str] | None
264
+ - What: Additional docker run arguments
265
+ - Default: None
266
+
267
+ - ``command``:
268
+ - Type: list[str] | None
269
+ - What: Command to run in the container
270
+ - Default: None
271
+ """
272
+ cmd = ["docker", "run", "-it"]
273
+ if extra_args:
274
+ cmd.extend(extra_args)
275
+ for port in ports or []:
276
+ cmd.extend(["-p", port])
277
+ if network:
278
+ cmd.extend(["--network", network])
279
+ for vol in volumes or []:
280
+ cmd.extend(["--volume", vol])
281
+ for key, val in (env_vars or {}).items():
282
+ cmd.extend(["-e", f"{key}={val}"])
283
+ cmd.extend(["--name", name])
284
+ cmd.append(image)
285
+ if command:
286
+ cmd.extend(command)
287
+ run(cmd, inherit_io=True)
288
+
289
+
290
+ def save_redis(app_name: str) -> None:
291
+ """
292
+ Usage:
293
+
294
+ - Persists Redis data before container termination
295
+
296
+ Requires:
297
+
298
+ - ``app_name``:
299
+ - Type: str
300
+ - What: The app name
301
+ """
302
+ logger.debug(
303
+ "Persisting Redis Data prior to Redis Container Termination..."
304
+ )
305
+ run_and_log(
306
+ ["docker", "exec", f"{app_name}_redis_host", "redis-cli", "save"]
307
+ )
308
+
309
+
310
+ def remove_containers(app_name: str) -> None:
311
+ """
312
+ Usage:
313
+
314
+ - Removes all Docker containers for a CAVE app
315
+
316
+ Requires:
317
+
318
+ - ``app_name``:
319
+ - Type: str
320
+ - What: The app name
321
+ """
322
+ save_redis(app_name)
323
+ logger.debug(f"Killing Running App ({app_name})...")
324
+ containers = [
325
+ f"{app_name}_django",
326
+ f"{app_name}_nginx_host",
327
+ f"{app_name}_db_host",
328
+ f"{app_name}_redis_host",
329
+ ]
330
+ run_and_log(["docker", "rm", "--force"] + containers)
331
+ remove_network(app_name)
332
+
333
+
334
+ def remove_volume(app_name: str, suffix: str = "pg_volume") -> None:
335
+ """
336
+ Usage:
337
+
338
+ - Removes a Docker volume for a CAVE app
339
+
340
+ Requires:
341
+
342
+ - ``app_name``:
343
+ - Type: str
344
+ - What: The app name
345
+
346
+ Optional:
347
+
348
+ - ``suffix``:
349
+ - Type: str
350
+ - What: The volume name suffix
351
+ - Default: "pg_volume"
352
+ """
353
+ logger.info(f"Removing Docker DB Volume for App ({app_name})...")
354
+ run_and_log(["docker", "volume", "rm", f"{app_name}_{suffix}"])
355
+
356
+
357
+ def remove_image(app_name: str) -> None:
358
+ """
359
+ Usage:
360
+
361
+ - Removes the Docker image for a CAVE app
362
+
363
+ Requires:
364
+
365
+ - ``app_name``:
366
+ - Type: str
367
+ - What: The app name
368
+ """
369
+ logger.info(f"Removing Docker Images for App ({app_name})...")
370
+ run_and_log(["docker", "rmi", f"cave-app:{app_name}"])
371
+
372
+
373
+ def get_running_apps() -> list[str]:
374
+ """
375
+ Usage:
376
+
377
+ - Lists all running CAVE app names
378
+
379
+ Returns:
380
+
381
+ - ``apps``:
382
+ - Type: list[str]
383
+ - What: A list of app names derived from container names
384
+ """
385
+ result = run(
386
+ ["docker", "ps", "-a", "--format", "{{.Names}}"]
387
+ )
388
+ apps: list[str] = []
389
+ if result.returncode != 0 or not result.stdout:
390
+ return apps
391
+ for line in result.stdout.strip().splitlines():
392
+ if line.endswith("_django"):
393
+ apps.append(line.replace("_django", ""))
394
+ return apps
395
+
396
+
397
+ def get_container_env(container: str, var: str) -> str:
398
+ """
399
+ Usage:
400
+
401
+ - Gets an environment variable value from a running container
402
+
403
+ Requires:
404
+
405
+ - ``container``:
406
+ - Type: str
407
+ - What: The container name
408
+
409
+ - ``var``:
410
+ - Type: str
411
+ - What: The environment variable name
412
+
413
+ Returns:
414
+
415
+ - ``value``:
416
+ - Type: str
417
+ - What: The variable value, or empty string if not found
418
+ """
419
+ import re
420
+
421
+ result = run(
422
+ [
423
+ "docker",
424
+ "inspect",
425
+ "-f",
426
+ "{{.Config.Env}}",
427
+ container,
428
+ ]
429
+ )
430
+ if result.returncode != 0 or not result.stdout:
431
+ return ""
432
+ match = re.search(rf"{var}=([^\s\]]*)", result.stdout)
433
+ return match.group(1) if match else ""
434
+
435
+
436
+ def get_container_host_port(container: str) -> str:
437
+ """
438
+ Usage:
439
+
440
+ - Gets the host port mapping for a container's port 8000
441
+
442
+ Requires:
443
+
444
+ - ``container``:
445
+ - Type: str
446
+ - What: The container name
447
+
448
+ Returns:
449
+
450
+ - ``port``:
451
+ - Type: str
452
+ - What: The host port, or empty string if not found
453
+ """
454
+ result = run(
455
+ [
456
+ "docker",
457
+ "inspect",
458
+ "-f",
459
+ "{{(index (index .NetworkSettings.Ports \"8000/tcp\") 0).HostPort}}",
460
+ container,
461
+ ]
462
+ )
463
+ if result.returncode != 0 or not result.stdout:
464
+ return ""
465
+ return result.stdout.strip()
466
+
467
+
468
+ def get_all_containers(pattern: str) -> list[str]:
469
+ """
470
+ Usage:
471
+
472
+ - Lists all container names matching a pattern suffix
473
+
474
+ Requires:
475
+
476
+ - ``pattern``:
477
+ - Type: str
478
+ - What: The suffix pattern to match (e.g. "_django")
479
+
480
+ Returns:
481
+
482
+ - ``containers``:
483
+ - Type: list[str]
484
+ - What: A list of matching container names
485
+ """
486
+ result = run(
487
+ ["docker", "ps", "-a", "--format", "{{.Names}}"]
488
+ )
489
+ containers: list[str] = []
490
+ if result.returncode != 0 or not result.stdout:
491
+ return containers
492
+ for line in result.stdout.strip().splitlines():
493
+ if line.endswith(pattern):
494
+ containers.append(line)
495
+ return containers
496
+
497
+
498
+ def generate_secret_key(app_name: str) -> str | None:
499
+ """
500
+ Usage:
501
+
502
+ - Generates a Django secret key using the app's Docker image
503
+
504
+ Requires:
505
+
506
+ - ``app_name``:
507
+ - Type: str
508
+ - What: The app name (used as the image tag)
509
+
510
+ Returns:
511
+
512
+ - ``key``:
513
+ - Type: str | None
514
+ - What: The generated secret key, or None if generation failed
515
+ """
516
+ result = run(
517
+ [
518
+ "docker",
519
+ "run",
520
+ "--rm",
521
+ f"cave-app:{app_name}",
522
+ "python",
523
+ "-c",
524
+ "from django.core.management.utils import get_random_secret_key; "
525
+ "print(get_random_secret_key())",
526
+ ]
527
+ )
528
+ if result.returncode != 0 or not result.stdout:
529
+ return None
530
+ return result.stdout.strip()