stackraise 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.
- stackraise/__init__.py +6 -0
- stackraise/ai/__init__.py +2 -0
- stackraise/ai/rpa.py +380 -0
- stackraise/ai/toolset.py +227 -0
- stackraise/app.py +23 -0
- stackraise/auth/__init__.py +2 -0
- stackraise/auth/model.py +24 -0
- stackraise/auth/service.py +240 -0
- stackraise/ctrl/__init__.py +4 -0
- stackraise/ctrl/change_stream.py +40 -0
- stackraise/ctrl/crud_controller.py +63 -0
- stackraise/ctrl/file_storage.py +68 -0
- stackraise/db/__init__.py +11 -0
- stackraise/db/adapter.py +60 -0
- stackraise/db/collection.py +292 -0
- stackraise/db/cursor.py +229 -0
- stackraise/db/document.py +282 -0
- stackraise/db/exceptions.py +9 -0
- stackraise/db/id.py +79 -0
- stackraise/db/index.py +84 -0
- stackraise/db/persistence.py +238 -0
- stackraise/db/pipeline.py +245 -0
- stackraise/db/protocols.py +141 -0
- stackraise/di.py +36 -0
- stackraise/event.py +150 -0
- stackraise/inflection.py +28 -0
- stackraise/io/__init__.py +3 -0
- stackraise/io/imap_client.py +400 -0
- stackraise/io/smtp_client.py +102 -0
- stackraise/logging.py +22 -0
- stackraise/model/__init__.py +11 -0
- stackraise/model/core.py +16 -0
- stackraise/model/dto.py +12 -0
- stackraise/model/email_message.py +88 -0
- stackraise/model/file.py +154 -0
- stackraise/model/name_email.py +45 -0
- stackraise/model/query_filters.py +231 -0
- stackraise/model/time_range.py +285 -0
- stackraise/model/validation.py +8 -0
- stackraise/templating/__init__.py +4 -0
- stackraise/templating/exceptions.py +23 -0
- stackraise/templating/image/__init__.py +2 -0
- stackraise/templating/image/model.py +51 -0
- stackraise/templating/image/processor.py +154 -0
- stackraise/templating/parser.py +156 -0
- stackraise/templating/pptx/__init__.py +3 -0
- stackraise/templating/pptx/pptx_engine.py +204 -0
- stackraise/templating/pptx/slide_renderer.py +181 -0
- stackraise/templating/tracer.py +57 -0
- stackraise-0.1.0.dist-info/METADATA +37 -0
- stackraise-0.1.0.dist-info/RECORD +52 -0
- stackraise-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
from pptx import Presentation
|
|
7
|
+
from pptx.slide import Slide
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ..parser import create_jinja_env, evaluate_condition, evaluate_conditional_block, extract_slide_conditional_block, extract_slide_loop
|
|
11
|
+
from .slide_renderer import render_slide
|
|
12
|
+
from ..tracer import Tracer
|
|
13
|
+
from ..exceptions import TemplateNotFoundError, RenderError
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
TODO: Implementar soporte para variables de tipo 'of' en los bucles de Jinja2.
|
|
18
|
+
Este operador permite iterar sobre listas y para cada elemento generar
|
|
19
|
+
un slide duplicado con el contexto actualizado.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PptxTemplateEngine:
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _resolve_path(ctx: dict, path: str):
|
|
27
|
+
"""
|
|
28
|
+
Resuelve rutas 'a.b.c' en dicts/objetos simples.
|
|
29
|
+
"""
|
|
30
|
+
cur = ctx
|
|
31
|
+
for part in [p.strip() for p in path.split('.') if p.strip()]:
|
|
32
|
+
if isinstance(cur, dict):
|
|
33
|
+
cur = cur.get(part, None)
|
|
34
|
+
else:
|
|
35
|
+
cur = getattr(cur, part, None)
|
|
36
|
+
if cur is None:
|
|
37
|
+
return None
|
|
38
|
+
return cur
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def render_from_template(
|
|
42
|
+
context: BaseModel,
|
|
43
|
+
template_path: str,
|
|
44
|
+
output_path: Optional[str] = None
|
|
45
|
+
) -> Union[str, BytesIO]:
|
|
46
|
+
tracer = Tracer()
|
|
47
|
+
tracer.start()
|
|
48
|
+
|
|
49
|
+
template_file = Path(template_path)
|
|
50
|
+
if not template_file.exists():
|
|
51
|
+
raise TemplateNotFoundError(f"Plantilla no encontrada en ruta: {template_path}")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
prs = Presentation(template_path)
|
|
55
|
+
# serializar el model pero RESTAURAR objetos ImageData top-level
|
|
56
|
+
context_dict = context.model_dump(by_alias=False, mode="python")
|
|
57
|
+
try:
|
|
58
|
+
# import tardío para evitar ciclos
|
|
59
|
+
from stackraise.templating.image.model import ImageData
|
|
60
|
+
except Exception:
|
|
61
|
+
ImageData = None
|
|
62
|
+
|
|
63
|
+
if ImageData is not None:
|
|
64
|
+
# Restaurar campos top-level que en el BaseModel son instancias ImageData
|
|
65
|
+
for name, raw_value in context.__dict__.items():
|
|
66
|
+
if isinstance(raw_value, ImageData):
|
|
67
|
+
context_dict[name] = raw_value
|
|
68
|
+
|
|
69
|
+
jinja_env = create_jinja_env()
|
|
70
|
+
|
|
71
|
+
slides = list(prs.slides)
|
|
72
|
+
slides_to_remove = []
|
|
73
|
+
|
|
74
|
+
for idx, slide in enumerate(slides):
|
|
75
|
+
loop_found = False
|
|
76
|
+
slide_level_conditional_found = False
|
|
77
|
+
|
|
78
|
+
# Buscar en todas las formas de texto de la slide
|
|
79
|
+
slide_text = ""
|
|
80
|
+
for shape in slide.shapes:
|
|
81
|
+
if shape.has_text_frame:
|
|
82
|
+
slide_text += shape.text_frame.text + " "
|
|
83
|
+
|
|
84
|
+
# Primero verificar si hay un bucle A NIVEL DE SLIDE
|
|
85
|
+
loop_info = extract_slide_loop(slide_text)
|
|
86
|
+
if loop_info:
|
|
87
|
+
loop_var, loop_list, loop_type = loop_info
|
|
88
|
+
loop_found = True
|
|
89
|
+
|
|
90
|
+
#items = context_dict.get(loop_list, []) or []
|
|
91
|
+
items = PptxTemplateEngine._resolve_path(context_dict, loop_list) or []
|
|
92
|
+
|
|
93
|
+
# if loop_type == "of":
|
|
94
|
+
# tracer.log_slide_duplication(idx, len(items))
|
|
95
|
+
# for i, item in enumerate(items):
|
|
96
|
+
# new_slide = PptxTemplateEngine._duplicate_slide(prs, slide)
|
|
97
|
+
# local_context = context_dict.copy()
|
|
98
|
+
# local_context[loop_var] = item
|
|
99
|
+
# render_slide(new_slide, local_context, jinja_env, tracer)
|
|
100
|
+
|
|
101
|
+
# slides_to_remove.append(slide)
|
|
102
|
+
# else:
|
|
103
|
+
# render_slide(slide, context_dict, jinja_env, tracer)
|
|
104
|
+
if loop_type == "of":
|
|
105
|
+
tracer.log_duplication(idx, len(items), "slide")
|
|
106
|
+
for i, item in enumerate(items):
|
|
107
|
+
new_slide = PptxTemplateEngine._duplicate_slide(prs, slide)
|
|
108
|
+
local_context = context_dict.copy()
|
|
109
|
+
local_context[loop_var] = item
|
|
110
|
+
# Evita que Jinja procese el 'for of' en la duplicada
|
|
111
|
+
PptxTemplateEngine._unwrap_slide_for_blocks(new_slide)
|
|
112
|
+
render_slide(new_slide, local_context, jinja_env, tracer)
|
|
113
|
+
|
|
114
|
+
slides_to_remove.append(slide)
|
|
115
|
+
else:
|
|
116
|
+
render_slide(slide, context_dict, jinja_env, tracer)
|
|
117
|
+
|
|
118
|
+
# Si no hay bucle, verificar si hay condicional A NIVEL DE SLIDE
|
|
119
|
+
elif not loop_found:
|
|
120
|
+
conditional_info = extract_slide_conditional_block(slide_text)
|
|
121
|
+
if conditional_info:
|
|
122
|
+
slide_level_conditional_found = True
|
|
123
|
+
try:
|
|
124
|
+
if conditional_info['has_block_structure']:
|
|
125
|
+
# Bloque condicional completo con if/elif/else
|
|
126
|
+
condition_result = evaluate_conditional_block(slide_text, context_dict, jinja_env)
|
|
127
|
+
tracer.log_conditional(idx, f"bloque condicional a nivel slide", condition_result)
|
|
128
|
+
else:
|
|
129
|
+
# Condicional simple
|
|
130
|
+
condition = conditional_info['condition']
|
|
131
|
+
condition_result = evaluate_condition(condition, context_dict, jinja_env)
|
|
132
|
+
tracer.log_conditional(idx, condition, condition_result)
|
|
133
|
+
|
|
134
|
+
if condition_result:
|
|
135
|
+
# Condición verdadera: renderizar la slide
|
|
136
|
+
render_slide(slide, context_dict, jinja_env, tracer)
|
|
137
|
+
else:
|
|
138
|
+
# Condición falsa: marcar slide para eliminación
|
|
139
|
+
slides_to_remove.append(slide)
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
tracer.log_error(f"Error evaluando condición en slide {idx}: {e}")
|
|
143
|
+
# En caso de error, mantener la slide pero sin renderizar
|
|
144
|
+
render_slide(slide, context_dict, jinja_env, tracer)
|
|
145
|
+
|
|
146
|
+
# Si no hay ni bucle ni condicional A NIVEL DE SLIDE, renderizar normalmente
|
|
147
|
+
# (los condicionales a nivel de texto se manejan en render_slide)
|
|
148
|
+
if not loop_found and not slide_level_conditional_found:
|
|
149
|
+
render_slide(slide, context_dict, jinja_env, tracer)
|
|
150
|
+
|
|
151
|
+
# Remover slides marcadas para eliminación
|
|
152
|
+
for slide in slides_to_remove:
|
|
153
|
+
PptxTemplateEngine._remove_slide(prs, slide)
|
|
154
|
+
|
|
155
|
+
if output_path:
|
|
156
|
+
prs.save(output_path)
|
|
157
|
+
tracer.end()
|
|
158
|
+
return str(output_path)
|
|
159
|
+
else:
|
|
160
|
+
pptx_io = BytesIO()
|
|
161
|
+
prs.save(pptx_io)
|
|
162
|
+
pptx_io.seek(0)
|
|
163
|
+
tracer.end()
|
|
164
|
+
return pptx_io
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
tracer.log_error(f"Error en render_from_template: {e}")
|
|
168
|
+
raise RenderError(f"Fallo al renderizar plantilla PPTX: {e}")
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _duplicate_slide(prs: Presentation, slide: Slide) -> Slide:
|
|
172
|
+
new_slide = prs.slides.add_slide(slide.slide_layout)
|
|
173
|
+
for shape in slide.shapes:
|
|
174
|
+
el = shape.element
|
|
175
|
+
new_el = deepcopy(el)
|
|
176
|
+
new_slide.shapes._spTree.insert_element_before(new_el, 'p:extLst')
|
|
177
|
+
return new_slide
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _remove_slide(prs: Presentation, slide: Slide):
|
|
181
|
+
slide_id = slide.slide_id
|
|
182
|
+
slides = prs.slides._sldIdLst
|
|
183
|
+
for sld in slides:
|
|
184
|
+
if sld.get("id") == str(slide_id):
|
|
185
|
+
slides.remove(sld)
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _unwrap_slide_for_blocks(slide: Slide) -> None:
|
|
190
|
+
"""Quita etiquetas {% for ... %}/{% endfor %} in-place sin tocar formato."""
|
|
191
|
+
start_tag = re.compile(r"""{%\s*for\s+\w+\s+(?:of|in)\s+.+?\s*%}""")
|
|
192
|
+
end_tag = re.compile(r"""{%\s*endfor\s*%}""")
|
|
193
|
+
for shape in slide.shapes:
|
|
194
|
+
if not getattr(shape, "has_text_frame", False):
|
|
195
|
+
continue
|
|
196
|
+
tf = shape.text_frame
|
|
197
|
+
for p in tf.paragraphs:
|
|
198
|
+
for r in p.runs:
|
|
199
|
+
if not r.text:
|
|
200
|
+
continue
|
|
201
|
+
# Elimina sólo las etiquetas, deja el cuerpo intacto
|
|
202
|
+
txt = start_tag.sub("", r.text)
|
|
203
|
+
txt = end_tag.sub("", txt)
|
|
204
|
+
r.text = txt
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from pptx.slide import Slide
|
|
2
|
+
from pptx.shapes.base import BaseShape
|
|
3
|
+
from pptx.dml.color import RGBColor
|
|
4
|
+
from stackraise.templating.image.processor import ImageProcessor
|
|
5
|
+
from ..parser import has_image_variables, render_text
|
|
6
|
+
from ..tracer import Tracer
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
def remove_for_of_blocks(text: str) -> str:
|
|
10
|
+
# Desenrolla bucles a nivel de slide: conserva solo el cuerpo
|
|
11
|
+
#pattern = r"""{%\s*for\s+\w+\s+(?:of|in)\s+\w+\s*%}(.*?){%\s*endfor\s*%}"""
|
|
12
|
+
pattern = r"""{%\s*for\s+\w+\s+(?:of|in)\s+.+?\s*%}(.*?){%\s*endfor\s*%}"""
|
|
13
|
+
return re.sub(pattern, r"\1", text, flags=re.DOTALL)
|
|
14
|
+
|
|
15
|
+
def has_conditional_blocks(text: str) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Verifica si el texto contiene bloques condicionales.
|
|
18
|
+
"""
|
|
19
|
+
pattern = r"""{%\s*if\s+.+?\s*%}"""
|
|
20
|
+
return bool(re.search(pattern, text))
|
|
21
|
+
|
|
22
|
+
def has_text_level_loops(text: str) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Verifica si el texto contiene bucles que deben procesarse como texto
|
|
25
|
+
(no como duplicación de slides).
|
|
26
|
+
"""
|
|
27
|
+
# Buscar bucles que estén mezclados con otro contenido
|
|
28
|
+
lines = text.strip().split('\n')
|
|
29
|
+
has_loop = False
|
|
30
|
+
has_other_content = False
|
|
31
|
+
|
|
32
|
+
for line in lines:
|
|
33
|
+
line = line.strip()
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
#if re.search(r'{%\s*for\s+\w+\s+(of|in)\s+\w+\s*%}', line):
|
|
37
|
+
if re.search(r'{%\s*for\s+\w+\s+(?:of|in)\s+.+?\s*%}', line):
|
|
38
|
+
has_loop = True
|
|
39
|
+
#elif not re.search(r'{%\s*(for|endfor|if|elif|else|endif)\s*.*%}', line):
|
|
40
|
+
elif not re.search(r'{%\s*(for|endfor|if|elif|else|endif)\s*.*%}', line):
|
|
41
|
+
has_other_content = True
|
|
42
|
+
|
|
43
|
+
return has_loop and has_other_content
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _copy_run_font(dst_run, src_run):
|
|
47
|
+
try:
|
|
48
|
+
dst_font = dst_run.font
|
|
49
|
+
src_font = src_run.font
|
|
50
|
+
dst_font.name = src_font.name
|
|
51
|
+
dst_font.size = src_font.size
|
|
52
|
+
dst_font.bold = src_font.bold
|
|
53
|
+
dst_font.italic = src_font.italic
|
|
54
|
+
dst_font.underline = src_font.underline
|
|
55
|
+
# Color: intenta RGB si existe
|
|
56
|
+
try:
|
|
57
|
+
rgb = src_font.color.rgb
|
|
58
|
+
if rgb is not None:
|
|
59
|
+
dst_font.color.rgb = RGBColor(rgb[0], rgb[1], rgb[2]) if isinstance(rgb, tuple) else rgb
|
|
60
|
+
except Exception:
|
|
61
|
+
try:
|
|
62
|
+
dst_font.color.theme_color = src_font.color.theme_color
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _snapshot_base_style(text_frame):
|
|
69
|
+
base_para = text_frame.paragraphs[0] if text_frame.paragraphs else None
|
|
70
|
+
base_run = base_para.runs[0] if (base_para and base_para.runs) else None
|
|
71
|
+
return base_para, base_run
|
|
72
|
+
|
|
73
|
+
def write_text_preserving_formatting(text_frame, text: str):
|
|
74
|
+
# Captura estilo base antes de limpiar
|
|
75
|
+
base_para_before, base_run_before = _snapshot_base_style(text_frame)
|
|
76
|
+
|
|
77
|
+
# Limpia el contenido (deja un párrafo vacío)
|
|
78
|
+
try:
|
|
79
|
+
text_frame.clear()
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
lines = (text or "").splitlines() or [""]
|
|
84
|
+
for idx, line in enumerate(lines):
|
|
85
|
+
para = text_frame.paragraphs[0] if idx == 0 else text_frame.add_paragraph()
|
|
86
|
+
|
|
87
|
+
# Restituye nivel/alineación del párrafo base si existe
|
|
88
|
+
try:
|
|
89
|
+
if base_para_before is not None:
|
|
90
|
+
para.level = base_para_before.level
|
|
91
|
+
para.alignment = base_para_before.alignment
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
run = para.runs[0] if para.runs else para.add_run()
|
|
96
|
+
# Copia estilo del run base si existe
|
|
97
|
+
if base_run_before is not None:
|
|
98
|
+
_copy_run_font(run, base_run_before)
|
|
99
|
+
run.text = line
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def has_only_simple_variables(text: str) -> bool:
|
|
103
|
+
# Hay variables {{ ... }} pero no bloques {% ... %}
|
|
104
|
+
return ("{{" in text) and ("{%" not in text)
|
|
105
|
+
|
|
106
|
+
def render_runs_preserving_formatting(text_frame, context: dict, jinja_env):
|
|
107
|
+
# Sustituye {{ expr }} dentro de cada run conservando su formato
|
|
108
|
+
var_re = re.compile(r"\{\{\s*(.*?)\s*\}\}")
|
|
109
|
+
for paragraph in text_frame.paragraphs:
|
|
110
|
+
for run in paragraph.runs:
|
|
111
|
+
original = run.text or ""
|
|
112
|
+
if not original:
|
|
113
|
+
continue
|
|
114
|
+
def repl(m: re.Match) -> str:
|
|
115
|
+
expr = m.group(1)
|
|
116
|
+
try:
|
|
117
|
+
tpl = jinja_env.from_string("{{ " + expr + " }}")
|
|
118
|
+
return str(tpl.render(context))
|
|
119
|
+
except Exception:
|
|
120
|
+
# Si falla, deja el placeholder intacto
|
|
121
|
+
return m.group(0)
|
|
122
|
+
replaced = var_re.sub(repl, original)
|
|
123
|
+
run.text = replaced
|
|
124
|
+
|
|
125
|
+
def render_slide(slide: Slide, context: dict, jinja_env, tracer: Tracer):
|
|
126
|
+
# Primero procesar imágenes (variables ImageData)
|
|
127
|
+
ImageProcessor.process_slide_images(slide, context, tracer)
|
|
128
|
+
|
|
129
|
+
# Luego procesar texto normal
|
|
130
|
+
for shape in slide.shapes:
|
|
131
|
+
if not getattr(shape, "has_text_frame", False):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
text_frame = shape.text_frame
|
|
135
|
+
all_text = ""
|
|
136
|
+
for paragraph in text_frame.paragraphs:
|
|
137
|
+
all_text += "".join(run.text for run in paragraph.runs) + "\n"
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Verificar el tipo de contenido
|
|
141
|
+
has_images = has_image_variables(all_text, context)
|
|
142
|
+
has_conditionals = has_conditional_blocks(all_text)
|
|
143
|
+
has_loops = has_text_level_loops(all_text)
|
|
144
|
+
|
|
145
|
+
if has_conditionals or has_loops or has_images:
|
|
146
|
+
# Si hay condicionales, bucles o imágenes, renderizar TODO con Jinja2
|
|
147
|
+
# Contenido complejo: render completo (posible pérdida de formatos finos)
|
|
148
|
+
rendered_text = render_text(all_text, context, jinja_env)
|
|
149
|
+
write_text_preserving_formatting(text_frame, rendered_text or "")
|
|
150
|
+
tracer.log_variable(all_text.strip(), (rendered_text or "").strip())
|
|
151
|
+
else:
|
|
152
|
+
# Sólo variables simples: sustituir por run conservando formato
|
|
153
|
+
render_runs_preserving_formatting(text_frame, context, jinja_env)
|
|
154
|
+
tracer.log_variable(all_text.strip(), (text_frame.text or "").strip())
|
|
155
|
+
|
|
156
|
+
# Limpiar el text_frame y agregar el texto renderizado
|
|
157
|
+
# while len(text_frame.paragraphs) > 1:
|
|
158
|
+
# text_frame._element.remove(text_frame._element[-1])
|
|
159
|
+
|
|
160
|
+
# paragraph = text_frame.paragraphs[0]
|
|
161
|
+
# while len(paragraph.runs) > 1:
|
|
162
|
+
# paragraph._element.remove(paragraph.runs[-1]._r)
|
|
163
|
+
|
|
164
|
+
# paragraph.runs[0].text = rendered_text
|
|
165
|
+
|
|
166
|
+
# Escritura segura: evita IndexError en runs vacíos
|
|
167
|
+
|
|
168
|
+
# try:
|
|
169
|
+
# text_frame.clear() # deja un párrafo vacío
|
|
170
|
+
# except Exception as e:
|
|
171
|
+
# print(f"No se pudo limpiar el text_frame: {e}")
|
|
172
|
+
# pass
|
|
173
|
+
# text_frame.text = rendered_text or ""
|
|
174
|
+
|
|
175
|
+
write_text_preserving_formatting(text_frame, rendered_text or "")
|
|
176
|
+
tracer.log_variable(all_text.strip(), (rendered_text or "").strip())
|
|
177
|
+
|
|
178
|
+
#tracer.log_variable(all_text.strip(), rendered_text.strip())
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
tracer.log_error(f"Error al renderizar texto '{all_text.strip()}': {e}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from time import perf_counter
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger("pptx_template_engine")
|
|
5
|
+
logger.setLevel(logging.DEBUG)
|
|
6
|
+
handler = logging.StreamHandler()
|
|
7
|
+
formatter = logging.Formatter("[PPTX-TEMPLATE] %(levelname)s - %(message)s")
|
|
8
|
+
handler.setFormatter(formatter)
|
|
9
|
+
logger.addHandler(handler)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Tracer:
|
|
13
|
+
"""Tracer genérico para motores de plantillas"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, logger_name: str = "template_engine"):
|
|
16
|
+
self.start_time = None
|
|
17
|
+
self.logger = logging.getLogger(logger_name)
|
|
18
|
+
if not self.logger.handlers:
|
|
19
|
+
self._setup_logger()
|
|
20
|
+
|
|
21
|
+
def _setup_logger(self):
|
|
22
|
+
"""Configura el logger si no está configurado"""
|
|
23
|
+
self.logger.setLevel(logging.DEBUG)
|
|
24
|
+
handler = logging.StreamHandler()
|
|
25
|
+
formatter = logging.Formatter(f"[{self.logger.name.upper()}] %(levelname)s - %(message)s")
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
self.logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
def start(self, engine_type: str = "plantilla"):
|
|
30
|
+
"""Inicia el trazado"""
|
|
31
|
+
self.start_time = perf_counter()
|
|
32
|
+
self.logger.info(f"Inicio del render de {engine_type}")
|
|
33
|
+
|
|
34
|
+
def end(self):
|
|
35
|
+
"""Finaliza el trazado"""
|
|
36
|
+
duration = perf_counter() - self.start_time
|
|
37
|
+
self.logger.info(f"Render finalizado en {duration:.2f} segundos")
|
|
38
|
+
|
|
39
|
+
def log_variable(self, var: str, value: any):
|
|
40
|
+
"""Registra el renderizado de una variable"""
|
|
41
|
+
self.logger.debug(f"Variable '{{ {var} }}' → {value}")
|
|
42
|
+
|
|
43
|
+
def log_duplication(self, item_idx: int, count: int, item_type: str = "elemento"):
|
|
44
|
+
"""Registra la duplicación de elementos"""
|
|
45
|
+
self.logger.info(f"{item_type.capitalize()} {item_idx} duplicado {count} veces por bucle")
|
|
46
|
+
|
|
47
|
+
def log_conditional(self, item_idx: int, condition: str, result: bool, item_type: str = "elemento"):
|
|
48
|
+
"""Registra la evaluación de condicionales"""
|
|
49
|
+
self.logger.info(f"Condicional '{condition}' evaluada como {result} en {item_type} {item_idx}")
|
|
50
|
+
|
|
51
|
+
def log_error(self, message: str):
|
|
52
|
+
"""Registra un error"""
|
|
53
|
+
self.logger.error(message)
|
|
54
|
+
|
|
55
|
+
def log_info(self, message: str):
|
|
56
|
+
"""Registra información general"""
|
|
57
|
+
self.logger.info(message)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackraise
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: J.David Luque
|
|
6
|
+
Author-email: jdluque@leitat.org
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: aioimaplib (>=2.0.1,<3.0.0)
|
|
13
|
+
Requires-Dist: aiosmtplib (>=4.0.1,<5.0.0)
|
|
14
|
+
Requires-Dist: docxtpl (>=0.20.0,<0.21.0)
|
|
15
|
+
Requires-Dist: email-validator (>=2.2.0,<3.0.0)
|
|
16
|
+
Requires-Dist: fastapi[standard] (>=0.115.6,<0.116.0)
|
|
17
|
+
Requires-Dist: inflector (>=3.1.1,<4.0.0)
|
|
18
|
+
Requires-Dist: motor (>=3.6.0,<4.0.0)
|
|
19
|
+
Requires-Dist: openai (>=2.15.0,<3.0.0)
|
|
20
|
+
Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
|
|
21
|
+
Requires-Dist: pydantic (>=2.11,<3.0)
|
|
22
|
+
Requires-Dist: pydantic-settings-yaml (>=0.2.0,<0.3.0)
|
|
23
|
+
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
|
24
|
+
Requires-Dist: pymongo (>=4.13.2,<5.0.0)
|
|
25
|
+
Requires-Dist: pypdf2 (>=3.0.1,<4.0.0)
|
|
26
|
+
Requires-Dist: python-pptx (==1.0.2)
|
|
27
|
+
Requires-Dist: python-slugify (>=8.0.4,<9.0.0)
|
|
28
|
+
Requires-Dist: xlsxtpl (>=0.3.1,<0.4.0)
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Nothingad backend
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
just bulkload # carga data/workbook.xlsx en la base de datos
|
|
35
|
+
|
|
36
|
+
just launch # ejecuta el backend
|
|
37
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
stackraise/__init__.py,sha256=T6LUkt1rdxY_zoFU2CgBaEG0bDBLRy6Nf9uv6d6dnKQ,119
|
|
2
|
+
stackraise/ai/__init__.py,sha256=SXz7CzvgQHiAbnsFvyKBsTmH1QpuXrlZg3o2kDQHLbw,41
|
|
3
|
+
stackraise/ai/rpa.py,sha256=sSZTpW_OlsOR2312cI-ttuh9GsggZoyXxJOZyZY2CT4,12371
|
|
4
|
+
stackraise/ai/toolset.py,sha256=iOIpuin_hpR6WGjj1DqHme2Lb-0HEba-lW-cIOwkCR8,6997
|
|
5
|
+
stackraise/app.py,sha256=toyeotSU_XL2U0Se8mCtp8tM61omehs34jFoBzV52L8,631
|
|
6
|
+
stackraise/auth/__init__.py,sha256=MzHWdMbPciCE17HfZx_Irr-lax8swtL85W_IubFqIGY,64
|
|
7
|
+
stackraise/auth/model.py,sha256=biudWx1IrwvwCgW5R6Hl0VTUPuQpk10my03qYj2SKAM,679
|
|
8
|
+
stackraise/auth/service.py,sha256=A20CwObI3VCXHBuX5eGnzHOiNgD1UhYeOL65aSPGCWw,7783
|
|
9
|
+
stackraise/ctrl/__init__.py,sha256=IDIG0MsDc8nxguMFttkqE_cQtH-FdMull4xd52ypVfo,99
|
|
10
|
+
stackraise/ctrl/change_stream.py,sha256=6QFp_Z1WL168p3IzFeu4mNJ2USaThPp-Qk7JRrxYb68,1180
|
|
11
|
+
stackraise/ctrl/crud_controller.py,sha256=YjN8RiimGfL_sISzjgwl0hjPNqOOmj4AtNPdTaL1-fA,2123
|
|
12
|
+
stackraise/ctrl/file_storage.py,sha256=MZnD45j4DCXVEpS1ricgjLw2-3dQ0NEloCOEUSVODBA,2262
|
|
13
|
+
stackraise/db/__init__.py,sha256=whU36yRPuwhQ45r9H493NCtJZsmkoU1AaDfK1401Bf0,272
|
|
14
|
+
stackraise/db/adapter.py,sha256=wQTbC1m7KKhKl31p847gnqYtVkfMBJR1g2Rj1UF6D78,1570
|
|
15
|
+
stackraise/db/collection.py,sha256=pGd2VrZ06El5e-HnT_Y2rQpbGz6JTD5KJGh7iL65BZ4,10752
|
|
16
|
+
stackraise/db/cursor.py,sha256=CBg2FutsGplst5g3F0UKYloaQC4i_7vq0h9qPicc_ZE,6504
|
|
17
|
+
stackraise/db/document.py,sha256=k1_7qNW9rhbpHK6VQcKmJKhuf9mEHDQB-umpP1P4OH8,9444
|
|
18
|
+
stackraise/db/exceptions.py,sha256=ICrWbW3IsZIOYS0OTI9RROmkmOeAlqKlOLQyNvF3Q-0,308
|
|
19
|
+
stackraise/db/id.py,sha256=cJl45y4HmtpAAVXasnmVtCIRB-gkuHexAAVNzHHF2Es,2426
|
|
20
|
+
stackraise/db/index.py,sha256=UeSa_eAkx2m_eVN2fOnjectXnnqp1tg_36tynDrabBM,2735
|
|
21
|
+
stackraise/db/persistence.py,sha256=qmB1371cluPBdtbJnYbMzFHCODxFDfob07PUe_trEjY,6776
|
|
22
|
+
stackraise/db/pipeline.py,sha256=qthH3b8rvE71r909RABpUpvJVDqyKhKQdrAvDpVX3HE,7223
|
|
23
|
+
stackraise/db/protocols.py,sha256=HCC3ic2MI47wpHJnYbFxwpXaJK2aPgMa9Q0sKGKqAoE,4545
|
|
24
|
+
stackraise/di.py,sha256=eg-nG0GtQZsFs6xyQ3ZKx5sY3EG6TQPe5N8rq8MMixc,921
|
|
25
|
+
stackraise/event.py,sha256=e-B-FYvyZN7xcv9ELOBlfDTrNsEh8taTEKoMDN4FZDQ,4184
|
|
26
|
+
stackraise/inflection.py,sha256=aHPU69PKs9qmv2L4HdY04Uqq6kqPBYa1k6gCKd0OZIo,542
|
|
27
|
+
stackraise/io/__init__.py,sha256=NZn3USTIx9jUAJ0J-KR63wm3HCqErGAl-oUwjgezoW4,84
|
|
28
|
+
stackraise/io/imap_client.py,sha256=zJDdgFqvCsq2IPon9Fs3qbKJSiqfw9D7-s54Wc5T-Po,15096
|
|
29
|
+
stackraise/io/smtp_client.py,sha256=9BCHWPb2hvXhRINPbkX43GbIgLDSZicfWJaV0SXenH0,3243
|
|
30
|
+
stackraise/logging.py,sha256=aAgognVCEjCMuhaApBKBGyc13TKnsRgYu9STIJUzLbY,470
|
|
31
|
+
stackraise/model/__init__.py,sha256=iyO_Q8Pnl-6UQCTV4IrZFx1N1TyeT8TX5TsbINM6OCs,261
|
|
32
|
+
stackraise/model/core.py,sha256=v9TpsUtQucvv9P1jB6NBcb2d4_S0mooujAt0rcbsHcM,415
|
|
33
|
+
stackraise/model/dto.py,sha256=SGm6JZ8JwW27iX5v4Ou7D-b73WpZY9Cb49PxdqK6yNs,277
|
|
34
|
+
stackraise/model/email_message.py,sha256=qZgj4HgLARrwNd34vW27lL5A9PBN1nl6MSZcwEbFnN4,2003
|
|
35
|
+
stackraise/model/file.py,sha256=Fb1yYpQbUgfL0HkJ9YEw0BpqJcpxDIMNKyD0TCYZh-w,5019
|
|
36
|
+
stackraise/model/name_email.py,sha256=_X9QCTqPOA977wvzZzObIHyIZNib63zpE4IKzYfeq5U,1408
|
|
37
|
+
stackraise/model/query_filters.py,sha256=1m25avpPIFPWJ6e_MVS0ilFtTEKUSp4JqGeq1RlpQDM,7391
|
|
38
|
+
stackraise/model/time_range.py,sha256=2uy7ik6u7W-AQT2ZPqIvVPRcMe4uryC94gSnz4awJWQ,8703
|
|
39
|
+
stackraise/model/validation.py,sha256=jdqVutwc7M7qjdtv8VKkYpCpyqSHWe2xSx0sNp1WT3o,255
|
|
40
|
+
stackraise/templating/__init__.py,sha256=S6Na4oFzHdxWM1YKzdPbUHMqeEHNXFjdCr5iHp_AsdI,84
|
|
41
|
+
stackraise/templating/exceptions.py,sha256=MlR9bw6SNu5Incd9CrHgz7iBPCWaAh6sL-efaxE_L0Q,412
|
|
42
|
+
stackraise/templating/image/__init__.py,sha256=jk_WNdl-lg8bGUpFxRhU4nZ7tmmvsxne1KFoX42H9rM,45
|
|
43
|
+
stackraise/templating/image/model.py,sha256=Bk0sGN7mdrgw7bg8YFf8HOhjJbivdmeo1NG-wvC9FZ0,1959
|
|
44
|
+
stackraise/templating/image/processor.py,sha256=Lz6xFXxXvcsdpD7EeaeWbUYD-Z9HZHQRBxWQ5jVpMQ0,6697
|
|
45
|
+
stackraise/templating/parser.py,sha256=5jmBFtn4hkrm8SfLc-fFTqIE2mJmNCqnYf3r7BGSg0Y,5743
|
|
46
|
+
stackraise/templating/pptx/__init__.py,sha256=nfC9DiY6QWGZ83grgInEYbTgZDKBu2DqOlWjSTk_bd8,83
|
|
47
|
+
stackraise/templating/pptx/pptx_engine.py,sha256=Ykb25fkJSvYrAzADPVSgQ_R6v558JzJETkpbo8wTz68,8916
|
|
48
|
+
stackraise/templating/pptx/slide_renderer.py,sha256=5jGisi-_Fy8iTGgszGRObTbqzaOKD_5LdzLw8uEil0E,7149
|
|
49
|
+
stackraise/templating/tracer.py,sha256=tJdBHWDyRVnAgtES26t26elgx8DdNVZKhAnP-ts0FhY,2214
|
|
50
|
+
stackraise-0.1.0.dist-info/METADATA,sha256=zXm3qLkPezeeA8W18ZG3CbTVE6tlkffo53divcuejwI,1243
|
|
51
|
+
stackraise-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
52
|
+
stackraise-0.1.0.dist-info/RECORD,,
|