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

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 

10import logging 

11from pathlib import Path 

12from re import findall, split, sub 

13 

14from markdown import markdown 

15 

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 

19 

20 

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

26 

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 

33 

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

41 

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

52 

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

60 

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

65 

66 

67 

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

69 

70{i["description"]} 

71 

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

73 return markdown(result) 

74 

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

85 

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 ) 

131 

132 if not builtin_params: 

133 return "" 

134 

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) 

137 

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: 

139 

140{items} 

141 

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. 

143 

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

145 return markdown(result) 

146 

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" 

153 

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] 

161 

162 params.append( 

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

164 ) 

165 

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

169 

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

180 

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> 

184 

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

186 

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 

193 

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) 

198 

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 } 

210 

211 * { 

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

213 } 

214 

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 } 

225 

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 } 

248 

249 .sidebar_menu , .sidebar_submenu { 

250 list-style-type: none; 

251 padding-left:0px !important; 

252 margin-top: 10px; 

253 

254 } 

255 

256 .sidebar_menu > li { 

257 padding: 2% 0px; 

258 border-top : solid 0.7px #595959; 

259 } 

260 

261 .sidebar_menu a { 

262 padding: 1% 9%; 

263 background-image: none !important; 

264 color: #595959; 

265 display: block; 

266 } 

267 

268 .sidebar_menu a:hover { 

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

270 font-weight: 400; 

271 } 

272 

273 .sidebar_submenu > li { 

274 padding-left:0px !important; 

275 background-color:#edf0f2; 

276 font-size: 1rem; 

277 } 

278 

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

280 margin-left: 33%; 

281 } 

282 .dashboard {text-align: center;} 

283 main h1+p , .info_api{ 

284 

285 padding-left: 3%; 

286 font-size: 1rem; 

287 line-height: 1.4em; 

288 } 

289 

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

291 

292 #operations h3 { 

293 color: #9931FC; 

294 margin-bottom: 0px; 

295 padding: 10px; 

296 } 

297 

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

299 padding-left: 0px !important; 

300 text-align: center; 

301 } 

302 

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 } 

317 

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 } 

325 

326 #operations > div > * { 

327 padding: 0px 2%; 

328 } 

329 

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 } 

337 

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 } 

345 

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 } 

354 

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 } 

363 

364 .attr_val { 

365 max-width: 50%; 

366 display:inline-table; 

367 height: 100%; 

368 vertical-align: top; 

369 } 

370 

371 .method { 

372 text-transform: uppercase; 

373 } 

374 

375 .params { 

376 margin-bottom: 0; 

377 } 

378 

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 } 

390 

391 pre code { 

392 font-family: monospace !important; 

393 } 

394 

395 p.ex { 

396 background-color: #f0f0f5; 

397 margin-bottom: 0px; 

398 padding-top: 5px; 

399 padding-bottom: 5px; 

400 } 

401 

402 h2:first-of-type { 

403 margin-bottom: 15px; 

404 } 

405 

406 ol:first-of-type { 

407 margin-top: 0; 

408 } 

409 

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 } 

418 

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

420 margin-left: 1.2%; 

421 }*/ 

422 

423 h1 {font-size: 2.5em;} 

424 h1, h2 { 

425 text-transform: uppercase; 

426 } 

427 

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 } 

434 

435 h2 ~ h2, section > h2 { 

436 

437 padding-top: 5px; 

438 margin-top: 40px; 

439 } 

440 

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 } 

450 

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 } 

460 

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

474 

475 p { 

476 overflow-wrap: break-word; 

477 word-wrap: break-word; 

478 } 

479 

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 } 

491 

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 } 

497 

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

506 

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 } 

515 

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 } 

526 

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

539 

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 } 

548 

549 .date_log , .method_log { 

550 color: #595959; 

551 font-size: 1rem; 

552 

553 } 

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

555 .date_log {display:inline-grid;} 

556 

557 .group_log:nth-child(odd) { 

558 margin-right:5px; 

559 font-size: 1rem; 

560 } 

561 

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 } 

579 

580 .code_200::before { 

581 background-color: #00cc00; 

582 } 

583 

584 .code_404::before { 

585 background-color: #cccc00; 

586 } 

587 

588 .code_500::before { 

589 background-color: #cc0000; 

590 } 

591 

592 """ 

593 

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

597 

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

603 

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

605 file_handler.setFormatter(log_formatter) 

606 root_logger.addHandler(file_handler) 

607 

608 console_handler = logging.StreamHandler() 

609 console_handler.setFormatter(log_formatter) 

610 root_logger.addHandler(console_handler) 

611 

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

620 

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) 

627 

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 ) 

632 

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

642 

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> 

660 

661 </div> 

662 """ 

663 return html 

664 

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] 

672 

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 ) 

693 

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

697 

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

714 

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) 

720 

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