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)

arc: str

Alias for field number 0

children: tuple[Arc, ...]

Child arcs, ascending, each with the name the arc index gives it.

module: str

The module registered at this arc, where one is. A page is written only for arcs where none is – an arc that is a module resolves to the module page instead – so this is what the parent’s child rows read.

name: str

Alias for field number 1

reference: str

Alias for field number 3

source: str

Alias for field number 2

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)

access: str

MAX-ACCESS, or "".

description: str

DESCRIPTION, or "" where the build carried no texts.

kind: str

What it is, from KIND.

name: str

The descriptor, which is also the page anchor.

oid: str

Its OID, or "" for a type, which has none.

status: str

STATUS, or "".

syntax: str

The SYNTAX, rendered flat – Integer32 (0..255), INTEGER {up(1), down(2)}. "" where the definition has none.

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)

arc: str

Alias for field number 1

contacts: tuple[Mapping[str, str], ...]

(source, organization, contact, email, module, authority) rows.

modules: tuple[str, ...]

Alias for field number 4

number: int

Alias for field number 0

organization: str

Alias for field number 2

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.

closure: tuple[str, ...]

The files a consumer needs in order to load this module.

conformance: tuple[Definition, ...]

Alias for field number 18

contact: str

Alias for field number 3

defects: tuple[tuple[str, str], ...]

The defects a patch on this module repairs, as (id, url).

description: str

Alias for field number 6

importedBy: tuple[str, ...]

The modules in this corpus that import this one.

imports: Mapping[str, tuple[str, ...]]

Module name to the symbols imported from it.

lastupdated: str

Alias for field number 4

missing: tuple[str, ...]

What the closure needs and the corpus does not hold.

module: str

Alias for field number 0

notifications: tuple[Definition, ...]

Alias for field number 17

objects: tuple[Definition, ...]

Alias for field number 16

organization: str

Alias for field number 2

patch: str

The patch itself, as a unified diff, or "".

provenance: Mapping[str, str]

Namespace, file and digest, as the provenance table records them.

revisions: tuple[str, ...]

Alias for field number 5

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)

arcs: int

Nodes in the arc index, or 0 for a build given no arc names.

datedChanged: Mapping[str, int]

Per tier, how many modules the distribution supplied a date for, which is the pool latestChanged is the newest few of. Not tiers: 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.

incomplete: int

Modules whose import closure names something the corpus does not hold.

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.

modules: int

Modules published, which is the figure the page leads with.

notifications: int

NOTIFICATION-TYPEs across the corpus.

objects: int

OBJECT-TYPEs and OBJECT-IDENTITYs across the corpus.

registrants: int

Enterprise arcs the corpus registers under, or 0 for a build that wrote no registrant pages.

repaired: int

Modules served with a patch applied to the publisher’s text.

revised: str

The newest date any module in the corpus carries, as YYYY-MM-DD.

tiers: Mapping[str, int]

Modules per tier, in pysmi.corpus.namespace.TIERS order and holding only the tiers this corpus has modules in.

types: int

TEXTUAL-CONVENTIONs and type assignments across the corpus.

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)

browse: int

browse/, the module list.

entity: int

A registrant’s module list, and the registrant list itself.

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 entities where the key is entity would 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.dates supplied it, or "" where none was supplied for this module. Independent of revised: a module taken today carries whatever revision its publisher last set.

definitions: int

How many OBJECT-TYPEs and NOTIFICATION-TYPEs it defines, so a row says how much moved rather than only that something did.

module: str

Alias for field number 0

organization: str

The ORGANIZATION the MODULE-IDENTITY names, or "".

revised: str

The newest date the module itself carries – its LAST-UPDATED or the latest REVISION – as YYYY-MM-DD.

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)

arcs: int

Alias for field number 2

bytes: int

Alias for field number 4

counts() → dict[str, int]

The tally a build report carries.

crawlable: bool

Whether the crawl surface was written at all.

entities: int

Alias for field number 1

listings: int

Alias for field number 3

modules: int

Alias for field number 0

property pages: int

Every page written, the stylesheet aside.

sitemaps: int

Sitemap files written, the index aside. Zero for a build that declared no base_url and so is a subtree rather than a distribution site.

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.dates supplied 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.TIERS order 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)

corpus: str

What the header calls this corpus.

page: str

The page template, with PLACEHOLDERS in it.

render(**fields: str) → str

One page, the frame filled in.

Unknown placeholders survive rather than raising: an overridden template with a typo in it should produce a page with $oops on it, which somebody will see, rather than stopping a build partway through the corpus.

stylesheet: str

The stylesheet, written once at the site root.

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 ArcPage per 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, as pysmi.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.txt and llms.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 None for 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 JsonCodeGen emits 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_documents yields 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)

arc: str

Alias for field number 0

children: tuple[Arc, ...]

Child arcs, ascending, each with the name the arc index gives it.

module: str

The module registered at this arc, where one is. A page is written only for arcs where none is – an arc that is a module resolves to the module page instead – so this is what the parent’s child rows read.

name: str

Alias for field number 1

reference: str

Alias for field number 3

source: str

Alias for field number 2

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)

access: str

MAX-ACCESS, or "".

description: str

DESCRIPTION, or "" where the build carried no texts.

kind: str

What it is, from KIND.

name: str

The descriptor, which is also the page anchor.

oid: str

Its OID, or "" for a type, which has none.

status: str

STATUS, or "".

syntax: str

The SYNTAX, rendered flat – Integer32 (0..255), INTEGER {up(1), down(2)}. "" where the definition has none.

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)

arc: str

Alias for field number 1

contacts: tuple[Mapping[str, str], ...]

(source, organization, contact, email, module, authority) rows.

modules: tuple[str, ...]

Alias for field number 4

number: int

Alias for field number 0

organization: str

Alias for field number 2

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.

closure: tuple[str, ...]

The files a consumer needs in order to load this module.

conformance: tuple[Definition, ...]

Alias for field number 18

contact: str

Alias for field number 3

defects: tuple[tuple[str, str], ...]

The defects a patch on this module repairs, as (id, url).

description: str

Alias for field number 6

importedBy: tuple[str, ...]

The modules in this corpus that import this one.

imports: Mapping[str, tuple[str, ...]]

Module name to the symbols imported from it.

lastupdated: str

Alias for field number 4

missing: tuple[str, ...]

What the closure needs and the corpus does not hold.

module: str

Alias for field number 0

notifications: tuple[Definition, ...]

Alias for field number 17

objects: tuple[Definition, ...]

Alias for field number 16

organization: str

Alias for field number 2

patch: str

The patch itself, as a unified diff, or "".

provenance: Mapping[str, str]

Namespace, file and digest, as the provenance table records them.

revisions: tuple[str, ...]

Alias for field number 5

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)

arcs: int

Nodes in the arc index, or 0 for a build given no arc names.

datedChanged: Mapping[str, int]

Per tier, how many modules the distribution supplied a date for, which is the pool latestChanged is the newest few of. Not tiers: 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.

incomplete: int

Modules whose import closure names something the corpus does not hold.

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.

modules: int

Modules published, which is the figure the page leads with.

notifications: int

NOTIFICATION-TYPEs across the corpus.

objects: int

OBJECT-TYPEs and OBJECT-IDENTITYs across the corpus.

registrants: int

Enterprise arcs the corpus registers under, or 0 for a build that wrote no registrant pages.

repaired: int

Modules served with a patch applied to the publisher’s text.

revised: str

The newest date any module in the corpus carries, as YYYY-MM-DD.

tiers: Mapping[str, int]

Modules per tier, in pysmi.corpus.namespace.TIERS order and holding only the tiers this corpus has modules in.

types: int

TEXTUAL-CONVENTIONs and type assignments across the corpus.

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.dates supplied it, or "" where none was supplied for this module. Independent of revised: a module taken today carries whatever revision its publisher last set.

definitions: int

How many OBJECT-TYPEs and NOTIFICATION-TYPEs it defines, so a row says how much moved rather than only that something did.

module: str

Alias for field number 0

organization: str

The ORGANIZATION the MODULE-IDENTITY names, or "".

revised: str

The newest date the module itself carries – its LAST-UPDATED or the latest REVISION – as YYYY-MM-DD.

pysmi.corpus.site.model.TYPES: Final[tuple[str, ...]] = ('textualconvention', 'type')

jsondoc classes for a type a module defines. textualconvention is what a TEXTUAL-CONVENTION comes out as; type is 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.dates supplied 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.TIERS order 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 ArcPage per 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 JsonCodeGen emits 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.model.tier_name(rank: int) → str

What a tier rank is called, as a namespace declares it.

read_documents yields the rank rather than the name, since ranking is what the index wants it for. A page states the name.

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.MIB: Final = 'mib'

Where each kind of page lives, under the site root.

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 import IF-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.arc_url(root: str, arc: str) → str

Where an arc’s page is.

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.entity_url(root: str, number: int) → str

Where a registrant’s page is.

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.module_url(root: str, module: str) → str

Where a module’s page is.

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:
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 None or False is left out entirely, which is what lets a caller pass an optional one without a conditional. True renders as a bare attribute.

A trailing underscore is dropped, so class_ and for_ reach the page as class and for – both are Python keywords and neither can be written as a keyword argument. Underscores inside a name become hyphens, so data_oid is data-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.

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 – JsonCodeGen collapses the publisher’s line breaks unless keepTextsLayout is 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. None renders 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 json or 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 at https://data.mibsdepot.com. Pages resolve a directory URL to its index.html and object storage does not, which is what splits them.

Only the llms.txt bulk links use this. A canonical link, a sitemap entry and the sitemap index are statements about a page, and a page is always on base. Empty means one origin serves both.

data_url(path: str) → str

The absolute URL of a bulk artifact.

data where a distribution declares one, and base otherwise, so a site on one origin is unaffected by the distinction.

description: str

One paragraph saying what this corpus is, for llms.txt.

policy: Mapping[str, Mapping[str, Sequence[str]]]

Per user-agent, which artifacts to allow and disallow, keyed allow and disallow and naming ARTIFACTS. * 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/ and json/ 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.

url(path: str) → str

The absolute URL of a path relative to the site root.

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 lastmod falls back to when nothing it describes carries a readable date. Absent rather than invented: a sitemap entry with no lastmod says “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.txt that 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 DefinedTermSet crossed with Dataset, 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 no inDefinedTermSet back-reference – nesting under hasDefinedTerm already 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-MIB carries 970514000000Z – 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.

pysmi.corpus.site.crawl.sitemap(entries: Iterable[tuple[str, str]]) → str

One sitemap file, from (url, lastmod) pairs.

A pair with no lastmod is written without one. See UNDATED.

pysmi.corpus.site.crawl.sitemap_index(entries: Iterable[tuple[str, str]]) → str

The sitemap index, from (url, lastmod) pairs of sitemap files.

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.

head is 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)

corpus: str

What the header calls this corpus.

page: str

The page template, with PLACEHOLDERS in it.

render(**fields: str) → str

One page, the frame filled in.

Unknown placeholders survive rather than raising: an overridden template with a typo in it should produce a page with $oops on it, which somebody will see, rather than stopping a build partway through the corpus.

stylesheet: str

The stylesheet, written once at the site root.

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 None for 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)

dates: Mapping[str, str]

Module to YYYY-MM-DD. A module absent from this has no such date.

label: str

The heading and tab label, e.g. “Added or updated here”.

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.dates names 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”.