Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions fast_flights/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
from . import integrations

from .querying import (
FlightQuery,
Query,
Passengers,
create_query,
create_query as create_filter, # alias
)
from .exceptions import FastFlightsError, APIError
from .querying import FlightQuery, Passengers, create_query
from .fetcher import get_flights, fetch_flights_html

# Create alias for backward compatibility
create_filter = create_query

__all__ = [
# Core functionality
"FlightQuery",
"Query",
"Passengers",
"create_query",
"create_filter",
"get_flights",
"fetch_flights_html",
"integrations",

# Public exceptions
"FastFlightsError",
"APIError"
]
33 changes: 33 additions & 0 deletions fast_flights/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Custom exceptions for the fast_flights package."""

class FastFlightsError(Exception):
"""Base exception for all fast_flights exceptions."""
pass

class ValidationError(FastFlightsError, ValueError):
"""Raised when input validation fails."""
pass

class AirportCodeError(ValidationError):
"""Raised when an invalid airport code is provided."""
pass

class DateFormatError(ValidationError):
"""Raised when a date string is in an invalid format."""
pass

class PassengerError(ValidationError):
"""Raised when there's an issue with passenger configuration."""
pass

class FlightQueryError(ValidationError):
"""Raised when there's an issue with flight query parameters."""
pass

class APIConnectionError(FastFlightsError):
"""Raised when there's an issue connecting to the flight data API."""
pass

class APIError(FastFlightsError):
"""Raised when the flight data API returns an error."""
pass
125 changes: 100 additions & 25 deletions fast_flights/fetcher.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import logging
from typing import Optional, Union, overload

from primp import Client

from .exceptions import APIConnectionError, APIError
from .querying import Query
from .parser import MetaList, parse
from .integrations import Integration

# Set up logging
logger = logging.getLogger(__name__)

URL = "https://www.google.com/travel/flights"


Expand Down Expand Up @@ -54,11 +59,28 @@ def get_flights(
"""Get flights.

Args:
q: The query.
proxy (str, optional): Proxy.
q: The query string or Query object.
proxy: Optional proxy configuration.
integration: Optional integration to use for fetching data.

Returns:
MetaList: Parsed flight data.

Raises:
APIConnectionError: If there's an issue connecting to the flight data source.
APIError: If the API returns an error or invalid response.
ValueError: If the input query is invalid.
"""
html = fetch_flights_html(q, proxy=proxy, integration=integration)
return parse(html)
try:
logger.debug("Fetching flight data...")
html = fetch_flights_html(q, proxy=proxy, integration=integration)
if not html or not isinstance(html, str):
raise APIError("Received empty or invalid response from the flight data source")
return parse(html)
except Exception as e:
if isinstance(e, (APIConnectionError, APIError, ValueError)):
raise
raise APIConnectionError(f"Failed to fetch flight data: {str(e)}") from e


def fetch_flights_html(
Expand All @@ -68,29 +90,82 @@ def fetch_flights_html(
proxy: Optional[str] = None,
integration: Optional[Integration] = None,
) -> str:
"""Fetch flights and get the **HTML**.
"""Fetch flights and get the HTML response.

Args:
q: The query.
proxy (str, optional): Proxy.
q: The query string or Query object.
proxy: Optional proxy configuration.
integration: Optional integration to use for fetching data.

Returns:
str: The HTML content of the flight search results.

Raises:
APIConnectionError: If there's an issue connecting to the flight data source.
APIError: If the API returns an error or invalid response.
ValueError: If the input query is invalid.
"""
if integration is None:
client = Client(
impersonate="chrome_133",
impersonate_os="macos",
referer=True,
proxy=proxy,
cookie_store=True,
)

if isinstance(q, Query):
params = q.params()
if not q:
raise ValueError("Query cannot be empty")

try:
if integration is None:
logger.debug("Using default client for fetching flight data")
client = Client(
impersonate="chrome_133",
impersonate_os="macos",
referer=True,
proxy=proxy,
cookie_store=True,
timeout=30, # 30 seconds timeout
)

try:
if isinstance(q, Query):
params = q.params()
else:
if not isinstance(q, str):
raise ValueError("Query must be a string or Query object")
params = {"q": q}

logger.debug(f"Sending request to {URL} with params: {params}")
res = client.get(URL, params=params)

# Check status code directly since primp's client might not have raise_for_status
if res.status_code >= 400:
error_msg = f"Flight data API returned status code {res.status_code}"
logger.error(error_msg)
raise APIError(error_msg)

if not res.text:
error_msg = "Received empty response from the flight data source"
logger.error(error_msg)
raise APIError(error_msg)

return res.text

except APIError:
# Re-raise APIError as is
raise
except ValueError as e:
# Re-raise ValueError as is
logger.error(f"Invalid query: {str(e)}")
raise
except Exception as e:
# Handle other exceptions
error_msg = f"Failed to connect to flight data source: {str(e)}"
logger.error(error_msg)
raise APIConnectionError(error_msg) from e

else:
params = {"q": q}

res = client.get(URL, params=params)
return res.text

else:
return integration.fetch_html(q)
logger.debug("Using integration for fetching flight data")
try:
return integration.fetch_html(q)
except Exception as e:
logger.error(f"Integration error while fetching flight data: {str(e)}")
raise APIError(f"Integration failed to fetch flight data: {str(e)}") from e

except Exception as e:
if isinstance(e, (APIConnectionError, APIError, ValueError)):
raise
raise APIConnectionError(f"Unexpected error while fetching flight data: {str(e)}") from e
62 changes: 44 additions & 18 deletions fast_flights/integrations/base.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,64 @@
"""Base integration module for flight data providers."""
import logging
import os
from abc import ABC, abstractmethod
from typing import Union, Optional

from abc import ABC
from typing import Union

from ..exceptions import APIConnectionError, APIError
from ..querying import Query

# Set up logging
logger = logging.getLogger(__name__)

try:
import dotenv # pip install python-dotenv

dotenv.load_dotenv()

except ModuleNotFoundError:
pass
logger.debug("python-dotenv not installed, skipping .env file loading")


class Integration(ABC):
"""Abstract base class for flight data integrations.

This class defines the interface that all flight data integrations must implement.
Subclasses should implement the fetch_html method to retrieve flight data.
"""

@abstractmethod
def fetch_html(self, q: Union[Query, str], /) -> str:
"""Fetch the flights page HTML from a query.

Args:
q: The query.
q: The query string or Query object.

Returns:
str: The HTML content of the flight search results.

Raises:
APIConnectionError: If there's an issue connecting to the data source.
APIError: If the API returns an error or invalid response.
ValueError: If the input query is invalid.
"""
raise NotImplementedError


def get_env(k: str, /) -> str:
"""(utility) Get environment variable.
raise NotImplementedError("Subclasses must implement this method")

If nothing found, raises an error.

def get_env(k: str, /, default: Optional[str] = None) -> str:
"""Get environment variable with optional default value.

Args:
k: The name of the environment variable.
default: Default value to return if the environment variable is not found.
If not provided, raises an OSError when the variable is not found.

Returns:
str: The value.
str: The value of the environment variable, or the default value if provided.

Raises:
OSError: If the environment variable is not found and no default is provided.
"""
try:
return os.environ[k]
except KeyError:
raise OSError(f"could not find environment variable: {k!r}")
value = os.environ.get(k, default)
if value is None:
error_msg = f"Required environment variable not found: {k!r}"
logger.error(error_msg)
raise OSError(error_msg)
return value
Loading