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
+ )
2
10
from werkzeug .exceptions import HTTPException
3
11
from flask .json import dumps
4
12
from ._analytics import record_analytics
5
- from typing import Dict
13
+ from typing import Dict , Iterable , Any , Union , Optional , List
14
+
6
15
7
16
MAX_RESULTS = 1000
8
17
@@ -48,13 +57,12 @@ def __init__(self):
48
57
class APrinter :
49
58
count : int = 0
50
59
result : int = - 1
51
- _began = False
52
- _use_status_codes = True
53
- endpoint : str
54
60
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
+ )
58
66
59
67
def print_non_standard (self , data ):
60
68
"""
@@ -67,163 +75,102 @@ def print_non_standard(self, data):
67
75
68
76
@property
69
77
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
75
79
76
- def _begin (self ):
77
- if self ._began :
78
- return
79
- self ._began = True
80
+ def begin (self ):
80
81
self .result = - 2 # no result
81
- self ._begin_impl ()
82
+ return self ._begin_impl ()
82
83
83
84
def _begin_impl (self ):
84
85
# hook
85
- pass
86
+ return None
86
87
87
- def __call__ (self , row : Dict ):
88
- if not self ._began :
89
- self ._begin ()
88
+ def __call__ (self , row : Dict ) -> Optional [Union [str , bytes ]]:
90
89
first = self .count == 0
91
90
if self .count >= MAX_RESULTS :
92
91
# hit the limit
93
92
self .result = 2
94
- return
93
+ return None
95
94
if first :
96
95
self .result = 1 # at least one row
97
- self ._print_row (first , row )
98
96
self .count += 1
97
+ return self ._print_row (first , row )
99
98
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
102
101
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 ()
108
105
109
106
def _end_impl (self ):
110
107
# hook
111
- pass
108
+ return None
109
+
110
+
111
+ class ClassicPrinter (APrinter ):
112
+ """
113
+ a printer class writing in the classic epidata format
114
+ """
112
115
116
+ def _begin_impl (self ):
117
+ return '{ "epidata": ['
113
118
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 } "
217
159
218
160
219
161
class CSVPrinter (APrinter ):
220
162
"""
221
163
a printer class writing in a CSV file
222
164
"""
223
165
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
+
224
173
def _begin_impl (self ):
225
- # header('Content-Type: text/csv; charset=utf8');
226
- # header('Content-Disposition: attachment; filename=epidata.csv');
227
174
pass
228
175
229
176
def _print_row_impl (self , first : bool , row : Dict ):
@@ -246,43 +193,58 @@ class JSONPrinter(APrinter):
246
193
"""
247
194
248
195
def _begin_impl (self ):
249
- # TODO header('Content-Type: application/json')
250
- # echo '['
251
- pass
196
+ return "["
252
197
253
- def _print_row_impl (self , first : bool , row : Dict ):
198
+ def _print_row (self , first : bool , row : Dict ):
254
199
sep = "," if not first else ""
255
200
return f"{ sep } { dumps (row )} "
256
201
257
202
def _end_impl (self ):
258
- # echo ']'
259
- pass
203
+ return "]"
260
204
261
205
262
206
class JSONLPrinter (APrinter ):
263
207
"""
264
208
a printer class writing in JSONLines format
265
209
"""
266
210
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" )
272
213
273
- def _print_row_impl (self , first : bool , row : Dict ):
214
+ def _print_row (self , first : bool , row : Dict ):
274
215
# each line is a JSON file with a new line to separate them
275
216
return f"{ dumps (row )} \n "
276
217
277
218
278
- def create_printer (endpoint : str ):
219
+ def create_printer ():
279
220
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