Skip to content

Commit 73ec501

Browse files
committed
feat: simple running version
1 parent ba99d6a commit 73ec501

File tree

9 files changed

+276
-168
lines changed

9 files changed

+276
-168
lines changed

mypy.ini

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[mypy]
2+
plugins = sqlmypy

requirements.dev.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
invoke>=1.4.1
2-
black>=20.8b1
2+
black>=20.8b1
3+
sqlalchemy-stubs>=0.3
4+
mypy>=0.790

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Flask==1.1.2
22
SQLAlchemy==1.3.22
3-
PyMySQL==0.10.1
3+
mysqlclient==2.0.2
44
python-dotenv==0.15.0

src/server/_analytics.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from sqlalchemy import Table, Column, String, Integer, DateTime, func
2-
from ._db import metadata
2+
from ._db import metadata, TABLE_OPTIONS
33
from flask import request
44
from ._common import db
55

@@ -13,6 +13,7 @@
1313
Column("source", String(32)),
1414
Column("result", Integer),
1515
Column("num_rows", Integer),
16+
**TABLE_OPTIONS
1617
)
1718

1819

src/server/_db.py

+7
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@
44

55
engine: Engine = create_engine(SQLALCHEMY_DATABASE_URI, **SQLALCHEMY_ENGINE_OPTIONS)
66
metadata = MetaData(bind=engine)
7+
8+
TABLE_OPTIONS = dict(
9+
mysql_engine="InnoDB",
10+
# mariadb_engine="InnoDB",
11+
mysql_charset="utf8mb4",
12+
# mariadb_charset="utf8",
13+
)

src/server/_printer.py

+121-159
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
from flask import request, jsonify, abort, make_response, request
1+
from flask import (
2+
request,
3+
jsonify,
4+
abort,
5+
make_response,
6+
request,
7+
Response,
8+
stream_with_context,
9+
)
210
from werkzeug.exceptions import HTTPException
311
from flask.json import dumps
412
from ._analytics import record_analytics
5-
from typing import Dict
13+
from typing import Dict, Iterable, Any, Union, Optional, List
14+
615

716
MAX_RESULTS = 1000
817

@@ -48,13 +57,12 @@ def __init__(self):
4857
class APrinter:
4958
count: int = 0
5059
result: int = -1
51-
_began = False
52-
_use_status_codes = True
53-
endpoint: str
5460

55-
def __init__(self, endpoint: str, use_status_codes=True):
56-
self.endpoint = endpoint
57-
self._use_status_codes = use_status_codes
61+
def make_response(self, gen):
62+
return Response(
63+
gen,
64+
mimetype="application/json",
65+
)
5866

5967
def print_non_standard(self, data):
6068
"""
@@ -67,163 +75,102 @@ def print_non_standard(self, data):
6775

6876
@property
6977
def remaining_rows(self) -> int:
70-
return MAX_RESULTS - self._count
71-
72-
def __enter__(self):
73-
self._begin()
74-
return self
78+
return MAX_RESULTS - self.count
7579

76-
def _begin(self):
77-
if self._began:
78-
return
79-
self._began = True
80+
def begin(self):
8081
self.result = -2 # no result
81-
self._begin_impl()
82+
return self._begin_impl()
8283

8384
def _begin_impl(self):
8485
# hook
85-
pass
86+
return None
8687

87-
def __call__(self, row: Dict):
88-
if not self._began:
89-
self._begin()
88+
def __call__(self, row: Dict) -> Optional[Union[str, bytes]]:
9089
first = self.count == 0
9190
if self.count >= MAX_RESULTS:
9291
# hit the limit
9392
self.result = 2
94-
return
93+
return None
9594
if first:
9695
self.result = 1 # at least one row
97-
self._print_row(first, row)
9896
self.count += 1
97+
return self._print_row(first, row)
9998

100-
def _print_row_impl(self, first: bool, row: Dict):
101-
pass
99+
def _print_row(self, first: bool, row: Dict) -> Optional[Union[str, bytes]]:
100+
return None
102101

103-
def __exit__(self, exc_type, exc_val, exc_tb):
104-
if not self._began:
105-
return
106-
# record_analytics($this->endpoint, $this->result, $this->count)
107-
self._end_impl()
102+
def end(self) -> Optional[Union[str, bytes]]:
103+
record_analytics(self.result, self.count)
104+
return self._end_impl()
108105

109106
def _end_impl(self):
110107
# hook
111-
pass
108+
return None
109+
110+
111+
class ClassicPrinter(APrinter):
112+
"""
113+
a printer class writing in the classic epidata format
114+
"""
112115

116+
def _begin_impl(self):
117+
return '{ "epidata": ['
113118

114-
# interface IRowPrinter {
115-
# /**
116-
# * returns the number of possible remaining rows to fetch, if < 0 it means no limit
117-
# */
118-
# public function remainingRows(): int;
119-
# /**
120-
# * starts printing rows, can be called multiple times without harm
121-
# */
122-
# public function begin();
123-
# /**
124-
# * print a specific row
125-
# */
126-
# public function printRow(array &$row);
127-
# /**
128-
# * finish writing rows needs to be called once
129-
# */
130-
# public function end();
131-
# }
132-
133-
# class CollectRowPrinter implements IRowPrinter {
134-
# public array $data = [];
135-
136-
# public function remainingRows(): int {
137-
# global $MAX_RESULTS;
138-
# return $MAX_RESULTS - count($this->data);
139-
# }
140-
141-
# public function begin() {
142-
# // dummy
143-
# }
144-
# public function printRow(array &$row) {
145-
# array_push($this->data, $row);
146-
# }
147-
# public function end() {
148-
# // dummy
149-
# }
150-
# }
151-
152-
# /**
153-
# * a printer class writing in the classic epidata format
154-
# */
155-
# class ClassicPrinter extends APrinter {
156-
157-
# function __construct(string $endpoint) {
158-
# parent::__construct($endpoint, FALSE);
159-
# }
160-
161-
# protected function beginImpl() {
162-
# header('Content-Type: application/json');
163-
# echo '{ "epidata": [';
164-
# }
165-
166-
# protected function printRowImpl(bool $first, array &$row) {
167-
# if (!$first) {
168-
# echo ',';
169-
# }
170-
# echo json_encode($row);
171-
# }
172-
173-
# protected function endImpl() {
174-
# $message = $this->count == 0 ? 'no results' : ($this->result == 2 ? 'too many results, data truncated' : 'success');
175-
# $messageEncoded = json_encode($message);
176-
# echo "], \"result\": {$this->result}, \"message\": {$messageEncoded} }";
177-
# }
178-
# }
179-
180-
# /**
181-
# * a printer class writing a tree by the given grouping criteria as the first element in the epidata array
182-
# */
183-
# class ClassicTreePrinter extends ClassicPrinter {
184-
# private array $tree = [];
185-
# private string $group;
186-
187-
# function __construct(string $endpoint, string $group) {
188-
# parent::__construct($endpoint);
189-
# $this->group = $group;
190-
# }
191-
192-
# protected function printRowImpl(bool $first, array &$row) {
193-
# $group = isset($row[$this->group]) ? $row[$this->group] : '';
194-
# unset($row[$this->group]);
195-
# if (isset($this->tree[$group])) {
196-
# array_push($this->tree[$group], $row);
197-
# } else {
198-
# $this->tree[$group] = [$row];
199-
# }
200-
# }
201-
202-
# private function printTree() {
203-
# if (count($this->tree) == 0) {
204-
# echo '{}'; // force object style
205-
# } else {
206-
# echo json_encode($this->tree);
207-
# }
208-
# // clean up
209-
# $this->tree = [];
210-
# }
211-
212-
# protected function endImpl() {
213-
# $this->printTree();
214-
# parent::endImpl();
215-
# }
216-
# }
119+
def _print_row(self, first: bool, row: Dict):
120+
sep = "," if not first else ""
121+
return f"{sep}{dumps(row)}"
122+
123+
def _end_impl(self):
124+
message = "success"
125+
if self.count == 0:
126+
message = "no results"
127+
elif self.result == 2:
128+
message = "too many results, data truncated"
129+
return f'], "result": {self.result}, "message": {dumps(message)} }}'
130+
131+
132+
class ClassicTreePrinter(ClassicPrinter):
133+
"""
134+
a printer class writing a tree by the given grouping criteria as the first element in the epidata array
135+
"""
136+
137+
group: str
138+
_tree: Dict[str, List[Dict]] = dict()
139+
140+
def __init__(self, group: str):
141+
super(ClassicTreePrinter, self).__init__()
142+
self.group = group
143+
self._tree = dict()
144+
145+
def _print_row(self, first: bool, row: Dict):
146+
group = row.get(self.group, "")
147+
del row[self.group]
148+
if group in self._tree:
149+
self._tree[group].append(row)
150+
else:
151+
self._tree[group] = [row]
152+
return None
153+
154+
def _end_impl(self):
155+
tree = dumps(self._tree)
156+
self._tree = dict()
157+
r = super(ClassicTreePrinter, self)._end_impl()
158+
return f"{tree}{r}"
217159

218160

219161
class CSVPrinter(APrinter):
220162
"""
221163
a printer class writing in a CSV file
222164
"""
223165

166+
def make_response(self, gen):
167+
return Response(
168+
gen,
169+
mimetype="text/csv; charset=utf8",
170+
headers={"Content-Disposition": "attachment; filename=epidata.csv"},
171+
)
172+
224173
def _begin_impl(self):
225-
# header('Content-Type: text/csv; charset=utf8');
226-
# header('Content-Disposition: attachment; filename=epidata.csv');
227174
pass
228175

229176
def _print_row_impl(self, first: bool, row: Dict):
@@ -246,43 +193,58 @@ class JSONPrinter(APrinter):
246193
"""
247194

248195
def _begin_impl(self):
249-
# TODO header('Content-Type: application/json')
250-
# echo '['
251-
pass
196+
return "["
252197

253-
def _print_row_impl(self, first: bool, row: Dict):
198+
def _print_row(self, first: bool, row: Dict):
254199
sep = "," if not first else ""
255200
return f"{sep}{dumps(row)}"
256201

257202
def _end_impl(self):
258-
# echo ']'
259-
pass
203+
return "]"
260204

261205

262206
class JSONLPrinter(APrinter):
263207
"""
264208
a printer class writing in JSONLines format
265209
"""
266210

267-
def _begin_impl(self):
268-
# TODO
269-
# there is no official mime type for json lines
270-
# header('Content-Type: text/plain; charset=utf8');
271-
pass
211+
def make_response(self, gen):
212+
return Response(gen, mimetype=" text/plain; charset=utf8")
272213

273-
def _print_row_impl(self, first: bool, row: Dict):
214+
def _print_row(self, first: bool, row: Dict):
274215
# each line is a JSON file with a new line to separate them
275216
return f"{dumps(row)}\n"
276217

277218

278-
def create_printer(endpoint: str):
219+
def create_printer():
279220
format = request.values.get("format", "classic")
280-
# if format == "tree":
281-
# return ClassicTreePrinter(endpoint, "signal")
282-
# if format == "json":
283-
return JSONPrinter(endpoint)
284-
# if format == "csv":
285-
# return CSVPrinter(endpoint)
286-
# if format == "jsonl":
287-
# return JSONLPrinter(endpoint)
288-
# return ClassicPrinter(endpoint)
221+
if format == "tree":
222+
return ClassicTreePrinter("signal")
223+
if format == "json":
224+
return JSONPrinter()
225+
if format == "csv":
226+
return CSVPrinter()
227+
if format == "jsonl":
228+
return JSONLPrinter()
229+
return ClassicPrinter()
230+
231+
232+
def send_rows(generator: Iterable[Dict[str, Any]]):
233+
printer = create_printer()
234+
235+
def gen():
236+
r = printer.begin()
237+
if r is not None:
238+
yield r
239+
for row in generator:
240+
r = printer(row)
241+
if r is not None:
242+
yield r
243+
r = printer.end()
244+
if r is not None:
245+
yield r
246+
247+
return printer.make_response(stream_with_context(gen()))
248+
249+
250+
# , execution_options={"stream_results": True}

0 commit comments

Comments
 (0)