spa-cli 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spa_cli-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: spa-cli
3
+ Version: 1.0.0
4
+ Summary: Un cli para manejar proyectos serverless en aws con python
5
+ Author: David Cuy
6
+ Author-email: david.cuy.sanchez@gmail.com
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: cookiecutter (>=2.6.0,<3.0.0)
14
+ Requires-Dist: pydantic (>=2.11.9,<3.0.0)
15
+ Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
16
+ Requires-Dist: setuptools (>=80.9.0,<81.0.0)
17
+ Requires-Dist: toml (>=0.10.2,<0.11.0)
18
+ Requires-Dist: typer (>=0.19.1,<0.20.0)
19
+ Project-URL: Documentation, https://github.com/DavidCuy/spa-cli
20
+ Project-URL: Homepage, https://github.com/DavidCuy/spa-cli
21
+ Project-URL: Repository, https://github.com/DavidCuy/spa-cli
22
+ Description-Content-Type: text/markdown
23
+
24
+
File without changes
@@ -0,0 +1,29 @@
1
+ [tool.poetry]
2
+ name = "spa-cli"
3
+ version = "1.0.0"
4
+ description = "Un cli para manejar proyectos serverless en aws con python"
5
+ authors = ["David Cuy <david.cuy.sanchez@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "spa_cli"}]
8
+ homepage = "https://github.com/DavidCuy/spa-cli"
9
+ repository = "https://github.com/DavidCuy/spa-cli"
10
+ documentation = "https://github.com/DavidCuy/spa-cli"
11
+
12
+
13
+ [tool.poetry.scripts]
14
+ spa-cli = "spa_cli.cli:app"
15
+ spa = "spa_cli.cli:app"
16
+
17
+ [tool.poetry.dependencies]
18
+ python = "^3.11"
19
+ python-dotenv = "^1.1.1"
20
+ toml = "^0.10.2"
21
+ cookiecutter = "^2.6.0"
22
+ setuptools = "^80.9.0"
23
+ pydantic = "^2.11.9"
24
+ typer = "^0.19.1"
25
+
26
+
27
+ [build-system]
28
+ requires = ["poetry-core"]
29
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,3 @@
1
+ from .cli import app
2
+
3
+ app(prog_name='spa-src')
@@ -0,0 +1,26 @@
1
+ from importlib.metadata import version as v
2
+
3
+ import typer
4
+ from dotenv import load_dotenv
5
+
6
+ from .src.project import project
7
+ from .src.model import model
8
+ from .src.endpoint import endpoint
9
+ from .src.lambda_function import lambda_function
10
+
11
+ load_dotenv()
12
+
13
+ app = typer.Typer()
14
+ app.add_typer(project.app, name='project')
15
+ # app.add_typer(model.app, name='model')
16
+ app.add_typer(endpoint.app, name='endpoint')
17
+ app.add_typer(lambda_function.app, name='lambda')
18
+
19
+
20
+ @app.callback(invoke_without_command=True)
21
+ def callback_version(version: bool = False):
22
+ """
23
+ Imprime la versión del CLI.
24
+ """
25
+ if version:
26
+ typer.echo(f'version: {v("spa-cli")}')
@@ -0,0 +1,215 @@
1
+ from enum import Enum
2
+ import os
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, TypeVar, Type, cast, Callable, List
6
+ import typer
7
+ import toml
8
+ import datetime
9
+ from dotenv import load_dotenv
10
+
11
+ class Constants(Enum):
12
+ PROJECT_TEMPLATE = "https://github.com/DavidCuy/serverless-python-application-pattern.git"
13
+ MYSQL_ENGINE = "mysql"
14
+ POSTGRESQL_ENGINE = "postgresql"
15
+
16
+ AWS_REPOSITORY = "aws"
17
+ OTHER_REPOSITORY = "other"
18
+
19
+ LOCALHOST_DB_DOCKER = "host.docker.internal"
20
+ MSSQL_SA_USER = "sa"
21
+
22
+ VALID_MODEL_TYPES = (int, str, float, datetime.datetime)
23
+ JSON_INT_DTYPE = 'int'
24
+ JSON_FLOAT_DTYPE = 'float'
25
+ JSON_STRING_DTYPE = 'str'
26
+ JSON_DATETIME_DTYPE = 'datetime'
27
+
28
+ DRIVERS = {
29
+ Constants.MYSQL_ENGINE.value: "pymysql",
30
+ Constants.POSTGRESQL_ENGINE.value: "psycopg2"
31
+ }
32
+
33
+ def get_driver_from_engine(engine: Constants):
34
+ return DRIVERS.get(engine.value, None)
35
+
36
+ SQL_PORTS_DEFAULT = {
37
+ Constants.MYSQL_ENGINE.value: "3306",
38
+ Constants.POSTGRESQL_ENGINE.value: "5432"
39
+ }
40
+
41
+ JSON_MAPPING_VALUE = {
42
+ Constants.JSON_INT_DTYPE.value: 'Column("{ColumnName}", Integer)',
43
+ Constants.JSON_STRING_DTYPE.value: 'Column("{ColumnName}", String)',
44
+ Constants.JSON_FLOAT_DTYPE.value: 'Column("{ColumnName}", Float)',
45
+ Constants.JSON_DATETIME_DTYPE.value: 'Column("{ColumnName}", DateTime)'
46
+ }
47
+
48
+ T = TypeVar("T")
49
+
50
+ def from_list(f: Callable[[Any], T], x: Any) -> List[T]:
51
+ assert isinstance(x, list)
52
+ return [f(y) for y in x]
53
+
54
+
55
+ def from_str(x: Any) -> str:
56
+ assert isinstance(x, str)
57
+ return x
58
+
59
+
60
+ def from_str_to_list(x: Any) -> List[str]:
61
+ assert isinstance(x, str)
62
+ return [x]
63
+
64
+
65
+ def from_union(fs, x):
66
+ for f in fs:
67
+ try:
68
+ return f(x)
69
+ except:
70
+ pass
71
+ assert False
72
+
73
+
74
+ def to_class(c: Type[T], x: Any) -> dict:
75
+ assert isinstance(x, c)
76
+ return cast(Any, x).to_dict()
77
+
78
+ class BaseConf:
79
+ @property
80
+ def attrs(self) -> List[str]:
81
+ """ Returns a list of the attributes of an object
82
+ Returns:
83
+ List[str]: Attributes
84
+ """
85
+ return list(filter(lambda prop: not str(prop).startswith('_'), self.__dict__.keys()))
86
+
87
+ @classmethod
88
+ def from_dict(cls_, obj: Any) -> 'Folders':
89
+ assert isinstance(obj, dict)
90
+
91
+ obj_dict = {}
92
+ attributes = list(filter(lambda prop: not str(prop).startswith('_'), (cls_).__dataclass_fields__.keys()))
93
+ for attr in attributes:
94
+ obj_dict.update({attr: from_str(obj.get(attr))})
95
+ return cls_(**obj_dict)
96
+
97
+ def to_dict(self):
98
+ output_dict = {}
99
+ for attr in self.attrs:
100
+ output_dict.update({attr: self.__getattribute__(attr)})
101
+ return output_dict
102
+
103
+ def __repr__(self) -> str:
104
+ """ Model representation
105
+ Returns:
106
+ str: Model output string formatted
107
+ """
108
+ attr_array = [f"{attr}={self.__getattribute__(attr)}" for attr in self.attrs]
109
+ args_format = ",".join(attr_array)
110
+ return f"<{type(self).__name__}({args_format})>"
111
+
112
+ @dataclass
113
+ class Files(BaseConf):
114
+ model: str
115
+ service: str
116
+ controller: str
117
+ endpoint: str
118
+ lambda_function: str
119
+ test_lambda: str
120
+ lambda_conf: str
121
+
122
+
123
+ @dataclass
124
+ class Folders(BaseConf):
125
+ models: str
126
+ services: str
127
+ controllers: str
128
+ lambdas: str
129
+ root: str
130
+ jsons: str
131
+ layers: str
132
+
133
+ @dataclass
134
+ class Definition(BaseConf):
135
+ name: str
136
+ description: str
137
+ author: str
138
+ author_email: str
139
+ base_api: str
140
+
141
+ @dataclass
142
+ class Project(BaseConf):
143
+ definition: Definition
144
+ folders: Folders
145
+
146
+ @staticmethod
147
+ def from_dict(obj: Any) -> 'Project':
148
+ assert isinstance(obj, dict)
149
+ definition = Definition.from_dict(obj.get("definition"))
150
+ folders = Folders.from_dict(obj.get("folders"))
151
+ return Project(definition, folders)
152
+
153
+ @dataclass
154
+ class Template(BaseConf):
155
+ files: Files
156
+
157
+ @staticmethod
158
+ def from_dict(obj: Any) -> 'Template':
159
+ assert isinstance(obj, dict)
160
+ files = Files.from_dict(obj.get("files"))
161
+ return Template(files)
162
+
163
+ @dataclass
164
+ class Config(BaseConf):
165
+ project: Project
166
+ template: Template
167
+
168
+ @staticmethod
169
+ def from_dict(obj: Any) -> 'Config':
170
+ assert isinstance(obj, dict)
171
+ project = Project.from_dict(obj.get("project"))
172
+ template = Template.from_dict(obj.get("template"))
173
+ return Config(project, template)
174
+
175
+
176
+ def load_config(path="spa_project.toml") -> Config:
177
+ config_path = Path(Path.cwd() / path)
178
+ if not config_path.exists():
179
+ typer.echo("config file not found in the project.", color=typer.colors.YELLOW)
180
+ load_dotenv(".env")
181
+ app_name = os.getenv("app_name") or ""
182
+ config_path.write_text(f"""[spa.project.definition]
183
+ name = "{app_name}"
184
+ description = ""
185
+ author = "David Cuy"
186
+ author_email = ""
187
+ base_api = "api.yaml"
188
+
189
+ [spa.template.files]
190
+ model = ".spa/templates/models/model.txt"
191
+ service = ".spa/templates/models/service.txt"
192
+ controller = ".spa/templates/models/controller.txt"
193
+ endpoint = ".spa/templates/lambda_endpoint.txt"
194
+ lambda_function = ".spa/templates/lambda.txt"
195
+ test_lambda = ".spa/templates/test_lambda_function.txt"
196
+ lambda_conf = ".spa/templates/lambda_conf.txt"
197
+
198
+ [spa.project.folders]
199
+ root = "src"
200
+ models = "src/layers/databases/python/core_db/models"
201
+ services = "src/layers/databases/python/core_db/services"
202
+ controllers = "src/layers/core/python/core_http/controllers"
203
+ jsons = ".spa/templates/json"
204
+ lambdas = "src/lambdas"
205
+ layers = "src/layers"
206
+ """)
207
+ typer.echo(
208
+ f"Created config file at {config_path} in this path you can find all configuration for the project here.")
209
+ typer.echo(f"Please add the file {config_path} to git tracking and commit it")
210
+ try:
211
+ toml_config = toml.loads(config_path.read_text())
212
+ except Exception as e:
213
+ typer.echo("Error reading the config file, please check the file format.", color=typer.colors.RED)
214
+ raise
215
+ return Config.from_dict(toml_config["spa"])
File without changes
File without changes
@@ -0,0 +1,75 @@
1
+ from ...globals import load_config
2
+ from ..utils.folders import validate_path_not_exist
3
+ from ..utils.strings import camel_case
4
+ from ..utils.template_gen import copy_template_file
5
+
6
+ import os
7
+ import typer
8
+ from pathlib import Path
9
+
10
+ app = typer.Typer()
11
+
12
+ @app.command('add')
13
+ def new_endpoint(
14
+ method: str = typer.Option(..., help='Metodo de la peticion. Valores permitidos [GET, POST, PUT, PATCH, DELETE]'),
15
+ path: str = typer.Option(..., help='Path del endpoint.'),
16
+ endpoint_name: str = typer.Option(help='Nombre de la funcion lambda.')
17
+ ):
18
+
19
+ if "-" in endpoint_name or " " in endpoint_name:
20
+ typer.echo('El nombre de la lambda no debe contener espacios o guiones. Se modificará por guiones bajos.', color=typer.colors.YELLOW)
21
+ endpoint_name = endpoint_name.replace("-", "_").replace(" ", "_")
22
+
23
+ config = load_config()
24
+ camel_name = camel_case(endpoint_name)
25
+
26
+ lambda_template_path = Path(config.template.files.lambda_function)
27
+ lambda_test_template_path = Path(config.template.files.test_lambda)
28
+ lambda_conf_template_path = Path(config.template.files.lambda_conf)
29
+ lambda_endpoint_template_path = Path(config.template.files.endpoint)
30
+
31
+ lambda_output_folder_path = Path(config.project.folders.lambdas)
32
+
33
+ try:
34
+ model_exists = validate_path_not_exist(path=lambda_output_folder_path.joinpath(endpoint_name), custom_error_message=f'Ya existe una ruta con nombre: {endpoint_name}', abort=False)
35
+ except Exception as e:
36
+ typer.echo(str(e))
37
+
38
+ if not model_exists:
39
+ os.mkdir(lambda_output_folder_path.joinpath(endpoint_name))
40
+
41
+ copy_template_file(
42
+ template_path=lambda_template_path,
43
+ destination_path=lambda_output_folder_path.joinpath(endpoint_name).joinpath('lambda_function.py'),
44
+ code_format_override={}
45
+ )
46
+
47
+ copy_template_file(
48
+ template_path=lambda_test_template_path,
49
+ destination_path=lambda_output_folder_path.joinpath(endpoint_name).joinpath('test_lambda_function.py'),
50
+ code_format_override={
51
+ "camel_name": camel_name,
52
+ "lambda_name": endpoint_name
53
+ }
54
+ )
55
+
56
+ copy_template_file(
57
+ template_path=lambda_conf_template_path,
58
+ destination_path=lambda_output_folder_path.joinpath(endpoint_name).joinpath('infra_config.py'),
59
+ code_format_override={
60
+ "lambda_name": endpoint_name,
61
+ "camel_name": camel_name
62
+ }
63
+ )
64
+
65
+ copy_template_file(
66
+ template_path=lambda_endpoint_template_path,
67
+ destination_path=lambda_output_folder_path.joinpath(endpoint_name).joinpath('endpoint.yaml'),
68
+ code_format_override={
69
+ "endpoint_url": path,
70
+ "endpoint_method": method.lower(),
71
+ }
72
+ )
73
+
74
+ typer.echo(f'La ruta {path} [{method}] se agrego correctamente!', color=typer.colors.GREEN)
75
+
File without changes
@@ -0,0 +1,62 @@
1
+ from ...globals import load_config
2
+ from ..utils.folders import validate_path_not_exist
3
+ from ..utils.strings import camel_case
4
+ from ..utils.template_gen import copy_template_file
5
+
6
+ import os
7
+ import typer
8
+ from pathlib import Path
9
+
10
+ app = typer.Typer()
11
+
12
+ @app.command('add')
13
+ def new_lambda(
14
+ lambda_name: str = typer.Option(help='Nombre de la funcion lambda.')
15
+ ):
16
+ config = load_config()
17
+
18
+ if "-" in lambda_name or " " in lambda_name:
19
+ typer.echo('El nombre de la lambda no debe contener espacios o guiones. Se modificará por guiones bajos.', color=typer.colors.YELLOW)
20
+ lambda_name = lambda_name.replace("-", "_").replace(" ", "_")
21
+ camel_name = camel_case(lambda_name)
22
+
23
+ lambda_template_path = Path(config.template.files.lambda_function)
24
+ lambda_test_template_path = Path(config.template.files.test_lambda)
25
+ lambda_conf_template_path = Path(config.template.files.lambda_conf)
26
+
27
+ lambda_output_folder_path = Path(config.project.folders.lambdas)
28
+
29
+ try:
30
+ model_exists = validate_path_not_exist(path=lambda_output_folder_path.joinpath(lambda_name), custom_error_message=f'Ya existe una ruta con nombre: {lambda_name}', abort=False)
31
+ except Exception as e:
32
+ typer.echo(str(e))
33
+
34
+ if not model_exists:
35
+ os.mkdir(lambda_output_folder_path.joinpath(lambda_name))
36
+
37
+ copy_template_file(
38
+ template_path=lambda_template_path,
39
+ destination_path=lambda_output_folder_path.joinpath(lambda_name).joinpath('lambda_function.py'),
40
+ code_format_override={}
41
+ )
42
+
43
+ copy_template_file(
44
+ template_path=lambda_test_template_path,
45
+ destination_path=lambda_output_folder_path.joinpath(lambda_name).joinpath('test_lambda_function.py'),
46
+ code_format_override={
47
+ "camel_name": camel_name,
48
+ "lambda_name": lambda_name
49
+ }
50
+ )
51
+
52
+ copy_template_file(
53
+ template_path=lambda_conf_template_path,
54
+ destination_path=lambda_output_folder_path.joinpath(lambda_name).joinpath('infra_config.py'),
55
+ code_format_override={
56
+ "lambda_name": lambda_name,
57
+ "camel_name": camel_name
58
+ }
59
+ )
60
+
61
+ typer.echo(f'La lambda {lambda_name} se agrego correctamente!', color=typer.colors.GREEN)
62
+
File without changes
@@ -0,0 +1,140 @@
1
+ from ...globals import load_config, Config, Constants, JSON_MAPPING_VALUE
2
+ from ..utils.folders import validate_path_not_exist, validate_path_exist
3
+ from ..utils.strings import camel_case, snake_case
4
+ from ..utils.template_gen import add_code_to_module, add_file_to_module, read_project_config
5
+
6
+ import json
7
+ import typer
8
+ from typing import cast
9
+ from pathlib import Path
10
+ from dateutil import parser
11
+ from typing_extensions import Annotated
12
+
13
+ app = typer.Typer()
14
+
15
+ @app.command('new')
16
+ def new_model(name: str = typer.Option(..., help='Nombre del nuevo modelo.'), tablename: str = typer.Option(help='Nombre de la tabla', default='same-model-name')):
17
+ config = load_config()
18
+ name = camel_case(name)
19
+ tablename = camel_case(name if tablename == 'same-model-name' else tablename)
20
+ models_folder_path = Path(config.project.folders.models)
21
+
22
+ validate_path_not_exist(path=models_folder_path.joinpath(f'{name}.py'), custom_error_message=f'Ya existe un modelo con nombre: {name}')
23
+ __generate_full_model_config(config, name, tablename)
24
+
25
+ typer.echo(f'{name} se agrego correctamente!', color=typer.colors.GREEN)
26
+
27
+
28
+
29
+ @app.command('fromJson')
30
+ def new_from_json(
31
+ name: str = typer.Option(..., help='Nombre del archivo JSON a leer. Sin extension'),
32
+ tablename: str = typer.Option(help='Nombre de la tabla', default='same-model-name')
33
+ ):
34
+ """
35
+ Crea un nuevo modelo de acuerdo con el nombre especificado y el archivo en la ruta "./.spa/templates/json"
36
+ """
37
+ config = load_config()
38
+ try:
39
+ project_config = read_project_config()
40
+ except:
41
+ typer.echo('No se puedo leer la configuracion del proyecto', color=typer.colors.RED)
42
+ raise typer.Abort()
43
+ name = camel_case(name)
44
+ tablename = camel_case(name if tablename == 'same-model-name' else tablename)
45
+ json_folder_path = Path(config.project.folders.jsons)
46
+ validate_path_exist(path=json_folder_path.joinpath(f'{name.lower()}.json'), custom_error_message=f'No se pudo encontrar el archivo {name.lower()}.json')
47
+
48
+ try:
49
+ model_map = cast(dict, json.loads(json_folder_path.joinpath(f'{name.lower()}.json').read_text()))
50
+ except Exception as e:
51
+ typer.echo('No se pudo leer el json correctamente', typer.colors.RED)
52
+ typer.Abort()
53
+
54
+ __generate_full_model_config(config, name, tablename)
55
+
56
+ full_column_text = ''
57
+ full_propmap_text = ''
58
+ full_display_member = ''
59
+ for key in model_map.keys():
60
+ if isinstance(model_map[key], Constants.VALID_MODEL_TYPES.value):
61
+ key_type = type(model_map[key]).__name__
62
+
63
+ if key_type == Constants.JSON_STRING_DTYPE.value:
64
+ try:
65
+ is_date = bool(parser.parse(model_map[key]))
66
+ if is_date:
67
+ key_type = Constants.JSON_DATETIME_DTYPE.value
68
+ except ValueError:
69
+ # 'String is not datetime format'
70
+ pass
71
+
72
+ data_column_line_text = f"""
73
+ {key} = {JSON_MAPPING_VALUE[key_type].format(**{'ColumnName': camel_case(key)})}"""
74
+ #here replace
75
+ if key_type == Constants.JSON_STRING_DTYPE.value:
76
+ if project_config['dbDialect'] == Constants.MYSQL_ENGINE.value:
77
+ data_column_line_text = data_column_line_text.replace('String', 'String(512)')
78
+ full_column_text += data_column_line_text
79
+ full_propmap_text += f""",
80
+ "{key}": "{camel_case(key)}\""""
81
+ full_display_member += f""",
82
+ "{key}\""""
83
+
84
+ models_folder_path = Path(config.project.folders.models)
85
+ model_text = models_folder_path.joinpath(f'{name}.py').read_text()
86
+ search_text_id = f'id = Column("Id{name}", Integer, primary_key=True)'
87
+ search_text_propmap = f"""def property_map(self) -> Dict:
88
+ return """ + '{' + f"""
89
+ "id": "Id{name}\""""
90
+ search_text_display = """def display_members(cls_) -> List[str]:
91
+ return [
92
+ "id\""""
93
+
94
+ search_id = model_text.index(search_text_id) + len(search_text_id)
95
+ search_propmap = model_text.index(search_text_propmap) + len(search_text_propmap)
96
+ search_display = model_text.index(search_text_display) + len(search_text_display)
97
+
98
+ model_text = model_text[:search_id] \
99
+ + full_column_text \
100
+ + model_text[search_id:search_propmap] \
101
+ + full_propmap_text \
102
+ + model_text[search_propmap:search_display] \
103
+ + full_display_member \
104
+ + model_text[search_display:]
105
+ models_folder_path.joinpath(f'{name}.py').write_text(model_text)
106
+
107
+
108
+ typer.echo(f'{name} se agrego correctamente!', color=typer.colors.GREEN)
109
+
110
+ def __generate_full_model_config(config: Config, name: str, tablename: str):
111
+ models_folder_path = Path(config.project.folders.models)
112
+ template_model_path = Path(config.template.files.model)
113
+ add_code_to_module(template_model_path, models_folder_path, name, {'model_name': name, 'model_name_lower': name.lower(), 'table_name': tablename})
114
+ add_file_to_module(models_folder_path, name)
115
+
116
+ service_template_path = Path(config.template.files.service)
117
+ service_folder_path = Path(config.project.folders.services)
118
+ add_code_to_module(service_template_path, service_folder_path, f"{name}Service", {'model_name': name})
119
+ add_file_to_module(service_folder_path, f"{name}Service")
120
+
121
+ controller_template_path = Path(config.template.files.controller)
122
+ controller_folder_path = Path(config.project.folders.controllers)
123
+ add_code_to_module(controller_template_path, controller_folder_path, f"{name}Controller", {})
124
+
125
+ routes_template_path = Path(config.template.files.endpoint)
126
+ routes_folder_path = Path(config.project.folders.endpoints)
127
+ add_code_to_module(routes_template_path, routes_folder_path, f"{name}Router", {'model_name': name, 'model_name_lower': name.lower()})
128
+ add_file_to_module(routes_folder_path, f"{name}Router", f"{name.lower()}_router")
129
+
130
+ index_code = Path(config.project.folders.root).joinpath('__init__.py').read_text()
131
+
132
+ search_return = index_code.index('return app')
133
+
134
+ block_insert_code = f"""from .routes import {name.lower()}_router
135
+ app.register_blueprint({name.lower()}_router, url_prefix='/{name.lower()}')
136
+
137
+ """
138
+
139
+ index_code = index_code[:search_return] + block_insert_code + index_code[search_return:]
140
+ Path(config.project.folders.root).joinpath('__init__.py').write_text(index_code)
File without changes