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
- Quick Start
- Advanced Usage
- Data Model
- Parser Architecture
- Custom Parser Plugin
- Error Handling
- Development Guide
- Release Guide
- Roadmap
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+.
Quick Start
from eticket_document_sdk import ETicketParser
parser = ETicketParser()
# 1) From a file path (PDF or image)
booking = parser.parse("ticket.pdf")
# 2) From raw bytes
booking = parser.parse_bytes(pdf_bytes)
# 3) 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
)
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")
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, fare_basis, seat
│ ├── free_baggage, status
├── 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
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
- Update the version in
pyproject.tomlandeticket_document_sdk/__init__.py(__version__). - Update the changelog / release notes.
- Run the full test suite and linters; ensure coverage ≥ 90%.
- Build the distributions:
python -m build
This producesdist/eticket_document_sdk-<version>-py3-none-any.whland the sdist.tar.gz. - Smoke-test the wheel in a clean virtualenv:
pip install dist/eticket_document_sdk-*.whl
- Publish:
python -m twine upload dist/*
- 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
OCREngineimplementations or text providers. - LLM structured outputs: OpenAI and Claude structured outputs as
alternative parser plugins behind the same
BaseParsercontract.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file eticket_document_sdk-1.2.0.tar.gz.
File metadata
- Download URL: eticket_document_sdk-1.2.0.tar.gz
- Upload date:
- Size: 277.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e27e0dd27bd78eb4717f8f44c536a10ff8e34be33c3397171c57d0d30244d4aa
|
|
| MD5 |
2c99555c10fad99891482d4fd4afcfb1
|
|
| BLAKE2b-256 |
b3834dc4bd8030b9a420e093e2c1c37ce7c6896297553b7ecd4b4b8d603dc624
|
Provenance
The following attestation bundles were made for eticket_document_sdk-1.2.0.tar.gz:
Publisher:
publish.yml on hailong2107/eticket-document-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eticket_document_sdk-1.2.0.tar.gz -
Subject digest:
e27e0dd27bd78eb4717f8f44c536a10ff8e34be33c3397171c57d0d30244d4aa - Sigstore transparency entry: 2172385336
- Sigstore integration time:
-
Permalink:
hailong2107/eticket-document-sdk@11310c091c7c253f4fe98563bfd2bd7b6ff0639a -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/hailong2107
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@11310c091c7c253f4fe98563bfd2bd7b6ff0639a -
Trigger Event:
push
-
Statement type:
File details
Details for the file eticket_document_sdk-1.2.0-py3-none-any.whl.
File metadata
- Download URL: eticket_document_sdk-1.2.0-py3-none-any.whl
- Upload date:
- Size: 46.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60164a703d47c5c4136b2fea1bc59b981d63e410c1d2b28a68d238422be72bf0
|
|
| MD5 |
d2ce63168690024f9afc4f2411c761e2
|
|
| BLAKE2b-256 |
24b9f218dcad169cbe17175b0992d96ab9d01186d6845c56e4b8a4f59a55e3df
|
Provenance
The following attestation bundles were made for eticket_document_sdk-1.2.0-py3-none-any.whl:
Publisher:
publish.yml on hailong2107/eticket-document-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eticket_document_sdk-1.2.0-py3-none-any.whl -
Subject digest:
60164a703d47c5c4136b2fea1bc59b981d63e410c1d2b28a68d238422be72bf0 - Sigstore transparency entry: 2172385948
- Sigstore integration time:
-
Permalink:
hailong2107/eticket-document-sdk@11310c091c7c253f4fe98563bfd2bd7b6ff0639a -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/hailong2107
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@11310c091c7c253f4fe98563bfd2bd7b6ff0639a -
Trigger Event:
push
-
Statement type: