Unified Python SDK for Vietnam Transportation (Flight, Train, Bus). Enriched data for RAG & AI Agents.
Project description
TravelSDK for Python
TravelSDK is a unified Python SDK for searching and comparing train, bus, and domestic flight tickets in Vietnam in real time. It returns Pydantic-validated data and is async-first, designed specifically for integration into AI Agents and RAG pipelines.
Disclaimer: This is an unofficial SDK, not affiliated with Vexere, VNR, or any airline. For research and educational use only.
Table of Contents
- Key Features
- Architecture
- Installation
- Quickstart — Up and Running in 5 Minutes
- Command Line Interface (CLI)
- Python API Reference
- Data Schema
- Location Format
- Error Handling
- AI Agent / RAG Integration
- Development & Testing
- Changelog
1. Key Features
| Feature | Description |
|---|---|
| Unified Search | Find trains + buses + flights in a single call, running in parallel |
| Smart Fuzzy Matching | Type "hnoi", "hcm", "sgn" — the SDK resolves to the correct location automatically |
| Hierarchical Location | Enter "District 1" or "Cau Giay" — the SDK resolves to the nearest transport hub |
| Offline Location DB | 63 provinces, 160+ train stations, all domestic airports — no network needed for lookup |
| Pydantic Models | All responses are validated, with full IDE autocomplete support |
| Async-first | Built on httpx + asyncio, suitable for server-side applications |
| Auto Token Refresh | Automatically handles token expiry with iterative retry (no recursion) |
| AI-Ready | summary() method exports a dict optimized for LLM consumption |
2. Architecture
TravelClient
├── TokenManager # Bearer token lifecycle (iterative auto-refresh)
├── search_trains() # → train.py → TrainTicket[]
├── search_buses() # → bus.py → BusTicket[]
├── search_flights() # → flight.py→ FlightTicket[]
├── search_all() # asyncio.gather() → SearchResult
│
├── Location Resolution (2 layers)
│ ├── Layer 1 - Local DB (all_locations.json, constants/)
│ │ └── Fuzzy Match (difflib, cutoff=0.7)
│ └── Layer 2 - Remote API (search_areas → get_area_details)
│ └── Hub Resolution (District → Province → hub)
│
└── CLI (travel-sdk) # search + list subcommands
Location Resolution Engine
The SDK uses a 2-layer strategy to resolve location names:
Layer 1 — Local Cache (sync, zero-latency):
- Exact match against station codes (
HNO,SGO) or IATA codes (HAN,SGN) - Fuzzy match via
difflib.get_close_matchesagainst names and aliases (cutoff 0.7) - Lookup from
all_locations.json(bundled, offline) - Lookup from
constants/(common names and aliases)
Layer 2 — Remote API (async, fallback):
- Calls
search_areas(query)to get a list of matching areas - Prioritizes Province type over District
- Hub Resolution: If a district has no hub (station/airport), automatically fetches the parent province to find the actual hub
3. Installation
Requirements: Python 3.9+
# Install from PyPI (stable)
pip install travel-sdk
# Install from GitHub (latest)
pip install git+https://github.com/Qhuy204/TravelSDK.git
# Install for development
git clone https://github.com/Qhuy204/TravelSDK.git
cd TravelSDK
pip install -e ".[dev]"
Dependencies:
httpx >= 0.27— async HTTP clientpydantic >= 2.0— data validation & serialization
4. Quickstart — Up and Running in 5 Minutes
Search All Transport Modes
import asyncio
from travel import TravelClient
async def main():
async with TravelClient() as client:
results = await client.search_all(
from_location="Hanoi",
to_location="Da Nang",
date="2026-05-20",
passengers=1,
)
print(f"Trains: {len(results.trains)} results")
print(f"Buses: {len(results.buses)} results")
print(f"Flights: {len(results.flights)} results")
# Get the cheapest ticket across all modes
cheapest = results.cheapest()
if cheapest:
print(f"\nCheapest: {cheapest}")
# View top 5 cheapest tickets
for ticket in results.all_tickets[:5]:
print(ticket)
asyncio.run(main())
Search Trains
async with TravelClient() as client:
trains = await client.search_trains(
from_location="Hanoi",
to_location="Saigon",
date="2026-05-20",
)
for t in trains:
print(f"{t.train_number} | {t.departure_time}→{t.arrival_time} | {t.min_price:,} VND | {t.seat_available} seats")
Search Flights
async with TravelClient() as client:
flights = await client.search_flights(
from_location="HAN", # IATA code
to_location="SGN",
date="2026-05-20",
passengers=2,
fare_class="economy", # or "business"
)
for f in flights:
print(f"{f.flight_number} | {f.airline_name} | {f.departure_time} | {f.final_price:,} VND | {f.baggage_info}")
Search Buses
async with TravelClient() as client:
buses = await client.search_buses(
from_location="Hanoi",
to_location="Hai Phong",
date="2026-05-20",
)
for b in buses:
print(f"{b.operator.name} | {b.bus_type} | {b.departure_time} | {b.final_price:,} VND | ⭐{b.rating}")
5. Command Line Interface (CLI)
After installation, the SDK exposes the travel-sdk command.
Search
# Search all transport modes (default)
travel-sdk search --from "Hanoi" --to "Da Nang" --date "2026-05-20"
# Search by specific mode
travel-sdk search --from "Hanoi" --to "Saigon" --date "2026-05-20" --mode train
travel-sdk search --from "Hai Phong" --to "Dalat" --date "2026-05-20" --mode flight
travel-sdk search --from "Hanoi" --to "Hai Phong" --date "2026-05-20" --mode bus
# Enable debug logging
travel-sdk search --from "Hanoi" --to "Da Nang" --date "2026-05-20" --verbose
# Fuzzy input works too
travel-sdk search --from "hnoi" --to "sgn" --date "2026-05-20"
Flags:
| Flag | Value | Description |
|---|---|---|
--from |
string | Departure location (required) |
--to |
string | Destination (required) |
--date |
YYYY-MM-DD |
Travel date (default: today) |
--mode |
all, train, bus, flight |
Transport mode (default: all) |
--verbose |
flag | Enable DEBUG logging |
--version |
flag | Display SDK version (from package metadata) |
List Locations
# List all 63 provinces
travel-sdk list provinces
# List airports
travel-sdk list airports
# List train stations
travel-sdk list stations
6. Python API Reference
TravelClient
The main entry point of the SDK. Manages tokens, connection pools, and retry logic.
client = TravelClient(
timeout=30.0, # float — HTTP timeout in seconds. Default: 30.0
max_retries=2, # int — Number of retries on timeout/5xx. Default: 2
verbose=False, # bool — Enable DEBUG logging. Default: False
)
Always use as an async context manager:
async with TravelClient() as client:
# Client initializes tokens and HTTP pool automatically
...
# Client closes connections on exit
Using
TravelClient()without a context manager will raiseRuntimeErrorwhen calling search methods. If you need standalone use, callawait client._init_client()first.
search_trains
trains: list[TrainTicket] = await client.search_trains(
from_location: str, # Departure location
to_location: str, # Destination
date: str, # "YYYY-MM-DD"
passengers: int = 1, # Number of passengers
sort: str = "fare:asc", # Sort order
)
sort options: "fare:asc" | "fare:desc" | "departure_time:asc"
Returns: list[TrainTicket] — list of train trips sorted by sort. Returns [] if no results or location cannot be resolved.
trains = await client.search_trains("Hanoi", "Da Nang", "2026-05-20")
for t in trains:
print(f"{t.train_number}: {t.departure_time}→{t.arrival_time} ({t.duration_str})")
print(f" From: {t.min_price:,} VND | Available: {t.seat_available} seats")
for car in t.cars:
print(f" [{car.car_number}] {car.car_type}: {car.min_price:,} VND ({car.total_available} seats)")
search_buses
buses: list[BusTicket] = await client.search_buses(
from_location: str | int, # Province name or region ID (int)
to_location: str | int, # Province name or region ID (int)
date: str, # "YYYY-MM-DD"
passengers: int = 1,
sort: str = "fare:asc",
page: int = 1, # Pagination
page_size: int = 20, # Results per page (max 100)
)
Tip: You can pass a region ID directly (int) to skip the resolve step. See
travel-sdk list provincesfor ID list.
buses = await client.search_buses("Hanoi", "Hai Phong", "2026-05-20")
for b in buses:
print(f"{b.operator.name} ({b.bus_type}) | {b.departure_time} | {b.final_price:,} VND | ⭐{b.rating}")
if b.pickup_points:
print(f" Pickup: {b.pickup_points[0].address}")
search_flights
flights: list[FlightTicket] = await client.search_flights(
from_location: str, # IATA code or province name
to_location: str, # IATA code or province name
date: str, # "YYYY-MM-DD"
passengers: int = 1,
fare_class: str = "economy", # "economy" | "business"
page: int = 1,
page_size: int = 20,
sort: str = "fare:asc",
)
flights = await client.search_flights("Hanoi", "Saigon", "2026-05-20", fare_class="business")
for f in flights:
print(f"{f.flight_number} ({f.airline_name})")
print(f" {f.departure_time}→{f.arrival_time} | {f.duration_minutes // 60}h{f.duration_minutes % 60}m")
print(f" {f.final_price:,} VND | {f.baggage_info}")
if f.utilities:
print(f" Amenities: {', '.join(f.utilities)}")
search_all
Searches all 3 transport modes in parallel (asyncio.gather), returning a SearchResult.
result: SearchResult = await client.search_all(
from_location: str,
to_location: str,
date: str,
passengers: int = 1,
include_trains: bool = True, # Include train results
include_buses: bool = True, # Include bus results
include_flights: bool = True, # Include flight results
page_size: int = 100, # Results per mode
)
If one mode fails, the remaining ones still return (errors are logged at WARNING level).
result = await client.search_all("Hanoi", "Saigon", "2026-05-20")
# Iterate each mode separately
for t in result.trains: ...
for b in result.buses: ...
for f in result.flights: ...
# All tickets sorted by price ascending
for ticket in result.all_tickets:
print(ticket)
# Overall cheapest ticket
cheapest = result.cheapest()
# Sort by different criteria
by_time = result.sort_tickets(result.trains, by="time")
by_airline = result.sort_tickets(result.flights, by="airline")
# Summary dict (ideal for LLM context)
summary = result.summary()
# {
# "from": "Hanoi", "to": "Saigon", "date": "2026-05-20",
# "train_count": 8, "bus_count": 42, "flight_count": 15,
# "cheapest_train": 380000, "cheapest_bus": 180000, "cheapest_flight": 990000,
# "top_train_info": "Train SE9 (07:00) - 380000 VND",
# "top_bus_info": "Phuong Trang Bus (06:00) - 180000 VND",
# "top_flight_info": "VietJet (06:30) - 990000 VND"
# }
Calendar APIs
Get monthly price calendars for UI display or agent trip planning.
# Train price calendar for May 2026
train_calendar: dict = await client.get_train_calendar(
from_location="Hanoi",
to_location="Saigon",
month=5,
year=2026,
passengers=1,
)
# Flight price calendar for May 2026
flight_calendar: dict = await client.get_flight_calendar(
from_location="HAN",
to_location="SGN",
month=5,
year=2026,
passengers=1,
)
Location Resolution
Use when you need to resolve a location manually without performing a search.
# --- Sync (uses local DB, zero-latency) ---
station = client.resolve_train_station("Hanoi")
# {"code": "HNO", "name": "Ha Noi Station", "location_id": 1, ...}
airport = client.resolve_flight_airport("SGN")
# {"iata": "SGN", "name": "Tan Son Nhat", "city": "Ho Chi Minh City", ...}
bus_region = client.resolve_bus_region("Da Nang")
# {"id": 15, "name": "Da Nang", "slug": "da-nang"}
# --- Async (with API fallback + hub resolution) ---
station = await client.resolve_train_station_async("Cau Giay")
# Resolves: Cau Giay (district) → Hanoi (province) → Ha Noi Station
airport = await client.resolve_flight_airport_async("District 1")
# Resolves: District 1 → Ho Chi Minh City → Tan Son Nhat Airport
# --- Offline DB helpers ---
provinces = client.get_provinces() # list[dict] — 63 provinces
airports = client.get_airports() # list[dict] — all airports
stations = client.get_train_stations() # list[dict] — all train stations
7. Data Schema
TrainTicket
class TrainTicket(BaseModel):
type: TicketType # Always TicketType.TRAIN
id_index: str # Internal API ID
train_number: str # Train code, e.g. "SE9", "TN1"
operator: Provider # {"code": "VNR", "name": "Vietnam Railways"}
from_code: str # Departure station code, e.g. "HNO"
to_code: str # Arrival station code, e.g. "SGO"
from_name: str # Departure station name
to_name: str # Arrival station name
departure_date: str # "YYYY-MM-DD"
departure_time: str # "HH:MM"
arrival_date: str # "YYYY-MM-DD"
arrival_time: str # "HH:MM"
duration_minutes: int # Travel duration in minutes
duration_str: str # Property: "32h05m"
distance_km: int # Distance in km
min_price: int # Lowest price (VND)
seat_available: int # Total available seats
cars: list[CarInfo] # Per-car details (see CarInfo below)
seat_types: list[str] # Available seat types, e.g. ["Soft Seat", "6-Berth Sleeper"]
utilities: list[str] # Amenities, e.g. ["Air Conditioning", "WiFi"]
policies: list[str] # Policies, e.g. ["Refund 24h before departure"]
promotions: list[str] # Promotions
baggage_info: str # Baggage information
description: str # Trip description
images: list[str] # Image URLs
highlights: list[str] # Highlights
CarInfo (part of TrainTicket.cars):
class CarInfo(BaseModel):
car_id: int
car_number: str # "1", "2A"
car_type: str # "Air-conditioned Soft Seat", "6-Berth Sleeper"
group_code: str # "NGM", "NAC", "NAM"
total_available: int # Available seats in this car
min_price: int # Lowest price in this car (VND)
seat_options: list[SeatOption] # Details per seat/berth type
SeatOption (part of CarInfo.seat_options):
class SeatOption(BaseModel):
code: str # Internal code
label: str # Display name, e.g. "4-Berth AC Sleeper"
seat_class: SeatClass # Enum: SOFT_SEAT, HARD_SEAT, SLEEPER_6, SLEEPER_4, VIP, ...
price: int # Base price (VND)
markup_price: Optional[int] # Price after markup
available: int # Remaining seats
BusTicket
class BusTicket(BaseModel):
type: TicketType # Always TicketType.BUS
operator: Provider # {"code": "...", "name": "Phuong Trang"}
from_id: int # Bus region ID for departure
to_id: int # Bus region ID for destination
from_name: str
to_name: str
departure_date: str # "YYYY-MM-DD"
departure_time: str # "HH:MM"
arrival_time: str # "HH:MM"
duration_minutes: int
bus_type: str # "Limousine", "40-seat Sleeper", "Sleeper VIP"
seat_available: int
price: int # Base price (VND)
final_price: int # Price after discount (VND)
pickup_points: list[PointInfo] # Pickup locations
dropoff_points: list[PointInfo]# Drop-off locations
rating: float # Average rating (0.0–5.0)
reviews: int # Number of reviews
utilities: list[str] # Amenities, e.g. ["WiFi", "AC", "USB Charger"]
policies: list[str]
promotions: list[str]
baggage_info: str
description: str
PointInfo (part of BusTicket.pickup_points / dropoff_points):
class PointInfo(BaseModel):
name: str # Stop name
address: str # Full address
time_offset_minutes: int # Offset from departure time
lat: float # Latitude
lon: float # Longitude
FlightTicket
class FlightTicket(BaseModel):
type: TicketType # Always TicketType.FLIGHT
id_index: str # Internal ID
airline_code: str # "VJ", "VN", "QH", "BL"
airline_name: str # "VietJet Air", "Vietnam Airlines"
flight_number: str # "VJ123", "VN204"
cabin: str # "Economy", "Business"
airplane_name: str # "Airbus A321", "Boeing 787"
is_non_stop: bool # True if direct flight
from_iata: str # "HAN", "DAD", "SGN"
to_iata: str
from_name: str
to_name: str
departure_date: str # "YYYY-MM-DD"
departure_time: str # "HH:MM"
arrival_time: str # "HH:MM"
duration_minutes: int
price: int # Pre-tax price (VND)
final_price: int # All-inclusive price (VND)
baggage_info: str # "7kg carry-on + 20kg checked"
utilities: list[str] # Included amenities
policies: list[str] # Change/refund policies
promotions: list[str]
description: str
SearchResult
Object returned by search_all().
class SearchResult(BaseModel):
query_from: str
query_to: str
query_date: str
query_passengers: int
trains: list[TrainTicket]
buses: list[BusTicket]
flights: list[FlightTicket]
searched_at: Optional[str] # ISO timestamp
# Properties & Methods:
all_tickets # property: all tickets sorted by price ascending
cheapest() # → the overall cheapest ticket
summary() # → summary dict (see below)
sort_tickets(tickets, by, reverse) # sort by "price", "time", "airline"/"operator"
summary() output:
{
"from": "Hanoi",
"to": "Saigon",
"date": "2026-05-20",
"train_count": 8,
"bus_count": 42,
"flight_count": 15,
"cheapest_train": 380000, # VND, or None if not available
"cheapest_bus": 180000,
"cheapest_flight": 990000,
"top_train_info": "Train SE9 (07:00) - 380000 VND",
"top_bus_info": "Phuong Trang Bus (06:00) - 180000 VND",
"top_flight_info": "VietJet (06:30) - 990000 VND"
}
8. Location Format
The SDK accepts multiple location input formats:
| Input | Type | Example |
|---|---|---|
| Province/city name | Fuzzy match | "Ha Noi", "hanoi", "Saigon", "tp hcm" |
| Common alias | Hard mapping | "hn", "hcm", "tphcm", "sgn" |
| IATA code | Exact match | "HAN", "SGN", "DAD", "HUI" |
| Train station code | Exact match | "HNO", "SGO", "DAN" |
| District/ward name | Hub resolution | "District 1", "Cau Giay", "Hoan Kiem" |
| Bus region ID | Direct (int) | 1, 34, 2 |
Fuzzy matching is powered by difflib.get_close_matches with a threshold of 0.7, so minor typos like "hnoi" or "danang" are resolved correctly.
Main airport IATA codes:
| Airport | IATA | Province |
|---|---|---|
| Noi Bai | HAN |
Hanoi |
| Tan Son Nhat | SGN |
Ho Chi Minh City |
| Da Nang | DAD |
Da Nang |
| Cam Ranh | CXR |
Khanh Hoa (Nha Trang) |
| Phu Quoc | PQC |
Kien Giang |
| Cat Bi | HPH |
Hai Phong |
| Phu Bai | HUI |
Thua Thien Hue |
| Lien Khuong | DLI |
Lam Dong (Da Lat) |
| Can Tho | VCA |
Can Tho |
Main train station codes:
| Station | Code |
|---|---|
| Ha Noi | HNO |
| Sai Gon | SGO |
| Da Nang | DAN |
| Hue | HUE |
| Nha Trang | NTR |
See full lists: travel-sdk list stations or travel-sdk list airports
9. Error Handling
Location Not Found
The SDK returns [] (empty list) instead of raising an exception when a location cannot be resolved. Check logs to debug:
import logging
logging.basicConfig(level=logging.INFO)
async with TravelClient() as client:
trains = await client.search_trains("XYZ nowhere", "Da Nang", "2026-05-20")
if not trains:
print("No trains found — check the location name")
Network Errors & Retry
TravelClient automatically retries on:
httpx.TimeoutException→ retry with exponential backoff (1s, 2s)- HTTP 5xx → retry with exponential backoff
- HTTP 401 → refresh token then retry
After retries are exhausted, the exception is raised for the application to handle:
import httpx
try:
async with TravelClient(timeout=10.0, max_retries=3) as client:
trains = await client.search_trains("Hanoi", "Da Nang", "2026-05-20")
except httpx.TimeoutException:
print("Request timed out after 10 seconds")
except httpx.HTTPStatusError as e:
print(f"HTTP error: {e.response.status_code}")
except Exception as e:
print(f"Unexpected error: {e}")
Partial Failure in search_all
If one mode fails in search_all, the remaining modes still return results:
result = await client.search_all("Hanoi", "Saigon", "2026-05-20")
# If search_flights fails → result.flights = [], trains and buses still have data
# Errors are logged at WARNING level
10. AI Agent / RAG Integration
The SDK is optimized for integration into LLM agents. Recommended patterns:
Use summary() as LLM Context
async def search_travel_for_agent(origin: str, destination: str, date: str) -> str:
"""Tool function for an LLM agent."""
async with TravelClient() as client:
result = await client.search_all(origin, destination, date)
summary = result.summary()
return f"""
Travel search results: {summary['from']} → {summary['to']} on {summary['date']}
Flights: {summary['flight_count']} options, cheapest at {summary.get('cheapest_flight', 'N/A')} VND
{summary.get('top_flight_info', 'None available')}
Trains: {summary['train_count']} options, cheapest at {summary.get('cheapest_train', 'N/A')} VND
{summary.get('top_train_info', 'None available')}
Buses: {summary['bus_count']} options, cheapest at {summary.get('cheapest_bus', 'N/A')} VND
{summary.get('top_bus_info', 'None available')}
""".strip()
Tool Definition for OpenAI / Claude Function Calling
travel_search_tool = {
"name": "search_travel",
"description": "Search for real-time train, bus, and domestic flight tickets in Vietnam.",
"parameters": {
"type": "object",
"properties": {
"from_location": {
"type": "string",
"description": "Departure location (province name, IATA code, or station code)"
},
"to_location": {
"type": "string",
"description": "Destination"
},
"date": {
"type": "string",
"description": "Travel date in YYYY-MM-DD format"
},
"mode": {
"type": "string",
"enum": ["all", "train", "bus", "flight"],
"description": "Transport mode to search"
}
},
"required": ["from_location", "to_location", "date"]
}
}
Handling Tool Calls in an Agent
async def handle_tool_call(tool_name: str, args: dict) -> str:
if tool_name == "search_travel":
async with TravelClient() as client:
mode = args.get("mode", "all")
date = args["date"]
origin = args["from_location"]
dest = args["to_location"]
if mode == "train":
tickets = await client.search_trains(origin, dest, date)
return format_trains(tickets)
elif mode == "flight":
tickets = await client.search_flights(origin, dest, date)
return format_flights(tickets)
elif mode == "bus":
tickets = await client.search_buses(origin, dest, date)
return format_buses(tickets)
else:
result = await client.search_all(origin, dest, date)
return str(result.summary())
11. Development & Testing
Install Dev Dependencies
pip install -e ".[dev]"
# Includes: pytest, pytest-asyncio, python-dotenv
Run Tests
# All tests
pytest tests/
# Location resolution tests (offline, no API needed)
pytest tests/test_locations.py
# Model tests (offline)
pytest tests/test_models.py
# Live API tests (requires internet)
pytest tests/test_live_api.py -v
# With verbose output and Windows UTF-8 support
$env:PYTHONUTF8=1; pytest tests/ -v
# With detailed logging
pytest tests/ -v --log-cli-level=DEBUG
Project Structure
TravelSDK/
├── travel/ # Main package
│ ├── __init__.py # Public API exports
│ ├── client.py # TravelClient entry point
│ ├── auth.py # TokenManager (iterative refresh)
│ ├── flight.py # Flight search logic
│ ├── train.py # Train search logic
│ ├── bus.py # Bus search logic
│ ├── locations.py # Location resolution engine (fuzzy matching)
│ ├── cli.py # CLI entry point
│ ├── py.typed # PEP 561 marker
│ ├── constants/ # URLs, province codes, station codes
│ │ ├── locations.py
│ │ ├── transport.py
│ │ └── urls.py
│ ├── models/ # Pydantic schemas
│ │ ├── base.py # TicketType, Provider, SeatOption, PointInfo
│ │ ├── train.py # TrainTicket, CarInfo
│ │ ├── bus.py # BusTicket
│ │ ├── flight.py # FlightTicket
│ │ └── search.py # SearchResult
│ └── data/
│ └── all_locations.json # Offline location database
├── tests/ # Unit & integration tests
│ ├── conftest.py # Shared pytest fixtures
│ ├── test_locations.py
│ ├── test_models.py
│ ├── test_live_api.py
│ └── test_real_searches.py
├── examples/ # Usage examples
│ └── search_demo.py
├── CHANGELOG.md
├── pyproject.toml
└── README.md
12. Changelog
v0.1.4 (2026-04-19)
- English Documentation: Rewrote full README in English for broader audience.
- Version Alignment: Fixed
pyproject.tomlversion mismatch that caused PyPI upload conflicts.
v0.1.3 (2026-04-19)
- Smart Fuzzy Matching: Integrated
difflib.get_close_matchesfor all 3 modes — Train, Flight, Bus. Input likehnoi,sgn,hcmis resolved automatically. - CLI Bug Fix: Fixed
AttributeErrorwhen displaying bus results (b.operator_name→b.operator.name). - Dynamic Versioning:
--versionflag reads from package metadata instead of hardcoded string. - Accurate
summary(): Usesmin()instead of[0]to guarantee the lowest price is always correct. - Auth Stability: Refactored
TokenManager._acquire()to use iterative retry loop, eliminating recursion-causedRuntimeError: Event loop is closed. - PEP 561: Added
travel/py.typedmarker for full IDE type hint support. - Test Suite: Converted all test scripts to standard
pytest-asyncio. Addedtests/conftest.py, achieving 35/35 tests passed. - Structure: Moved test files into
tests/, addedscratch/to.gitignore.
v0.1.2 (2026-04-19)
- Added
include_trains/buses/flightsflags tosearch_all - Improved hierarchical hub resolution for district-level queries
- CLI: added
--modeflag and--list/--searchaliases - Fixed PyPI version conflict by bumping version
v0.1.1 (2026-04-19)
- Fixed token invalidation on 401 responses
- Added exponential backoff for retries
- Registered
travel-sdkCLI entry point
v0.1.0 (2026-04-18)
- Initial release
- Unified search API for trains, buses, and flights
- Offline location database (63 provinces, 160+ stations, all domestic airports)
- Hierarchical location resolution (District → Province Hub)
License
MIT License — Copyright (c) 2026 Qhuy204
This SDK is unofficial and not endorsed by Vexere, VNR, or any airline. For research and educational use only.
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 travel_sdk-0.1.4.tar.gz.
File metadata
- Download URL: travel_sdk-0.1.4.tar.gz
- Upload date:
- Size: 64.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f347ac35f4178043f55aeac06ef196d4b539192c62f08015c1c9f4d876f51e9
|
|
| MD5 |
a0be066d8b250dc7968f660dcfb6608d
|
|
| BLAKE2b-256 |
a6de98513b63e288f855abc163c3b78486179e636c5e8069bd86c8540a80fc74
|
Provenance
The following attestation bundles were made for travel_sdk-0.1.4.tar.gz:
Publisher:
workflow.yml on Qhuy204/TravelSDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
travel_sdk-0.1.4.tar.gz -
Subject digest:
5f347ac35f4178043f55aeac06ef196d4b539192c62f08015c1c9f4d876f51e9 - Sigstore transparency entry: 1340128258
- Sigstore integration time:
-
Permalink:
Qhuy204/TravelSDK@a75158e3389f66d00b0011cbd6b978bd757ee8c4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Qhuy204
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@a75158e3389f66d00b0011cbd6b978bd757ee8c4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file travel_sdk-0.1.4-py3-none-any.whl.
File metadata
- Download URL: travel_sdk-0.1.4-py3-none-any.whl
- Upload date:
- Size: 46.4 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 |
14f5ad97c725cc24bd9232de0e7c615f8e1b219d750124dbe01548f59afad00a
|
|
| MD5 |
f1d8a6dd292a90d78932e0980e22e21a
|
|
| BLAKE2b-256 |
9814a70d16b6bb1ca7b053b86eab21310eaa2eccad0cdccbdd317e4866e0e7f9
|
Provenance
The following attestation bundles were made for travel_sdk-0.1.4-py3-none-any.whl:
Publisher:
workflow.yml on Qhuy204/TravelSDK
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
travel_sdk-0.1.4-py3-none-any.whl -
Subject digest:
14f5ad97c725cc24bd9232de0e7c615f8e1b219d750124dbe01548f59afad00a - Sigstore transparency entry: 1340128265
- Sigstore integration time:
-
Permalink:
Qhuy204/TravelSDK@a75158e3389f66d00b0011cbd6b978bd757ee8c4 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Qhuy204
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@a75158e3389f66d00b0011cbd6b978bd757ee8c4 -
Trigger Event:
push
-
Statement type: