The browsable site¶
The corpus rendered as pages: one per module, one per registrant, one per node of the registration tree. See The browsable site for the artifact a build writes and why the generator lives here.
A browsable rendering of a corpus, written by the build that produced it.
--emit=site is a corpus artifact like the indexes and the database: a
projection of the same jsondoc tree, written by the same build, over whatever
modules the manifest declared. An enterprise pointing mibcorpus at its own
private collection gets the same site over its own modules.
The alternative – a generator living in the repository that publishes the corpus – is the shape pysnmp/mibs#365 removed: that repository’s compiler, dependency resolver and OID indexer were each a second implementation of PySMI’s, and each had drifted.
See The mibcorpus tool for the artifact and pysnmp/pysmi#276 for the reasoning.
- class pysmi.corpus.site.ArcPage(arc: str, name: str, source: str, reference: str, children: tuple[Arc, ...], module: str = '')¶
One node of the registration tree.
Create new instance of ArcPage(arc, name, source, reference, children, module)
- class pysmi.corpus.site.Definition(name: str, oid: str, kind: str, syntax: str, access: str, status: str, description: str)¶
One thing a module defines, as its row on the module page.
Create new instance of Definition(name, oid, kind, syntax, access, status, description)
- class pysmi.corpus.site.EntityPage(number: int, arc: str, organization: str, contacts: tuple[Mapping[str, str], ...], modules: tuple[str, ...])¶
One registrant, and what the corpus holds under its arc.
Create new instance of EntityPage(number, arc, organization, contacts, modules)
- class pysmi.corpus.site.ModulePage(module: str, anchors: tuple[str, ...], organization: str, contact: str, lastupdated: str, revisions: tuple[str, ...], description: str, provenance: Mapping[str, str], imports: Mapping[str, tuple[str, ...]], importedBy: tuple[str, ...], closure: tuple[str, ...], missing: tuple[str, ...], under: tuple[Arc, ...], defects: tuple[tuple[str, str], ...], patch: str, types: tuple[Definition, ...], objects: tuple[Definition, ...], notifications: tuple[Definition, ...], conformance: tuple[Definition, ...])¶
One module: everything the corpus holds about it.
Create new instance of ModulePage(module, anchors, organization, contact, lastupdated, revisions, description, provenance, imports, importedBy, closure, missing, under, defects, patch, types, objects, notifications, conformance)
- anchors: tuple[str, ...]¶
The arcs this module registers at, ascending. Empty for a module that registers nothing, such as a TEXTUAL-CONVENTIONS-only module.
- conformance: tuple[Definition, ...]¶
Alias for field number 18
- notifications: tuple[Definition, ...]¶
Alias for field number 17
- objects: tuple[Definition, ...]¶
Alias for field number 16
- types: tuple[Definition, ...]¶
Alias for field number 15
- under: tuple[Arc, ...]¶
The arcs directly below this module’s own registrations, which is how the tree carries on past it. A module’s anchor gets no arc page of its own – that arc is the module – so without this the structural arcs under it are reachable from nothing. See pysnmp/pysmi#284 on orphans.
- class pysmi.corpus.site.Overview(modules: int, tiers: Mapping[str, int], registrants: int, arcs: int, objects: int, notifications: int, types: int, repaired: int, incomplete: int, revised: str, latest: Mapping[str, tuple[Recent, ...]], latestChanged: Mapping[str, tuple[Recent, ...]] = {}, datedChanged: Mapping[str, int] = {})¶
What the corpus holds, as the entry point states it.
Every figure is something the build computed while writing the pages. A front page that asked a question the build had not already answered would be a second pass over the corpus for the sake of a headline.
Create new instance of Overview(modules, tiers, registrants, arcs, objects, notifications, types, repaired, incomplete, revised, latest, latestChanged, datedChanged)
- datedChanged: Mapping[str, int]¶
Per tier, how many modules the distribution supplied a date for, which is the pool
latestChangedis the newest few of. Nottiers: a distribution may date a fraction of a tier, and “the ten most recent of the 6,982 it tracks” and “of the 6,982 this corpus holds” are different claims.
- latest: Mapping[str, tuple[Recent, ...]]¶
Per tier, the most recently revised modules, newest first. Same keys and same order as
tiers, a tier none of whose modules carries a readable date aside.
- latestChanged: Mapping[str, tuple[Recent, ...]]¶
Per tier, the modules the distribution changed most recently, newest first. Empty for a build given no dates, and a tier whose modules were all supplied without one is left out – which is what makes this list honest on a corpus whose standard tier comes from somewhere the distribution does not track.
- registrants: int¶
Enterprise arcs the corpus registers under, or 0 for a build that wrote no registrant pages.
- tiers: Mapping[str, int]¶
Modules per tier, in
pysmi.corpus.namespace.TIERSorder and holding only the tiers this corpus has modules in.
- class pysmi.corpus.site.PageSizes(browse: int = 500, entity: int = 500)¶
How long each kind of list gets before it splits into buckets.
One number would do if the lists were alike, and they are not. The module list is the site’s front door and a reader scrolling it wants few pages; a registrant’s list is reached by someone already narrowed to one vendor, and the largest vendors hold enough modules that the same size would give them a handful of pages of 500 links each. Measured over pysnmp/mibs, 500 and 200 are the sizes that fall out. See pysnmp/pysmi#287.
Create new instance of PageSizes(browse, entity)
- classmethod of(declared: int | Mapping[str, int] | PageSizes | None) PageSizes¶
Page sizes from a number, a mapping, or nothing.
A manifest may say one number for every list or name them individually, and a caller passing neither gets the defaults.
- Raises:
ValueError – a list was named that has no page size, or a size is not a positive number. Refused rather than ignored: a manifest saying
entitieswhere the key isentitywould otherwise publish a site silently paginated wrong.TypeError – declared is neither a number nor a mapping.
- class pysmi.corpus.site.Recent(module: str, revised: str, organization: str, definitions: int, changed: str = '')¶
One module’s row in a recently revised list.
Create new instance of Recent(module, revised, organization, definitions, changed)
- changed: str¶
The date the publishing distribution last changed it, as
pysmi.corpus.site.datessupplied it, or""where none was supplied for this module. Independent ofrevised: a module taken today carries whatever revision its publisher last set.
- class pysmi.corpus.site.SiteReport(modules: int, entities: int, arcs: int, listings: int, bytes: int, sitemaps: int = 0, crawlable: bool = False)¶
What a site build wrote.
Create new instance of SiteReport(modules, entities, arcs, listings, bytes, sitemaps, crawlable)
- class pysmi.corpus.site.Tally¶
Running counts over the module pages a build renders.
Accumulated as the pages are written rather than in a pass of its own: the figures are sums over the models the build already built, and reading the jsondoc tree again to total them is a second parse of the whole corpus for numbers that were on hand.
A tally holding nothing, which is a corpus of no modules.
- add(page: ModulePage, *, tier: str = 'vendor', revised: str = '', changed: str = '') None¶
One module’s page, counted.
- Parameters:
page – the model, as
module_page()built it.tier – the tier its namespace declared, from
tier_name().revised – the newest date the module carries, as
pysmi.corpus.site.crawl.newest()read it. A module carrying none is counted and is not listed as a recent revision, since there is no date to place it by.changed – the date the publishing distribution last changed it, as
pysmi.corpus.site.datessupplied it. Same rule: a module supplied without one is counted and is not listed under a date it does not have.
- dated: dict[str, list[Recent]]¶
Per tier, one row per module carrying a date of either kind. A module with a publisher revision and no distribution date, or the other way about, is in here once and reaches only the list it has a date for.
- overview(*, registrants: int = 0, arcs: int = 0, recent: int = 10) Overview¶
The figures as the entry point states them.
- Parameters:
registrants – enterprise arcs the corpus registers under.
arcs – nodes in the arc index.
recent – how many modules each tier’s recent list holds.
- Returns:
The overview. Tiers come out in
pysmi.corpus.namespace.TIERSorder rather than in the order the modules happened to be read, so two builds of one corpus state it the same way.
- class pysmi.corpus.site.Theme(page: str = '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>$title</title>\n<link rel="stylesheet" href="$stylesheet">\n$head</head>\n<body>\n<header class="site">\n<a class="home" href="${root}browse/">$corpus</a>\n<nav class="breadcrumb">$breadcrumb</nav>\n</header>\n<main>\n<h1>$heading</h1>\n$content</main>\n<footer class="site">\n<p>Generated by $generator.</p>\n</footer>\n</body>\n</html>\n', stylesheet: str = ':root {\n color-scheme: light dark;\n --ink: #1a1a1a;\n --dim: #595959;\n --rule: #d4d4d4;\n --ground: #ffffff;\n --raised: #f6f6f6;\n --accent: #0b5fa5;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #e8e8e8;\n --dim: #a0a0a0;\n --rule: #3a3a3a;\n --ground: #16181a;\n --raised: #1f2225;\n --accent: #6fb3ec;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n background: var(--ground);\n color: var(--ink);\n font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", Arial, sans-serif;\n}\n\na { color: var(--accent); }\n\nheader.site, footer.site, main {\n max-width: 62rem;\n margin: 0 auto;\n padding: 1rem 1.25rem;\n}\n\nheader.site {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem 1.5rem;\n align-items: baseline;\n border-bottom: 1px solid var(--rule);\n}\n\nheader.site .home { font-weight: 600; text-decoration: none; }\n\nnav.breadcrumb, nav.keys { font-size: 0.875rem; color: var(--dim); }\nnav.breadcrumb a::after { content: " /"; color: var(--rule); }\nnav.breadcrumb a:last-child::after { content: ""; }\n\nfooter.site {\n border-top: 1px solid var(--rule);\n color: var(--dim);\n font-size: 0.8125rem;\n}\n\nh1 { font-size: 1.6rem; margin: 0 0 0.25rem; overflow-wrap: anywhere; }\nh2 { font-size: 1.15rem; margin: 2rem 0 0.5rem; }\nh3 { font-size: 1rem; margin: 1.5rem 0 0.35rem; }\n\ncode, .oid, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\ncode, .oid { font-size: 0.9em; overflow-wrap: anywhere; }\n\npre {\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n padding: 0.75rem;\n overflow-x: auto;\n font-size: 0.8125rem;\n}\n\ndl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1.25rem; margin: 0.75rem 0; }\ndl.facts dt { color: var(--dim); }\ndl.facts dd { margin: 0; overflow-wrap: anywhere; }\n\ntable { border-collapse: collapse; width: 100%; margin: 0.75rem 0; font-size: 0.9375rem; }\nth, td { text-align: left; vertical-align: top; padding: 0.35rem 0.6rem 0.35rem 0; border-bottom: 1px solid var(--rule); }\nth { color: var(--dim); font-weight: 600; white-space: nowrap; }\n\n.wide { overflow-x: auto; }\n\nul.plain, ul.keys { list-style: none; padding: 0; margin: 0.5rem 0; }\nul.plain li { padding: 0.15rem 0; }\n\nul.keys { display: flex; flex-wrap: wrap; gap: 0.35rem 0.6rem; }\nul.keys li { font-size: 0.8125rem; }\nul.keys .here { font-weight: 600; }\n\nsection > :last-child { margin-bottom: 0; }\n.note { color: var(--dim); font-size: 0.9375rem; }\n\n/* The entry point\'s figures. One hero number, then a tile per count: no\n chart, because none of these is a series -- they are eight unrelated\n totals, and a bar chart of unrelated totals invites a comparison that\n means nothing. */\n.hero { margin: 0.5rem 0 1rem; }\n.hero .figure { display: block; font-size: 3rem; font-weight: 600; line-height: 1.1; }\n.hero .label { color: var(--dim); }\n\nul.kpis {\n list-style: none;\n padding: 0;\n margin: 1rem 0;\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));\n gap: 0.75rem;\n}\n\nul.kpis li {\n padding: 0.6rem 0.75rem;\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n}\n\nul.kpis .figure { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; }\nul.kpis .label { display: block; color: var(--dim); font-size: 0.8125rem; }\n\n/* Dates and counts in a column, so the eye compares them down the column\n rather than reading each row\'s digits as a word. */\n.recent td:nth-child(2), .recent td:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }\n.recent td:last-child, .recent th:last-child { text-align: right; }\n\n/* Two lists of the same modules under two different dates, one shown at a\n time. The radio a label points at is the whole mechanism -- no script, and\n the reader\'s choice survives nothing, which is right for a page they arrive\n at rather than work in. Without this stylesheet both panels show, one after\n the other under their own headings, which is the state every crawler and\n every reader with no CSS sees: the page states the same facts either way. */\n.switch > input { position: absolute; width: 1px; height: 1px; opacity: 0; }\n.switch .tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 1.25rem 0 0; }\n\n.switch .tab {\n padding: 0.4rem 0.75rem;\n border: 1px solid var(--rule);\n border-radius: 4px;\n color: var(--dim);\n cursor: pointer;\n font-size: 0.9375rem;\n}\n\n.switch .tab:hover { color: var(--ink); }\n.switch .panel { display: none; }\n.switch .panel > section:first-child > h2 { margin-top: 1rem; }\n\n/* :focus-visible on the input, styled on the label, so the tab a reader\n tabbed to is visible -- the input itself is one pixel and invisible. */\n#recent-published:focus-visible ~ .tabs .tab[for="recent-published"],\n#recent-changed:focus-visible ~ .tabs .tab[for="recent-changed"] { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n#recent-published:checked ~ .tabs .tab[for="recent-published"],\n#recent-changed:checked ~ .tabs .tab[for="recent-changed"] { background: var(--raised); border-color: var(--dim); color: var(--ink); font-weight: 600; }\n\n#recent-published:checked ~ .panel.recent-published,\n#recent-changed:checked ~ .panel.recent-changed { display: block; }\n\n.defs td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; overflow-wrap: anywhere; }\n.defs td:first-child { white-space: nowrap; }\n.defs td:last-child, .defs.typed td:nth-child(4) { font-family: inherit; font-size: inherit; }\n.defs .desc { font-family: inherit; color: var(--dim); font-size: 0.875rem; overflow-wrap: normal; }\n\n:target { background: var(--raised); }\n\n@media (max-width: 32rem) {\n dl.facts { grid-template-columns: 1fr; gap: 0.1rem; }\n dl.facts dt { margin-top: 0.5rem; }\n .hero .figure { font-size: 2.25rem; }\n}\n', corpus: str = 'MIB corpus')¶
The page frame and the stylesheet a build renders with.
Create new instance of Theme(page, stylesheet, corpus)
- page: str¶
The page template, with
PLACEHOLDERSin it.
- pysmi.corpus.site.arc_pages(found: Mapping[str, Arc], structural: Iterable[str], anchors: Mapping[str, str], children: Mapping[str, tuple[Arc, ...]] | None = None) dict[str, ArcPage]¶
One page per structural arc, each carrying its children.
- Parameters:
found – the arc names, as
pysmi.corpus.arcs.arcs()returns them.structural – the arcs that get a page, as
pysmi.corpus.pages.structural_arcs()picks them out.anchors – arc to the module registered there, so a child row can link to a module page rather than to an arc page that does not exist.
children – the child map, as
children_of()builds it. Built here when not given.
- Returns:
One
ArcPageper structural arc, keyed by arc.
- pysmi.corpus.site.build_site(directory: str, documents: Iterable[tuple[str, Mapping[str, Any], Any, Any]], *, theme: Theme | None = None, anchors: Mapping[str, str] | None = None, arcs: Mapping[str, Arc] | None = None, entities: Mapping[str, Mapping[str, Any]] | None = None, provenance: Mapping[str, Mapping[str, str]] | None = None, closure: Mapping[str, Mapping[str, Sequence[str]]] | None = None, patches: Mapping[str, tuple[tuple[tuple[str, str], ...], str]] | None = None, size: int | Mapping[str, int] | PageSizes | None = None, crawl: Crawl | None = None, changed: Changed | None = None) SiteReport¶
Render the corpus as a browsable site.
- Parameters:
directory – the site root. Written into, never read from.
documents –
(module, jsondoc, tier, rfc)per module, aspysmi.corpus.index.read_documents()yields them. The module set is whatever this gives, which is the corpus the manifest declared – a namespace a build resolves against without publishing contributes no pages.theme – the frame to render into. The built-in one otherwise.
anchors – arc to module, as
pysmi.corpus.index.anchor_index()returns it. Without it no OID tree is written: a tree whose nodes cannot say which module registered them is a tree of numbers.arcs – the arc names, as
pysmi.corpus.arcs.arcs()returns them.entities – the entity index, as
pysmi.corpus.entity.as_document()writes it. Without it no registrant pages are written.provenance – where each module came from.
closure – the load order per module.
patches – per module, the defects a patch repairs and the diff.
size – entries per bucket, as
of()takes it – one number for every list, or a mapping naming them.crawl – what this build declares about the site it publishes. Given, the whole crawl surface is written – canonical links, JSON-LD, the sitemap index,
robots.txtandllms.txt. Omitted, the pages are written without it, which is what a build producing a subtree somebody else will assemble wants. See pysnmp/pysmi#284.changed – the dates this distribution last changed each module, as
pysmi.corpus.site.dates.read_changed()read them. Given, the entry point offers them beside the publishers’ own revision dates; omitted, it shows the publishers’ alone. A module the mapping does not name keeps no such date, which is what lets a distribution supply dates for the part of the corpus it tracks and stay quiet about the rest.
- Returns:
What was written.
- pysmi.corpus.site.entity_page(arc: str, record: Mapping[str, Any]) EntityPage¶
One registrant, from its row of the entity index.
- pysmi.corpus.site.imported_by(documents: Iterable[tuple[str, Mapping[str, Any]]]) dict[str, tuple[str, ...]]¶
Which modules import each module, from the import clauses themselves.
The reverse of what every jsondoc already states, which nothing else in a corpus build computes – the closure runs the other way. A page saying “nothing in this corpus imports this” is a fact about the corpus and is worth stating.
- pysmi.corpus.site.load_theme(page: str | None = None, stylesheet: str | None = None, corpus: str | None = None) Theme¶
A theme, with either file replaced by one the manifest names.
- Parameters:
page – a file holding a replacement page template, or
Nonefor the built-in one.stylesheet – a file holding a replacement stylesheet, likewise.
corpus – what the header calls this corpus.
- Returns:
The theme.
- Raises:
PySmiError – a named file could not be read. A theme that silently fell back to the default would publish a whole site in the wrong skin and say nothing.
- pysmi.corpus.site.module_page(module: str, document: Mapping[str, Any], *, anchors: Iterable[str] | None = None, provenance: Mapping[str, str] | None = None, importedBy: Iterable[str] | None = None, closure: Iterable[str] | None = None, missing: Iterable[str] | None = None, defects: Iterable[tuple[str, str]] | None = None, patch: str = '', under: Iterable[Arc] | None = None) ModulePage¶
Everything the corpus holds about one module, as its page states it.
- Parameters:
module – the module name.
document – its jsondoc, as
JsonCodeGenemits it. Read for everything the module itself says; a build that carried texts gives a page with prose on it and one that did not gives the same page without.anchors – the arcs this module registers at, as
anchor_index()picks them out.provenance – its row of the provenance table.
importedBy – the modules in this corpus that import it.
closure – the files needed to load it, and missing what is not held.
defects – what a patch on it repairs, as
(identifier, url).patch – the patch itself, as a unified diff.
under – the arcs directly below its registrations, so the tree does not stop at a module page.
- Returns:
The page model.
- pysmi.corpus.site.tier_name(rank: int) str¶
What a tier rank is called, as a namespace declares it.
read_documentsyields the rank rather than the name, since ranking is what the index wants it for. A page states the name.
The page models¶
What each page of the site states, read off the corpus the build produced.
Separate from the rendering so that what a page says can be tested without parsing HTML, and so that a distribution replacing the templates is replacing presentation rather than deciding which facts survive.
Every page is a projection of artifacts the build already has – the jsondoc tree, the import closure, the entity index, the arc names, the provenance table. Nothing here re-reads a MIB or recompiles anything.
See pysnmp/pysmi#276.
- class pysmi.corpus.site.model.ArcPage(arc: str, name: str, source: str, reference: str, children: tuple[Arc, ...], module: str = '')¶
One node of the registration tree.
Create new instance of ArcPage(arc, name, source, reference, children, module)
- pysmi.corpus.site.model.CONFORMANCE: Final[tuple[str, ...]] = ('objectgroup', 'notificationgroup', 'modulecompliance', 'agentcapabilities')¶
jsondoc classes carrying a module’s conformance statements.
- pysmi.corpus.site.model.DEFINED: Final[tuple[str, ...]] = ('objectidentity', 'objecttype', 'notificationtype')¶
jsondoc classes that are a module’s own definitions rather than its structure, in the order a page renders them.
- class pysmi.corpus.site.model.Definition(name: str, oid: str, kind: str, syntax: str, access: str, status: str, description: str)¶
One thing a module defines, as its row on the module page.
Create new instance of Definition(name, oid, kind, syntax, access, status, description)
- class pysmi.corpus.site.model.EntityPage(number: int, arc: str, organization: str, contacts: tuple[Mapping[str, str], ...], modules: tuple[str, ...])¶
One registrant, and what the corpus holds under its arc.
Create new instance of EntityPage(number, arc, organization, contacts, modules)
- pysmi.corpus.site.model.KIND: Final[dict[str, str]] = {'agentcapabilities': 'AGENT-CAPABILITIES', 'modulecompliance': 'MODULE-COMPLIANCE', 'moduleidentity': 'MODULE-IDENTITY', 'notificationgroup': 'NOTIFICATION-GROUP', 'notificationtype': 'NOTIFICATION-TYPE', 'objectgroup': 'OBJECT-GROUP', 'objectidentity': 'OBJECT-IDENTITY', 'objecttype': 'OBJECT-TYPE', 'textualconvention': 'TEXTUAL-CONVENTION', 'type': 'type'}¶
What a jsondoc class is called on a page.
- class pysmi.corpus.site.model.ModulePage(module: str, anchors: tuple[str, ...], organization: str, contact: str, lastupdated: str, revisions: tuple[str, ...], description: str, provenance: Mapping[str, str], imports: Mapping[str, tuple[str, ...]], importedBy: tuple[str, ...], closure: tuple[str, ...], missing: tuple[str, ...], under: tuple[Arc, ...], defects: tuple[tuple[str, str], ...], patch: str, types: tuple[Definition, ...], objects: tuple[Definition, ...], notifications: tuple[Definition, ...], conformance: tuple[Definition, ...])¶
One module: everything the corpus holds about it.
Create new instance of ModulePage(module, anchors, organization, contact, lastupdated, revisions, description, provenance, imports, importedBy, closure, missing, under, defects, patch, types, objects, notifications, conformance)
- anchors: tuple[str, ...]¶
The arcs this module registers at, ascending. Empty for a module that registers nothing, such as a TEXTUAL-CONVENTIONS-only module.
- conformance: tuple[Definition, ...]¶
Alias for field number 18
- notifications: tuple[Definition, ...]¶
Alias for field number 17
- objects: tuple[Definition, ...]¶
Alias for field number 16
- types: tuple[Definition, ...]¶
Alias for field number 15
- under: tuple[Arc, ...]¶
The arcs directly below this module’s own registrations, which is how the tree carries on past it. A module’s anchor gets no arc page of its own – that arc is the module – so without this the structural arcs under it are reachable from nothing. See pysnmp/pysmi#284 on orphans.
- class pysmi.corpus.site.model.Overview(modules: int, tiers: Mapping[str, int], registrants: int, arcs: int, objects: int, notifications: int, types: int, repaired: int, incomplete: int, revised: str, latest: Mapping[str, tuple[Recent, ...]], latestChanged: Mapping[str, tuple[Recent, ...]] = {}, datedChanged: Mapping[str, int] = {})¶
What the corpus holds, as the entry point states it.
Every figure is something the build computed while writing the pages. A front page that asked a question the build had not already answered would be a second pass over the corpus for the sake of a headline.
Create new instance of Overview(modules, tiers, registrants, arcs, objects, notifications, types, repaired, incomplete, revised, latest, latestChanged, datedChanged)
- datedChanged: Mapping[str, int]¶
Per tier, how many modules the distribution supplied a date for, which is the pool
latestChangedis the newest few of. Nottiers: a distribution may date a fraction of a tier, and “the ten most recent of the 6,982 it tracks” and “of the 6,982 this corpus holds” are different claims.
- latest: Mapping[str, tuple[Recent, ...]]¶
Per tier, the most recently revised modules, newest first. Same keys and same order as
tiers, a tier none of whose modules carries a readable date aside.
- latestChanged: Mapping[str, tuple[Recent, ...]]¶
Per tier, the modules the distribution changed most recently, newest first. Empty for a build given no dates, and a tier whose modules were all supplied without one is left out – which is what makes this list honest on a corpus whose standard tier comes from somewhere the distribution does not track.
- registrants: int¶
Enterprise arcs the corpus registers under, or 0 for a build that wrote no registrant pages.
- tiers: Mapping[str, int]¶
Modules per tier, in
pysmi.corpus.namespace.TIERSorder and holding only the tiers this corpus has modules in.
- pysmi.corpus.site.model.RECENT: Final[int] = 10¶
How many recently revised modules the front page lists per tier.
The list answers “what moved lately”, which a handful of rows answers and a longer one does not: past the first few the reader is reading an arbitrary slice of a corpus whose modules were nearly all revised on some date or other. The whole corpus stays one hop away through the range keys.
- class pysmi.corpus.site.model.Recent(module: str, revised: str, organization: str, definitions: int, changed: str = '')¶
One module’s row in a recently revised list.
Create new instance of Recent(module, revised, organization, definitions, changed)
- changed: str¶
The date the publishing distribution last changed it, as
pysmi.corpus.site.datessupplied it, or""where none was supplied for this module. Independent ofrevised: a module taken today carries whatever revision its publisher last set.
- pysmi.corpus.site.model.TYPES: Final[tuple[str, ...]] = ('textualconvention', 'type')¶
jsondoc classes for a type a module defines.
textualconventionis what a TEXTUAL-CONVENTION comes out as;typeis a plain type assignment, which is rarer and belongs in the same table.
- class pysmi.corpus.site.model.Tally¶
Running counts over the module pages a build renders.
Accumulated as the pages are written rather than in a pass of its own: the figures are sums over the models the build already built, and reading the jsondoc tree again to total them is a second parse of the whole corpus for numbers that were on hand.
A tally holding nothing, which is a corpus of no modules.
- add(page: ModulePage, *, tier: str = 'vendor', revised: str = '', changed: str = '') None¶
One module’s page, counted.
- Parameters:
page – the model, as
module_page()built it.tier – the tier its namespace declared, from
tier_name().revised – the newest date the module carries, as
pysmi.corpus.site.crawl.newest()read it. A module carrying none is counted and is not listed as a recent revision, since there is no date to place it by.changed – the date the publishing distribution last changed it, as
pysmi.corpus.site.datessupplied it. Same rule: a module supplied without one is counted and is not listed under a date it does not have.
- dated: dict[str, list[Recent]]¶
Per tier, one row per module carrying a date of either kind. A module with a publisher revision and no distribution date, or the other way about, is in here once and reaches only the list it has a date for.
- overview(*, registrants: int = 0, arcs: int = 0, recent: int = 10) Overview¶
The figures as the entry point states them.
- Parameters:
registrants – enterprise arcs the corpus registers under.
arcs – nodes in the arc index.
recent – how many modules each tier’s recent list holds.
- Returns:
The overview. Tiers come out in
pysmi.corpus.namespace.TIERSorder rather than in the order the modules happened to be read, so two builds of one corpus state it the same way.
- pysmi.corpus.site.model.arc_pages(found: Mapping[str, Arc], structural: Iterable[str], anchors: Mapping[str, str], children: Mapping[str, tuple[Arc, ...]] | None = None) dict[str, ArcPage]¶
One page per structural arc, each carrying its children.
- Parameters:
found – the arc names, as
pysmi.corpus.arcs.arcs()returns them.structural – the arcs that get a page, as
pysmi.corpus.pages.structural_arcs()picks them out.anchors – arc to the module registered there, so a child row can link to a module page rather than to an arc page that does not exist.
children – the child map, as
children_of()builds it. Built here when not given.
- Returns:
One
ArcPageper structural arc, keyed by arc.
- pysmi.corpus.site.model.children_of(found: Mapping[str, Arc]) dict[str, tuple[Arc, ...]]¶
The arcs directly below each arc, in tree order.
One pass over the arc index, since both the OID tree’s pages and the module pages need it and walking every arc twice to build the same map is the kind of cost that only shows at corpus scale.
- pysmi.corpus.site.model.entity_page(arc: str, record: Mapping[str, Any]) EntityPage¶
One registrant, from its row of the entity index.
- pysmi.corpus.site.model.imported_by(documents: Iterable[tuple[str, Mapping[str, Any]]]) dict[str, tuple[str, ...]]¶
Which modules import each module, from the import clauses themselves.
The reverse of what every jsondoc already states, which nothing else in a corpus build computes – the closure runs the other way. A page saying “nothing in this corpus imports this” is a fact about the corpus and is worth stating.
- pysmi.corpus.site.model.module_page(module: str, document: Mapping[str, Any], *, anchors: Iterable[str] | None = None, provenance: Mapping[str, str] | None = None, importedBy: Iterable[str] | None = None, closure: Iterable[str] | None = None, missing: Iterable[str] | None = None, defects: Iterable[tuple[str, str]] | None = None, patch: str = '', under: Iterable[Arc] | None = None) ModulePage¶
Everything the corpus holds about one module, as its page states it.
- Parameters:
module – the module name.
document – its jsondoc, as
JsonCodeGenemits it. Read for everything the module itself says; a build that carried texts gives a page with prose on it and one that did not gives the same page without.anchors – the arcs this module registers at, as
anchor_index()picks them out.provenance – its row of the provenance table.
importedBy – the modules in this corpus that import it.
closure – the files needed to load it, and missing what is not held.
defects – what a patch on it repairs, as
(identifier, url).patch – the patch itself, as a unified diff.
under – the arcs directly below its registrations, so the tree does not stop at a module page.
- Returns:
The page model.
Rendering¶
Turning a page model into the HTML that is served.
Everything is in the bytes. No page here fetches anything: not the object table, not the description, not the navigation. pysnmp/pysmi#284 measured the alternative and it costs the same, while being unreadable to the AI crawlers that do not run JavaScript – to them, a client-rendered object table is a page about a MIB module that does not say what the module defines.
URLs are relative to the site root, which every page carries as root –
../../ and so on. A site published under /mibs/ on a project host works
without being told where it lives, and so does one opened from a local
directory.
See pysnmp/pysmi#276.
- pysmi.corpus.site.render.CHANGED: Final = ({}, {}, 'changed', 'Changed', 'Updated here', 'changed', 'the day this distribution last changed the module, which is not a date the module itself carries', 'distribution tracks', 'recent-changed')¶
The axis a distribution supplies. See
pysmi.corpus.site.dates.
- pysmi.corpus.site.render.PUBLISHED: Final = ({}, {}, 'revised', 'Revised', 'Revised by publisher', 'revised', 'the one the module itself carries', 'corpus holds', 'recent-published')¶
The axis every corpus has. The dates are the publishers’ own, so this is what the industry did rather than what this distribution did.
- pysmi.corpus.site.render.REVERSE: Final = 50¶
How many reverse dependencies a module page lists before it stops and gives a count instead.
This is the one list on a module page with no natural bound. Nearly every module in a corpus imports
SNMPv2-SMI, and a large fraction importIF-MIB, so an unbounded list runs to an order of magnitude more bytes than the page’s own content – a page about a base module that is mostly a list of everything else. The count is the fact worth stating; the names past the first few are not.
- pysmi.corpus.site.render.TIER_RECENT: Final[dict[str, str]] = {'draft': 'Internet-Draft', 'standard': 'standard', 'vendor': 'vendor'}¶
What a tier is called inside the heading of its recent-revisions table.
- pysmi.corpus.site.render.TIER_TILE: Final[dict[str, str]] = {'draft': 'Internet-Draft modules', 'standard': 'Standard modules', 'vendor': 'Vendor modules'}¶
What a tier’s module-count tile is called.
- pysmi.corpus.site.render.arc_html(page: ArcPage, theme: Theme, *, anchors: Mapping[str, str] | None = None, head: str = '') str¶
One node of the registration tree, and its children.
- pysmi.corpus.site.render.entity_html(page: EntityPage, theme: Theme, *, buckets: Sequence[Bucket] = (), here: str = '', head: str = '') str¶
One registrant’s page, or one bucket of a long module list.
- pysmi.corpus.site.render.entity_list(root: str, pages: Iterable[EntityPage]) str¶
The entries of the registrant list.
- pysmi.corpus.site.render.figure(count: int) str¶
A count as a page writes it, grouped so the magnitude is readable.
- pysmi.corpus.site.render.key_strip(root: str, found: Sequence[Bucket], here: str, base: str) str¶
Every bucket of a list, on every page of that list.
So any bucket is one hop from any other and crawl depth does not grow with the corpus (pysnmp/pysmi#284). The rendered label is clipped; the URL keeps the whole key.
- pysmi.corpus.site.render.kpis(overview: Overview) str¶
The corpus in figures.
Each tile is a count the build already had. A tile whose count is zero is left out rather than rendered as
0: a build given no arc names holds no opinion about how many arcs there are, and “0 registrants” states one.
- pysmi.corpus.site.render.listing_html(theme: Theme, *, heading: str, intro: str, entries: str, base: str, buckets: Sequence[Bucket] = (), here: str = '', depth: int = 1, head: str = '', summary: str = '', listing: str = '') str¶
A list page: the browse index, the registrant list, or one bucket of either.
- Parameters:
theme – the frame to render into.
heading – the page’s own heading, which is also its breadcrumb.
intro – one line saying what the list holds, or
"".entries – the list itself, already rendered.
base – where this list lives under the site root, for the key strip.
buckets – every bucket of the list, so any one is one hop from any other.
here – the bucket this page is, or
""for the list’s own root.depth – how many directories down the page sits, for relative URLs.
head – extra
<head>markup.summary – markup between the intro and the list – what
overview_html()renders for the entry point. Empty on a bucket page, which is a slice of a list rather than a front page.listing – a heading over the list itself. A page carrying a summary needs one: the list is then the last of several sections rather than the whole page, and without a heading it reads as a continuation of the section above it.
- pysmi.corpus.site.render.module_html(page: ModulePage, theme: Theme, *, held: Iterable[str] = (), anchors: Mapping[str, str] | None = None, head: str = '') str¶
One module’s page.
- Parameters:
page – the model, as
module_page()builds it.theme – the frame to render into.
held – the modules this corpus holds, so an import is linked only where the link would resolve.
anchors – arc to module, so an arc below this one that is itself a module links to that module’s page.
head – extra
<head>markup, which pysnmp/pysmi#284 fills in.
- pysmi.corpus.site.render.module_list(root: str, modules: Iterable[str]) str¶
The entries of a module list page.
- pysmi.corpus.site.render.overview_html(root: str, overview: Overview, *, label: str = '') str¶
The entry point’s figures, and what the corpus changed most recently.
One list of recent modules per tier, ordered by the date their publishers last revised them. A build given a distribution’s own dates – see
pysmi.corpus.site.dates– offers those as a second tab, since “what has this distribution done lately” is a question the publishers’ dates cannot answer.- Parameters:
root – the prefix that reaches the site root, since the module links in the recent tables are relative like every other link here.
overview – the figures, as
pysmi.corpus.site.model.Tally.overview()totalled them.label – what to call the distribution’s own dates, from
site.changed.label.pysmi.corpus.site.dates.LABELotherwise.
- Returns:
Markup for the entry point, or
""for a corpus of no modules.
- pysmi.corpus.site.render.recent_list(root: str, found: Sequence[Recent], *, dated: str = 'revised', column: str = 'Revised') str¶
The most recently dated modules of one tier, newest first.
- Parameters:
root – the prefix that reaches the site root.
found – the rows, already ordered, as
pysmi.corpus.site.model.Tally.overview()ordered them.dated – which of
Recent’s dates the rows are ordered by, and so which one the table prints.column – what to call that date’s column.
- pysmi.corpus.site.render.root_for(depth: int) str¶
The prefix that reaches the site root from depth directories down.
Writing HTML without a template engine.
PySMI’s runtime dependencies are ply and requests. A site generator is
not a reason to add a third: everything here is a handful of stdlib string
operations, and a corpus of thousands of modules renders in seconds without
one.
What matters at corpus scale is that nothing reaches a page unescaped.
Every MIB in the corpus is third-party text – a DESCRIPTION is whatever a
vendor typed, and CISCO-LWAPP-TC-MIB has one containing <. The one
rule here is that text() is the only way a value becomes markup, and
the element builders call it for you.
See pysnmp/pysmi#276.
- pysmi.corpus.site.html.DOCTYPE: Final = '<!doctype html>'¶
Written ahead of every page, so a browser reads it as HTML5.
- pysmi.corpus.site.html.VOID: Final = frozenset({'br', 'hr', 'img', 'input', 'link', 'meta'})¶
Elements that take no closing tag.
</input>is not merely redundant – it is a parse error, and a page that carries one is a page whose markup a validator refuses.
- pysmi.corpus.site.html.attributes(pairs: Mapping[str, object]) str¶
Attributes as they are written inside a start tag.
An attribute whose value is
NoneorFalseis left out entirely, which is what lets a caller pass an optional one without a conditional.Truerenders as a bare attribute.A trailing underscore is dropped, so
class_andfor_reach the page asclassandfor– both are Python keywords and neither can be written as a keyword argument. Underscores inside a name become hyphens, sodata_oidisdata-oid.
- pysmi.corpus.site.html.definitions(pairs: Iterable[tuple[object, str]], /, **attrs: object) str¶
A description list. Terms are text, values are markup.
A pair whose value is empty is left out: the corpus not holding a fact is different from it holding an empty one, and a blank row says the second.
- pysmi.corpus.site.html.element(name: str, content: object = '', /, **pairs: object) str¶
One element holding content as text rather than as markup.
- pysmi.corpus.site.html.join(parts: Iterable[str]) str¶
Markup fragments, one per line, so the source is readable.
- pysmi.corpus.site.html.link(href: str, label: object, /, **pairs: object) str¶
An anchor, with label as text.
- pysmi.corpus.site.html.paragraphs(prose: str) str¶
A MIB’s prose as paragraphs, blank lines separating them.
A DESCRIPTION arrives normalised –
JsonCodeGencollapses the publisher’s line breaks unlesskeepTextsLayoutis set – but the blank lines between paragraphs survive, and they are the only structure the text has. Everything is escaped: this is vendor text, not markup.
- pysmi.corpus.site.html.table(headings: Iterable[object], rows: Iterable[Iterable[str]], /, *, anchors: Iterable[object] = (), **pairs: object) str¶
A table whose cells are already markup.
Headings are text; cells are not, because a cell routinely holds a link or a code span. The caller escapes what it puts in one, which the builders above do.
anchors gives each row an
id, in row order, so a row is something a URL can point at. Shorter than anchoring an element inside the row, and one anchor per row is what a deep link wants: a definition, not a cell. Rows past the end of it get none.Cells are joined with nothing between them. Every other builder here writes one fragment per line, because a person reads the source; the definition tables of a corpus run to hundreds of thousands of rows, and a newline per cell is megabytes of them. The rows stay one per line, which is the granularity anyone reading the source is looking at anyway.
- pysmi.corpus.site.html.tag(name: str, content: str = '', /, **pairs: object) str¶
One element, its attributes escaped and its content taken as markup.
content is markup rather than text, since an element holds other elements. Anything that is not already markup goes through
text()first –element()is the shorthand for that.
- pysmi.corpus.site.html.text(value: object) str¶
One value, escaped for a text node or an attribute.
Quotes included, so the same function serves both and a caller cannot pick the wrong one.
Nonerenders as the empty string rather than as"None": a fact the corpus does not hold is absent, not the word.
The crawl surface¶
The crawl surface: head metadata, JSON-LD, sitemap, robots.txt, llms.txt.
Written only when the manifest says this build is producing a distribution
site rather than a subtree somebody else will assemble – which is what
base_url declares. Without it there is nothing to put in a canonical link
or a sitemap entry, and guessing an origin would publish a site claiming to
live somewhere it does not.
Why the generator emits robots.txt¶
It is policy, and policy is configuration, which a manifest carries perfectly
well. Making every downstream distribution hand-write a robots.txt that
correctly excludes its own asn1/ and json/ trees from search crawlers
while admitting agents is asking each of them to re-derive what the generator
already knows: which paths it wrote, what is a page and what is raw data, and
where the sitemap is.
What the generator cannot do is decide where the file lands. On GitHub Pages a
project site cannot serve its own robots.txt – crawlers read it only
from the host root, which belongs to a different repository. So PySMI produces
the file and the deployment places it.
Why lastmod is a module’s own revision date¶
A corpus rebuilt on every push that stamps every page with today’s date
teaches a crawler the field is noise. The date a module page’s content last
changed is the date the module last changed, which the module states itself
in LAST-UPDATED and its REVISION clauses. A page listing modules takes
the newest date among them.
So no build clock reaches a sitemap, and two builds of one corpus produce the same one – which is the same property every other corpus artifact has.
See pysnmp/pysmi#284.
- pysmi.corpus.site.crawl.ARTIFACTS: Final[dict[str, str]] = {'arcs': '/arcs.json', 'asn1': '/asn1/', 'closure': '/closure.json', 'core-db': '/core.db', 'entity-index': '/entity.json', 'index': '/index.csv', 'index-v2': '/index-v2.csv', 'json': '/json/', 'notexts': '/notexts/', 'report': '/report.json', 'site': '/', 'standard': '/standard.txt', 'texts': '/texts/'}¶
The artifacts a crawl policy may name, mapped to the path each one occupies under the published root.
A policy names artifacts rather than paths because the paths are the generator’s to choose: a distribution saying “keep search engines out of the raw JSON” should not have to know whether that tree is called
jsonor something else this release renamed it to.
- class pysmi.corpus.site.crawl.Crawl(base: str, description: str = '', data: str = '', policy: Mapping[str, Mapping[str, Sequence[str]]] = {})¶
What a distribution build declares about the site it is publishing.
Create new instance of Crawl(base, description, data, policy)
- base: str¶
The site’s origin and path, without a trailing slash –
https://mibsdepot.com. Every canonical link and sitemap entry is built from it. This is what makes a build a distribution site.
- data: str¶
Where the bulk artifacts are, when they are not on
base.A distribution may serve its pages and its downloads from two origins, and the pysnmp corpus does: pages at
https://mibsdepot.com, files athttps://data.mibsdepot.com. Pages resolve a directory URL to itsindex.htmland object storage does not, which is what splits them.Only the
llms.txtbulk links use this. A canonical link, a sitemap entry and the sitemap index are statements about a page, and a page is always onbase. Empty means one origin serves both.
- data_url(path: str) str¶
The absolute URL of a bulk artifact.
datawhere a distribution declares one, andbaseotherwise, so a site on one origin is unaffected by the distinction.
- policy: Mapping[str, Mapping[str, Sequence[str]]]¶
Per user-agent, which artifacts to allow and disallow, keyed
allowanddisallowand namingARTIFACTS.*is the catch-all agent.A search engine and an agent want opposite policies over the same tree: to the first, the hundreds of megabytes of
asn1/andjson/are crawl budget spent on files with no indexing value; to the second they are the point. One policy cannot serve both, which is why this is per-agent and why the agent list is configuration – crawler names change faster than releases.
- pysmi.corpus.site.crawl.PER_SITEMAP: Final = 50000¶
URLs per sitemap file. The protocol’s own limit is 50,000 and 50 MB, and this build is nowhere near either – but a downstream corpus may be, and exceeding it fails silently, so the index is emitted regardless and each tree gets its own file.
- pysmi.corpus.site.crawl.UNDATED: Final = ''¶
What a page’s
lastmodfalls back to when nothing it describes carries a readable date. Absent rather than invented: a sitemap entry with nolastmodsays “I do not know”, which is true, where one carrying the build date says something false.
- pysmi.corpus.site.crawl.clip(prose: str, width: int = 300) str¶
One line of prose, for a meta description.
Whitespace collapsed, because a DESCRIPTION arrives with the publisher’s line breaks in it and a meta tag is one line.
- pysmi.corpus.site.crawl.collection_jsonld(name: str, url: str, *, description: str = '', items: int = 0) dict[str, Any]¶
A list page as structured data: a collection, and how big it is.
- pysmi.corpus.site.crawl.head(*, title: str, description: str = '', canonical: str = '', jsonld: Mapping[str, Any] | None = None) str¶
The
<head>markup a page carries beyond its title and stylesheet.- Parameters:
title – the page’s own title, which the frame renders separately; this is here only for the JSON-LD.
description – one line for
meta name="description", clipped by the caller.canonical – the page’s absolute URL.
jsonld – the structured description of what the page is about.
- Returns:
Markup ending in a newline, or
""when there is nothing to say.
- pysmi.corpus.site.crawl.llms(crawl: Crawl, sections: Iterable[tuple[str, Sequence[tuple[str, str]]]], name: str = 'MIB corpus') str¶
llms.txt: what this corpus is, and where the data actually is.A convention rather than a mechanism – no major AI crawler is documented as consuming it. It is emitted, and nothing depends on it. What reaches an AI crawler is server-rendered HTML and a
robots.txtthat admits it.- Parameters:
crawl – the site’s declaration.
sections –
(heading, [(label, url), ...])in the order they are written.name – what the corpus is called, as the title.
- pysmi.corpus.site.crawl.module_jsonld(module: str, url: str, *, oid: str = '', organization: str = '', revised: str = '', description: str = '', terms: Iterable[tuple[str, str]] | None = None, corpus: str = '') dict[str, Any]¶
A MIB module as structured data.
A module is a published artifact with an identifier, a publisher and a revision date, which defines a vocabulary of named terms. That is
DefinedTermSetcrossed withDataset, and schema.org allows both types on one node.The identifiers are the OIDs, which is the whole point: an OID is a globally unique identifier that already exists, and a consumer holding one should be able to match it without reading prose.
- Parameters:
module – the module name, which is its
name.url – the page’s absolute URL.
oid – the arc the module registers at, as its own identifier.
organization – the ORGANIZATION clause, as the publisher.
revised – the newest revision, as
YYYY-MM-DD.description – the module’s DESCRIPTION.
terms –
(name, oid)per thing the module defines. Names and identifiers only, and noinDefinedTermSetback-reference – nesting underhasDefinedTermalready says a term belongs to this set, and the descriptions are in the HTML this sits in. Both were in the first cut and between them they were 148 bytes per term, which over a corpus’s definitions is hundreds of megabytes of duplication.corpus – what the corpus is called, as the containing collection.
- Returns:
The JSON-LD node.
- pysmi.corpus.site.crawl.newest(dates: Iterable[str]) str¶
The latest of the dates given, as
YYYY-MM-DD.Anything that is not a date is dropped rather than ranked, for the reason
pysmi.corpus.index.read_stamp()gives:HPR-MIBcarries970514000000Z– year 9705, month 14 – and comparing it as a number would make it the newest date there will ever be.
- pysmi.corpus.site.crawl.robots(crawl: Crawl, sitemaps: Iterable[str]) str¶
robots.txt, from the policy the manifest declares.An artifact the policy names that this build did not produce is skipped rather than written: a rule about a tree that is not there is a rule nobody can act on, and it would read as though the tree existed.
Theming¶
The frame every page is written into, and how a distribution replaces it.
A downstream distribution publishes this site beside its own documentation and
wants it to look like the rest of that documentation. Forking the generator to
change a header is the outcome to avoid, so the frame is data: a
Theme carries the page template and the stylesheet, and a manifest
names files that replace either.
string.Template rather than a template engine, because PySMI’s
runtime dependencies are ply and requests and a page frame is not a
reason for a third. $name substitution is all a frame needs, and
safe_substitute means an unknown placeholder in an overridden template
renders as itself rather than raising in the middle of a build of thousands of
pages.
See pysnmp/pysmi#276.
- pysmi.corpus.site.theme.PAGE: Final = '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>$title</title>\n<link rel="stylesheet" href="$stylesheet">\n$head</head>\n<body>\n<header class="site">\n<a class="home" href="${root}browse/">$corpus</a>\n<nav class="breadcrumb">$breadcrumb</nav>\n</header>\n<main>\n<h1>$heading</h1>\n$content</main>\n<footer class="site">\n<p>Generated by $generator.</p>\n</footer>\n</body>\n</html>\n'¶
The default page template – one file, no scripts, nothing fetched. Every fact the page states is in the bytes the server sent, which is what pysnmp/pysmi#284 requires of it.
- pysmi.corpus.site.theme.PLACEHOLDERS: Final[tuple[str, ...]] = ('title', 'head', 'root', 'stylesheet', 'heading', 'breadcrumb', 'content', 'corpus', 'generator')¶
The placeholders a page template may use.
headis where the crawl metadata a distribution build asks for goes – see pysnmp/pysmi#284 – and is empty otherwise.
- pysmi.corpus.site.theme.STYLESHEET: Final = ':root {\n color-scheme: light dark;\n --ink: #1a1a1a;\n --dim: #595959;\n --rule: #d4d4d4;\n --ground: #ffffff;\n --raised: #f6f6f6;\n --accent: #0b5fa5;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #e8e8e8;\n --dim: #a0a0a0;\n --rule: #3a3a3a;\n --ground: #16181a;\n --raised: #1f2225;\n --accent: #6fb3ec;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n background: var(--ground);\n color: var(--ink);\n font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", Arial, sans-serif;\n}\n\na { color: var(--accent); }\n\nheader.site, footer.site, main {\n max-width: 62rem;\n margin: 0 auto;\n padding: 1rem 1.25rem;\n}\n\nheader.site {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem 1.5rem;\n align-items: baseline;\n border-bottom: 1px solid var(--rule);\n}\n\nheader.site .home { font-weight: 600; text-decoration: none; }\n\nnav.breadcrumb, nav.keys { font-size: 0.875rem; color: var(--dim); }\nnav.breadcrumb a::after { content: " /"; color: var(--rule); }\nnav.breadcrumb a:last-child::after { content: ""; }\n\nfooter.site {\n border-top: 1px solid var(--rule);\n color: var(--dim);\n font-size: 0.8125rem;\n}\n\nh1 { font-size: 1.6rem; margin: 0 0 0.25rem; overflow-wrap: anywhere; }\nh2 { font-size: 1.15rem; margin: 2rem 0 0.5rem; }\nh3 { font-size: 1rem; margin: 1.5rem 0 0.35rem; }\n\ncode, .oid, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\ncode, .oid { font-size: 0.9em; overflow-wrap: anywhere; }\n\npre {\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n padding: 0.75rem;\n overflow-x: auto;\n font-size: 0.8125rem;\n}\n\ndl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1.25rem; margin: 0.75rem 0; }\ndl.facts dt { color: var(--dim); }\ndl.facts dd { margin: 0; overflow-wrap: anywhere; }\n\ntable { border-collapse: collapse; width: 100%; margin: 0.75rem 0; font-size: 0.9375rem; }\nth, td { text-align: left; vertical-align: top; padding: 0.35rem 0.6rem 0.35rem 0; border-bottom: 1px solid var(--rule); }\nth { color: var(--dim); font-weight: 600; white-space: nowrap; }\n\n.wide { overflow-x: auto; }\n\nul.plain, ul.keys { list-style: none; padding: 0; margin: 0.5rem 0; }\nul.plain li { padding: 0.15rem 0; }\n\nul.keys { display: flex; flex-wrap: wrap; gap: 0.35rem 0.6rem; }\nul.keys li { font-size: 0.8125rem; }\nul.keys .here { font-weight: 600; }\n\nsection > :last-child { margin-bottom: 0; }\n.note { color: var(--dim); font-size: 0.9375rem; }\n\n/* The entry point\'s figures. One hero number, then a tile per count: no\n chart, because none of these is a series -- they are eight unrelated\n totals, and a bar chart of unrelated totals invites a comparison that\n means nothing. */\n.hero { margin: 0.5rem 0 1rem; }\n.hero .figure { display: block; font-size: 3rem; font-weight: 600; line-height: 1.1; }\n.hero .label { color: var(--dim); }\n\nul.kpis {\n list-style: none;\n padding: 0;\n margin: 1rem 0;\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));\n gap: 0.75rem;\n}\n\nul.kpis li {\n padding: 0.6rem 0.75rem;\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n}\n\nul.kpis .figure { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; }\nul.kpis .label { display: block; color: var(--dim); font-size: 0.8125rem; }\n\n/* Dates and counts in a column, so the eye compares them down the column\n rather than reading each row\'s digits as a word. */\n.recent td:nth-child(2), .recent td:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }\n.recent td:last-child, .recent th:last-child { text-align: right; }\n\n/* Two lists of the same modules under two different dates, one shown at a\n time. The radio a label points at is the whole mechanism -- no script, and\n the reader\'s choice survives nothing, which is right for a page they arrive\n at rather than work in. Without this stylesheet both panels show, one after\n the other under their own headings, which is the state every crawler and\n every reader with no CSS sees: the page states the same facts either way. */\n.switch > input { position: absolute; width: 1px; height: 1px; opacity: 0; }\n.switch .tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 1.25rem 0 0; }\n\n.switch .tab {\n padding: 0.4rem 0.75rem;\n border: 1px solid var(--rule);\n border-radius: 4px;\n color: var(--dim);\n cursor: pointer;\n font-size: 0.9375rem;\n}\n\n.switch .tab:hover { color: var(--ink); }\n.switch .panel { display: none; }\n.switch .panel > section:first-child > h2 { margin-top: 1rem; }\n\n/* :focus-visible on the input, styled on the label, so the tab a reader\n tabbed to is visible -- the input itself is one pixel and invisible. */\n#recent-published:focus-visible ~ .tabs .tab[for="recent-published"],\n#recent-changed:focus-visible ~ .tabs .tab[for="recent-changed"] { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n#recent-published:checked ~ .tabs .tab[for="recent-published"],\n#recent-changed:checked ~ .tabs .tab[for="recent-changed"] { background: var(--raised); border-color: var(--dim); color: var(--ink); font-weight: 600; }\n\n#recent-published:checked ~ .panel.recent-published,\n#recent-changed:checked ~ .panel.recent-changed { display: block; }\n\n.defs td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; overflow-wrap: anywhere; }\n.defs td:first-child { white-space: nowrap; }\n.defs td:last-child, .defs.typed td:nth-child(4) { font-family: inherit; font-size: inherit; }\n.defs .desc { font-family: inherit; color: var(--dim); font-size: 0.875rem; overflow-wrap: normal; }\n\n:target { background: var(--raised); }\n\n@media (max-width: 32rem) {\n dl.facts { grid-template-columns: 1fr; gap: 0.1rem; }\n dl.facts dt { margin-top: 0.5rem; }\n .hero .figure { font-size: 2.25rem; }\n}\n'¶
The default stylesheet – deliberately small and unopinionated. It sets type, spacing and table rules and nothing else, so a distribution replacing it is replacing a page’s appearance rather than fighting one.
No web fonts and no external anything – a page that fetches a stylesheet from a third party is a page that renders differently once that third party is unreachable, and a corpus is published to be durable.
- class pysmi.corpus.site.theme.Theme(page: str = '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n<title>$title</title>\n<link rel="stylesheet" href="$stylesheet">\n$head</head>\n<body>\n<header class="site">\n<a class="home" href="${root}browse/">$corpus</a>\n<nav class="breadcrumb">$breadcrumb</nav>\n</header>\n<main>\n<h1>$heading</h1>\n$content</main>\n<footer class="site">\n<p>Generated by $generator.</p>\n</footer>\n</body>\n</html>\n', stylesheet: str = ':root {\n color-scheme: light dark;\n --ink: #1a1a1a;\n --dim: #595959;\n --rule: #d4d4d4;\n --ground: #ffffff;\n --raised: #f6f6f6;\n --accent: #0b5fa5;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n --ink: #e8e8e8;\n --dim: #a0a0a0;\n --rule: #3a3a3a;\n --ground: #16181a;\n --raised: #1f2225;\n --accent: #6fb3ec;\n }\n}\n\n* { box-sizing: border-box; }\n\nbody {\n margin: 0;\n background: var(--ground);\n color: var(--ink);\n font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", Arial, sans-serif;\n}\n\na { color: var(--accent); }\n\nheader.site, footer.site, main {\n max-width: 62rem;\n margin: 0 auto;\n padding: 1rem 1.25rem;\n}\n\nheader.site {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem 1.5rem;\n align-items: baseline;\n border-bottom: 1px solid var(--rule);\n}\n\nheader.site .home { font-weight: 600; text-decoration: none; }\n\nnav.breadcrumb, nav.keys { font-size: 0.875rem; color: var(--dim); }\nnav.breadcrumb a::after { content: " /"; color: var(--rule); }\nnav.breadcrumb a:last-child::after { content: ""; }\n\nfooter.site {\n border-top: 1px solid var(--rule);\n color: var(--dim);\n font-size: 0.8125rem;\n}\n\nh1 { font-size: 1.6rem; margin: 0 0 0.25rem; overflow-wrap: anywhere; }\nh2 { font-size: 1.15rem; margin: 2rem 0 0.5rem; }\nh3 { font-size: 1rem; margin: 1.5rem 0 0.35rem; }\n\ncode, .oid, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }\ncode, .oid { font-size: 0.9em; overflow-wrap: anywhere; }\n\npre {\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n padding: 0.75rem;\n overflow-x: auto;\n font-size: 0.8125rem;\n}\n\ndl.facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.3rem 1.25rem; margin: 0.75rem 0; }\ndl.facts dt { color: var(--dim); }\ndl.facts dd { margin: 0; overflow-wrap: anywhere; }\n\ntable { border-collapse: collapse; width: 100%; margin: 0.75rem 0; font-size: 0.9375rem; }\nth, td { text-align: left; vertical-align: top; padding: 0.35rem 0.6rem 0.35rem 0; border-bottom: 1px solid var(--rule); }\nth { color: var(--dim); font-weight: 600; white-space: nowrap; }\n\n.wide { overflow-x: auto; }\n\nul.plain, ul.keys { list-style: none; padding: 0; margin: 0.5rem 0; }\nul.plain li { padding: 0.15rem 0; }\n\nul.keys { display: flex; flex-wrap: wrap; gap: 0.35rem 0.6rem; }\nul.keys li { font-size: 0.8125rem; }\nul.keys .here { font-weight: 600; }\n\nsection > :last-child { margin-bottom: 0; }\n.note { color: var(--dim); font-size: 0.9375rem; }\n\n/* The entry point\'s figures. One hero number, then a tile per count: no\n chart, because none of these is a series -- they are eight unrelated\n totals, and a bar chart of unrelated totals invites a comparison that\n means nothing. */\n.hero { margin: 0.5rem 0 1rem; }\n.hero .figure { display: block; font-size: 3rem; font-weight: 600; line-height: 1.1; }\n.hero .label { color: var(--dim); }\n\nul.kpis {\n list-style: none;\n padding: 0;\n margin: 1rem 0;\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));\n gap: 0.75rem;\n}\n\nul.kpis li {\n padding: 0.6rem 0.75rem;\n background: var(--raised);\n border: 1px solid var(--rule);\n border-radius: 4px;\n}\n\nul.kpis .figure { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; }\nul.kpis .label { display: block; color: var(--dim); font-size: 0.8125rem; }\n\n/* Dates and counts in a column, so the eye compares them down the column\n rather than reading each row\'s digits as a word. */\n.recent td:nth-child(2), .recent td:last-child { font-variant-numeric: tabular-nums; white-space: nowrap; }\n.recent td:last-child, .recent th:last-child { text-align: right; }\n\n/* Two lists of the same modules under two different dates, one shown at a\n time. The radio a label points at is the whole mechanism -- no script, and\n the reader\'s choice survives nothing, which is right for a page they arrive\n at rather than work in. Without this stylesheet both panels show, one after\n the other under their own headings, which is the state every crawler and\n every reader with no CSS sees: the page states the same facts either way. */\n.switch > input { position: absolute; width: 1px; height: 1px; opacity: 0; }\n.switch .tabs { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 1.25rem 0 0; }\n\n.switch .tab {\n padding: 0.4rem 0.75rem;\n border: 1px solid var(--rule);\n border-radius: 4px;\n color: var(--dim);\n cursor: pointer;\n font-size: 0.9375rem;\n}\n\n.switch .tab:hover { color: var(--ink); }\n.switch .panel { display: none; }\n.switch .panel > section:first-child > h2 { margin-top: 1rem; }\n\n/* :focus-visible on the input, styled on the label, so the tab a reader\n tabbed to is visible -- the input itself is one pixel and invisible. */\n#recent-published:focus-visible ~ .tabs .tab[for="recent-published"],\n#recent-changed:focus-visible ~ .tabs .tab[for="recent-changed"] { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n#recent-published:checked ~ .tabs .tab[for="recent-published"],\n#recent-changed:checked ~ .tabs .tab[for="recent-changed"] { background: var(--raised); border-color: var(--dim); color: var(--ink); font-weight: 600; }\n\n#recent-published:checked ~ .panel.recent-published,\n#recent-changed:checked ~ .panel.recent-changed { display: block; }\n\n.defs td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; overflow-wrap: anywhere; }\n.defs td:first-child { white-space: nowrap; }\n.defs td:last-child, .defs.typed td:nth-child(4) { font-family: inherit; font-size: inherit; }\n.defs .desc { font-family: inherit; color: var(--dim); font-size: 0.875rem; overflow-wrap: normal; }\n\n:target { background: var(--raised); }\n\n@media (max-width: 32rem) {\n dl.facts { grid-template-columns: 1fr; gap: 0.1rem; }\n dl.facts dt { margin-top: 0.5rem; }\n .hero .figure { font-size: 2.25rem; }\n}\n', corpus: str = 'MIB corpus')¶
The page frame and the stylesheet a build renders with.
Create new instance of Theme(page, stylesheet, corpus)
- page: str¶
The page template, with
PLACEHOLDERSin it.
- pysmi.corpus.site.theme.load_theme(page: str | None = None, stylesheet: str | None = None, corpus: str | None = None) Theme¶
A theme, with either file replaced by one the manifest names.
- Parameters:
page – a file holding a replacement page template, or
Nonefor the built-in one.stylesheet – a file holding a replacement stylesheet, likewise.
corpus – what the header calls this corpus.
- Returns:
The theme.
- Raises:
PySmiError – a named file could not be read. A theme that silently fell back to the default would publish a whole site in the wrong skin and say nothing.
What the distribution changed¶
A second date per module, supplied by the distribution publishing the site.
Every date a MIB carries is the publisher’s: LAST-UPDATED and the REVISION clauses say when the vendor changed the module, which is what the entry point’s recent lists are ordered by. That answers “what has the industry published lately” and cannot answer “what have we done lately” – a module taken today may carry a revision from 1994, and a module corrected today carries whatever date the publisher last put in it.
That second date is a fact about a distribution rather than about a MIB, so it
arrives from one. A manifest names a JSON file of module to YYYY-MM-DD:
{"IF-MIB": "2026-09-17", "CISCO-IPMCAST-MIB": "2026-04-02"}
How a distribution knows the date is its own business – pysnmp/mibs reads the commit that last touched each file – and a module the file does not name simply has no such date and is not listed under one.
See site.changed in pysmi.corpus.namespace.SITE_SETTINGS.
- class pysmi.corpus.site.dates.Changed(label: str, dates: Mapping[str, str])¶
The distribution’s own dates, and what to call them.
Create new instance of Changed(label, dates)
- pysmi.corpus.site.dates.DATE: Final = re.compile('^\\d{4}-\\d{2}-\\d{2}$')¶
A date this can place a module by. The recent lists sort on these and print them verbatim, so anything else is refused rather than rendered: a row reading “last week” sorts nowhere and says nothing a reader can compare.
- pysmi.corpus.site.dates.LABEL: Final = 'Updated here'¶
What the entry point calls the list when a manifest names no label. Said from the reader’s side – they are on the distribution’s own site – and neutral about what changed, since taking a module and correcting one both land here.
- pysmi.corpus.site.dates.read_changed(path: str, label: str = '') Changed¶
The dates file a manifest named.
- Parameters:
path – the JSON file, as
site.changed.datesnames it.label – what to call the list, or “” for
LABEL.
- Returns:
The dates, keyed by module name.
- Raises:
pysmi.error.PySmiError – the file will not read, is not an object of module to date, or carries a date this cannot sort by. A build that quietly published an empty list here would state that the distribution has changed nothing, which is a different claim from “this build was not told”.