fasttests 0.1.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.
- fasttests/__init__.py +3 -0
- fasttests/main.py +1027 -0
- fasttests-0.1.0.dist-info/METADATA +8 -0
- fasttests-0.1.0.dist-info/RECORD +6 -0
- fasttests-0.1.0.dist-info/WHEEL +5 -0
- fasttests-0.1.0.dist-info/top_level.txt +1 -0
fasttests/__init__.py
ADDED
fasttests/main.py
ADDED
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import pkgutil
|
|
4
|
+
from io import TextIOWrapper
|
|
5
|
+
import ssl
|
|
6
|
+
import json
|
|
7
|
+
import urllib.request
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal, Callable, ClassVar, TypedDict
|
|
10
|
+
from collections import Counter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigEnv(TypedDict):
|
|
14
|
+
dev__user__admin__login: str
|
|
15
|
+
dev__user__admin__password: str
|
|
16
|
+
dev__user__regmn__login: str
|
|
17
|
+
dev__user__regmn__password: str
|
|
18
|
+
dev__app__domain: str
|
|
19
|
+
dev__app__isso_url: str
|
|
20
|
+
dev__app__client_id: str
|
|
21
|
+
dev__app__client_secret: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigPath(TypedDict):
|
|
25
|
+
path: str
|
|
26
|
+
autotest: str
|
|
27
|
+
tests: str
|
|
28
|
+
params: str
|
|
29
|
+
scr: str
|
|
30
|
+
scr_swagger: str
|
|
31
|
+
scr_swagger_api: str
|
|
32
|
+
scr_swagger_schemas: str
|
|
33
|
+
scr_browser: str
|
|
34
|
+
scr_browser_pages: str
|
|
35
|
+
scr_browser_components: str
|
|
36
|
+
params_api: str
|
|
37
|
+
tests_test_api: str
|
|
38
|
+
tests_test_ui: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Page(TypedDict):
|
|
42
|
+
name: str
|
|
43
|
+
url: str
|
|
44
|
+
components: list[str]
|
|
45
|
+
dependencies: list[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Config:
|
|
50
|
+
UI: bool = None
|
|
51
|
+
API: ClassVar[str] = ""
|
|
52
|
+
PATHS: ClassVar[ConfigPath] = {
|
|
53
|
+
"path": f'{Path(__file__).parent}/'.replace("\\", "/"),
|
|
54
|
+
"autotest": "autotest/",
|
|
55
|
+
"swagger": "autotest/swagger/",
|
|
56
|
+
"swagger_endpoints": "autotest/swagger/endpoints/",
|
|
57
|
+
"swagger_schemas": "autotest/swagger/schemas/",
|
|
58
|
+
"browser": "autotest/browser/",
|
|
59
|
+
"browser_pages": "autotest/browser/pages/",
|
|
60
|
+
"browser_components": "autotest/browser/components/",
|
|
61
|
+
"params": "autotest/params/",
|
|
62
|
+
"params_api": "autotest/params/api/",
|
|
63
|
+
"params_ui": "autotest/params/ui/",
|
|
64
|
+
"tests": "autotest/tests/",
|
|
65
|
+
"tests_test_api": "autotest/tests/test_api/",
|
|
66
|
+
"tests_test_ui": "autotest/tests/test_ui/"
|
|
67
|
+
}
|
|
68
|
+
ENV: ClassVar[ConfigEnv] = {
|
|
69
|
+
"dev__user__admin__login": "",
|
|
70
|
+
"dev__user__admin__password": "",
|
|
71
|
+
"dev__user__regmn__login": "",
|
|
72
|
+
"dev__user__regmn__password": "",
|
|
73
|
+
"dev__app__domain": "",
|
|
74
|
+
"dev__app__isso_url": "",
|
|
75
|
+
"dev__app__client_id": "",
|
|
76
|
+
"dev__app__client_secret": ""
|
|
77
|
+
}
|
|
78
|
+
PAGES: ClassVar[list[Page]] = []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class Parameter:
|
|
83
|
+
param: Literal["path", "query"] = None
|
|
84
|
+
alias: str = None
|
|
85
|
+
type: str = None
|
|
86
|
+
|
|
87
|
+
def __post_init__(self):
|
|
88
|
+
self.param = self.set_param(self.param)
|
|
89
|
+
self.alias = self.set_alias(self.alias)
|
|
90
|
+
self.name = self.set_name(self.alias)
|
|
91
|
+
self.type = self.set_type(self.type)
|
|
92
|
+
|
|
93
|
+
def set_param(self, attr: dict):
|
|
94
|
+
return attr["in"]
|
|
95
|
+
|
|
96
|
+
def set_alias(self, attr: dict):
|
|
97
|
+
return attr["name"]
|
|
98
|
+
|
|
99
|
+
def set_name(self, attr: str):
|
|
100
|
+
return attr.replace(".", "_")
|
|
101
|
+
|
|
102
|
+
def set_type(self, attr: dict):
|
|
103
|
+
return Text.to_python_type(attr["schema"]["type"])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Props:
|
|
108
|
+
name: str = None
|
|
109
|
+
type: str = None
|
|
110
|
+
schema: str = None
|
|
111
|
+
|
|
112
|
+
def __post_init__(self):
|
|
113
|
+
self.schema = self.set_schema(self.schema)
|
|
114
|
+
self.type = self.set_type(self.type)
|
|
115
|
+
|
|
116
|
+
def set_schema(self, attr: dict):
|
|
117
|
+
if (schema := Text.find(attr, '$ref')) != None:
|
|
118
|
+
return schema.split('/')[-1]
|
|
119
|
+
|
|
120
|
+
def set_type(self, attr: dict):
|
|
121
|
+
return ' | '.join(
|
|
122
|
+
[Text.to_python_type(_.get("type")) if _.get("type") != None and _.get("items") == None else
|
|
123
|
+
_.get("$ref").split('/')[-1] if _.get("type") == None and _.get("$ref") != None else
|
|
124
|
+
f'{Text.to_python_type(_.get("type"))}[{Text.to_python_type(_.get("items").get("type"))}]' if _.get("type") != None and _.get("items").get("type") != None else
|
|
125
|
+
f'{Text.to_python_type(_.get("type"))}[{_.get("items").get("$ref").split('/')[-1]}]' if _.get("type") != None and _.get("items").get("$ref") != None else None for _ in attr.get("oneOf", [attr])] +
|
|
126
|
+
['None' for _ in attr.get("oneOf", [attr]) if _.get("nullable") == True]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class Schema:
|
|
132
|
+
status: str = None
|
|
133
|
+
name: str = None
|
|
134
|
+
body_type: Literal["request", "response"] = None
|
|
135
|
+
content_type: str = None
|
|
136
|
+
properties: list[Props] = None
|
|
137
|
+
|
|
138
|
+
def __post_init__(self):
|
|
139
|
+
self.content_type = self.set_content_type(self.content_type)
|
|
140
|
+
self.name = self.set_name(self.name)
|
|
141
|
+
|
|
142
|
+
def set_name(self, attr: dict):
|
|
143
|
+
if isinstance(attr, str):
|
|
144
|
+
return attr
|
|
145
|
+
return name.split('/')[-1] if (name := Text.find(attr, '$ref')) != None else None
|
|
146
|
+
|
|
147
|
+
def set_content_type(self, attr: dict):
|
|
148
|
+
if self.content_type != None:
|
|
149
|
+
return ''.join(content_type.keys()) if (content_type := Text.find(attr, 'content')) != None else None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class Method:
|
|
154
|
+
name: str = None
|
|
155
|
+
tag: str = None
|
|
156
|
+
schemas: list[Schema] = None
|
|
157
|
+
parameters: list[Parameter] = None
|
|
158
|
+
|
|
159
|
+
def __post_init__(self):
|
|
160
|
+
self.tag = self.set_tag(self.tag)
|
|
161
|
+
|
|
162
|
+
def set_tag(self, attr: dict):
|
|
163
|
+
return attr["tags"][0]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class Endpoint:
|
|
168
|
+
endpoint: str = None
|
|
169
|
+
name: str = None
|
|
170
|
+
methods: list[Method] = None
|
|
171
|
+
|
|
172
|
+
def __post_init__(self):
|
|
173
|
+
self.name = self.set_name(self.name)
|
|
174
|
+
|
|
175
|
+
def set_name(self, attr: str):
|
|
176
|
+
return attr.replace("/api/v1/", "").replace("/", "_").replace("{", "").replace("}", "")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Parsing:
|
|
180
|
+
|
|
181
|
+
def __init__(self):
|
|
182
|
+
self.__context = ssl.create_default_context()
|
|
183
|
+
self.__context.check_hostname = False
|
|
184
|
+
self.__context.verify_mode = ssl.CERT_NONE
|
|
185
|
+
self.__open_api = json.loads(urllib.request.urlopen(url=urllib.request.Request(Config.API), context=self.__context).read().decode('UTF-8'))
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def environments(self) -> dict:
|
|
189
|
+
def add_dict_env(env_dict: dict, env_list: list, env_str: str):
|
|
190
|
+
"""
|
|
191
|
+
Создать список с переменными окружения
|
|
192
|
+
- env_dict: словарь для добавления новых переменных
|
|
193
|
+
- env_list: список ключей для создания вложенности
|
|
194
|
+
- env_str: имя переменной окружения
|
|
195
|
+
"""
|
|
196
|
+
if len(env_list) != 1:
|
|
197
|
+
env_dict.setdefault(env_list[0], {})
|
|
198
|
+
add_dict_env(env_dict[env_list[0]], env_list[1:], env_str)
|
|
199
|
+
else:
|
|
200
|
+
env_dict.setdefault(env_list[0], env_str)
|
|
201
|
+
if self.__dict__.get('_environments') == None:
|
|
202
|
+
self._environments = {}
|
|
203
|
+
for name_env in Config.ENV:
|
|
204
|
+
add_dict_env(self._environments, name_env.split('__'), name_env.upper())
|
|
205
|
+
return self._environments
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def endpoints(self) -> list[Endpoint]:
|
|
209
|
+
if self.__dict__.get('_endpoints') == None:
|
|
210
|
+
self._endpoints = [
|
|
211
|
+
Endpoint(
|
|
212
|
+
endpoint=endpoint,
|
|
213
|
+
name=endpoint,
|
|
214
|
+
methods=[
|
|
215
|
+
Method(
|
|
216
|
+
name=method,
|
|
217
|
+
tag=method_values,
|
|
218
|
+
parameters=[
|
|
219
|
+
Parameter(
|
|
220
|
+
alias=parameter,
|
|
221
|
+
param=parameter,
|
|
222
|
+
type=parameter
|
|
223
|
+
) for parameter in method_values.get("parameters")
|
|
224
|
+
] if method_values.get("parameters") != None else None,
|
|
225
|
+
schemas=[__ for _ in [[
|
|
226
|
+
Schema(
|
|
227
|
+
body_type="response",
|
|
228
|
+
content_type=method_values.get("responses")[status],
|
|
229
|
+
status=status,
|
|
230
|
+
name=method_values.get("responses")[status],
|
|
231
|
+
properties=[
|
|
232
|
+
Props(
|
|
233
|
+
name=props,
|
|
234
|
+
type=props_values,
|
|
235
|
+
schema=props_values
|
|
236
|
+
) for props, props_values in Text.find(method_values.get("responses")[status], "properties").items()
|
|
237
|
+
] if Text.find(method_values.get("responses")[status], '$ref') == None and Text.find(method_values.get("responses")[status], "properties") != None else None
|
|
238
|
+
) for status in method_values.get("responses")
|
|
239
|
+
] if method_values.get("responses") != None else []] + [[
|
|
240
|
+
Schema(
|
|
241
|
+
body_type="request",
|
|
242
|
+
content_type=method_values.get("requestBody"),
|
|
243
|
+
name=method_values.get("requestBody"),
|
|
244
|
+
properties=[
|
|
245
|
+
Props(
|
|
246
|
+
name=props,
|
|
247
|
+
type=props_values,
|
|
248
|
+
schema=props_values
|
|
249
|
+
) for props, props_values in Text.find(method_values.get("requestBody"), "properties").items()
|
|
250
|
+
] if Text.find(method_values.get("requestBody"), '$ref') == None and Text.find(method_values.get("requestBody"), "properties") != None else None
|
|
251
|
+
)
|
|
252
|
+
] if method_values.get("requestBody") != None else []] for __ in _]
|
|
253
|
+
) for method, method_values in endpoint_values.items()]
|
|
254
|
+
) for endpoint, endpoint_values in self.__open_api["paths"].items()]
|
|
255
|
+
return self._endpoints
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def schemas(self) -> list[Schema]:
|
|
259
|
+
if self.__dict__.get('_schemas') == None:
|
|
260
|
+
self._schemas = [
|
|
261
|
+
Schema(
|
|
262
|
+
name=schema,
|
|
263
|
+
properties=[
|
|
264
|
+
Props(
|
|
265
|
+
name=props,
|
|
266
|
+
type=props_values,
|
|
267
|
+
schema=props_values
|
|
268
|
+
) for props, props_values in schema_values.get("properties").items()
|
|
269
|
+
] if schema_values.get("properties") != None else None
|
|
270
|
+
) for schema, schema_values in self.__open_api["components"]["schemas"].items()]
|
|
271
|
+
return self._schemas
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def tags(self) -> list:
|
|
275
|
+
if self.__dict__.get('_tags') == None:
|
|
276
|
+
self._tags = list(set([__.tag for _ in self.endpoints for __ in _.methods]))
|
|
277
|
+
return self._tags
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class Text:
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def to_comment_line(cls, text: str) -> str:
|
|
284
|
+
return f'# {"=" * int((79 - len(text)) / 2)} {text} {"=" * int((79 - len(text)) / 2)}\n'
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def to_python_type(cls, text: str) -> str:
|
|
288
|
+
return {
|
|
289
|
+
"string": "str",
|
|
290
|
+
"number": "float",
|
|
291
|
+
"array": "list",
|
|
292
|
+
"integer": "int",
|
|
293
|
+
"boolean": "bool",
|
|
294
|
+
"object": "dict"
|
|
295
|
+
}.get(text)
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def find(cls, data: dict, text: str) -> str | dict | None:
|
|
299
|
+
for _ in data:
|
|
300
|
+
if _ == text:
|
|
301
|
+
return data[_]
|
|
302
|
+
if isinstance(data[_], dict):
|
|
303
|
+
if (__ := cls.find(data[_], text)) != None:
|
|
304
|
+
return __
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def to_snake_case(cls, text: str) -> str:
|
|
308
|
+
"""Преобразует текст в snake_case"""
|
|
309
|
+
text = re.sub(r'[\s-]+', '_', text) # Заменяем пробелы и дефисы на подчеркивания
|
|
310
|
+
text = re.sub(r'([a-z])([A-Z])', r'\1_\2', text) # Вставляем подчеркивания между строчными и прописными буквами
|
|
311
|
+
text = re.sub(r'_+', '_', text) # Заменяем несколько подчеркиваний на одно
|
|
312
|
+
return text.strip().lower() # Приводим к нижнему регистру и убираем пробелы по краям
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def to_camel_case(cls, text: str) -> str:
|
|
316
|
+
"""Преобразует текст в camelCase"""
|
|
317
|
+
text = re.sub(r'[\s\-_]+', ' ', text) # Заменяем пробелы, дефисы, подчеркивания на пробелы
|
|
318
|
+
text = text.strip().split() # Разбиваем на слова
|
|
319
|
+
text = ''.join([_.capitalize() for _ in text]) # Объединяем слова с заглавной буквой
|
|
320
|
+
return text
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class Matrix:
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def packing(cls, lines: str):
|
|
327
|
+
return [[_.splitlines(keepends=True) for _ in re.split(r'(?<=\n)\n(?=[^\n])', line)] for line in re.split(r'(?<=\n)\n{2}(?=[^\n])', lines)]
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def unpacking(cls, lines: list, types: Literal["str", "list"] = "list", filters: Callable = lambda _: _):
|
|
331
|
+
flag_1, flag_2, last = True, True, ''
|
|
332
|
+
def auto_replace(line: str):
|
|
333
|
+
nonlocal flag_1, flag_2, last
|
|
334
|
+
while line.count('\n') > 0:
|
|
335
|
+
line = line.replace('\n', '')
|
|
336
|
+
if any([
|
|
337
|
+
line[:1] == '#' and flag_1,
|
|
338
|
+
line[:5] == ' @' and last[:5] != ' @' and flag_1,
|
|
339
|
+
line[:10] == ' class ' and flag_1,
|
|
340
|
+
line[:8] == ' def ' and last[:5] != ' @' and flag_1,
|
|
341
|
+
line[:14] == ' async def ' and last[:5] != ' @' and flag_1,
|
|
342
|
+
line[:10] == ' PARAM_' and flag_1
|
|
343
|
+
]):
|
|
344
|
+
last, flag_1, flag_2 = line, False, True
|
|
345
|
+
return '\n' + line + '\n'
|
|
346
|
+
elif any([
|
|
347
|
+
line[:1] == '@' and last[:1] != '@' and flag_2,
|
|
348
|
+
line[:6] == 'class ' and last[:1] != '@' and flag_2,
|
|
349
|
+
line[:4] == 'def ' and flag_2,
|
|
350
|
+
line[:10] == 'async def ' and flag_2
|
|
351
|
+
]):
|
|
352
|
+
last, flag_1, flag_2 = line, True, False
|
|
353
|
+
return '\n\n' + line + '\n'
|
|
354
|
+
else:
|
|
355
|
+
last, flag_1, flag_2 = line, True, True
|
|
356
|
+
return line + '\n'
|
|
357
|
+
def auto_unpacking(lines: list[str]):
|
|
358
|
+
__ = []
|
|
359
|
+
for line in lines:
|
|
360
|
+
if isinstance(line, list):
|
|
361
|
+
__.extend(auto_unpacking(line))
|
|
362
|
+
else:
|
|
363
|
+
if line.strip() and filters(line):
|
|
364
|
+
__.append(auto_replace(line))
|
|
365
|
+
return __
|
|
366
|
+
if types == "list":
|
|
367
|
+
return auto_unpacking(lines)
|
|
368
|
+
elif types == "str":
|
|
369
|
+
return ''.join(auto_unpacking(lines)).strip() + '\n'
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
def find_point(cls, lines: list, key: str, point: list = None):
|
|
373
|
+
if point == None:
|
|
374
|
+
point = []
|
|
375
|
+
if isinstance(lines, list):
|
|
376
|
+
for _, line in enumerate(lines):
|
|
377
|
+
if (__ := cls.find_point(line, key, point + [_])) is not None:
|
|
378
|
+
return __
|
|
379
|
+
elif key in lines:
|
|
380
|
+
return point
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class File:
|
|
384
|
+
|
|
385
|
+
LIBS = [name for path, name, _ in pkgutil.iter_modules() if 'Python3'in str(path)]
|
|
386
|
+
|
|
387
|
+
def __init__(self, file: TextIOWrapper):
|
|
388
|
+
self.__file = file
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def depends(self) -> list:
|
|
392
|
+
if self.__dict__.get('_depends') == None:
|
|
393
|
+
self._depends = []
|
|
394
|
+
return self.__dict__.get('_depends')
|
|
395
|
+
|
|
396
|
+
@depends.setter
|
|
397
|
+
def depends(self, lines: list[str]):
|
|
398
|
+
def check(line: str):
|
|
399
|
+
return all([
|
|
400
|
+
line not in ['# Стандартная библиотека\n', '# Установленные библиотеки\n', '# Локальные импорты\n'],
|
|
401
|
+
line[0] != '@',
|
|
402
|
+
line[:4] != 'def ',
|
|
403
|
+
line[:5] != 'from ',
|
|
404
|
+
line[:6] != 'class ',
|
|
405
|
+
line[:6] != 'async ',
|
|
406
|
+
line[:7] != 'import ',
|
|
407
|
+
line[:5] != ' ',
|
|
408
|
+
line[:5] != ' @',
|
|
409
|
+
line[:8] != ' def ',
|
|
410
|
+
line[:10] != ' class ',
|
|
411
|
+
line[:10] != ' async ',
|
|
412
|
+
line[:11] != ' return ',
|
|
413
|
+
])
|
|
414
|
+
lines = Matrix.packing(Matrix.unpacking(lines, types="str"))
|
|
415
|
+
if (point := Matrix.find_point(lines, 'class ')) != None:
|
|
416
|
+
lines = lines[:point[0]]
|
|
417
|
+
elif (point := Matrix.find_point(lines, 'def ')) != None:
|
|
418
|
+
lines = lines[:point[0]]
|
|
419
|
+
self._depends = Matrix.packing(Matrix.unpacking(lines, types="str", filters=check))
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def imports(self) -> list:
|
|
423
|
+
if self.__dict__.get('_imports') == None:
|
|
424
|
+
if (point := Matrix.find_point(self.lines, 'import')) != None:
|
|
425
|
+
self._imports = [self.lines[point[0]]]
|
|
426
|
+
return [[
|
|
427
|
+
sorted(list(filter(lambda _: 'from' not in _, ___))) +
|
|
428
|
+
sorted(list(filter(lambda _: 'from' in _, ___)))
|
|
429
|
+
for __ in self.__dict__.get('_imports', []) for ___ in __
|
|
430
|
+
]]
|
|
431
|
+
|
|
432
|
+
@imports.setter
|
|
433
|
+
def imports(self, lines: list[str]):
|
|
434
|
+
if (lines := list(filter(lambda _: 'import ' in _ and _[0] != ' ' in _, Matrix.unpacking(self.imports + lines)))) == []:
|
|
435
|
+
self._imports = lines
|
|
436
|
+
else:
|
|
437
|
+
__ = [
|
|
438
|
+
['# Стандартная библиотека\n'],
|
|
439
|
+
['# Установленные библиотеки\n'],
|
|
440
|
+
['# Локальные импорты\n']
|
|
441
|
+
]
|
|
442
|
+
for _ in list(set(lines)):
|
|
443
|
+
if _.split()[1] in self.LIBS:
|
|
444
|
+
__[0].append(_)
|
|
445
|
+
elif Config.PATHS['autotest'][:-1] in _:
|
|
446
|
+
__[2].append(_)
|
|
447
|
+
elif '#' not in _:
|
|
448
|
+
__[1].append(_)
|
|
449
|
+
self._imports = [[_ for _ in __ if len(_) > 1]]
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def classes(self) -> list:
|
|
453
|
+
if self.__dict__.get('_classes') == None:
|
|
454
|
+
if (point := Matrix.find_point(self.lines, 'class ')) != None:
|
|
455
|
+
self._classes = self.lines[point[0]:]
|
|
456
|
+
return [[
|
|
457
|
+
_[0],
|
|
458
|
+
*sorted(list(filter(lambda __: Matrix.find_point(__, ' class') != None, _[1:])),
|
|
459
|
+
key=lambda __: re.findall(r'\s(\w+)[^\w\s]', __[Matrix.find_point(__, 'class')[0]])[0]),
|
|
460
|
+
*sorted(list(filter(lambda __: Matrix.find_point(__, ' def') != None, _[1:])),
|
|
461
|
+
key=lambda __: re.findall(r'\s(\w+)[^\w\s]', __[Matrix.find_point(__, 'def')[0]])[0]),
|
|
462
|
+
*sorted(list(filter(lambda __: Matrix.find_point(__, ' PARAM_') != None, _[1:])),
|
|
463
|
+
key=lambda __: re.findall(r'\s(\w+)[^\w\s]', __[Matrix.find_point(__, 'PARAM_')[0]])[0])
|
|
464
|
+
] for _ in self.__dict__.get('_classes', [])]
|
|
465
|
+
|
|
466
|
+
@classes.setter
|
|
467
|
+
def classes(self, lines: list):
|
|
468
|
+
if Matrix.find_point(lines, 'class ') != None:
|
|
469
|
+
for lines in Matrix.packing(Matrix.unpacking(lines, types="str"))[Matrix.find_point(Matrix.packing(Matrix.unpacking(lines, types="str")), 'class ')[0]:]:
|
|
470
|
+
name = re.findall(r'\s(\w+)[^\w\s]', ''.join([_ for _ in lines[0] if 'class' in _]))[0]
|
|
471
|
+
point = Matrix.find_point([[_[0]] for _ in self.classes], name)
|
|
472
|
+
if point == None:
|
|
473
|
+
self._classes = self.__dict__.get('_classes', []) + [lines]
|
|
474
|
+
else:
|
|
475
|
+
for line in lines[1:]:
|
|
476
|
+
_name: str = re.findall(r'\s(\w+)[^\w\s]', ''.join(line))[0]
|
|
477
|
+
_point = Matrix.find_point(self._classes[point[0]], _name)
|
|
478
|
+
if _point == None:
|
|
479
|
+
self._classes[point[0]] += [line]
|
|
480
|
+
elif (not _name.islower() and not _name.isupper()) or (_name == '__init__' and Matrix.find_point(self._classes[point[0]], '__init__(self, client:') != None):
|
|
481
|
+
self._classes[point[0]][_point[0]] = line
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def funcs(self) -> list:
|
|
485
|
+
if self.__dict__.get('_funcs') == None:
|
|
486
|
+
if Matrix.find_point(self.lines, 'class ') == None:
|
|
487
|
+
point = [point[0] for point in [Matrix.find_point(self.lines, 'def '), Matrix.find_point(self.lines, 'async def ')] if point != None]
|
|
488
|
+
if len(point) != 0:
|
|
489
|
+
self._funcs = self.lines[min(point):]
|
|
490
|
+
return self.__dict__.get('_funcs', [])
|
|
491
|
+
|
|
492
|
+
@funcs.setter
|
|
493
|
+
def funcs(self, lines: list):
|
|
494
|
+
if Matrix.find_point(lines, 'class ') == None:
|
|
495
|
+
lines = Matrix.packing(Matrix.unpacking(lines, types='str'))
|
|
496
|
+
point = [point[0] for point in [Matrix.find_point(lines, 'def '), Matrix.find_point(lines, 'async def ')] if point != None]
|
|
497
|
+
if len(point) != 0:
|
|
498
|
+
self._funcs = lines[min(point):]
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def lines(self) -> list:
|
|
502
|
+
if self.__dict__.get('_lines') == None:
|
|
503
|
+
self.__file.seek(0)
|
|
504
|
+
if (lines := self.__file.readlines()) != []:
|
|
505
|
+
self._lines = Matrix.packing(Matrix.unpacking(lines, types="str"))
|
|
506
|
+
return self.__dict__.get('_lines', [])
|
|
507
|
+
|
|
508
|
+
@lines.setter
|
|
509
|
+
def lines(self, lines: list):
|
|
510
|
+
self.imports = lines
|
|
511
|
+
self.depends = lines
|
|
512
|
+
self.classes = lines
|
|
513
|
+
self.funcs = lines
|
|
514
|
+
self.__file.truncate(0)
|
|
515
|
+
self.__file.write('\n\n'.join([
|
|
516
|
+
line for line in
|
|
517
|
+
[
|
|
518
|
+
Matrix.unpacking(self.imports, types="str"),
|
|
519
|
+
Matrix.unpacking(self.depends, types="str"),
|
|
520
|
+
Matrix.unpacking(self.classes, types="str"),
|
|
521
|
+
Matrix.unpacking(self.funcs, types="str")
|
|
522
|
+
] if line != '\n'
|
|
523
|
+
]))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class Creator:
|
|
527
|
+
|
|
528
|
+
PARSING = Parsing()
|
|
529
|
+
|
|
530
|
+
def __init__(self, path: str, mode: Literal["+a", "+w"], name: Literal["blank_client", "blank_page", "endpoints", "environments", "gitignore", "schemas", "pytest", "pages", "param_env", "params_api", "params_ui", "readme", "requirements", "conftest_api", "conftest_ui", "tests_api", "tests_ui"]):
|
|
531
|
+
self.__path = path
|
|
532
|
+
self.__mode = mode
|
|
533
|
+
self.__name = name
|
|
534
|
+
|
|
535
|
+
def __call__(self, func):
|
|
536
|
+
def wrapper(wrapper_self, *args, **kwds):
|
|
537
|
+
LINES = {
|
|
538
|
+
"endpoints": [{"lines": lines, "file_name": f'{Text.to_snake_case(file_name)}_endpoints.py', "file_path": ""} for lines, file_name in zip([list(filter(lambda _: len([__ for __ in _.methods if __.tag == tag]) != 0, Creator.PARSING.endpoints)) for tag in Creator.PARSING.tags], Creator.PARSING.tags)],
|
|
539
|
+
"blank_client": [{"lines": {"tags": Creator.PARSING.tags, "environments": Creator.PARSING.environments}, "file_name": "blank_client.py", "file_path": ""}],
|
|
540
|
+
"blank_page": [{"lines": {"pages": Config.PAGES, "environments": Creator.PARSING.environments}, "file_name": "blank_page.py", "file_path": ""}],
|
|
541
|
+
"components": [{"lines": line, "file_name": f'component_{line.lower()}.py', "file_path": ""} for line, count in Counter([__ for _ in Config.PAGES for __ in _["components"]]).items() if count > 1],
|
|
542
|
+
"conftest": [{"lines": {"environments": Creator.PARSING.environments}, "file_name": "conftest.py", "file_path": ""}],
|
|
543
|
+
"conftest_ui": [{"lines": {"environments": Creator.PARSING.environments}, "file_name": "conftest.py", "file_path": ""}],
|
|
544
|
+
"environments": [{"lines": {"environments": environment, "file_name": file_name}, "file_name": file_name, "file_path": ""} for environment, file_name in [(Config.ENV, name) for name in [".env", ".env.example"]]],
|
|
545
|
+
"gitignore": [{"lines": [], "file_name": ".gitignore", "file_path": ""}],
|
|
546
|
+
"readme": [{"lines": [], "file_name": "README.md", "file_path": ""}],
|
|
547
|
+
"pages": [{"lines": {"page": line, "components": [___ for ___, count in Counter([__ for _ in Config.PAGES for __ in _["components"]]).items() if count > 1]}, "file_name": f'{line["name"].lower()}.py', "file_path": ""} for line in Config.PAGES],
|
|
548
|
+
"param_env": [{"lines": {"environments": Creator.PARSING.environments}, "file_name": "param_env.py", "file_path": ""}],
|
|
549
|
+
"params_api": [{"lines": lines, "file_name": f'param_{Text.to_snake_case(file_name)}_api.py', "file_path": ""} for lines, file_name in zip([list(filter(lambda _: len([__ for __ in _.methods if __.tag == tag]) != 0, Creator.PARSING.endpoints)) for tag in Creator.PARSING.tags], Creator.PARSING.tags)],
|
|
550
|
+
"params_ui": [{"lines": lines, "file_name": f'param_{Text.to_snake_case(lines["name"])}.py', "file_path": ""} for lines in Config.PAGES],
|
|
551
|
+
"pytest": [{"lines": {"api": Creator.PARSING.tags, "ui": Config.PAGES}, "file_name": "pytest.ini", "file_path": ""}],
|
|
552
|
+
"requirements": [{"lines": [], "file_name": "requirements.txt", "file_path": ""}],
|
|
553
|
+
"schemas": [{"lines": schema, "file_name": f'{Text.to_snake_case(schema.name)}.py', "file_path": ""} for schema in Creator.PARSING.schemas],
|
|
554
|
+
"tests_api": [{"lines": line, "file_name": f'test_{line.name}.py', "file_path": f'test_{Text.to_snake_case(line.methods[0].tag)}/'} for line in Creator.PARSING.endpoints],
|
|
555
|
+
"tests_ui": [{"lines": {"component": component, "name": line["name"], "url": line["url"]}, "file_name": f'test_{Text.to_snake_case(line["name"] + '_' + component.replace(f'{line["name"]}_', ''))}.py', "file_path": f'test_{Text.to_snake_case(line["name"])}/'} for line in Config.PAGES for component in line["components"]]
|
|
556
|
+
}
|
|
557
|
+
for line in LINES[self.__name]:
|
|
558
|
+
Path(f'{Config.PATHS["path"]}{self.__path}{line["file_path"]}').mkdir(exist_ok=True, parents=True)
|
|
559
|
+
with open(f'{Config.PATHS["path"]}{self.__path}{line["file_path"]}{line["file_name"]}', self.__mode, encoding="UTF-8") as file:
|
|
560
|
+
file = File(file)
|
|
561
|
+
file.lines = func(wrapper_self, line["lines"], *args, **kwds)
|
|
562
|
+
return wrapper
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class Create:
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
@Creator(path=Config.PATHS["tests"], mode="+w", name="conftest")
|
|
569
|
+
def conftest_api(cls, lines: Literal["environments"]):
|
|
570
|
+
return [
|
|
571
|
+
'import pytest',
|
|
572
|
+
'from httpx import AsyncClient',
|
|
573
|
+
'from dotenv import load_dotenv',
|
|
574
|
+
f'from {Config.PATHS["swagger"].replace("/", ".")}blank_client import BlankClient',
|
|
575
|
+
'load_dotenv()',
|
|
576
|
+
'@pytest.fixture(scope="session")',
|
|
577
|
+
'def environments():',
|
|
578
|
+
f' return {lines["environments"]}',
|
|
579
|
+
'@pytest.fixture(scope="session", autouse=True)',
|
|
580
|
+
'async def token(environments):',
|
|
581
|
+
' async with AsyncClient(verify=False) as client:',
|
|
582
|
+
' client = BlankClient(client)',
|
|
583
|
+
' await client.create_token(environments)',
|
|
584
|
+
'@pytest.fixture(scope="function")',
|
|
585
|
+
'async def client():',
|
|
586
|
+
' async with AsyncClient(verify=False) as client:',
|
|
587
|
+
' client = BlankClient(client)',
|
|
588
|
+
' yield client'
|
|
589
|
+
]
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
@Creator(path=Config.PATHS["tests_test_ui"], mode="+w", name="conftest_ui")
|
|
593
|
+
def conftest_ui(cls, lines: Literal["environments"]):
|
|
594
|
+
return [
|
|
595
|
+
'import pytest',
|
|
596
|
+
'import allure',
|
|
597
|
+
'import shutil',
|
|
598
|
+
'import os',
|
|
599
|
+
'from pathlib import Path',
|
|
600
|
+
'from playwright.async_api import async_playwright, Browser',
|
|
601
|
+
f'from {Config.PATHS["browser"].replace("/", ".")}blank_page import BlankPage',
|
|
602
|
+
f'from {Config.PATHS["swagger"].replace("/", ".")}blank_client import BlankClient',
|
|
603
|
+
'@pytest.fixture(scope="session")',
|
|
604
|
+
'async def browser():',
|
|
605
|
+
' async with async_playwright() as _:',
|
|
606
|
+
' browser = await _.chromium.launch(',
|
|
607
|
+
' args=[',
|
|
608
|
+
' \'--disable-gpu\',',
|
|
609
|
+
' \'--disable-dev-shm-usage\',',
|
|
610
|
+
' \'--disable-setuid-sandbox\',',
|
|
611
|
+
' \'--no-first-run\',',
|
|
612
|
+
' \'--no-sandbox\',',
|
|
613
|
+
' \'--no-zygote\',',
|
|
614
|
+
' \'--disable-web-security\',',
|
|
615
|
+
' \'--disable-features=VizDisplayCompositor\',',
|
|
616
|
+
' \'--disable-background-timer-throttling\',',
|
|
617
|
+
' \'--disable-renderer-backgrounding\',',
|
|
618
|
+
' \'--disable-backgrounding-occluded-windows\'',
|
|
619
|
+
' ],',
|
|
620
|
+
' # headless=False,',
|
|
621
|
+
' # slow_mo=500',
|
|
622
|
+
' )',
|
|
623
|
+
' yield browser',
|
|
624
|
+
' await browser.close()',
|
|
625
|
+
'@pytest.fixture(scope="function")',
|
|
626
|
+
'async def page(browser: Browser, request: pytest.FixtureRequest, environments):',
|
|
627
|
+
' params = request.node.name.split(\'[\')[-1].replace(\']\', \'\').split(\'-\')',
|
|
628
|
+
f' domain, user = set(params) & {set(lines["environments"])}, set(params) & {set(lines["environments"][list(lines["environments"])[0]]["user"])}',
|
|
629
|
+
' domain = \'\'.join(domain) if len(domain) != 0 else \'dev\'',
|
|
630
|
+
' user = \'\'.join(user) if len(user) != 0 else \'admin\'',
|
|
631
|
+
' path = f\'{Path(__file__).parent.parent.parent}\\\\auth\\\\{domain}_{user}.json\'',
|
|
632
|
+
' Path(path).parent.mkdir(parents=True, exist_ok=True)',
|
|
633
|
+
' context = await browser.new_context(',
|
|
634
|
+
' viewport={\'width\': 1920, \'height\': 1080},',
|
|
635
|
+
' base_url=os.environ[environments["dev"]["app"]["domain"]],',
|
|
636
|
+
' storage_state=path if Path(path).exists() and \'auth\' not in request.node.name else None,',
|
|
637
|
+
' record_video_dir=f\'{Path(__file__).parent.parent.parent}\\\\videos\\\\{request.node.name}\\\\\',',
|
|
638
|
+
' ignore_https_errors=True,',
|
|
639
|
+
' java_script_enabled=True,',
|
|
640
|
+
' bypass_csp=True',
|
|
641
|
+
' )',
|
|
642
|
+
' page = await context.new_page()',
|
|
643
|
+
' yield BlankPage(page)',
|
|
644
|
+
' if \'auth\' in request.node.name:',
|
|
645
|
+
' await page.context.storage_state(path=path)',
|
|
646
|
+
' await page.close()',
|
|
647
|
+
' await context.close()',
|
|
648
|
+
'@pytest.hookimpl(tryfirst=True, hookwrapper=True)',
|
|
649
|
+
'def pytest_runtest_makereport(item: pytest.Item):',
|
|
650
|
+
' """Получаем отчет о выполнении теста и удаляем видео успешных тестов"""',
|
|
651
|
+
' def replace(lines: list, params: dict):',
|
|
652
|
+
' _ = []',
|
|
653
|
+
' def recursion(line: str, params: dict):',
|
|
654
|
+
' for param in params:',
|
|
655
|
+
' if isinstance(params[param], dict):',
|
|
656
|
+
' return recursion(line, params[param])',
|
|
657
|
+
' return line.replace("{" + param + "}", str(params[param]))',
|
|
658
|
+
' for line in lines:',
|
|
659
|
+
' line = recursion(line, params)[8:]',
|
|
660
|
+
' if "{" not in line and "}" not in line:',
|
|
661
|
+
' _.append(line)',
|
|
662
|
+
' return _',
|
|
663
|
+
' outcome = yield',
|
|
664
|
+
' report = outcome.get_result()',
|
|
665
|
+
' if report.when == "setup" and (doc := item.function.__doc__) != None:',
|
|
666
|
+
' doc = replace(doc.strip().split(\'\n\'), {"id": item.callspec.id} | item.callspec.params)',
|
|
667
|
+
' allure.dynamic.title(doc[0])',
|
|
668
|
+
' allure.dynamic.description("\n".join(doc[2:]))',
|
|
669
|
+
' if report.when == "call" and report.outcome == "passed":',
|
|
670
|
+
' shutil.rmtree(Path(f\'{Path(__file__).parent.parent.parent}\\\\videos\\\\{item.name}\\\\\'))'
|
|
671
|
+
]
|
|
672
|
+
|
|
673
|
+
@classmethod
|
|
674
|
+
@Creator(path="", mode="+w", name="pytest")
|
|
675
|
+
def pytest(cls, lines: dict[Literal["api", "ui"], list[str | Page]]):
|
|
676
|
+
return [
|
|
677
|
+
'[pytest]',
|
|
678
|
+
'pythonpath = . ',
|
|
679
|
+
f'testspath = {Config.PATHS["tests"]}',
|
|
680
|
+
'disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True',
|
|
681
|
+
'max_asyncio_tasks = 8',
|
|
682
|
+
# 'asyncio_default_fixture_loop_scope = function',
|
|
683
|
+
'addopts = ',
|
|
684
|
+
' -m \'regression\'',
|
|
685
|
+
' -p no:pytest_asyncio',
|
|
686
|
+
# ' -p no:pytest-xdist',
|
|
687
|
+
f' --alluredir={Config.PATHS["path"]}{Config.PATHS["autotest"]}results',
|
|
688
|
+
' --clean-alluredir',
|
|
689
|
+
'markers = ',
|
|
690
|
+
' regression: регресс тесты',
|
|
691
|
+
' api: api тесты',
|
|
692
|
+
' ui: ui тесты',
|
|
693
|
+
[f' api_{Text.to_snake_case(line)}: api тесты {line}' for line in lines["api"]],
|
|
694
|
+
[f' ui_{Text.to_snake_case(line["name"])}: ui тесты {line["name"]}' for line in lines["ui"]]
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
@classmethod
|
|
698
|
+
@Creator(path="", mode="+w", name="readme")
|
|
699
|
+
def readme(cls, *args):
|
|
700
|
+
return [
|
|
701
|
+
f'# Autotest (первый запуск, настройка для Windows)',
|
|
702
|
+
f'- устанавливаем vscode',
|
|
703
|
+
f'- устанавляваем python>=3.12',
|
|
704
|
+
f'- клонируем репозиторий',
|
|
705
|
+
f'- создаем витруальное окружение со всеми зависимостями',
|
|
706
|
+
f'```',
|
|
707
|
+
f'pip install --upgrade pip',
|
|
708
|
+
f'cd {Config.PATHS["path"].split("/")[-2]}',
|
|
709
|
+
f'python -m venv .venv',
|
|
710
|
+
r'.venv\Scripts\activate.ps1',
|
|
711
|
+
f'pip install -r requirements.txt',
|
|
712
|
+
'$env:PLAYWRIGHT_DOWNLOAD_HOST="https://nexus-cache.services.mts.ru/repository/raw-playwright.azureedge.net-proxy"',
|
|
713
|
+
'$env:NODE_TLS_REJECT_UNAUTHORIZED=0',
|
|
714
|
+
'playwright install',
|
|
715
|
+
f'```',
|
|
716
|
+
f'- открываем палитру команд (ctrl+shift+p)',
|
|
717
|
+
f'- выбираем настройку тестов (configure tests)',
|
|
718
|
+
f'- выбираем pytest',
|
|
719
|
+
f'- выбираем {Config.PATHS["path"].split("/")[-2]}',
|
|
720
|
+
f'- запускаем тесты через вкладку тестирование',
|
|
721
|
+
'# Дополнительно (настройка .env)',
|
|
722
|
+
'- на основании шаблона .env.example создать файл .env',
|
|
723
|
+
'- заполнить данные о пользователях',
|
|
724
|
+
' - admin - role_id (1,2,10,11,14,15,16,20,21,22,23)',
|
|
725
|
+
' - regmn - role_id (3,4,5,6,7,11,17,18,19) region_id (02b50159-88e5-448c-935b-81f7cd8c0401, 087d56f0-fa0b-4dca-8163-f51880039387, 3ff456b7-98e3-4087-9fd9-9b60d3e40c0f, 46c2c07d-523d-430f-a6d9-6bd142858ad5, 5f476894-1f72-4940-8220-16b758167432, f58338d4-d8fe-4c75-9cfd-f12dc2725b63)',
|
|
726
|
+
'- данные о стенде получить из https://ocean.mts.ru/tenant/ac20b856-b7d9-49a1-a54d-178288f059b9/spaces/coffers-dev/iam/871a3c57-fa4b-43e5-bf7b-02b161738add?clientId=coffers-dev&stand=isso-dev.mts.ru'
|
|
727
|
+
]
|
|
728
|
+
|
|
729
|
+
@classmethod
|
|
730
|
+
@Creator(path="", mode="+w", name="requirements")
|
|
731
|
+
def requirements(cls, *args):
|
|
732
|
+
return sorted([
|
|
733
|
+
'pytest>=8.4.2',
|
|
734
|
+
'pytest-playwright>=0.7.1',
|
|
735
|
+
'pydantic>=2.12.3',
|
|
736
|
+
'opencv-python>=4.12.0.88',
|
|
737
|
+
'pytest-asyncio-cooperative>=0.40.0',
|
|
738
|
+
'httpx>=0.28.1',
|
|
739
|
+
'allure-pytest>=2.15.0',
|
|
740
|
+
'python-dotenv>=1.2.1'
|
|
741
|
+
])
|
|
742
|
+
|
|
743
|
+
@classmethod
|
|
744
|
+
@Creator(path="", mode="+w", name="gitignore")
|
|
745
|
+
def gitignore(cls, *args):
|
|
746
|
+
return sorted([
|
|
747
|
+
'sandbox',
|
|
748
|
+
'.env',
|
|
749
|
+
'.venv',
|
|
750
|
+
'.pytest_cache',
|
|
751
|
+
'__pycache__',
|
|
752
|
+
'auth\n',
|
|
753
|
+
'screenshots',
|
|
754
|
+
'allure-results',
|
|
755
|
+
'videos'
|
|
756
|
+
])
|
|
757
|
+
|
|
758
|
+
@classmethod
|
|
759
|
+
@Creator(path="", mode="+w", name="environments")
|
|
760
|
+
def environments(cls, lines: dict[Literal["environments", "file_name"]]):
|
|
761
|
+
return [
|
|
762
|
+
[Text.to_comment_line(translation),
|
|
763
|
+
sorted([f'{key.upper()}=\'{value if lines["file_name"] == ".env" else ""}\'' for key, value in lines["environments"].items() if section in key])] for section, translation in [("user", "ПОЛЬЗОВАТЕЛИ"), ("app", "СТЕНДЫ")]
|
|
764
|
+
]
|
|
765
|
+
|
|
766
|
+
@classmethod
|
|
767
|
+
@Creator(path=Config.PATHS["swagger"], mode="+w", name="blank_client")
|
|
768
|
+
def blank_client(cls, lines: dict[Literal["tags", "environments"]]):
|
|
769
|
+
return [
|
|
770
|
+
'import os\n',
|
|
771
|
+
'from typing import Literal\n',
|
|
772
|
+
'from httpx import AsyncClient\n',
|
|
773
|
+
[f'from {Config.PATHS["swagger_endpoints"].replace("/", ".")}{Text.to_snake_case(tag)}_endpoints import {Text.to_camel_case(tag)}\n' for tag in lines["tags"]],
|
|
774
|
+
'class BlankClient:\n',
|
|
775
|
+
' TOKEN = {}\n',
|
|
776
|
+
' def __init__(self, client: AsyncClient):\n',
|
|
777
|
+
' self.__client = client\n',
|
|
778
|
+
' async def create_token(self, environment: dict):\n',
|
|
779
|
+
' data = {"grant_type": "password", "username": None, "password": None, "client_id": None, "client_secret": None}\n',
|
|
780
|
+
' for stand in environment:\n',
|
|
781
|
+
' users = environment[stand]["user"]\n',
|
|
782
|
+
' app = environment[stand]["app"]\n',
|
|
783
|
+
' for user in users:\n',
|
|
784
|
+
' data["username"] = os.environ[users[user]["login"]]\n',
|
|
785
|
+
' data["password"] = os.environ[users[user]["password"]]\n',
|
|
786
|
+
' data["client_id"] = os.environ[app["client_id"]]\n',
|
|
787
|
+
' data["client_secret"] = os.environ[app["client_secret"]]\n',
|
|
788
|
+
' response = await self.__client.post(os.environ[app["isso_url"]], data=data)\n',
|
|
789
|
+
' BlankClient.TOKEN.setdefault(stand, {}).setdefault(user, {}).setdefault("token", response.json()["access_token"])\n',
|
|
790
|
+
' BlankClient.TOKEN.setdefault(stand, {}).setdefault(user, {}).setdefault("domain", os.environ[app["domain"]])\n\n\n',
|
|
791
|
+
f' def set_token(self, client: Literal{list(lines["environments"][list(lines["environments"])[0]]["user"])} = "{list(lines["environments"][list(lines["environments"])[0]]["user"])[0]}", domain: Literal{list(lines["environments"])} = "{list(lines["environments"])[0]}"):\n',
|
|
792
|
+
' self.__client.base_url = BlankClient.TOKEN[domain][client]["domain"]\n',
|
|
793
|
+
' self.__client.headers = {"Authorization": f\'Bearer {BlankClient.TOKEN[domain][client]["token"]}\'}\n',
|
|
794
|
+
[f' self.{Text.to_snake_case(tag)} = {Text.to_camel_case(tag)}(self.__client)\n' for tag in lines["tags"]]
|
|
795
|
+
]
|
|
796
|
+
|
|
797
|
+
@classmethod
|
|
798
|
+
@Creator(path=Config.PATHS["swagger_endpoints"], mode="+a", name="endpoints")
|
|
799
|
+
def endpoints(cls, lines: list[Endpoint]):
|
|
800
|
+
return [
|
|
801
|
+
'from httpx import AsyncClient\n',
|
|
802
|
+
'from pydantic import BaseModel, Field\n',
|
|
803
|
+
[f'from {Config.PATHS["swagger_schemas"].replace("/", ".")}{Text.to_snake_case(schema.name)} import {schema.name}\n' for line in lines for method in line.methods for schema in method.schemas if schema.name != None],
|
|
804
|
+
f'class {Text.to_camel_case(lines[0].methods[0].tag)}:\n',
|
|
805
|
+
' def __init__(self, client):\n',
|
|
806
|
+
[f' self.{Text.to_snake_case(line.name)} = Endpoint{Text.to_camel_case(line.name)}(client)\n' for line in lines],
|
|
807
|
+
[[f'class Endpoint{Text.to_camel_case(line.name)}:\n',
|
|
808
|
+
[[f' class {method.name.capitalize() + "Params"}(BaseModel):\n',
|
|
809
|
+
[f' {parameter.name}: {parameter.type} = Field(None, alias=\'{parameter.alias}\')\n' for parameter in list(filter(lambda _: _.param == "query", method.parameters))]] for method in line.methods if method.parameters != None and len(list(filter(lambda _: _.param == "query", method.parameters))) != 0],
|
|
810
|
+
[[f' class {method.name.capitalize()}RequestBody(BaseModel):\n',
|
|
811
|
+
[f' {props.name}: {props.type} = None\n' for props in schema.properties]] for method in line.methods for schema in method.schemas if schema.name == None and schema.properties != None and schema.body_type == "request"],
|
|
812
|
+
[[f' class {method.name.capitalize()}Status{schema.status}(BaseModel):\n',
|
|
813
|
+
[f' {props.name}: {props.type}\n' for props in schema.properties]] for method in line.methods for schema in method.schemas if schema.name == None and schema.properties != None and schema.body_type != "request"],
|
|
814
|
+
' def __init__(self, client: AsyncClient):\n',
|
|
815
|
+
' self.__client = client\n',
|
|
816
|
+
f' self.__url = \'{line.endpoint}\'\n',
|
|
817
|
+
[f' self.body_{method.name} = {request[0].name}()\n' if request[0].name != None else f' self.body_{method.name} = Endpoint{Text.to_camel_case(line.name)}.{method.name.capitalize()}RequestBody()\n' for method in line.methods if len(request := list(filter(lambda _: _.body_type == 'request' , method.schemas))) != 0],
|
|
818
|
+
[f' self.params = Endpoint{Text.to_camel_case(line.name)}.{method.name.capitalize() + "Params"}()\n' for method in line.methods if method.parameters != None and len(list(filter(lambda _: _.param == "query", method.parameters))) != 0],
|
|
819
|
+
[
|
|
820
|
+
[f' async def {method.name}(self{', ' + ', '.join([f'{parameter.name}: {parameter.type}' for parameter in list(filter(lambda _: _.param == "path", method.parameters))]) if method.parameters != None and len(list(filter(lambda _: _.param == "path", method.parameters))) != 0 else ''}):\n',
|
|
821
|
+
f' response = await self.__client.{method.name}(\n',
|
|
822
|
+
[f' self.__url{'.replace(' + '.replace('.join([f'\'{{{parameter.name}}}\', str({parameter.name}))' for parameter in list(filter(lambda _: _.param == "path", method.parameters))]) if method.parameters != None and len(list(filter(lambda _: _.param == "path", method.parameters))) != 0 else ''},\n',],
|
|
823
|
+
[f' headers={{\'Content-Type\': \'{schema.content_type}\'}},\n' for schema in list(filter(lambda _: _.body_type == "response" and 200 <= int(_.status) < 300 , method.schemas)) if schema.content_type != None],
|
|
824
|
+
[f' data = self.body_{method.name}.model_dump_json(),\n'] if len(request := list(filter(lambda _: _.body_type == 'request' , method.schemas))) != 0 else [],
|
|
825
|
+
[f' params = self.params.model_dump(exclude_none=True, by_alias=True),\n'] if method.parameters != None and len(list(filter(lambda _: _.param == "query", method.parameters))) != 0 else [],
|
|
826
|
+
f' )\n',
|
|
827
|
+
[[f' if response.status_code == {schema.status}:\n',
|
|
828
|
+
f' self.response_status_{schema.status} = Endpoint{Text.to_camel_case(line.name)}.{method.name.capitalize() + "Status" + schema.status}(**response.json())\n' if schema.name == None else f' self.response_status_{schema.status} = {schema.name}(**response.json())\n',] for schema in method.schemas if schema.status != None and (schema.name != None or schema.properties != None)],
|
|
829
|
+
f' return response\n'] for method in line.methods]] for line in lines]
|
|
830
|
+
]
|
|
831
|
+
|
|
832
|
+
@classmethod
|
|
833
|
+
@Creator(path=Config.PATHS["swagger_schemas"], mode="+w", name="schemas")
|
|
834
|
+
def schemas(cls, lines: Schema):
|
|
835
|
+
return [
|
|
836
|
+
'from pydantic import BaseModel\n',
|
|
837
|
+
[f'from {Config.PATHS["swagger_schemas"].replace("/", ".")}{Text.to_snake_case(props.schema)} import {props.schema}\n' for props in list(filter(lambda _: _.schema != None, lines.properties))],
|
|
838
|
+
f'class {lines.name}(BaseModel):\n',
|
|
839
|
+
[f' {props.name}: {props.type} = None\n' for props in lines.properties]
|
|
840
|
+
]
|
|
841
|
+
|
|
842
|
+
@classmethod
|
|
843
|
+
@Creator(path=Config.PATHS["params"], mode="+w", name="param_env")
|
|
844
|
+
def param_env(cls, lines: Literal["environments"]):
|
|
845
|
+
return [
|
|
846
|
+
'import pytest',
|
|
847
|
+
'from dataclasses import dataclass',
|
|
848
|
+
'@dataclass',
|
|
849
|
+
'class ParamEnv:',
|
|
850
|
+
' PARAM_USERS: tuple = (',
|
|
851
|
+
' "user",',
|
|
852
|
+
' [',
|
|
853
|
+
[[
|
|
854
|
+
' pytest.param(',
|
|
855
|
+
f' "{user}",',
|
|
856
|
+
f' id="{user}",',
|
|
857
|
+
f' ),'] for user in lines["environments"][list(lines["environments"])[0]]["user"]],
|
|
858
|
+
' ]',
|
|
859
|
+
' )',
|
|
860
|
+
' PARAM_URLS: tuple = (',
|
|
861
|
+
' "url",',
|
|
862
|
+
' [',
|
|
863
|
+
[[' pytest.param(',
|
|
864
|
+
f' "{url}",',
|
|
865
|
+
f' id="{url}",',
|
|
866
|
+
' ),'] for url in lines["environments"]],
|
|
867
|
+
' ]',
|
|
868
|
+
' )',
|
|
869
|
+
]
|
|
870
|
+
|
|
871
|
+
@classmethod
|
|
872
|
+
@Creator(path=Config.PATHS["params_api"], mode="+a", name="params_api")
|
|
873
|
+
def params_api(cls, lines: list[Endpoint]):
|
|
874
|
+
return [
|
|
875
|
+
'import pytest',
|
|
876
|
+
'from dataclasses import dataclass',
|
|
877
|
+
[['@dataclass',
|
|
878
|
+
f'class Param{Text.to_camel_case(line.name)}:',
|
|
879
|
+
[[f' PARAM_{method.name.upper()}_STATUS_{schema.status}: tuple = (',
|
|
880
|
+
' "param",',
|
|
881
|
+
' [',
|
|
882
|
+
' pytest.param(',
|
|
883
|
+
' {"sandbox": "sandbox"},',
|
|
884
|
+
' # marks=pytest.mark.skip(reason="sandbox"),',
|
|
885
|
+
' id="sandbox"',
|
|
886
|
+
' )',
|
|
887
|
+
' ]',
|
|
888
|
+
' )'] for method in line.methods for schema in method.schemas if schema.status != None]] for line in lines]
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
@classmethod
|
|
892
|
+
@Creator(path=Config.PATHS["tests_test_api"], mode="+a", name="tests_api")
|
|
893
|
+
def tests_api(cls, lines: Endpoint):
|
|
894
|
+
return [
|
|
895
|
+
'import pytest',
|
|
896
|
+
f'from {Config.PATHS["params"].replace("/", ".")}param_env import ParamEnv',
|
|
897
|
+
f'from {Config.PATHS["swagger"].replace("/", ".")}blank_client import BlankClient',
|
|
898
|
+
f'from {Config.PATHS["params_api"].replace("/", ".")}param_{Text.to_snake_case(lines.methods[0].tag)}_api import Param{Text.to_camel_case(lines.name)}',
|
|
899
|
+
[f'@pytest.mark.api_{Text.to_snake_case(lines.methods[0].tag)}',
|
|
900
|
+
'@pytest.mark.api',
|
|
901
|
+
'@pytest.mark.regression',
|
|
902
|
+
f'class Test{Text.to_camel_case(lines.name)}:',
|
|
903
|
+
[[f' @pytest.mark.asyncio_cooperative',
|
|
904
|
+
f' @pytest.mark.parametrize(*Param{Text.to_camel_case(lines.name)}.PARAM_{method.name.upper()}_STATUS_{schema.status})',
|
|
905
|
+
f' @pytest.mark.parametrize(*ParamEnv.PARAM_USERS)',
|
|
906
|
+
f' async def test_{Text.to_snake_case(method.tag)}_{method.name}_status_{schema.status}(',
|
|
907
|
+
f' self,',
|
|
908
|
+
f' client: BlankClient,',
|
|
909
|
+
f' user,',
|
|
910
|
+
f' param',
|
|
911
|
+
f' ):',
|
|
912
|
+
f' client.set_token(client=user)',
|
|
913
|
+
f' pass'] for method in lines.methods for schema in method.schemas if schema.status != None]]
|
|
914
|
+
]
|
|
915
|
+
|
|
916
|
+
@classmethod
|
|
917
|
+
@Creator(path=Config.PATHS["browser_pages"], mode="+a", name="pages")
|
|
918
|
+
def pages(cls, lines: dict[Literal["page", "components"], Page | str]):
|
|
919
|
+
return [
|
|
920
|
+
'from playwright.async_api import Page, expect',
|
|
921
|
+
[f'from {Config.PATHS["browser_components"].replace("/", ".")}component_{Text.to_snake_case(line)} import Component{Text.to_camel_case(line)}' for line in lines["page"]["components"] if line in lines["components"]],
|
|
922
|
+
f'class {Text.to_camel_case(lines["page"]["name"])}:',
|
|
923
|
+
' def __init__(self, page: Page):',
|
|
924
|
+
[f' self.{Text.to_snake_case(line.replace(f'{lines["page"]["name"]}_', ''))} = {Text.to_camel_case(line.replace(f'{lines["page"]["name"]}_', ''))}(page)' for line in lines["page"]["components"]],
|
|
925
|
+
[[f'class {Text.to_camel_case(line)}(Component{Text.to_camel_case(line)}):' if line in lines["components"] else f'class {Text.to_camel_case(line.replace(f'{lines["page"]["name"]}_', ''))}:',
|
|
926
|
+
' def __init__(self, page: Page):',
|
|
927
|
+
' super().__init__(page)' if line in lines["components"] else ' pass'] for line in lines["page"]["components"]]
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
@classmethod
|
|
931
|
+
@Creator(path=Config.PATHS["browser_components"], mode="+a", name="components")
|
|
932
|
+
def components(cls, lines: str):
|
|
933
|
+
return [
|
|
934
|
+
'from playwright.async_api import Page, expect',
|
|
935
|
+
[f'class Component{Text.to_camel_case(lines)}:',
|
|
936
|
+
' def __init__(self, page: Page):',
|
|
937
|
+
' pass']
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
@classmethod
|
|
941
|
+
@Creator(path=Config.PATHS["browser"], mode="+w", name="blank_page")
|
|
942
|
+
def blank_page(cls, lines: dict[Literal["pages", "environments"], list[Page]]):
|
|
943
|
+
return [
|
|
944
|
+
'from typing import Literal',
|
|
945
|
+
'from playwright.async_api import Page',
|
|
946
|
+
[f'from {Config.PATHS["browser_pages"].replace("/", ".")}{Text.to_snake_case(line["name"])} import {Text.to_camel_case(line["name"])}' for line in lines["pages"]],
|
|
947
|
+
'class BlankPage:',
|
|
948
|
+
f' URLS = Literal{sorted(list(set(line["url"] for line in lines["pages"])))}',
|
|
949
|
+
' def __init__(self, page: Page):',
|
|
950
|
+
' self._page = page',
|
|
951
|
+
' async def close(self):',
|
|
952
|
+
' await self._page.close()',
|
|
953
|
+
' async def go_back(self):',
|
|
954
|
+
' await self._page.go_back()',
|
|
955
|
+
' async def reload(self):',
|
|
956
|
+
' await self._page.reload()',
|
|
957
|
+
[f' async def go_to(self, *, url: URLS = "", **kwargs):',
|
|
958
|
+
' for key, value in kwargs.items():',
|
|
959
|
+
' url = url.replace(f\'{key}\', str(value))',
|
|
960
|
+
' await self._page.goto(url=url, timeout=300000)',
|
|
961
|
+
[f' self.{Text.to_snake_case(line["name"]).replace('_page', '')} = {Text.to_camel_case(line["name"])}(self._page)' for line in lines["pages"]]]
|
|
962
|
+
]
|
|
963
|
+
|
|
964
|
+
@classmethod
|
|
965
|
+
@Creator(path=Config.PATHS["tests_test_ui"], mode="+a", name="tests_ui")
|
|
966
|
+
def tests_ui(cls, lines: Literal["component", "name", "url"]):
|
|
967
|
+
return [
|
|
968
|
+
'import pytest',
|
|
969
|
+
f'from {Config.PATHS["params"].replace("/", ".")}param_env import ParamEnv',
|
|
970
|
+
f'from {Config.PATHS["browser"].replace("/", ".")}blank_page import BlankPage',
|
|
971
|
+
f'from {Config.PATHS["params_ui"].replace("/", ".")}param_{Text.to_snake_case(lines["name"])} import Param{Text.to_camel_case(lines["name"] + '_' + lines["component"].replace(f'{lines["name"]}_', ''))}',
|
|
972
|
+
f'@pytest.mark.ui_{Text.to_snake_case(lines["name"])}',
|
|
973
|
+
'@pytest.mark.ui',
|
|
974
|
+
'@pytest.mark.regression',
|
|
975
|
+
f'class Test{Text.to_camel_case(lines["name"] + '_' + lines["component"].replace(f'{lines["name"]}_', ''))}:',
|
|
976
|
+
' @pytest.mark.asyncio_cooperative',
|
|
977
|
+
f' @pytest.mark.parametrize(*Param{Text.to_camel_case(lines["name"] + '_' + lines["component"].replace(f'{lines["name"]}_', ''))}.PARAM_{lines["component"].replace(f'{lines["name"]}_', '').upper()})',
|
|
978
|
+
' @pytest.mark.parametrize(*ParamEnv.PARAM_USERS)',
|
|
979
|
+
f' async def test_{Text.to_snake_case(lines["name"] + '_' + lines["component"])}(self, page: BlankPage, user, param):',
|
|
980
|
+
f' # await page.go_to(url={Text.to_snake_case(lines["url"])})',
|
|
981
|
+
' pass'
|
|
982
|
+
]
|
|
983
|
+
|
|
984
|
+
@classmethod
|
|
985
|
+
@Creator(path=Config.PATHS["params_ui"], mode="+a", name="params_ui")
|
|
986
|
+
def params_ui(cls, lines: Page):
|
|
987
|
+
return [
|
|
988
|
+
'import pytest',
|
|
989
|
+
'from dataclasses import dataclass',
|
|
990
|
+
[['@dataclass',
|
|
991
|
+
f'class Param{Text.to_camel_case(lines["name"] + '_' + line.replace(f'{lines["name"]}_', ''))}:',
|
|
992
|
+
f' PARAM_{line.replace(f'{lines["name"]}_', '').upper()}: tuple = (',
|
|
993
|
+
' "param",',
|
|
994
|
+
' [',
|
|
995
|
+
' pytest.param(',
|
|
996
|
+
' {"sandbox": "sandbox"},',
|
|
997
|
+
' # marks=pytest.mark.skip(reason="sandbox"),',
|
|
998
|
+
' id="sandbox"',
|
|
999
|
+
' )',
|
|
1000
|
+
' ]',
|
|
1001
|
+
' )'] for line in lines["components"]]
|
|
1002
|
+
]
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def create_api_tests():
|
|
1006
|
+
Create.endpoints()
|
|
1007
|
+
Create.environments()
|
|
1008
|
+
Create.gitignore()
|
|
1009
|
+
Create.readme()
|
|
1010
|
+
Create.pytest()
|
|
1011
|
+
Create.requirements()
|
|
1012
|
+
Create.schemas()
|
|
1013
|
+
Create.blank_client()
|
|
1014
|
+
Create.conftest_api()
|
|
1015
|
+
Create.param_env()
|
|
1016
|
+
Create.params_api()
|
|
1017
|
+
Create.tests_api()
|
|
1018
|
+
|
|
1019
|
+
def create_ui_tests():
|
|
1020
|
+
if Config.UI:
|
|
1021
|
+
Create.tests_ui()
|
|
1022
|
+
Create.pages()
|
|
1023
|
+
Create.components()
|
|
1024
|
+
Create.blank_page()
|
|
1025
|
+
Create.tests_ui()
|
|
1026
|
+
Create.params_ui()
|
|
1027
|
+
Create.conftest_ui()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fasttests/__init__.py,sha256=lqxiPCQp3-14RfU8PJofWddTA4-VSUDVI0nfsMAuXik,122
|
|
2
|
+
fasttests/main.py,sha256=u5Q7c8EoOi6FaDuXDYY875hdk993fyvs5-4En_qT-nA,54451
|
|
3
|
+
fasttests-0.1.0.dist-info/METADATA,sha256=0HuJb8x_77CTwou9yClv_ktYIiFTwtlfTA-jcsMf1ko,249
|
|
4
|
+
fasttests-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
fasttests-0.1.0.dist-info/top_level.txt,sha256=2boXWSt-QXZl4qv461k_glBSfWdSv2nizM9B665ssdU,10
|
|
6
|
+
fasttests-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fasttests
|