compose-lazy 0.7.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,10 @@
1
+ """A smart CLI wrapper for docker compose with interactive selection support."""
2
+
3
+ import logging
4
+ from importlib.metadata import version
5
+
6
+ __version__ = version("compose-lazy")
7
+
8
+ logger = logging.getLogger("compose_lazy")
9
+ logger.addHandler(logging.NullHandler())
10
+ logger.propagate = False
compose_lazy/args.py ADDED
@@ -0,0 +1,155 @@
1
+ import logging
2
+ from argparse import ArgumentParser, Namespace
3
+ from typing import Callable, Self
4
+
5
+ from .process import DockerCmdProcessor
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class ArgBuilder:
11
+ def __init__(self, parser: ArgumentParser):
12
+ self.parser = parser
13
+
14
+ def set_defaults(self, func: Callable[[Namespace], int] | None = None) -> Self:
15
+ func = func or DockerCmdProcessor()
16
+ self.parser.set_defaults(func=func)
17
+ return self
18
+
19
+ def add_common_compose_options(self) -> Self:
20
+ return self._add_file_args()._add_profile_args()._add_project_args()
21
+
22
+ def add_service_name_subcmd(self, multiple: bool = False) -> Self:
23
+ """Add positional argument of service name(s) to command definition.
24
+
25
+ multiple=True: accepts multiple service names, all optional (e.g. up, build, stop, etc.).
26
+ multiple=False: accepts single service name, required by docker compose (e.g. exec, run).
27
+ """
28
+ if multiple:
29
+ self.parser.add_argument(
30
+ "service_name",
31
+ nargs="*",
32
+ default=[],
33
+ help="(Optional) target service names",
34
+ )
35
+ # Add trigger option to start interactive selection explicitly.
36
+ self.parser.add_argument(
37
+ "-s",
38
+ "--service",
39
+ action="store_true",
40
+ help="show service name candidates, select interactively",
41
+ )
42
+ else:
43
+ # If no service_name given, interactive selection starts automatically.
44
+ self.parser.add_argument(
45
+ "service_name",
46
+ nargs="?",
47
+ type=lambda x: [x],
48
+ default=[],
49
+ help="(Required) target service name",
50
+ )
51
+ return self
52
+
53
+ def add_inner_bash_cmd_args(self) -> Self:
54
+ self.parser.add_argument(
55
+ "inner_bash_cmd",
56
+ nargs="*",
57
+ default=[],
58
+ help="command to run inside the service (default: bash)",
59
+ )
60
+ return self
61
+
62
+ def add_build_args(self) -> Self:
63
+ self.parser.add_argument(
64
+ "-b",
65
+ "--build",
66
+ action="store_true",
67
+ help="docker compose up `--build`",
68
+ )
69
+ return self
70
+
71
+ def add_detach_args(self) -> Self:
72
+ """add optional argument `-d` to `docker compose up` command."""
73
+ self.parser.add_argument(
74
+ "-d", "--detach", action="store_true", help="docker compose up `-d`"
75
+ )
76
+ return self
77
+
78
+ def add_follow_args(self) -> Self:
79
+ """add optional argument `-f` to `docker compose logs` command."""
80
+ self.parser.add_argument(
81
+ "-fo", "--follow", action="store_true", help="docker compose logs `-f`"
82
+ )
83
+ return self
84
+
85
+ def add_all_args(self) -> Self:
86
+ """add optional argument `--all(-a)` to `docker compose ps` command."""
87
+ self.parser.add_argument(
88
+ "-a",
89
+ "--all",
90
+ action="store_true",
91
+ help="docker compose ps `-a`",
92
+ )
93
+ return self
94
+
95
+ def add_status_args(self) -> Self:
96
+ self.parser.add_argument(
97
+ "-st",
98
+ "--status",
99
+ choices=[
100
+ "created",
101
+ "restarting",
102
+ "running",
103
+ "removing",
104
+ "paused",
105
+ "exited",
106
+ "dead",
107
+ ],
108
+ help="docker compose ps `--status` <STATUS>",
109
+ )
110
+ return self
111
+
112
+ def add_remove_orphans_args(self) -> Self:
113
+ self.parser.add_argument(
114
+ "-ro",
115
+ "--remove-orphans",
116
+ action="store_true",
117
+ help="docker compose down `--remove-orphans`",
118
+ )
119
+ return self
120
+
121
+ def add_wait_args(self) -> Self:
122
+ self.parser.add_argument(
123
+ "-w",
124
+ "--wait",
125
+ action="store_true",
126
+ help="docker compose up `--wait`",
127
+ )
128
+ return self
129
+
130
+ def _add_file_args(self) -> Self:
131
+ self.parser.add_argument(
132
+ "-f",
133
+ "--file",
134
+ nargs="*",
135
+ help="specify compose file(s). if omitted with -f, select interactively",
136
+ )
137
+ return self
138
+
139
+ def _add_project_args(self) -> Self:
140
+ self.parser.add_argument(
141
+ "-p",
142
+ "--project",
143
+ default="",
144
+ help="docker compose `-p PROJECT_NAME`",
145
+ )
146
+ return self
147
+
148
+ def _add_profile_args(self) -> Self:
149
+ self.parser.add_argument(
150
+ "-pf",
151
+ "--profile",
152
+ nargs="*",
153
+ help="specify profile(s). if omitted with -pf, select interactively",
154
+ )
155
+ return self
@@ -0,0 +1,105 @@
1
+ import logging
2
+ import sys
3
+ from typing import Iterable, Literal, overload
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ @overload
9
+ def interactive_select(
10
+ candidates, flag=..., *, multiple=..., allow_zero: Literal[True]
11
+ ) -> None | list[str]: ...
12
+
13
+
14
+ @overload
15
+ def interactive_select(
16
+ candidates, flag=..., *, multiple=..., allow_zero: Literal[False] = ...
17
+ ) -> list[str]: ...
18
+
19
+
20
+ def interactive_select(
21
+ candidates: Iterable[str],
22
+ flag: str | None = None,
23
+ *,
24
+ multiple: bool = True,
25
+ allow_zero: bool = False,
26
+ ) -> list[str] | None:
27
+ """Starts general interactive session.
28
+
29
+ When `allow_zero` is False, returns list[str].
30
+ Only when `allow_zero` is True, may return None (if the user inputs exactly `0`).
31
+ Despite `multiple` being True or False, the choice `0` cannot be included in multiple choices (the session continues).
32
+
33
+ Args:
34
+ candidates (Iterable[str]): list, set, dict, etc... and generator-like objects.
35
+ flag (str | None, optional): Specified when each candidate needs prefix tag in list. Defaults to None.
36
+ multiple (bool, optional): Enables multiple select. Defaults to True.
37
+ allow_zero (bool, optional): Enables the choice `0` make it return None. Defaults to False.
38
+
39
+ Raises:
40
+ SystemExit: Raised when user input `q` or `Q`.
41
+ KeyboardInterrupt: Raised when user stopped session by Ctrl+C.
42
+
43
+ Returns:
44
+ list[str] | None: The list of candidate name(s) user selected, or None when `0` inputed.
45
+ """
46
+ prompt = (
47
+ "\nEnter your choices (e.g., 1,3,4) or 'q' to quit: "
48
+ if multiple
49
+ else "\nEnter your choice or 'q' to quit: "
50
+ )
51
+ err_msg = (
52
+ "☓ Invalid selection. Please use valid numbers."
53
+ if multiple
54
+ else "☓ Invalid selection. Please use a valid number."
55
+ )
56
+
57
+ candidates = sorted(candidates)
58
+ # Show choices
59
+ for idx, candidate in enumerate(candidates, start=1):
60
+ print(f"{idx:>5}. {candidate}")
61
+
62
+ if allow_zero:
63
+ print("\nOr 0 to enter an alternative choice.", end="")
64
+
65
+ # User input
66
+ while True:
67
+ args = []
68
+
69
+ try:
70
+ if (choices_str := input(prompt)) in ["Q", "q"]:
71
+ print("\nCancelled.")
72
+ raise SystemExit
73
+
74
+ choices = list(
75
+ map(
76
+ lambda i: int(i) - 1,
77
+ (i.strip() for i in choices_str.split(",") if i.strip()),
78
+ )
79
+ )
80
+ if allow_zero:
81
+ if choices == [-1]: # When input is "0"
82
+ return None
83
+ if len(choices) > 1 and -1 in choices: # When multiple input includes "0"
84
+ raise ValueError
85
+ if not multiple and len(choices) != 1:
86
+ raise ValueError
87
+ if any((i < 0 for i in choices)):
88
+ raise IndexError
89
+
90
+ for idx in choices:
91
+ chosen = candidates[idx]
92
+ if flag is None:
93
+ args += [chosen]
94
+ else:
95
+ args += [flag, chosen]
96
+
97
+ except (ValueError, IndexError):
98
+ print(err_msg, file=sys.stderr)
99
+ except KeyboardInterrupt as e:
100
+ print("\nCancelled.")
101
+ raise e
102
+ else:
103
+ print()
104
+ break
105
+ return args
compose_lazy/config.py ADDED
@@ -0,0 +1,38 @@
1
+ import logging
2
+ import os
3
+
4
+ try:
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ except ImportError:
10
+ pass
11
+
12
+ DEBUG = os.environ.get("COMPOSE_LAZY_DEBUG", "False").lower() in ["true", "t"]
13
+
14
+ LOG_LEVEL = "DEBUG" if DEBUG else "INFO"
15
+
16
+
17
+ def setup_logger(name: str) -> None:
18
+ """Add console handler only when "DEBUG" is active.
19
+
20
+ :param name: the name of this package
21
+ :return: None
22
+ """
23
+ logger = logging.getLogger(name)
24
+ logger.setLevel(logging.DEBUG)
25
+ logger.propagate = False
26
+
27
+ if any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
28
+ return
29
+
30
+ ch = logging.StreamHandler()
31
+ ch.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
32
+ ch_formatter = logging.Formatter(
33
+ "%(asctime)s | %(levelname)s | %(filename)s %(message)s", "%H:%M:%S"
34
+ )
35
+ ch.setFormatter(ch_formatter)
36
+
37
+ if DEBUG:
38
+ logger.addHandler(ch)
compose_lazy/main.py ADDED
@@ -0,0 +1,330 @@
1
+ import logging
2
+ import sys
3
+ from argparse import ArgumentParser
4
+ from importlib.metadata import version
5
+
6
+ from . import config
7
+ from .args import ArgBuilder
8
+ from .process import DockerCmdProcessor
9
+ from .workspace import WorkspaceProcessor, WorkspaceRegistrar
10
+
11
+ VERSION = version("compose_lazy")
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ processor = DockerCmdProcessor()
16
+ ws_registrar = WorkspaceRegistrar()
17
+ ws_processor = WorkspaceProcessor()
18
+
19
+
20
+ def main() -> None:
21
+ config.setup_logger("compose_lazy")
22
+
23
+ base_parser = ArgumentParser(
24
+ allow_abbrev=False,
25
+ usage="dcp <SUBCOMMAND> [options]",
26
+ description="Shorthand aliases for docker compose commands.",
27
+ epilog="See also: `dcpu -h`, `dcpe -h`, `dcp ws -h`",
28
+ )
29
+ base_parser.suggest_on_error = True
30
+ base_parser.add_argument("--version", action="version", version=f"compose-lazy {VERSION}")
31
+
32
+ root_subparsers = base_parser.add_subparsers(dest="subcmd")
33
+
34
+ # dcp up(u) command
35
+ _up = root_subparsers.add_parser(
36
+ "up",
37
+ aliases=["u"],
38
+ allow_abbrev=False,
39
+ usage="dcp up(u) [SERVICE_NAME ...] [options]",
40
+ description="Shorthand for `docker compose up`.",
41
+ help="docker compose `up`, also available as: dcpu",
42
+ )
43
+ (
44
+ ArgBuilder(_up)
45
+ .add_service_name_subcmd(multiple=True)
46
+ .add_detach_args()
47
+ .add_build_args()
48
+ .add_wait_args()
49
+ .add_common_compose_options()
50
+ .set_defaults(func=processor)
51
+ )
52
+
53
+ # dcp build(b) command
54
+ _build = root_subparsers.add_parser(
55
+ "build",
56
+ aliases=["b"],
57
+ allow_abbrev=False,
58
+ usage="dcp build(b) [SERVICE_NAME ...] [options]",
59
+ description="Shorthand for `docker compose build`.",
60
+ help="docker compose `build`",
61
+ )
62
+ (
63
+ ArgBuilder(_build)
64
+ .add_service_name_subcmd(multiple=True)
65
+ .add_common_compose_options()
66
+ .set_defaults(func=processor)
67
+ )
68
+
69
+ # dcp exec(e) command
70
+ _exec = root_subparsers.add_parser(
71
+ "exec",
72
+ aliases=["e"],
73
+ allow_abbrev=False,
74
+ usage="dcp exec(e) <SERVICE_NAME> [BASH|commands] [options]",
75
+ description="Shorthand for `docker compose exec`.",
76
+ help="docker compose `exec`, also available as: dcpe",
77
+ )
78
+ (
79
+ ArgBuilder(_exec)
80
+ .add_service_name_subcmd(multiple=False)
81
+ .add_inner_bash_cmd_args()
82
+ .add_common_compose_options()
83
+ .set_defaults(func=processor)
84
+ )
85
+
86
+ # dcp run command
87
+ _run = root_subparsers.add_parser(
88
+ "run",
89
+ allow_abbrev=False,
90
+ usage="dcp run <SERVICE_NAME> [BASH|commands]",
91
+ description="Shorthand for `docker compose run`.",
92
+ help="docker compose `run`",
93
+ )
94
+ (
95
+ ArgBuilder(_run)
96
+ .add_service_name_subcmd(multiple=False)
97
+ .add_inner_bash_cmd_args()
98
+ .add_common_compose_options()
99
+ .set_defaults(func=processor)
100
+ )
101
+
102
+ # dcp restart(re) command
103
+ _restart = root_subparsers.add_parser(
104
+ "restart",
105
+ aliases=["re"],
106
+ allow_abbrev=False,
107
+ usage="dcp restart(re) [SERVICE_NAME ...]",
108
+ description="Shorthand for `docker compose restart`.",
109
+ help="docker compose `restart`",
110
+ )
111
+ (
112
+ ArgBuilder(_restart)
113
+ .add_service_name_subcmd(multiple=True)
114
+ .add_common_compose_options()
115
+ .set_defaults(func=processor)
116
+ )
117
+
118
+ # dcp ps command
119
+ _ps = root_subparsers.add_parser(
120
+ "ps",
121
+ allow_abbrev=False,
122
+ usage="dcp ps [SERVICE_NAME ...] [-a] [-st STATUS]",
123
+ description="Shorthand for `docker compose ps`.",
124
+ help="docker compose `ps`",
125
+ )
126
+ (
127
+ ArgBuilder(_ps)
128
+ .add_service_name_subcmd(multiple=True)
129
+ .add_all_args()
130
+ .add_status_args()
131
+ .add_common_compose_options()
132
+ .set_defaults(func=processor)
133
+ )
134
+
135
+ # dcp logs(l) command
136
+ _logs = root_subparsers.add_parser(
137
+ "logs",
138
+ aliases=["l"],
139
+ allow_abbrev=False,
140
+ usage="dcp logs(l) [SERVICE_NAME ...] [-f]",
141
+ description="Shorthand for `docker compose logs`.",
142
+ help="docker compose `logs`",
143
+ )
144
+ (
145
+ ArgBuilder(_logs)
146
+ .add_service_name_subcmd(multiple=True)
147
+ .add_follow_args()
148
+ .add_common_compose_options()
149
+ .set_defaults(func=processor)
150
+ )
151
+
152
+ # dcp stop(s) command
153
+ _stop = root_subparsers.add_parser(
154
+ "stop",
155
+ aliases=["s"],
156
+ allow_abbrev=False,
157
+ usage="dcp stop(s) [SERVICE_NAME ...]",
158
+ description="Shorthand for `docker compose stop`.",
159
+ help="docker compose `stop`",
160
+ )
161
+ (
162
+ ArgBuilder(_stop)
163
+ .add_service_name_subcmd(multiple=True)
164
+ .add_common_compose_options()
165
+ .set_defaults(func=processor)
166
+ )
167
+
168
+ # dcp down command
169
+ _down = root_subparsers.add_parser(
170
+ "down",
171
+ allow_abbrev=False,
172
+ usage="dcp down [-f FILE_NAME ...] [-p PROJECT_NAME] [-ro]",
173
+ description="Shorthand for `docker compose down`.",
174
+ help="docker compose `down`",
175
+ )
176
+ (
177
+ ArgBuilder(_down)
178
+ .add_remove_orphans_args()
179
+ .add_common_compose_options()
180
+ .set_defaults(func=processor)
181
+ )
182
+
183
+ # dcp workspace(ws) command
184
+ _workspace = root_subparsers.add_parser(
185
+ "workspace",
186
+ aliases=["ws"],
187
+ allow_abbrev=False,
188
+ usage="dcp workspace(ws) [SUBCOMMAND] [options]",
189
+ description="Operate all repos in a user-defined workspace (a named group of repositories).",
190
+ help="Original command, operate multiple repos at once. See also `dcp ws -h`.",
191
+ )
192
+ ws_subparsers = _workspace.add_subparsers(dest="ws_subcmd")
193
+
194
+ # dcp ws register command
195
+ ws_subparsers.add_parser(
196
+ "register",
197
+ aliases=["reg"],
198
+ allow_abbrev=False,
199
+ usage="dcp ws register(reg)",
200
+ description="Register a new repository to a workspace interactively.",
201
+ help="Register a new repo to a workspace.",
202
+ ).set_defaults(func=ws_registrar)
203
+
204
+ # dcp ws delete command
205
+ ws_subparsers.add_parser(
206
+ "delete",
207
+ aliases=["del"],
208
+ allow_abbrev=False,
209
+ usage="dcp ws delete(del)",
210
+ description="Delete a repository from a workspace interactively.",
211
+ help="Delete a repo from a workspace.",
212
+ ).set_defaults(func=ws_registrar)
213
+
214
+ # dcp ws list command
215
+ ws_subparsers.add_parser(
216
+ "list",
217
+ aliases=["li"],
218
+ allow_abbrev=False,
219
+ usage="dcp ws list(li)",
220
+ description="Show all registered workspaces and their repositories.",
221
+ help="List all registered workspaces.",
222
+ ).set_defaults(func=ws_registrar)
223
+
224
+ # dcp ws up command
225
+ ws_subparsers.add_parser(
226
+ "up",
227
+ aliases=["u"],
228
+ allow_abbrev=False,
229
+ usage="dcp ws up(u)",
230
+ description="Run `docker compose up` for all repos in a selected workspace.",
231
+ help="docker compose `up` for each repo.",
232
+ ).set_defaults(func=ws_processor)
233
+
234
+ # dcp ws restart command
235
+ ws_subparsers.add_parser(
236
+ "restart",
237
+ aliases=["re"],
238
+ allow_abbrev=False,
239
+ usage="dcp ws restart(re)",
240
+ description="Run `docker compose restart` for all repos in a selected workspace.",
241
+ help="docker compose `restart` for each repo.",
242
+ ).set_defaults(func=ws_processor)
243
+
244
+ # dcp ws stop command
245
+ ws_subparsers.add_parser(
246
+ "stop",
247
+ aliases=["s"],
248
+ allow_abbrev=False,
249
+ usage="dcp ws stop(s)",
250
+ description="Run `docker compose stop` for all repos in a selected workspace.",
251
+ help="docker compose `stop` for each repo.",
252
+ ).set_defaults(func=ws_processor)
253
+
254
+ # dcp ws down command
255
+ ws_subparsers.add_parser(
256
+ "down",
257
+ allow_abbrev=False,
258
+ usage="dcp ws down",
259
+ description="Run `docker compose down` for all repos in a selected workspace.",
260
+ help="docker compose `down` for each repo.",
261
+ ).set_defaults(func=ws_processor)
262
+
263
+ args = base_parser.parse_args()
264
+ if args.subcmd is None:
265
+ base_parser.print_help()
266
+ sys.exit(0)
267
+ if args.subcmd in ("workspace", "ws") and args.ws_subcmd is None:
268
+ _workspace.print_help()
269
+ sys.exit(0)
270
+
271
+ code = args.func(args)
272
+ sys.exit(code)
273
+
274
+
275
+ def dcpu_main() -> None:
276
+ config.setup_logger("compose_lazy")
277
+
278
+ parser = ArgumentParser(
279
+ allow_abbrev=False,
280
+ usage="dcpu [SERVICE_NAME] [options]",
281
+ description="Shorthand for `docker compose up`.",
282
+ epilog="See also: `dcp -h`, `dcpe -h`",
283
+ )
284
+
285
+ # dcpu command
286
+ (
287
+ ArgBuilder(parser)
288
+ .add_service_name_subcmd(multiple=True)
289
+ .add_detach_args()
290
+ .add_build_args()
291
+ .add_wait_args()
292
+ .add_common_compose_options()
293
+ .set_defaults(func=processor.call_dcpu)
294
+ )
295
+
296
+ parser.add_argument(
297
+ "-v", "--version", action="version", version=f"compose-lazy {VERSION}"
298
+ )
299
+
300
+ args = parser.parse_args()
301
+ code = args.func(args)
302
+ sys.exit(code)
303
+
304
+
305
+ def dcpe_main() -> None:
306
+ config.setup_logger("compose_lazy")
307
+
308
+ parser = ArgumentParser(
309
+ allow_abbrev=False,
310
+ usage="dcpe <SERVICE_NAME> [BASH|commands] [options]",
311
+ description="Shorthand for `docker compose exec`.",
312
+ epilog="See also: `dcp -h`, `dcpu -h`",
313
+ )
314
+
315
+ # dcpe command
316
+ (
317
+ ArgBuilder(parser)
318
+ .add_service_name_subcmd(multiple=False)
319
+ .add_inner_bash_cmd_args()
320
+ .add_common_compose_options()
321
+ .set_defaults(func=processor.call_dcpe)
322
+ )
323
+
324
+ parser.add_argument(
325
+ "-v", "--version", action="version", version=f"compose-lazy {VERSION}"
326
+ )
327
+
328
+ args = parser.parse_args()
329
+ code = args.func(args)
330
+ sys.exit(code)