From 37f39264fa1521fdda0f10806605b1946c28208c Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Sat, 2 Jan 2021 21:23:50 +0100 Subject: [PATCH] =?UTF-8?q?web.table:=20Podpora=20pro=20streamov=C3=A1n?= =?UTF-8?q?=C3=AD=20exportu=20tabulek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Table.rows může být cokoliv iterovatelného, z čeho padají řádky v podobě slovníků, tedy i generátor. Funkce send_as jde zavolat se streaming=True a pak odpověď streamuje. To nicméně není default, protože je hezké, aby krátké odpovědi měly Content-Length. Také jsem vylepšil definici sloupečků, aby pouze key byl povinný a vše ostatní defaultovalo. --- mo/web/table.py | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/mo/web/table.py b/mo/web/table.py index a6d08b8f..c1e6a8f8 100644 --- a/mo/web/table.py +++ b/mo/web/table.py @@ -4,7 +4,7 @@ from flask import Response, url_for from html import escape import io from markupsafe import Markup -from typing import Sequence, Optional +from typing import Sequence, Optional, Iterable import werkzeug.exceptions import mo.csv @@ -16,7 +16,12 @@ from mo.web import app class Column: key: str # Jméno klíče ve slovnících name: str # Název v hlavičce CSV - title: Optional[str] # Titulek v HTML tabulkách + title: str # Titulek v HTML tabulkách + + def __init__(self, key: str, name: Optional[str] = None, title: Optional[str] = None): + self.key = key + self.name = name or key + self.title = title or self.name class Cell: @@ -46,10 +51,10 @@ class CellLink(Cell): class Table: columns: Sequence[Column] - rows: Sequence[dict] + rows: Iterable[dict] filename: str - def __init__(self, columns: Sequence[Column], rows: Sequence[dict], filename: str): + def __init__(self, columns: Sequence[Column], rows: Iterable[dict], filename: str): self.columns = columns self.rows = rows self.filename = filename @@ -58,7 +63,7 @@ class Table: tab = ['<table class=data>', '<tr>'] for c in self.columns: - tab.append(f'\t<th>{c.title or c.name}') + tab.append(f'\t<th>{c.title}') for r in self.rows: tab.append('<tr>') @@ -87,19 +92,45 @@ class Table: return out.getvalue() - def send_as(self, format: str) -> Response: + def to_csv_stream(self, dialect: str) -> Iterable[str]: + out = io.StringIO() + writer = csv.writer(out, dialect=dialect) + + header = [c.name for c in self.columns] + writer.writerow(header) + + nrows = 0 + for row in self.rows: + r = [row.get(c.key) for c in self.columns] + writer.writerow(r) + + nrows += 1 + if nrows >= 100: + yield out.getvalue() + out.seek(0) + out.truncate() + nrows = 0 + + yield out.getvalue() + + def send_as(self, format: str, streaming: bool = False) -> Response: if format == 'csv': - out = self.to_csv(dialect='excel') + dialect = 'excel' ctype = 'text/csv; charset=utf=8' filename = self.filename + '.csv' elif format == 'tsv': - out = self.to_csv(dialect='tsv') + dialect = 'tsv' ctype = 'text/tab-separated-values; charset=utf=8' filename = self.filename + '.tsv' else: raise werkzeug.exceptions.NotFound() - resp = app.make_response(out) + if streaming: + resp = Response(self.to_csv_stream(dialect=dialect)) + else: + out = self.to_csv(dialect=dialect) + resp = app.make_response(out) + resp.content_type = ctype resp.headers.add('Content-Disposition', f'attachment; filename={filename}') return resp -- GitLab