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