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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compose-generator
3
- Version: 0.1.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.1.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
-