Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ jobs:
run: |
set -euo pipefail
if [ "${USE_NOTES_FILE}" = "1" ]; then
gh release create "$TAG_NAME" dist/* \
gh release create "$TAG_NAME" dist/* book/dist/*.epub book/dist/*.pdf \
--repo "$REPO" \
--title "$TAG_NAME" \
--notes-file RELEASE_NOTES.md
else
gh release create "$TAG_NAME" dist/* \
gh release create "$TAG_NAME" dist/* book/dist/*.epub book/dist/*.pdf \
--repo "$REPO" \
--title "$TAG_NAME" \
--generate-notes
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ docs/superpowers/
book/dist/**
!book/dist/pyfly-by-example.pdf
!book/dist/pyfly-by-example.epub
!book/dist/pyfly-by-example-es.pdf
!book/dist/pyfly-by-example-es.epub
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## v26.06.111 (2026-06-16)

### Added

- **"PyFly by Example" — Spanish edition.** The book is now published in Spanish
(`book/dist/pyfly-by-example-es.{epub,pdf}`) alongside the English edition,
built from a parallel `book/manuscript-es/` manuscript via `book.es.yaml`. The
book build (`book/build/build.py`) is now language-parameterized (`--config`,
per-manifest `manuscript_dir` / `output_basename` / localized labels). Both
editions are attached to the GitHub release.
- **Quick Start tutorial + step-by-step depth.** A new "Build Lumen Step by Step"
walkthrough takes the reader from an empty folder to a running, tested wallet
feature; every chapter was deepened into a more granular, beginner-friendly
tutorial. A dedication was added (EN + ES).

### Fixed

- **`pyfly new` no longer scaffolds the removed `pyfly.web.port` key.** The project
template (`pyfly.yaml.j2`) now emits the port under `server:` as
`pyfly.server.port` (Spring `server.port` parity); the legacy `pyfly.web.port`
was removed in v26.06.102 and had been left as a dead key in freshly scaffolded
applications.

---

## v26.06.110 (2026-06-16)

### Fixed
Expand Down
61 changes: 61 additions & 0 deletions book/book.es.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
title: "PyFly by Example"
subtitle: "Microservicios Python Orientados a Eventos con el Firefly Framework"
author: "Firefly Software Foundation"
publisher: "Firefly Software Foundation"
language: "es"
identifier: "urn:uuid:5e2d9b41-7a3c-4f8e-b1d6-c0ffeeab1e5e"
rights: "Copyright (c) 2026 Firefly Software Foundation. Bajo licencia Apache-2.0."
cover_svg: "art/cover.svg"
cover_png: "art/cover.png"
trim_width: "7.5in"
trim_height: "9.25in"
manuscript_dir: "manuscript-es"
output_basename: "pyfly-by-example-es"
labels:
contents: "Contenido"
front:
- {id: title, file: 00-front/00-title.md, nav: false}
- {id: copyright, file: 00-front/00-copyright.md, nav: false}
- {id: dedication, file: 00-front/00-dedication.md, nav: false}
- {id: preface, file: 00-front/00-preface.md, title: "Prefacio"}
- {id: conventions, file: 00-front/00-conventions.md, title: "Convenciones"}
parts:
- title: "Inicio Rápido"
chapters:
- {id: quickstart, file: 00-quickstart.md, num: "", title: "Construye Lumen Paso a Paso", opener: art/openers/ch01.svg}
- title: "Parte I — Fundamentos"
chapters:
- {id: ch01, file: 01-why-pyfly.md, num: 1, title: "¿Por qué PyFly?", opener: art/openers/ch01.svg}
- {id: ch02, file: 02-dependency-injection.md, num: 2, title: "Inyección de Dependencias y el Contexto de Aplicación", opener: art/openers/ch02.svg}
- {id: ch03, file: 03-configuration.md, num: 3, title: "Configuración, Perfiles y Secretos", opener: art/openers/ch03.svg}
- {id: ch04, file: 04-first-http-api.md, num: 4, title: "Tu Primera API HTTP", opener: art/openers/ch04.svg}
- title: "Parte II — Modelar y Persistir el Dominio"
chapters:
- {id: ch05, file: 05-persistence.md, num: 5, title: "Persistencia y el Patrón Repositorio", opener: art/openers/ch05.svg}
- {id: ch06, file: 06-domain-driven-design.md, num: 6, title: "Diseño Guiado por el Dominio", opener: art/openers/ch06.svg}
- {id: ch07, file: 07-cqrs.md, num: 7, title: "CQRS: Comandos y Consultas", opener: art/openers/ch07.svg}
- title: "Parte III — Arquitectura Orientada a Eventos"
chapters:
- {id: ch08, file: 08-eda.md, num: 8, title: "Eventos de Dominio y Arquitectura Orientada a Eventos", opener: art/openers/ch08.svg}
- {id: ch09, file: 09-event-sourcing.md, num: 9, title: "Event Sourcing del Libro Mayor", opener: art/openers/ch09.svg}
- {id: ch10, file: 10-messaging.md, num: 10, title: "Mensajería con Kafka y RabbitMQ", opener: art/openers/ch10.svg}
- title: "Parte IV — Hacia los Microservicios"
chapters:
- {id: ch11, file: 11-http-clients.md, num: 11, title: "Dividir el Monolito: Clientes HTTP y el BFF", opener: art/openers/ch11.svg}
- {id: ch12, file: 12-sagas.md, num: 12, title: "Transacciones Distribuidas: Sagas, Workflows y TCC", opener: art/openers/ch12.svg}
- {id: ch13, file: 13-caching-resilience.md, num: 13, title: "Caché y Resiliencia", opener: art/openers/ch13.svg}
- title: "Parte V — Asegurar, Observar y Desplegar"
chapters:
- {id: ch14, file: 14-security.md, num: 14, title: "Seguridad, Sesiones e Identidad", opener: art/openers/ch14.svg}
- {id: ch15, file: 15-observability.md, num: 15, title: "Observabilidad y el Panel de Administración", opener: art/openers/ch15.svg}
- {id: ch16, file: 16-testing.md, num: 16, title: "Pruebas de Aplicaciones PyFly", opener: art/openers/ch16.svg}
- {id: ch17, file: 17-scheduling-notifications.md, num: 17, title: "Programación, Notificaciones, Webhooks y Callbacks", opener: art/openers/ch17.svg}
- {id: ch18, file: 18-production.md, num: 18, title: "Extender PyFly y Llevarlo a Producción", opener: art/openers/ch18.svg}
- title: "Apéndices"
chapters:
- {id: appa, file: 90-appendix-a-spring.md, num: "A", title: "Chuleta de Spring Boot → PyFly"}
- {id: appb, file: 91-appendix-b-mongodb.md, num: "B", title: "MongoDB y Datos Documentales"}
- {id: appc, file: 92-appendix-c-ecm.md, num: "C", title: "ECM: Contenido y Firma Electrónica"}
- {id: appd, file: 93-appendix-d-cli.md, num: "D", title: "CLI y Resolución de Problemas"}
- {id: glossary, file: 94-glossary.md, num: "", title: "Glosario"}
back: []
8 changes: 8 additions & 0 deletions book/book.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ cover_svg: "art/cover.svg"
cover_png: "art/cover.png"
trim_width: "7.5in"
trim_height: "9.25in"
manuscript_dir: "manuscript"
output_basename: "pyfly-by-example"
labels:
contents: "Contents"
front:
- {id: title, file: 00-front/00-title.md, nav: false}
- {id: copyright, file: 00-front/00-copyright.md, nav: false}
- {id: dedication, file: 00-front/00-dedication.md, nav: false}
- {id: preface, file: 00-front/00-preface.md, title: "Preface"}
- {id: conventions, file: 00-front/00-conventions.md, title: "Conventions"}
parts:
- title: "Quick Start"
chapters:
- {id: quickstart, file: 00-quickstart.md, num: "", title: "Build Lumen Step by Step", opener: art/openers/ch01.svg}
- title: "Part I — Foundations"
chapters:
- {id: ch01, file: 01-why-pyfly.md, num: 1, title: "Why PyFly?", opener: art/openers/ch01.svg}
Expand Down
56 changes: 38 additions & 18 deletions book/build/build.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
"""Build *PyFly by Example* into EPUB + PDF from book.yaml."""
"""Build *PyFly by Example* into EPUB + PDF from a book manifest.

Defaults to ``book.yaml`` (English). Pass ``--config book.es.yaml`` to build the
Spanish edition; each manifest names its own ``manuscript_dir``, ``language``,
localized ``labels`` (e.g. the Contents heading) and ``output_basename``.

book/build/run.sh # English -> pyfly-by-example.{epub,pdf}
book/build/run.sh --config book.es.yaml # Spanish -> pyfly-by-example-es.{epub,pdf}
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
Expand All @@ -12,7 +21,6 @@
from pdf import render_pdf # noqa: E402

BOOK = Path(__file__).resolve().parents[1]
MAN = BOOK / "manuscript"
THEME = BOOK / "theme"
DIST = BOOK / "dist"

Expand All @@ -29,7 +37,7 @@ def _split_part(part_title: str) -> tuple[str, str]:
return "", part_title.strip()


def _items_from_manifest(cfg: dict) -> list[dict]:
def _items_from_manifest(cfg: dict, man: Path, *, contents_label: str) -> list[dict]:
"""Ordered build items, each tagged ``kind`` and carrying the metadata that
kind needs. Consumed by BOTH the EPUB and PDF assemblers.

Expand All @@ -42,7 +50,7 @@ def _items_from_manifest(cfg: dict) -> list[dict]:
items: list[dict] = []
# 1) front matter
for fm in cfg.get("front", []):
p = MAN / fm["file"]
p = man / fm["file"]
if not p.exists():
continue
items.append({
Expand All @@ -53,12 +61,12 @@ def _items_from_manifest(cfg: dict) -> list[dict]:
"in_nav": bool(fm.get("nav", True)) and "title" in fm,
})
# 2) Contents page — after front matter, before Part I
items.append({"kind": "toc", "id": "toc", "title": "Contents"})
items.append({"kind": "toc", "id": "toc", "title": contents_label})
# 3) parts: a divider then each chapter
for part in cfg.get("parts", []):
ptitle_full = part["title"]
eyebrow, ptitle = _split_part(ptitle_full)
chapters = [ch for ch in part["chapters"] if (MAN / ch["file"]).exists()]
chapters = [ch for ch in part["chapters"] if (man / ch["file"]).exists()]
if not chapters:
continue
# stable divider id from the eyebrow, e.g. "Part I" -> "part-i"
Expand All @@ -77,13 +85,13 @@ def _items_from_manifest(cfg: dict) -> list[dict]:
"id": ch["id"],
"title": (f'{ch["num"]}. {ch["title"]}' if ch.get("num") not in (None, "") else ch["title"]),
"num": ch["num"],
"path": str(MAN / ch["file"]),
"path": str(man / ch["file"]),
"part": ptitle_full,
})
return items


def _toc_html(items: list[dict], *, href_fmt: str) -> str:
def _toc_html(items: list[dict], *, href_fmt: str, label: str) -> str:
"""Generate the Contents body. ``href_fmt`` formats a chapter id into a link
target: '#{cid}' for the single-document PDF, '{cid}.xhtml' for the EPUB."""
parts: list[str] = []
Expand All @@ -107,7 +115,7 @@ def _toc_html(items: list[dict], *, href_fmt: str) -> str:
if open_group:
parts.append("</ol></div>")
body = "".join(parts)
return f'<h1 class="chtitle">Contents</h1>{body}'
return f'<h1 class="chtitle">{escape(label)}</h1>{body}'


def _divider_html(eyebrow: str, ptitle: str) -> str:
Expand All @@ -116,11 +124,22 @@ def _divider_html(eyebrow: str, ptitle: str) -> str:
f'<h1 class="part-title">{escape(ptitle)}</h1></div>')


def main() -> int:
cfg = yaml.safe_load((BOOK / "book.yaml").read_text())
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="Build PyFly by Example (EPUB + PDF).")
ap.add_argument("--config", default="book.yaml",
help="Manifest file under book/ (default: book.yaml).")
ap.add_argument("--out", default=None,
help="Output basename (default: manifest 'output_basename' or 'pyfly-by-example').")
args = ap.parse_args(argv)

cfg = yaml.safe_load((BOOK / args.config).read_text())
man = BOOK / cfg.get("manuscript_dir", "manuscript")
contents_label = cfg.get("labels", {}).get("contents", "Contents")
out_base = args.out or cfg.get("output_basename") or "pyfly-by-example"

css_text = [(THEME / "book.css").read_text(), (THEME / "tokens.css").read_text(),
(THEME / "pygments.css").read_text()]
items = _items_from_manifest(cfg)
items = _items_from_manifest(cfg, man, contents_label=contents_label)

# ---- EPUB ----
epub = EpubBuilder(title=cfg["title"], author=cfg["author"], language=cfg["language"],
Expand All @@ -130,7 +149,7 @@ def main() -> int:
epub.add_file(cover_png, "art/cover.png", "cover-img", properties="cover-image")
for it in items:
if it["kind"] == "toc":
body = _toc_html(items, href_fmt="{cid}.xhtml")
body = _toc_html(items, href_fmt="{cid}.xhtml", label=contents_label)
epub.add_doc(Doc(id=it["id"], title=it["title"], xhtml_body=body,
in_nav=True, kind="toc"))
elif it["kind"] == "divider":
Expand All @@ -143,15 +162,15 @@ def main() -> int:
in_nav=it.get("in_nav", True), kind=it["kind"],
part=it.get("part"), num=it.get("num")))
DIST.mkdir(exist_ok=True)
epub.build(DIST / "pyfly-by-example.epub")
epub.build(DIST / f"{out_base}.epub")

# ---- PDF (single concatenated document) ----
parts_html: list[str] = []
if cover_png.exists():
parts_html.append(f'<div class="cover-page"><img src="{cfg["cover_png"]}"/></div>')
for it in items:
if it["kind"] == "toc":
body = _toc_html(items, href_fmt="#{cid}")
body = _toc_html(items, href_fmt="#{cid}", label=contents_label)
parts_html.append(f'<section class="toc" id="{it["id"]}">{body}</section>')
elif it["kind"] == "divider":
body = _divider_html(it["eyebrow"], it["ptitle"])
Expand All @@ -164,10 +183,11 @@ def main() -> int:
render_pdf(full, base_url=BOOK,
css_paths=[THEME / "tokens.css", THEME / "pygments.css",
THEME / "book.css", THEME / "print.css"],
out=DIST / "pyfly-by-example.pdf")
out=DIST / f"{out_base}.pdf")
n = sum(1 for it in items if it["kind"] in ("front", "chapter"))
print(f"Built {n} document(s) + TOC + {sum(1 for it in items if it['kind']=='divider')} "
f"part divider(s) -> EPUB + PDF in {DIST}")
print(f"[{cfg['language']}] Built {n} document(s) + TOC + "
f"{sum(1 for it in items if it['kind']=='divider')} part divider(s) "
f"-> {out_base}.epub + {out_base}.pdf in {DIST}")
return 0


Expand Down
Binary file added book/dist/pyfly-by-example-es.epub
Binary file not shown.
Binary file added book/dist/pyfly-by-example-es.pdf
Binary file not shown.
Binary file modified book/dist/pyfly-by-example.epub
Binary file not shown.
Binary file modified book/dist/pyfly-by-example.pdf
Binary file not shown.
46 changes: 46 additions & 0 deletions book/manuscript-es/00-front/00-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## Convenciones

Esta página explica las convenciones tipográficas y estructurales que se usan a lo largo del libro.

### Listados de código

Cada ejemplo de código de varias líneas tiene una **pestaña con el nombre del archivo** en la esquina superior izquierda que muestra el archivo al que pertenece, y un pie **"Listado N.N"** debajo del bloque que lo identifica por capítulo y número de secuencia. Por ejemplo:

::: listing wallet/domain/wallet.py | Listado 5.1 — Raíz del agregado Wallet
from pyfly.core import component
from dataclasses import dataclass, field
from decimal import Decimal

@component
@dataclass
class Wallet:
id: str
balance: Decimal = field(default=Decimal("0.00"))

def deposit(self, amount: Decimal) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
:::

Las referencias de código en línea dentro de la prosa usan fuente `monoespaciada`, como en "el decorador `@component` registra la clase en el contenedor de PyFly".

### Notas al margen

En los márgenes y en el cuerpo aparecen cuatro estilos de notas al margen:

!!! note "Nota"
Las notas aportan contexto complementario o aclaran una sutileza del texto principal: merece la pena leerlas, pero no son bloqueantes.

!!! tip "Consejo"
Los consejos comparten un atajo, un idiom o una buena práctica que te ahorrará tiempo en proyectos reales.

!!! warning "Advertencia"
Las advertencias señalan un error habitual o un punto delicado que puede provocar problemas difíciles de depurar si se ignora.

!!! spring "Equivalencia con Spring"
Las notas de equivalencia con Spring relacionan un concepto de PyFly directamente con su equivalente en Spring Boot: ideales para quienes migran desde el ecosistema de la JVM.

### Figuras

Los diagramas se numeran como **Figura N.N** y llevan un pie debajo de la imagen. Se incrustan como SVG en línea, de modo que se renderizan con nitidez a cualquier nivel de zoom tanto en la edición en pantalla como en la impresa. Te encontrarás con la primera en la página inicial del Capítulo 1.
13 changes: 13 additions & 0 deletions book/manuscript-es/00-front/00-copyright.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright &copy; 2026 Firefly Software Foundation.

Publicado bajo la Licencia Apache, Versión 2.0 (la "Licencia"); no puedes utilizar este material salvo en cumplimiento de la Licencia. Puedes obtener una copia de la Licencia en <https://www.apache.org/licenses/LICENSE-2.0>.

Salvo que lo exija la legislación aplicable o se acuerde por escrito, el software y la documentación distribuidos bajo la Licencia se distribuyen "TAL CUAL", SIN GARANTÍAS NI CONDICIONES DE NINGÚN TIPO, ya sean expresas o implícitas. Consulta la Licencia para conocer el régimen específico de permisos y limitaciones que la rigen.

---

**Primera edición, 2026.**

Todos los listados de código de este libro se escribieron y verificaron con la versión 26.6.x del framework PyFly.

Publicado por la Firefly Software Foundation.
7 changes: 7 additions & 0 deletions book/manuscript-es/00-front/00-dedication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*Para **Jacinto Arias** y el equipo de **Taidy** —*

*por empujarnos, una y otra vez, a construir un framework de verdad para Python.*

*Gracias a vosotros, ahora por fin sí se harán las cosas de una única forma.*

*Una. Sola. Forma. (Le damos una semana.)*
Loading
Loading