Skip to main content
Edison

Edison

An EDIFACT parser with a real syntax tree.

Build Version Python versions

Edison is a Python library for parsing EDIFACT messages. Instead of treating a message as a flat stream of segments, it models valid messages as a tree of transitions. You define the schema once, then parse, validate, or pretty-print messages against that structure.

Highlights

  • Define EDIFACT syntax trees directly in Python.
  • Attach parsing callbacks to the segments you care about.
  • Select schemas by message version.
  • Model conditional, mandatory, and repeatable segments.
  • Handle escaped EDIFACT separators and EBCDIC control characters.
  • Properly tokenize segments with specified lengths like BLB
  • Export transition graphs with pydot.

Installation

Edison requires Python 3.12 or newer.

pip install edison-parser

Quick start

Subclass BaseParser, define one or more schemas, and attach callback method names to the schema nodes you want to process.

from edison import BaseParser


class PIZZAQParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            ["CVR"],
            ["REF"],
            [
                "PIZ",
                [
                    ["TYP", [], "parse_typ"],
                    ["REF", [], "parse_ref"],
                ],
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.summary = {}
        self.pizza = None

    def parse_typ(self, segment: str):
        self.pizza = segment[4:]

    def parse_ref(self, segment: str):
        refs = segment[4:].split("*")
        self.summary[self.pizza] = len(refs)


edi = """
UNH+1+PIZZAQ:1:1:1A+00012480825035'
CVR+USD'
REF+LUIS'
PIZ+1'
TYP+MARGARITA'
REF+1*2'
PIZ+2'
TYP+PESTO'
REF+3'
UNT+19+1'
"""

parser = PIZZAQParser()
parser.parse(edi)

assert parser.summary == {"MARGARITA": 2, "PESTO": 1}
assert PIZZAQParser.validate(edi)
print(PIZZAQParser.show(edi))
Edison

Tree-based structure.

Schema format

A schema is a dictionary keyed by the minimum supported version:

schemas = {
    (0, 0): [
        ["UNH"],
        ["BGM"],
        ["UNT"],
    ],
}

Each segment node accepts these positional fields:

["TAG", children, action, repetition, flag]
Field Default Meaning
TAG required Three-character EDIFACT segment tag.
children [] Nested segment group.
action None Parser method name called with the raw segment.
repetition 99999 1 means single-use; any higher value means repeatable.
flag "C" "C" for conditional, "M" for mandatory.

Main API

API Use
parser.parse(edi, version=None) Walk an EDI message and call matching segment actions.
Parser.validate(edi, version=None) Return True when all segments follow the schema transitions.
Parser.show(edi, version=None, colorize=True) Return a formatted, indented representation of an EDI message.
Parser.build_dot_object(version=None, include_up_links=True) Build a pydot.Dot transition graph.
extract_envelope_headers(edi) Quickly return the UNB and UNH envelope segments.
extract_envelope_footers(edi) Quickly return the UNT and UNZ envelope segments.
escape(data) / unescape(data) Escape or unescape EDIFACT separators: +, :, *, '.
normalize_ebcdic_separators(edi) Convert EBCDIC EDIFACT separators to ASCII safely.

Advanced usage

Versioned syntax trees

Define multiple syntax trees when the same message evolves across versions. The parser selects the highest schema version less than or equal to the version argument, while parsing callbacks can stay the same.

from edison import BaseParser


class RouteParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            ["ORG", [], "parse_origin"],
            ["DST", [], "parse_destination"],
            ["UNT"],
        ],
        (2, 0): [
            ["UNH"],
            [
                "LEG",
                [
                    ["ORG", [], "parse_origin"],
                    ["DST", [], "parse_destination"],
                ],
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.route = {}

    def parse_origin(self, segment: str):
        self.route["origin"] = segment[4:]

    def parse_destination(self, segment: str):
        self.route["destination"] = segment[4:]


legacy = RouteParser()
legacy.parse("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))

modern = RouteParser()
modern.parse("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))

assert legacy.route == modern.route == {
    "origin": "PAR",
    "destination": "NCE",
}
assert RouteParser.validate("UNH'ORG+PAR'DST+NCE'UNT'", version=(1, 0))
assert RouteParser.validate("UNH'LEG+1'ORG+PAR'DST+NCE'UNT'", version=(2, 0))

When version is omitted, incomplete, or newer than all known schemas, Edison uses the latest schema. If a requested version is older than the first schema, find_transitions() raises ValueError.

Mandatory and repeatable segments

repetition controls whether the transition graph can come back to the same node:

  • 1 means the node can be used once in a given sequence.
  • Any value greater than 1 means the node is repeatable. The transition graph only distinguishes 1 from "repeatable"; it does not count exact occurrence limits such as 2 versus 99999.

Mandatory nodes constrain valid paths through the tree. Once a mandatory node is next in a sequence, the parser cannot skip over it to reach a later sibling, a parent, or a child that would bypass it. A mandatory child does not make its parent mandatory by itself: if the whole group is required, mark the group node as "M" too.

from edison import BaseParser


class OrderParser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            [
                "ITM",
                [
                    ["DSC"],
                    ["QTY", [], "parse_quantity", 1, "M"],
                    ["PRI"],
                ],
                None,
                99999,
                "M",
            ],
            ["UNT"],
        ],
    }

    def __init__(self):
        self.quantities = []

    def parse_quantity(self, segment: str):
        self.quantities.append(segment[4:])


assert OrderParser.validate("UNH'ITM'DSC'QTY+2'PRI'ITM'QTY+1'UNT'")
assert not OrderParser.validate("UNH'UNT'")  # ITM is mandatory.
assert not OrderParser.validate("UNH'ITM'PRI'UNT'")  # QTY is mandatory.
assert not OrderParser.validate("UNH'ITM'QTY+2'QTY+3'UNT'")  # QTY repeats once.
Shared headers, footers, and parser hooks

Use header and footer when several schemas share envelope segments. They are automatically prepended and appended to every schema at class compilation time.

from edison import BaseParser


class EnvelopeParser(BaseParser):
    header = [["UNH", [], "on_segment"]]
    footer = [["UNT", [], "on_segment"]]
    schemas = {(0, 0): [["BGM", [], "on_segment"]]}

    def __init__(self):
        self.segments = []

    def set_up(self):
        self.segments.clear()

    def on_segment(self, segment: str):
        self.segments.append(segment)

    def tear_down(self):
        self.count = len(self.segments)

set_up() runs before parsing starts, and tear_down() runs after parsing finishes.

Transition graphs

pydot is installed with Edison. To write PNG files, make sure Graphviz is available on your system, then call build_dot_object() on a BaseParser subclass.

from edison import BaseParser


class Parser(BaseParser):
    schemas = {
        (0, 0): [
            ["UNH"],
            [
                "BGM",
                [
                    ["DTM"],
                ],
            ],
            ["UNT"],
        ],
    }


dot = Parser.build_dot_object()
dot.write_png("transitions.png")

You can pass the same version selector used by parsing, and hide upward links for a simpler tree-like graph:

dot = Parser.build_dot_object(version=(13, 2), include_up_links=False)
dot.write_png("transitions-v13.png")

Because build_dot_object() returns the underlying pydot.Dot object, you can customize it before writing.

Escaping and EBCDIC EDIFACT separators

Edison understands escaped EDIFACT separators and can normalize EBCDIC separator characters often found in binary payloads.

from edison import normalize_ebcdic_separators, escape, unescape


assert escape("REC+") == r"REC\+"
assert unescape(r"REC\+") == "REC+"
assert normalize_ebcdic_separators("UNB\x1dSENDER\x1fREC") == "UNB+SENDER:REC"

The segment tokenizer also handles escaped quotes, so content such as IFT+1+AIRLINE\'SERVER' stays in a single EDIFACT segment.

EDIFACT envelope extraction

For the common case of reading EDIFACT envelope boundaries, use extract_envelope_headers() to lazily return the first UNB/UNH segments or extract_envelope_footers() to lazily return the last UNT/UNZ segments:

from edison import extract_envelope_footers, extract_envelope_headers

edi = "UNB+SENDER+RECEIVER'UNH+1+PIZZAQ:1:1:1A'BGM+1'UNT+3+1'UNZ+1+1'"
headers = extract_envelope_headers(edi)
footers = extract_envelope_footers(edi)

assert headers.unb == "UNB+SENDER+RECEIVER"
assert headers.unh == "UNH+1+PIZZAQ:1:1:1A"
assert footers.unt == "UNT+3+1"
assert footers.unz == "UNZ+1+1"

Development

Local commands

This repository uses uv, tox, pytest, ruff, black, ty, and pre-commit hooks.

uv tool install tox --with tox-uv
tox -e py

Build distributions with:

uv build --sdist --wheel -o dist/

Run the full pre-commit suite with:

uv tool install prek==0.4.10
prek run --all-files

CI runs tests through tox on CPython 3.12 and 3.14 across Linux, Windows, and macOS, then builds source and wheel distributions.

Metadata

Release files for edison-parser 0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for edison-parser 0.2
File Size Uploaded Provenance
edison_parser-0.2.tar.gz 21.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for edison-parser 0.2
File Interpreter ABI Platform
edison_parser-0.2-py3-none-any.whl Python 3 none any Details

Total release size: 37.6 kB

Release files / edison_parser-0.2.tar.gz

Download URL edison_parser-0.2.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
24ec39f2a5977d583f0fb2e22ce2d54f0b7f8b1ee6341d83801997bc7292f22e
BLAKE2b-256 checksum
How to use checksums
d918c5f6aefc022ee7904d3ddbc1eb34c3aec12caaf61b74acfc78878b17bd59
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / edison_parser-0.2-py3-none-any.whl

Download URL edison_parser-0.2-py3-none-any.whl
Size 16.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a57fea554f5c051c352527c6a1d230d8c6628872378a47325fb83463f918a37
BLAKE2b-256 checksum
How to use checksums
3683f7d86812edd36d45b5a4e6160bda35e689c459dd13d93c63c9c6f4c1f46f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for edison-parser 0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for edison_parser-0.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page