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