Coverage for aisdb/rest_api.py: 0%
54 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 04:02 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 04:02 +0000
1'''
2Run the server in a development environment:
3 python -m flask --app aisdb/rest_api.py run
5Deploying flask to production with IIS:
6 <https://learn.microsoft.com/en-us/visualstudio/python/configure-web-apps-for-iis-windows>
7'''
8from datetime import datetime, timedelta
9from tempfile import SpooledTemporaryFile
10import gzip
11import os
12import secrets
14import aisdb
15from aisdb import PostgresDBConn, DBQuery
17from flask import (
18 Flask,
19 Markup,
20 Response,
21 request,
22)
24# Maximum bytes for client CSV files stored in memory before spilling to disk.
25# Set the directory for files exceeding this value with $TMPDIR
26MAX_CLIENT_MEMORY = 1024 * 1e6 # 1GB
28# TODO: auth
29app = Flask("aisdb-rest-api")
30app.config.from_mapping(SECRET_KEY=secrets.token_bytes())
32# postgres database connection arguments
33db_args = dict(
34 host=os.environ.get('AISDB_REST_DBHOST', 'fc00::17'),
35 port=os.environ.get('AISDB_REST_DBPORT', 5431),
36 user=os.environ.get('AISDB_REST_DBUSER', 'postgres'),
37 password=os.environ.get('AISDB_REST_DBPASSWORD', 'devel'),
38)
40# verify database connection and prepare an example GET request
41with PostgresDBConn(**db_args) as dbconn:
42 db_rng = dbconn.db_daterange
43 end = db_rng['end']
44 start = max(db_rng['start'], db_rng['end'] - timedelta(days=31))
45 default_query = {
46 'start': int(datetime(start.year, start.month, start.day).timestamp()),
47 'end': int(datetime(end.year, end.month, end.day).timestamp()),
48 'xmin': -65,
49 'xmax': -62,
50 'ymin': 43,
51 'ymax': 45,
52 }
55@app.route('/', methods=['GET', 'POST'])
56def download():
57 http_qry = dict(request.args)
58 print(f'received request {http_qry} from client {request.remote_addr}')
60 example_GET_qry = '<div id="base_uri" style="display: inline;" ></div>?' + '&'.join(
61 f'{k}={v}' for k, v in default_query.items())
63 # validate the request parameters
64 need_keys = set(default_query.keys())
65 recv_keys = set(http_qry.keys())
66 missing = need_keys - recv_keys
68 if len(recv_keys) == 0:
69 return Markup(
70 '<h3>AIS REST API</h3>'
71 '<p>'
72 'Query AIS message history using time and coordinate region to download a CSV data export. '
73 'Begin request using a GET or POST request to this endpoint.'
74 '</p>'
75 '<p>Description of query parameters:</p>'
76 '<ul>'
77 '<li>xmin: minimum longitude (decimal degrees)</li>'
78 '<li>xmax: maximum longitude (decimal degrees)</li>'
79 '<li>ymin: minimum latitude (decimal degrees)</li>'
80 '<li>ymax: maximum latitude (decimal degrees)</li>'
81 '<li>start: beginning timestamp (epoch seconds)</li>'
82 '<li>end: end timestamp (epoch seconds)</li>'
83 '</ul>'
84 '<p>'
85 'Requests are limited to 31 days at a time. Data is available from'
86 f' <code>{db_rng["start"]}</code>'
87 ' to'
88 f' <code>{db_rng["end"]}</code>.'
89 '</p>'
90 '<p>Example GET request:</p>'
91 f'<code>{example_GET_qry}<code>'
92 #'<form action="/" method="POST">'
93 #'</form>'
94 '''
95 <script>
96 document.getElementById("base_uri").innerHTML = window.location;
98 //let status_display = function() { };
99 </script>
100 ''')
102 if len(missing) > 0:
103 return Markup(f'Error: missing keys from request: {missing}<br>'
104 f'example:<br><code>{example_GET_qry}<code>')
106 # convert parameter types from string
107 http_qry['start'] = datetime.utcfromtimestamp(int(http_qry['start']))
108 http_qry['end'] = datetime.utcfromtimestamp(int(http_qry['end']))
109 for arg in ['xmin', 'xmax', 'ymin', 'ymax']:
110 http_qry[arg] = float(http_qry[arg])
112 # error handling for invalid requests
113 if http_qry['end'] - http_qry['start'] > timedelta(days=31):
114 return Markup("Error: a maximum of 31 days can be queried at once")
116 if http_qry['end'] <= http_qry['start']:
117 return Markup("Error: end must occur after start")
119 if not (-180 <= http_qry['xmin'] < http_qry['xmax'] <= 180):
120 return Markup("Error: invalid longitude range")
122 if not (-90 <= http_qry['ymin'] < http_qry['ymax'] <= 90):
123 return Markup("Error: invalid longitude range")
125 with PostgresDBConn(**db_args) as dbconn:
126 buf = SpooledTemporaryFile(max_size=MAX_CLIENT_MEMORY)
128 dbqry = DBQuery(dbconn=dbconn,
129 callback=aisdb.sqlfcn_callbacks.in_bbox_time_validmmsi,
130 **http_qry).gen_qry(
131 fcn=aisdb.sqlfcn.crawl_dynamic_static,
132 verbose=False)
134 tracks = aisdb.TrackGen(dbqry, decimate=0.0001)
135 #csv_rows = aisdb.proc_util.tracks_csv(tracks)
136 '''
137 def generate(csv_rows):
138 start_qry = next(csv_rows)
139 yield ','.join(map(str, start_qry)) + '\n'
140 yield ','.join(map(str, start_qry)) + '\n'
141 for row in csv_rows:
142 yield ','.join(map(str, row)) + '\n'
144 lines = generate(csv_rows)
145 # start query generation so that the DBConn object isnt garbage collected
146 _ = next(lines)
147 '''
148 lines = aisdb.proc_util.write_csv(tracks, buf)
149 buf.flush()
151 download_name = f'ais_{http_qry["start"].date()}_{http_qry["end"].date()}.csv'
152 buf.seek(0)
153 count = sum(1 for line in buf)
154 print(f'sending {count} rows to client {request.remote_addr}',
155 flush=True)
156 buf.seek(0)
157 return Response(
158 gzip.compress(buf.read(), compresslevel=9),
159 #gzip.compress(lines, compresslevel=7),
160 mimetype='application/csv',
161 headers={
162 'Content-Disposition': f'attachment;filename={download_name}',
163 'Content-Encoding': 'gzip',
164 'Keep-Alive': 'timeout=0'
165 },
166 )
167 try:
168 pass
169 except aisdb.track_gen.EmptyRowsException:
170 buf.close()
171 return Markup("No results found for query")
172 except Exception as err:
173 raise err
176if __name__ == '__main__':
177 app.run()