mkdocs-owl-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,570 @@
1
+ """
2
+ AsyncAPI 2.x/3.0 page renderer.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any
8
+
9
+ from .common import (
10
+ _anchor,
11
+ _demote_headings,
12
+ _heading,
13
+ _pill,
14
+ _render_bindings,
15
+ _render_downloads_table,
16
+ _render_examples,
17
+ _render_schema,
18
+ _render_security_inline,
19
+ _render_tags,
20
+ _resolve_ref,
21
+ _ref_link,
22
+ _schema_depth,
23
+ )
24
+
25
+
26
+ def _render_info_extras(info: dict[str, Any]) -> str:
27
+ """
28
+ Render the metadata.
29
+ Covers `info.license`, `info.contact`, `info.externalDocs`.
30
+ """
31
+ parts: list[str] = []
32
+
33
+ license_ = info.get("license")
34
+ if isinstance(license_, dict):
35
+ name = license_.get("name") or "license"
36
+ url = license_.get("url")
37
+ parts.append(f"**License:** [{name}]({url})" if url else f"**License:** {name}")
38
+
39
+ contact = info.get("contact")
40
+ if isinstance(contact, dict):
41
+ bits: list[str] = []
42
+ if contact.get("name"):
43
+ bits.append(contact["name"])
44
+ if contact.get("email"):
45
+ bits.append(f"[{contact['email']}](mailto:{contact['email']})")
46
+ if contact.get("url"):
47
+ bits.append(f"[{contact['url']}]({contact['url']})")
48
+ if bits:
49
+ parts.append(f"**Contact:** {', '.join(bits)}")
50
+
51
+ ext_docs = info.get("externalDocs")
52
+ if isinstance(ext_docs, dict) and ext_docs.get("url"):
53
+ url = ext_docs["url"]
54
+ desc = ext_docs.get("description") or url
55
+ parts.append(f"**External docs:** [{desc}]({url})")
56
+
57
+ if not parts:
58
+ return ""
59
+ parts.append("")
60
+ return "\n".join(parts)
61
+
62
+
63
+ def _render_servers(spec: dict[str, Any], opts: dict[str, Any]) -> str:
64
+ servers = spec.get("servers")
65
+ if not isinstance(servers, dict) or not servers:
66
+ return ""
67
+
68
+ hide_security = bool(opts.get("hide_security"))
69
+ hide_bindings = bool(opts.get("hide_bindings"))
70
+
71
+ parts: list[str] = ["## Servers", ""]
72
+ for sname, server in servers.items():
73
+ if not isinstance(server, dict):
74
+ continue
75
+ parts.append(_heading(3, sname, anchor=_anchor("servers", sname)))
76
+ parts.append("")
77
+
78
+ desc = (server.get("description") or "").strip()
79
+ if desc:
80
+ parts.append(_demote_headings(desc))
81
+ parts.append("")
82
+
83
+ meta_emitted = False
84
+ host = server.get("host")
85
+ if host:
86
+ parts.append(f"**Host:** `{host}`")
87
+ meta_emitted = True
88
+ protocol = server.get("protocol")
89
+ if protocol:
90
+ parts.append(f"**Protocol:** {_pill(str(protocol), kind='protocol')}")
91
+ meta_emitted = True
92
+ for label, key in (("Protocol version", "protocolVersion"),
93
+ ("Pathname", "pathname")):
94
+ v = server.get(key)
95
+ if v:
96
+ parts.append(f"**{label}:** `{v}`")
97
+ meta_emitted = True
98
+ if meta_emitted:
99
+ parts.append("")
100
+
101
+ tags = _render_tags(server.get("tags"))
102
+ if tags:
103
+ parts.append(tags)
104
+ parts.append("")
105
+
106
+ security = server.get("security") or []
107
+ if security and not hide_security:
108
+ for entry in security:
109
+ block = _render_security_inline(spec, entry)
110
+ if block:
111
+ parts.append(block)
112
+ parts.append("")
113
+
114
+ bindings = _render_bindings(server.get("bindings"), hide_bindings=hide_bindings)
115
+ if bindings:
116
+ parts.append(bindings)
117
+
118
+ return "\n".join(parts)
119
+
120
+
121
+ def _render_message(
122
+ msg: dict[str, Any],
123
+ *,
124
+ name: str | None = None,
125
+ hide_internal: bool,
126
+ hide_bindings: bool,
127
+ hide_traits: bool,
128
+ max_depth: int = 3,
129
+ show_message_id: bool = False,
130
+ ) -> str:
131
+ parts: list[str] = []
132
+
133
+ if show_message_id:
134
+ mid = msg.get("messageId")
135
+ if mid:
136
+ parts.append(f"**Message ID:** `{mid}`")
137
+ parts.append("")
138
+
139
+ title = (msg.get("title") or "").strip()
140
+ if title and title != name:
141
+ parts.append(f"_{title}_")
142
+ parts.append("")
143
+
144
+ summary = (msg.get("summary") or "").strip()
145
+ if summary:
146
+ parts.append(summary)
147
+ parts.append("")
148
+
149
+ desc = (msg.get("description") or "").strip()
150
+ if desc:
151
+ parts.append(_demote_headings(desc))
152
+ parts.append("")
153
+
154
+ ct = msg.get("contentType")
155
+ if ct:
156
+ parts.append(f"**Content type:** {_pill(str(ct), kind='contenttype')}")
157
+ parts.append("")
158
+
159
+ tags = _render_tags(msg.get("tags"))
160
+ if tags:
161
+ parts.append(tags)
162
+ parts.append("")
163
+
164
+ headers = msg.get("headers")
165
+ if isinstance(headers, dict):
166
+ parts.append("**Headers**")
167
+ parts.append("")
168
+ parts.append(_render_schema(headers, hide_internal=hide_internal, max_depth=max_depth))
169
+ parts.append("")
170
+
171
+ payload = msg.get("payload")
172
+ if isinstance(payload, dict):
173
+ parts.append("**Payload**")
174
+ parts.append("")
175
+ parts.append(_render_schema(payload, hide_internal=hide_internal, max_depth=max_depth))
176
+ parts.append("")
177
+
178
+ traits = msg.get("traits") or []
179
+ if traits and not hide_traits:
180
+ parts.append("**Traits:**")
181
+ parts.append("")
182
+ for t in traits:
183
+ if isinstance(t, dict) and "$ref" in t:
184
+ parts.append(f"- {_ref_link(t['$ref'])}")
185
+ parts.append("")
186
+
187
+ examples = msg.get("examples")
188
+ if examples:
189
+ ex_block = _render_examples(examples)
190
+ if ex_block:
191
+ parts.append(ex_block)
192
+
193
+ bindings = _render_bindings(msg.get("bindings"), hide_bindings=hide_bindings)
194
+ if bindings:
195
+ parts.append(bindings)
196
+
197
+ return "\n".join(parts).strip()
198
+
199
+
200
+ def _render_v2_operation(
201
+ op: dict[str, Any],
202
+ *,
203
+ action: str,
204
+ channel_name: str,
205
+ channel: dict[str, Any],
206
+ hide_internal: bool,
207
+ hide_bindings: bool,
208
+ hide_traits: bool,
209
+ max_depth: int = 3,
210
+ ) -> str:
211
+ op_id = op.get("operationId") or f"{action} {channel_name}"
212
+ parts: list[str] = [_heading(3, op_id, anchor=_anchor("operations", op_id)), ""]
213
+
214
+ parts.append(f"**Action:** {_pill(action, kind=f'action-{action}')}")
215
+ parts.append("")
216
+
217
+ summary = (op.get("summary") or "").strip()
218
+ if summary:
219
+ parts.append(summary)
220
+ parts.append("")
221
+
222
+ desc = (op.get("description") or "").strip()
223
+ if desc:
224
+ parts.append(_demote_headings(desc))
225
+ parts.append("")
226
+
227
+ tags = _render_tags(op.get("tags"))
228
+ if tags:
229
+ parts.append(tags)
230
+ parts.append("")
231
+
232
+ addr = channel.get("address") or channel_name
233
+ parts.append(f"**Channel:** `{addr}`")
234
+ parts.append("")
235
+
236
+ params = channel.get("parameters") or {}
237
+ if isinstance(params, dict) and params:
238
+ parts.append("**Parameters:**")
239
+ parts.append("")
240
+ for pname, p in params.items():
241
+ if isinstance(p, dict) and "$ref" in p:
242
+ parts.append(f"- `{pname}` — {_ref_link(p['$ref'])}")
243
+ elif isinstance(p, dict):
244
+ pdesc = (p.get("description") or "").strip()
245
+ parts.append(f"- `{pname}`" + (f" — {pdesc}" if pdesc else ""))
246
+ else:
247
+ parts.append(f"- `{pname}`")
248
+ parts.append("")
249
+
250
+ msg = op.get("message") or {}
251
+ messages = msg.get("oneOf", [msg] if msg and "oneOf" not in msg else [])
252
+ for m in messages:
253
+ if not isinstance(m, dict):
254
+ continue
255
+ if "$ref" in m:
256
+ parts.append(f"**Message:** {_ref_link(m['$ref'])}")
257
+ parts.append("")
258
+ continue
259
+ mname = m.get("name") or m.get("title") or "Message"
260
+ parts.append(f"**Message: {mname}**")
261
+ parts.append("")
262
+ parts.append(_render_message(
263
+ m, name=mname,
264
+ hide_internal=hide_internal, hide_bindings=hide_bindings,
265
+ hide_traits=hide_traits, max_depth=max_depth,
266
+ ))
267
+ parts.append("")
268
+
269
+ bindings = _render_bindings(op.get("bindings"), hide_bindings=hide_bindings)
270
+ if bindings:
271
+ parts.append(bindings)
272
+
273
+ return "\n".join(parts)
274
+
275
+
276
+ def _render_operations_v2(spec: dict[str, Any], opts: dict[str, Any]) -> str:
277
+ channels = spec.get("channels")
278
+ if not isinstance(channels, dict) or not channels:
279
+ return ""
280
+
281
+ hide_internal = bool(opts.get("hide_internal"))
282
+ hide_bindings = bool(opts.get("hide_bindings"))
283
+ hide_traits = bool(opts.get("hide_traits"))
284
+ max_depth = _schema_depth(opts)
285
+
286
+ parts: list[str] = ["## Operations", ""]
287
+ emitted = False
288
+ for cname, channel in channels.items():
289
+ if not isinstance(channel, dict):
290
+ continue
291
+ for action in ("publish", "subscribe"):
292
+ op = channel.get(action)
293
+ if not isinstance(op, dict):
294
+ continue
295
+ parts.append(_render_v2_operation(
296
+ op,
297
+ action=action,
298
+ channel_name=cname,
299
+ channel=channel,
300
+ hide_internal=hide_internal,
301
+ hide_bindings=hide_bindings,
302
+ hide_traits=hide_traits,
303
+ max_depth=max_depth,
304
+ ))
305
+ parts.append("")
306
+ emitted = True
307
+
308
+ return "\n".join(parts) if emitted else ""
309
+
310
+
311
+ def _render_operations(spec: dict[str, Any], opts: dict[str, Any]) -> str:
312
+ """Render the `## Operations` section with one `### opName` per operation.
313
+
314
+ AsyncAPI 3.0 has a top-level `operations` map, rendered as is.
315
+ AsyncAPI 2.x collects from `publish`/`subscribe`
316
+ """
317
+ ops = spec.get("operations")
318
+ if not isinstance(ops, dict) or not ops:
319
+ return _render_operations_v2(spec, opts)
320
+
321
+ hide_bindings = bool(opts.get("hide_bindings"))
322
+ hide_traits = bool(opts.get("hide_traits"))
323
+
324
+ parts: list[str] = ["## Operations", ""]
325
+ for oname, op in ops.items():
326
+ if not isinstance(op, dict):
327
+ continue
328
+ parts.append(_heading(3, oname, anchor=_anchor("operations", oname)))
329
+ parts.append("")
330
+
331
+ action = op.get("action")
332
+ if action:
333
+ kind = "action-send" if action == "send" else "action-receive"
334
+ line = f"**Action:** {_pill(str(action), kind=kind)}"
335
+ if op.get("deprecated"):
336
+ line += " " + _pill("deprecated", kind="deprecated")
337
+ parts.append(line)
338
+ parts.append("")
339
+
340
+ summary = (op.get("summary") or "").strip()
341
+ if summary:
342
+ parts.append(summary)
343
+ parts.append("")
344
+
345
+ desc = (op.get("description") or "").strip()
346
+ if desc:
347
+ parts.append(_demote_headings(desc))
348
+ parts.append("")
349
+
350
+ tags = _render_tags(op.get("tags"))
351
+ if tags:
352
+ parts.append(tags)
353
+ parts.append("")
354
+
355
+ ch = op.get("channel")
356
+ if isinstance(ch, dict) and "$ref" in ch:
357
+ resolved = _resolve_ref(spec, ch["$ref"])
358
+ if isinstance(resolved, dict) and resolved.get("address"):
359
+ parts.append(f"**Channel:** `{resolved['address']}`")
360
+ else:
361
+ parts.append(f"**Channel:** `{ch['$ref'].rsplit('/', 1)[-1]}`")
362
+ parts.append("")
363
+
364
+ msgs = op.get("messages") or []
365
+ if isinstance(msgs, list) and msgs:
366
+ parts.append("**Messages:**")
367
+ parts.append("")
368
+ for m in msgs:
369
+ if isinstance(m, dict) and "$ref" in m:
370
+ parts.append(f"- {_ref_link(m['$ref'])}")
371
+ parts.append("")
372
+
373
+ traits = op.get("traits") or []
374
+ if traits and not hide_traits:
375
+ parts.append("**Traits:**")
376
+ parts.append("")
377
+ for t in traits:
378
+ if isinstance(t, dict) and "$ref" in t:
379
+ parts.append(f"- {_ref_link(t['$ref'])}")
380
+ parts.append("")
381
+
382
+ bindings = _render_bindings(op.get("bindings"), hide_bindings=hide_bindings)
383
+ if bindings:
384
+ parts.append(bindings)
385
+
386
+ return "\n".join(parts)
387
+
388
+
389
+ def _render_messages(spec: dict[str, Any], opts: dict[str, Any]) -> str:
390
+ msgs = (spec.get("components") or {}).get("messages")
391
+ if not isinstance(msgs, dict) or not msgs:
392
+ return ""
393
+
394
+ hide_internal = bool(opts.get("hide_internal"))
395
+ hide_bindings = bool(opts.get("hide_bindings"))
396
+ hide_traits = bool(opts.get("hide_traits"))
397
+ max_depth = _schema_depth(opts)
398
+
399
+ parts: list[str] = ["## Messages", ""]
400
+ for mname, msg in msgs.items():
401
+ if not isinstance(msg, dict):
402
+ continue
403
+ parts.append(_heading(3, mname, anchor=_anchor("messages", mname)))
404
+ parts.append("")
405
+ parts.append(_render_message(
406
+ msg, name=mname,
407
+ hide_internal=hide_internal, hide_bindings=hide_bindings,
408
+ hide_traits=hide_traits, max_depth=max_depth, show_message_id=True,
409
+ ))
410
+ parts.append("")
411
+
412
+ return "\n".join(parts)
413
+
414
+
415
+ def _render_schemas_section(spec: dict[str, Any], opts: dict[str, Any]) -> str:
416
+ schemas = (spec.get("components") or {}).get("schemas")
417
+ if not isinstance(schemas, dict) or not schemas:
418
+ return ""
419
+
420
+ hide_internal = bool(opts.get("hide_internal"))
421
+ max_depth = _schema_depth(opts)
422
+
423
+ parts: list[str] = ["## Schemas", ""]
424
+ for sname, sch in schemas.items():
425
+ if not isinstance(sch, dict):
426
+ continue
427
+ parts.append(_heading(3, sname, anchor=_anchor("schemas", sname)))
428
+ parts.append("")
429
+ parts.append(_render_schema(sch, hide_internal=hide_internal, max_depth=max_depth))
430
+ parts.append("")
431
+ return "\n".join(parts)
432
+
433
+
434
+ def _render_parameters(spec: dict[str, Any], opts: dict[str, Any]) -> str:
435
+ params = (spec.get("components") or {}).get("parameters")
436
+ if not isinstance(params, dict) or not params:
437
+ return ""
438
+
439
+ parts: list[str] = ["## Parameters", ""]
440
+ for pname, p in params.items():
441
+ if not isinstance(p, dict):
442
+ continue
443
+ parts.append(_heading(3, pname, anchor=_anchor("parameters", pname)))
444
+ parts.append("")
445
+
446
+ desc = (p.get("description") or "").strip()
447
+ if desc:
448
+ parts.append(_demote_headings(desc))
449
+ parts.append("")
450
+
451
+ enum = p.get("enum")
452
+ if enum:
453
+ parts.append("**Allowed values:**")
454
+ parts.append("")
455
+ for v in enum:
456
+ parts.append(f"- `{v}`")
457
+ parts.append("")
458
+
459
+ default = p.get("default")
460
+ if default is not None:
461
+ parts.append(f"**Default:** `{default}`")
462
+ parts.append("")
463
+
464
+ return "\n".join(parts)
465
+
466
+
467
+ def _render_traits(
468
+ spec: dict[str, Any],
469
+ opts: dict[str, Any],
470
+ *,
471
+ container: str,
472
+ heading: str,
473
+ ) -> str:
474
+ if bool(opts.get("hide_traits")):
475
+ return ""
476
+ traits = (spec.get("components") or {}).get(container)
477
+ if not isinstance(traits, dict) or not traits:
478
+ return ""
479
+
480
+ hide_internal = bool(opts.get("hide_internal"))
481
+ max_depth = _schema_depth(opts)
482
+
483
+ parts: list[str] = [f"## {heading}", ""]
484
+ for tname, trait in traits.items():
485
+ if not isinstance(trait, dict):
486
+ continue
487
+ parts.append(_heading(3, tname, anchor=_anchor(container, tname)))
488
+ parts.append("")
489
+
490
+ desc = (trait.get("description") or "").strip()
491
+ if desc:
492
+ parts.append(_demote_headings(desc))
493
+ parts.append("")
494
+
495
+ ct = trait.get("contentType")
496
+ if ct:
497
+ parts.append(f"**Content type:** {_pill(str(ct), kind='contenttype')}")
498
+ parts.append("")
499
+
500
+ headers = trait.get("headers")
501
+ if isinstance(headers, dict):
502
+ parts.append("**Headers**")
503
+ parts.append("")
504
+ parts.append(_render_schema(headers, hide_internal=hide_internal, max_depth=max_depth))
505
+ parts.append("")
506
+
507
+ return "\n".join(parts)
508
+
509
+
510
+ def _render_page(
511
+ spec: dict[str, Any],
512
+ opts: dict[str, Any],
513
+ *,
514
+ spec_url: str = "",
515
+ attachments: list[dict[str, Any]] | None = None,
516
+ ) -> str:
517
+ """Render the full AsyncAPI page Markdown from a spec + page options."""
518
+ info = spec.get("info") or {}
519
+ title = (opts.get("title") or info.get("title") or "API Reference").strip()
520
+ intro = (opts.get("intro") or "").strip()
521
+ hide_version = bool(opts.get("hide_version"))
522
+ hide_download = bool(opts.get("hide_download_link"))
523
+
524
+ version = (info.get("version") or "").strip()
525
+ description = (info.get("description") or "").strip()
526
+
527
+ parts: list[str] = [f"# {title}", ""]
528
+
529
+ if intro:
530
+ parts.append(intro)
531
+ parts.append("")
532
+
533
+ if version and not hide_version:
534
+ parts.append(f"**Version:** `{version}`")
535
+ parts.append("")
536
+
537
+ downloads = _render_downloads_table(spec_url, attachments or [], hide_download=hide_download)
538
+ if downloads:
539
+ parts.append(downloads)
540
+
541
+ extras = _render_info_extras(info)
542
+ if extras:
543
+ parts.append(extras)
544
+
545
+ dct = spec.get("defaultContentType")
546
+ if dct:
547
+ parts.append(f"**Default content type:** {_pill(str(dct), kind='contenttype')}")
548
+ parts.append("")
549
+
550
+ if description:
551
+ parts.append(_demote_headings(description))
552
+ parts.append("")
553
+
554
+ sections = [
555
+ _render_servers(spec, opts),
556
+ _render_operations(spec, opts),
557
+ _render_messages(spec, opts),
558
+ _render_schemas_section(spec, opts),
559
+ _render_parameters(spec, opts),
560
+ _render_traits(spec, opts, container="messageTraits", heading="Message traits"),
561
+ _render_traits(spec, opts, container="operationTraits", heading="Operation traits"),
562
+ ]
563
+ for section in sections:
564
+ if section:
565
+ parts.append(section)
566
+
567
+ while parts and parts[-1] in ("", "---"):
568
+ parts.pop()
569
+
570
+ return "\n".join(parts)