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 weighted scoring heuristic, rather than a single strict rule — an all-or-nothing gate rejects too many otherwise-clear itineraries.

The itinerary must first return to its starting airport (round-trip or closed-loop multi-city); a genuine one-way routing has no return leg, so trip_direction is left None immediately. Past that, points accumulate (out of 100):

Signal Points
Returns to its starting airport (closed loop) +50
Exactly 2 segments (split point is unambiguous) +20
Largest gap between consecutive segments exceeds 24h +15
That gap is the only one exceeding 24h (no competing boundary) +10
The stopover airport differs from the itinerary's home airport +5
Ticket's own fare calculation confirms the same break point +30

trip_direction is assigned only when the score reaches 60/100. The split falls right after the segment preceding the largest gap (or, for an unambiguous 2-segment round trip, always after the first segment) — unless the ticket's fare calculation line gives a cleaner answer (see below), in which case its break point wins instead.

Fare calculation is the most reliable signal, when present. IATA fare calculation lines (ticket.fare_calculation) price each leg as a separate fare component — e.g. PAR780.50 marks a component ending in Paris. When the line shows exactly two components and the break lands on a segment boundary (matched via IATA metropolitan-area codes, so PAR correctly matches CDG), that break point overrides the gap-based split and adds +30 — reliable enough on its own, combined with the +50 closed-loop requirement, to clear the threshold even when stopovers are short or ambiguous.

Example — booking F65A7Y (NRT→HAN→CDG→SGN→NRT, with a 3-day stopover in CDG before the journey continues): score = 50 (closed loop) + 15 (gap > 24h) + 10 (only one large gap) + 5 (CDG ≠ NRT) + 30 (fare calc PAR780.50…TYO780.50 confirms the same CDG break) = 110. 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.5.0.tar.gz (285.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.5.0-py3-none-any.whl (53.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: eticket_document_sdk-1.5.0.tar.gz
  • Upload date:
  • Size: 285.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.5.0.tar.gz
Algorithm Hash digest
SHA256 817951d34fc33bcee53b3efd41a276a5d28e6d9358425536ddda5d73a5ef41bd
MD5 7771b6e09d39d8645f301bb3dfe00346
BLAKE2b-256 e51ff2a3c776fb5b07d7e3b686bf448a301bd8ca01a53bac9cc90729d5dfe9ac

See more details on using hashes here.

Provenance

The following attestation bundles were made for eticket_document_sdk-1.5.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.5.0-py3-none-any.whl.

File metadata

File hashes

Hashes for eticket_document_sdk-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 31dd48b9ffc1a40f69b619d6a2b3479e4dd100eaeee8062be5e6844106981ee3
MD5 d77179bf29a866204ebd73263621b18a
BLAKE2b-256 9de3ff9189c1fe1a76c7b45099b110e7d2910c76fc4bb480a2efb39ff7ccd309

See more details on using hashes here.

Provenance

The following attestation bundles were made for eticket_document_sdk-1.5.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