Skip to content

Commit 10e47d2

Browse files
committed
Add docker_cli_wrapper.py
Replace prune cache with build --no-cache Fix unit tests - needs_docker test failing Fix end to end tests and docker info Testing unit tesseract tests since it's so slow on my machine Switch from monkeypatched fixture to mocking actual method Fix unused import Reinsert mocked_docker fixture in test_needs_docker Fix args which were missed in refactor Fix run bug
1 parent bb92967 commit 10e47d2

File tree

9 files changed

+784
-596
lines changed

9 files changed

+784
-596
lines changed

tesseract_core/sdk/cli.py

Lines changed: 71 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55

66
import json
77
import re
8-
import subprocess
98
import sys
109
import time
1110
import webbrowser
12-
from collections import defaultdict
1311
from contextlib import nullcontext
1412
from enum import Enum
1513
from logging import getLogger
@@ -18,11 +16,6 @@
1816
from typing import Annotated, Any, NoReturn
1917

2018
import click
21-
import docker
22-
import docker.errors
23-
import docker.models
24-
import docker.models.containers
25-
import docker.models.images
2619
import typer
2720
from jinja2 import Environment, PackageLoader, StrictUndefined
2821
from rich.console import Console as RichConsole
@@ -35,6 +28,7 @@
3528
TesseractConfig,
3629
get_non_base_fields_in_tesseract_config,
3730
)
31+
from .docker_cli_wrapper import DockerWrapper
3832
from .exceptions import UserError
3933
from .logs import DEFAULT_CONSOLE, set_logger
4034

@@ -296,9 +290,10 @@ def build_image(
296290
keep_build_cache=keep_build_cache,
297291
generate_only=generate_only,
298292
)
299-
except docker.errors.BuildError as e:
300-
raise UserError(f"Error building Tesseract: {e}") from e
301-
except docker.errors.APIError as e:
293+
except RuntimeError as e:
294+
# If "build" in error message:
295+
if "build" in str(e):
296+
raise UserError(f"Error building Tesseract: {e}") from e
302297
raise UserError(f"Docker server error: {e}") from e
303298
except TypeError as e:
304299
raise UserError(f"Input error building Tesseract: {e}") from e
@@ -443,19 +438,13 @@ def serve(
443438
)
444439

445440
try:
446-
project_id = engine.serve(image_names, port, volume, gpus)
447-
containers = engine.project_containers(project_id)
448-
_display_container_meta(containers)
441+
project_id, docker_wrapper = engine.serve(image_names, port, volume, gpus)
442+
container_ports = _display_project_meta(project_id, docker_wrapper)
449443
logger.info(
450444
f"Docker Compose Project ID, use it with 'tesseract teardown' command: {project_id}"
451445
)
452446

453-
project_meta = {"project_id": project_id, "containers": []}
454-
for container in containers:
455-
project_meta["containers"].append(
456-
{"name": container.name, "port": _get_container_host_port(container)}
457-
)
458-
447+
project_meta = {"project_id": project_id, "containers": container_ports}
459448
json_info = json.dumps(project_meta)
460449
typer.echo(json_info, nl=False)
461450

@@ -471,30 +460,31 @@ def serve(
471460
@engine.needs_docker
472461
def list_tesseract_images() -> None:
473462
"""Display all Tesseract images."""
474-
tesseract_images = engine.get_tesseract_images()
475-
_display_tesseract_image_meta(tesseract_images)
463+
docker_wrapper = DockerWrapper()
464+
_display_tesseract_image_meta(docker_wrapper)
476465

477466

478467
@app.command("ps")
479468
@engine.needs_docker
480469
def list_tesseract_containers() -> None:
481470
"""Display all Tesseract containers."""
482-
tesseract_containers = engine.get_tesseract_containers()
483-
_display_tesseract_containers_meta(tesseract_containers)
471+
docker_wrapper = DockerWrapper()
472+
_display_tesseract_containers_meta(docker_wrapper)
484473

485474

486475
def _display_tesseract_image_meta(
487-
docker_assets: list[docker.models.images.Image],
476+
docker_wrapper: DockerWrapper,
488477
) -> None:
489478
"""Display Tesseract image metadata."""
490479
table = RichTable("ID", "Tags", "Name", "Version", "Description")
491-
for asset in docker_assets:
492-
tesseract_vals = _get_tesseract_env_vals(asset)
480+
images = docker_wrapper.get_all_images()
481+
for image in images:
482+
tesseract_vals = _get_tesseract_env_vals(image)
493483
if tesseract_vals:
494484
table.add_row(
495485
# Checksum Type + First 12 Chars of ID
496-
asset.id[:19],
497-
str(asset.attrs["RepoTags"]),
486+
image.id[:19],
487+
image.name,
498488
tesseract_vals["TESSERACT_NAME"],
499489
tesseract_vals["TESSERACT_VERSION"],
500490
tesseract_vals.get("TESSERACT_DESCRIPTION", "").replace("\n", " "),
@@ -503,91 +493,52 @@ def _display_tesseract_image_meta(
503493

504494

505495
def _display_tesseract_containers_meta(
506-
docker_assets: list[docker.models.containers.Container],
496+
docker_wrapper: DockerWrapper,
507497
) -> None:
508498
"""Display Tesseract containers metadata."""
509499
table = RichTable("ID", "Name", "Version", "Host Port", "Project ID", "Description")
510-
docker_compose_projects = _docker_compose_projects()
511500

512-
for asset in docker_assets:
513-
tesseract_vals = _get_tesseract_env_vals(asset)
501+
containers = docker_wrapper.get_all_containers()
502+
for _, container in containers.items():
503+
tesseract_vals = _get_tesseract_env_vals(container)
514504
if tesseract_vals:
515-
tesseract_project = _find_tesseract_project(asset, docker_compose_projects)
505+
tesseract_project = _find_tesseract_project(container, docker_wrapper)
516506
table.add_row(
517-
asset.id[:12],
507+
container.id[:12],
518508
tesseract_vals["TESSERACT_NAME"],
519509
tesseract_vals["TESSERACT_VERSION"],
520-
_get_container_host_port(asset),
510+
container.host_port,
521511
tesseract_project,
522512
tesseract_vals.get("TESSERACT_DESCRIPTION", "").replace("\\n", " "),
523513
)
524514
RichConsole().print(table)
525515

526516

527517
def _get_tesseract_env_vals(
528-
docker_asset: docker.models.images.Image | docker.models.containers.Container,
518+
docker_asset: DockerWrapper.Image | DockerWrapper.Container,
529519
) -> dict:
530520
"""Convert Tesseract environment variables from list to dictionary."""
531521
env_vals = [s for s in docker_asset.attrs["Config"]["Env"] if "TESSERACT_" in s]
532522
return {item.split("=")[0]: item.split("=")[1] for item in env_vals}
533523

534524

535525
def _find_tesseract_project(
536-
tesseract: docker.models.containers.Container,
537-
docker_compose_projects: defaultdict[str, list],
526+
tesseract: DockerWrapper.Container,
527+
docker_wrapper: DockerWrapper,
538528
) -> str:
539529
"""Find the Tesseract Project ID for a given tesseract."""
530+
if tesseract.project_id is not None:
531+
return tesseract.project_id
532+
540533
tesseract_id = tesseract.id[:12]
541534

542-
for project, containers in docker_compose_projects.items():
535+
for project, containers in docker_wrapper.get_projects.items():
543536
if tesseract_id in containers:
544537
return project
545538

546539
return "Unknown"
547540

548541

549-
def _docker_compose_projects() -> defaultdict[str, list]:
550-
"""List Docker Compose projects.
551-
552-
Build a dictionary {project_name: [container ID]}.
553-
"""
554-
proc = subprocess.run(
555-
["docker", "compose", "ls", "--format", "json"],
556-
check=True,
557-
capture_output=True,
558-
)
559-
560-
out = proc.stdout.decode().strip()
561-
compose_projects = json.loads(out)
562-
563-
projects_map = defaultdict(list)
564-
565-
for project in compose_projects:
566-
project_name = project["Name"]
567-
568-
proc = subprocess.run(
569-
[
570-
"docker",
571-
"compose",
572-
"--project-name",
573-
project_name,
574-
"ps",
575-
"--format",
576-
"json",
577-
],
578-
check=True,
579-
capture_output=True,
580-
)
581-
# This command outputs a series of JSON documents, one for each
582-
# container, instead of a JSON with a list of entries.
583-
compose_ps = proc.stdout.decode().strip().split("\n")
584-
for asset in compose_ps:
585-
metadata = json.loads(asset)
586-
projects_map[project_name].append(metadata["ID"])
587-
588-
return projects_map
589-
590-
591542
@app.command("apidoc")
592543
@engine.needs_docker
593544
def apidoc(
@@ -602,12 +553,11 @@ def apidoc(
602553
) -> None:
603554
"""Serve the OpenAPI schema for a Tesseract."""
604555
project_id = None
605-
556+
docker_wrapper = None
606557
try:
607-
project_id = engine.serve([image_name])
608-
container = engine.project_containers(project_id)[0]
609-
host_port = _get_container_host_port(container)
610-
url = f"http://localhost:{host_port}/docs"
558+
project_id, docker_wrapper = engine.serve([image_name])
559+
container = docker_wrapper.get_projects()[project_id][0]
560+
url = f"http://localhost:{container.host_port}/docs"
611561
logger.info(f"Serving OpenAPI docs for Tesseract {image_name} at {url}")
612562
logger.info(" Press Ctrl+C to stop")
613563
if browser:
@@ -619,27 +569,28 @@ def apidoc(
619569
return
620570
finally:
621571
if project_id is not None:
622-
engine.teardown(project_id)
572+
engine.teardown(project_id, docker_wrapper=docker_wrapper)
623573

624574

625-
def _display_container_meta(
626-
containers: list[docker.models.containers.Container],
627-
) -> None:
628-
"""Display container metadata."""
629-
for container in containers:
575+
def _display_project_meta(project_id: str, docker_wrapper: DockerWrapper) -> list:
576+
"""Display project metadata.
577+
578+
Returns a list of dictionaries {name: container_name, port: host_port}.
579+
"""
580+
container_ports = []
581+
projects = docker_wrapper.get_projects()
582+
containers = projects[project_id]
583+
for container_id in containers:
584+
container = docker_wrapper.get_container(container_id)
630585
logger.info(f"Container ID: {container.id}")
631586
logger.info(f"Name: {container.name}")
632587
entrypoint = container.attrs["Config"]["Entrypoint"]
633588
logger.info(f"Entrypoint: {entrypoint}")
634-
port_key = next(iter(container.ports))
635-
host_port = container.ports[port_key][0]["HostPort"]
589+
host_port = container.host_port
636590
logger.info(f"View Tesseract: http://localhost:{host_port}/docs")
591+
container_ports.append({"name": container.name, "port": host_port})
637592

638-
639-
def _get_container_host_port(container: docker.models.containers.Container):
640-
port_key = next(iter(container.ports))
641-
host_port = container.ports[port_key][0]["HostPort"]
642-
return host_port
593+
return container_ports
643594

644595

645596
@app.command("teardown")
@@ -672,27 +623,19 @@ def teardown(
672623
"Either project IDs or --all flag must be provided, but not both",
673624
param_hint="project_ids",
674625
)
675-
if tear_all:
676-
project_ids = []
677-
docker_compose_projects = _docker_compose_projects()
678-
for project, _ in docker_compose_projects.items():
679-
if "tesseract-" in project:
680-
project_ids.append(project)
681-
682-
for project_id in project_ids:
683-
try:
684-
engine.teardown(project_id)
685-
except ValueError as ex:
686-
raise UserError(
687-
f"Input error occurred while tearing down Tesseracts: {ex}"
688-
) from ex
689-
except RuntimeError as ex:
690-
raise UserError(
691-
f"Internal Docker error occurred while tearing down Tesseracts: {ex}"
692-
) from ex
693-
logger.info(
694-
f"Tesseracts are shutdown for Docker Compose project ID: {project_id}"
695-
)
626+
627+
try:
628+
if not project_ids:
629+
project_ids = [] # Pass in empty list if no project_ids are provided.
630+
engine.teardown(project_ids, tear_all=tear_all)
631+
except ValueError as ex:
632+
raise UserError(
633+
f"Input error occurred while tearing down Tesseracts: {ex}"
634+
) from ex
635+
except RuntimeError as ex:
636+
raise UserError(
637+
f"Internal Docker error occurred while tearing down Tesseracts: {ex}"
638+
) from ex
696639

697640

698641
def _sanitize_error_output(error_output: str, tesseract_image: str) -> str:
@@ -810,23 +753,21 @@ def run_container(
810753
tesseract_image, cmd, args, volumes=volume, gpus=gpus
811754
)
812755

813-
except docker.errors.ImageNotFound as e:
814-
raise UserError(
815-
"Tesseract image not found. "
816-
f"Are you sure your tesseract image name is {tesseract_image}?\n\n{e}"
817-
) from e
818-
819-
except (
820-
docker.errors.APIError,
821-
docker.errors.ContainerError,
822-
) as e:
756+
except RuntimeError as e:
823757
if "No such command" in str(e):
824758
error_string = f"Error running Tesseract '{tesseract_image}' \n\n Error: Unimplemented command '{cmd}'. "
825759
else:
826760
error_string = _sanitize_error_output(
827761
f"Error running Tesseract. \n\n{e}", tesseract_image
828762
)
829763

764+
765+
if "repository" in str(e) or "Repository" in str(e):
766+
raise UserError(
767+
"Tesseract image not found. "
768+
f"Are you sure your tesseract image name is {tesseract_image}?\n\n{e}"
769+
) from e
770+
830771
raise UserError(error_string) from e
831772

832773
if invoke_help:

0 commit comments

Comments
 (0)