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

1''' 

2Run the server in a development environment: 

3 python -m flask --app aisdb/rest_api.py run 

4 

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 

13 

14import aisdb 

15from aisdb import PostgresDBConn, DBQuery 

16 

17from flask import ( 

18 Flask, 

19 Markup, 

20 Response, 

21 request, 

22) 

23 

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 

27 

28# TODO: auth 

29app = Flask("aisdb-rest-api") 

30app.config.from_mapping(SECRET_KEY=secrets.token_bytes()) 

31 

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) 

39 

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 } 

53 

54 

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}') 

59 

60 example_GET_qry = '<div id="base_uri" style="display: inline;" ></div>?' + '&'.join( 

61 f'{k}={v}' for k, v in default_query.items()) 

62 

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 

67 

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.&ensp;' 

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; 

97 

98 //let status_display = function() { }; 

99 </script> 

100 ''') 

101 

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>') 

105 

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]) 

111 

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") 

115 

116 if http_qry['end'] <= http_qry['start']: 

117 return Markup("Error: end must occur after start") 

118 

119 if not (-180 <= http_qry['xmin'] < http_qry['xmax'] <= 180): 

120 return Markup("Error: invalid longitude range") 

121 

122 if not (-90 <= http_qry['ymin'] < http_qry['ymax'] <= 90): 

123 return Markup("Error: invalid longitude range") 

124 

125 with PostgresDBConn(**db_args) as dbconn: 

126 buf = SpooledTemporaryFile(max_size=MAX_CLIENT_MEMORY) 

127 

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) 

133 

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' 

143 

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() 

150 

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 

174 

175 

176if __name__ == '__main__': 

177 app.run()