Skip to main content

Parse airline e-ticket PDFs into strongly-typed JSON with high accuracy.

Project description

eticket-document-sdk

Parse airline e-ticket PDFs and convert them into strongly-typed JSON with very high accuracy.

The SDK primarily targets PDFs that already contain a text layer (the common case for e-ticket receipts, booking confirmations and itineraries). OCR is used only as a fallback when text extraction is insufficient.

from eticket_document_sdk import ETicketParser

parser = ETicketParser()
booking = parser.parse("ticket.pdf")
print(booking.model_dump())
{
  "booking_code": "F65A7Y",
  "ticket_number": "7382421531079",
  "passenger": { "first_name": "YUMIKO", "last_name": "KOHNO" },
  "currency": "JPY",
  "total_price": 467510,
  "segments": [{ "flight_number": "VN311", "origin": "NRT", "destination": "HAN" }]
}

Table of Contents


Installation

pip install eticket-document-sdk

Base install includes PDF text extraction (PyMuPDF + pdfplumber). The OCR fallback backend is a heavy optional extra:

# Only needed for scanned / image-only PDFs
pip install "eticket-document-sdk[ocr]"
Extra Pulls in When you need it
(base) pydantic, pymupdf, pdfplumber, python-dateutil Text-layer PDFs (the common case)
ocr paddleocr, opencv-python, numpy Scanned/image PDFs requiring OCR
dev pytest, pytest-cov, ruff, build Contributing / running tests

Requires Python 3.10+. URL input requires no extra dependencies (uses stdlib urllib).


Quick Start

from eticket_document_sdk import ETicketParser

parser = ETicketParser()

# 1) From a local file path (PDF or image)
booking = parser.parse("ticket.pdf")

# 2) From a remote URL (http/https — GCS, S3, CDN, …)
booking = parser.parse("https://storage.googleapis.com/bucket/ticket.pdf")

# 3) From raw bytes
booking = parser.parse_bytes(pdf_bytes)

# 4) From already-extracted text (no PDF/OCR needed)
booking = parser.parse_text(raw_text)

print(booking.model_dump())          # python types
print(booking.model_dump(mode="json"))  # JSON-serializable (datetimes -> str)

# Convenience top-level accessors:
booking.booking_code     # "F65A7Y"
booking.ticket_number    # "7382421531079"
booking.currency         # "JPY"
booking.total_price      # 467510
booking.passenger.full_name
booking.segments[0].origin

Advanced Usage

Configuration

parser = ETicketParser(
    enable_ocr=True,             # allow OCR fallback (default True)
    ocr_langs=["vi", "en", "ja"],# OCR languages (Vietnamese/English/Japanese)
    debug=False,                 # enable DEBUG logging for the SDK
    strict_validation=False,     # raise ValidationError on invalid output
    text_quality_threshold=0.35, # below this, OCR fallback kicks in
)

URL input

parse() accepts any HTTP or HTTPS URL. The file is downloaded automatically using the Python standard library (no extra dependency required):

booking = parser.parse(
    "https://storage.googleapis.com/meetingsvn/cms/vnzamazing-tour/"
    "passport_images/F65A7Y.pdf"
)

Supported URL patterns:

Provider Example
Google Cloud Storage https://storage.googleapis.com/bucket/path/ticket.pdf
AWS S3 (public) https://s3.amazonaws.com/bucket/ticket.pdf
Azure Blob Storage https://account.blob.core.windows.net/container/ticket.pdf
Any CDN / direct link https://cdn.example.com/tickets/ABC123.pdf

Limit: 50 MB maximum. Private URLs (requiring auth headers/signed tokens) are not currently supported via this method — download the file first and use parse_bytes() instead.

Detailed result envelope

parse() returns a Booking. For metadata about how the document was parsed, use the *_detailed variants, which return a ParseResult:

result = parser.parse_detailed("ticket.pdf")
# or
result = parser.parse_detailed("https://storage.googleapis.com/bucket/ticket.pdf")

result.success            # True
result.parser             # "vietnam_airlines"
result.extraction_method  # ExtractionMethod.TEXT_LAYER | OCR | RAW_TEXT
result.confidence         # 0.0 .. 1.0 completeness heuristic
result.warnings           # list of non-fatal validation issues
result.booking            # the Booking object

Also available: parse_bytes_detailed(...) and parse_text_detailed(...).

Thread-safety & performance

A single ETicketParser instance is reusable and thread-safe. The PaddleOCR model, compiled regexes and the parser registry are created once and shared, so models are never reloaded between calls. Create one instance and reuse it (e.g. for batch processing — see examples/batch_parse.py).


Data Model

All models are Pydantic v2 (eticket_document_sdk.models, re-exported from eticket_document_sdk.schemas.pydantic_models).

Booking
├── status: TICKETED | CONFIRMED | PENDING | CANCELLED | UNKNOWN
├── passenger: Passenger
│   ├── title, first_name, last_name, full_name
│   ├── membership_number          # frequent-flyer number
│   └── passenger_type             # ADT | CHD | INF
├── segments: list[FlightSegment]
│   ├── segment_id, flight_number, airline
│   ├── origin, destination        # IATA codes
│   ├── departure_datetime, arrival_datetime
│   ├── departure_terminal, arrival_terminal
│   ├── cabin_class, booking_class, fare_basis, seat
│   ├── free_baggage, status
│   └── trip_direction             # OUTBOUND | RETURN | None — see below
├── ticket: Ticket
│   ├── booking_code, ticket_number, currency
│   ├── base_fare, taxes, total_price
│   ├── tax_breakdown: list[TaxItem]   # {code, amount}
│   ├── fare_basis, fare_calculation, endorsement
│   ├── issue_date, payment
└── booking_code / ticket_number / currency / total_price   # convenience mirrors

trip_direction: outbound vs. return

Most e-ticket receipts don't print an explicit "outbound/return" label — flight segments are just listed in chronological order. The Vietnam Airlines parser infers trip_direction (OUTBOUND / RETURN / None) per segment using a stopover-gap heuristic:

  1. Only applies to itineraries with 2+ segments, all with resolved departure/arrival times, that return to their starting airport (round-trip or closed-loop multi-city). A genuine one-way routing has no return leg, so trip_direction is left None rather than guessed.
  2. Compute the gap between each segment's arrival and the next segment's departure. A short gap (a few hours) is a same-direction connection; a long gap (a stopover before flying back) marks the outbound/return boundary.
  3. The largest gap — if ≥ 24 hours — is taken as that boundary. Segments before it are OUTBOUND, segments after are RETURN. A simple two-segment round trip (A→B, B→A) is always split after the first segment, regardless of gap size.

Example — booking F65A7Y (NRT→HAN→CDG→SGN→NRT, with a 3-day stopover in CDG before the journey continues): segments 1–2 (NRT→HAN→CDG) are OUTBOUND, segments 3–4 (CDG→SGN→NRT) are RETURN — matching the GDS's own Leg grouping.


Parser Architecture

The SDK follows a clean, layered pipeline:

            ┌─────────────────────────────────────────────────────────┐
 source ──▶ │ 1. detect type (PDF / IMAGE / TEXT)        core.classifier│
            │ 2. extract text layer (PyMuPDF → pdfplumber) pdf.*         │
            │ 3. measure quality                          pdf.text_extr. │
            │ 4. OCR fallback if needed (PaddleOCR)        ocr.*          │
            │ 5. classify airline → pick parser           core.classifier│
            │ 6. parse fields                             parsers.*       │
            │ 7. validate (business rules)                core.validator  │
            │ 8. return Pydantic Booking                  models.*        │
            └─────────────────────────────────────────────────────────┘

Key design points:

  • Text-first. OCR is only attempted when the extracted text layer falls below the quality threshold. PyMuPDF is the primary extractor; pdfplumber is the fallback engine.
  • Lazy heavy deps. PyMuPDF, pdfplumber, PaddleOCR, OpenCV and numpy are all imported lazily, so importing the SDK is cheap and the OCR stack is optional.
  • Centralized regex (utils/regex.py) compiled once at import time.
  • Pluggable parsers via a thread-safe registry (see below).

Custom Parser Plugin

Add a new airline without modifying the SDK core. Implement BaseParser and register it:

from eticket_document_sdk import (
    BaseParser, Booking, Ticket, FlightSegment, Passenger, register_parser,
)
from eticket_document_sdk.parsers.generic.parser import GenericExtractors


class JapanAirlinesParser(BaseParser):
    code = "JL"
    name = "japan_airlines"

    def can_parse(self, text: str) -> float:
        # Return 0..1 confidence that this layout is yours.
        return 0.9 if "JAPAN AIRLINES" in text.upper() else 0.0

    def parse(self, text: str) -> Booking:
        return Booking(
            ticket=Ticket(
                booking_code=GenericExtractors.find_pnr(text),
                ticket_number=GenericExtractors.find_ticket_number(text),
                currency=GenericExtractors.find_currency(text),
            ),
            passenger=Passenger(),
            segments=[],
        )


register_parser("JL", JapanAirlinesParser())

# From now on ETicketParser auto-selects it for matching tickets.

The classifier calls can_parse() on every registered parser and selects the highest scorer; airline-specific parsers always win ties against the generic fallback. See examples/custom_parser.py for a runnable version.

This is the same mechanism by which future airlines (ANA, Air France, Emirates, Singapore Airlines, …) are added.


Error Handling

All exceptions derive from ETicketSDKError:

Exception Raised when
DocumentReadError File can't be opened/read, or is empty/corrupt
UnsupportedDocumentError Input type/layout not supported
OCRFailedError OCR needed but disabled/unavailable, or produced nothing
ParserError No parser matched, or a parser failed
ValidationError Output failed business-rule validation (strict mode)
from eticket_document_sdk import ETicketParser, ETicketSDKError

try:
    booking = ETicketParser().parse("ticket.pdf")
except ETicketSDKError as exc:
    print(f"Failed: {exc}")

Validation (core/validator.py) checks ticket-number format (13 digits), PNR format (6 alphanumerics), currency code, flight-number format and segment presence. In non-strict mode, issues are returned as result.warnings; in strict_validation=True mode a ValidationError is raised.


Development Guide

git clone https://github.com/your-org/eticket-document-sdk
cd eticket-document-sdk

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

# Run the test suite with coverage (target: 90%+)
pytest --cov=eticket_document_sdk --cov-report=term-missing

# Lint / format
ruff check .
ruff format .

Tests use a bundled real Vietnam Airlines fixture (tests/fixtures/). OCR-path tests use a fake engine, so PaddleOCR is not required to run the suite.

Project layout mirrors the pipeline: core/ (orchestration), pdf/ (extraction), ocr/ (fallback), parsers/ (plugins), models/ + schemas/ (Pydantic), utils/, exceptions/.


Release Guide

  1. Update the version in pyproject.toml and eticket_document_sdk/__init__.py (__version__).
  2. Update the changelog / release notes.
  3. Run the full test suite and linters; ensure coverage ≥ 90%.
  4. Build the distributions:
    python -m build
    
    This produces dist/eticket_document_sdk-<version>-py3-none-any.whl and the sdist .tar.gz.
  5. Smoke-test the wheel in a clean virtualenv:
    pip install dist/eticket_document_sdk-*.whl
    
  6. Publish:
    python -m twine upload dist/*
    
  7. Tag the release: git tag v<version> && git push --tags.

Roadmap

The extractor/parser boundary is deliberately abstract so additional backends can be added without breaking the public API:

  • Cloud OCR / Document AI: Azure Document Intelligence, AWS Textract, Google Document AI — pluggable as alternative OCREngine implementations or text providers.
  • LLM structured outputs: OpenAI and Claude structured outputs as alternative parser plugins behind the same BaseParser contract.

Because selection happens via the registry + classifier, these can be layered in as new strategies while ETicketParser.parse(...) stays unchanged.


License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

eticket_document_sdk-1.4.0.tar.gz (280.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

eticket_document_sdk-1.4.0-py3-none-any.whl (50.7 kB view details)

Uploaded Python 3

File details

Details for the file eticket_document_sdk-1.4.0.tar.gz.

File metadata

  • Download URL: eticket_document_sdk-1.4.0.tar.gz
  • Upload date:
  • Size: 280.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for eticket_document_sdk-1.4.0.tar.gz
Algorithm Hash digest
SHA256 c4922e7377ffee19c9dba8b4dcf33a4db89aa36f2808f06771c7afa20cd153a8
MD5 4b7f703e3225f525df9f734c8308205f
BLAKE2b-256 4526883f1bca420cccb17b6d64cf06a54ff1c8cc7dab83d6e70e90a470827a9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for eticket_document_sdk-1.4.0.tar.gz:

Publisher: publish.yml on hailong2107/eticket-document-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file eticket_document_sdk-1.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for eticket_document_sdk-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 11825086447060899184720467a923d094c6a0e7fef44d6b490f0874639f792c
MD5 f4624eb59f11ddbb16930ffe1284c574
BLAKE2b-256 d27f77f0add3f02469d5ad4f0e2d40abd57ac04c6f71c580e7cf6a9848710bc1

See more details on using hashes here.

Provenance

The following attestation bundles were made for eticket_document_sdk-1.4.0-py3-none-any.whl:

Publisher: publish.yml on hailong2107/eticket-document-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page