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

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 

9 

10from __future__ import annotations 

11 

12import logging 

13from pathlib import Path 

14from re import findall, split, sub 

15from typing import TYPE_CHECKING 

16 

17from markdown import markdown 

18 

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 

22 

23if TYPE_CHECKING: 

24 from ramose.api_manager import APIConfig 

25 

26 

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"] 

32 

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 

39 

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""" 

47 

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 """ 

58 

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"]} 

66 

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/> 

71 

72 

73 

74## <a id="description"></a>Description [back to top](#toc) 

75 

76{i["description"]} 

77 

78{self.__parameters(conf)}""" 

79 return markdown(result) 

80 

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 ) 

127 

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"])) 

138 

139 builtin_params = [text for param_name, text in self._BUILTIN_PARAM_DOCS if param_name not in overridden] 

140 

141 if not builtin_params: 

142 return "" 

143 

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) 

160 

161{params_intro} 

162 

163{items} 

164 

165{filtering_note} 

166 

167Example: `<api_operation_url>?require=doi&filter=date:>2015&sort=desc(date)`.""" 

168 return markdown(result) 

169 

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" 

176 

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] 

184 

185 params.append( 

186 f"<em>{p}</em>: type <code>{p_type}</code>, regular expression shape <code>{p_shape}</code>", 

187 ) 

188 

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 ) 

194 

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"] 

205 

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>' 

219 

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> 

223 

224{markdown(op["description"])} 

225 

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 

232 

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) 

248 

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 } 

261 

262 * { 

263 font-family: 'Karla', Geneva, sans-serif; 

264 } 

265 

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 } 

276 

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 } 

299 

300 .sidebar_menu , .sidebar_submenu { 

301 list-style-type: none; 

302 padding-left:0px !important; 

303 margin-top: 10px; 

304 

305 } 

306 

307 .sidebar_menu > li { 

308 padding: 2% 0px; 

309 border-top : solid 0.7px #595959; 

310 } 

311 

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 } 

320 

321 .sidebar_menu a:hover { 

322 border-left: solid 5px rgba(154, 49, 252,.5); 

323 font-weight: 400; 

324 } 

325 

326 .sidebar_submenu > li { 

327 padding-left:0px !important; 

328 background-color:#edf0f2; 

329 font-size: 1rem; 

330 } 

331 

332 main , #operations , .dashboard, body>footer { 

333 margin-left: 33%; 

334 } 

335 .dashboard {text-align: center;} 

336 main h1+p , .info_api{ 

337 

338 padding-left: 3%; 

339 font-size: 1rem; 

340 line-height: 1.4em; 

341 } 

342 

343 main h1+p {border-left: solid 5px rgba(154, 49, 252,.5);} 

344 

345 #operations h3 { 

346 color: #9931FC; 

347 margin-bottom: 0px; 

348 padding: 10px; 

349 } 

350 

351 #operations > ul:nth-of-type(1) { 

352 padding-left: 0px !important; 

353 text-align: center; 

354 } 

355 

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 } 

372 

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 } 

380 

381 #operations > div > * { 

382 padding: 0px 2%; 

383 } 

384 

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 } 

392 

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 } 

400 

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 } 

409 

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 } 

418 

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 } 

427 

428 .method { 

429 text-transform: uppercase; 

430 } 

431 

432 .params { 

433 margin-bottom: 0; 

434 } 

435 

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 } 

447 

448 pre code { 

449 font-family: monospace !important; 

450 } 

451 

452 p.ex { 

453 background-color: #f0f0f5; 

454 margin-bottom: 0px; 

455 padding-top: 5px; 

456 padding-bottom: 5px; 

457 } 

458 

459 h2:first-of-type { 

460 margin-bottom: 15px; 

461 } 

462 

463 ol:first-of-type { 

464 margin-top: 0; 

465 } 

466 

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 } 

477 

478 /**:not(div) > p { 

479 margin-left: 1.2%; 

480 }*/ 

481 

482 h1 {font-size: 2.5em;} 

483 h1, h2 { 

484 text-transform: uppercase; 

485 } 

486 

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 } 

493 

494 h2 ~ h2, section > h2 { 

495 

496 padding-top: 5px; 

497 margin-top: 40px; 

498 } 

499 

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 } 

509 

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 } 

519 

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 }*/ 

533 

534 p { 

535 overflow-wrap: break-word; 

536 word-wrap: break-word; 

537 } 

538 

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 } 

553 

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 } 

562 

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 */ 

571 

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 } 

580 

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 } 

591 

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;} 

604 

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 } 

613 

614 .date_log , .method_log { 

615 color: #595959; 

616 font-size: 1rem; 

617 

618 } 

619 .method_log {margin-left: 15px;} 

620 .date_log {display:inline-grid;} 

621 

622 .group_log:nth-child(odd) { 

623 margin-right:5px; 

624 font-size: 1rem; 

625 } 

626 

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 } 

644 

645 .code_200::before { 

646 background-color: #00cc00; 

647 } 

648 

649 .code_404::before { 

650 background-color: #cccc00; 

651 } 

652 

653 .code_500::before { 

654 background-color: #cc0000; 

655 } 

656 

657 """ 

658 ) 

659 

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 "" 

663 

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() 

669 

670 file_handler = logging.FileHandler("ramose.log") 

671 file_handler.setFormatter(log_formatter) 

672 root_logger.addHandler(file_handler) 

673 

674 console_handler = logging.StreamHandler() 

675 console_handler.setFormatter(log_formatter) 

676 root_logger.addHandler(console_handler) 

677 

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 = "" 

686 

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) 

693 

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 ) 

701 

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>""" 

711 

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> 

729 

730 </div> 

731 """ 

732 return html 

733 

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] 

747 

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 ) 

768 

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.""" 

772 

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 """ 

789 

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) 

801 

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