compose-generator 0.1.0__tar.gz → 0.3.0__tar.gz
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.
- {compose_generator-0.1.0 → compose_generator-0.3.0}/PKG-INFO +1 -7
- {compose_generator-0.1.0 → compose_generator-0.3.0}/README.md +0 -6
- compose_generator-0.3.0/compose_generator/__init__.py +228 -0
- {compose_generator-0.1.0 → compose_generator-0.3.0}/compose_generator/example.py +28 -25
- {compose_generator-0.1.0 → compose_generator-0.3.0}/pyproject.toml +1 -1
- compose_generator-0.1.0/compose_generator/__init__.py +0 -81
- {compose_generator-0.1.0 → compose_generator-0.3.0}/compose_generator/example.env +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compose-generator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Проект представляет собой генератор docker-compose.yml файла на основе настроек приложения
|
|
5
5
|
Author: pasha_danilevich
|
|
6
6
|
Author-email: danilevitch.pasha@yandex.ru
|
|
@@ -37,12 +37,6 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
## Особенности реализации
|
|
39
39
|
|
|
40
|
-
### Шаблон Docker Compose
|
|
41
|
-
Создает сервисы:
|
|
42
|
-
- **api** - веб-сервер с пробросом портов
|
|
43
|
-
- **bg** - фоновый сервис
|
|
44
|
-
- Оба используют общую сеть `common-net`
|
|
45
|
-
|
|
46
40
|
### Автоматизация конфигурации
|
|
47
41
|
- Извлекает все переменные из объектов `BaseSettings`
|
|
48
42
|
- Сохраняет структуру и префиксы конфигурации
|
|
@@ -21,12 +21,6 @@
|
|
|
21
21
|
|
|
22
22
|
## Особенности реализации
|
|
23
23
|
|
|
24
|
-
### Шаблон Docker Compose
|
|
25
|
-
Создает сервисы:
|
|
26
|
-
- **api** - веб-сервер с пробросом портов
|
|
27
|
-
- **bg** - фоновый сервис
|
|
28
|
-
- Оба используют общую сеть `common-net`
|
|
29
|
-
|
|
30
24
|
### Автоматизация конфигурации
|
|
31
25
|
- Извлекает все переменные из объектов `BaseSettings`
|
|
32
26
|
- Сохраняет структуру и префиксы конфигурации
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
ENV_KEY = '[ENV]'
|
|
6
|
+
VERSION_KEY = '[VERSION]'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigurationComponent:
|
|
10
|
+
"""Конфигурационный объект, составляющий настройки приложения."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
item: BaseSettings,
|
|
15
|
+
ignore: Optional[list[str]] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
self.item = item
|
|
18
|
+
self.ignore = ignore or []
|
|
19
|
+
|
|
20
|
+
self.env_prefix = item.model_config.get("env_prefix", "")
|
|
21
|
+
|
|
22
|
+
self.schema: dict[str, Any] = item.__class__.model_json_schema()
|
|
23
|
+
self.properties: dict[str, Any] = self.schema.get("properties", {})
|
|
24
|
+
self.required: set[str] = set(self.schema.get("required", []))
|
|
25
|
+
|
|
26
|
+
self.attrs = list(self.properties.keys())
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def title(self) -> str:
|
|
30
|
+
return f"# {self.item.__class__.__name__}"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def max_attr_length(self) -> int:
|
|
34
|
+
return max(
|
|
35
|
+
(
|
|
36
|
+
len(self.env_prefix + attr)
|
|
37
|
+
for attr in self.attrs
|
|
38
|
+
if self.env_prefix + attr not in self.ignore
|
|
39
|
+
),
|
|
40
|
+
default=0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def compose_parsed(self, max_length: int = 0) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Генерирует переменные для docker-compose.yml.
|
|
46
|
+
Обязательные поля получают проверку на пустое значение.
|
|
47
|
+
Имена переменных выравниваются по общей максимальной длине.
|
|
48
|
+
"""
|
|
49
|
+
result = [self.title]
|
|
50
|
+
|
|
51
|
+
for attr in self.attrs:
|
|
52
|
+
full_attr = self.env_prefix + attr
|
|
53
|
+
|
|
54
|
+
if full_attr in self.ignore:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
name = f"{full_attr}:".ljust(max_length + 1)
|
|
58
|
+
|
|
59
|
+
if attr in self.required:
|
|
60
|
+
value = f"${{{full_attr}:?{full_attr} is required}}"
|
|
61
|
+
else:
|
|
62
|
+
value = f"${{{full_attr}}}"
|
|
63
|
+
|
|
64
|
+
result.append(f"{name} {value}")
|
|
65
|
+
|
|
66
|
+
return "\n".join(result)
|
|
67
|
+
|
|
68
|
+
def get_type_string(self, attr: str) -> str:
|
|
69
|
+
"""Возвращает тип атрибута из JSON Schema."""
|
|
70
|
+
property_schema = self.properties.get(attr, {})
|
|
71
|
+
|
|
72
|
+
if "type" in property_schema:
|
|
73
|
+
return property_schema["type"]
|
|
74
|
+
|
|
75
|
+
any_of = property_schema.get("anyOf")
|
|
76
|
+
|
|
77
|
+
if any_of:
|
|
78
|
+
types = [
|
|
79
|
+
item["type"]
|
|
80
|
+
for item in any_of
|
|
81
|
+
if "type" in item
|
|
82
|
+
]
|
|
83
|
+
return "|".join(types)
|
|
84
|
+
|
|
85
|
+
return "unknown"
|
|
86
|
+
|
|
87
|
+
def env_parsed(self) -> str:
|
|
88
|
+
"""Генерирует переменные для .env.example."""
|
|
89
|
+
result = [self.title]
|
|
90
|
+
|
|
91
|
+
for attr in self.attrs:
|
|
92
|
+
full_attr = self.env_prefix + attr
|
|
93
|
+
|
|
94
|
+
if full_attr in self.ignore:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
line = f"{full_attr}={self.get_type_string(attr)}"
|
|
98
|
+
result.append(line)
|
|
99
|
+
|
|
100
|
+
return "\n".join(result)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DockerCompose:
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
settings: list[BaseSettings],
|
|
107
|
+
template: str,
|
|
108
|
+
version: Optional[str] = None,
|
|
109
|
+
ignore: Optional[list[str]] = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
self.settings = settings
|
|
112
|
+
self.template = template
|
|
113
|
+
self.version = version
|
|
114
|
+
self.ignore = ignore
|
|
115
|
+
|
|
116
|
+
def insert_to_template(self, content: str, key: str) -> None:
|
|
117
|
+
# TODO: делать Trilling Whitespace
|
|
118
|
+
# Разбиваем вставляемую строку на отдельные строки
|
|
119
|
+
|
|
120
|
+
def define_indentation() -> str:
|
|
121
|
+
lines = self.template.split('\n')
|
|
122
|
+
for line in lines:
|
|
123
|
+
if key in line:
|
|
124
|
+
return line.split(key)[0]
|
|
125
|
+
return ''
|
|
126
|
+
|
|
127
|
+
# Форматируем каждую строку с правильным отступом
|
|
128
|
+
formatted = '\n'.join(
|
|
129
|
+
[define_indentation() + line for line in content.split('\n')]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Заменяем {ENV} в шаблоне
|
|
133
|
+
self.template = self.template.replace(key, formatted.strip())
|
|
134
|
+
|
|
135
|
+
def replace_version(self, version: Optional[str] = None) -> None:
|
|
136
|
+
if version is not None:
|
|
137
|
+
self.template = self.template.replace(VERSION_KEY, version)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def max_attr_length(self) -> int:
|
|
141
|
+
return max(
|
|
142
|
+
(
|
|
143
|
+
ConfigurationComponent(c, self.ignore).max_attr_length
|
|
144
|
+
for c in self.settings
|
|
145
|
+
),
|
|
146
|
+
default=0,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def __call__(self, filename: str) -> None:
|
|
150
|
+
max_length = self.max_attr_length
|
|
151
|
+
|
|
152
|
+
parsed_components: list[str] = [
|
|
153
|
+
ConfigurationComponent(c, self.ignore).compose_parsed(max_length)
|
|
154
|
+
for c in self.settings
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
self.insert_to_template(
|
|
158
|
+
content='\n\n'.join(parsed_components),
|
|
159
|
+
key=ENV_KEY,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.replace_version(self.version)
|
|
163
|
+
|
|
164
|
+
with open(filename, 'w') as f:
|
|
165
|
+
f.write(self.template)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ExampleEnv:
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
settings: list[BaseSettings],
|
|
172
|
+
ignore: Optional[list[str]] = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
self.settings = settings
|
|
175
|
+
self.ignore = ignore
|
|
176
|
+
|
|
177
|
+
def __call__(self, filename: str) -> None:
|
|
178
|
+
parsed_components: list[str] = [
|
|
179
|
+
ConfigurationComponent(c, self.ignore).env_parsed() for c in self.settings
|
|
180
|
+
]
|
|
181
|
+
text = '\n\n'.join(parsed_components)
|
|
182
|
+
with open(filename, 'w') as f:
|
|
183
|
+
f.write(text)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Generate:
|
|
187
|
+
def __init__(
|
|
188
|
+
self,
|
|
189
|
+
settings: list[BaseSettings],
|
|
190
|
+
) -> None:
|
|
191
|
+
self.settings = settings
|
|
192
|
+
|
|
193
|
+
def docker_compose(
|
|
194
|
+
self,
|
|
195
|
+
template: str,
|
|
196
|
+
version: Optional[str] = None,
|
|
197
|
+
ignore: Optional[list[str]] = None,
|
|
198
|
+
filename: str = 'docker-compose.yml'
|
|
199
|
+
) -> None:
|
|
200
|
+
DockerCompose(
|
|
201
|
+
settings=self.settings,
|
|
202
|
+
template=template,
|
|
203
|
+
version=version,
|
|
204
|
+
ignore=ignore,
|
|
205
|
+
)(filename)
|
|
206
|
+
|
|
207
|
+
def example_env(self,
|
|
208
|
+
ignore: Optional[list[str]] = None,
|
|
209
|
+
filename: str = '.env.example'
|
|
210
|
+
) -> None:
|
|
211
|
+
ExampleEnv(
|
|
212
|
+
settings=self.settings,
|
|
213
|
+
ignore=ignore,
|
|
214
|
+
)(filename)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Generator:
|
|
218
|
+
"""Генератор docker-compose.yml"""
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self,
|
|
222
|
+
settings: list[BaseSettings],
|
|
223
|
+
) -> None:
|
|
224
|
+
self.settings = settings
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def generate(self) -> Generate:
|
|
228
|
+
return Generate(settings=self.settings)
|
|
@@ -1,28 +1,6 @@
|
|
|
1
1
|
# Example
|
|
2
2
|
from compose_generator import Generator
|
|
3
3
|
|
|
4
|
-
TEMPLATE = """version: "3.9"
|
|
5
|
-
|
|
6
|
-
networks:
|
|
7
|
-
common-net:
|
|
8
|
-
external: true
|
|
9
|
-
|
|
10
|
-
x-serv-common-env: &serv-common-env
|
|
11
|
-
[ENV]
|
|
12
|
-
|
|
13
|
-
services:
|
|
14
|
-
api:
|
|
15
|
-
image: /project/user/super-project/api:[VERSION]
|
|
16
|
-
networks:
|
|
17
|
-
- common-net
|
|
18
|
-
ports:
|
|
19
|
-
- ${APP_SERVER_PORT}:${APP_SERVER_PORT}
|
|
20
|
-
environment:
|
|
21
|
-
<<: *serv-common-env
|
|
22
|
-
restart:
|
|
23
|
-
unless-stopped
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
4
|
from pathlib import Path
|
|
27
5
|
|
|
28
6
|
from pydantic import Field
|
|
@@ -42,7 +20,7 @@ class BaseConnection(FromEnvFile):
|
|
|
42
20
|
HOST: str
|
|
43
21
|
PORT: int
|
|
44
22
|
NAME: str
|
|
45
|
-
|
|
23
|
+
|
|
46
24
|
def get_connection_config(self) -> dict:
|
|
47
25
|
return {
|
|
48
26
|
"engine": "tortoise.backends.asyncpg",
|
|
@@ -91,11 +69,36 @@ class Settings(BaseSettings):
|
|
|
91
69
|
swagger: Swagger = Field(default_factory=Swagger)
|
|
92
70
|
|
|
93
71
|
|
|
72
|
+
TEMPLATE = """version: "3.9"
|
|
73
|
+
|
|
74
|
+
networks:
|
|
75
|
+
common-net:
|
|
76
|
+
external: true
|
|
77
|
+
|
|
78
|
+
x-serv-common-env: &serv-common-env
|
|
79
|
+
[ENV]
|
|
80
|
+
|
|
81
|
+
services:
|
|
82
|
+
api:
|
|
83
|
+
image: /project/user/super-project/api:[VERSION]
|
|
84
|
+
networks:
|
|
85
|
+
- common-net
|
|
86
|
+
ports:
|
|
87
|
+
- ${APP_SERVER_PORT}:${APP_SERVER_PORT}
|
|
88
|
+
environment:
|
|
89
|
+
<<: *serv-common-env
|
|
90
|
+
restart:
|
|
91
|
+
unless-stopped
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
94
|
if __name__ == "__main__":
|
|
95
95
|
stt = Settings()
|
|
96
96
|
generator = Generator(
|
|
97
|
-
settings=stt,
|
|
97
|
+
settings=list(stt.__dict__.values()),
|
|
98
|
+
)
|
|
99
|
+
generator.generate.docker_compose(
|
|
98
100
|
template=TEMPLATE,
|
|
99
101
|
version=stt.app.VERSION,
|
|
102
|
+
ignore=['APP_VERSION'],
|
|
100
103
|
)
|
|
101
|
-
generator.generate()
|
|
104
|
+
generator.generate.example_env()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "compose-generator"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Проект представляет собой генератор docker-compose.yml файла на основе настроек приложения"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "pasha_danilevich",email = "danilevitch.pasha@yandex.ru"}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
from pydantic_settings import BaseSettings
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ConfigurationComponent:
|
|
5
|
-
"""Конфигурационные объекты, составляющие настройки всего приложения"""
|
|
6
|
-
|
|
7
|
-
def __init__(self, item: BaseSettings) -> None:
|
|
8
|
-
self.item = item
|
|
9
|
-
|
|
10
|
-
self.env_prefix = item.model_config.get("env_prefix", "")
|
|
11
|
-
self.title = self.item.__class__.__name__
|
|
12
|
-
self.attrs = list(item.model_dump().keys())
|
|
13
|
-
|
|
14
|
-
def parsed(self) -> str:
|
|
15
|
-
"""
|
|
16
|
-
Парсит объект в строку для записи в docker-compose.yml
|
|
17
|
-
|
|
18
|
-
Пример:
|
|
19
|
-
# RabbitMQ
|
|
20
|
-
RABBITMQ_HOST: ${RABBITMQ_HOST}
|
|
21
|
-
RABBITMQ_USER: ${RABBITMQ_USER}
|
|
22
|
-
RABBITMQ_PASS: ${RABBITMQ_PASS}
|
|
23
|
-
"""
|
|
24
|
-
title = f'# {self.title}' if self.title else " "
|
|
25
|
-
result = [title]
|
|
26
|
-
|
|
27
|
-
for attr in self.attrs:
|
|
28
|
-
full_attr = self.env_prefix + attr
|
|
29
|
-
line = f"{full_attr}: " + f"${{{full_attr}}}"
|
|
30
|
-
result.append(line)
|
|
31
|
-
|
|
32
|
-
return "\n".join(result)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Generator:
|
|
36
|
-
"""Генератор docker-compose.yml"""
|
|
37
|
-
|
|
38
|
-
ENV_KEY = '[ENV]'
|
|
39
|
-
VERSION_KEY = '[VERSION]'
|
|
40
|
-
FILE_NAME = 'docker-compose.yml'
|
|
41
|
-
|
|
42
|
-
def __init__(self, settings: BaseSettings, template: str, version: str) -> None:
|
|
43
|
-
self.settings = settings
|
|
44
|
-
self.template = template
|
|
45
|
-
self.version = version
|
|
46
|
-
|
|
47
|
-
def insert_to_template(self, content: str, key: str) -> None:
|
|
48
|
-
# Разбиваем вставляемую строку на отдельные строки
|
|
49
|
-
|
|
50
|
-
def define_indentation() -> str:
|
|
51
|
-
lines = self.template.split('\n')
|
|
52
|
-
for line in lines:
|
|
53
|
-
if key in line:
|
|
54
|
-
return line.split(key)[0]
|
|
55
|
-
return ''
|
|
56
|
-
|
|
57
|
-
# Форматируем каждую строку с правильным отступом
|
|
58
|
-
formatted = '\n'.join(
|
|
59
|
-
[define_indentation() + line for line in content.split('\n')]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# Заменяем {ENV} в шаблоне
|
|
63
|
-
self.template = self.template.replace(key, formatted.strip())
|
|
64
|
-
|
|
65
|
-
def replace_version(self, version: str) -> None:
|
|
66
|
-
self.template = self.template.replace(self.VERSION_KEY, version)
|
|
67
|
-
|
|
68
|
-
def generate(self) -> None:
|
|
69
|
-
components = list(self.settings.__dict__.values())
|
|
70
|
-
parsed_components: list[str] = [
|
|
71
|
-
ConfigurationComponent(c).parsed() for c in components
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
self.insert_to_template(
|
|
75
|
-
content='\n\n'.join(parsed_components), key=self.ENV_KEY
|
|
76
|
-
)
|
|
77
|
-
self.replace_version(self.version)
|
|
78
|
-
|
|
79
|
-
with open(self.FILE_NAME, 'w') as f:
|
|
80
|
-
f.write(self.template)
|
|
81
|
-
|
|
File without changes
|