Coverage for ramose / html_documentation.py: 99%
123 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-15 15:58 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-15 15:58 +0000
1# SPDX-FileCopyrightText: 2018-2021 Silvio Peroni <silvio.peroni@unibo.it>
2# SPDX-FileCopyrightText: 2020-2021 Marilena Daquino <marilena.daquino2@unibo.it>
3# SPDX-FileCopyrightText: 2022 Davide Brembilla
4# SPDX-FileCopyrightText: 2024 Ivan Heibi <ivan.heibi2@unibo.it>
5# SPDX-FileCopyrightText: 2025 Sergei Slinkin
6# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it>
7#
8# SPDX-License-Identifier: ISC
10import logging
11from pathlib import Path
12from re import findall, split, sub
14from markdown import markdown
16from ramose._constants import FIELD_TYPE_RE, PARAM_NAME
17from ramose.documentation import DocumentationHandler
18from ramose.hash_format import parse_custom_params, parse_disable_params
21class HTMLDocumentationHandler(DocumentationHandler):
22 # HTML documentation: START
23 def __title(self, conf):
24 """This method returns the title string defined in the API specification."""
25 return conf["conf_json"][0]["title"]
27 def __htmlmetadescription(self, conf):
28 """This method returns the HTML meta-description tag defined in the API specification."""
29 desc = conf["conf_json"][0].get("html_meta_description")
30 if desc:
31 return f'<meta name="description" content="{desc}"/>'
32 return "" # pragma: no cover
34 def __sidebar(self, conf):
35 """This method builds the sidebar of the API documentation"""
36 i = conf["conf_json"][0]
37 ops_html = "".join(
38 f"<li><a class='btn' href='#{op['url']}'>{op['url']}</a></li>" for op in conf["conf_json"][1:]
39 )
40 return f"""
42 <h4>{i["title"]}</h4>
43 <ul id="sidebar_menu" class="sidebar_menu">
44 <li><a class="btn active" href="#description">DESCRIPTION</a></li>
45 <li><a class="btn" href="#parameters">PARAMETERS</a></li>
46 <li><a class="btn" href="#operations">OPERATIONS</a>
47 <ul class="sidebar_submenu">{ops_html}</ul>
48 </li>
49 <li><a class="btn active" href="/">HOME</a></li>
50 </ul>
51 """
53 def __header(self, conf):
54 """This method builds the header of the API documentation"""
55 i = conf["conf_json"][0]
56 api_url = i["base"] + i["url"]
57 result = f"""
58<a id='toc'></a>
59# {i["title"]}
61**Version:** {i["version"]} <br/>
62**API URL:** <a href="{api_url}">{api_url}</a><br/>
63**Contact:** {i["contacts"]}<br/>
64**License:** {i["license"]}<br/>
68## <a id="description"></a>Description [back to top](#toc)
70{i["description"]}
72{self.__parameters(conf)}"""
73 return markdown(result)
75 def __parameters(self, conf):
76 overridden: set[str] = set()
77 api_meta = conf["conf_json"][0]
78 if "disable_params" in api_meta:
79 overridden.update(parse_disable_params(api_meta["disable_params"]))
80 for op in conf["conf_json"][1:]:
81 if "custom_params" in op:
82 overridden.update(parse_custom_params(op["custom_params"]))
83 if "disable_params" in op:
84 overridden.update(parse_disable_params(op["disable_params"]))
86 builtin_params = []
87 if "require" not in overridden:
88 builtin_params.append(
89 "`require=<field_name>`: all the rows that have an empty value in the `<field_name>` specified are "
90 "removed from the result set - e.g. `require=given_name` removes all the rows that do not have any "
91 "string specified in the `given_name` field."
92 )
93 if "filter" not in overridden:
94 builtin_params.append(
95 "`filter=<field_name>:<operator><value>`: only the rows compliant with `<value>` are kept in the "
96 "result set. The parameter `<operation>` is not mandatory. If `<operation>` is not specified, "
97 "`<value>` is interpreted as a regular expression, otherwise it is compared by means of the specified "
98 'operation. Possible operators are "=", "<", and ">". For instance, `filter=title:semantics?` returns '
99 'all the rows that contain the string "semantic" or "semantics" in the field `title`, while '
100 "`filter=date:>2016-05` returns all the rows that have a `date` greater than May 2016."
101 )
102 if "sort" not in overridden:
103 builtin_params.append(
104 '`sort=<order>(<field_name>)`: sort in ascending (`<order>` set to "asc") or descending (`<order>` '
105 'set to "desc") order the rows in the result set according to the values in `<field_name>`. For '
106 "instance, `sort=desc(date)` sorts all the rows according to the value specified in the field `date` "
107 "in descending order."
108 )
109 if "format" not in overridden:
110 builtin_params.append(
111 "`format=<format_type>`: the final table is returned in the format specified in `<format_type>` that "
112 'can be either "csv" or "json" - e.g. `format=csv` returns the final table in CSV format. This '
113 'parameter has higher priority of the type specified through the "Accept" header of the request. Thus, '
114 "if the header of a request to the API specifies `Accept: text/csv` and the URL of such request "
115 "includes `format=json`, the final table is returned in JSON."
116 )
117 if "json" not in overridden:
118 builtin_params.append(
119 '`json=<operation_type>("<separator>",<field>,<new_field_1>,<new_field_2>,...)`: in case a JSON format '
120 "is requested in return, tranform each row of the final JSON table according to the rule specified. "
121 'If `<operation_type>` is set to "array", the string value associated to the field name `<field>` is '
122 "converted into an array by splitting the various textual parts by means of `<separator>`. For "
123 'instance, considering the JSON table `[ { "names": "Doe, John; Doe, Jane" }, ... ]`, the execution '
124 'of `array("; ",names)` returns `[ { "names": [ "Doe, John", "Doe, Jane" ], ... ]`. Instead, if '
125 '`<operation_type>` is set to "dict", the string value associated to the field name `<field>` is '
126 "converted into a dictionary by splitting the various textual parts by means of `<separator>` and by "
127 "associating the new fields `<new_field_1>`, `<new_field_2>`, etc., to these new parts. For instance, "
128 'considering the JSON table `[ { "name": "Doe, John" }, ... ]`, the execution of '
129 '`dict(", ",name,fname,gname)` returns `[ { "name": { "fname": "Doe", "gname": "John" }, ... ]`.'
130 )
132 if not builtin_params:
133 return ""
135 items = "\n\n".join(f"{idx}. {text}" for idx, text in enumerate(builtin_params, 1))
136 result = f"""## <a id="parameters"></a>Parameters [back to top](#toc)
138Parameters can be used to filter and control the results returned by the API. They are passed as normal HTTP parameters in the URL of the call. They are:
140{items}
142It is possible to specify one or more filtering operation of the same kind (e.g. `require=given_name&require=family_name`). In addition, these filtering operations are applied in the order presented above - first all the `require` operation, then all the `filter` operations followed by all the `sort` operation, and finally the `format` and the `json` operation (if applicable). It is worth mentioning that each of the aforementioned rules is applied in order, and it works on the structure returned after the execution of the previous rule.
144Example: `<api_operation_url>?require=doi&filter=date:>2015&sort=desc(date)`."""
145 return markdown(result)
147 def __operations(self, conf):
148 """This method returns the description of all the operations defined in the API."""
149 result = """## Operations [back to top](#toc)
150The operations that this API implements are:
151"""
152 ops = "\n"
154 for op in conf["conf_json"][1:]:
155 params = []
156 for p in findall(PARAM_NAME, op["url"]):
157 p_type = "str"
158 p_shape = ".+"
159 if p in op:
160 p_type, p_shape = findall(r"^\s*([^\(]+)\((.+)\)\s*$", op[p])[0]
162 params.append(
163 f"<em>{p}</em>: type <code>{p_type}</code>, regular expression shape <code>{p_shape}</code>"
164 )
166 if "custom_params" in op:
167 for param_name, param_conf in parse_custom_params(op["custom_params"]).items():
168 params.append(f"<em>{param_name}</em> (query, optional): {param_conf['description']}")
170 op_url = op["url"]
171 methods = ", ".join(split(r"\s+", op["method"].strip()))
172 params_html = "<ul><li>" + "</li>\n<li>".join(params) + "</li></ul>" if params else ""
173 has_custom_default = "default_format" in op and op["default_format"].strip().lower() not in ("csv", "json")
174 fields_html = (
175 ", ".join(f"{f} <em>({t})</em>" for t, f in findall(FIELD_TYPE_RE, op["field_type"]))
176 if "field_type" in op and not has_custom_default
177 else ""
178 )
179 example_url = conf["website"] + conf["base_url"] + op["call"]
181 result += f"\n* [{op_url}](#{op_url}): {op['description'].split(chr(10))[0]}"
182 ops += f"""<div id="{op_url}">
183<h3>{op_url} <a href="#operations">back to operations</a></h3>
185{markdown(op["description"])}
187<p class="attr"><strong>Accepted HTTP method(s)</strong> <span class="attr_val method">{methods}</span></p>
188<div class="attr params"><strong>Parameter(s)</strong>{params_html}</div>
189{'<p class="attr"><strong>Result fields type</strong><span class="attr_val">' + fields_html + "</span></p>" if fields_html else ""}
190<p class="attr"><strong>Example</strong><span class="attr_val"><a target="_blank" href="{example_url}">{op["call"]}</a></span></p>
191{'<p class="ex attr"><strong>Exemplar output (in JSON)</strong></p>' + chr(10) + "<pre><code>" + op["output_json"] + "</code></pre>" if "output_json" in op else ""}</div>"""
192 return markdown(result) + ops
194 def __footer(self):
195 """This method returns the footer of the API documentation."""
196 result = """This API and the related documentation has been created with <a href="https://github.com/opencitations/ramose" target="_blank">RAMOSE</a>, the *Restful API Manager Over SPARQL Endpoints*, developed by <a href="https://orcid.org/0000-0003-0530-4305" target="_blank">Silvio Peroni</a>, <a href="https://orcid.org/0000-0002-1113-7550" target="_blank">Marilena Daquino</a> and <a href="https://orcid.org/0000-0002-8420-0696" target="_blank">Arcangelo Massari</a>."""
197 return markdown(result)
199 def __css(self):
200 return """
201 @import url('https://fonts.googleapis.com/css2?family=Karla:wght@300;400&display=swap');
202 @media screen and (max-width: 850px) {
203 aside { display: none; }
204 main, #operations, .dashboard, body>footer {margin-left: 15% !important;}
205 #operations > ul:nth-of-type(1) li { display:block !important; max-width: 100% !important; }
206 h3 a[href] {display:block !important; float: none !important; font-size: 1rem !important;}
207 a {overflow: hidden; text-overflow: ellipsis;}
208 .info_api, .api_calls {display: block !important; max-width: 100% !important;}
209 }
211 * {
212 font-family: 'Karla', Geneva, sans-serif;
213 }
215 body {
216 margin: 3% 15% 7% 0px;
217 line-height: 1.5em;
218 letter-spacing: 0.02em;
219 font-size : 1em;
220 font-weight:300;
221 color: #303030;
222 text-align: justify;
223 background-color: #edf0f2;
224 }
226 aside {
227 height : 100%;
228 width: 20%;
229 position: fixed;
230 z-index: 1;
231 top: 0;
232 left: 0;
233 /*background-color: #404040;*/
234 overflow-x: hidden;
235 background-color: white;
236 box-shadow:0px 10px 30px 0px rgba(133,66,189,0.1);
237 }
238 p strong {
239 text-transform: uppercase;
240 font-size: 1rem;
241 }
242 aside h4 {
243 padding: 20px 9%;
244 margin: 0px !important;
245 color: #9931FC;
246 text-align: left !important;
247 }
249 .sidebar_menu , .sidebar_submenu {
250 list-style-type: none;
251 padding-left:0px !important;
252 margin-top: 10px;
254 }
256 .sidebar_menu > li {
257 padding: 2% 0px;
258 border-top : solid 0.7px #595959;
259 }
261 .sidebar_menu a {
262 padding: 1% 9%;
263 background-image: none !important;
264 color: #595959;
265 display: block;
266 }
268 .sidebar_menu a:hover {
269 border-left: solid 5px rgba(154, 49, 252,.5);
270 font-weight: 400;
271 }
273 .sidebar_submenu > li {
274 padding-left:0px !important;
275 background-color:#edf0f2;
276 font-size: 1rem;
277 }
279 main , #operations , .dashboard, body>footer {
280 margin-left: 33%;
281 }
282 .dashboard {text-align: center;}
283 main h1+p , .info_api{
285 padding-left: 3%;
286 font-size: 1rem;
287 line-height: 1.4em;
288 }
290 main h1+p {border-left: solid 5px rgba(154, 49, 252,.5);}
292 #operations h3 {
293 color: #9931FC;
294 margin-bottom: 0px;
295 padding: 10px;
296 }
298 #operations > ul:nth-of-type(1) {
299 padding-left: 0px !important;
300 text-align: center;
301 }
303 #operations > ul:nth-of-type(1) li {
304 background-color: white;
305 text-align: left;
306 display: inline-block;
307 overflow: visible;
308 text-overflow: ellipsis;
309 max-width: 35%;
310 min-height: 200px;
311 padding:4%;
312 margin: 1% 2% 1% 0px;
313 border-radius: 10px;
314 box-shadow: 0px 10px 30px 0px rgba(133,66,189,0.1);
315 vertical-align:top;
316 }
318 #operations > div {
319 background-color: white;
320 margin-top: 20px;
321 padding: 2%;
322 border-radius: 18px;
323 box-shadow: 0px 10px 30px 0px rgba(133,66,189,0.1);
324 }
326 #operations > div > * {
327 padding: 0px 2%;
328 }
330 #operations > div ul, .params+ul{
331 list-style-type: none;
332 font-size: 1rem;
333 }
334 #operations > div ul:nth-of-type(1) li, .params+ul li {
335 margin: 10px 0px;
336 }
338 #operations > div ul:nth-of-type(1) li em, .params+ul li em {
339 font-style: normal;
340 font-weight: 400;
341 color: #9931FC;
342 border-left: solid 2px #9931FC;
343 padding:5px;
344 }
346 .attr {
347 border-top: solid 1px rgba(133,66,189,0.1);
348 padding: 2% !important;
349 display:block;
350 vertical-align: top;
351 font-size: 1rem;
352 text-align: left;
353 }
355 .attr > strong {
356 width: 30%;
357 color: #595959;
358 font-weight: 400;
359 font-style: normal;
360 display:inline-block;
361 vertical-align: top;
362 }
364 .attr_val {
365 max-width: 50%;
366 display:inline-table;
367 height: 100%;
368 vertical-align: top;
369 }
371 .method {
372 text-transform: uppercase;
373 }
375 .params {
376 margin-bottom: 0;
377 }
379 pre {
380 background-color: #f0f0f5;
381 padding: 10px;
382 margin-top: 0;
383 margin-bottom: 0;
384 border-radius: 0 0 14px 14px;
385 font-family: monospace !important;
386 overflow: scroll;
387 line-height: 1.2em;
388 height: 250px;
389 }
391 pre code {
392 font-family: monospace !important;
393 }
395 p.ex {
396 background-color: #f0f0f5;
397 margin-bottom: 0px;
398 padding-top: 5px;
399 padding-bottom: 5px;
400 }
402 h2:first-of-type {
403 margin-bottom: 15px;
404 }
406 ol:first-of-type {
407 margin-top: 0;
408 }
410 :not(pre) > code {
411 background-color: #f0f0f5;
412 color: #54547a;
413 padding: 0 2px 0 2px;
414 border-radius: 3px;
415 font-family : monospace;
416 font-size: 1em !important;
417 }
419 /**:not(div) > p {
420 margin-left: 1.2%;
421 }*/
423 h1 {font-size: 2.5em;}
424 h1, h2 {
425 text-transform: uppercase;
426 }
428 h1, h2, h3, h4, h5, h6 {
429 line-height: 1.2em;
430 padding-top:1em;
431 text-align: left !important;
432 font-weight:400;
433 }
435 h2 ~ h2, section > h2 {
437 padding-top: 5px;
438 margin-top: 40px;
439 }
441 h2 a[href], h3 a[href] {
442 background-image: none;
443 text-transform:uppercase;
444 padding: 1px 3px 1px 3px;
445 font-size: 1rem;
446 float: right;
447 position:relative;
448 top: -3px;
449 }
451 h2 a[href]::before , h3 a[href]::before {
452 content: " \u2191";
453 width: 20px;
454 height: 20px;
455 display:inline-block;
456 color: #9931FC;
457 text-align:center;
458 margin-right: 10px;
459 }
461 /*h3 a[href] {
462 color:white
463 background-image: none;
464 text-transform:uppercase;
465 padding: 1px 3px 1px 3px;
466 font-size: 8pt !important;
467 border: 1px solid #9931FC;
468 float: right;
469 position:relative;
470 top: -11px;
471 right: -11px;
472 border-radius: 0 14px 0 0;
473 }*/
475 p {
476 overflow-wrap: break-word;
477 word-wrap: break-word;
478 }
480 a {
481 color : black;
482 text-decoration: none;
483 background-image: -webkit-gradient(linear,left top, left bottom,color-stop(70%, transparent),color-stop(0, rgba(154, 49, 252,.5)));
484 background-image: linear-gradient(180deg,transparent 70%,rgba(154, 49, 252,.5) 0);
485 background-position-y: 3px;
486 background-position-x: 0px;
487 background-repeat: no-repeat;
488 -webkit-transition: .15s ease;
489 transition: .15s ease;
490 }
492 a:hover {
493 color: #282828;
494 background-image: -webkit-gradient(linear,left top, left bottom,color-stop(70%, transparent),color-stop(0, rgba(154, 49, 252,.25)));
495 background-image: linear-gradient(180deg,transparent 70%,rgba(154, 49, 252,.25) 0);
496 }
498 footer {
499 margin-top: 20px;
500 border-top: 1px solid lightgrey;
501 text-align: center;
502 color: #595959;
503 font-size: 1rem;
504 }
505 /* dashboard */
507 .info_api {
508 max-width: 35%;
509 border-radius: 15px;
510 text-align: left;
511 vertical-align: top;
512 background-color: #9931FC;
513 color: white;
514 }
516 .info_api, .api_calls {
517 display: inline-block;
518 text-align: left;
519 height: 200px;
520 padding:4%;
521 margin: 1% 2% 1% 0px;
522 border-radius: 10px;
523 box-shadow: 0px 10px 30px 0px rgba(133,66,189,0.1);
524 vertical-align:top;
525 }
527 .api_calls {
528 max-width: 40%;
529 background-color: white;
530 scroll-behavior: smooth;
531 overflow: auto;
532 overflow-y: scroll;
533 scrollbar-color: #9931FC rgb(154, 49, 252);
534 border-radius: 10px;
535 overflow-wrap: break-word;
536 word-break: break-word;
537 }
538 .api_calls div {padding-bottom:2%; overflow-wrap: break-word; word-break: break-word;}
540 .api_calls:hover {
541 overflow-y: scroll;
542 }
543 .api_calls h4, .info_api h2 {padding-top: 0px !important; margin-top: 0px !important;}
544 .api_calls div p {
545 padding: 0.2em 0.5em;
546 border-top: solid 1px #F8F8F8;
547 }
549 .date_log , .method_log {
550 color: #595959;
551 font-size: 1rem;
553 }
554 .method_log {margin-left: 15px;}
555 .date_log {display:inline-grid;}
557 .group_log:nth-child(odd) {
558 margin-right:5px;
559 font-size: 1rem;
560 }
562 .group_log:nth-child(even) {
563 display: inline-grid;
564 vertical-align: top;
565 }
566 .status_log {padding-right:15px;}
567 .status_log::before {
568 content: '';
569 display: inline-block;
570 width: 1em;
571 height: 1em;
572 vertical-align: middle;
573 -moz-border-radius: 50%;
574 -webkit-border-radius: 50%;
575 border-radius: 50%;
576 background-color: #595959;
577 margin-right: 0.8em;
578 }
580 .code_200::before {
581 background-color: #00cc00;
582 }
584 .code_404::before {
585 background-color: #cccc00;
586 }
588 .code_500::before {
589 background-color: #cc0000;
590 }
592 """
594 def __css_path(self, css_path=None):
595 """Add link to a css file if specified in argument -css"""
596 return f'<link rel="stylesheet" type="text/css" href=\'{css_path}\'>' if css_path else ""
598 def logger_ramose(self): # pragma: no cover
599 """This method adds logging info to a local file"""
600 # logging
601 log_formatter = logging.Formatter("[%(asctime)s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s")
602 root_logger = logging.getLogger()
604 file_handler = logging.FileHandler("ramose.log")
605 file_handler.setFormatter(log_formatter)
606 root_logger.addHandler(file_handler)
608 console_handler = logging.StreamHandler()
609 console_handler.setFormatter(log_formatter)
610 root_logger.addHandler(console_handler)
612 def __parse_logger_ramose(self):
613 """This method reads logging info stored into a local file, so as to be browsed in the dashboard.
614 Returns: the html including the list of URLs of current working APIs and basic logging info"""
615 try:
616 with Path("ramose.log").open() as l_f:
617 logs = l_f.read()
618 except FileNotFoundError:
619 logs = ""
621 seen = set()
622 rev_list = []
623 for x in reversed(logs.splitlines()):
624 if x not in seen:
625 seen.add(x)
626 rev_list.append(x)
628 sidebar_items = "".join(
629 f'\n <li><a class="btn active" href="{api_url}">{api_dict["conf_json"][0]["title"]}</a></li>\n '
630 for api_url, api_dict in self.conf_doc.items()
631 )
633 html = f"""
634 <p></p>
635 <aside>
636 <h4>RAMOSE API DASHBOARD</h4>
637 <ul id="sidebar_menu" class="sidebar_menu">{sidebar_items}
638 </ul>
639 </aside>
640 <header class="dashboard">
641 <h1>API MONITORING</h1>"""
643 for api_url, api_dict in self.conf_doc.items():
644 clean_list = [line for line in rev_list if api_url in line and "debug" not in line]
645 api_logs_list = "".join(
646 f"<p>{self.clean_log(line, api_url)}</p>" for line in clean_list if self.clean_log(line, api_url) != ""
647 )
648 api_title = api_dict["conf_json"][0]["title"]
649 html += f"""
650 <div class="info_api">
651 <h2>{api_title}</h2>
652 <a id="view_doc" href="{api_url}">VIEW DOCUMENTATION</a><br/>
653 <a href="{api_dict["tp"]}" target="_blank">GO TO SPARQL ENDPOINT</a><br/>
654 </div>
655 <div class="api_calls">
656 <h4>Last calls</h4>
657 <div>
658 {api_logs_list}
659 </div>
661 </div>
662 """
663 return html
665 def get_documentation(self, css_path=None, base_url=None):
666 """This method generates the HTML documentation of an API described in configuration file."""
667 if base_url is None:
668 first_key = next(iter(self.conf_doc))
669 conf = self.conf_doc[first_key]
670 else:
671 conf = self.conf_doc["/" + base_url]
673 return (
674 200,
675 f"""<!DOCTYPE html>
676<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
677 <head>
678 <title>{self.__title(conf)}</title>
679 {self.__htmlmetadescription(conf)}
680 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
681 <meta name="viewport" content="width=device-width" />
682 <style>{self.__css()}</style>
683 {self.__css_path(css_path)}
684 </head>
685 <body>
686 <aside>{self.__sidebar(conf)}</aside>
687 <main>{self.__header(conf)}</main>
688 <section id="operations">{self.__operations(conf)}</section>
689 <footer>{self.__footer()}</footer>
690 </body>
691</html>""",
692 )
694 def get_index(self, css_path=None):
695 """This method generates the index of all the HTML documentations that can be
696 created from the configuration file."""
698 return f"""
699 <!doctype html>
700 <html lang="en">
701 <head>
702 <meta charset="utf-8">
703 <title>RAMOSE</title>
704 <meta name="description" content="Documentation of RAMOSE API Manager">
705 <style>{self.__css()}</style>
706 {self.__css_path(css_path)}
707 </head>
708 <body>
709 {self.__parse_logger_ramose()}
710 <footer>{self.__footer()}</footer>
711 </body>
712 </html>
713 """
715 def store_documentation(self, file_path, css_path=None):
716 """This method stores the HTML documentation of an API in a file."""
717 _, html = self.get_documentation(css_path)
718 with Path(file_path).open("w") as f:
719 f.write(html)
721 def clean_log(self, log_line, api_url):
722 """This method parses logs lines into structured data."""
723 if "- - " not in log_line:
724 return ""
725 s = log_line.split("- - ", 1)[1]
726 date = s[s.find("[") + 1 : s.find("]")]
727 method = s.split('"')[1::2][0].split()[0]
728 cur_call = s.split('"')[1::2][0].split()[1].strip()
729 status = sub(r"\D+", "", s.split('"', 2)[2])
730 if cur_call != api_url + "/":
731 full_str = (
732 "<span class='group_log'><span class='status_log code_"
733 + status
734 + "'>"
735 + status
736 + "</span>"
737 + "<span class='date_log'>"
738 + date
739 + "</span><span class='method_log'>"
740 + method
741 + "</span></span>"
742 + "<span class='group_log'><span class='call_log'><a href='"
743 + cur_call
744 + "' target='_blank'>"
745 + cur_call
746 + "</a></span></span>"
747 )
748 else:
749 full_str = ""
750 return full_str