bisslog-flask 0.0.1__py3-none-any.whl → 0.0.3__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.
- bisslog_flask/builder/__init__.py +0 -0
- bisslog_flask/builder/builder_flask_app_manager.py +371 -0
- bisslog_flask/builder/static_python_construct_data.py +172 -0
- bisslog_flask/cli/__init__.py +90 -0
- bisslog_flask/cli/commands/__init__.py +0 -0
- bisslog_flask/cli/commands/build.py +57 -0
- bisslog_flask/cli/commands/run.py +52 -0
- bisslog_flask/initializer/bisslog_flask_http_resolver.py +53 -12
- bisslog_flask/initializer/bisslog_flask_resolver.py +0 -1
- bisslog_flask/initializer/bisslog_flask_ws_resolver.py +25 -9
- bisslog_flask/initializer/init_flask_app_manager.py +16 -2
- bisslog_flask/socket_helper/socket_helper.py +0 -3
- {bisslog_flask-0.0.1.dist-info → bisslog_flask-0.0.3.dist-info}/METADATA +59 -44
- bisslog_flask-0.0.3.dist-info/RECORD +21 -0
- bisslog_flask-0.0.3.dist-info/entry_points.txt +2 -0
- bisslog_flask-0.0.1.dist-info/RECORD +0 -13
- {bisslog_flask-0.0.1.dist-info → bisslog_flask-0.0.3.dist-info}/WHEEL +0 -0
- {bisslog_flask-0.0.1.dist-info → bisslog_flask-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {bisslog_flask-0.0.1.dist-info → bisslog_flask-0.0.3.dist-info}/top_level.txt +0 -0
File without changes
|
@@ -0,0 +1,371 @@
|
|
1
|
+
"""
|
2
|
+
Module for generating a Flask application boilerplate from Bisslog metadata and use case code.
|
3
|
+
|
4
|
+
This builder analyzes declared metadata (e.g., triggers) and discovered use case implementations,
|
5
|
+
and generates the corresponding Flask code—including HTTP routes, WebSocket endpoints, security
|
6
|
+
configuration, and runtime setup.
|
7
|
+
|
8
|
+
The generated code is returned as a full Python script and can be written to a file (e.g.,
|
9
|
+
`flask_app.py`).
|
10
|
+
"""
|
11
|
+
|
12
|
+
from typing import Optional, Callable
|
13
|
+
import json
|
14
|
+
|
15
|
+
from bisslog_schema import read_full_service_metadata
|
16
|
+
from bisslog_schema.eager_import_module_or_package import EagerImportModulePackage
|
17
|
+
from bisslog_schema.schema import UseCaseInfo, TriggerHttp, TriggerWebsocket
|
18
|
+
from bisslog_schema.setup import get_setup_metadata
|
19
|
+
from bisslog_schema.use_case_code_inspector.use_case_code_metadata import UseCaseCodeInfo, \
|
20
|
+
UseCaseCodeInfoClass, UseCaseCodeInfoObject
|
21
|
+
|
22
|
+
from .static_python_construct_data import StaticPythonConstructData
|
23
|
+
|
24
|
+
|
25
|
+
class BuilderFlaskAppManager:
|
26
|
+
"""
|
27
|
+
Flask application builder for Bisslog-based services.
|
28
|
+
|
29
|
+
This class dynamically generates Flask code based on user-declared metadata and
|
30
|
+
the implementation of use cases discovered in the source tree. It supports HTTP
|
31
|
+
and WebSocket triggers, integrates runtime setup from decorators, and configures
|
32
|
+
environment-based security.
|
33
|
+
|
34
|
+
The result is a complete Flask application scaffold that can be directly executed
|
35
|
+
or used as a starting point for further customization.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(self, eager_importer: Callable[[str], None]):
|
39
|
+
self._eager_importer = eager_importer
|
40
|
+
|
41
|
+
|
42
|
+
def _get_bisslog_setup(self, infra_path: Optional[str]) -> Optional[StaticPythonConstructData]:
|
43
|
+
"""
|
44
|
+
Retrieves the Bisslog setup call for the 'flask' runtime, if defined.
|
45
|
+
|
46
|
+
This inspects the global Bisslog configuration and returns the corresponding
|
47
|
+
setup function call code for Flask.
|
48
|
+
|
49
|
+
Returns
|
50
|
+
-------
|
51
|
+
Optional[StaticPythonConstructData]
|
52
|
+
The setup code and imports, or None if no setup was declared.
|
53
|
+
"""
|
54
|
+
self._eager_importer(infra_path)
|
55
|
+
setup_metadata = get_setup_metadata()
|
56
|
+
if setup_metadata is None:
|
57
|
+
return None
|
58
|
+
|
59
|
+
if setup_metadata.setup_function is not None:
|
60
|
+
n_params = setup_metadata.setup_function.n_params
|
61
|
+
if n_params == 0:
|
62
|
+
build = f"{setup_metadata.setup_function.function_name}()"
|
63
|
+
elif n_params == 1:
|
64
|
+
build = f"{setup_metadata.setup_function.function_name}(\"flask\")"
|
65
|
+
else:
|
66
|
+
build = (f"{setup_metadata.setup_function.function_name}(\"flask\")"
|
67
|
+
" # TODO: change this")
|
68
|
+
return StaticPythonConstructData(
|
69
|
+
importing={setup_metadata.setup_function.module:
|
70
|
+
{setup_metadata.setup_function.function_name}},
|
71
|
+
build=build,
|
72
|
+
)
|
73
|
+
custom_runtime_setup = setup_metadata.runtime.get("flask", None)
|
74
|
+
if custom_runtime_setup is not None:
|
75
|
+
return StaticPythonConstructData(
|
76
|
+
importing={custom_runtime_setup.module:
|
77
|
+
{custom_runtime_setup.function_name}},
|
78
|
+
build=f"{custom_runtime_setup.function_name}()"
|
79
|
+
)
|
80
|
+
return None
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def _generate_security_code() -> StaticPythonConstructData:
|
84
|
+
"""
|
85
|
+
Generates Flask configuration code for secret keys using environment variables.
|
86
|
+
|
87
|
+
Returns
|
88
|
+
-------
|
89
|
+
StaticPythonConstructData
|
90
|
+
Code that assigns `SECRET_KEY` and `JWT_SECRET_KEY` to the Flask app.
|
91
|
+
"""
|
92
|
+
build = """
|
93
|
+
if "SECRET_KEY" in os.environ:
|
94
|
+
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
|
95
|
+
if "JWT_SECRET_KEY" in os.environ:
|
96
|
+
app.config["JWT_SECRET_KEY"] = os.environ["JWT_SECRET_KEY"]
|
97
|
+
"""
|
98
|
+
return StaticPythonConstructData(build=build)
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _generate_use_case_code_build(use_case_code_info: UseCaseCodeInfo):
|
102
|
+
"""
|
103
|
+
Prepares the use case callable to be used in HTTP or WebSocket routes.
|
104
|
+
|
105
|
+
If the use case is a class, an instance is created. If it's an object, it's referenced.
|
106
|
+
|
107
|
+
Parameters
|
108
|
+
----------
|
109
|
+
use_case_code_info : UseCaseCodeInfo
|
110
|
+
Static metadata about the use case implementation.
|
111
|
+
|
112
|
+
Returns
|
113
|
+
-------
|
114
|
+
Tuple[str, StaticPythonConstructData]
|
115
|
+
- Name of the callable reference (e.g., variable or instance).
|
116
|
+
- Generated setup code and required imports.
|
117
|
+
"""
|
118
|
+
importing = {"flask": {"request"}, "bisslog.utils.mapping": {"Mapper"}}
|
119
|
+
starting_build = ""
|
120
|
+
if isinstance(use_case_code_info, UseCaseCodeInfoClass):
|
121
|
+
importing[use_case_code_info.module] = {use_case_code_info.class_name}
|
122
|
+
uc_callable = f"{use_case_code_info.name}_uc"
|
123
|
+
starting_build += f"{uc_callable} = {use_case_code_info.class_name}()"
|
124
|
+
elif isinstance(use_case_code_info, UseCaseCodeInfoObject):
|
125
|
+
importing[use_case_code_info.module] = {use_case_code_info.var_name}
|
126
|
+
uc_callable = use_case_code_info.var_name
|
127
|
+
else:
|
128
|
+
raise ValueError("Unsupported UseCaseCodeInfo type")
|
129
|
+
return uc_callable, StaticPythonConstructData(build=starting_build, importing=importing)
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def _generate_use_case_code_http_trigger(
|
133
|
+
use_case_key: str, uc_callable: str, use_case_code_info: UseCaseCodeInfo,
|
134
|
+
trigger_info: TriggerHttp, identifier: int) -> StaticPythonConstructData:
|
135
|
+
"""
|
136
|
+
Generates the code for a use case with an HTTP trigger.
|
137
|
+
|
138
|
+
Parameters
|
139
|
+
----------
|
140
|
+
use_case_key : str
|
141
|
+
Name used to identify the use case route.
|
142
|
+
use_case_code_info : UseCaseCodeInfo
|
143
|
+
Static code metadata for the specific use case.
|
144
|
+
trigger_info : TriggerHttp
|
145
|
+
Metadata of the HTTP trigger.
|
146
|
+
|
147
|
+
Returns
|
148
|
+
-------
|
149
|
+
StaticPythonConstructData
|
150
|
+
The generated code for the HTTP trigger.
|
151
|
+
"""
|
152
|
+
imports = {
|
153
|
+
"flask": {"jsonify"}
|
154
|
+
}
|
155
|
+
starting_build = ""
|
156
|
+
mapper_code_lines = []
|
157
|
+
if trigger_info.mapper is not None:
|
158
|
+
mapper_name = f"{use_case_code_info.name}_mapper_{identifier}"
|
159
|
+
starting_build += (f"\n{mapper_name} = Mapper(name=\"{use_case_key}_mapper\", "
|
160
|
+
f"base={json.dumps(trigger_info.mapper)})")
|
161
|
+
mapper_code_lines.append(f"""
|
162
|
+
res_map = {mapper_name}.map({{
|
163
|
+
"path_query": route_vars,
|
164
|
+
"body": request.get_json(silent=True) or {{}},
|
165
|
+
"params": request.args.to_dict(),
|
166
|
+
"headers": request.headers,
|
167
|
+
}})""")
|
168
|
+
method = trigger_info.method.upper()
|
169
|
+
flask_path = (trigger_info.path or f"/{use_case_key}").replace("{", "<").replace("}", ">")
|
170
|
+
handler_name = f"{use_case_key}_handler_{identifier}"
|
171
|
+
|
172
|
+
lines = [
|
173
|
+
f'@app.route("{flask_path}", methods=["{method}"])',
|
174
|
+
]
|
175
|
+
|
176
|
+
if use_case_code_info.is_coroutine:
|
177
|
+
lines.append(f"async def {handler_name}(**route_vars):")
|
178
|
+
else:
|
179
|
+
lines.append(f"def {handler_name}(**route_vars):")
|
180
|
+
|
181
|
+
if not mapper_code_lines:
|
182
|
+
lines.append(" kwargs = {}")
|
183
|
+
lines.append(" kwargs.update(route_vars)")
|
184
|
+
lines.append(" kwargs.update(request.get_json(silent=True) or {})")
|
185
|
+
lines.append(" kwargs.update(request.args.to_dict())")
|
186
|
+
lines.append(" kwargs.update(dict(request.headers))")
|
187
|
+
var_to_unpack = "kwargs"
|
188
|
+
else:
|
189
|
+
lines.extend(mapper_code_lines)
|
190
|
+
var_to_unpack = "res_map"
|
191
|
+
|
192
|
+
if use_case_code_info.is_coroutine:
|
193
|
+
lines.append(f' result = await {uc_callable}(**{var_to_unpack})')
|
194
|
+
else:
|
195
|
+
lines.append(f' result = {uc_callable}(**{var_to_unpack})\n')
|
196
|
+
|
197
|
+
lines.append(' return jsonify(result)\n')
|
198
|
+
|
199
|
+
return StaticPythonConstructData(build=starting_build,
|
200
|
+
body="\n".join(lines), importing=imports)
|
201
|
+
|
202
|
+
@staticmethod
|
203
|
+
def _generate_use_case_code_websocket_trigger(
|
204
|
+
use_case_key: str,
|
205
|
+
uc_callable: str,
|
206
|
+
use_case_code_info: UseCaseCodeInfo,
|
207
|
+
trigger_info: TriggerWebsocket,
|
208
|
+
identifier: int
|
209
|
+
) -> StaticPythonConstructData:
|
210
|
+
"""
|
211
|
+
Generates the code for a use case with a WebSocket trigger using flask-sock.
|
212
|
+
|
213
|
+
Parameters
|
214
|
+
----------
|
215
|
+
use_case_key : str
|
216
|
+
The identifier of the use case.
|
217
|
+
uc_callable : str
|
218
|
+
The callable name to invoke.
|
219
|
+
use_case_code_info : UseCaseCodeInfo
|
220
|
+
Info about where the use case is defined.
|
221
|
+
trigger_info : TriggerWebsocket
|
222
|
+
Metadata describing the trigger.
|
223
|
+
identifier : int
|
224
|
+
An integer used to ensure uniqueness of function names.
|
225
|
+
|
226
|
+
Returns
|
227
|
+
-------
|
228
|
+
StaticPythonConstructData
|
229
|
+
Code and imports needed for WebSocket registration.
|
230
|
+
"""
|
231
|
+
route_key = trigger_info.route_key or f"{use_case_key}.default"
|
232
|
+
handler_name = f"{use_case_key}_ws_handler_{identifier}"
|
233
|
+
mapper_decl = ""
|
234
|
+
|
235
|
+
imports = {
|
236
|
+
use_case_code_info.module: {use_case_code_info.name},
|
237
|
+
"flask_sock": {"Sock"},
|
238
|
+
"flask": {"request", "jsonify"},
|
239
|
+
"bisslog.utils.mapping": {"Mapper"},
|
240
|
+
"json": None
|
241
|
+
}
|
242
|
+
|
243
|
+
if trigger_info.mapper:
|
244
|
+
mapper_var = f"{use_case_key}_ws_mapper_{identifier}"
|
245
|
+
mapper_json = json.dumps(trigger_info.mapper)
|
246
|
+
mapper_decl = (f'\n{mapper_var} = Mapper(name="{use_case_key}_ws_mapper",'
|
247
|
+
f' base={mapper_json})')
|
248
|
+
|
249
|
+
mapper_code = f"""
|
250
|
+
try:
|
251
|
+
body = json.loads(data)
|
252
|
+
except Exception:
|
253
|
+
body = {{}}
|
254
|
+
res_map = {mapper_var}.map({{
|
255
|
+
"route_key": "{route_key}",
|
256
|
+
"connection_id": request.headers.get("Sec-WebSocket-Key"),
|
257
|
+
"headers": request.headers,
|
258
|
+
"body": body
|
259
|
+
}})
|
260
|
+
response = {uc_callable}(**res_map)
|
261
|
+
"""
|
262
|
+
|
263
|
+
else:
|
264
|
+
# fallback: pass entire raw message
|
265
|
+
mapper_code = f"""
|
266
|
+
try:
|
267
|
+
payload = json.loads(data)
|
268
|
+
except Exception:
|
269
|
+
payload = {{}}
|
270
|
+
response = {uc_callable}(**payload)
|
271
|
+
"""
|
272
|
+
|
273
|
+
build = f"""
|
274
|
+
@sock.route("/ws/{route_key}")
|
275
|
+
def {handler_name}(ws):
|
276
|
+
while True:
|
277
|
+
data = ws.receive()
|
278
|
+
if data is None:
|
279
|
+
break; # Client disconnected
|
280
|
+
{mapper_code}
|
281
|
+
if response is not None:
|
282
|
+
ws.send(response)
|
283
|
+
"""
|
284
|
+
|
285
|
+
return StaticPythonConstructData(
|
286
|
+
importing=imports,
|
287
|
+
build=(mapper_decl + build)
|
288
|
+
)
|
289
|
+
|
290
|
+
def __call__(self,
|
291
|
+
metadata_file: Optional[str] = None,
|
292
|
+
use_cases_folder_path: Optional[str] = None,
|
293
|
+
infra_path: Optional[str] = None,
|
294
|
+
*,
|
295
|
+
encoding: str = "utf-8",
|
296
|
+
secret_key: Optional[str] = None,
|
297
|
+
jwt_secret_key: Optional[str] = None,
|
298
|
+
**kwargs) -> str:
|
299
|
+
"""
|
300
|
+
Main entry point for generating the full Flask application code.
|
301
|
+
|
302
|
+
This method orchestrates metadata loading, trigger processing, and Flask code generation
|
303
|
+
(HTTP routes, WebSocket handlers, runtime setup, security config). The resulting app code
|
304
|
+
is returned as a ready-to-write Python string.
|
305
|
+
|
306
|
+
Parameters
|
307
|
+
----------
|
308
|
+
metadata_file : str, optional
|
309
|
+
Path to the YAML or JSON metadata file.
|
310
|
+
use_cases_folder_path : str, optional
|
311
|
+
Path to the folder where use case implementations are located.
|
312
|
+
infra_path : str, optional
|
313
|
+
Path to additional infrastructure or adapter code.
|
314
|
+
encoding : str, default="utf-8"
|
315
|
+
Encoding used to read the metadata file.
|
316
|
+
secret_key : str, optional
|
317
|
+
secret key for Flask configuration.
|
318
|
+
jwt_secret_key : str, optional
|
319
|
+
JWT secret key for Flask configuration.
|
320
|
+
**kwargs
|
321
|
+
Additional keyword arguments (currently unused).
|
322
|
+
|
323
|
+
Returns
|
324
|
+
-------
|
325
|
+
str
|
326
|
+
The complete Flask application source code as a string.
|
327
|
+
"""
|
328
|
+
full_service_metadata = read_full_service_metadata(
|
329
|
+
metadata_file=metadata_file,
|
330
|
+
use_cases_folder_path=use_cases_folder_path,
|
331
|
+
encoding=encoding
|
332
|
+
)
|
333
|
+
service_info = full_service_metadata.declared_metadata
|
334
|
+
use_cases = full_service_metadata.discovered_use_cases
|
335
|
+
|
336
|
+
res = StaticPythonConstructData(
|
337
|
+
importing={"flask": {"Flask"}, "os": None},
|
338
|
+
build="app = Flask(__name__)"
|
339
|
+
)
|
340
|
+
res += self._get_bisslog_setup(infra_path)
|
341
|
+
|
342
|
+
res += self._generate_security_code()
|
343
|
+
|
344
|
+
# Use cases
|
345
|
+
for use_case_key in service_info.use_cases:
|
346
|
+
use_case_info: UseCaseInfo = service_info.use_cases[use_case_key]
|
347
|
+
use_case_code_info: UseCaseCodeInfo = use_cases[use_case_key]
|
348
|
+
triggers_http = [t for t in use_case_info.triggers
|
349
|
+
if isinstance(t.options, TriggerHttp)]
|
350
|
+
triggers_ws = [t for t in use_case_info.triggers
|
351
|
+
if isinstance(t.options, TriggerWebsocket)]
|
352
|
+
triggers_flask = triggers_http + triggers_ws
|
353
|
+
if len(triggers_flask) == 0:
|
354
|
+
continue
|
355
|
+
uc_callable, res_uc = self._generate_use_case_code_build(use_case_code_info)
|
356
|
+
res += res_uc
|
357
|
+
for i, trigger in enumerate(triggers_flask):
|
358
|
+
if isinstance(trigger.options, TriggerHttp):
|
359
|
+
res += self._generate_use_case_code_http_trigger(
|
360
|
+
use_case_key, uc_callable, use_case_code_info, trigger.options, i
|
361
|
+
)
|
362
|
+
elif isinstance(trigger.options, TriggerWebsocket):
|
363
|
+
res += self._generate_use_case_code_websocket_trigger(
|
364
|
+
use_case_key, uc_callable, use_case_code_info, trigger.options, i
|
365
|
+
)
|
366
|
+
res += StaticPythonConstructData(body='\nif __name__ == "__main__":\n'
|
367
|
+
' app.run(debug=True, host="0.0.0.0")')
|
368
|
+
return res.generate_boiler_plate_flask()
|
369
|
+
|
370
|
+
|
371
|
+
bisslog_flask_builder = BuilderFlaskAppManager(EagerImportModulePackage(("src.infra", "infra")))
|
@@ -0,0 +1,172 @@
|
|
1
|
+
"""
|
2
|
+
Module defining the response structure for AWS Lambda handler code generation.
|
3
|
+
|
4
|
+
This module provides a dataclass that encapsulates the components produced during
|
5
|
+
AWS Lambda handler code generation, including the function body, optional setup code,
|
6
|
+
and required import statements.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from typing import Optional, Dict, Any, Set
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class StaticPythonConstructData:
|
16
|
+
"""
|
17
|
+
Response structure for generated AWS Lambda handler components.
|
18
|
+
|
19
|
+
This dataclass represents the output of a code generator that produces AWS Lambda
|
20
|
+
handler functions. It contains the function body as a string, any optional setup/build
|
21
|
+
logic, and the necessary import statements grouped by module.
|
22
|
+
|
23
|
+
Attributes
|
24
|
+
----------
|
25
|
+
body : Optional[str]
|
26
|
+
The body of the Lambda handler function as a Python code string.
|
27
|
+
build : Optional[str]
|
28
|
+
Optional setup or preconstruction code to include before the function body.
|
29
|
+
importing : Dict[str, Set[str]]
|
30
|
+
A mapping of module names to sets of symbols to import from them.
|
31
|
+
|
32
|
+
This structure assumes simple `from module import symbol` semantics.
|
33
|
+
|
34
|
+
Examples
|
35
|
+
--------
|
36
|
+
{
|
37
|
+
"typing": {"List", "Optional"},
|
38
|
+
"os": {"path", "environ"}
|
39
|
+
}
|
40
|
+
|
41
|
+
extra : Dict[str, Any]
|
42
|
+
Optional extra data provided by the generator.
|
43
|
+
"""
|
44
|
+
body: Optional[str] = None
|
45
|
+
build: Optional[str] = None
|
46
|
+
importing: Dict[str, Set[str]] = field(default_factory=dict)
|
47
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
48
|
+
|
49
|
+
def add_imports(self, new_imports: Dict[str, Set[str]]) -> None:
|
50
|
+
"""
|
51
|
+
Adds new import statements to the existing import dictionary.
|
52
|
+
|
53
|
+
Parameters
|
54
|
+
----------
|
55
|
+
new_imports : Dict[str, Set[str]]
|
56
|
+
A dictionary of module names and symbols to import.
|
57
|
+
"""
|
58
|
+
for module, symbols in new_imports.items():
|
59
|
+
if module not in self.importing:
|
60
|
+
self.importing[module] = set()
|
61
|
+
self.importing[module].update(symbols)
|
62
|
+
|
63
|
+
@staticmethod
|
64
|
+
def _generate_imports_string(imports: Dict[str, Set[str]]) -> str:
|
65
|
+
"""
|
66
|
+
Generates a string representing import statements.
|
67
|
+
|
68
|
+
Parameters
|
69
|
+
----------
|
70
|
+
imports : Dict[str, Set[str]]
|
71
|
+
A dictionary where keys are module names and values are sets of symbols.
|
72
|
+
|
73
|
+
Returns
|
74
|
+
-------
|
75
|
+
str
|
76
|
+
Formatted import statements, one per line.
|
77
|
+
"""
|
78
|
+
return "\n".join(
|
79
|
+
f"from {source} import {', '.join(sorted(var))}" if var else f"import {source}"
|
80
|
+
for source, var in imports.items()
|
81
|
+
)
|
82
|
+
|
83
|
+
def generate_boiler_plate_flask(self) -> str:
|
84
|
+
"""
|
85
|
+
Builds the final AWS Lambda handler code as a complete Python string.
|
86
|
+
|
87
|
+
This includes import statements, any pre-construction logic, and
|
88
|
+
the `lambda_handler` function definition.
|
89
|
+
|
90
|
+
Returns
|
91
|
+
-------
|
92
|
+
str
|
93
|
+
The full source code of the handler as a string.
|
94
|
+
"""
|
95
|
+
imports_chunk = self._generate_imports_string(self.importing)
|
96
|
+
sep = "\n" * 3
|
97
|
+
return f"{imports_chunk}{sep}{self.build or ''}{sep}{self.body or ''}\n"
|
98
|
+
|
99
|
+
def __add__(self, other: "StaticPythonConstructData") -> "StaticPythonConstructData":
|
100
|
+
"""
|
101
|
+
Combine two AWSHandlerGenResponse objects by merging their fields.
|
102
|
+
|
103
|
+
Parameters
|
104
|
+
----------
|
105
|
+
other : StaticPythonConstructData
|
106
|
+
Another response to merge.
|
107
|
+
|
108
|
+
Returns
|
109
|
+
-------
|
110
|
+
StaticPythonConstructData
|
111
|
+
A new instance with merged content.
|
112
|
+
|
113
|
+
Raises
|
114
|
+
------
|
115
|
+
NotImplementedError
|
116
|
+
If `other` is not an instance of AWSHandlerGenResponse.
|
117
|
+
"""
|
118
|
+
if not isinstance(other, StaticPythonConstructData):
|
119
|
+
raise NotImplementedError
|
120
|
+
|
121
|
+
merged_body = "\n".join(filter(None, [self.body, other.body])) or None
|
122
|
+
merged_build = "\n".join(filter(None, [self.build, other.build])) or None
|
123
|
+
|
124
|
+
merged_importing: Dict[str, Set[str]] = {}
|
125
|
+
for module in set(self.importing) | set(other.importing):
|
126
|
+
symbols_self = self.importing.get(module, set())
|
127
|
+
symbols_other = other.importing.get(module, set())
|
128
|
+
merged_importing[module] = symbols_self.union(symbols_other)
|
129
|
+
|
130
|
+
return StaticPythonConstructData(
|
131
|
+
body=merged_body,
|
132
|
+
build=merged_build,
|
133
|
+
importing=merged_importing,
|
134
|
+
extra={}
|
135
|
+
)
|
136
|
+
|
137
|
+
def __iadd__(self, other: "StaticPythonConstructData") -> "StaticPythonConstructData":
|
138
|
+
"""
|
139
|
+
In-place addition of another AWSHandlerGenResponse instance.
|
140
|
+
|
141
|
+
Modifies the current instance by merging the fields from `other`.
|
142
|
+
|
143
|
+
Parameters
|
144
|
+
----------
|
145
|
+
other : StaticPythonConstructData
|
146
|
+
The response to merge into this one.
|
147
|
+
|
148
|
+
Returns
|
149
|
+
-------
|
150
|
+
StaticPythonConstructData
|
151
|
+
The modified instance (`self`).
|
152
|
+
|
153
|
+
Raises
|
154
|
+
------
|
155
|
+
NotImplementedError
|
156
|
+
If `other` is not an instance of AWSHandlerGenResponse.
|
157
|
+
"""
|
158
|
+
if other is None:
|
159
|
+
return self
|
160
|
+
if not isinstance(other, StaticPythonConstructData):
|
161
|
+
raise NotImplementedError
|
162
|
+
|
163
|
+
self.body = "\n".join(filter(None, [self.body, other.body])) or None
|
164
|
+
self.build = "\n".join(filter(None, [self.build, other.build])) or None
|
165
|
+
|
166
|
+
for module, symbols in other.importing.items():
|
167
|
+
if module not in self.importing:
|
168
|
+
self.importing[module] = set()
|
169
|
+
self.importing[module].update(symbols)
|
170
|
+
|
171
|
+
self.extra = {}
|
172
|
+
return self
|
@@ -0,0 +1,90 @@
|
|
1
|
+
"""Command-line interface for the `bisslog_flask` package."""
|
2
|
+
import argparse
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
import traceback
|
6
|
+
|
7
|
+
from .commands.build import build_boiler_plate_flask
|
8
|
+
from .commands.run import run
|
9
|
+
|
10
|
+
|
11
|
+
def main():
|
12
|
+
"""
|
13
|
+
Entry point for the `bisslog_flask` command-line interface.
|
14
|
+
|
15
|
+
This function parses command-line arguments and executes the corresponding
|
16
|
+
subcommand. Currently, it supports the `run` and `build` commands.
|
17
|
+
|
18
|
+
Commands
|
19
|
+
--------
|
20
|
+
run
|
21
|
+
Starts a Flask application based on provided metadata and use case folder.
|
22
|
+
|
23
|
+
build
|
24
|
+
Generates a Flask boilerplate app file from metadata and use cases.
|
25
|
+
|
26
|
+
Raises
|
27
|
+
------
|
28
|
+
Exception
|
29
|
+
Any exception raised during command execution is caught and printed to stderr.
|
30
|
+
"""
|
31
|
+
project_root = os.getcwd()
|
32
|
+
|
33
|
+
if project_root not in sys.path:
|
34
|
+
sys.path.insert(0, project_root)
|
35
|
+
|
36
|
+
parser = argparse.ArgumentParser(prog="bisslog_flask")
|
37
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
38
|
+
|
39
|
+
# Define `run` command
|
40
|
+
run_parser = subparsers.add_parser("run", help="Run a Flask app using metadata.")
|
41
|
+
run_parser.add_argument("--metadata-file", type=str, default=None,
|
42
|
+
help="Path to metadata file (YAML or JSON).")
|
43
|
+
run_parser.add_argument("--use-cases-folder-path", type=str, default=None,
|
44
|
+
help="Path to use case source folder.")
|
45
|
+
run_parser.add_argument("--infra-path", type=str, default=None,
|
46
|
+
help="Path to infrastructure code folder (optional).")
|
47
|
+
run_parser.add_argument("--encoding", type=str, default="utf-8",
|
48
|
+
help="File encoding (default: utf-8).")
|
49
|
+
run_parser.add_argument("--secret-key", type=str,
|
50
|
+
help="Flask SECRET_KEY config value.")
|
51
|
+
run_parser.add_argument("--jwt-secret-key", type=str,
|
52
|
+
help="Flask JWT_SECRET_KEY config value.")
|
53
|
+
|
54
|
+
# Define `build` command
|
55
|
+
build_parser = subparsers.add_parser("build",
|
56
|
+
help="Generate a Flask app boilerplate file.")
|
57
|
+
build_parser.add_argument("--metadata-file", type=str, default=None,
|
58
|
+
help="Path to metadata file (YAML or JSON).")
|
59
|
+
build_parser.add_argument("--use-cases-folder-path", type=str, default=None,
|
60
|
+
help="Path to use case source folder.")
|
61
|
+
build_parser.add_argument("--infra-path", type=str, default=None,
|
62
|
+
help="Path to infrastructure code folder (optional).")
|
63
|
+
build_parser.add_argument("--encoding", type=str, default="utf-8",
|
64
|
+
help="File encoding (default: utf-8).")
|
65
|
+
build_parser.add_argument("--target-filename", type=str, default="flask_app.py",
|
66
|
+
help="Filename to write the generated "
|
67
|
+
"boilerplate (default: flask_app.py)")
|
68
|
+
|
69
|
+
args = parser.parse_args()
|
70
|
+
|
71
|
+
try:
|
72
|
+
if args.command == "run":
|
73
|
+
run(metadata_file=args.metadata_file,
|
74
|
+
use_cases_folder_path=args.use_cases_folder_path,
|
75
|
+
infra_path=args.infra_path,
|
76
|
+
encoding=args.encoding,
|
77
|
+
secret_key=args.secret_key,
|
78
|
+
jwt_secret_key=args.jwt_secret_key)
|
79
|
+
elif args.command == "build":
|
80
|
+
build_boiler_plate_flask(
|
81
|
+
metadata_file=args.metadata_file,
|
82
|
+
use_cases_folder_path=args.use_cases_folder_path,
|
83
|
+
infra_path=args.infra_path,
|
84
|
+
encoding=args.encoding,
|
85
|
+
target_filename=args.target_filename
|
86
|
+
)
|
87
|
+
except Exception as e: # pylint: disable=broad-except
|
88
|
+
traceback.print_exc()
|
89
|
+
print(e)
|
90
|
+
sys.exit(1)
|
File without changes
|
@@ -0,0 +1,57 @@
|
|
1
|
+
"""
|
2
|
+
CLI-compatible utility for generating a Flask application boilerplate from Bisslog metadata.
|
3
|
+
|
4
|
+
This module defines a function that reads service metadata and discovered use cases,
|
5
|
+
generates the corresponding Flask application source code, and writes it to a file.
|
6
|
+
It is intended to be used as part of a command-line interface or automation script
|
7
|
+
to scaffold ready-to-run Flask services.
|
8
|
+
|
9
|
+
The generated code supports HTTP and WebSocket routes, environment-based security
|
10
|
+
configuration, and respects the Bisslog runtime setup defined via decorators.
|
11
|
+
"""
|
12
|
+
from typing import Optional
|
13
|
+
|
14
|
+
from ...builder.builder_flask_app_manager import bisslog_flask_builder
|
15
|
+
|
16
|
+
|
17
|
+
def build_boiler_plate_flask(
|
18
|
+
metadata_file: Optional[str] = None,
|
19
|
+
use_cases_folder_path: Optional[str] = None,
|
20
|
+
infra_path: Optional[str] = None,
|
21
|
+
encoding: str = "utf-8",
|
22
|
+
target_filename: str = "flask_app.py"
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
Generates a Flask application boilerplate file from Bisslog metadata and use case code.
|
26
|
+
|
27
|
+
This function loads the service metadata and associated use cases, builds the Flask
|
28
|
+
application source code dynamically, and writes it to a Python file (e.g., `flask_app.py`).
|
29
|
+
The generated app includes route registration, security setup, and optional WebSocket support.
|
30
|
+
|
31
|
+
Parameters
|
32
|
+
----------
|
33
|
+
metadata_file : str, optional
|
34
|
+
Path to the YAML or JSON metadata file describing the service.
|
35
|
+
use_cases_folder_path : str, optional
|
36
|
+
Path to the folder where the use case implementations are located.
|
37
|
+
infra_path : str, optional
|
38
|
+
Path to the folder where infrastructure components (e.g., adapters) are defined.
|
39
|
+
encoding : str, default="utf-8"
|
40
|
+
The file encoding to use when reading and writing files.
|
41
|
+
target_filename : str, default="flask_app.py"
|
42
|
+
The output filename where the Flask boilerplate code will be written.
|
43
|
+
|
44
|
+
Returns
|
45
|
+
-------
|
46
|
+
None
|
47
|
+
This function writes the generated Flask app code directly to the specified file.
|
48
|
+
"""
|
49
|
+
flask_boiler_plate_string = bisslog_flask_builder(
|
50
|
+
metadata_file=metadata_file,
|
51
|
+
use_cases_folder_path=use_cases_folder_path,
|
52
|
+
infra_path=infra_path,
|
53
|
+
encoding=encoding
|
54
|
+
)
|
55
|
+
|
56
|
+
with open(target_filename, "w", encoding=encoding) as f:
|
57
|
+
f.write(flask_boiler_plate_string)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"""
|
2
|
+
Command module to run a Flask application using Bisslog metadata.
|
3
|
+
|
4
|
+
This module provides a simple `run` function that initializes a Flask app
|
5
|
+
with use case metadata and launches the server. It is intended to be used
|
6
|
+
by the `bisslog_flask run` CLI command or directly from Python code.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
from bisslog_flask import BisslogFlask
|
12
|
+
|
13
|
+
|
14
|
+
def run(metadata_file: Optional[str] = None,
|
15
|
+
use_cases_folder_path: Optional[str] = None,
|
16
|
+
infra_path: Optional[str] = None,
|
17
|
+
encoding: str = "utf-8",
|
18
|
+
secret_key: Optional[str] = None,
|
19
|
+
jwt_secret_key: Optional[str] = None):
|
20
|
+
"""
|
21
|
+
Run a Flask application using metadata and use-case source.
|
22
|
+
|
23
|
+
This function creates and runs a Flask app configured through the
|
24
|
+
BisslogFlask integration layer. It loads metadata definitions,
|
25
|
+
applies HTTP and WebSocket use case resolvers, and starts the server.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
metadata_file : str, optional
|
30
|
+
Path to the metadata file (YAML or JSON) containing service and trigger definitions.
|
31
|
+
use_cases_folder_path : str, optional
|
32
|
+
Path to the folder where the use case implementation code is located.
|
33
|
+
infra_path : str, optional
|
34
|
+
Path to the folder containing infrastructure code (e.g., database, cache).
|
35
|
+
This is not used in the current implementation but can be extended.
|
36
|
+
encoding : str, optional
|
37
|
+
Encoding used to read the metadata file (default is "utf-8").
|
38
|
+
secret_key : str, optional
|
39
|
+
Value to set as Flask's SECRET_KEY for session signing.
|
40
|
+
jwt_secret_key : str, optional
|
41
|
+
Value to set as Flask's JWT_SECRET_KEY for JWT-based authentication.
|
42
|
+
"""
|
43
|
+
app = BisslogFlask(
|
44
|
+
metadata_file=metadata_file,
|
45
|
+
use_cases_folder_path=use_cases_folder_path,
|
46
|
+
infra_path=infra_path,
|
47
|
+
encoding=encoding,
|
48
|
+
secret_key=secret_key,
|
49
|
+
jwt_secret_key=jwt_secret_key
|
50
|
+
)
|
51
|
+
|
52
|
+
app.run()
|
@@ -16,10 +16,12 @@ Dependencies
|
|
16
16
|
- bisslog_schema
|
17
17
|
- bisslog.utils.mapping
|
18
18
|
"""
|
19
|
+
import inspect
|
19
20
|
from copy import deepcopy
|
20
|
-
from typing import Callable, Optional, Dict
|
21
|
+
from typing import Callable, Optional, Dict, Union, Awaitable, Any
|
21
22
|
|
22
23
|
from flask import Flask, request, jsonify
|
24
|
+
|
23
25
|
try:
|
24
26
|
from flask_cors import cross_origin
|
25
27
|
except ImportError:
|
@@ -82,10 +84,10 @@ class BisslogFlaskHttpResolver(BisslogFlaskResolver):
|
|
82
84
|
|
83
85
|
@staticmethod
|
84
86
|
def _use_case_factory(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
87
|
+
use_case_name: str,
|
88
|
+
fn: Callable,
|
89
|
+
mapper: Optional[Dict[str, str]] = None,
|
90
|
+
trigger: Optional[TriggerHttp] = None
|
89
91
|
):
|
90
92
|
"""
|
91
93
|
Factory to produce a Flask view function with optional mapping and CORS.
|
@@ -109,10 +111,49 @@ class BisslogFlaskHttpResolver(BisslogFlaskResolver):
|
|
109
111
|
use_case_fn_copy = deepcopy(fn)
|
110
112
|
__mapper__ = Mapper(name=f"Mapper {use_case_name}", base=mapper) if mapper else None
|
111
113
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
114
|
+
is_async = (
|
115
|
+
inspect.iscoroutinefunction(fn)
|
116
|
+
or inspect.iscoroutinefunction(getattr(fn, "__call__", None))
|
117
|
+
)
|
118
|
+
|
119
|
+
if is_async:
|
120
|
+
async def uc(*args, **kwargs):
|
121
|
+
if __mapper__ is None:
|
122
|
+
more_kwargs = {}
|
123
|
+
if request.method.lower() != "get":
|
124
|
+
more_kwargs.update(request.get_json(silent=True) or {})
|
125
|
+
res = await use_case_fn_copy(*args, **kwargs, **more_kwargs)
|
126
|
+
return jsonify(res)
|
127
|
+
|
128
|
+
res_map = __mapper__.map({
|
129
|
+
"path_query": request.view_args or {},
|
130
|
+
"body": request.get_json(silent=True) or {},
|
131
|
+
"params": request.args.to_dict(),
|
132
|
+
"headers": request.headers,
|
133
|
+
})
|
134
|
+
res = await use_case_fn_copy(**res_map)
|
135
|
+
return jsonify(res)
|
136
|
+
|
137
|
+
view = uc
|
138
|
+
else:
|
139
|
+
def uc(*args, **kwargs):
|
140
|
+
if __mapper__ is None:
|
141
|
+
more_kwargs = {}
|
142
|
+
if request.method.lower() != "get":
|
143
|
+
more_kwargs.update(request.get_json(silent=True) or {})
|
144
|
+
res = use_case_fn_copy(*args, **kwargs, **more_kwargs)
|
145
|
+
return jsonify(res)
|
146
|
+
|
147
|
+
res_map = __mapper__.map({
|
148
|
+
"path_query": request.view_args or {},
|
149
|
+
"body": request.get_json(silent=True) or {},
|
150
|
+
"params": request.args.to_dict(),
|
151
|
+
"headers": request.headers,
|
152
|
+
})
|
153
|
+
res = use_case_fn_copy(**res_map)
|
154
|
+
return jsonify(res)
|
155
|
+
|
156
|
+
view = uc
|
116
157
|
|
117
158
|
# Apply CORS dynamically if allowed
|
118
159
|
if trigger and trigger.allow_cors:
|
@@ -124,13 +165,13 @@ class BisslogFlaskHttpResolver(BisslogFlaskResolver):
|
|
124
165
|
"allow_headers": ["Content-Type", "Authorization"],
|
125
166
|
"supports_credentials": True
|
126
167
|
}
|
127
|
-
return cross_origin(**cors_kwargs)(
|
168
|
+
return cross_origin(**cors_kwargs)(view)
|
128
169
|
|
129
|
-
return
|
170
|
+
return view
|
130
171
|
|
131
172
|
@classmethod
|
132
173
|
def _add_use_case(cls, app: Flask, use_case_info: UseCaseInfo, trigger: TriggerInfo,
|
133
|
-
use_case_function):
|
174
|
+
use_case_function: Union[Callable[..., Any], Callable[..., Awaitable[Any]]]):
|
134
175
|
"""
|
135
176
|
Adds an HTTP endpoint to the Flask app for a given use case.
|
136
177
|
|
@@ -5,7 +5,9 @@ This module defines the `BisslogFlaskWebSocketResolver` class, which dynamically
|
|
5
5
|
WebSocket-based use case triggers in a Flask app. The class uses metadata definitions to
|
6
6
|
configure event routes (via `route_key`) and binds them to corresponding use case functions.
|
7
7
|
"""
|
8
|
+
import inspect
|
8
9
|
from typing import Callable
|
10
|
+
|
9
11
|
from flask import Flask, request
|
10
12
|
|
11
13
|
try:
|
@@ -64,13 +66,27 @@ class BisslogFlaskWebSocketResolver(BisslogFlaskResolver):
|
|
64
66
|
base=trigger_info.options.mapper
|
65
67
|
) if trigger_info.options.mapper else None
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
is_async = inspect.iscoroutinefunction(use_case_callable)
|
70
|
+
|
71
|
+
if is_async:
|
72
|
+
@socket_io_obj.on(route_key)
|
73
|
+
async def on_event(data):
|
74
|
+
mapped_data = mapper.map({
|
75
|
+
"route_key": route_key,
|
76
|
+
"connection_id": request.sid,
|
77
|
+
"body": data,
|
78
|
+
"headers": dict(request.headers)
|
79
|
+
}) if mapper else data
|
80
|
+
|
81
|
+
return await use_case_callable(**mapped_data) if mapper else use_case_callable(data)
|
82
|
+
else:
|
83
|
+
@socket_io_obj.on(route_key)
|
84
|
+
def on_event(data):
|
85
|
+
mapped_data = mapper.map({
|
86
|
+
"route_key": route_key,
|
87
|
+
"connection_id": request.sid,
|
88
|
+
"body": data,
|
89
|
+
"headers": dict(request.headers)
|
90
|
+
}) if mapper else data
|
75
91
|
|
76
|
-
|
92
|
+
return use_case_callable(**mapped_data) if mapper else use_case_callable(data)
|
@@ -18,7 +18,9 @@ Dependencies
|
|
18
18
|
from typing import Optional, Callable
|
19
19
|
|
20
20
|
from bisslog_schema import read_service_info_with_code
|
21
|
+
from bisslog_schema.eager_import_module_or_package import EagerImportModulePackage
|
21
22
|
from bisslog_schema.schema import UseCaseInfo, TriggerHttp, TriggerWebsocket
|
23
|
+
from bisslog_schema.setup import run_setup
|
22
24
|
from flask import Flask
|
23
25
|
|
24
26
|
from .bisslog_flask_http_resolver import BisslogFlaskHttpResolver
|
@@ -42,14 +44,17 @@ class InitFlaskAppManager:
|
|
42
44
|
"""
|
43
45
|
|
44
46
|
def __init__(self, http_processor: BisslogFlaskResolver,
|
45
|
-
websocket_processor: BisslogFlaskResolver
|
47
|
+
websocket_processor: BisslogFlaskResolver,
|
48
|
+
force_import: Callable[[str], None]) -> None:
|
46
49
|
self._http_processor = http_processor
|
47
50
|
self._websocket_processor = websocket_processor
|
51
|
+
self._force_import = force_import
|
48
52
|
|
49
53
|
def __call__(
|
50
54
|
self,
|
51
55
|
metadata_file: Optional[str] = None,
|
52
56
|
use_cases_folder_path: Optional[str] = None,
|
57
|
+
infra_path: Optional[str] = None,
|
53
58
|
app: Optional[Flask] = None,
|
54
59
|
*,
|
55
60
|
encoding: str = "utf-8",
|
@@ -69,6 +74,9 @@ class InitFlaskAppManager:
|
|
69
74
|
Path to the metadata file (YAML/JSON).
|
70
75
|
use_cases_folder_path : str, optional
|
71
76
|
Directory where use case code is located.
|
77
|
+
infra_path : str, optional
|
78
|
+
Path to the folder where infrastructure components (e.g., adapters) are defined.
|
79
|
+
This is used to ensure that necessary modules are imported before route registration.
|
72
80
|
app : Flask, optional
|
73
81
|
An existing Flask app instance to which routes will be added.
|
74
82
|
If not provided, a new app is created using the service name.
|
@@ -94,6 +102,11 @@ class InitFlaskAppManager:
|
|
94
102
|
service_info = full_service_data.declared_metadata
|
95
103
|
use_cases = full_service_data.discovered_use_cases
|
96
104
|
|
105
|
+
# Force import
|
106
|
+
self._force_import(infra_path)
|
107
|
+
# Run global setup if defined
|
108
|
+
run_setup("flask")
|
109
|
+
|
97
110
|
# Initialize Flask app
|
98
111
|
if app is None:
|
99
112
|
app = Flask(service_info.name)
|
@@ -120,4 +133,5 @@ class InitFlaskAppManager:
|
|
120
133
|
return app
|
121
134
|
|
122
135
|
|
123
|
-
BisslogFlask = InitFlaskAppManager(BisslogFlaskHttpResolver(), BisslogFlaskWebSocketResolver()
|
136
|
+
BisslogFlask = InitFlaskAppManager(BisslogFlaskHttpResolver(), BisslogFlaskWebSocketResolver(),
|
137
|
+
EagerImportModulePackage(("src.infra", "infra")))
|
@@ -20,9 +20,6 @@ class BisslogFlaskSocketHelper(WebSocketManager):
|
|
20
20
|
a consistent interface.
|
21
21
|
"""
|
22
22
|
|
23
|
-
def __init__(self, conn) -> None:
|
24
|
-
super().__init__(conn)
|
25
|
-
|
26
23
|
def emit(self, event: str, connection_id: str, payload: Any,
|
27
24
|
broadcast: bool = False, to: str = None):
|
28
25
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: bisslog_flask
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.3
|
4
4
|
Summary: It is an extension of the bisslog library to support processes with flask
|
5
5
|
Author-email: Darwin Stiven Herrera Cartagena <darwinsherrerac@gmail.com>
|
6
6
|
Project-URL: Homepage, https://github.com/darwinhc/bisslog-flask
|
@@ -10,54 +10,54 @@ Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Python: >=3.7
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
|
-
Requires-Dist: bisslog>=0.0.
|
14
|
-
Requires-Dist: bisslog-schema>=0.0.
|
13
|
+
Requires-Dist: bisslog>=0.0.9
|
14
|
+
Requires-Dist: bisslog-schema>=0.0.10
|
15
15
|
Requires-Dist: flask
|
16
16
|
Provides-Extra: websocket
|
17
17
|
Requires-Dist: flask-socketio; extra == "websocket"
|
18
18
|
Provides-Extra: cors
|
19
19
|
Requires-Dist: flask-cors>=6.0.0; extra == "cors"
|
20
|
+
Provides-Extra: async
|
21
|
+
Requires-Dist: Flask[async]>=2.2; extra == "async"
|
20
22
|
Dynamic: license-file
|
21
23
|
|
22
|
-
|
23
24
|
# bisslog-flask
|
24
25
|
|
25
26
|
[](https://pypi.org/project/bisslog-flask/)
|
26
27
|
[](LICENSE)
|
27
28
|
|
28
|
-
**bisslog-flask** is an extension of the
|
29
|
+
**bisslog-flask** is an extension of the Bisslog library to support processes with Flask.
|
30
|
+
It enables dynamic HTTP and WebSocket route registration from use case metadata, allowing developers to build clean, modular, and metadata-driven APIs with minimal boilerplate.
|
29
31
|
|
30
|
-
Part of the
|
32
|
+
Part of the Bisslog ecosystem, it is designed to work seamlessly with domain-centric architectures like Hexagonal or Clean Architecture.
|
31
33
|
|
32
|
-
|
34
|
+
---
|
33
35
|
|
34
|
-
|
36
|
+
## ✨ Features
|
35
37
|
|
38
|
+
- 🔁 Dynamic route registration for HTTP and WebSocket triggers
|
36
39
|
- 🧠 Metadata-driven setup – use YAML or JSON to declare your use cases
|
37
|
-
|
38
|
-
- 🔒 Automatic CORS per endpoint using flask-cors
|
39
|
-
|
40
|
+
- 🔒 Automatic CORS per endpoint using `flask-cors`
|
40
41
|
- 🔌 Extensible resolver pattern – plug in your own processor
|
41
|
-
|
42
42
|
- ⚙️ Mapper integration – maps HTTP request parts to domain function arguments
|
43
43
|
|
44
|
-
|
44
|
+
---
|
45
45
|
|
46
46
|
## 📦 Installation
|
47
47
|
|
48
|
-
|
48
|
+
```bash
|
49
49
|
pip install bisslog-flask
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
```
|
53
51
|
|
52
|
+
---
|
54
53
|
|
55
54
|
## 🚀 Quickstart
|
56
55
|
|
57
56
|
### Programmatically
|
58
57
|
|
59
|
-
if you want to configure the app before
|
60
|
-
|
58
|
+
Use this approach if you want to configure the app before Bisslog touches it:
|
59
|
+
|
60
|
+
```python
|
61
61
|
from flask import Flask
|
62
62
|
from bisslog_flask import BisslogFlask
|
63
63
|
|
@@ -70,11 +70,11 @@ BisslogFlask(
|
|
70
70
|
|
71
71
|
if __name__ == "__main__":
|
72
72
|
app.run(debug=True)
|
73
|
-
|
73
|
+
```
|
74
74
|
|
75
|
-
|
75
|
+
Or use the factory version:
|
76
76
|
|
77
|
-
|
77
|
+
```python
|
78
78
|
from bisslog_flask import BisslogFlask
|
79
79
|
|
80
80
|
app = BisslogFlask(
|
@@ -84,58 +84,73 @@ app = BisslogFlask(
|
|
84
84
|
|
85
85
|
if __name__ == "__main__":
|
86
86
|
app.run(debug=True)
|
87
|
-
|
88
|
-
|
87
|
+
```
|
89
88
|
|
89
|
+
---
|
90
90
|
|
91
|
-
##
|
91
|
+
## 🖥️ CLI Usage
|
92
92
|
|
93
|
-
|
93
|
+
You can also use the `bisslog_flask` CLI to run or generate a Flask app.
|
94
94
|
|
95
|
+
```bash
|
96
|
+
bisslog_flask run [--metadata-file FILE] [--use-cases-folder-path DIR]
|
97
|
+
[--infra-folder-path DIR] [--encoding ENC]
|
98
|
+
[--secret-key KEY] [--jwt-secret-key KEY]
|
95
99
|
|
96
|
-
|
100
|
+
bisslog_flask build [--metadata-file FILE] [--use-cases-folder-path DIR]
|
101
|
+
[--infra-folder-path DIR] [--encoding ENC]
|
102
|
+
[--target-filename FILE]
|
103
|
+
```
|
97
104
|
|
98
|
-
|
105
|
+
- `run`: Launches the Flask application from metadata.
|
106
|
+
- `build`: Generates a boilerplate Flask file (`flask_app.py` by default).
|
99
107
|
|
100
|
-
|
108
|
+
All options are optional. You can override defaults via CLI flags.
|
101
109
|
|
102
|
-
|
110
|
+
---
|
103
111
|
|
112
|
+
## 🔐 CORS Handling
|
104
113
|
|
105
|
-
|
114
|
+
CORS is applied only when `allow_cors: true` is specified in the trigger.
|
106
115
|
|
107
|
-
|
116
|
+
Fully dynamic: works even with Flask dynamic routes like `/users/<id>`.
|
108
117
|
|
109
|
-
|
118
|
+
Powered by `@cross_origin` from `flask-cors`.
|
110
119
|
|
111
|
-
|
120
|
+
---
|
112
121
|
|
113
|
-
|
122
|
+
## ✅ Requirements
|
114
123
|
|
115
|
-
|
124
|
+
- Python ≥ 3.7
|
125
|
+
- Flask ≥ 2.0
|
126
|
+
- bisslog-schema ≥ 0.0.3
|
127
|
+
- flask-cors
|
128
|
+
- (Optional) flask-sock if using WebSocket triggers
|
116
129
|
|
130
|
+
---
|
117
131
|
|
118
132
|
## 🧪 Testing Tip
|
119
133
|
|
120
|
-
You can test the generated Flask app directly with `app.test_client()` if
|
134
|
+
You can test the generated Flask app directly with `app.test_client()` if using the programmatic interface:
|
121
135
|
|
122
136
|
```python
|
123
137
|
from bisslog_flask import BisslogFlask
|
124
138
|
|
125
139
|
def test_user_create():
|
126
|
-
app = BisslogFlask(
|
140
|
+
app = BisslogFlask(
|
141
|
+
metadata_file="metadata.yml",
|
142
|
+
use_cases_folder_path="src/use_cases"
|
143
|
+
)
|
127
144
|
client = app.test_client()
|
128
145
|
response = client.post("/user", json={"name": "Ana", "email": "ana@example.com"})
|
129
146
|
assert response.status_code == 200
|
130
147
|
```
|
131
148
|
|
132
|
-
|
133
|
-
|
149
|
+
If you're generating the code (boilerplate), you just need to test your use cases.
|
134
150
|
|
151
|
+
---
|
135
152
|
|
136
153
|
## 📜 License
|
137
154
|
|
138
|
-
This project is licensed under the MIT License.
|
139
|
-
|
140
|
-
|
141
|
-
|
155
|
+
This project is licensed under the MIT License.
|
156
|
+
See the [LICENSE](LICENSE) file for details.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
bisslog_flask/__init__.py,sha256=BEf_UxFtcMfaM-Smh_bwc6Xn8pR8LcEjby3nCs0hdXE,758
|
2
|
+
bisslog_flask/builder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
bisslog_flask/builder/builder_flask_app_manager.py,sha256=HffxWfMRRlrfmYSojCdH563bJgQGa3vF2GB9rOYnINw,14777
|
4
|
+
bisslog_flask/builder/static_python_construct_data.py,sha256=p8QcXUAXZRXtNlghWv-husw9hByGTp53_g0RxhmzoNc,5735
|
5
|
+
bisslog_flask/cli/__init__.py,sha256=DlqqUfYI3e3Q6wY7xg7X-Ux9_6gTNQAAClAe2aI2-tg,3792
|
6
|
+
bisslog_flask/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
bisslog_flask/cli/commands/build.py,sha256=uI-aH8HvW7UPdqGJvtQ2p5tw5nEFcUAzvicLCGuKccY,2341
|
8
|
+
bisslog_flask/cli/commands/run.py,sha256=gUfgpLzPxQ-hB2DApA-BcxpkPaI_QCNH7OUUpkl8D34,1953
|
9
|
+
bisslog_flask/initializer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
bisslog_flask/initializer/bisslog_flask_http_resolver.py,sha256=nxAZFyl_7akpMzgDQsURVAbJmTxuFKlpCu3SfYioh44,7890
|
11
|
+
bisslog_flask/initializer/bisslog_flask_resolver.py,sha256=pt7EC-WqfsBp7w3Z0SE378hfwWTLfRDM6fGMfF3_sb8,1707
|
12
|
+
bisslog_flask/initializer/bisslog_flask_ws_resolver.py,sha256=wcG8iPky2PYyRUovHJg5woX1vLJ-fmLRmgUW-PLU_Jc,3572
|
13
|
+
bisslog_flask/initializer/init_flask_app_manager.py,sha256=gwu6cg6RLh5UMT4GGiIFJx8cD385j7EvUnGfk9S6aC0,5389
|
14
|
+
bisslog_flask/socket_helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
bisslog_flask/socket_helper/socket_helper.py,sha256=HHENwYT-qdrJlwmGszYqFM1MA1g6naRurUp6sYCHRRQ,2967
|
16
|
+
bisslog_flask-0.0.3.dist-info/licenses/LICENSE,sha256=TSlM1hRIXc6yR3xpGzy2DMSSbds0svqHSetfNfQqCEk,1074
|
17
|
+
bisslog_flask-0.0.3.dist-info/METADATA,sha256=8X8f3Lsko5s7NX8iZwkP1gFpVNoOdRVUSjKinqaaVyk,4251
|
18
|
+
bisslog_flask-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
19
|
+
bisslog_flask-0.0.3.dist-info/entry_points.txt,sha256=A2_A5lZt973oUBpuOCC78aTm7dGyHk6cemnn5_jOXHw,57
|
20
|
+
bisslog_flask-0.0.3.dist-info/top_level.txt,sha256=Xk85d0SIhkUP1HjsXOtq2vlU7yQT3mFApyb5IVgtG6w,14
|
21
|
+
bisslog_flask-0.0.3.dist-info/RECORD,,
|
@@ -1,13 +0,0 @@
|
|
1
|
-
bisslog_flask/__init__.py,sha256=BEf_UxFtcMfaM-Smh_bwc6Xn8pR8LcEjby3nCs0hdXE,758
|
2
|
-
bisslog_flask/initializer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
bisslog_flask/initializer/bisslog_flask_http_resolver.py,sha256=d-KFQwMXeRyq3_Cu5bxCF7CpnetfnKainChEbdxDk2c,6257
|
4
|
-
bisslog_flask/initializer/bisslog_flask_resolver.py,sha256=QWjft6HZbBplbP1vVMNoPhGoSZBJubAotwLR76HVttw,1708
|
5
|
-
bisslog_flask/initializer/bisslog_flask_ws_resolver.py,sha256=s-eDdk5HA6E_D8iZC6IN7RSW-96mzu8m8VGL3Xqm9PQ,2974
|
6
|
-
bisslog_flask/initializer/init_flask_app_manager.py,sha256=QzO1dae3YFap2VEXK3gSexLkdfBGaLWrHK732z5s0K0,4682
|
7
|
-
bisslog_flask/socket_helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
bisslog_flask/socket_helper/socket_helper.py,sha256=sR9xQSPWzijhPNRh8WRxDbzE7rqFqm4w0Vql4gBGiGU,3037
|
9
|
-
bisslog_flask-0.0.1.dist-info/licenses/LICENSE,sha256=TSlM1hRIXc6yR3xpGzy2DMSSbds0svqHSetfNfQqCEk,1074
|
10
|
-
bisslog_flask-0.0.1.dist-info/METADATA,sha256=w82no5oSrRtS7XEDT38gs6UYIrzyuBHK4eEJkpQptH0,3551
|
11
|
-
bisslog_flask-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
12
|
-
bisslog_flask-0.0.1.dist-info/top_level.txt,sha256=Xk85d0SIhkUP1HjsXOtq2vlU7yQT3mFApyb5IVgtG6w,14
|
13
|
-
bisslog_flask-0.0.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|