Coverage for src/pyrsslocal/custom_server/aserver.py: 55%
146 statements
« prev ^ index » next coverage.py v7.1.0, created at 2024-04-30 08:45 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2024-04-30 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
13from pyquickhelper.loghelper import fLOG
14from pyensae.sql.database_main import Database
15from ..simple_server.simple_server_custom import SimpleHandler, ThreadServer
18class CustomDBServerHandler(SimpleHandler):
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 """
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
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
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]
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 ,
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
64 def process_event(self, st):
65 """
66 process an event, and log it
68 @param st string to process
69 """
70 self.server.process_event(st)
72 def serve_content_web(self, path, method, params):
73 """
74 functions to overload (executed after serve_content)
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("")
87 else:
88 url = path.path
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
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)
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)
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)
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)
137class CustomDBServer(ThreadingMixIn, HTTPServer):
139 """
140 defines a custom server which includes an access to a database,
141 this database will contain de table to store the clicks
143 .. exref::
144 :title: create a custom local server
146 The following code creates an instance of a local server.
147 The server expects to find its content in the same folder.
149 ::
151 from pyensae import Database
153 db = Database(dbfile)
154 df = pandas.DataFrame ( [ {"name":"xavier", "module":"pyrsslocal"} ] )
155 db.connect()
156 db.import_dataframe(df, "example")
157 db.close()
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("."))
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.
168 ::
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>
182 </head>
184 <body onload="setPositions(['divtable', ])" class="mymainbody">
186 <div class="divtop">
187 <h1>Custom DB Server unittest</h1>
188 </div>
190 <div class="divtable" id="divfiles" onscroll="savePosition('divtable')">
192 <h2>Content of table example</h2>
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>
204 <p>end.</p>
206 </div>
207 </body>
208 </html>
209 """
211 @staticmethod
212 def schema_table(table):
213 """
214 returns the schema for a specific table
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 ValueError("unexpected table name") # pragma: no cover
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
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)
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))
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)
274 self.table_event = "cs_events"
275 self.table_stats = "cs_stats"
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
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()
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()
307 def __enter__(self):
308 """
309 What to do when creating the class.
310 """
311 return self
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()
320 def process_event(self, event):
321 """
322 Processes an event, it expects a format like the following:
324 ::
326 type1/uuid/type2/args
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()
336 info = event.split("/")
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": ""}
346 if len(info) > 4:
347 info[3:] = ["/".join(info[3:])]
348 if len(info) < 4:
349 raise OSError("unable to log event: " + event)
351 values = {"type1": info[0],
352 "uuid": info[1],
353 "type2": info[2],
354 "dtime": now,
355 "args": info[3]}
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
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()
373 @staticmethod
374 def run_server(server, dbfile, thread=False, port=8080, logfile=None,
375 extra_path=None):
376 """
377 start the server
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)
389 @warning If you kill the python program while the thread is still running, python interpreter might be closed completely.
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