service-dev-env 0.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.
- service_dev_env/__init__.py +0 -0
- service_dev_env/bash_scripts/bash_container.bash +18 -0
- service_dev_env/bash_scripts/docker_log.bash +36 -0
- service_dev_env/build/lib/docker_dev_env/ui.tcss +10 -0
- service_dev_env/cli.py +111 -0
- service_dev_env/docker_utils.py +247 -0
- service_dev_env/fun.py +18 -0
- service_dev_env/layout.py +33 -0
- service_dev_env/model/__init__.py +0 -0
- service_dev_env/model/env.py +42 -0
- service_dev_env/model/layout.py +21 -0
- service_dev_env/model/project.py +23 -0
- service_dev_env/model/publish.py +11 -0
- service_dev_env/model/service.py +23 -0
- service_dev_env/model/terminal.py +12 -0
- service_dev_env/model/window.py +17 -0
- service_dev_env/project.py +78 -0
- service_dev_env/runner.py +54 -0
- service_dev_env/screens/log_service.py +105 -0
- service_dev_env/screens/log_service.tcss +2 -0
- service_dev_env/terminals/__init__.py +7 -0
- service_dev_env/terminals/base.py +112 -0
- service_dev_env/terminals/terminator.py +15 -0
- service_dev_env/terminals/tilix.py +181 -0
- service_dev_env/ui.py +130 -0
- service_dev_env/ui.tcss +9 -0
- service_dev_env-0.5.0.dist-info/METADATA +41 -0
- service_dev_env-0.5.0.dist-info/RECORD +32 -0
- service_dev_env-0.5.0.dist-info/WHEEL +5 -0
- service_dev_env-0.5.0.dist-info/entry_points.txt +2 -0
- service_dev_env-0.5.0.dist-info/licenses/LICENSE +11 -0
- service_dev_env-0.5.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
SCRIPT_PATH="$(dirname "$0")"
|
|
3
|
+
BASE_PATH=$(realpath "$SCRIPT_PATH/..")
|
|
4
|
+
DOCKER_PATH=$(realpath "$1")
|
|
5
|
+
SERVICE="$2"
|
|
6
|
+
|
|
7
|
+
if [ $# -eq 2 ]; then
|
|
8
|
+
ENV_FILE=$(realpath $BASE_PATH/local_vars.env)
|
|
9
|
+
else
|
|
10
|
+
ENV_FILE=$3
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
cd "$DOCKER_PATH" || exit 1
|
|
14
|
+
|
|
15
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE run --rm "$SERVICE" bash
|
|
16
|
+
|
|
17
|
+
echo "Press CTRL+C to close..."
|
|
18
|
+
sleep infinity
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
SCRIPT_PATH="$(dirname "$0")"
|
|
3
|
+
BASE_PATH=$(realpath "$SCRIPT_PATH/..")
|
|
4
|
+
DOCKER_PATH=$(realpath "$1")
|
|
5
|
+
SERVICE="$2"
|
|
6
|
+
|
|
7
|
+
if [ $# -eq 2 ]; then
|
|
8
|
+
ENV_FILE=$(realpath $BASE_PATH/local_vars.env)
|
|
9
|
+
else
|
|
10
|
+
ENV_FILE=$3
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
emoji[0]="😖"
|
|
14
|
+
emoji[1]="😭"
|
|
15
|
+
emoji[2]="😫"
|
|
16
|
+
emoji[3]="😞"
|
|
17
|
+
emoji[4]="😓"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
cd $DOCKER_PATH || exit 1
|
|
21
|
+
|
|
22
|
+
while :
|
|
23
|
+
do
|
|
24
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE logs -f --tail=50 $SERVICE
|
|
25
|
+
|
|
26
|
+
size=${#emoji[@]}
|
|
27
|
+
index=$(($RANDOM % $size))
|
|
28
|
+
echo "${emoji[$index]} $SERVICE log ended, press <CTRL+C> to close..."
|
|
29
|
+
sleep 5
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ENV_FILE=$ENV_FILE docker compose --env-file $ENV_FILE logs -f $SERVICE
|
|
34
|
+
|
|
35
|
+
echo "Press CTRL+C to close..."
|
|
36
|
+
sleep infinity
|
service_dev_env/cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from .docker_utils import DockerUtils
|
|
4
|
+
from .fun import goodbye
|
|
5
|
+
from .project import Project
|
|
6
|
+
from .terminals import TERMINALS
|
|
7
|
+
from .terminals.tilix import Tilix # default terminal
|
|
8
|
+
from .ui import DockerDevEnvUi
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _setup_argparse() -> argparse.ArgumentParser:
|
|
12
|
+
"""Configure argument parser."""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description=__doc__,
|
|
15
|
+
formatter_class=argparse.RawTextHelpFormatter)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
'-c', '--cleanup', action='store_true',
|
|
18
|
+
help='just cleanup, do not start')
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
'-n', '--no-cleanup', action='store_true',
|
|
21
|
+
help='no cleanup after finish')
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
'-p', '--project', default='project-config.yml',
|
|
24
|
+
help='project yaml file')
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
'-s', '--stopped', action='store_true',
|
|
27
|
+
help='do not create container, start with all container stopped')
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
'-l', '--log',
|
|
30
|
+
help='only show log of <service name>')
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'--cmd', action='store_true',
|
|
33
|
+
help='only show commands to run the services for easy copy-and-paste,'
|
|
34
|
+
'do not start.')
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
'-e', '--env',
|
|
37
|
+
help='name of environment (defaults to default_env from your config)')
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
'-t', '--terminal', default='tilix',
|
|
40
|
+
help='name of the terminal (defaults to tilix)')
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'-r', '--rebuild', action='store_true',
|
|
43
|
+
help='rebuild container before start')
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
'-ro', '--rebuild-only', action='store_true',
|
|
46
|
+
help='rebuild container only, do not start')
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
'-b', '--bash', default=None,
|
|
49
|
+
help='run bash inside the container')
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
'-d', '--deploy', action='store_true',
|
|
52
|
+
help='build docker container for deploy environment and '
|
|
53
|
+
'run deploy script')
|
|
54
|
+
# we show "-i"/init for --help but as its already handled
|
|
55
|
+
# by the ./run bash script we ignore it.
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
'-i', '--init', action='store_true',
|
|
58
|
+
help='(re)init: deletes the local dev and recreates it.')
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
parser = _setup_argparse()
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
project = Project(args.project)
|
|
66
|
+
if args.env:
|
|
67
|
+
project.select_env(args.env)
|
|
68
|
+
elif args.deploy:
|
|
69
|
+
if not project.select_deploy_env():
|
|
70
|
+
return
|
|
71
|
+
terminal = args.terminal
|
|
72
|
+
_terminal = TERMINALS[terminal] if terminal in TERMINALS else Tilix
|
|
73
|
+
_docker_utils = DockerUtils(project)
|
|
74
|
+
start = True
|
|
75
|
+
if args.cleanup:
|
|
76
|
+
_docker_utils.cleanup()
|
|
77
|
+
start = False
|
|
78
|
+
if args.rebuild or args.rebuild_only or args.deploy:
|
|
79
|
+
_docker_utils.build_all()
|
|
80
|
+
if args.rebuild_only:
|
|
81
|
+
start = False
|
|
82
|
+
if args.cmd:
|
|
83
|
+
_docker_utils.print_cmds()
|
|
84
|
+
start = False
|
|
85
|
+
if args.log:
|
|
86
|
+
_docker_utils.log(args.log)
|
|
87
|
+
# logs run forever, we don't want to continue
|
|
88
|
+
return
|
|
89
|
+
if args.bash:
|
|
90
|
+
_docker_utils.bash(args.bash)
|
|
91
|
+
# bash runs forever, we don't want to continue
|
|
92
|
+
return
|
|
93
|
+
if args.deploy:
|
|
94
|
+
project.deploy()
|
|
95
|
+
return
|
|
96
|
+
if not start:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if not args.stopped:
|
|
100
|
+
_docker_utils.autostart()
|
|
101
|
+
app = DockerDevEnvUi(_docker_utils, _terminal(project))
|
|
102
|
+
app.run()
|
|
103
|
+
|
|
104
|
+
# Cleanup at the end
|
|
105
|
+
if not args.no_cleanup:
|
|
106
|
+
_docker_utils.cleanup()
|
|
107
|
+
goodbye()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == '__main__':
|
|
111
|
+
main()
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
import docker
|
|
8
|
+
|
|
9
|
+
from .model.project import ProjectConfig
|
|
10
|
+
from .model.service import ServiceConfig
|
|
11
|
+
from .runner import Runner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
BASE_PATH = Path(__file__).absolute().parent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DockerUtils(Runner):
|
|
18
|
+
"""Wrapper around Popen and docker to manage docker container."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project: ProjectConfig):
|
|
21
|
+
super().__init__(project)
|
|
22
|
+
self.docker_client = docker.from_env()
|
|
23
|
+
|
|
24
|
+
def autostart(self):
|
|
25
|
+
if self.env.networks:
|
|
26
|
+
self.create_network(self.env.networks, )
|
|
27
|
+
for service in self.services:
|
|
28
|
+
if service.autostart:
|
|
29
|
+
self.compose_start(service)
|
|
30
|
+
|
|
31
|
+
def compose_cmd(self, cmd: List):
|
|
32
|
+
docker_cmd = ['docker', 'compose']
|
|
33
|
+
if self.env.docker_compose_path:
|
|
34
|
+
_path = self.env.docker_compose_path
|
|
35
|
+
if not Path(_path).exists():
|
|
36
|
+
raise RuntimeError(f'ERROR: {_path} does not exist.')
|
|
37
|
+
docker_cmd += ['-f', _path]
|
|
38
|
+
if self.env.env_file:
|
|
39
|
+
_path = self.env.env_path()
|
|
40
|
+
if not Path(_path).exists():
|
|
41
|
+
raise RuntimeError(f'ERROR: {_path} does not exist.')
|
|
42
|
+
docker_cmd += ['--env-file', _path]
|
|
43
|
+
if isinstance(cmd, str):
|
|
44
|
+
docker_cmd.append(cmd)
|
|
45
|
+
else:
|
|
46
|
+
docker_cmd += cmd
|
|
47
|
+
return docker_cmd
|
|
48
|
+
|
|
49
|
+
def compose_bash_cmd(self, service):
|
|
50
|
+
return self.compose_cmd(['run', '--rm', '-it', service.name, 'bash'])
|
|
51
|
+
|
|
52
|
+
def compose_logs_cmd(self, service):
|
|
53
|
+
return self.compose_cmd(['logs', '-f', '--tail=50', service.name])
|
|
54
|
+
|
|
55
|
+
def compose_start_cmd(self, service: ServiceConfig):
|
|
56
|
+
return self.compose_cmd(['up', '-d', service.name])
|
|
57
|
+
|
|
58
|
+
def _run_cmd(
|
|
59
|
+
self, cmd: List, service: ServiceConfig = None,
|
|
60
|
+
check: bool = False, env: Dict = None):
|
|
61
|
+
args = {'env': os.environ.copy()}
|
|
62
|
+
if env:
|
|
63
|
+
args['env'].update(env)
|
|
64
|
+
elif env is None:
|
|
65
|
+
args['env'].update(self._get_env())
|
|
66
|
+
if service and service.folder:
|
|
67
|
+
args['cwd'] = service.folder
|
|
68
|
+
args['check'] = check
|
|
69
|
+
try:
|
|
70
|
+
subprocess.run(cmd, **args)
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def compose_start(self, service: ServiceConfig):
|
|
75
|
+
"""Start docker compose command."""
|
|
76
|
+
if hasattr(service, 'description') and service.description:
|
|
77
|
+
print(f'🚀 Starting {service.description}...')
|
|
78
|
+
cmd = self.compose_start_cmd(service)
|
|
79
|
+
self._run_cmd(cmd, service)
|
|
80
|
+
|
|
81
|
+
def bash(self, service_name: str):
|
|
82
|
+
for service in self.services:
|
|
83
|
+
if service.name == service_name:
|
|
84
|
+
cmd = self.compose_bash_cmd(service)
|
|
85
|
+
self._run_cmd(cmd, service)
|
|
86
|
+
return
|
|
87
|
+
print(f'Error: unknown service {service_name}')
|
|
88
|
+
|
|
89
|
+
def log(self, service_name: str):
|
|
90
|
+
"""Print out the log of the given service name."""
|
|
91
|
+
for service in self.services:
|
|
92
|
+
if service.name == service_name:
|
|
93
|
+
cmd = self.compose_logs_cmd(service)
|
|
94
|
+
self._run_cmd(cmd, service)
|
|
95
|
+
return
|
|
96
|
+
print(f'Error: unknown service {service_name}')
|
|
97
|
+
|
|
98
|
+
def print_cmds(self):
|
|
99
|
+
for env_name, env in self.project.env.items():
|
|
100
|
+
print(f'== {env_name} ==')
|
|
101
|
+
for service in env.services:
|
|
102
|
+
print(f'==== {service.name} ====')
|
|
103
|
+
print(' '.join(self.compose_build_cmd(service)))
|
|
104
|
+
print(' '.join(self.compose_start_cmd(service)))
|
|
105
|
+
print(' '.join(self.compose_logs_cmd(service)))
|
|
106
|
+
print(' '.join(self.compose_bash_cmd(service)))
|
|
107
|
+
|
|
108
|
+
def compose_kill_cmd(self, service: ServiceConfig):
|
|
109
|
+
"""Kill container using compose (for faster down)."""
|
|
110
|
+
return self.compose_cmd(['kill', service.name])
|
|
111
|
+
|
|
112
|
+
def compose_kill(self, service: ServiceConfig):
|
|
113
|
+
"""Kill container using compose (for faster down)."""
|
|
114
|
+
title = service.name
|
|
115
|
+
if hasattr(service, 'description') and service.description:
|
|
116
|
+
title = service.description
|
|
117
|
+
print(f'➜ Kill container {title}')
|
|
118
|
+
cmd = self.compose_kill_cmd(service)
|
|
119
|
+
self._run_cmd(cmd, service)
|
|
120
|
+
|
|
121
|
+
def compose_build_cmd(self, service: ServiceConfig):
|
|
122
|
+
"""Build image by service name."""
|
|
123
|
+
return self.compose_cmd(['build', service.name])
|
|
124
|
+
|
|
125
|
+
def compose_build(self, service: ServiceConfig):
|
|
126
|
+
"""Build image by service name."""
|
|
127
|
+
cmd = self.compose_build_cmd(service)
|
|
128
|
+
self._run_cmd(cmd, service)
|
|
129
|
+
|
|
130
|
+
def compose_down(self):
|
|
131
|
+
"""Run docker compose down to clean up."""
|
|
132
|
+
cmd = self.compose_cmd(['down'])
|
|
133
|
+
self._run_cmd(cmd, check=False)
|
|
134
|
+
|
|
135
|
+
def create_network(self, networks):
|
|
136
|
+
if isinstance(networks, str):
|
|
137
|
+
networks = [networks]
|
|
138
|
+
for network in networks:
|
|
139
|
+
print(f'🌐 Create docker network {network}')
|
|
140
|
+
docker_cmd = ['docker', 'network', 'create', network]
|
|
141
|
+
subprocess.run(docker_cmd, check=False)
|
|
142
|
+
|
|
143
|
+
def rm_network(self, networks):
|
|
144
|
+
if isinstance(networks, str):
|
|
145
|
+
networks = [networks]
|
|
146
|
+
for network in networks:
|
|
147
|
+
print(f'➜ Remove network {network}')
|
|
148
|
+
docker_cmd = ['docker', 'network', 'rm', network]
|
|
149
|
+
subprocess.run(docker_cmd, check=False)
|
|
150
|
+
|
|
151
|
+
def _not_available(self, service: ServiceConfig):
|
|
152
|
+
return {
|
|
153
|
+
'service': service.title,
|
|
154
|
+
'container_id': 'N/A',
|
|
155
|
+
'status': 'not running',
|
|
156
|
+
'health': 'unknown',
|
|
157
|
+
'image': 'N/A',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _get_health_status(self, container) -> str:
|
|
161
|
+
"""Get health check status from container."""
|
|
162
|
+
container.reload()
|
|
163
|
+
state = container.attrs.get('State', {})
|
|
164
|
+
|
|
165
|
+
if state.get('Health'):
|
|
166
|
+
health = state['Health'].get('Status', 'unknown')
|
|
167
|
+
if health == 'healthy':
|
|
168
|
+
color = 'green'
|
|
169
|
+
elif health == 'unhealthy':
|
|
170
|
+
color = 'red'
|
|
171
|
+
else:
|
|
172
|
+
color = 'yellow'
|
|
173
|
+
return f'[{color}]{health}[/]'
|
|
174
|
+
return 'no check'
|
|
175
|
+
|
|
176
|
+
def service_status(self, service: ServiceConfig):
|
|
177
|
+
containers = self.docker_client.containers.list(
|
|
178
|
+
all=True,
|
|
179
|
+
filters={'label': f'com.docker.compose.service={service.name}'}
|
|
180
|
+
)
|
|
181
|
+
if not containers:
|
|
182
|
+
return self._not_available(service)
|
|
183
|
+
container = containers[0]
|
|
184
|
+
health_status = self._get_health_status(container)
|
|
185
|
+
tags = container.image.tags
|
|
186
|
+
return {
|
|
187
|
+
'service': service.title,
|
|
188
|
+
'container_id': container.short_id,
|
|
189
|
+
'status': container.status,
|
|
190
|
+
'health': health_status,
|
|
191
|
+
'image': tags[0] if tags else 'unknown',
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
def service_running(self, service: ServiceConfig):
|
|
195
|
+
containers = self.docker_client.containers.list(
|
|
196
|
+
all=True,
|
|
197
|
+
filters={'label': f'com.docker.compose.service={service.name}'}
|
|
198
|
+
)
|
|
199
|
+
if not containers:
|
|
200
|
+
return False
|
|
201
|
+
container = containers[0]
|
|
202
|
+
return container.status == 'running'
|
|
203
|
+
|
|
204
|
+
def health_row(self, service: ServiceConfig):
|
|
205
|
+
service_status = self.service_status(service)
|
|
206
|
+
return [
|
|
207
|
+
service_status['service'],
|
|
208
|
+
service_status['container_id'],
|
|
209
|
+
service_status['status'],
|
|
210
|
+
service_status['health'],
|
|
211
|
+
service_status['image'],
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
def rebuild(self, app, service: ServiceConfig):
|
|
215
|
+
async def _rebuild_done():
|
|
216
|
+
app.notify(f'Rebuild for {service.name} done.')
|
|
217
|
+
if service.autostart:
|
|
218
|
+
self.run_background(
|
|
219
|
+
service.folder, [self.compose_start_cmd(service)], app)
|
|
220
|
+
cmds = [
|
|
221
|
+
self.compose_kill_cmd(service),
|
|
222
|
+
self.compose_build_cmd(service)]
|
|
223
|
+
self.run_background(service.folder, cmds, app, _rebuild_done)
|
|
224
|
+
|
|
225
|
+
def restart(self, app, service: ServiceConfig):
|
|
226
|
+
cmds = []
|
|
227
|
+
if self.service_running(service):
|
|
228
|
+
cmds.append(self.compose_kill_cmd(service))
|
|
229
|
+
cmds.append(self.compose_start_cmd(service))
|
|
230
|
+
self.run_background(service.folder, cmds, app)
|
|
231
|
+
|
|
232
|
+
def kill(self, app, service: ServiceConfig):
|
|
233
|
+
self.run_background(
|
|
234
|
+
service.folder, [self.compose_kill_cmd(service)], app)
|
|
235
|
+
|
|
236
|
+
def build_all(self):
|
|
237
|
+
for service in self.services:
|
|
238
|
+
if service.rebuild:
|
|
239
|
+
self.compose_build(service)
|
|
240
|
+
|
|
241
|
+
def cleanup(self):
|
|
242
|
+
for service in self.services:
|
|
243
|
+
self.compose_kill(service)
|
|
244
|
+
self.compose_down()
|
|
245
|
+
if self.env.networks and self.env.cleanup_networks:
|
|
246
|
+
self.rm_network(self.env.networks)
|
|
247
|
+
print(' 🧽 Cleanup complete.')
|
service_dev_env/fun.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
FAREWELL = [
|
|
4
|
+
'How lucky I am to have something that makes saying goodbye so hard.',
|
|
5
|
+
"Don't cry because it's over, smile because it happened.",
|
|
6
|
+
'The pain of parting is nothing to the joy of meeting again.',
|
|
7
|
+
"Man's feelings are always purest and most glowing in the "
|
|
8
|
+
'hour of meeting and farewell.',
|
|
9
|
+
'So long, and thanks for all the fish.',
|
|
10
|
+
'We only part to meet again.',
|
|
11
|
+
'Every goodbye makes the next hello closer.',
|
|
12
|
+
'Let there be spaces in your togetherness.',
|
|
13
|
+
'My battery is low, and its getting dark.'
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def goodbye():
|
|
18
|
+
print(f'\n 👋 Goodbye: {random.choice(FAREWELL)} ')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LayoutCalculator:
|
|
5
|
+
"""Calculates optimal layout dimensions."""
|
|
6
|
+
|
|
7
|
+
# Strategy pattern: custom layout functions
|
|
8
|
+
_custom_strategies: dict[int, tuple[int, int]] = {}
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def register_strategy(cls, num_windows: int, rows: int, cols: int) -> None:
|
|
12
|
+
"""Register a custom layout strategy."""
|
|
13
|
+
cls._custom_strategies[num_windows] = (rows, cols)
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def calculate(
|
|
17
|
+
cls,
|
|
18
|
+
num_windows: int,
|
|
19
|
+
prefer_horizontal: bool = False,
|
|
20
|
+
) -> tuple[int, int]:
|
|
21
|
+
"""Calculate optimal rows and columns."""
|
|
22
|
+
# Check custom strategies first
|
|
23
|
+
if num_windows in cls._custom_strategies:
|
|
24
|
+
return cls._custom_strategies[num_windows]
|
|
25
|
+
|
|
26
|
+
if 12 > num_windows > 17:
|
|
27
|
+
# this would end up in a 3-5 window, we prefer 4x4 instead
|
|
28
|
+
return 4, 4
|
|
29
|
+
else:
|
|
30
|
+
# heuristic to calculate square-ish terminal alignment
|
|
31
|
+
cols = int(math.sqrt(num_windows))
|
|
32
|
+
rows = (num_windows + cols - 1) // cols
|
|
33
|
+
return (cols, rows) if prefer_horizontal else (rows, cols)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .publish import PublishConfig
|
|
5
|
+
from .service import ServiceConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class EnvConfig:
|
|
10
|
+
"""Defined environment with multiple services and its own compose file."""
|
|
11
|
+
|
|
12
|
+
services: list[ServiceConfig]
|
|
13
|
+
docker_compose_path: str | None = None
|
|
14
|
+
update_interval: int = 1
|
|
15
|
+
log_tail: int = 100
|
|
16
|
+
build_timeout: float = 3600
|
|
17
|
+
env_file: str | None = None
|
|
18
|
+
networks: list | None = None
|
|
19
|
+
cleanup_networks: bool | None = True
|
|
20
|
+
publish: PublishConfig | None = None
|
|
21
|
+
deploy_script: str | None = None
|
|
22
|
+
folder: str = '.'
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
services = []
|
|
26
|
+
if self.services:
|
|
27
|
+
for _service in self.services:
|
|
28
|
+
if isinstance(_service, ServiceConfig):
|
|
29
|
+
services.append(_service)
|
|
30
|
+
else:
|
|
31
|
+
services.append(ServiceConfig(**_service))
|
|
32
|
+
self.services = services
|
|
33
|
+
if self.publish:
|
|
34
|
+
if not isinstance(self.publish, PublishConfig):
|
|
35
|
+
self.publish = PublishConfig(**self.publish)
|
|
36
|
+
|
|
37
|
+
def env_path(self):
|
|
38
|
+
_env = self.env_file or ''
|
|
39
|
+
if _env.startswith('/'):
|
|
40
|
+
return _env
|
|
41
|
+
path = Path('.').absolute()
|
|
42
|
+
return str(path / _env)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Layout mode and configuration models."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LayoutMode(Enum):
|
|
7
|
+
"""Provide layout configuration modes."""
|
|
8
|
+
|
|
9
|
+
AUTO = 'auto' # Smart layout based on window count
|
|
10
|
+
GRID = 'grid' # Manual rows x columns
|
|
11
|
+
CUSTOM = 'custom' # Manual window specification
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LayoutConfig:
|
|
16
|
+
"""Layout configuration."""
|
|
17
|
+
|
|
18
|
+
mode: LayoutMode
|
|
19
|
+
rows: int | None = None
|
|
20
|
+
cols: int | None = None
|
|
21
|
+
prefer_horizontal: bool = False # Favor vertical alginment if True
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Project definition."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from .terminal import TerminalConfig
|
|
5
|
+
from .env import EnvConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ProjectConfig:
|
|
10
|
+
name: str = 'new project'
|
|
11
|
+
default_env: str = 'dev'
|
|
12
|
+
deploy_env: str = 'deploy'
|
|
13
|
+
env: dict[str, EnvConfig] = field(default_factory=dict)
|
|
14
|
+
terminal: TerminalConfig = field(default_factory=TerminalConfig)
|
|
15
|
+
|
|
16
|
+
def __post_init__(self):
|
|
17
|
+
_envs = {}
|
|
18
|
+
for env_name, env_data in self.env.items():
|
|
19
|
+
if isinstance(env_data, EnvConfig):
|
|
20
|
+
_envs[env_name] = env_data
|
|
21
|
+
else:
|
|
22
|
+
_envs[env_name] = EnvConfig(**env_data)
|
|
23
|
+
self.env = _envs
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class ServiceConfig:
|
|
6
|
+
# unique name, has to match docker service name in compose file
|
|
7
|
+
name: str
|
|
8
|
+
# (optional) description of the service
|
|
9
|
+
description: str | None = None
|
|
10
|
+
rebuild: bool = True # set to false if you have just a base image
|
|
11
|
+
# do not start the service when calling service-dev-env
|
|
12
|
+
autostart: bool = True
|
|
13
|
+
folder: str | None = None
|
|
14
|
+
open_url: str | None = None
|
|
15
|
+
|
|
16
|
+
# allow terminal windows for log/bash
|
|
17
|
+
terminal_log: bool = True
|
|
18
|
+
terminal_bash: bool = True
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def title(self):
|
|
22
|
+
"""Return the title we show to the user."""
|
|
23
|
+
return self.description if self.description else self.name
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Terminal configuration models."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from .layout import LayoutMode, LayoutConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class TerminalConfig:
|
|
8
|
+
"""Base terminal configuration."""
|
|
9
|
+
|
|
10
|
+
terminal_type: str = 'tilix'
|
|
11
|
+
layout_config: LayoutConfig = field(
|
|
12
|
+
default_factory=lambda: LayoutConfig(mode=LayoutMode.AUTO))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Window specification models."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from .service import ServiceConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class WindowConfig:
|
|
8
|
+
"""Specification for a single terminal window."""
|
|
9
|
+
|
|
10
|
+
service_name: str
|
|
11
|
+
width_fraction: float = 1.0
|
|
12
|
+
height_fraction: float = 1.0
|
|
13
|
+
x_position: int = 0
|
|
14
|
+
y_position: int = 0
|
|
15
|
+
profile: str | None = None
|
|
16
|
+
extra_config: dict = field(default_factory=dict)
|
|
17
|
+
service: ServiceConfig | None = None
|