easy-docker-manager 1.0.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.
Files changed (42) hide show
  1. easy_docker_manager/__init__.py +0 -0
  2. easy_docker_manager/app/__init__.py +3 -0
  3. easy_docker_manager/app/app.py +176 -0
  4. easy_docker_manager/app/background_notifier.py +206 -0
  5. easy_docker_manager/app/background_task_result_handler.py +447 -0
  6. easy_docker_manager/app/background_task_runner.py +211 -0
  7. easy_docker_manager/app/runtime_factory.py +122 -0
  8. easy_docker_manager/app/scheduler.py +422 -0
  9. easy_docker_manager/config/__init__.py +3 -0
  10. easy_docker_manager/config/app_config_store.py +143 -0
  11. easy_docker_manager/constants.py +3 -0
  12. easy_docker_manager/core/__init__.py +16 -0
  13. easy_docker_manager/core/config.py +54 -0
  14. easy_docker_manager/core/containers.py +25 -0
  15. easy_docker_manager/core/content_cache.py +129 -0
  16. easy_docker_manager/core/log_text.py +103 -0
  17. easy_docker_manager/core/tabs.py +17 -0
  18. easy_docker_manager/core/ui_session_state.py +142 -0
  19. easy_docker_manager/docker/__init__.py +0 -0
  20. easy_docker_manager/docker/base.py +143 -0
  21. easy_docker_manager/docker/client_factory.py +34 -0
  22. easy_docker_manager/docker/container_mapper.py +31 -0
  23. easy_docker_manager/docker/error_mapping.py +34 -0
  24. easy_docker_manager/docker/local.py +222 -0
  25. easy_docker_manager/docker/log_availability.py +60 -0
  26. easy_docker_manager/logging/__init__.py +3 -0
  27. easy_docker_manager/logging/app_logging.py +94 -0
  28. easy_docker_manager/main.py +24 -0
  29. easy_docker_manager/tabs/__init__.py +3 -0
  30. easy_docker_manager/tabs/config_tab_formatter.py +356 -0
  31. easy_docker_manager/tabs/tab_data_loader.py +145 -0
  32. easy_docker_manager/ui/__init__.py +5 -0
  33. easy_docker_manager/ui/formatting.py +358 -0
  34. easy_docker_manager/ui/keyboard_controller.py +167 -0
  35. easy_docker_manager/ui/terminal_layout.py +423 -0
  36. easy_docker_manager/ui/ui_controller.py +272 -0
  37. easy_docker_manager-1.0.0.dist-info/METADATA +266 -0
  38. easy_docker_manager-1.0.0.dist-info/RECORD +42 -0
  39. easy_docker_manager-1.0.0.dist-info/WHEEL +5 -0
  40. easy_docker_manager-1.0.0.dist-info/entry_points.txt +2 -0
  41. easy_docker_manager-1.0.0.dist-info/licenses/LICENSE +21 -0
  42. easy_docker_manager-1.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,3 @@
1
+ from easy_docker_manager.app.app import EDMApp
2
+
3
+ __all__ = ["EDMApp"]
@@ -0,0 +1,176 @@
1
+ """Run the Easy Docker Manager terminal application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Optional
7
+
8
+ import urwid
9
+
10
+ from easy_docker_manager.app.background_notifier import (
11
+ BackgroundNotifier,
12
+ create_background_notifier,
13
+ )
14
+ from easy_docker_manager.app.background_task_result_handler import (
15
+ BackgroundTaskResultHandler,
16
+ )
17
+ from easy_docker_manager.app.runtime_factory import EDMRuntimeFactory
18
+ from easy_docker_manager.app.scheduler import BackgroundTaskScheduler
19
+ from easy_docker_manager.core import AppConfig
20
+ from easy_docker_manager.docker.base import ContainerDataSource
21
+ from easy_docker_manager.ui.keyboard_controller import KeyAction, KeyboardController
22
+ from easy_docker_manager.ui.terminal_layout import TerminalLayoutView
23
+ from easy_docker_manager.ui.ui_controller import UIController
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class _KeyboardRoutingWidget(urwid.WidgetWrap):
29
+ """Send Urwid keypresses to EDMApp."""
30
+
31
+ def __init__(self, app: EDMApp) -> None:
32
+ """Wrap the main layout so EDMApp can handle every keypress."""
33
+ self.app = app
34
+ super().__init__(app.layout)
35
+
36
+ def keypress(self, size: tuple[int, ...], key: str) -> Optional[str]:
37
+ """Pass one Urwid keypress to EDMApp."""
38
+ return self.app.handle_keyboard_input(key, size)
39
+
40
+
41
+ class EDMApp:
42
+ """Run the terminal UI and coordinate the application.
43
+
44
+ EDMApp handles keyboard input, processes completed background tasks, redraws
45
+ the screen when data changes, and closes application resources during
46
+ shutdown. The console entry point creates one EDMApp and calls run().
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ app_config: Optional[AppConfig] = None,
52
+ container_data_source: Optional[ContainerDataSource] = None,
53
+ runtime_factory: Optional[EDMRuntimeFactory] = None,
54
+ background_notifier: Optional[BackgroundNotifier] = None,
55
+ ) -> None:
56
+ # Workers need a way to wake EDMApp before the Urwid loop exists. The
57
+ # notifier uses a pipe on Unix-like systems and polling on Windows.
58
+ self.background_notifier = (
59
+ background_notifier
60
+ if background_notifier is not None
61
+ else create_background_notifier()
62
+ )
63
+ self.ui_event_loop: Optional[urwid.MainLoop] = None
64
+ self._background_check_timer_handle: Optional[Any] = None
65
+
66
+ selected_runtime_factory = (
67
+ runtime_factory
68
+ if runtime_factory is not None
69
+ else EDMRuntimeFactory(
70
+ app_config=app_config,
71
+ container_data_source=container_data_source,
72
+ )
73
+ )
74
+ runtime = selected_runtime_factory.create_runtime(
75
+ self._notify_background_task_ready
76
+ )
77
+
78
+ # Keep the objects used to run background work, draw the terminal, and
79
+ # close the Docker connection when EDM stops.
80
+ self.container_data_source: ContainerDataSource = runtime.container_data_source
81
+ self.task_runner = runtime.task_runner
82
+ self.terminal_layout_view: TerminalLayoutView = runtime.terminal_layout_view
83
+ self.layout = self.terminal_layout_view.layout
84
+
85
+ # Keep the objects that schedule Docker requests, handle keyboard
86
+ # actions, and apply results returned by background workers.
87
+ self.scheduler: BackgroundTaskScheduler = runtime.scheduler
88
+ self.ui_controller: UIController = runtime.ui_controller
89
+ self.keyboard_controller: KeyboardController = runtime.keyboard_controller
90
+ self.background_task_result_handler: BackgroundTaskResultHandler = (
91
+ runtime.background_task_result_handler
92
+ )
93
+
94
+ def run(self) -> None:
95
+ """Start the terminal UI, then close its resources when the UI stops."""
96
+ logger.info("Starting EDM app")
97
+ try:
98
+ self.ui_event_loop = urwid.MainLoop(
99
+ _KeyboardRoutingWidget(self),
100
+ palette=self.terminal_layout_view.build_palette(),
101
+ handle_mouse=False,
102
+ )
103
+ self.background_notifier.start(
104
+ self.ui_event_loop,
105
+ self._process_completed_background_tasks,
106
+ )
107
+ self.scheduler.schedule_container_refresh(force=True)
108
+ self.ui_controller.render_current_state()
109
+ self._schedule_next_background_check(delay=0)
110
+ self.ui_event_loop.run()
111
+ finally:
112
+ self.background_notifier.stop()
113
+ self.task_runner.shutdown(wait=True)
114
+ self.container_data_source.close()
115
+ logger.info("Stopped EDM app")
116
+
117
+ def handle_keyboard_input(
118
+ self,
119
+ key: str,
120
+ terminal_size: Optional[tuple[int, ...]] = None,
121
+ ) -> Optional[str]:
122
+ """Handle one keypress, redraw when needed, or exit on Quit."""
123
+ action = self.keyboard_controller.handle_keypress(key, terminal_size)
124
+ if action == KeyAction.QUIT:
125
+ raise urwid.ExitMainLoop()
126
+ if action == KeyAction.RENDER:
127
+ self.ui_controller.render_current_state()
128
+ self.scheduler.schedule_next_tasks()
129
+ self._schedule_next_background_check()
130
+ return None
131
+
132
+ def _schedule_next_background_tasks(
133
+ self,
134
+ _loop: urwid.MainLoop,
135
+ _data: Any = None,
136
+ ) -> None:
137
+ """Start due background work, then schedule the next check."""
138
+ self._background_check_timer_handle = None
139
+ self.scheduler.schedule_next_tasks()
140
+ self._schedule_next_background_check()
141
+
142
+ def _process_completed_background_tasks(self, _data: bytes) -> None:
143
+ """Handle completed background tasks and redraw if the screen changed."""
144
+ should_redraw = False
145
+ for completed_task in self.task_runner.pop_all_completed_tasks():
146
+ should_redraw = (
147
+ self.background_task_result_handler.handle_completed_task(
148
+ completed_task
149
+ )
150
+ or should_redraw
151
+ )
152
+ self.scheduler.schedule_next_tasks()
153
+ self._schedule_next_background_check()
154
+ if should_redraw:
155
+ self.ui_controller.render_current_state()
156
+
157
+ def _schedule_next_background_check(self, delay: Optional[float] = None) -> None:
158
+ """Set a timer for the next container, tab, or log update."""
159
+ if self.ui_event_loop is None:
160
+ return
161
+ if self._background_check_timer_handle is not None:
162
+ self.ui_event_loop.remove_alarm(self._background_check_timer_handle)
163
+ next_delay = (
164
+ self.scheduler.seconds_until_next_task_check() if delay is None else delay
165
+ )
166
+ self._background_check_timer_handle = self.ui_event_loop.set_alarm_in(
167
+ next_delay,
168
+ self._schedule_next_background_tasks,
169
+ )
170
+
171
+ def _notify_background_task_ready(self) -> None:
172
+ """Tell the notifier that a worker result is ready for EDMApp."""
173
+ self.background_notifier.notify()
174
+
175
+
176
+ __all__ = ["EDMApp"]
@@ -0,0 +1,206 @@
1
+ """Tell EDMApp when background work is ready to process.
2
+
3
+ Background threads cannot update the terminal UI directly. On Unix-like
4
+ systems, a pipe wakes the UI as soon as a task finishes. On Windows, a timer
5
+ checks for finished tasks every 0.2 seconds. Both methods run the callback on
6
+ the UI thread, where it is safe to update the screen.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ from abc import ABC, abstractmethod
14
+ from collections.abc import Callable
15
+ from threading import Lock
16
+ from typing import Any, Optional
17
+
18
+ import urwid
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ BackgroundTaskReadyCallback = Callable[[bytes], None]
23
+
24
+
25
+ class BackgroundNotifier(ABC):
26
+ """Wake EDMApp on the UI thread when worker results are ready."""
27
+
28
+ @abstractmethod
29
+ def start(
30
+ self,
31
+ loop: urwid.MainLoop,
32
+ callback: BackgroundTaskReadyCallback,
33
+ ) -> None:
34
+ """Connect the notifier to the event loop that manages the terminal UI.
35
+
36
+ Urwid's MainLoop handles keyboard input, timers, and screen updates.
37
+ This method registers callback so completed work is handled on the same
38
+ thread as those UI operations.
39
+ """
40
+
41
+ @abstractmethod
42
+ def notify(self) -> None:
43
+ """Tell EDMApp that one or more worker results are ready."""
44
+
45
+ @abstractmethod
46
+ def stop(self) -> None:
47
+ """Stop notifications and release any Urwid resources."""
48
+
49
+
50
+ class PipeBackgroundNotifier(BackgroundNotifier):
51
+ """Notify EDMApp through a pipe on Unix-like operating systems.
52
+
53
+ Linux and macOS support Urwid's watch_pipe feature. When background work
54
+ finishes, notify() writes one byte to the pipe. Urwid detects the byte and
55
+ immediately runs EDMApp's callback on the UI thread. This implementation
56
+ does not need a timer because the pipe itself signals that work is ready.
57
+ """
58
+
59
+ def __init__(self) -> None:
60
+ self._loop: Optional[urwid.MainLoop] = None
61
+ self._pipe_write: Optional[int] = None
62
+ self._lock = Lock()
63
+
64
+ def start(
65
+ self,
66
+ loop: urwid.MainLoop,
67
+ callback: BackgroundTaskReadyCallback,
68
+ ) -> None:
69
+ """Register a pipe that runs callback on the Urwid UI thread."""
70
+ watch_pipe = getattr(loop, "watch_pipe", None)
71
+ if not callable(watch_pipe):
72
+ raise RuntimeError("Urwid watch_pipe is not available on this platform")
73
+ with self._lock:
74
+ self._loop = loop
75
+ self._pipe_write = watch_pipe(callback)
76
+
77
+ def notify(self) -> None:
78
+ """Write one byte to wake the Urwid pipe watcher."""
79
+ with self._lock:
80
+ if self._pipe_write is None:
81
+ return
82
+ try:
83
+ os.write(self._pipe_write, b"x")
84
+ except OSError:
85
+ logger.debug("Unable to notify the terminal UI event loop")
86
+
87
+ def stop(self) -> None:
88
+ """Remove the pipe watcher and close its write descriptor."""
89
+ with self._lock:
90
+ loop = self._loop
91
+ pipe_write = self._pipe_write
92
+ self._loop = None
93
+ self._pipe_write = None
94
+ if loop is None or pipe_write is None:
95
+ return
96
+ try:
97
+ loop.remove_watch_pipe(pipe_write)
98
+ except (OSError, ValueError):
99
+ logger.debug("Unable to remove Urwid pipe watch")
100
+ try:
101
+ os.close(pipe_write)
102
+ except OSError:
103
+ logger.debug("Unable to close Urwid pipe")
104
+
105
+
106
+ class PollingBackgroundNotifier(BackgroundNotifier):
107
+ """Notify EDMApp through a repeating timer on Windows.
108
+
109
+ Urwid's watch_pipe feature is unavailable on Windows. Instead, notify()
110
+ records that background work is ready. A timer checks that state every 0.2
111
+ seconds and runs EDMApp's callback on the UI thread when needed.
112
+ """
113
+
114
+ def __init__(self, poll_interval: float = 0.2) -> None:
115
+ if poll_interval <= 0:
116
+ raise ValueError("poll_interval must be positive")
117
+ self.poll_interval = poll_interval
118
+ self._loop: Optional[urwid.MainLoop] = None
119
+ self._callback: Optional[BackgroundTaskReadyCallback] = None
120
+ self._timer_handle: Optional[Any] = None
121
+ self._notification_pending = False
122
+ self._lock = Lock()
123
+
124
+ def start(
125
+ self,
126
+ loop: urwid.MainLoop,
127
+ callback: BackgroundTaskReadyCallback,
128
+ ) -> None:
129
+ """Start a repeating timer that checks for worker notifications."""
130
+ with self._lock:
131
+ self._loop = loop
132
+ self._callback = callback
133
+ self._notification_pending = False
134
+ self._schedule_next_notification_check()
135
+
136
+ def notify(self) -> None:
137
+ """Mark worker results as ready for the next timer check."""
138
+ with self._lock:
139
+ if self._loop is not None:
140
+ self._notification_pending = True
141
+
142
+ def stop(self) -> None:
143
+ """Stop the polling timer and clear the callback."""
144
+ with self._lock:
145
+ loop = self._loop
146
+ timer_handle = self._timer_handle
147
+ self._loop = None
148
+ self._callback = None
149
+ self._timer_handle = None
150
+ self._notification_pending = False
151
+ if loop is not None and timer_handle is not None:
152
+ try:
153
+ loop.remove_alarm(timer_handle)
154
+ except ValueError:
155
+ logger.debug("Unable to remove Urwid polling timer")
156
+
157
+ # Urwid calls timer callbacks with the event loop and optional user data.
158
+ # EDM does not use the loop here, but the callback must accept it to match that
159
+ # callback signature. The leading underscore marks it as intentionally unused.
160
+ def _check_for_task_notifications(
161
+ self,
162
+ _loop: urwid.MainLoop,
163
+ _data: Any = None,
164
+ ) -> None:
165
+ """Run the EDMApp callback when a worker notification is pending."""
166
+ callback: Optional[BackgroundTaskReadyCallback] = None
167
+ with self._lock:
168
+ self._timer_handle = None
169
+ if self._loop is None:
170
+ return
171
+ if self._notification_pending and self._callback is not None:
172
+ self._notification_pending = False
173
+ callback = self._callback
174
+ if callback is not None:
175
+ callback(b"")
176
+ self._schedule_next_notification_check()
177
+
178
+ def _schedule_next_notification_check(self) -> None:
179
+ """Schedule the next timer check while the notifier is running."""
180
+ with self._lock:
181
+ if self._loop is None:
182
+ return
183
+ self._timer_handle = self._loop.set_alarm_in(
184
+ self.poll_interval,
185
+ self._check_for_task_notifications,
186
+ )
187
+
188
+
189
+ def create_background_notifier() -> BackgroundNotifier:
190
+ """Use polling on Windows and a pipe on other platforms."""
191
+ # os.name is "nt" on Windows, where Urwid's watch_pipe is unavailable.
192
+ # A short polling timer provides a Windows-compatible alternative.
193
+ if os.name == "nt":
194
+ return PollingBackgroundNotifier()
195
+
196
+ # Linux and macOS normally report "posix", where a pipe can wake the UI
197
+ # immediately after background work finishes.
198
+ return PipeBackgroundNotifier()
199
+
200
+
201
+ __all__ = [
202
+ "BackgroundNotifier",
203
+ "PipeBackgroundNotifier",
204
+ "PollingBackgroundNotifier",
205
+ "create_background_notifier",
206
+ ]