Status: v0.3.0 — shipped 2026-05-06
Author: GhaatakJi
Last updated: 2026-05-06
Build a clean, modern, pure-Python SMI/MIB compiler that:
.py modules (secondary output, for compat).py MIB modules to JSON (tsmi convert FILE.py — shipped in v0.2.0)The existing reference implementation — pysmi — works but carries significant technical debt:
| Problem in pysmi | Impact |
|---|---|
dataObj used before assignment in nested ZIPs |
Runtime NameError crash |
UnboundLocalError on empty refs |
Silent failure on edge cases |
Misplaced raise after successful file read |
Incorrect control flow |
| No HTTP timeout implemented | Process hangs indefinitely |
requests.Session never closed |
Resource leak in long-running apps |
File handles without with statements |
Leak on exception |
Circular imports in error.py |
ImportError on some environments |
**options kwargs with no type safety |
Opaque, fragile API |
| Mixed concerns across modules | Hard to test in isolation |
| Built on PLY (aging lex/yacc port) | Verbose, manual AST construction |
Rather than patching pysmi incrementally, trishul-smi is a ground-up rewrite with correctness, testability, and clean design as first-class goals.
.py modules (secondary, --format pysnmp)--format json pysnmp).py MIB modules → JSON (tsmi convert — shipped v0.2.0)trishul-smi compile <MIB-NAME>MibCompiler class0.1.0 (unstable marker)pysmi’s core issues are architectural, not just bugs. The parser is built on PLY (an aging lex/yacc port), the pipeline has tight coupling between reader/parser/codegen, and the public API uses **kwargs throughout — making it hard to add type safety without a near-total rewrite. Fixing it means owning their architecture.
JSON is universally consumable. Every language, framework, and tool can read JSON. A MIB compiled to JSON can be used in:
PySNMP .py output is a walled garden — only useful inside the PySNMP ecosystem. JSON breaks MIB data free for any consumer.
.py output?.py formatAbstractCodeGen architecture makes adding a second codegen zero-cost to the pipelinetrishul-smi a complete drop-in replacement for pysmi, not just a partial toolFetching MIBs from the web is I/O bound. Async allows parallel fetching of independent MIB dependencies without blocking on each HTTP request. httpx is the modern replacement for requests — async-native, timeout-safe, and easier to mock in tests.
| Concern | PLY | Lark |
|---|---|---|
| Grammar style | Verbose BNF, C-like | Clean EBNF |
| AST construction | Manual p_rule() for every rule |
Auto-built from grammar structure |
| Ambiguity handling | ❌ LALR(1) only | ✅ Earley algorithm for ambiguous grammars |
| Python 3 support | Legacy, Python 2 roots | Native Python 3, typed since v1.0 |
| Maintenance | Largely inactive | Actively maintained |
| Relevance to SMI | Vendor dialect quirks cause ambiguity | Earley handles this gracefully |
Decision: Use lark-parser with LALR(1) for standard SMIv2; fall back to Earley for ambiguous vendor dialects.
.py as secondary outputDecision: JSON is the default and primary output format. PySNMP .py is supported as an optional secondary format via --format pysnmp. Both can be generated in a single run.
trishul-smi compile IF-MIB # JSON only (default)
trishul-smi compile IF-MIB --format pysnmp # PySNMP .py only
trishul-smi compile IF-MIB --format json pysnmp # both simultaneously
.py → JSON as a separate utility commandDecision: Implemented as tsmi convert FILE.py in v0.2.0. Uses Python’s ast module — no SMI grammar required. Extracts OIDs, object types, syntax (resolving _Name_Type wrapper classes to their base type), max_access, status, and description from setMaxAccess/setStatus/setDescription calls. Emits JSON via JsonFormatter — same schema as the compile path.
.py output from v1.0Decision: Use Jinja2 for PysnmpFormatter from day one. Avoids two code paths (manual string building vs. template). Templates live in output/templates/pysnmp_module.j2.
Decision: Deferred to v1.x. In v1.0 a failed fetch/parse raises MibNotFoundError or ParseError. CompileResult.status is Literal["compiled", "cached", "failed"].
Decision: Yes, from v1.0. Resolver uses asyncio.gather() per BFS level. Architecturally cheap; deferring would make the resolver hard to parallelize later.
Decision: Disk cache uses orjson JSON serialization. Pickle banned — silently breaks on model changes, security risk.
Decision (v0.1.0): PysnmpFormatter targets the common subset sufficient for 95% of MIBs:
OBJECT-TYPE definitionsOBJECT-TYPE (with INDEX / AUGMENTS)NOTIFICATION-TYPE definitionsTEXTUAL-CONVENTION typesMODULE-IDENTITY metadataExtended in v0.2.0:
MibTableColumn correctly classified (two-pass OID tree walk after full OID resolution)subtypeSpec, displayHint, status, description_Name_Type wrapper classes for inline constraintssetIndexNames / AUGMENTS → getIndexNames()setOrganization, setRevisions, setDescription on MODULE-IDENTITYsetDescription on NOTIFICATION-TYPE, OBJECT-GROUP, MODULE-COMPLIANCE, AGENT-CAPABILITIESexportSymbols single-dict format--no-texts flag to suppress all text fields0.1.0 from day oneDecision: Publish early with Development Status :: 3 - Alpha classifier. Costs nothing; enables pip install trishul-smi from day one and creates accountability. Version 0.1.0 published when end-to-end compile of IF-MIB works.
[Source: file / zip / http]
↓ Reader
[Raw ASN.1 text]
↓ Parser (lark EBNF grammar, run via asyncio.to_thread)
[AST]
↓ Transformer
[MibModule dataclass]
↓ Dependency Resolver (Kahn’s algorithm + asyncio.gather for parallel fetch)
[Ordered MibModule list]
↓ CodeGen (json_codegen / pysnmp_codegen — one or both)
[dict / .py string]
↓ Writer (file / stdout / callback)
[Output artifacts]
Each stage is independently testable with clean interfaces.
| Concern | Choice | Reason |
|---|---|---|
| ASN.1 parsing | lark-parser |
Clean EBNF, auto AST, Earley for ambiguity |
| HTTP client | httpx |
Async, timeout-safe, easy to mock |
| Retry logic | tenacity |
Exponential backoff, clean decorator API |
| JSON output + cache | orjson |
Fast, compact; also used for disk cache (replaces pickle) |
| PySNMP codegen | jinja2 |
Template-based, testable, single code path from v1.0 |
| CLI | typer |
Type-annotated, auto --help, built on Click |
| Terminal output | rich |
Pretty tables, progress bars |
| Linting | ruff |
Replaces flake8 + black + isort in one tool |
| Type checking | mypy (strict) |
Catches bugs at dev time |
| Testing | pytest + pytest-httpx |
Async support, HTTP mocking |
| Packaging | hatchling + pyproject.toml |
Modern Python packaging standard |
trishul_smi/
├── compiler.py ← orchestrator (MibCompiler class)
├── config.py ← CompilerConfig dataclass + VALID_FORMATS
├── errors.py ← exception hierarchy (no circular imports)
├── models/ ← MibModule, MibObject, MibType, CompileResult
├── parser/
│ ├── grammar/
│ │ ├── common.lark ← shared terminals (string literals, OIDs, comments)
│ │ ├── smiv2.lark ← complete SMIv2 grammar (RFC 2578)
│ │ └── smiv1.lark ← complete SMIv1 grammar (RFC 1155)
│ ├── transformer.py ← Lark tree → MibModule
│ ├── smi_parser.py ← public parse(text) → MibModule
│ └── _constants.py ← BASE_MIBS skip list
├── reader/
│ ├── localfile.py ← filesystem reader (enforces max_mib_size)
│ ├── httpclient.py ← async HTTP reader (enforces max_mib_size, ETag + TTL)
│ ├── zipreader.py ← ZIP archive reader
│ └── chain.py ← ReaderChain
├── resolver/
│ ├── resolver.py ← MibResolver (Kahn’s + asyncio.gather)
│ ├── dependency.py ← topological sort
│ └── cache.py ← MibCache (orjson disk cache, atomic writes)
├── output/
│ ├── json_fmt.py ← MibModule → JSON [PRIMARY]
│ └── pysnmp_fmt.py ← MibModule → PySNMP .py [SECONDARY, Jinja2 inline template]
├── convert/
│ └── pysnmp_reader.py ← compiled .py → MibModule [ast-based, no grammar]
└── cli/
└── main.py ← typer app (compile + convert + version commands)
models/ — pure data structures, no depsconfig.py — CompilerConfig dataclass (needed by reader, compiler, cli)errors.py — exception hierarchyreader/ — fetch raw MIB textparser/grammar/smiv2.lark — hardest piece, SMIv2 firstparser/transformer.py + smi_parser.pyresolver/ — Kahn’s algorithm + parallel fetchoutput/json_fmt.py — primary outputoutput/pysnmp_fmt.py + templates/pysnmp_module.j2compiler.py — wire everythingcli/ — last, always backed by real logic{
"module": "IF-MIB",
"language": "SMIv2",
"generated_by": "trishul-smi",
"generated_at": "2026-05-06T12:00:00Z",
"imports": {
"SNMPv2-SMI": ["MODULE-IDENTITY", "OBJECT-TYPE", "Integer32"],
"SNMPv2-TC": ["DisplayString", "PhysAddress", "TruthValue"]
},
"objects": {
"ifDescr": {
"oid": "1.3.6.1.2.1.2.2.1.2",
"oid_path": [1, 3, 6, 1, 2, 1, 2, 2, 1, 2],
"object_type": "OBJECT-TYPE",
"class": "objecttype",
"nodetype": "column",
"syntax": "DisplayString",
"max_access": "read-only",
"status": "current",
"description": "A textual string containing information about the interface."
}
},
"types": {
"InterfaceIndex": {
"class": "textualconvention",
"base_type": "Integer32",
"display_hint": "d",
"status": "current",
"description": "..."
}
},
"notifications": {
"linkDown": {
"oid": "1.3.6.1.6.3.1.1.5.3",
"oid_path": [1, 3, 6, 1, 6, 3, 1, 1, 5, 3],
"object_type": "NOTIFICATION-TYPE",
"class": "notificationtype",
"members": [
{"module": "IF-MIB", "object": "ifIndex"},
{"module": "IF-MIB", "object": "ifAdminStatus"},
{"module": "IF-MIB", "object": "ifOperStatus"}
]
}
},
"module_metadata": {
"lastupdated": "2000-06-14",
"revisions": [{"date": "2000-06-14", "description": "..."}],
"organization": "IETF Interfaces MIB Working Group",
"contactinfo": "...",
"description": "..."
}
}
.py OutputGenerated from an inline Jinja2 template in output/pysnmp_fmt.py. Covers the full set defined in DD-8 (as extended in v0.2.0): scalars, tables, table columns (with INDEX/AUGMENTS), notifications, textual-conventions with full subtypeSpec, module-identity with organization/revisions/description.
The project is considered v1.0 ready when:
trishul-smi compile IF-MIB works end-to-end from a clean environmenttrishul-smi compile IF-MIB --format json pysnmp produces both output formats correctlymypy --strict passes with zero errorsruff check passes with zero warningstrishul-smi 0.1.0