Coverage for src/pyrsslocal/custom_server/aserver.py: 55%

146 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2023-02-02 02:59 +0100

1""" 

2@file 

3@brief This modules contains a class which implements a simple server. 

4""" 

5 

6import sys 

7import os 

8import urllib 

9import datetime 

10from http.server import HTTPServer 

11from socketserver import ThreadingMixIn 

12 

13from pyquickhelper.loghelper import fLOG 

14from pyensae.sql.database_main import Database 

15from ..simple_server.simple_server_custom import SimpleHandler, ThreadServer 

16 

17 

18class CustomDBServerHandler(SimpleHandler): 

19 

20 """ 

21 The server proposes a simple way to create one server on your own. 

22 It includes an access to a SQLlite3 database. 

23 """ 

24 

25 def __init__(self, request, client_address, server): 

26 """ 

27 Regular constructor, an instance is created for each request, 

28 do not store any data for a longer time than a request. 

29 """ 

30 SimpleHandler.__init__(self, request, client_address, server) 

31 #self.m_database = server._my_database 

32 #self.m_main_page = server._my_main_page 

33 #self.m_root = server._my_root 

34 

35 def main_page(self): 

36 """ 

37 returns the main page (case the server is called with no path) 

38 @return default page 

39 """ 

40 return self.server._my_main_page 

41 

42 def get_javascript_paths(self): 

43 """ 

44 returns all the location where the server should look for a java script 

45 @return list of paths 

46 """ 

47 return [self.server._my_root, SimpleHandler.javascript_path] 

48 

49 def interpret_parameter_as_list_int(self, ps): 

50 """ 

51 interpret a list of parameters, each of them is a list of integer 

52 separated by , 

53 

54 @param ps something like ``params.get("blog_selected")`` 

55 @return list of int 

56 """ 

57 res = [] 

58 for ins in ps: 

59 spl = ins.split(",") 

60 ii = [int(_) for _ in spl] 

61 res.extend(ii) 

62 return res 

63 

64 def process_event(self, st): 

65 """ 

66 process an event, and log it 

67 

68 @param st string to process 

69 """ 

70 self.server.process_event(st) 

71 

72 def serve_content_web(self, path, method, params): 

73 """ 

74 functions to overload (executed after serve_content) 

75 

76 @param path ParseResult 

77 @param method GET or POST 

78 @param params params parsed from the url + others 

79 """ 

80 if path.path.startswith("/logs/"): 

81 url = path.path[6:] 

82 targ = urllib.parse.unquote(url) 

83 self.process_event(targ) 

84 self.send_response(200) 

85 self.send_headers("") 

86 

87 else: 

88 url = path.path 

89 

90 htype, ftype = self.get_ftype(url) 

91 for p in self.server._my_root: 

92 local = os.path.join(p, url.lstrip("/")) 

93 if os.path.exists(local): 

94 break 

95 

96 if htype == "text/html": 

97 if os.path.exists(local): 

98 content = self.get_file_content(local, ftype) 

99 self.send_response(200) 

100 self.send_headers(path.path) 

101 

102 # context 

103 params["db"] = self.server._my_database 

104 params["page"] = url 

105 params[ 

106 "website"] = "http://%s:%d/" % self.server.server_address 

107 self.feed(content, True, params) 

108 else: 

109 self.send_response(200) 

110 self.send_headers("") 

111 self.feed( 

112 "unable to find (CustomServerHanlder): " + 

113 path.geturl() + 

114 "\nlocal file:" + 

115 local + 

116 "\n") 

117 self.send_error(404) 

118 

119 elif os.path.exists(local): 

120 content = self.get_file_content(local, ftype) 

121 self.send_response(200) 

122 self.send_headers(url) 

123 self.feed(content, False, params) 

124 

125 else: 

126 self.send_response(200) 

127 self.send_headers("") 

128 self.feed( 

129 "unable to find (CustomServerHanlder): " + 

130 path.geturl() + 

131 "\nlocal file:" + 

132 local + 

133 "\n") 

134 self.send_error(404) 

135 

136 

137class CustomDBServer (ThreadingMixIn, HTTPServer): 

138 

139 """ 

140 defines a custom server which includes an access to a database, 

141 this database will contain de table to store the clicks 

142 

143 .. exref:: 

144 :title: create a custom local server 

145 

146 The following code creates an instance of a local server. 

147 The server expects to find its content in the same folder. 

148 

149 :: 

150 

151 from pyensae import Database 

152 

153 db = Database(dbfile) 

154 df = pandas.DataFrame ( [ {"name":"xavier", "module":"pyrsslocal"} ] ) 

155 db.connect() 

156 db.import_dataframe(df, "example") 

157 db.close() 

158 

159 url = "http://localhost:%d/p_aserver.html" % port 

160 webbrowser.open(url) 

161 CustomDBServer.run_server(None, dbfile, port = port, extra_path = os.path.join(".")) 

162 

163 The main page is the following one and it can contains a Python script 

164 which will be interpreter by the server. 

165 It gives access to a variable ``db`` which is a local database 

166 in SQLlite. 

167 

168 :: 

169 

170 <?xml version="1.0" encoding="utf-8"?> 

171 <html> 

172 <head> 

173 <link type="text/css" href="/p_aserver.css" rel="stylesheet"/> 

174 <title>Custom DB Server</title> 

175 <meta content="dupre, pyrsslocal, custom server" name="keywords"/> 

176 <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/> 

177 <link rel="shortcut icon" href="p_aserver.ico" /> 

178 <meta content="CustomServer from pyrsslocal" name="description" /> 

179 <script type="text/javascript" src="/p_aserver.js"></script> 

180 <script src="/js/run_prettify.js" type="text/javascript"></script> 

181 

182 </head> 

183 

184 <body onload="setPositions(['divtable', ])" class="mymainbody"> 

185 

186 <div class="divtop"> 

187 <h1>Custom DB Server unittest</h1> 

188 </div> 

189 

190 <div class="divtable" id="divfiles" onscroll="savePosition('divtable')"> 

191 

192 <h2>Content of table example</h2> 

193 

194 <script type="text/python"> 

195 print("<table>") 

196 db.connect() 

197 for row in db.execute_view("SELECT * FROM example") : 

198 srow = [ str(_) for _ in row ] 

199 print( "<tr><td>{0}</td></tr>".format("</td><td>".join(srow) ) ) 

200 db.close() 

201 print("</table>") 

202 </script> 

203 

204 <p>end.</p> 

205 

206 </div> 

207 </body> 

208 </html> 

209 """ 

210 

211 @staticmethod 

212 def schema_table(table): 

213 """ 

214 returns the schema for a specific table 

215 

216 @param table name (in ["stats", "event"]) 

217 @return dictionary 

218 """ 

219 if table == "stats": 

220 return {0: ("id_post", int), 

221 1: ("dtime", datetime.datetime), 

222 2: ("status", str), 

223 3: ("rate", int), 

224 4: ("comment", str), 

225 } 

226 if table == "event": 

227 return {-1: ("id_event", int, "PRIMARYKEY", "AUTOINCREMENT"), 

228 0: ("dtime", datetime.datetime), 

229 1: ("uuid", str), 

230 2: ("type1", str), 

231 3: ("type2", str), 

232 4: ("args", str), 

233 } 

234 raise Exception("unexpected table name") # pragma: no cover 

235 

236 def __init__(self, 

237 server_address, 

238 dbfile, 

239 RequestHandlerClass=CustomDBServerHandler, 

240 main_page="index.html", 

241 root=None, 

242 logfile=None 

243 ): 

244 """ 

245 constructor 

246 

247 @param server_address addess of the server 

248 @param RequestHandlerClass it should be @see cl CustomServerHandler 

249 @param dbfile database filename (SQLlite format) 

250 @param main_page main page for the service (when requested with no specific file) 

251 @param root folder or list of folders where the server will look into for files such as the main page 

252 """ 

253 HTTPServer.__init__(self, server_address, RequestHandlerClass) 

254 self._my_database = Database(dbfile, LOG=fLOG) 

255 self._my_database_ev = Database(dbfile, LOG=fLOG) 

256 

257 this = os.path.abspath(os.path.split(__file__)[0]) 

258 if root is None: 

259 root = [this] 

260 elif isinstance(root, str): 

261 root = [root, this] 

262 elif isinstance(root, list): 

263 root = root + [this] 

264 else: 

265 raise TypeError( # pragma: no cover 

266 "Unable to interpret root '%s'." % str(root)) 

267 

268 self._my_root = root 

269 self._my_main_page = main_page 

270 self._my_address = server_address 

271 fLOG("CustomServer.init: root=", root) 

272 fLOG("CustomServer.init: db=", dbfile) 

273 

274 self.table_event = "cs_events" 

275 self.table_stats = "cs_stats" 

276 

277 self.logfile = logfile 

278 if self.logfile is not None: 

279 if self.logfile == "stdout": 

280 self.flog = sys.stdout 

281 elif isinstance(self.logfile, str): 

282 self.flog = open(self.logfile, "a", encoding="utf8") 

283 else: 

284 self.flog = self.logfile 

285 else: 

286 self.flog = None 

287 

288 self._my_database_ev.connect() 

289 if not self._my_database_ev.has_table(self.table_stats): 

290 schema = CustomDBServer.schema_table("stats") 

291 self._my_database_ev.create_table(self.table_stats, schema) 

292 self._my_database_ev.commit() 

293 self._my_database_ev.create_index( 

294 "id_post_" + 

295 self.table_stats, 

296 self.table_stats, 

297 "id_post", 

298 False) 

299 self._my_database_ev.commit() 

300 

301 if not self._my_database_ev.has_table(self.table_event): 

302 schema = CustomDBServer.schema_table("event") 

303 self._my_database_ev.create_table(self.table_event, schema) 

304 self._my_database_ev.commit() 

305 self._my_database_ev.close() 

306 

307 def __enter__(self): 

308 """ 

309 What to do when creating the class. 

310 """ 

311 return self 

312 

313 def __exit__(self, exc_type, exc_value, traceback): # pylint: disable=W0221 

314 """ 

315 What to do when removing the instance (close the log file). 

316 """ 

317 if self.flog is not None and self.logfile != "stdout": 

318 self.flog.close() 

319 

320 def process_event(self, event): 

321 """ 

322 Processes an event, it expects a format like the following: 

323 

324 :: 

325 

326 type1/uuid/type2/args 

327 

328 @param event string to log 

329 """ 

330 now = datetime.datetime.now() 

331 if self.flog is not None: 

332 self.flog.write(str(now) + " " + event) 

333 self.flog.write("\n") 

334 self.flog.flush() 

335 

336 info = event.split("/") 

337 

338 status = None 

339 if len(info) >= 4 and info[2] == "status": 

340 status = {"status": info[4], 

341 "id_post": int(info[3]), 

342 "dtime": now, 

343 "rate": -1, 

344 "comment": ""} 

345 

346 if len(info) > 4: 

347 info[3:] = ["/".join(info[3:])] 

348 if len(info) < 4: 

349 raise OSError("unable to log event: " + event) 

350 

351 values = {"type1": info[0], 

352 "uuid": info[1], 

353 "type2": info[2], 

354 "dtime": now, 

355 "args": info[3]} 

356 

357 # to avoid database to collide 

358 iscon = self._my_database_ev.is_connected() 

359 if iscon: 

360 if self.flog is not None: 

361 self.flog.write("unable to connect the database") 

362 if status is not None: 

363 self.flog.write("unable to update status: " + str(status)) 

364 return 

365 

366 self._my_database_ev.connect() 

367 self._my_database_ev.insert(self.table_event, values) 

368 if status is not None: 

369 self._my_database_ev.insert(self.table_stats, status) 

370 self._my_database_ev.commit() 

371 self._my_database_ev.close() 

372 

373 @staticmethod 

374 def run_server(server, dbfile, thread=False, port=8080, logfile=None, 

375 extra_path=None): 

376 """ 

377 start the server 

378 

379 @param server if None, it becomes ``CustomServer(dbfile, ('localhost', 8080), CustomServerHandler)`` 

380 @param dbfile file to the RSS database (SQLite) 

381 @param thread if True, the server is run in a thread 

382 and the function returns right away, 

383 otherwite, it runs the server. 

384 @param port port to use 

385 @param logfile file for the log or "stdout" for the standard output 

386 @param extra_path additional path the server should look into to find a page 

387 @return server if thread is False, the thread otherwise (the thread is started) 

388 

389 @warning If you kill the python program while the thread is still running, python interpreter might be closed completely. 

390 

391 """ 

392 if server is None: 

393 server = CustomDBServer( 

394 ('localhost', 

395 port), 

396 dbfile, 

397 CustomDBServerHandler, 

398 logfile=logfile, 

399 root=extra_path) 

400 if thread: 

401 th = ThreadServer(server) 

402 th.start() 

403 return th 

404 else: # pragma: no cover 

405 server.serve_forever() 

406 return server