Coverage for src/pyrsslocal/rss/rss_simple_server.py: 53%

135 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 

12from .rss_database import DatabaseRSS 

13from ..simple_server.simple_server_custom import SimpleHandler, ThreadServer 

14 

15 

16class RSSSimpleHandler(SimpleHandler): 

17 

18 """ 

19 You can read the blog post `RSS Reader 

20 <http://www.xavierdupre.fr/blog/2013-07-28_nojs.html>`_. 

21 Defines a simple handler used by HTTPServer. 

22 Firefox works better for local files. 

23 This server serves RSS content. 

24 

25 For every section containing a python script, the class will add a local 

26 variable ``dbrss`` which gives access to the blogs database. 

27 That's why the following section works: 

28 

29 :: 

30 

31 <script type="text/python"> 

32 for blogs in dbrss.enumerate_blogs() : 

33 print (blogs.html()) 

34 </script> 

35 

36 Whenever a url is preceded by ``/logs/click/```, 

37 the class will log an event in the logs. 

38 The final page will look like this: 

39 

40 @image images/page1.png 

41 

42 .. faqref: 

43 :title: The server is slow or does not respond to a click) 

44 

45 For some reason, it usually happens on Firefox. 

46 Chrome works better. The script python embedded in the HTML page 

47 and interpreted by the server make it slow to run. 

48 It should be replaced by javascript. Maybe Firefox detects the answer is not 

49 fast enough and it requires one or two more clicks to get the server to respond. 

50 """ 

51 

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

53 """ 

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

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

56 """ 

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

58 #self.m_database = server._my_database 

59 #self.m_main_page = server._my_main_page 

60 #self.m_root = server._my_root 

61 

62 def main_page(self): 

63 """ 

64 Returns the main page (case the server is called with no path). 

65 @return default page 

66 """ 

67 return self.server._my_main_page 

68 

69 def get_javascript_paths(self): 

70 """ 

71 Returns all the location where the server should look for a java script. 

72 @return list of paths 

73 """ 

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

75 

76 def interpret_parameter_as_list_int(self, ps): 

77 """ 

78 Interprets a list of parameters, each of them is a list of integer 

79 separated by ``,``. 

80 

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

82 @return list of int 

83 """ 

84 res = [] 

85 for ins in ps: 

86 spl = ins.split(",") 

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

88 res.extend(ii) 

89 return res 

90 

91 def process_event(self, st): 

92 """ 

93 Processes an event, and log it. 

94 

95 @param st string to process 

96 """ 

97 self.server.process_event(st) 

98 

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

100 """ 

101 Functions to overload (executed after serve_content). 

102 

103 @param path ParseResult 

104 @param method GET or POST 

105 @param params params parsed from the url + others 

106 """ 

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

108 url = path.path[6:] 

109 targ = urllib.parse.unquote(url) 

110 self.process_event(targ) 

111 self.send_response(200) 

112 self.send_headers("") 

113 else: 

114 if path.path.startswith("/rssfetchlocalexe/"): 

115 url = path.path.replace("/rssfetchlocalexe/", "") 

116 else: 

117 url = path.path 

118 

119 htype, ftype = self.get_ftype(url) 

120 local = os.path.join(self.server._my_root, url.lstrip("/")) 

121 if htype == "text/html": 

122 if os.path.exists(local): 

123 content = self.get_file_content(local, ftype) 

124 self.send_response(200) 

125 self.send_headers(path.path) 

126 

127 # context 

128 params["dbrss"] = self.server._my_database 

129 params["main_page"] = url 

130 params["blog_selected"] = self.interpret_parameter_as_list_int( 

131 params.get( 

132 "blog_selected", 

133 [])) 

134 params["post_selected"] = self.interpret_parameter_as_list_int( 

135 params.get( 

136 "post_selected", 

137 [])) 

138 params["search"] = params.get("search", [None])[0] 

139 params[ 

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

141 self.feed(content, True, params) 

142 else: 

143 self.send_response(200) 

144 self.send_headers("") 

145 self.feed( 

146 "unable to find (RSSSimpleHandler): " + 

147 path.geturl() + 

148 "\nlocal file:" + 

149 local + 

150 "\n") 

151 self.send_error(404) 

152 

153 elif os.path.exists(local): 

154 content = self.get_file_content(local, ftype) 

155 self.send_response(200) 

156 self.send_headers(url) 

157 self.feed(content, False, params) 

158 

159 else: 

160 self.send_response(200) 

161 self.send_headers("") 

162 self.feed( 

163 "unable to find (RSSSimpleHandler): " + 

164 path.geturl() + 

165 "\nlocal file:" + 

166 local + 

167 "\n") 

168 self.send_error(404) 

169 

170 

171class RSSServer(ThreadingMixIn, HTTPServer): 

172 """ 

173 Defines a :epkg:`RSS` server dedicated to one specific database. 

174 You can read the blog post `RSS Reader 

175 <http://www.xavierdupre.fr/blog/2013-07-28_nojs.html>`_. 

176 """ 

177 

178 def __init__(self, server_address, dbfile, 

179 RequestHandlerClass=RSSSimpleHandler, 

180 main_page="rss_reader.html", 

181 root=os.path.abspath(os.path.split(__file__)[0]), 

182 logfile=None, fLOG=None): 

183 """ 

184 @param server_address address of the server 

185 @param RequestHandlerClass it should be @see cl RSSSimpleHandler 

186 @param dbfile database filename (SQLlite format) 

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

188 @param root folder when the server will look into for files such as the main page 

189 @param fLOG logging function 

190 """ 

191 if fLOG: 

192 fLOG("RSSServer.init: begin server") 

193 HTTPServer.__init__(self, server_address, RequestHandlerClass) 

194 if fLOG: 

195 fLOG("RSSServer.init: begin db rss") 

196 self._my_database = DatabaseRSS(dbfile, LOG=fLOG) 

197 if fLOG: 

198 fLOG("RSSServer.init: begin db event") 

199 self._my_database_ev = DatabaseRSS(dbfile, LOG=fLOG) 

200 if fLOG: 

201 fLOG("RSSServer.init: end db") 

202 self._my_root = root 

203 self._my_main_page = main_page 

204 self._my_address = server_address 

205 if SimpleHandler == RequestHandlerClass: 

206 raise TypeError("this request handler should not be SimpleHandler") 

207 if fLOG: 

208 fLOG("RSSServer.init: root=", root) 

209 fLOG("RSSServer.init: db=", dbfile) 

210 

211 self.logfile = logfile 

212 if self.logfile is not None: 

213 if self.logfile == "stdout": 

214 self.flog = sys.stdout 

215 elif isinstance(self.logfile, str): 

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

217 else: 

218 self.flog = self.logfile 

219 else: 

220 self.flog = None 

221 

222 def __enter__(self): 

223 """ 

224 What to do when creating the class. 

225 """ 

226 return self 

227 

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

229 """ 

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

231 """ 

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

233 self.flog.close() 

234 

235 def process_event(self, event): 

236 """ 

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

238 

239 :: 

240 

241 type1/uuid/type2/args 

242 

243 @param event string to log 

244 """ 

245 now = datetime.datetime.now() 

246 if self.flog is not None: 

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

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

249 self.flog.flush() 

250 

251 info = event.split("/") 

252 

253 status = None 

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

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

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

257 "dtime": now, 

258 "rate": -1, 

259 "comment": ""} 

260 

261 if len(info) > 4: 

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

263 if len(info) < 4: 

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

265 

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

267 "uuid": info[1], 

268 "type2": info[2], 

269 "dtime": now, 

270 "args": info[3]} 

271 

272 # to avoid database to collide 

273 iscon = self._my_database_ev.is_connected() 

274 if iscon: 

275 if self.flog is not None: 

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

277 if status is not None: 

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

279 return 

280 

281 self._my_database_ev.connect() 

282 self._my_database_ev.insert(self._my_database.table_event, values) 

283 if status is not None: 

284 self._my_database_ev.insert(self._my_database.table_stats, status) 

285 self._my_database_ev.commit() 

286 self._my_database_ev.close() 

287 

288 @staticmethod 

289 def run_server(server, dbfile, thread=False, port=8080, 

290 logfile=None, fLOG=None): 

291 """ 

292 Starts the server. 

293 

294 @param server None or string, see below 

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

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

297 and the function returns right away, 

298 otherwite, it runs the server. 

299 @param port port to use 

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

301 @param fLOG logging function 

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

303 

304 About the parameter ``server``: 

305 

306 * ``None``, it becomes ``RSSServer(('localhost', 8080), dbfile, RSSSimpleHandler)`` 

307 * ``<server>``, it becomes ``RSSServer((server, 8080), dbfile, RSSSimpleHandler)`` 

308 

309 @warning If you kill the python program while the thread is still running, 

310 python interpreter might be closed completely. 

311 """ 

312 if server is None: 

313 server = RSSServer( 

314 ('localhost', 

315 port), 

316 dbfile, 

317 RSSSimpleHandler, 

318 logfile=logfile, 

319 fLOG=fLOG) 

320 elif isinstance(server, str): 

321 server = RSSServer( 

322 (server, 

323 port), 

324 dbfile, 

325 RSSSimpleHandler, 

326 logfile=logfile, 

327 fLOG=fLOG) 

328 if thread: 

329 th = ThreadServer(server) 

330 th.start() 

331 return th 

332 else: 

333 server.serve_forever() 

334 return server