Contenuti




Generatore di Documenti Python: Guida alla Creazione di Documenti Personalizzati

Sviluppa un sistema completo per generare PDF, DOCX, HTML e report automatizzati con template dinamici


Generatore di Documenti Python: guida alla creazione di documenti personalizzati

Creare un generatore di documenti automatizzato è una delle applicazioni più pratiche di Python in ambito aziendale. Questa guida ti mostrerà come sviluppare un sistema completo per generare documenti professionali in formato PDF, DOCX e HTML utilizzando template dinamici, integrazione database e automazione di livello enterprise.

In questo articolo
  • Architettura modulare per generazione documenti multi-formato
  • Motori di template avanzati con Jinja2 per contenuti dinamici
  • Librerie specializzate (ReportLab, python-docx, WeasyPrint)
  • Integrazione database per automazione report
  • Sistema plugin per estensibilità
  • Distribuzione e containerizzazione per produzione
  • Buone pratiche per performance e manutenibilità

Indice della Guida

Parte I - Fondamenti e configurazione

  1. Architettura del sistema
  2. Configurazione ambiente di sviluppo
  3. Dipendenze e librerie

Parte II - Generazione multi-formato

  1. Motore template con Jinja2
  2. Generazione PDF con ReportLab
  3. Documenti DOCX con python-docx
  4. Esportazione HTML e CSS

Parte III - Funzionalita avanzate

  1. Integrazione database
  2. Sistema di plugin
  3. Generazione grafici e tabelle
  4. Automazione e pianificazione

Parte IV - Ambito enterprise e distribuzione

  1. API REST per integrazione
  2. Test e controllo qualita
  3. Distribuzione e containerizzazione
  4. Monitoraggio e prestazioni

Architettura del sistema

Pattern di progettazione e struttura

Il nostro generatore di documenti seguira un’architettura modulare basata su pattern consolidati:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# core/base.py
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
from pathlib import Path
import logging
from dataclasses import dataclass, field
from enum import Enum

class OutputFormat(Enum):
    """Formati di output supportati"""
    PDF = "pdf"
    DOCX = "docx"
    HTML = "html"
    TXT = "txt"
    XLSX = "xlsx"

@dataclass
class DocumentConfig:
    """Configurazione per la generazione del documento"""
    template_name: str
    output_format: OutputFormat
    output_path: Path
    data: Dict[str, Any] = field(default_factory=dict)
    options: Dict[str, Any] = field(default_factory=dict)

class DocumentGenerator(ABC):
    """Classe base astratta per tutti i generatori"""

    def __init__(self, config: DocumentConfig):
        self.config = config
        self.logger = logging.getLogger(self.__class__.__name__)

    @abstractmethod
    def generate(self) -> Path:
        """Genera il documento e restituisce il path del file creato"""
        pass

    @abstractmethod
    def validate_template(self, template_path: Path) -> bool:
        """Valida la correttezza del template"""
        pass

class DocumentFactory:
    """Factory per creare generatori specifici per formato"""

    _generators = {}

    @classmethod
    def register_generator(cls, format_type: OutputFormat, generator_class):
        """Registra un nuovo generatore per un formato"""
        cls._generators[format_type] = generator_class

    @classmethod
    def create_generator(cls, config: DocumentConfig) -> DocumentGenerator:
        """Crea il generatore appropriato basato sulla configurazione"""
        generator_class = cls._generators.get(config.output_format)
        if not generator_class:
            raise ValueError(f"Generatore non supportato per formato: {config.output_format}")
        return generator_class(config)

Struttura Directory del Progetto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
document_generator/
├── core/                    # Moduli core del sistema
│   ├── __init__.py
│   ├── base.py             # Classi base e interfaces
│   ├── factory.py          # Pattern factory
│   └── exceptions.py       # Eccezioni personalizzate
├── generators/             # Generatori per formato specifico
│   ├── __init__.py
│   ├── pdf_generator.py    # Generatore PDF
│   ├── docx_generator.py   # Generatore DOCX
│   ├── html_generator.py   # Generatore HTML
│   └── excel_generator.py  # Generatore Excel
├── templates/              # Template per ogni formato
│   ├── pdf/
│   ├── docx/
│   ├── html/
│   └── shared/            # Componenti condivisi
├── plugins/               # Sistema di plugin estensibili
│   ├── __init__.py
│   ├── charts/           # Plugin per grafici
│   ├── database/         # Plugin per database
│   └── custom/           # Plugin personalizzati
├── utils/                # Utilities e helper functions
│   ├── __init__.py
│   ├── data_processing.py
│   ├── file_utils.py
│   └── validation.py
├── api/                  # API REST (opzionale)
│   ├── __init__.py
│   ├── main.py          # FastAPI app
│   └── routes/
├── tests/               # Test suite completa
│   ├── unit/
│   ├── integration/
│   └── fixtures/
├── config/              # File di configurazione
│   ├── settings.py
│   └── logging.conf
├── docs/               # Documentazione
├── docker/             # Dockerfile e compose
├── requirements.txt    # Dipendenze Python
└── setup.py           # Package setup

Configurazione ambiente di sviluppo

Installazione e Configurazione Iniziale

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Crea virtual environment
python -m venv document_generator_env
source document_generator_env/bin/activate  # Linux/Mac
# document_generator_env\Scripts\activate  # Windows

# Aggiorna pip
python -m pip install --upgrade pip

# Crea struttura progetto
mkdir -p document_generator/{core,generators,templates,plugins,utils,api,tests,config,docs}
cd document_generator

# Inizializza git repository
git init

Per una spiegazione completa di venv e Poetry, vedi guida ai virtual environment.

Requirements.txt Completo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Core dependencies
jinja2==3.1.2
python-docx==0.8.11
reportlab==4.0.4
weasyprint==60.1
openpyxl==3.1.2

# Template e rendering
markdown==3.5.1
pygments==2.16.1
pillow==10.0.1

# Database integration
sqlalchemy==2.0.23
pandas==2.1.3
psycopg2-binary==2.9.7  # PostgreSQL
pymongo==4.6.0          # MongoDB

# API e web framework
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
python-multipart==0.0.6

# Grafici e visualizzazione
matplotlib==3.8.1
seaborn==0.13.0
plotly==5.17.0

# Utilities
python-dotenv==1.0.0
click==8.1.7
rich==13.7.0
tqdm==4.66.1

# Testing
pytest==7.4.3
pytest-cov==4.1.0
pytest-asyncio==0.21.1
factory-boy==3.3.0

# Development tools
black==23.11.0
flake8==6.1.0
mypy==1.7.1
pre-commit==3.5.0

# Logging e monitoring
structlog==23.2.0
sentry-sdk==1.38.0

# Containerization
gunicorn==21.2.0

Configurazione Base del Sistema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# config/settings.py
import os
from pathlib import Path
from typing import Dict, Any
from dataclasses import dataclass, field

@dataclass
class AppConfig:
    """Configurazione principale dell'applicazione"""

    # Paths
    BASE_DIR: Path = Path(__file__).parent.parent
    TEMPLATES_DIR: Path = BASE_DIR / "templates"
    OUTPUT_DIR: Path = BASE_DIR / "output"
    PLUGINS_DIR: Path = BASE_DIR / "plugins"

    # Database
    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///documents.db")

    # Logging
    LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
    LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

    # Prestazioni
    MAX_WORKERS: int = int(os.getenv("MAX_WORKERS", "4"))
    CACHE_TTL: int = int(os.getenv("CACHE_TTL", "3600"))

    # Security
    SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-here")
    ALLOWED_HOSTS: list = field(default_factory=lambda: ["localhost", "127.0.0.1"])

    # File limits
    MAX_FILE_SIZE: int = 50 * 1024 * 1024  # 50MB
    MAX_TEMPLATE_SIZE: int = 10 * 1024 * 1024  # 10MB

    def __post_init__(self):
        """Crea directory necessarie se non esistono"""
        self.OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
        (self.BASE_DIR / "logs").mkdir(exist_ok=True)

# Istanza globale configurazione
config = AppConfig()

Dipendenze e librerie

Panoramica delle librerie core

1. Jinja2 - Motore template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# utils/template_loader.py
from jinja2 import Environment, FileSystemLoader, select_autoescape, Template
from jinja2.exceptions import TemplateError, TemplateNotFound
from pathlib import Path
from typing import Dict, Any, Optional
import json

class TemplateManager:
    """Gestione avanzata dei template Jinja2"""

    def __init__(self, templates_dir: Path):
        self.templates_dir = templates_dir
        self.env = Environment(
            loader=FileSystemLoader(str(templates_dir)),
            autoescape=select_autoescape(['html', 'xml']),
            trim_blocks=True,
            lstrip_blocks=True
        )

        # Registra filtri personalizzati
        self._register_custom_filters()

    def _register_custom_filters(self):
        """Registra filtri Jinja2 personalizzati"""

        @self.env.filter
        def currency(value: float, symbol: str = "€") -> str:
            """Formatta come valuta"""
            return f"{symbol} {value:,.2f}"

        @self.env.filter
        def date_format(value, format_string: str = "%d/%m/%Y") -> str:
            """Formatta data personalizzata"""
            if hasattr(value, 'strftime'):
                return value.strftime(format_string)
            return str(value)

        @self.env.filter
        def truncate_words(value: str, length: int = 50) -> str:
            """Tronca testo per numero di parole"""
            words = str(value).split()
            if len(words) <= length:
                return value
            return ' '.join(words[:length]) + '...'

    def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
        """Renderizza template con contesto"""
        try:
            template = self.env.get_template(template_name)
            return template.render(**context)
        except TemplateNotFound:
            raise FileNotFoundError(f"Template non trovato: {template_name}")
        except TemplateError as e:
            raise ValueError(f"Errore nel template: {e}")

    def render_string(self, template_string: str, context: Dict[str, Any]) -> str:
        """Renderizza template da stringa"""
        template = self.env.from_string(template_string)
        return template.render(**context)

    def validate_template(self, template_name: str) -> bool:
        """Valida la sintassi di un template"""
        try:
            self.env.get_template(template_name)
            return True
        except Exception:
            return False

2. ReportLab - Generazione PDF Avanzata

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# generators/pdf_generator.py
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from io import BytesIO
import matplotlib.pyplot as plt
from core.base import DocumentGenerator, DocumentConfig, OutputFormat

class PDFGenerator(DocumentGenerator):
    """Generatore PDF avanzato con ReportLab"""

    def __init__(self, config: DocumentConfig):
        super().__init__(config)
        self.doc = None
        self.story = []
        self.styles = getSampleStyleSheet()
        self._create_custom_styles()

    def _create_custom_styles(self):
        """Crea stili personalizzati per il documento"""
        # Titolo principale
        self.styles.add(ParagraphStyle(
            name='CustomTitle',
            parent=self.styles['Heading1'],
            fontSize=24,
            spaceAfter=30,
            textColor=colors.HexColor('#2C3E50'),
            alignment=TA_CENTER
        ))

        # Sottotitolo
        self.styles.add(ParagraphStyle(
            name='CustomSubtitle',
            parent=self.styles['Heading2'],
            fontSize=16,
            spaceAfter=20,
            textColor=colors.HexColor('#34495E'),
            leftIndent=0.5*inch
        ))

        # Paragrafo con indentazione
        self.styles.add(ParagraphStyle(
            name='IndentedParagraph',
            parent=self.styles['Normal'],
            leftIndent=0.5*inch,
            spaceAfter=12
        ))

    def generate(self) -> Path:
        """Genera il documento PDF"""
        try:
            # Inizializza documento
            self.doc = SimpleDocTemplate(
                str(self.config.output_path),
                pagesize=A4,
                rightMargin=2*cm,
                leftMargin=2*cm,
                topMargin=2*cm,
                bottomMargin=2*cm
            )

            # Costruisci contenuto
            self._build_content()

            # Genera PDF
            self.doc.build(self.story)

            self.logger.info(f"PDF generato: {self.config.output_path}")
            return self.config.output_path

        except Exception as e:
            self.logger.error(f"Errore generazione PDF: {e}")
            raise

    def _build_content(self):
        """Costruisce il contenuto del documento"""
        data = self.config.data

        # Header con logo se presente
        if 'logo_path' in data:
            self._add_logo(data['logo_path'])

        # Titolo documento
        if 'title' in data:
            title = Paragraph(data['title'], self.styles['CustomTitle'])
            self.story.append(title)
            self.story.append(Spacer(1, 20))

        # Sottotitolo
        if 'subtitle' in data:
            subtitle = Paragraph(data['subtitle'], self.styles['CustomSubtitle'])
            self.story.append(subtitle)
            self.story.append(Spacer(1, 15))

        # Sezioni del documento
        if 'sections' in data:
            for section in data['sections']:
                self._add_section(section)

        # Tabelle se presenti
        if 'tables' in data:
            for table_data in data['tables']:
                self._add_table(table_data)

        # Grafici se presenti
        if 'charts' in data:
            for chart_data in data['charts']:
                self._add_chart(chart_data)

    def _add_logo(self, logo_path: str):
        """Aggiunge logo al documento"""
        try:
            logo = Image(logo_path, width=2*inch, height=1*inch)
            logo.hAlign = 'CENTER'
            self.story.append(logo)
            self.story.append(Spacer(1, 20))
        except Exception as e:
            self.logger.warning(f"Impossibile caricare logo: {e}")

    def _add_section(self, section: dict):
        """Aggiunge una sezione al documento"""
        # Titolo sezione
        if 'title' in section:
            section_title = Paragraph(section['title'], self.styles['Heading2'])
            self.story.append(section_title)
            self.story.append(Spacer(1, 10))

        # Contenuto sezione
        if 'content' in section:
            for paragraph in section['content']:
                p = Paragraph(paragraph, self.styles['IndentedParagraph'])
                self.story.append(p)

        self.story.append(Spacer(1, 15))

    def _add_table(self, table_data: dict):
        """Aggiunge tabella formattata"""
        data_table = table_data.get('data', [])
        if not data_table:
            return

        # Crea tabella
        table = Table(data_table)

        # Applica stile
        table_style = [
            ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498DB')),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('FONTSIZE', (0, 0), (-1, 0), 12),
            ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
            ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
            ('GRID', (0, 0), (-1, -1), 1, colors.black)
        ]

        table.setStyle(TableStyle(table_style))

        # Titolo tabella
        if 'title' in table_data:
            table_title = Paragraph(table_data['title'], self.styles['Heading3'])
            self.story.append(table_title)
            self.story.append(Spacer(1, 10))

        self.story.append(table)
        self.story.append(Spacer(1, 20))

    def _add_chart(self, chart_data: dict):
        """Aggiunge grafico generato con matplotlib"""
        try:
            # Genera grafico in memoria
            fig, ax = plt.subplots(figsize=(8, 6))

            chart_type = chart_data.get('type', 'bar')

            if chart_type == 'bar':
                ax.bar(chart_data['labels'], chart_data['values'])
            elif chart_type == 'line':
                ax.plot(chart_data['labels'], chart_data['values'])
            elif chart_type == 'pie':
                ax.pie(chart_data['values'], labels=chart_data['labels'], autopct='%1.1f%%')

            ax.set_title(chart_data.get('title', 'Grafico'))

            # Salva in BytesIO
            img_buffer = BytesIO()
            plt.savefig(img_buffer, format='png', dpi=150, bbox_inches='tight')
            img_buffer.seek(0)
            plt.close()

            # Aggiungi al documento
            chart_img = Image(img_buffer, width=6*inch, height=4*inch)
            chart_img.hAlign = 'CENTER'
            self.story.append(chart_img)
            self.story.append(Spacer(1, 20))

        except Exception as e:
            self.logger.error(f"Errore generazione grafico: {e}")

    def validate_template(self, template_path: Path) -> bool:
        """Valida template PDF (placeholder per future implementazioni)"""
        return template_path.exists() and template_path.suffix in ['.json', '.yaml']

# Registra il generatore
from core.base import DocumentFactory
DocumentFactory.register_generator(OutputFormat.PDF, PDFGenerator)

3. python-docx - Documenti Word Professionali

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# generators/docx_generator.py
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
from core.base import DocumentGenerator, DocumentConfig, OutputFormat

class DOCXGenerator(DocumentGenerator):
    """Generatore DOCX con formatting avanzato"""

    def __init__(self, config: DocumentConfig):
        super().__init__(config)
        self.doc = Document()
        self._setup_styles()

    def _setup_styles(self):
        """Configura stili personalizzati per il documento"""
        styles = self.doc.styles

        # Stile titolo personalizzato
        try:
            title_style = styles.add_style('CustomTitle', WD_STYLE_TYPE.PARAGRAPH)
            title_font = title_style.font
            title_font.name = 'Arial'
            title_font.size = Pt(24)
            title_font.bold = True
            title_font.color.rgb = RGBColor(44, 62, 80)
            title_style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
            title_style.paragraph_format.space_after = Pt(20)
        except ValueError:
            pass

    def generate(self) -> Path:
        """Genera il documento DOCX"""
        try:
            data = self.config.data

            # Titolo principale
            if 'title' in data:
                title_para = self.doc.add_paragraph(data['title'], style='CustomTitle')

            # Contenuto principale
            if 'sections' in data:
                for section in data['sections']:
                    self._add_section(section)

            # Salva documento
            self.doc.save(str(self.config.output_path))

            self.logger.info(f"DOCX generato: {self.config.output_path}")
            return self.config.output_path

        except Exception as e:
            self.logger.error(f"Errore generazione DOCX: {e}")
            raise

    def _add_section(self, section: dict):
        """Aggiunge sezione con formattazione"""
        if 'title' in section:
            self.doc.add_heading(section['title'], level=2)

        if 'content' in section:
            if isinstance(section['content'], list):
                for paragraph in section['content']:
                    self.doc.add_paragraph(paragraph)
            else:
                self.doc.add_paragraph(section['content'])

    def validate_template(self, template_path: Path) -> bool:
        """Valida template DOCX"""
        return template_path.exists() and template_path.suffix in ['.docx', '.dotx']

# Registra il generatore
DocumentFactory.register_generator(OutputFormat.DOCX, DOCXGenerator)

Motore template con Jinja2

Sistema di Template Avanzato

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
# utils/advanced_templating.py
from jinja2 import Environment, BaseLoader, ChoiceLoader, FileSystemLoader
from jinja2.exceptions import TemplateError
from typing import Dict, Any, List, Optional, Union
from pathlib import Path
import yaml
import json
from dataclasses import dataclass

@dataclass
class TemplateContext:
    """Contesto per il rendering del template"""
    data: Dict[str, Any]
    globals: Dict[str, Any] = None
    filters: Dict[str, callable] = None

    def __post_init__(self):
        if self.globals is None:
            self.globals = {}
        if self.filters is None:
            self.filters = {}

class AdvancedTemplateEngine:
    """Sistema di template avanzato con supporto multi-source"""

    def __init__(self, template_paths: List[Union[str, Path]], db_connection=None):
        self.template_paths = [Path(p) for p in template_paths]

        # Configurazione loaders multipli
        loaders = [FileSystemLoader(str(path)) for path in self.template_paths]

        # Ambiente Jinja2 con loader multipli
        self.env = Environment(
            loader=ChoiceLoader(loaders),
            autoescape=True,
            trim_blocks=True,
            lstrip_blocks=True,
            keep_trailing_newline=True
        )

        # Registra filtri e funzioni personalizzate
        self._register_custom_filters()
        self._register_global_functions()

    def _register_custom_filters(self):
        """Registra filtri personalizzati per business logic"""

        @self.env.filter
        def format_currency(value: Union[int, float], currency: str = "EUR") -> str:
            """Formatta valori monetari con separatori migliaia"""
            if currency == "EUR":
                return f"€ {value:,.2f}".replace(',', ' ')
            elif currency == "USD":
                return f"$ {value:,.2f}"
            return f"{value:,.2f} {currency}"

        @self.env.filter
        def format_percentage(value: Union[int, float], decimals: int = 2) -> str:
            """Formatta percentuali"""
            return f"{value:.{decimals}f}%"

        @self.env.filter
        def safe_divide(numerator: Union[int, float], denominator: Union[int, float], default: Union[int, float] = 0) -> Union[int, float]:
            """Divisione sicura che evita divisione per zero"""
            try:
                return numerator / denominator if denominator != 0 else default
            except (TypeError, ZeroDivisionError):
                return default

    def _register_global_functions(self):
        """Registra funzioni globali disponibili nei template"""

        def calculate_total(items: List[Dict], field: str) -> float:
            """Calcola totale di un campo specifico"""
            try:
                return sum(float(item.get(field, 0)) for item in items)
            except (ValueError, TypeError):
                return 0.0

        def group_by(items: List[Dict], field: str) -> Dict:
            """Raggruppa items per campo specifico"""
            groups = {}
            for item in items:
                key = item.get(field, 'Unknown')
                if key not in groups:
                    groups[key] = []
                groups[key].append(item)
            return groups

        # Registra nel namespace globale
        self.env.globals.update({
            'calculate_total': calculate_total,
            'group_by': group_by,
        })

    def render(self, template_name: str, context: TemplateContext) -> str:
        """Renderizza template con contesto"""
        try:
            template = self.env.get_template(template_name)

            # Merge contesto con globals e filtri personalizzati
            render_context = {**context.data}
            if context.globals:
                render_context.update(context.globals)

            return template.render(**render_context)

        except TemplateError as e:
            raise ValueError(f"Errore nel template '{template_name}': {e}")

Integrazione database

Sistema di Persistenza Avanzato

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# utils/database_integration.py
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Float, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.dialects.postgresql import JSON
from datetime import datetime
from typing import Dict, Any, List, Optional
import pandas as pd

Base = declarative_base()

class Template(Base):
    """Modello per template salvati in database"""
    __tablename__ = 'templates'

    id = Column(Integer, primary_key=True)
    name = Column(String(255), unique=True, nullable=False)
    format_type = Column(String(50), nullable=False)
    content = Column(Text, nullable=False)
    metadata = Column(JSON)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # Relazione con documenti generati
    generated_documents = relationship("GeneratedDocument", back_populates="template")

class GeneratedDocument(Base):
    """Modello per tracciare documenti generati"""
    __tablename__ = 'generated_documents'

    id = Column(Integer, primary_key=True)
    template_id = Column(Integer, ForeignKey('templates.id'), nullable=False)
    output_path = Column(String(500), nullable=False)
    format_type = Column(String(50), nullable=False)
    generation_time = Column(Float)  # Tempo in secondi
    file_size = Column(Integer)  # Dimensione in bytes
    metadata = Column(JSON)
    created_at = Column(DateTime, default=datetime.utcnow)

    # Relazione con template
    template = relationship("Template", back_populates="generated_documents")

class DatabaseManager:
    """Gestione avanzata database per il document generator"""

    def __init__(self, database_url: str):
        self.engine = create_engine(database_url)
        self.SessionLocal = sessionmaker(bind=self.engine)

        # Crea tabelle se non esistono
        Base.metadata.create_all(bind=self.engine)

    def get_session(self):
        """Ottiene sessione database"""
        return self.SessionLocal()

    def save_template(self, name: str, format_type: str, content: str, metadata: Dict = None) -> Template:
        """Salva template in database"""
        with self.get_session() as session:
            template = Template(
                name=name,
                format_type=format_type,
                content=content,
                metadata=metadata or {}
            )
            session.add(template)
            session.commit()
            session.refresh(template)
            return template

    def get_template(self, name: str) -> Optional[Template]:
        """Recupera template per nome"""
        with self.get_session() as session:
            return session.query(Template).filter(Template.name == name).first()

API REST per integrazione

FastAPI Service Completo

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# api/main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Dict, Any, List, Optional
import asyncio
import uuid
from pathlib import Path
from datetime import datetime

from core.base import DocumentConfig, OutputFormat, DocumentFactory
from utils.database_integration import DatabaseManager
from config.settings import config

app = FastAPI(
    title="API generatore documenti",
    description="API per generazione automatica documenti multi-formato",
    version="1.0.0"
)

# CORS middleware per frontend integration
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Inizializza componenti
db_manager = DatabaseManager(config.DATABASE_URL)

# Modelli Pydantic per API
class DocumentGenerationRequest(BaseModel):
    """Request per generazione documento"""
    template_name: str = Field(..., description="Nome del template da utilizzare")
    output_format: OutputFormat = Field(..., description="Formato di output")
    data: Dict[str, Any] = Field(default_factory=dict, description="Dati per popolamento template")
    options: Dict[str, Any] = Field(default_factory=dict, description="Opzioni aggiuntive")
    filename: Optional[str] = Field(None, description="Nome file personalizzato")

class TemplateUploadRequest(BaseModel):
    """Request per upload template"""
    name: str
    format_type: str
    content: str
    metadata: Dict[str, Any] = Field(default_factory=dict)

@app.get("/")
async def root():
    """Health check endpoint"""
    return {"message": "API generatore documenti attiva", "version": "1.0.0"}

@app.post("/generate", response_model=Dict[str, str])
async def generate_document(request: DocumentGenerationRequest):
    """Genera documento sincronamente"""
    try:
        # Genera nome file se non specificato
        if not request.filename:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            request.filename = f"document_{timestamp}.{request.output_format.value}"

        # Configura path output
        output_path = config.OUTPUT_DIR / request.filename

        # Crea configurazione documento
        doc_config = DocumentConfig(
            template_name=request.template_name,
            output_format=request.output_format,
            output_path=output_path,
            data=request.data,
            options=request.options
        )

        # Genera documento
        generator = DocumentFactory.create_generator(doc_config)
        result_path = generator.generate()

        # Log nel database
        template = db_manager.get_template(request.template_name)
        if template:
            file_size = result_path.stat().st_size if result_path.exists() else 0
            db_manager.log_document_generation(
                template_id=template.id,
                output_path=str(result_path),
                format_type=request.output_format.value,
                generation_time=0,  # Da implementare timing
                file_size=file_size
            )

        return {
            "status": "success",
            "file_path": str(result_path),
            "download_url": f"/download/{result_path.name}"
        }

    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.post("/templates/upload")
async def upload_template(request: TemplateUploadRequest):
    """Carica nuovo template"""
    try:
        # Salva template nel database
        template = db_manager.save_template(
            name=request.name,
            format_type=request.format_type,
            content=request.content,
            metadata=request.metadata
        )

        return {
            "status": "success",
            "template_id": template.id,
            "message": f"Template '{request.name}' caricato con successo"
        }

    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/download/{filename}")
async def download_file(filename: str):
    """Download file generato"""
    file_path = config.OUTPUT_DIR / filename

    if not file_path.exists():
        raise HTTPException(status_code=404, detail="File non trovato")

    return FileResponse(
        path=str(file_path),
        filename=filename,
        media_type='application/octet-stream'
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Esempio di Utilizzo Pratico

Script di Esempio Completo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# example_usage.py
from pathlib import Path
from core.base import DocumentConfig, OutputFormat, DocumentFactory

def main():
    """Esempio di utilizzo del document generator"""

    # Dati per il documento
    document_data = {
        "title": "Report Vendite Q1 2024",
        "subtitle": "Analisi prestazioni e trend di mercato",
        "sections": [
            {
                "title": "Executive Summary",
                "content": [
                    "Le vendite del primo trimestre 2024 hanno registrato un incremento del 15% rispetto al periodo precedente.",
                    "I settori di maggiore crescita sono stati tecnologia ed e-commerce."
                ]
            },
            {
                "title": "Analisi Dettagliata",
                "content": [
                    "Il mercato ha mostrato una forte ripresa dopo il rallentamento del 2023.",
                    "Le strategie digitali hanno contribuito significativamente ai risultati positivi."
                ]
            }
        ],
        "tables": [
            {
                "title": "Vendite per Categoria",
                "data": [
                    ["Categoria", "Q1 2023", "Q1 2024", "Crescita %"],
                    ["Tecnologia", "150.000€", "172.500€", "+15%"],
                    ["E-commerce", "200.000€", "240.000€", "+20%"],
                    ["Retail", "100.000€", "105.000€", "+5%"]
                ]
            }
        ],
        "charts": [
            {
                "title": "Crescita Vendite Trimestrale",
                "type": "bar",
                "labels": ["Q1", "Q2", "Q3", "Q4"],
                "values": [450000, 480000, 520000, 580000]
            }
        ]
    }

    # Genera documenti in tutti i formati
    formats = [OutputFormat.PDF, OutputFormat.DOCX, OutputFormat.HTML]

    for format_type in formats:
        # Configura documento
        config = DocumentConfig(
            template_name=f"report_template_{format_type.value}",
            output_format=format_type,
            output_path=Path(f"output/report_q1_2024.{format_type.value}"),
            data=document_data,
            options={"page_size": "A4", "margins": "normal"}
        )

        # Genera documento
        try:
            generator = DocumentFactory.create_generator(config)
            result_path = generator.generate()
            print(f"✅ Documento {format_type.value.upper()} generato: {result_path}")

        except Exception as e:
            print(f"❌ Errore generazione {format_type.value.upper()}: {e}")

if __name__ == "__main__":
    main()

Distribuzione e containerizzazione

Docker Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Dockerfile
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    libpq-dev \
    libxml2-dev \
    libxslt1-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create necessary directories
RUN mkdir -p /app/output /app/logs /app/templates

# Set environment variables
ENV PYTHONPATH=/app
ENV LOG_LEVEL=INFO
ENV MAX_WORKERS=4

# Expose port
EXPOSE 8000

# Start application
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# docker-compose.yml
version: '3.8'

services:
  document-generator:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/docgen
      - SECRET_KEY=your-secret-key-change-in-production
      - LOG_LEVEL=INFO
      - MAX_WORKERS=4
    volumes:
      - ./templates:/app/templates
      - ./output:/app/output
      - ./logs:/app/logs
    depends_on:
      - postgres
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=docgen
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Conclusioni e buone pratiche

Buone pratiche implementate

Sicurezza

  • Validazione input rigorosa
  • Sanitizzazione template
  • Gestione sicura file upload
  • Logging strutturato per audit

Prestazioni

  • Caching intelligente template
  • Pool connessioni database
  • Generazione asincrona per documenti complessi
  • Monitoraggio in tempo reale

Manutenibilità

  • Codice modulare e testabile
  • Documentazione API automatica
  • Logging strutturato
  • Configurazione basata su ambiente

Scalabilita

  • Architettura stateless
  • Scalabilita orizzontale con container
  • Bilanciamento del carico
  • Storage distribuito

Estensioni Future

Il sistema è progettato per essere facilmente estendibile con nuove funzionalità come integrazione AI/ML per content generation, workflow engine per approval processes, e connettori per sistemi enterprise.

Questo generatore di documenti Python rappresenta una soluzione enterprise-grade completa, progettata per crescere con le esigenze della tua organizzazione e integrarsi nei workflow esistenti.