XLSX z Pyramid bezbolestně

... Petr Blahoš, 17. 5. 2018 Pyramid Python

V Pyramid Cookbook najedete návod, jak udělat vlastní renderer. Na první pohled by se přesně tento mohl dobře hodit. Předhodíme mu tabulku, a místo do csv bude dělat export do xlsx. Jednoduché.

Jenže on očekává data v tom nejprimitivnějším formátu - jako už připravenou tabulku. My máme naopak ve view nějaké volání SQL dotazu, které nám vrátí result set, ze kterého si pak v rendereru vybereme, co potřebujeme. Jak to tedy udělat obecněji?

Rozhraní pro registraci rendereru

Při registraci rendereru je první parametr buď přímo jméno rendereru, jako v případě string, json. Nebo je to přípona - .mako, .pt... My využijeme situaci, kdy je to přípona.

Návrh rozhraní rendereru

Nebudeme to komplikovat. Navrhneme si takový šablonovací systém, který bude definovat dvě funkce. Jedna vrátí hlavičku, druhá vrátí řádky tabulky. Naše šablony budou python moduly.

Cílový stav

Chceme být schopni použít kód našeho view tak, jak je. Pouze konfigurací nastavit, že aby se v nějakých případech použil jiný renderer. Např. takto:

# první view_config pro xlsx renderer
@view_config(context="pyrasample.resources.DBContext",
             renderer="dbtable.xlsx",
             request_param="xlsx")
# druhý view_config pro normální mako
@view_config(context="pyrasample.resources.DBContext",
             renderer="templates/dbtable.mako")
def dbtable(request):
    # případně nějaký kód, ale v tomto případě máme všechna data
    # připravena v contextu.
    return {}

Všiměte si predikátu request_param="xlsx". Ten, jak jste asi uhodli, způsobí, že tato konfigurace view se uplatní v případě, že mezi request.params je parametr xlsx.

Implementace rendereru

Pro zjednodušení situace řekneme, že šablony budou v modulu nášbalík.xlsx, a budou to normální python soubory s příponou .py. Renderer je musí umět najít, a spustit ty dvě funkce: get_header a iterate_rows. Zkusíme to takto:

import importlib

import openpyxl
import openpyxl.styles
import openpyxl.writer.excel


class XLSXRenderer(object):
    def __init__(self, info):
        # info.type je přípona, pro kterou jsme renderer registrovali
        self.suffix = info.type
        # info.package je package, který byl aktuální, když se
        # registroval renderer
        self.templates_pkg = info.package.__name__ + ".xlsx"

    def __call__(self, value, system):
        # najdeme a nahrajeme modul - šablonu
        templ_name = system["renderer_name"][:-len(self.suffix)]
        templ_module = importlib.import_module("." + templ_name, self.templates_pkg)

        # vytvoříme a naplníme workbook
        wb = openpyxl.Workbook()
        ws = wb.active
        if "get_header" in dir(templ_module):
            ws.append(getattr(templ_module, "get_header")(system, value))
            ws.row_dimensions[1].font = openpyxl.styles.Font(bold=True)
        if "iterate_rows" in dir(templ_module):
            for row in getattr(templ_module, "iterate_rows")(system, value):
                ws.append(row)

        # změníme content type a content disposition
        request = system.get('request')
        if not request is None:
            response = request.response
            ct = response.content_type
            if ct == response.default_content_type:
                response.content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
            response.content_disposition = 'attachment;filename=%s.xlsx' % templ_name

        # vrátíme data
        return openpyxl.writer.excel.save_virtual_workbook(wb)

Naše šablony budou v adresáři nášbalík/xlsx/. Šablona dbtable.py může vypadat např. takto:

def get_header(system, value):
    return [str(col) for col in system["context"].model.columns]


def iterate_rows(system, value):
    request = system["request"]
    context = system["context"]
    for row in request.db.execute(context.get_query()):
        yield [row[col.name] for col in context.model.columns]

Závěr

Tento nástroj má několik možností vylepšení:

  • Nelze konfigurovat modul, ve kterém se nachází šablony.
  • Jméno šablony v konfiguraci view je něco.xlsx, ale jméno skutečné šablony na disku je něco.py.
  • Adresářová struktura šablon je plochá, což stačí, pokud je šablon málo. Měla by ale kopírovat adresářovou strukturu hlavního šablonovacího systému.

Na ukázku je to ale dostačující.

Fungující aplikaci, která tento renderer využívá najdete na https://github.com/petrblahos/pyrasample. Více o rendererech najdete v dokumentaci k Pyramid.