Coverage for harbor_cli/state.py: 58%
61 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3import asyncio
4from pathlib import Path
5from typing import Awaitable
6from typing import Optional
7from typing import Tuple
8from typing import Type
9from typing import TYPE_CHECKING
10from typing import TypeVar
12if TYPE_CHECKING:
13 from rich.console import Console
15from harborapi import HarborAsyncClient
16from harborapi.exceptions import StatusError
17from pydantic import BaseModel
19from .config import HarborCLIConfig
20from .exceptions import handle_status_error
22T = TypeVar("T")
25class CommonOptions(BaseModel):
26 """Options that can be used with any command.
28 These options are not specific to any particular command.
29 """
31 # Output
32 verbose: bool = False
33 with_stdout: bool = False
34 # File
35 output_file: Optional[Path] = None
36 no_overwrite: bool = False
38 class Config:
39 extra = "allow"
42class State:
43 """Class used to manage the state of the program.
45 It is used as a singleton shared between all commands.
46 Unlike a context object, the state object is not passed to each
47 command, but is instead accessed via the global state variable.
48 """
50 config: HarborCLIConfig
51 client: HarborAsyncClient
52 loop: asyncio.AbstractEventLoop
53 options: CommonOptions
54 repl: bool = False
55 config_loaded: bool = False
56 console: Optional[Console]
58 def __init__(self) -> None:
59 """Initialize the state object."""
60 self.config = HarborCLIConfig()
61 self.client = None # type: ignore # will be patched by init_state
62 self.loop = asyncio.get_event_loop()
63 self.options = CommonOptions()
64 self.console = None
66 def add_config(self, config: "HarborCLIConfig") -> None:
67 """Add a config object to the state."""
68 self.config = config
69 self.config_loaded = True
71 def add_client(self, client: HarborAsyncClient) -> None:
72 """Add a client object to the state."""
73 self.client = client
75 def _init_console(self) -> None:
76 """Import the console object if it hasn't been imported yet.
77 We do this here, so that we don't create a circular import
78 between the state module and the output module."""
79 # fmt: off
80 from .output.console import console
81 self.console = console
82 # fmt: on
84 def run(
85 self,
86 coro: Awaitable[T],
87 status: Optional[str] = None,
88 no_handle: Type[Exception] | Tuple[Type[Exception], ...] | None = None,
89 ) -> T:
90 """Run a coroutine in the event loop.
92 Parameters
93 ----------
94 coro : Awaitable[T]
95 The coroutine to run.
96 no_handle : Type[Exception] | Tuple[Type[Exception], ...]
97 A single exception type or a tuple of exception types that
98 should not be passed to the default exception handler.
99 Exceptions of this type will be raised as-is.
100 """
101 if self.console is None:
102 self._init_console()
103 assert self.console is not None
105 if not status:
106 status = "Working..."
107 if not status.endswith("..."): # aesthetic :)
108 status += "..."
110 try:
111 # show spinner when running a coroutine
112 with self.console.status(status):
113 resp = self.loop.run_until_complete(coro)
114 except StatusError as e:
115 if no_handle and isinstance(e, no_handle):
116 raise
117 handle_status_error(e)
118 return resp
121state = State()