~/asthma-pdf/generate_asthma_pdf.py
#!/usr/bin/env python3
"""
Asthma Management Reference PDF
Comprehensive clinical reference for respiratory medicine PGT
Covers: GINA 2026 stepwise management, biologic selection, acute severe asthma, severity assessment
"""
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.platypus.flowables import BalancedColumns
from reportlab.graphics.shapes import Drawing, Rect, String, Line, Arrow, Polygon
from reportlab.graphics import renderPDF
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.pdfgen import canvas
import datetime
# ─────────────────────────── COLOUR PALETTE ───────────────────────────
DARK_BLUE = colors.HexColor('#1A3A5C')
MED_BLUE = colors.HexColor('#2E6DA4')
LIGHT_BLUE = colors.HexColor('#D6E8F7')
TEAL = colors.HexColor('#007B8A')
LIGHT_TEAL = colors.HexColor('#D0F0F3')
GREEN = colors.HexColor('#1E7A45')
LIGHT_GREEN = colors.HexColor('#D4EDDA')
ORANGE = colors.HexColor('#D4600A')
LIGHT_ORANGE = colors.HexColor('#FDEBD0')
RED = colors.HexColor('#B22222')
LIGHT_RED = colors.HexColor('#FADBD8')
YELLOW = colors.HexColor('#856404')
LIGHT_YELLOW = colors.HexColor('#FFF3CD')
PURPLE = colors.HexColor('#5B2C8D')
LIGHT_PURPLE = colors.HexColor('#E8D5F5')
GREY_LIGHT = colors.HexColor('#F5F7FA')
GREY_MED = colors.HexColor('#DEE2E6')
GREY_DARK = colors.HexColor('#6C757D')
WHITE = colors.white
BLACK = colors.black
# ─────────────────────────── PAGE SETUP ───────────────────────────────
OUTPUT = '/home/daytona/workspace/asthma-pdf/Asthma_Management_Reference.pdf'
PAGE_W, PAGE_H = A4
MARGIN = 1.6 * cm
def make_doc():
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=2.2*cm, bottomMargin=2.0*cm,
title='Asthma Management Reference – GINA 2026',
author='Orris Medical AI',
subject='Respiratory Medicine – PGT Reference',
)
return doc
# ─────────────────────────── STYLES ───────────────────────────────────
ss = getSampleStyleSheet()
def S(name, parent='Normal', **kw):
return ParagraphStyle(name, parent=ss[parent], **kw)
COVER_TITLE = S('CoverTitle', 'Normal',
fontSize=28, leading=34, textColor=WHITE,
fontName='Helvetica-Bold', alignment=TA_CENTER)
COVER_SUB = S('CoverSub', 'Normal',
fontSize=14, leading=18, textColor=LIGHT_BLUE,
fontName='Helvetica', alignment=TA_CENTER)
COVER_NOTE = S('CoverNote', 'Normal',
fontSize=9, leading=12, textColor=GREY_MED,
fontName='Helvetica-Oblique', alignment=TA_CENTER)
H1 = S('H1', 'Normal',
fontSize=13, leading=16, textColor=WHITE,
fontName='Helvetica-Bold', alignment=TA_LEFT,
spaceBefore=4, spaceAfter=2,
backColor=DARK_BLUE, borderPadding=(5,8,5,8))
H2 = S('H2', 'Normal',
fontSize=11, leading=14, textColor=DARK_BLUE,
fontName='Helvetica-Bold',
spaceBefore=8, spaceAfter=3,
borderPadding=(0,0,2,0))
H3 = S('H3', 'Normal',
fontSize=9.5, leading=12, textColor=MED_BLUE,
fontName='Helvetica-Bold',
spaceBefore=6, spaceAfter=2)
BODY = S('Body', 'Normal',
fontSize=8.5, leading=12, textColor=BLACK,
fontName='Helvetica',
spaceAfter=3, alignment=TA_JUSTIFY)
BODY_BOLD = S('BodyBold', 'Normal',
fontSize=8.5, leading=12, textColor=DARK_BLUE,
fontName='Helvetica-Bold', spaceAfter=2)
BULLET = S('Bullet', 'Normal',
fontSize=8.5, leading=12, textColor=BLACK,
fontName='Helvetica',
leftIndent=14, firstLineIndent=-10,
spaceAfter=2)
NOTE = S('Note', 'Normal',
fontSize=7.5, leading=10.5, textColor=GREY_DARK,
fontName='Helvetica-Oblique',
leftIndent=4, spaceAfter=2)
WARN = S('Warn', 'Normal',
fontSize=8, leading=11, textColor=RED,
fontName='Helvetica-Bold',
leftIndent=8, spaceAfter=3)
TH = S('TH', 'Normal',
fontSize=8, leading=10, textColor=WHITE,
fontName='Helvetica-Bold', alignment=TA_CENTER)
TC = S('TC', 'Normal',
fontSize=7.8, leading=10.5, textColor=BLACK,
fontName='Helvetica', alignment=TA_LEFT)
TC_C = S('TC_C', 'Normal',
fontSize=7.8, leading=10.5, textColor=BLACK,
fontName='Helvetica', alignment=TA_CENTER)
TC_B = S('TC_B', 'Normal',
fontSize=7.8, leading=10.5, textColor=DARK_BLUE,
fontName='Helvetica-Bold', alignment=TA_LEFT)
FOOTER_STYLE = S('Footer', 'Normal',
fontSize=7, leading=9, textColor=GREY_DARK,
fontName='Helvetica', alignment=TA_CENTER)
# ─────────────────────────── HELPERS ──────────────────────────────────
def HR(color=GREY_MED, width=1, spaceB=4, spaceA=4):
return HRFlowable(width='100%', thickness=width, color=color,
spaceAfter=spaceA, spaceBefore=spaceB)
def SP(h=0.3):
return Spacer(1, h*cm)
def p(text, style=BODY):
return Paragraph(text, style)
def bullet(text, symbol='•'):
return Paragraph(f'{symbol} {text}', BULLET)
def section_header(title, color=DARK_BLUE, text_color=WHITE):
data = [[Paragraph(title, ParagraphStyle('sh', parent=H1,
backColor=color,
textColor=text_color))]]
t = Table(data, colWidths=[PAGE_W - 2*MARGIN])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), color),
('ROUNDEDCORNERS', [4]),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 10),
]))
return t
def info_box(title, content_paras, bg=LIGHT_BLUE, border=MED_BLUE):
"""A titled info box."""
inner = [[p(title, S('ib_title','Normal',
fontSize=8.5, fontName='Helvetica-Bold',
textColor=border))]]
for c in content_paras:
inner.append([c])
t = Table(inner, colWidths=[PAGE_W - 2*MARGIN - 0.4*cm])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), bg),
('BOX', (0,0), (-1,-1), 1, border),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 8),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('ROUNDEDCORNERS', [4]),
]))
return t
def make_table(headers, rows, col_widths, header_bg=DARK_BLUE,
row_alt=GREY_LIGHT, zebra=True):
header_row = [Paragraph(h, TH) for h in headers]
data = [header_row]
for i, row in enumerate(rows):
data.append([Paragraph(str(c), TC) if not isinstance(c, Paragraph) else c
for c in row])
t = Table(data, colWidths=col_widths, repeatRows=1)
style = [
('BACKGROUND', (0,0), (-1,0), header_bg),
('GRID', (0,0), (-1,-1), 0.4, GREY_MED),
('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, row_alt] if zebra else [WHITE]),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 5),
('RIGHTPADDING', (0,0), (-1,-1), 5),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
]
t.setStyle(TableStyle(style))
return t
def colored_cell(text, bg, text_color=BLACK, bold=False):
style = S('cc','Normal', fontSize=7.8, leading=10.5,
textColor=text_color,
fontName='Helvetica-Bold' if bold else 'Helvetica',
alignment=TA_CENTER)
return Paragraph(text, style)
# ─────────────────────── COVER PAGE ──────────────────────────────────
def build_cover(canvas_obj, doc):
canvas_obj.saveState()
# Background gradient-like blocks
canvas_obj.setFillColor(DARK_BLUE)
canvas_obj.rect(0, PAGE_H*0.38, PAGE_W, PAGE_H*0.62, fill=1, stroke=0)
canvas_obj.setFillColor(MED_BLUE)
canvas_obj.rect(0, PAGE_H*0.35, PAGE_W, PAGE_H*0.05, fill=1, stroke=0)
canvas_obj.setFillColor(TEAL)
canvas_obj.rect(0, 0, PAGE_W, PAGE_H*0.35, fill=1, stroke=0)
# Accent line
canvas_obj.setStrokeColor(LIGHT_BLUE)
canvas_obj.setLineWidth(3)
canvas_obj.line(MARGIN, PAGE_H*0.38, PAGE_W - MARGIN, PAGE_H*0.38)
# Title
canvas_obj.setFillColor(WHITE)
canvas_obj.setFont('Helvetica-Bold', 30)
canvas_obj.drawCentredString(PAGE_W/2, PAGE_H*0.72, 'ASTHMA MANAGEMENT')
canvas_obj.setFont('Helvetica-Bold', 24)
canvas_obj.drawCentredString(PAGE_W/2, PAGE_H*0.65, 'CLINICAL REFERENCE')
# Subtitle box
canvas_obj.setFillColor(LIGHT_BLUE)
canvas_obj.roundRect(MARGIN, PAGE_H*0.58, PAGE_W-2*MARGIN, 1.2*cm, 5, fill=1, stroke=0)
canvas_obj.setFillColor(DARK_BLUE)
canvas_obj.setFont('Helvetica-Bold', 12)
canvas_obj.drawCentredString(PAGE_W/2, PAGE_H*0.587, 'Algorithms, Biologic Selection & Acute Management')
# Info boxes in lower section
boxes = [
('GINA 2026', 'Stepwise Management\n5-Step Algorithm'),
('BIOLOGICS', 'Selection Criteria\n8 Approved Agents'),
('ACUTE ASTHMA', 'Severity Grading\nStatus Asthmaticus'),
('BIOMARKERS', 'FeNO · Eosinophils\nIgE · Periostin'),
]
bw = (PAGE_W - 2*MARGIN - 0.3*cm*3) / 4
bx = MARGIN
by = PAGE_H*0.17
for i, (title, desc) in enumerate(boxes):
canvas_obj.setFillColor(colors.HexColor('#1A6B7C'))
canvas_obj.roundRect(bx, by, bw, 2.8*cm, 5, fill=1, stroke=0)
canvas_obj.setStrokeColor(LIGHT_TEAL)
canvas_obj.setLineWidth(1)
canvas_obj.roundRect(bx, by, bw, 2.8*cm, 5, fill=0, stroke=1)
canvas_obj.setFillColor(LIGHT_TEAL)
canvas_obj.setFont('Helvetica-Bold', 9)
canvas_obj.drawCentredString(bx + bw/2, by + 2.0*cm, title)
canvas_obj.setFillColor(WHITE)
canvas_obj.setFont('Helvetica', 7.5)
lines = desc.split('\n')
for j, line in enumerate(lines):
canvas_obj.drawCentredString(bx + bw/2, by + 1.2*cm - j*0.45*cm, line)
bx += bw + 0.3*cm
# Footer
canvas_obj.setFillColor(GREY_LIGHT)
canvas_obj.setFont('Helvetica-Oblique', 7.5)
canvas_obj.drawCentredString(PAGE_W/2, 0.9*cm,
f'Prepared for Respiratory Medicine PGT | Based on GINA 2026, Harrison\'s 22E, Fishman\'s Pulmonary | {datetime.date.today().strftime("%B %Y")}')
canvas_obj.restoreState()
# ─────────────────────── PAGE TEMPLATE ───────────────────────────────
def build_page_header_footer(canvas_obj, doc):
canvas_obj.saveState()
# Header bar
canvas_obj.setFillColor(DARK_BLUE)
canvas_obj.rect(0, PAGE_H - 1.4*cm, PAGE_W, 1.4*cm, fill=1, stroke=0)
canvas_obj.setFillColor(WHITE)
canvas_obj.setFont('Helvetica-Bold', 9)
canvas_obj.drawString(MARGIN, PAGE_H - 0.9*cm, 'ASTHMA MANAGEMENT REFERENCE')
canvas_obj.setFont('Helvetica', 8)
canvas_obj.drawRightString(PAGE_W - MARGIN, PAGE_H - 0.9*cm,
f'GINA 2026 | Harrison\'s 22E | Fishman\'s Pulmonary')
# Footer
canvas_obj.setFillColor(GREY_LIGHT)
canvas_obj.rect(0, 0, PAGE_W, 1.2*cm, fill=1, stroke=0)
canvas_obj.setStrokeColor(GREY_MED)
canvas_obj.setLineWidth(0.5)
canvas_obj.line(0, 1.2*cm, PAGE_W, 1.2*cm)
canvas_obj.setFillColor(GREY_DARK)
canvas_obj.setFont('Helvetica', 7)
canvas_obj.drawCentredString(PAGE_W/2, 0.45*cm,
f'For educational use only – Not for clinical decision-making without specialist review | Page {doc.page}')
canvas_obj.restoreState()
# ─────────────────────────── CONTENT ──────────────────────────────────
def build_story():
story = []
W = PAGE_W - 2*MARGIN # usable width
# ══════════════════════════════════════════════════════════════════
# PAGE 1 – DEFINITION, EPIDEMIOLOGY, DIAGNOSIS OVERVIEW
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.3))
story.append(section_header('1. DEFINITION & KEY CONCEPTS'))
story.append(SP(0.3))
defn_data = [
[Paragraph('<b>ASTHMA</b> is a heterogeneous disease characterised by <b>chronic airway inflammation</b>, '
'<b>airway hyperresponsiveness (AHR)</b>, and <b>variable airflow obstruction</b> that is '
'usually reversible. In a large proportion, inflammation is <i>eosinophilic (T2-high)</i>; '
'some patients present with neutrophilic or paucigranulocytic inflammation (T2-low).', BODY)]
]
t = Table(defn_data, colWidths=[W])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), LIGHT_BLUE),
('BOX', (0,0), (-1,-1), 1.5, MED_BLUE),
('LEFTPADDING', (0,0), (-1,-1), 10),
('RIGHTPADDING', (0,0), (-1,-1), 10),
('TOPPADDING', (0,0), (-1,-1), 8),
('BOTTOMPADDING', (0,0), (-1,-1), 8),
]))
story.append(t)
story.append(SP(0.4))
# Three-column key facts
facts = [
[Paragraph('<b>GLOBAL BURDEN</b>\n262 million people\nworldwide affected', TC_C),
Paragraph('<b>US PREVALENCE</b>\n7.9% adults\n8.4% children', TC_C),
Paragraph('<b>ECONOMICS</b>\nUS $82 billion/year\ntotal burden (2013)', TC_C),
Paragraph('<b>MORTALITY</b>\n~3–4,000 deaths/year\nin USA; preventable', TC_C)],
]
fact_t = Table(facts, colWidths=[W/4]*4)
fact_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (0,0), LIGHT_BLUE),
('BACKGROUND', (1,0), (1,0), LIGHT_GREEN),
('BACKGROUND', (2,0), (2,0), LIGHT_ORANGE),
('BACKGROUND', (3,0), (3,0), LIGHT_RED),
('BOX', (0,0), (-1,-1), 0.5, GREY_MED),
('INNERGRID', (0,0), (-1,-1), 0.5, GREY_MED),
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 6),
]))
story.append(fact_t)
story.append(SP(0.5))
# ── DIAGNOSIS CRITERIA ──
story.append(p('DIAGNOSTIC CRITERIA', H2))
story.append(HR(MED_BLUE, 1.5, 2, 5))
diag_headers = ['Criterion', 'Threshold / Finding', 'Notes']
diag_rows = [
[p('<b>Spirometry reversibility</b>', TC_B),
'FEV₁ ↑ ≥12% AND ≥200 mL post-bronchodilator',
'Most specific objective test; confirm before starting ICS if possible'],
[p('<b>PEFR variability</b>', TC_B),
'>10% diurnal variability (am vs pm)',
'Higher variability = worse control; better predictor of exacerbations than single PEFR'],
[p('<b>Methacholine PC₂₀</b>', TC_B),
'<8 mg/mL = significant AHR',
'High sensitivity; false positives in COPD, rhinitis, post-viral; use when spirometry normal'],
[p('<b>Mannitol challenge</b>', TC_B),
'≥15% fall in FEV₁ at ≤635 mg cumulative',
'Higher specificity than methacholine for active asthma'],
[p('<b>FeNO</b>', TC_B),
'≥40 ppb = high T2 inflammation',
'<25 ppb = eosinophilic inflammation less likely; falls with ICS (adherence marker)'],
[p('<b>Trial of ICS therapy</b>', TC_B),
'Significant symptomatic improvement',
'In mild cases where spirometry is normal; confirms diagnosis retrospectively'],
]
story.append(make_table(diag_headers, diag_rows,
[3.5*cm, 6.5*cm, W-10*cm]))
story.append(SP(0.3))
story.append(p('⚠ <b>IMPORTANT:</b> >1/3 of physician-diagnosed asthma does NOT meet objective criteria. '
'Always confirm with spirometry or provocation testing before starting long-term therapy.', WARN))
story.append(SP(0.5))
# ── BIOMARKERS TABLE ──
story.append(p('KEY BIOMARKERS & INTERPRETATION', H2))
story.append(HR(MED_BLUE, 1.5, 2, 5))
bio_headers = ['Biomarker', 'Low', 'Borderline', 'High', 'Clinical Use']
bio_rows = [
['FeNO (ppb)',
colored_cell('<25\nT2-low', LIGHT_GREEN, GREEN, True),
colored_cell('25–39\nBorderline', LIGHT_YELLOW, YELLOW, True),
colored_cell('≥40\nEosinophilic', LIGHT_RED, RED, True),
'T2 phenotyping; ICS response prediction; adherence monitoring; biologic eligibility'],
['Blood Eos (/µL)',
colored_cell('<150\nT2-low', LIGHT_GREEN, GREEN, True),
colored_cell('150–299\nMild T2', LIGHT_YELLOW, YELLOW, True),
colored_cell('≥300\nHigh T2', LIGHT_RED, RED, True),
'Biologic eligibility (mepolizumab ≥150/300, benralizumab ≥300, reslizumab ≥400)'],
['Total IgE (IU/mL)',
colored_cell('<30\nUnlikely allergic', LIGHT_GREEN, GREEN, True),
colored_cell('30–700\nAllergic range', LIGHT_YELLOW, YELLOW, True),
colored_cell('>700\nHigh IgE', LIGHT_RED, RED, True),
'Omalizumab eligibility (IgE 30–700 + perennial allergen sensitisation)'],
['Sputum Eos (%)',
colored_cell('<2%\nNormal', LIGHT_GREEN, GREEN, True),
colored_cell('2–3%\nMild eos', LIGHT_YELLOW, YELLOW, True),
colored_cell('≥3%\nEosinophilic', LIGHT_RED, RED, True),
'Gold standard for T2 phenotyping; ICS dose titration in severe asthma'],
['Periostin (ng/mL)',
colored_cell('<25\nLow', LIGHT_GREEN, GREEN, True),
colored_cell('25–50\nModerate', LIGHT_YELLOW, YELLOW, True),
colored_cell('>50\nHigh (esp. AERD)', LIGHT_RED, RED, True),
'T2 inflammation marker; highest in AERD (mean 64.7); predicts dupilumab response'],
]
t = make_table(bio_headers, bio_rows,
[2.8*cm, 2.2*cm, 2.2*cm, 2.2*cm, W-9.4*cm])
story.append(t)
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════
# PAGE 2 – GINA 2026 STEPWISE MANAGEMENT
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.2))
story.append(section_header('2. GINA 2026 STEPWISE MANAGEMENT ALGORITHM', TEAL))
story.append(SP(0.3))
# Key paradigm box
story.append(info_box(
'🔑 KEY PARADIGM CHANGE – GINA 2026 (Anti-Inflammatory Reliever, AIR)',
[
p('<b>ICS/formoterol as reliever at ALL steps</b> – replacing SABA-only approach. '
'Using ICS/formoterol as-needed ensures anti-inflammatory therapy accompanies '
'every bronchodilator dose, reducing severe exacerbations and OCS use.', BODY),
p('<b>ICS-formoterol (MART strategy)</b> = Maintenance And Reliever Therapy: '
'same inhaler used for both daily controller AND as-needed relief at steps 3–4.', BODY),
p('<b>Never use LABA as monotherapy</b> in asthma. LABAs must always be combined with ICS.', WARN),
], LIGHT_TEAL, TEAL))
story.append(SP(0.4))
# 5-Step Table
step_colors = [LIGHT_GREEN, LIGHT_BLUE, LIGHT_YELLOW, LIGHT_ORANGE, LIGHT_RED]
step_borders = [GREEN, MED_BLUE, YELLOW, ORANGE, RED]
step_labels = ['STEP 1', 'STEP 2', 'STEP 3', 'STEP 4', 'STEP 5']
step_severity = ['Intermittent\n(<2×/month)', 'Mild persistent\n(≥2×/month,\nnot daily)',
'Moderate\n(daily symptoms)', 'Severe\n(uncontrolled\non step 3)',
'Very severe\n(uncontrolled\non step 4)']
step_controller = [
'As-needed\nlow-dose\nICS-formoterol*',
'Low-dose ICS daily\n+ as-needed\nICS-formoterol*',
'Low-dose\nICS/LABA\n(daily)',
'Medium-dose\nICS/LABA\n(daily)',
'High-dose ICS/LABA\n+ phenotypic\nassessment\n± biologic therapy',
]
step_reliever = ['ICS/formoterol\n(AIR)'] * 5
step_alt = [
'Low-dose ICS\nwith each SABA†',
'LTRA (note:\nmontelukast\nwarning!)',
'Medium-dose ICS\nor low-dose\nICS + LTRA',
'Add LAMA\n(tiotropium)\nor add LTRA',
'Low-dose OCS\n(last resort;\nconsider side-effects)',
]
step_data = [
[Paragraph('<b>STEP</b>', TH),
Paragraph('<b>SEVERITY</b>', TH),
Paragraph('<b>PREFERRED\nCONTROLLER</b>', TH),
Paragraph('<b>PREFERRED\nRELIEVER</b>', TH),
Paragraph('<b>ALTERNATIVE /\nADD-ON OPTIONS</b>', TH)],
]
for i in range(5):
step_data.append([
colored_cell(step_labels[i], step_colors[i],
step_borders[i], True),
Paragraph(step_severity[i], TC_C),
Paragraph(step_controller[i], TC_C),
Paragraph(step_reliever[i], TC_C),
Paragraph(step_alt[i], TC),
])
step_t = Table(step_data, colWidths=[1.6*cm, 2.8*cm, 3.6*cm, 2.6*cm, W-10.6*cm],
repeatRows=1)
step_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), DARK_BLUE),
('TEXTCOLOR', (0,0), (-1,0), WHITE),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('ROWBACKGROUNDS', (0,1), (-1,-1),
[LIGHT_GREEN, LIGHT_BLUE, LIGHT_YELLOW, LIGHT_ORANGE, LIGHT_RED]),
]))
story.append(step_t)
story.append(SP(0.25))
# Footnotes
story.append(p('* As-needed ICS-formoterol = Anti-Inflammatory Reliever (AIR); preferred at all steps per GINA 2026. '
'† NAEPP alternative: ICS taken whenever SABA taken (step 1). '
'‡ ICS/formoterol MART = same inhaler for controller + reliever (steps 3–4).', NOTE))
story.append(SP(0.4))
# Stepping up/down rules
story.append(p('STEPPING UP / DOWN RULES', H2))
story.append(HR(TEAL, 1.5, 2, 5))
updown_data = [
[Paragraph('<b>Before Stepping UP – Check First:</b>', TC_B),
Paragraph('<b>Stepping DOWN – When to:</b>', TC_B)],
[Paragraph('✓ Inhaler technique correct?\n'
'✓ Adherence adequate (>80%)?\n'
'✓ Trigger avoidance addressed?\n'
'✓ Comorbidities treated (GERD, rhinitis, OSA, VCD)?\n'
'✓ Diagnosis confirmed (not VCD/HF mimicking asthma)?\n\n'
'Step up only if poor control persists ≥2–3 months\n'
'after all of the above are optimised.', BODY),
Paragraph('✓ Asthma well-controlled for ≥3 months\n'
'✓ No exacerbations, normal FEV₁\n'
'✓ Not high-risk season (e.g., viral season)\n\n'
'⚠ NEVER fully stop ICS (even in step 1)\n'
'⚠ ICS dose reductions: decrease by 25–50% every 3 months\n'
'⚠ ICS discontinuation risks exacerbation and rebound eosinophilia', BODY)],
]
updown_t = Table(updown_data, colWidths=[W/2, W/2])
updown_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), LIGHT_TEAL),
('BACKGROUND', (0,1), (0,1), LIGHT_GREEN),
('BACKGROUND', (1,1), (1,1), LIGHT_YELLOW),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('TOPPADDING', (0,0), (-1,-1), 6),
('BOTTOMPADDING', (0,0), (-1,-1), 6),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(updown_t)
story.append(SP(0.4))
# OCS minimisation (GINA 2026 new)
story.append(info_box(
'OCS MINIMISATION – GINA 2026 New Recommendation',
[
bullet('All patients must receive <b>ICS-containing therapy</b> – this is non-negotiable.'),
bullet('ICS-formoterol AIR or ICS-SABA reduces risk of severe exacerbations requiring OCS.'),
bullet('Optimise modifiable risk factors before adding OCS (see comorbidities).'),
bullet('If OCS required long-term → refer for phenotyping + biologic therapy assessment.'),
p('OCS side effects: Diabetes, osteoporosis, HPA axis suppression, cataracts, '
'weight gain, immunosuppression, hypertension, avascular necrosis.', NOTE),
], LIGHT_YELLOW, ORANGE))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════
# PAGE 3 – BIOLOGIC SELECTION
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.2))
story.append(section_header('3. BIOLOGIC THERAPY – TARGETS, ELIGIBILITY & SELECTION', PURPLE))
story.append(SP(0.3))
story.append(p('Biologics are used at <b>Step 5</b> (or after Step 4 failure). All current approved biologics '
'target the <b>T2 inflammatory cascade</b>. Selection is based on biomarker profile, '
'comorbidities, route/frequency preference, and cost.', BODY))
story.append(SP(0.3))
# T2 cascade summary
cascade_data = [
[Paragraph('<b>Epithelial Alarmins</b>\n(upstream)', TC_C),
Paragraph('→', TC_C),
Paragraph('<b>ILC2 / Th2</b>\nActivation', TC_C),
Paragraph('→', TC_C),
Paragraph('<b>IL-4, IL-5, IL-13</b>\n(effector cytokines)', TC_C),
Paragraph('→', TC_C),
Paragraph('<b>IgE · Eosinophils\nMucus · AHR · Fibrosis</b>', TC_C)],
[Paragraph('TSLP, IL-33, IL-25', NOTE),
Paragraph('', TC_C),
Paragraph('', TC_C),
Paragraph('', TC_C),
Paragraph('IL-4 → IgE switching\nIL-5 → Eosinophil Δ\nIL-13 → Mucus, AHR', NOTE),
Paragraph('', TC_C),
Paragraph('', TC_C)],
]
cascade_t = Table(cascade_data, colWidths=[2.8*cm,0.6*cm,2.4*cm,0.6*cm,3.2*cm,0.6*cm,W-10.2*cm])
cascade_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (0,-1), LIGHT_PURPLE),
('BACKGROUND', (2,0), (2,-1), LIGHT_TEAL),
('BACKGROUND', (4,0), (4,-1), LIGHT_BLUE),
('BACKGROUND', (6,0), (6,-1), LIGHT_RED),
('BOX', (0,0), (0,-1), 0.5, PURPLE),
('BOX', (2,0), (2,-1), 0.5, TEAL),
('BOX', (4,0), (4,-1), 0.5, MED_BLUE),
('BOX', (6,0), (6,-1), 0.5, RED),
('ALIGN', (0,0), (-1,-1), 'CENTER'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
]))
story.append(cascade_t)
story.append(SP(0.4))
# Biologic table
story.append(p('APPROVED BIOLOGICS – COMPREHENSIVE COMPARISON', H2))
story.append(HR(PURPLE, 1.5, 2, 5))
bio_t_headers = ['Agent', 'Target', 'Route /\nFreq.', 'Eos\nThreshold', 'IgE\nRange',
'Key Indications / Notes']
bio_t_rows = [
[p('<b>Omalizumab</b>\n(Xolair)', TC_B),
'Anti-IgE', 'SC\nq2–4w',
p('Not required\n(IgE-based)', TC_C),
p('30–700\nIU/mL', TC_C),
'Perennial allergen sensitisation + IgE 30–700; also: CRSwNP, chronic urticaria; reduces exacerb. 25–50%'],
[p('<b>Omalizumab-igec</b>\n(biosimilar) ★NEW 2026', TC_B),
'Anti-IgE', 'SC\nq2–4w',
p('Not required', TC_C),
p('30–700\nIU/mL', TC_C),
'Biosimilar of omalizumab; GINA 2026: now approved for CRSwNP'],
[p('<b>Mepolizumab</b>\n(Nucala)', TC_B),
'Anti-IL-5', 'SC 100mg\nq4w',
p('≥150 screen\n≥300 preferred', TC_C),
p('N/A', TC_C),
'Also: EGPA, HES, CRSwNP; reduces exacerb. ~50%; reduces OCS use; weight-independent dose'],
[p('<b>Reslizumab</b>\n(Cinqair)', TC_B),
'Anti-IL-5', 'IV 3mg/kg\nq4w',
p('≥400/µL', TC_C),
p('N/A', TC_C),
'Only IV biologic for asthma; weight-based dosing; reduces exacerb. ~50%; anaphylaxis risk (0.3%)'],
[p('<b>Benralizumab</b>\n(Fasenra)', TC_B),
'Anti-IL-5Rα', 'SC 30mg\nq4w ×3\nthen q8w',
p('≥300/µL', TC_C),
p('N/A', TC_C),
'Near-complete eosinophil depletion via ADCC; also CRSwNP; convenient q8w maintenance'],
[p('<b>Depemokimab</b>\n(Slynd) ★GINA 2026', TC_B),
'Anti-IL-5\n(long-acting)', 'SC\nq6 months',
p('≥300/µL', TC_C),
p('N/A', TC_C),
'★ NEWEST biologic (GINA 2026); longest dosing interval; ≥12y severe eos asthma; ≥18y CRSwNP'],
[p('<b>Dupilumab</b>\n(Dupixent)', TC_B),
'Anti-IL-4Rα\n(blocks IL-4\n+ IL-13)', 'SC\nq2w',
p('≥150/µL or\nFeNO ≥25ppb', TC_C),
p('N/A', TC_C),
'Broadest T2 coverage; also: atopic dermatitis, CRSwNP, eosinophilic oesophagitis, COPD with eos; arthralgia SE'],
[p('<b>Tezepelumab</b>\n(Tezspire)', TC_B),
'Anti-TSLP\n(upstream\nalarmin)', 'SC 210mg\nq4w',
p('NONE\nrequired', TC_C),
p('N/A', TC_C),
'Broadest efficacy including T2-low/paucigranulocytic; works even with low eos/FeNO; also CRSwNP'],
]
story.append(make_table(bio_t_headers, bio_t_rows,
[2.5*cm, 2.0*cm, 1.8*cm, 1.8*cm, 1.5*cm, W-9.6*cm],
header_bg=PURPLE))
story.append(SP(0.3))
story.append(p('ADCC = Antibody-Dependent Cell-mediated Cytotoxicity. EGPA = Eosinophilic Granulomatosis with Polyangiitis. '
'HES = Hypereosinophilic Syndrome. CRSwNP = Chronic Rhinosinusitis with Nasal Polyps. '
'★ = new/updated in GINA 2026.', NOTE))
story.append(SP(0.4))
# Biologic selection algorithm
story.append(p('BIOLOGIC SELECTION ALGORITHM (GINA 2026)', H2))
story.append(HR(PURPLE, 1.5, 2, 5))
algo_data = [
[Paragraph('<b>Step 5 Severe Asthma – Assess T2 Biomarkers</b>', TH),
Paragraph('<b>Biologic of Choice</b>', TH),
Paragraph('<b>Key Consideration</b>', TH)],
[Paragraph('<b>Allergic</b>: IgE 30–700 IU/mL + perennial\nallergen sensitisation (any eos level)', TC_B),
colored_cell('OMALIZUMAB\nor biosimilar', LIGHT_PURPLE, PURPLE, True),
'Skin prick test / RAST positive; also consider if CRSwNP present'],
[Paragraph('<b>Eosinophilic</b>: Blood eos ≥300/µL\n(no allergic features / IgE not eligible)', TC_B),
colored_cell('MEPOLIZUMAB\nBENRALIZUMAB\nDEPEMABOKIMAB★', LIGHT_RED, RED, True),
'Depemokimab preferred if 6-monthly dosing desired; benralizumab for q8w maintenance'],
[Paragraph('<b>High eosinophils</b>: Blood eos ≥400/µL\n(overweight/obese patient)', TC_B),
colored_cell('RESLIZUMAB\n(IV, weight-based)', LIGHT_ORANGE, ORANGE, True),
'Weight-based dosing may be advantageous in obese patients'],
[Paragraph('<b>T2 + Atopic dermatitis, CRSwNP, or\neosinophilic oesophagitis comorbidity</b>', TC_B),
colored_cell('DUPILUMAB\n(IL-4Rα)', LIGHT_BLUE, MED_BLUE, True),
'"One airway, one disease" – addresses multiple atopic manifestations simultaneously'],
[Paragraph('<b>Uncertain T2 phenotype</b>: Low eos, low FeNO;\nor T2-low / paucigranulocytic', TC_B),
colored_cell('TEZEPELUMAB\n(anti-TSLP)', LIGHT_TEAL, TEAL, True),
'Only biologic with no minimum biomarker threshold; broadest efficacy spectrum'],
[Paragraph('<b>Multiple positive biomarkers</b>: Elevated eos\nAND elevated IgE AND high FeNO', TC_B),
colored_cell('SELECT BASED ON:\nComorbidities / Cost\nRoute / Frequency', LIGHT_GREEN, GREEN, True),
'No head-to-head RCTs; GINA 2026 decision tree uses comorbidities + patient preference'],
]
algo_t = Table(algo_data, colWidths=[5.5*cm, 3.8*cm, W-9.3*cm], repeatRows=1)
algo_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), PURPLE),
('TEXTCOLOR', (0,0), (-1,0), WHITE),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, GREY_LIGHT]),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 5),
('RIGHTPADDING', (0,0), (-1,-1), 5),
]))
story.append(algo_t)
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════
# PAGE 4 – ACUTE SEVERE ASTHMA / STATUS ASTHMATICUS
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.2))
story.append(section_header('4. ACUTE SEVERE ASTHMA & STATUS ASTHMATICUS', RED))
story.append(SP(0.3))
# ABG evolution
story.append(p('ABG EVOLUTION IN ACUTE ASTHMA ATTACK', H2))
story.append(HR(RED, 1.5, 2, 5))
abg_data = [
[Paragraph('<b>Stage</b>', TH),
Paragraph('<b>PaO₂</b>', TH),
Paragraph('<b>PaCO₂</b>', TH),
Paragraph('<b>pH</b>', TH),
Paragraph('<b>SpO₂</b>', TH),
Paragraph('<b>Interpretation / Action</b>', TH)],
[colored_cell('EARLY\nMILD', LIGHT_GREEN, GREEN, True),
colored_cell('Normal\nor ↓ mildly', LIGHT_GREEN, GREEN),
colored_cell('↓ <40\nHypocapnia', LIGHT_GREEN, GREEN, True),
colored_cell('↑ >7.45\nAlkalosis', LIGHT_GREEN, GREEN),
colored_cell('>95%', LIGHT_GREEN, GREEN),
'Tachypnoea compensates; hyperventilation. Treat with SABA + ICS.'],
[colored_cell('MODERATE', LIGHT_YELLOW, YELLOW, True),
colored_cell('↓ 60–80', LIGHT_YELLOW, YELLOW),
colored_cell('Normal\n~35–45\n⚠ DANGER', LIGHT_RED, RED, True),
colored_cell('Normal\n7.40–7.45', LIGHT_YELLOW, YELLOW),
colored_cell('90–95%', LIGHT_YELLOW, YELLOW),
'⚠ NORMAL PaCO₂ in DISTRESSED asthmatic = impending respiratory failure. Aggressive treatment.'],
[colored_cell('SEVERE /\nFAILURE', LIGHT_RED, RED, True),
colored_cell('↓↓ <60', LIGHT_RED, RED),
colored_cell('↑ >45\nHypercapnia', LIGHT_RED, RED, True),
colored_cell('↓ <7.35\nAcidosis', LIGHT_RED, RED, True),
colored_cell('<90%', LIGHT_RED, RED),
'⚠ INTUBATE. Permissive hypercapnia strategy. Aim pH ≥7.2; bicarbonate if needed.'],
]
abg_t = Table(abg_data, colWidths=[1.8*cm, 1.8*cm, 2.5*cm, 2.2*cm, 1.8*cm, W-10.1*cm],
repeatRows=1)
abg_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), RED),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('ALIGN', (0,0), (-1,0), 'CENTER'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 5),
('RIGHTPADDING', (0,0), (-1,-1), 5),
]))
story.append(abg_t)
story.append(SP(0.3))
# Severity grading
story.append(p('SEVERITY GRADING', H2))
story.append(HR(RED, 1.5, 2, 5))
sev_headers = ['Feature', 'Mild-Moderate', 'Severe', 'Life-Threatening']
sev_rows = [
['Speech', 'Full sentences', 'Phrases', 'Words / unable to speak'],
['RR (breaths/min)', '20–25', '25–30', '>30'],
['HR (bpm)', '<110', '110–120', '>120'],
['PEFR (% best/predicted)', '>50–75%', '33–50%', '<33% ("brittle")'],
['SpO₂', '>95%', '92–95%', '<92%'],
['Wheeze', 'Moderate', 'Loud', '★ SILENT CHEST (DANGER)'],
['Mental status', 'Normal', 'Agitated', '★ Drowsy / confused'],
['PaCO₂', '<40 (hypocapnia)', '<40', '★ ≥45 (fatigue/failure)'],
]
sev_col = [4.0*cm, 3.5*cm, 3.0*cm, W-10.5*cm]
data = [[Paragraph(h, TH) for h in sev_headers]] + \
[[Paragraph(str(r[i]), TC) for i in range(4)] for r in sev_rows]
# Highlight danger signs in last column
for i, r in enumerate(sev_rows):
if '★' in r[3]:
data[i+1][3] = Paragraph(r[3].replace('★',''), WARN)
sev_t = Table(data, colWidths=sev_col, repeatRows=1)
sev_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), RED),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, GREY_LIGHT]),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 4),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('LEFTPADDING', (0,0), (-1,-1), 5),
('RIGHTPADDING', (0,0), (-1,-1), 5),
# Highlight life-threatening column
('BACKGROUND', (3,1), (3,-1), colors.HexColor('#FFF5F5')),
('TEXTCOLOR', (3,1), (3,-1), RED),
]))
story.append(sev_t)
story.append(SP(0.3))
# Management protocol
story.append(p('ACUTE MANAGEMENT PROTOCOL (GINA 2026)', H2))
story.append(HR(RED, 1.5, 2, 5))
mgmt_data = [
[Paragraph('<b>TIME</b>', TH),
Paragraph('<b>INTERVENTION</b>', TH),
Paragraph('<b>DOSE / DETAILS</b>', TH)],
[colored_cell('0–20 min\nIMM-\nEDIATE', LIGHT_RED, RED, True),
Paragraph('1. Supplemental O₂\n2. SABA (salbutamol)\n3. Ipratropium bromide\n4. Systemic corticosteroids', TC_B),
Paragraph('SpO₂ target 93–95% (avoid hyperoxia, GINA 2026)\n'
'MDI+spacer: 4–8 puffs; or nebuliser 2.5–5mg; repeat ×3 q20min\n'
'4–8 puffs MDI or 0.5mg neb; with each salbutamol dose (up to ×3)\n'
'Prednisolone 40–50mg PO or Hydrocortisone 100–200mg IV', TC)],
[colored_cell('1–2 h\nNO RESP-\nONSE', LIGHT_ORANGE, ORANGE, True),
Paragraph('5. IV Magnesium sulfate\n6. IV salbutamol infusion\n7. IV aminophylline\n8. Measure PEFR/FEV₁', TC_B),
Paragraph('2g IV over 20 min (single dose); reduces hospitalisation + intubation\n'
'5–15 mcg/kg/min; continuous; if not responding to inhaled\n'
'Loading 5mg/kg over 20min (if not on oral theophylline); rarely used\n'
'PEFR >60% → likely discharge with step-up; <60% → admit', TC)],
[colored_cell('If\nFAILING', LIGHT_RED, RED, True),
Paragraph('9. NIV (BiPAP)\n10. Heliox\n11. INTUBATION', TC_B),
Paragraph('BiPAP well-tolerated; reduces intubation rate; avoids ICU in selected patients\n'
'60:40 He:O₂; reduces turbulence; decreases WOB; use while preparing for intubation\n'
'RSI with KETAMINE (preferred: bronchodilator properties); avoid propofol if hypotensive', TC)],
[colored_cell('MV\nSTRATEGY', GREY_DARK, WHITE, True),
Paragraph('Mechanical ventilation\nin status asthmaticus', TC_B),
Paragraph('Low RR (8–12/min); low TV (6–8 mL/kg IBW); long expiratory time (I:E 1:3 or 1:4)\n'
'Permissive hypercapnia: allow PaCO₂ to rise; target pH ≥7.2; NaHCO₃ if pH <7.2\n'
'Monitor auto-PEEP (dynamic hyperinflation); consider NMB short-term\n'
'Volatile anaesthetics (isoflurane/sevoflurane) for refractory bronchospasm', TC)],
]
mgmt_t = Table(mgmt_data, colWidths=[2.2*cm, 4.8*cm, W-7.0*cm], repeatRows=1)
mgmt_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), RED),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 5),
('RIGHTPADDING', (0,0), (-1,-1), 5),
]))
story.append(mgmt_t)
story.append(SP(0.3))
# ATS/IDSA severe criteria
story.append(info_box(
'ATS/IDSA CRITERIA FOR SEVERE / ICU ASTHMA',
[
p('<b>Major criteria (1 = ICU):</b> Invasive mechanical ventilation; '
'haemodynamic compromise requiring vasopressor support', BODY),
p('<b>Minor criteria (≥3 = ICU):</b> RR ≥30; PaO₂/FiO₂ ≤250; multilobar infiltrates; '
'confusion/disorientation; uremia (BUN ≥20 mg/dL); WBC <4000 cells/mm³; '
'platelets <100,000; hypothermia (<36°C); hypotension requiring aggressive fluid resuscitation', BODY),
], LIGHT_RED, RED))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════
# PAGE 5 – ASTHMA MORTALITY RISK & SPECIAL SITUATIONS
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.2))
story.append(section_header('5. MORTALITY RISK FACTORS & SPECIAL CLINICAL SCENARIOS', ORANGE))
story.append(SP(0.3))
# Mortality risk factors
story.append(p('FATALITY RISK INDICATORS (GINA 2026 – NEW EMPHASIS)', H2))
story.append(HR(ORANGE, 1.5, 2, 5))
risk_col1 = [
'<b>1.</b> Previous near-fatal attack (intubation / ICU)',
'<b>2.</b> Hospitalisation for asthma in past 12 months',
'<b>3.</b> ≥2 ED visits in past 6 months',
'<b>4.</b> ≥2 courses of systemic corticosteroids in past year',
'<b>5.</b> Overuse of SABAs (>1 canister/month)',
'<b>6.</b> Currently not using ICS or non-adherent to ICS',
]
risk_col2 = [
'<b>7.</b> No written asthma action plan',
'<b>8.</b> Illicit drug use',
'<b>9.</b> Depression or anxiety',
'<b>10.</b> Severe psychosocial problems',
'<b>11.</b> Lower socioeconomic status / limited healthcare access',
'<b>12.</b> Food allergy + confirmed asthma (anaphylaxis risk)',
]
risk_data = [
[Paragraph('<b>⚠ HIGH-RISK PATIENT INDICATORS</b>', TH),
Paragraph('<b>⚠ FURTHER RISK FACTORS</b>', TH)],
]
for i in range(6):
risk_data.append([
Paragraph(risk_col1[i], BODY),
Paragraph(risk_col2[i], BODY),
])
risk_t = Table(risk_data, colWidths=[W/2, W/2])
risk_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), ORANGE),
('ROWBACKGROUNDS', (0,1), (-1,-1), [LIGHT_ORANGE, WHITE]),
('GRID', (0,0), (-1,-1), 0.5, GREY_MED),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
('LEFTPADDING', (0,0), (-1,-1), 8),
('RIGHTPADDING', (0,0), (-1,-1), 8),
]))
story.append(risk_t)
story.append(SP(0.4))
# Special scenarios
story.append(p('SPECIAL CLINICAL SCENARIOS', H2))
story.append(HR(ORANGE, 1.5, 2, 5))
# Two-column layout for special scenarios
col_w = (W - 0.4*cm) / 2
# AERD
aerd_data = [
[Paragraph('<b>ASPIRIN-EXACERBATED RESPIRATORY DISEASE (AERD)</b>', TC_B)],
[Paragraph('<b>Triad:</b> Asthma + Nasal polyposis + NSAID/aspirin sensitivity\n'
'<b>Mechanism:</b> COX-1 inhibition → ↓PGE₂ + ↑CysLTs → bronchoconstriction\n'
'<b>Diagnosis:</b> Aspirin oral/inhalation challenge (gold standard)\n'
'<b>Tx:</b> Avoid COX-1 inhibitors; LTRAs; aspirin desensitisation; dupilumab/tezepelumab for nasal polyps\n'
'<b>Periostin:</b> Highest among asthma endotypes (mean 64.7 ng/mL)', BODY)],
]
aerd_t = Table(aerd_data, colWidths=[col_w])
aerd_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), LIGHT_ORANGE),
('BOX', (0,0), (-1,-1), 1, ORANGE),
('LEFTPADDING', (0,0), (-1,-1), 7),
('RIGHTPADDING', (0,0), (-1,-1), 7),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
]))
# ACO
aco_data = [
[Paragraph('<b>ASTHMA-COPD OVERLAP (ACO)</b>', TC_B)],
[Paragraph('<b>Features:</b> Variable symptoms (asthma) + incomplete reversibility + smoking history (COPD)\n'
'<b>Spirometry:</b> Partial reversibility; FEV₁/FVC <0.70 post-BD\n'
'<b>Key rule:</b> ICS is MANDATORY (reduces mortality vs withdrawal)\n'
'<b>Add-on:</b> LABA + LAMA both may be used\n'
'⚠ SABA monotherapy is CONTRAINDICATED in ACO', BODY)],
]
aco_t = Table(aco_data, colWidths=[col_w])
aco_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), LIGHT_TEAL),
('BOX', (0,0), (-1,-1), 1, TEAL),
('LEFTPADDING', (0,0), (-1,-1), 7),
('RIGHTPADDING', (0,0), (-1,-1), 7),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
]))
# Pregnancy
preg_data = [
[Paragraph('<b>ASTHMA IN PREGNANCY</b>', TC_B)],
[Paragraph('<b>~4%</b> of pregnant women have asthma; 1/3 worsen during pregnancy\n'
'<b>Risk:</b> Moderate-severe asthma → preterm labour, LBW, perinatal death, pre-eclampsia\n'
'<b>ABG normal:</b> pH 7.45, PaCO₂ 27–32 mmHg → "normal" PaCO₂ 40 = hypercapnia!\n'
'<b>FEV₁ and PEFR:</b> Unchanged in pregnancy – useful for monitoring\n'
'<b>Safety:</b> ICS (all safe); salbutamol safe; montelukast avoid if possible', BODY)],
]
preg_t = Table(preg_data, colWidths=[col_w])
preg_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), LIGHT_PURPLE),
('BOX', (0,0), (-1,-1), 1, PURPLE),
('LEFTPADDING', (0,0), (-1,-1), 7),
('RIGHTPADDING', (0,0), (-1,-1), 7),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
]))
# EIB
eib_data = [
[Paragraph('<b>EXERCISE-INDUCED BRONCHOCONSTRICTION (EIB)</b>', TC_B)],
[Paragraph('<b>Mechanism:</b> Hyperventilation → airway desiccation → osmolarity change → mediator release\n'
'<b>Peak:</b> 5–15 min post-exercise; resolves 30–60 min spontaneously\n'
'<b>Diagnosis:</b> >10–15% fall in FEV₁ at 15–30 min post-exercise challenge\n'
'<b>Refractory period:</b> 2h window after exercise → less bronchoconstriction (mast cell depletion)\n'
'<b>Rx:</b> Pre-treatment SABA 15–20 min before; warm-up; ICS long-term', BODY)],
]
eib_t = Table(eib_data, colWidths=[col_w])
eib_t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,-1), LIGHT_GREEN),
('BOX', (0,0), (-1,-1), 1, GREEN),
('LEFTPADDING', (0,0), (-1,-1), 7),
('RIGHTPADDING', (0,0), (-1,-1), 7),
('TOPPADDING', (0,0), (-1,-1), 5),
('BOTTOMPADDING', (0,0), (-1,-1), 5),
]))
# Layout in 2 columns
two_col = Table(
[[aerd_t, aco_t],
[SP(0.25), SP(0.25)],
[preg_t, eib_t]],
colWidths=[col_w, col_w]
)
two_col.setStyle(TableStyle([
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 0),
('TOPPADDING', (0,0), (-1,-1), 0),
('BOTTOMPADDING', (0,0), (-1,-1), 0),
('INNERGRID', (0,0), (-1,-1), 0, WHITE),
('BOX', (0,0), (-1,-1), 0, WHITE),
('COLPADDING', (0,0), (-1,-1), 5),
]))
story.append(two_col)
story.append(SP(0.4))
# ── COMORBIDITIES TABLE ──
story.append(p('KEY COMORBIDITIES AFFECTING ASTHMA CONTROL', H2))
story.append(HR(ORANGE, 1.5, 2, 5))
comor_headers = ['Comorbidity', 'Impact on Asthma', 'Management']
comor_rows = [
['Rhinosinusitis / Nasal polyps',
'Worsens AHR via inflammatory mediators, post-nasal drip, neural reflexes',
'Intranasal steroids (reduce AHR + ED visits); biologics for polyps (dupilumab, tezepelumab)'],
['GERD',
'Independent predictor of exacerbations; microaspiration + vagal reflexes',
'Treat symptomatic GERD (PPI ± lifestyle); asymptomatic GERD treatment NOT beneficial'],
['Obesity',
'2–4× hospitalisation risk; non-eosinophilic pattern; mechanical restriction + adipokines',
'Weight loss (bariatric surgery reduces exacerbations); non-T2 management; LAMA add-on'],
['OSA',
'Increased exacerbation severity; AHI correlates with exacerbations; OCS worsens OSA',
'CPAP → improves QoL, reduces exacerbations, reduces bronchodilator use'],
['Anxiety / Depression',
'Increased exacerbation rates; may not distinguish anxiety from asthma',
'Psychological support; distinguish from VCD; optimise asthma control'],
['Vocal cord dysfunction (ILO)',
'Can mimic OR coexist with asthma; inspiratory stridor; commoner in women',
'Laryngoscopy during symptoms; speech therapy; treat anxiety; NOT responsive to bronchodilators'],
]
story.append(make_table(comor_headers, comor_rows,
[4.0*cm, 5.5*cm, W-9.5*cm], header_bg=ORANGE))
story.append(PageBreak())
# ══════════════════════════════════════════════════════════════════
# PAGE 6 – DRUG MECHANISMS & QUICK REFERENCE
# ══════════════════════════════════════════════════════════════════
story.append(SP(0.2))
story.append(section_header('6. DRUG MECHANISMS & QUICK REFERENCE', MED_BLUE))
story.append(SP(0.3))
# Drug mechanism table
story.append(p('MECHANISM OF ACTION – KEY DRUG CLASSES', H2))
story.append(HR(MED_BLUE, 1.5, 2, 5))
drug_headers = ['Drug Class', 'Mechanism', 'Key Points']
drug_rows = [
[p('<b>SABA\n</b>Salbutamol,\nLevalbuterol', TC_B),
'β₂ receptor → Gs → ↑cAMP → PKA → MLCK inhibition\n+ opens K⁺ channels → hyperpolarisation → SM relaxation',
'⚠ NEVER use as monotherapy (increased mortality). '
'Onset 3–5 min. Duration 4–6h. Side effects: tremor, tachycardia, hypokalaemia, Type B lactic acidosis.'],
[p('<b>LABA\n</b>Formoterol (rapid)\nSalmeterol (slow)', TC_B),
'Same as SABA. Formoterol = moderate lipophilicity (rapid + sustained).\nSalmeterol = anchored in exosite (slow onset)',
'Duration ~12h. FORMOTEROL can be reliever (rapid onset). SALMETEROL cannot. '
'NEVER use as monotherapy in asthma.'],
[p('<b>ICS\n</b>Budesonide,\nFluticasone,\nCiclesonide', TC_B),
'Binds GR → nuclear translocation → trans-repression of NF-κB/AP-1\n→ ↓IL-4, IL-5, IL-13, TNF-α, iNOS',
'Oral bioavailability: Beclomethasone 20%; Budesonide 10–15%; Fluticasone ~1%; Mometasone <1%; '
'Ciclesonide = pro-drug (activated in lung). Reduces FeNO + eosinophils + AHR. '
'Side effects: growth suppression (children), osteoporosis, cataracts.'],
[p('<b>LAMA\n</b>Tiotropium,\nUmeclidinium', TC_B),
'M₃ receptor blockade → ↓ACh-induced smooth muscle contraction;\nfunctional selectivity M₃/M₁ > M₂',
'Add-on Step 4–5. Note: blocking M₂ (autoinhibitory) would → ↑ACh release – '
'LAMAs have favourable kinetics avoiding this. Side effects: dry mouth, urinary retention, glaucoma (high dose).'],
[p('<b>LTRA\n</b>Montelukast,\nZafirlukast', TC_B),
'CysLT₁ receptor antagonism → blocks LTC₄/LTD₄/LTE₄\n→ ↓bronchoconstriction, ↓mucus, ↓eosinophil recruitment',
'⚠ FDA BLACK BOX: Montelukast → serious neuropsychiatric effects (suicidal ideation, depression). '
'Alternative Step 2. Good for AERD and EIB. Reduces BOTH early and late phase response.'],
[p('<b>IV Mg²⁺\n</b>Sulphate', TC_B),
'Competes with Ca²⁺ → inhibits smooth muscle contraction;\nantagonises Ca²⁺-dependent mediator release from mast cells',
'2g IV over 20 min. Single dose. For severe/life-threatening acute asthma. '
'Reduces hospitalisation rate and intubation need. Safe; mild hypotension risk.'],
[p('<b>Theophylline', TC_B),
'Inhibits phosphodiesterase → ↑cAMP + ↑cGMP\nAlso: adenosine receptor antagonism; restores HDAC2 activity (low dose)',
'Now RARELY used for asthma. Narrow therapeutic window. '
'Interactions: erythromycin, ciprofloxacin, rifampicin. '
'Low-dose theophylline may partially reverse steroid resistance via HDAC2.'],
]
story.append(make_table(drug_headers, drug_rows,
[2.8*cm, 5.0*cm, W-7.8*cm]))
story.append(SP(0.4))
# ICS potency/bioavailability comparison
story.append(p('ICS COMPARISON – ORAL BIOAVAILABILITY & PROPERTIES', H2))
story.append(HR(MED_BLUE, 1.5, 2, 5))
ics_headers = ['ICS Agent', 'Oral Bioavail.', 'Half-life', 'Special Feature', 'Pregnancy']
ics_rows = [
['Beclomethasone dipropionate', '~20%', '0.5h (BDP) → 17-BMP active', 'Oldest; oropharyngeal deposition issue; pMDI fine particle', 'B'],
['Budesonide', '~10–15%', '2–3h', 'Both pMDI and DPI; reference standard ICS', '★ Safe'],
['Fluticasone propionate', '~1%', '14h', 'High topical potency; commonly combined with salmeterol/vilanterol', 'B'],
['Fluticasone furoate', 'Negligible', '24h', 'Once-daily; only with vilanterol (ultra-LABA)', 'B'],
['Mometasone furoate', '<1%', '5h', 'Very low oral BA; combined with formoterol/indacaterol', 'C'],
['Ciclesonide', '<1%', '0.7h (parent) 45h (active)', 'Pro-drug activated in lung by esterases; NO oropharyngeal deposition', 'C'],
]
story.append(make_table(ics_headers, ics_rows,
[3.8*cm, 2.5*cm, 3.2*cm, 5.0*cm, 1.8*cm]))
story.append(SP(0.3))
# Quick reference summary box
story.append(info_box(
'📋 QUICK CLINICAL REFERENCE SUMMARY',
[
p('<b>T2 biomarkers positive (FeNO ≥40, eos ≥300, IgE elevated) + severe:</b> Step up to biologic. '
'Choose based on dominant comorbidity (atopic → omalizumab/dupilumab; eosinophilic → anti-IL-5; '
'no biomarker dominance → tezepelumab).', BODY),
p('<b>Normal PaCO₂ in a distressed asthmatic = DANGER:</b> Impending respiratory failure. '
'Do not be falsely reassured. Prepare for intubation.', WARN),
p('<b>ICS/formoterol (AIR) is now preferred at ALL steps</b> per GINA 2026 – not just steps 3–4.', BODY),
p('<b>Before stepping up:</b> Always check adherence (responsible for ≥50% of "refractory" cases), '
'inhaler technique, triggers, and comorbidities first.', BODY),
p('<b>Permissive hypercapnia</b> during MV in status asthmaticus: accept PaCO₂ rising; '
'target pH ≥7.2; use NaHCO₃ if pH <7.2; ketamine for induction.', BODY),
p('<b>Montelukast:</b> FDA black box warning for neuropsychiatric effects – inform patients before prescribing.', WARN),
p('<b>Depemokimab (GINA 2026):</b> Newest anti-IL-5 biologic; SC every 6 months; '
'longest dosing interval of any approved biologic for asthma.', BODY),
], LIGHT_BLUE, MED_BLUE))
story.append(SP(0.4))
# Sources
story.append(HR(GREY_MED, 0.5, 4, 4))
story.append(p('<b>Sources:</b> GINA Global Strategy for Asthma Management and Prevention 2026 (released May 5, 2026); '
'Harrison\'s Principles of Internal Medicine 22E (2025), Chapter 298; '
'Goodman & Gilman\'s Pharmacological Basis of Therapeutics, Chapter 44; '
'Fishman\'s Pulmonary Diseases and Disorders; '
'Murray & Nadel\'s Textbook of Respiratory Medicine; '
'Systematic Reviews: PMID 40520782 (biologics umbrella review, 2025).', NOTE))
story.append(p('Prepared for Respiratory Medicine PGT educational use. '
'Not a substitute for clinical judgment. All treatment decisions should be guided by '
'local guidelines, specialist review, and individual patient assessment.', NOTE))
return story
# ─────────────────────────── MAIN ─────────────────────────────────────
def main():
doc = make_doc()
story = build_story()
# Build with page templates
doc.build(
story,
onFirstPage=build_cover,
onLaterPages=build_page_header_footer,
)
print(f'✅ PDF created: {OUTPUT}')
if __name__ == '__main__':
main()