Coverage for src/pyrsslocal/rss/rss_simple_server.py: 53%
135 statements
« prev ^ index » next coverage.py v7.1.0, created at 2024-04-16 08:45 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2024-04-16 08:45 +0200
1"""
2@file
3@brief This modules contains a class which implements a simple server.
4"""
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
16class RSSSimpleHandler(SimpleHandler):
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.
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:
29 ::
31 <script type="text/python">
32 for blogs in dbrss.enumerate_blogs() :
33 print (blogs.html())
34 </script>
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:
40 @image images/page1.png
42 .. faqref:
43 :title: The server is slow or does not respond to a click)
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 """
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
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
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]
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 ``,``.
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
91 def process_event(self, st):
92 """
93 Processes an event, and log it.
95 @param st string to process
96 """
97 self.server.process_event(st)
99 def serve_content_web(self, path, method, params):
100 """
101 Functions to overload (executed after serve_content).
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
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)
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)
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)
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)
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 """
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)
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
222 def __enter__(self):
223 """
224 What to do when creating the class.
225 """
226 return self
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()
235 def process_event(self, event):
236 """
237 Processes an event, it expects a format like the following:
239 ::
241 type1/uuid/type2/args
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()
251 info = event.split("/")
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": ""}
261 if len(info) > 4:
262 info[3:] = ["/".join(info[3:])]
263 if len(info) < 4:
264 raise OSError("unable to log event: " + event)
266 values = {"type1": info[0],
267 "uuid": info[1],
268 "type2": info[2],
269 "dtime": now,
270 "args": info[3]}
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
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()
288 @staticmethod
289 def run_server(server, dbfile, thread=False, port=8080,
290 logfile=None, fLOG=None):
291 """
292 Starts the server.
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)
304 About the parameter ``server``:
306 * ``None``, it becomes ``RSSServer(('localhost', 8080), dbfile, RSSSimpleHandler)``
307 * ``<server>``, it becomes ``RSSServer((server, 8080), dbfile, RSSSimpleHandler)``
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