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 ADDED
@@ -0,0 +1,3 @@
1
+ from .main import Config, create_api_tests, create_ui_tests
2
+
3
+ __all__ = ['Config', 'create_api_tests', 'create_ui_tests']
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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: fasttests
3
+ Version: 0.1.0
4
+ Summary: library for generating automated test templates
5
+ Author-email: Alexander Tomonov <tomonow.a@yandex.ru>
6
+ License: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ fasttests