nsj-rest-lib2 0.0.26__py3-none-any.whl → 0.0.27__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.
@@ -0,0 +1,550 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, Optional, Sequence
7
+
8
+ import black
9
+
10
+ from nsj_rest_lib2.compiler.compiler_structures import PropertiesCompilerStructure
11
+ from nsj_rest_lib2.compiler.edl_model.api_model import HandlerConfig, HandlerMapping
12
+ from nsj_rest_lib2.compiler.edl_model.entity_model_base import EntityModelBase
13
+ from nsj_rest_lib2.compiler.edl_model.primitives import PrimitiveTypes
14
+ from nsj_rest_lib2.compiler.function_model import (
15
+ FunctionBindingConfig,
16
+ FunctionCompilationOutput,
17
+ FunctionRelationBinding,
18
+ )
19
+ from nsj_rest_lib2.compiler.util.str_util import CompilerStrUtil
20
+ from nsj_rest_lib2.compiler.util.type_naming_util import compile_function_class_name
21
+ from nsj_rest_lib2.compiler.util.type_util import TypeUtil
22
+
23
+
24
+ @dataclass
25
+ class _MappingNode:
26
+ attr: str
27
+ python_name: str
28
+ dto_field: Optional[str]
29
+ children: list["_MappingNode"] = field(default_factory=list)
30
+ pg_type: Optional[str] = None
31
+ is_array: bool = False
32
+
33
+
34
+ @dataclass
35
+ class _FieldSpec:
36
+ name: str
37
+ annotation: str
38
+ type_field_name: Optional[str]
39
+ relation_class: Optional[str]
40
+ is_relation: bool
41
+ is_list: bool
42
+
43
+
44
+ @dataclass
45
+ class _ClassSpec:
46
+ name: str
47
+ type_name: str
48
+ function_name: str
49
+ fields: list[_FieldSpec]
50
+ children: list["_ClassSpec"] = field(default_factory=list)
51
+
52
+
53
+ class FunctionCompiler:
54
+ """
55
+ Responsável por compilar tipos usados por funções de banco (Insert/Update)
56
+ com base na configuração declarada no EDL.
57
+ """
58
+
59
+ def compile_insert(
60
+ self,
61
+ entity_model: EntityModelBase,
62
+ properties_structure: PropertiesCompilerStructure,
63
+ handler_config: Optional[HandlerConfig],
64
+ prefx_class_name: str,
65
+ ) -> FunctionCompilationOutput:
66
+ return self._compile(
67
+ entity_model,
68
+ properties_structure,
69
+ handler_config,
70
+ prefx_class_name,
71
+ operation="insert",
72
+ )
73
+
74
+ def compile_update(
75
+ self,
76
+ entity_model: EntityModelBase,
77
+ properties_structure: PropertiesCompilerStructure,
78
+ handler_config: Optional[HandlerConfig],
79
+ prefx_class_name: str,
80
+ ) -> FunctionCompilationOutput:
81
+ return self._compile(
82
+ entity_model,
83
+ properties_structure,
84
+ handler_config,
85
+ prefx_class_name,
86
+ operation="update",
87
+ )
88
+
89
+ # ------------------------------------------------------------------ #
90
+ # Internal helpers
91
+ # ------------------------------------------------------------------ #
92
+
93
+ def _compile(
94
+ self,
95
+ entity_model: EntityModelBase,
96
+ properties_structure: PropertiesCompilerStructure,
97
+ handler_config: Optional[HandlerConfig],
98
+ prefx_class_name: str,
99
+ operation: str,
100
+ ) -> FunctionCompilationOutput:
101
+ if not handler_config or handler_config.impl != "pg_function":
102
+ return FunctionCompilationOutput()
103
+
104
+ if not handler_config.call or not handler_config.call.arg_binding:
105
+ return FunctionCompilationOutput()
106
+
107
+ arg_binding = handler_config.call.arg_binding
108
+ if not arg_binding.mapping or not arg_binding.type_name:
109
+ return FunctionCompilationOutput()
110
+
111
+ function_name = handler_config.function_ref
112
+ if not function_name:
113
+ return FunctionCompilationOutput()
114
+
115
+ mapping_nodes = self._build_nodes(arg_binding.mapping)
116
+ if not mapping_nodes:
117
+ return FunctionCompilationOutput()
118
+
119
+ class_name = compile_function_class_name(
120
+ entity_model.id,
121
+ prefx_class_name,
122
+ [],
123
+ operation,
124
+ )
125
+
126
+ field_bindings: Dict[str, str] = {}
127
+ relation_bindings: Dict[str, FunctionRelationBinding] = {}
128
+
129
+ class_spec = self._build_class_spec(
130
+ mapping_nodes,
131
+ entity_model,
132
+ properties_structure,
133
+ function_name,
134
+ class_name,
135
+ arg_binding.type_name,
136
+ path=[],
137
+ operation=operation,
138
+ field_bindings=field_bindings,
139
+ relation_bindings=relation_bindings,
140
+ prefx_class_name=prefx_class_name,
141
+ )
142
+
143
+ module_ast, has_relations, needs_any = self._build_module_ast(
144
+ class_spec, operation
145
+ )
146
+
147
+ if not module_ast:
148
+ return FunctionCompilationOutput()
149
+
150
+ imports = self._build_imports(operation, has_relations, needs_any)
151
+ module = ast.Module(body=imports + module_ast, type_ignores=[])
152
+ module = ast.fix_missing_locations(module)
153
+ code = ast.unparse(module)
154
+ code = black.format_str(code, mode=black.FileMode())
155
+
156
+ return FunctionCompilationOutput(
157
+ class_name=class_name,
158
+ code=code,
159
+ field_bindings=field_bindings,
160
+ relation_bindings=relation_bindings,
161
+ )
162
+
163
+ def _build_nodes(self, mappings: Sequence[HandlerMapping]) -> list[_MappingNode]:
164
+ nodes: list[_MappingNode] = []
165
+ used_names: Dict[str, int] = {}
166
+
167
+ for entry in mappings or []:
168
+ if not entry.attr:
169
+ continue
170
+
171
+ python_name = self._sanitize_identifier(entry.attr)
172
+ python_name = self._ensure_unique_name(python_name, used_names)
173
+ dto_field = self._extract_dto_field(entry.from_)
174
+
175
+ node = _MappingNode(
176
+ attr=entry.attr,
177
+ python_name=python_name,
178
+ dto_field=dto_field,
179
+ )
180
+
181
+ if entry.mapping:
182
+ pg_type, is_array = self._parse_type_declaration(entry.as_)
183
+ node.pg_type = pg_type
184
+ node.is_array = is_array
185
+ node.children = self._build_nodes(entry.mapping)
186
+
187
+ nodes.append(node)
188
+
189
+ return nodes
190
+
191
+ def _build_class_spec(
192
+ self,
193
+ nodes: Sequence[_MappingNode],
194
+ entity_model: EntityModelBase,
195
+ properties_structure: PropertiesCompilerStructure,
196
+ function_name: str,
197
+ class_name: str,
198
+ type_name: str,
199
+ path: list[str],
200
+ operation: str,
201
+ field_bindings: Dict[str, str],
202
+ relation_bindings: Dict[str, FunctionRelationBinding],
203
+ prefx_class_name: str,
204
+ ) -> _ClassSpec:
205
+ fields: list[_FieldSpec] = []
206
+ children_specs: list[_ClassSpec] = []
207
+
208
+ for node in nodes:
209
+ if node.children:
210
+ if not node.pg_type:
211
+ raise ValueError(
212
+ f"O campo '{node.attr}' possui sub-mapeamento, porém o tipo Postgres não foi informado em 'as'."
213
+ )
214
+
215
+ child_class_name = compile_function_class_name(
216
+ entity_model.id,
217
+ prefx_class_name,
218
+ path + [node.python_name],
219
+ operation,
220
+ )
221
+
222
+ child_spec = self._build_class_spec(
223
+ node.children,
224
+ entity_model,
225
+ properties_structure,
226
+ function_name,
227
+ child_class_name,
228
+ node.pg_type,
229
+ path + [node.python_name],
230
+ operation,
231
+ field_bindings,
232
+ relation_bindings,
233
+ prefx_class_name,
234
+ )
235
+
236
+ children_specs.append(child_spec)
237
+ fields.append(
238
+ _FieldSpec(
239
+ name=node.python_name,
240
+ annotation=self._relation_annotation(
241
+ child_class_name, node.is_array
242
+ ),
243
+ type_field_name=(
244
+ node.attr if node.python_name != node.attr else None
245
+ ),
246
+ relation_class=child_class_name,
247
+ is_relation=True,
248
+ is_list=node.is_array,
249
+ )
250
+ )
251
+
252
+ if node.dto_field:
253
+ relation_bindings[node.dto_field] = FunctionRelationBinding(
254
+ field_name=node.python_name,
255
+ function_type_class=child_class_name,
256
+ )
257
+
258
+ continue
259
+
260
+ annotation = self._resolve_annotation(node, properties_structure)
261
+ fields.append(
262
+ _FieldSpec(
263
+ name=node.python_name,
264
+ annotation=annotation,
265
+ type_field_name=(
266
+ node.attr if node.python_name != node.attr else None
267
+ ),
268
+ relation_class=None,
269
+ is_relation=False,
270
+ is_list=False,
271
+ )
272
+ )
273
+
274
+ if node.dto_field and node.dto_field != node.python_name:
275
+ field_bindings[node.dto_field] = node.python_name
276
+
277
+ return _ClassSpec(
278
+ name=class_name,
279
+ type_name=type_name,
280
+ function_name=function_name,
281
+ fields=fields,
282
+ children=children_specs,
283
+ )
284
+
285
+ def _build_module_ast(
286
+ self,
287
+ spec: _ClassSpec,
288
+ operation: str,
289
+ ) -> tuple[list[ast.stmt], bool, bool]:
290
+ class_defs: list[ast.stmt] = []
291
+ has_relation = False
292
+ needs_any = False
293
+
294
+ for child in spec.children:
295
+ child_defs, child_has_relation, child_needs_any = self._build_module_ast(
296
+ child, operation
297
+ )
298
+ class_defs.extend(child_defs)
299
+ has_relation = has_relation or child_has_relation
300
+ needs_any = needs_any or child_needs_any
301
+
302
+ body: list[ast.stmt] = []
303
+ for field in spec.fields:
304
+ annotation_expr = self._build_annotation_expr(field.annotation, field)
305
+ descriptor_call = self._build_descriptor_call(field)
306
+ body.append(
307
+ ast.AnnAssign(
308
+ target=ast.Name(id=field.name, ctx=ast.Store()),
309
+ annotation=annotation_expr,
310
+ value=descriptor_call,
311
+ simple=1,
312
+ )
313
+ )
314
+ if field.is_relation:
315
+ has_relation = True
316
+ if field.annotation == "Any":
317
+ needs_any = True
318
+
319
+ if not body:
320
+ body = [ast.Pass()]
321
+
322
+ decorator_name = (
323
+ "InsertFunctionType" if operation == "insert" else "UpdateFunctionType"
324
+ )
325
+ base_class = (
326
+ "InsertFunctionTypeBase"
327
+ if operation == "insert"
328
+ else "UpdateFunctionTypeBase"
329
+ )
330
+
331
+ decorator = ast.Call(
332
+ func=ast.Name(id=decorator_name, ctx=ast.Load()),
333
+ args=[],
334
+ keywords=[
335
+ ast.keyword(arg="type_name", value=ast.Constant(value=spec.type_name)),
336
+ ast.keyword(
337
+ arg="function_name", value=ast.Constant(value=spec.function_name)
338
+ ),
339
+ ],
340
+ )
341
+
342
+ class_defs.append(
343
+ ast.ClassDef(
344
+ name=spec.name,
345
+ bases=[ast.Name(id=base_class, ctx=ast.Load())],
346
+ keywords=[],
347
+ decorator_list=[decorator],
348
+ body=body,
349
+ )
350
+ )
351
+
352
+ return class_defs, has_relation, needs_any
353
+
354
+ def _build_imports(
355
+ self, operation: str, has_relation_fields: bool, needs_any: bool
356
+ ) -> list[ast.stmt]:
357
+ imports: list[ast.stmt] = [
358
+ ast.Import(names=[ast.alias(name="datetime", asname=None)]),
359
+ ast.Import(names=[ast.alias(name="uuid", asname=None)]),
360
+ ]
361
+
362
+ if needs_any:
363
+ imports.append(
364
+ ast.ImportFrom(
365
+ module="typing",
366
+ names=[ast.alias(name="Any", asname=None)],
367
+ level=0,
368
+ )
369
+ )
370
+
371
+ decorator_module = (
372
+ "nsj_rest_lib.decorator.insert_function_type"
373
+ if operation == "insert"
374
+ else "nsj_rest_lib.decorator.update_function_type"
375
+ )
376
+ decorator_name = (
377
+ "InsertFunctionType" if operation == "insert" else "UpdateFunctionType"
378
+ )
379
+
380
+ base_module = "nsj_rest_lib.entity.function_type_base"
381
+ base_name = (
382
+ "InsertFunctionTypeBase"
383
+ if operation == "insert"
384
+ else "UpdateFunctionTypeBase"
385
+ )
386
+
387
+ imports.extend(
388
+ [
389
+ ast.ImportFrom(
390
+ module=decorator_module,
391
+ names=[ast.alias(name=decorator_name, asname=None)],
392
+ level=0,
393
+ ),
394
+ ast.ImportFrom(
395
+ module="nsj_rest_lib.descriptor.function_field",
396
+ names=[ast.alias(name="FunctionField", asname=None)],
397
+ level=0,
398
+ ),
399
+ ast.ImportFrom(
400
+ module=base_module,
401
+ names=[ast.alias(name=base_name, asname=None)],
402
+ level=0,
403
+ ),
404
+ ]
405
+ )
406
+
407
+ if has_relation_fields:
408
+ imports.append(
409
+ ast.ImportFrom(
410
+ module="nsj_rest_lib.descriptor.function_relation_field",
411
+ names=[ast.alias(name="FunctionRelationField", asname=None)],
412
+ level=0,
413
+ )
414
+ )
415
+
416
+ return imports
417
+
418
+ def _build_annotation_expr(
419
+ self, annotation: str, field: _FieldSpec
420
+ ) -> ast.expr:
421
+ if field.is_relation and field.is_list:
422
+ return ast.Subscript(
423
+ value=ast.Name(id="list", ctx=ast.Load()),
424
+ slice=ast.Name(id=field.relation_class, ctx=ast.Load()),
425
+ ctx=ast.Load(),
426
+ )
427
+
428
+ if field.is_relation:
429
+ return ast.Name(id=field.relation_class, ctx=ast.Load())
430
+
431
+ if annotation == "Any":
432
+ return ast.Name(id="Any", ctx=ast.Load())
433
+
434
+ if "." in annotation:
435
+ parts = annotation.split(".")
436
+ expr = ast.Name(id=parts[0], ctx=ast.Load())
437
+ for part in parts[1:]:
438
+ expr = ast.Attribute(value=expr, attr=part, ctx=ast.Load())
439
+ return expr
440
+
441
+ return ast.Name(id=annotation, ctx=ast.Load())
442
+
443
+ def _build_descriptor_call(self, field: _FieldSpec) -> ast.Call:
444
+ descriptor_cls = (
445
+ "FunctionRelationField" if field.is_relation else "FunctionField"
446
+ )
447
+ keywords: list[ast.keyword] = []
448
+ if field.type_field_name:
449
+ keywords.append(
450
+ ast.keyword(
451
+ arg="type_field_name",
452
+ value=ast.Constant(value=field.type_field_name),
453
+ )
454
+ )
455
+ return ast.Call(
456
+ func=ast.Name(id=descriptor_cls, ctx=ast.Load()),
457
+ args=[],
458
+ keywords=keywords,
459
+ )
460
+
461
+ def _resolve_annotation(
462
+ self,
463
+ node: _MappingNode,
464
+ properties_structure: PropertiesCompilerStructure,
465
+ ) -> str:
466
+ dto_field = node.dto_field
467
+ if not dto_field:
468
+ return "Any"
469
+
470
+ prop = properties_structure.properties.get(dto_field) if properties_structure else None
471
+ if not prop or not isinstance(prop.type, PrimitiveTypes):
472
+ return "Any"
473
+
474
+ return TypeUtil.property_type_to_python_type(prop.type)
475
+
476
+ # ------------------------------------------------------------------ #
477
+ # Utility helpers
478
+ # ------------------------------------------------------------------ #
479
+
480
+ def _sanitize_identifier(self, name: str) -> str:
481
+ candidate = CompilerStrUtil.to_snake_case(name)
482
+ candidate = re.sub(r"\W+", "_", candidate)
483
+ if not candidate:
484
+ candidate = "field"
485
+ if candidate[0].isdigit():
486
+ candidate = f"_{candidate}"
487
+ return candidate
488
+
489
+ def _ensure_unique_name(self, name: str, used: Dict[str, int]) -> str:
490
+ counter = used.setdefault(name, 0)
491
+ if counter == 0:
492
+ used[name] = 1
493
+ return name
494
+
495
+ while True:
496
+ candidate = f"{name}_{counter}"
497
+ counter += 1
498
+ if candidate not in used:
499
+ used[name] = counter
500
+ used[candidate] = 1
501
+ return candidate
502
+
503
+ def _extract_dto_field(self, source: Optional[str]) -> Optional[str]:
504
+ if not source:
505
+ return None
506
+
507
+ if source.startswith("body."):
508
+ segment = source[5:].split(".", 1)[0]
509
+ return CompilerStrUtil.to_snake_case(segment)
510
+
511
+ return None
512
+
513
+ def _parse_type_declaration(self, declaration: Optional[str]) -> tuple[str, bool]:
514
+ if not declaration:
515
+ return (None, False)
516
+
517
+ declaration = declaration.strip()
518
+ if declaration.endswith("[]"):
519
+ return (declaration[:-2], True)
520
+ return (declaration, False)
521
+
522
+ def _relation_annotation(self, class_name: str, is_array: bool) -> str:
523
+ if is_array:
524
+ return f"list[{class_name}]"
525
+ return class_name
526
+
527
+
528
+ def inject_function_bindings(
529
+ target_config: FunctionBindingConfig,
530
+ insert_output: FunctionCompilationOutput,
531
+ update_output: FunctionCompilationOutput,
532
+ ) -> FunctionBindingConfig:
533
+ """
534
+ Popula um FunctionBindingConfig a partir dos resultados de compilação das
535
+ funções de insert/update.
536
+ """
537
+
538
+ for dto_field, function_field in insert_output.field_bindings.items():
539
+ target_config.insert_fields[dto_field] = function_field
540
+
541
+ for dto_field, relation in insert_output.relation_bindings.items():
542
+ target_config.insert_relations[dto_field] = relation
543
+
544
+ for dto_field, function_field in update_output.field_bindings.items():
545
+ target_config.update_fields[dto_field] = function_field
546
+
547
+ for dto_field, relation in update_output.relation_bindings.items():
548
+ target_config.update_relations[dto_field] = relation
549
+
550
+ return target_config
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, Optional
5
+
6
+
7
+ @dataclass
8
+ class FunctionRelationBinding:
9
+ """
10
+ Representa o vínculo entre um campo do DTO e um campo declarado
11
+ no FunctionType correspondente.
12
+ """
13
+
14
+ field_name: str
15
+ function_type_class: Optional[str] = None
16
+
17
+
18
+ @dataclass
19
+ class FunctionBindingConfig:
20
+ """
21
+ Estrutura usada pelo PropertyCompiler para aplicar metadados de
22
+ binding entre DTOs e FunctionTypes (insert/update).
23
+ """
24
+
25
+ insert_fields: Dict[str, str] = field(default_factory=dict)
26
+ update_fields: Dict[str, str] = field(default_factory=dict)
27
+ insert_relations: Dict[str, FunctionRelationBinding] = field(
28
+ default_factory=dict
29
+ )
30
+ update_relations: Dict[str, FunctionRelationBinding] = field(
31
+ default_factory=dict
32
+ )
33
+
34
+ def merge_insert_field(self, dto_field: str, function_field: str) -> None:
35
+ if not dto_field or not function_field:
36
+ return
37
+ self.insert_fields[dto_field] = function_field
38
+
39
+ def merge_update_field(self, dto_field: str, function_field: str) -> None:
40
+ if not dto_field or not function_field:
41
+ return
42
+ self.update_fields[dto_field] = function_field
43
+
44
+ def merge_insert_relation(
45
+ self, dto_field: str, binding: FunctionRelationBinding
46
+ ) -> None:
47
+ if not dto_field or not binding:
48
+ return
49
+ self.insert_relations[dto_field] = binding
50
+
51
+ def merge_update_relation(
52
+ self, dto_field: str, binding: FunctionRelationBinding
53
+ ) -> None:
54
+ if not dto_field or not binding:
55
+ return
56
+ self.update_relations[dto_field] = binding
57
+
58
+
59
+ @dataclass
60
+ class FunctionCompilationOutput:
61
+ """
62
+ Resultado da compilação de um FunctionType (insert/update).
63
+ """
64
+
65
+ class_name: Optional[str] = None
66
+ code: Optional[str] = None
67
+ field_bindings: Dict[str, str] = field(default_factory=dict)
68
+ relation_bindings: Dict[str, FunctionRelationBinding] = field(
69
+ default_factory=dict
70
+ )
@@ -43,3 +43,7 @@ class CompilerResult:
43
43
  self.api_resource: str | None = None
44
44
  self.api_verbs: list[str] | None = None
45
45
  self.relations_dependencies: list[RelationDependency] | None = None
46
+ self.insert_function_class_name: str | None = None
47
+ self.source_insert_function: str | None = None
48
+ self.update_function_class_name: str | None = None
49
+ self.source_update_function: str | None = None