Skip to content

eve-esi-link package.

CLI and library interface for working with EVE Online ESI.

For library usage, EsiLink is the primary entrypoint.

EsiLink

Execute ESI request groups with schema validation and runtime services.

This class must be used as an async context manager so request and auth backends are initialized and cleaned up correctly.

Source code in src/pfmsoft/eve_link/esi_link.py
class EsiLink:
    """Execute ESI request groups with schema validation and runtime services.

    This class must be used as an async context manager so request and auth
    backends are initialized and cleaned up correctly.
    """

    def __init__(
        self,
        auth_manager_db_path: Path,
        web_cache_path: Path,
        max_rate: float = 20.0,
        time_period: float = 1.0,
    ):
        """Configure an EsiLink instance.

        Args:
            auth_manager_db_path: Path to the SqliteAuthManager database.
            web_cache_path: Path to the HTTP response cache database.
            max_rate: Maximum number of requests per rate-limit window.
            time_period: Window duration in seconds for rate limiting.
        """
        self.api_requester: api_request.ApiRequester | None = None
        self.auth_manager: SqliteAuthManager | None = None
        self.auth_manager_db_path = auth_manager_db_path
        self.web_cache_path = web_cache_path
        self.max_rate = max_rate
        self.time_period = time_period

    async def __aenter__(self) -> Self:
        """Initialize API requester and auth manager resources.

        Returns:
            The initialized EsiLink instance.
        """
        web_cache_factory = SqliteCacheFactory(db_path=self.web_cache_path)
        rate_limiter_factory = AiolimiterRateLimiterFactory(
            max_rate=self.max_rate, time_period=self.time_period
        )
        self.api_requester = api_request.ApiRequester(
            cache_factory=web_cache_factory, rate_limiter_factory=rate_limiter_factory
        )
        self.auth_manager = SqliteAuthManager(db_path=self.auth_manager_db_path)
        await self.api_requester.__aenter__()
        self.auth_manager.__enter__()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ):
        """Release API requester and auth manager resources."""
        if self.api_requester is not None:
            await self.api_requester.__aexit__(exc_type, exc_value, traceback)
        if self.auth_manager is not None:
            self.auth_manager.__exit__(exc_type, exc_value, traceback)

    @classmethod
    def from_settings(cls, settings: EsiLinkSettings) -> Self:
        """Create an EsiLink instance from EsiLinkSettings.

        Args:
            settings: EsiLinkSettings object containing configuration parameters.

        Returns:
            EsiLink: An initialized EsiLink instance.
        """
        return cls(
            auth_manager_db_path=settings.eve_auth_manager_settings.authorization_database_path,
            web_cache_path=settings.api_request_settings.web_cache_path,
            max_rate=settings.max_rate,
            time_period=settings.time_period,
        )

    def _check_api_requester_initialized(self) -> api_request.ApiRequester:
        """Return initialized ApiRequester instance.

        Returns:
            Initialized ApiRequester.

        Raises:
            RuntimeError: If EsiLink is used outside async context manager scope.
        """
        if self.api_requester is None:
            raise RuntimeError("EsiLink must be used as an async context manager.")
        return self.api_requester

    def _check_auth_manager(self) -> SqliteAuthManager:
        """Return initialized SqliteAuthManager instance.

        Returns:
            Initialized SqliteAuthManager.

        Raises:
            RuntimeError: If EsiLink is used outside async context manager scope.
        """
        if self.auth_manager is None:
            raise RuntimeError("EsiLink must be used as an async context manager.")
        return self.auth_manager

    def _check_operation(
        self, operation_id: str, esi_schema: EsiSchema
    ) -> SchemaOperation:
        """Check if the operation_id exists in the schema.

        Args:
            operation_id (str): The operation ID to check.
            esi_schema (EsiSchema): The schema to check against.

        Returns:
            SchemaOperation: The schema operation corresponding to the operation ID.

        Raises:
            ValueError: If the operation ID is not found in the schema.
        """
        operation = esi_schema.operations.get(operation_id)
        if operation is None:
            raise ValueError(f"Operation ID '{operation_id}' not found in ESI schema.")
        return operation

    async def _attach_access_token_if_required(
        self,
        esi_request: EsiRequest,
        runtime_esi_request: RuntimeEsiRequest,
    ) -> None:
        """Attach access token to runtime request when authorization is required.

        Args:
            esi_request: The ESI request to check.
            runtime_esi_request: Runtime request to mutate with access token.

        Raises:
            ValueError: If authorization tuple is incomplete.
        """
        if not esi_request.has_authorization:
            return
        auth_manager = self._check_auth_manager()
        cred_id = esi_request.auth_credential_id
        character_id = esi_request.auth_character_id
        if cred_id is None or character_id is None:
            raise ValueError(
                "Credential ID and Character ID must be provided for authorized requests."
            )
        access_token = auth_manager.refresh_character(
            cred_id, character_id
        ).access_token
        runtime_esi_request.access_token = access_token

    async def make_request(
        self, esi_request: EsiRequest, schema: EsiSchema
    ) -> EsiResponse | FailedEsiResponse:
        """Validate, execute, and return a single ESI request response.

        Args:
            esi_request: The ESI request to execute.
            schema: Schema used for operation lookup and validation.

        Returns:
            EsiResponse | FailedEsiResponse: The response for the ESI request.

        Raises:
            RuntimeError: If EsiLink is used outside async context manager scope.
            EsiRequestValidationErrors: If the request fails schema validation.
            Exception: Any backend execution exception from request or auth layers.
        """
        request_group = EsiRequestGroup(
            name="single_request_group",
            description="A group containing a single ESI request.",
            requests={esi_request.request_id: esi_request},
        )
        response_group = await self.make_requests(request_group, schema)
        if esi_request.request_id in response_group.failed_responses:
            return response_group.failed_responses[esi_request.request_id]
        elif esi_request.request_id in response_group.successful_responses:
            return response_group.successful_responses[esi_request.request_id]
        else:
            raise RuntimeError(
                f"Response for request ID {esi_request.request_id} not found in response group."
            )

    async def make_requests(
        self,
        esi_requests: EsiRequestGroup,
        schema: EsiSchema,
    ) -> EsiResponseGroup:
        """Validate, execute, and group responses for an ESI request batch.

        Args:
            esi_requests: ESI requests to execute.
            schema: Schema used for operation lookup and validation.

        Returns:
            Grouped successful and failed responses keyed by runtime request key.

        Raises:
            RuntimeError: If EsiLink is used outside async context manager scope.
            EsiRequestValidationErrors: If any request fails schema validation.
            Exception: Any backend execution exception from request or auth layers.
        """
        requester = self._check_api_requester_initialized()

        runtime_requests: dict[UUID, RuntimeEsiRequest] = {}
        for _, request in esi_requests.requests.items():
            self.validate_request(request, schema)
            runtime_request = build_runtime_esi_request(request, schema)
            await self._attach_access_token_if_required(request, runtime_request)
            runtime_requests[runtime_request.request_key] = runtime_request

        request_objects = {
            key: _make_request_from_runtime_request(request)
            for key, request in runtime_requests.items()
        }
        responses: Responses = await requester.process_requests(request_objects)
        esi_responses = _make_esi_response_group(
            responses, esi_requests, runtime_requests
        )
        return esi_responses

    @staticmethod
    def validate_request(
        esi_request: EsiRequest,
        schema: EsiSchema,
    ) -> None:
        """Validate one ESI request against the provided schema.

        Args:
            esi_request: The ESI request to validate.
            schema: Schema used for validation rules.

        Raises:
            EsiRequestValidationErrors: If request data violates schema-derived rules.
        """
        try:
            validate_esi_request(esi_request, schema)
        except EsiRequestValidationErrors as e:
            logger.error("Validation failed for request %s: %s", esi_request, e)
            raise

__aenter__() -> Self async

Initialize API requester and auth manager resources.

Returns:

Type Description
Self

The initialized EsiLink instance.

Source code in src/pfmsoft/eve_link/esi_link.py
async def __aenter__(self) -> Self:
    """Initialize API requester and auth manager resources.

    Returns:
        The initialized EsiLink instance.
    """
    web_cache_factory = SqliteCacheFactory(db_path=self.web_cache_path)
    rate_limiter_factory = AiolimiterRateLimiterFactory(
        max_rate=self.max_rate, time_period=self.time_period
    )
    self.api_requester = api_request.ApiRequester(
        cache_factory=web_cache_factory, rate_limiter_factory=rate_limiter_factory
    )
    self.auth_manager = SqliteAuthManager(db_path=self.auth_manager_db_path)
    await self.api_requester.__aenter__()
    self.auth_manager.__enter__()
    return self

__aexit__(exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) async

Release API requester and auth manager resources.

Source code in src/pfmsoft/eve_link/esi_link.py
async def __aexit__(
    self,
    exc_type: type[BaseException] | None,
    exc_value: BaseException | None,
    traceback: TracebackType | None,
):
    """Release API requester and auth manager resources."""
    if self.api_requester is not None:
        await self.api_requester.__aexit__(exc_type, exc_value, traceback)
    if self.auth_manager is not None:
        self.auth_manager.__exit__(exc_type, exc_value, traceback)

__init__(auth_manager_db_path: Path, web_cache_path: Path, max_rate: float = 20.0, time_period: float = 1.0)

Configure an EsiLink instance.

Parameters:

Name Type Description Default
auth_manager_db_path Path

Path to the SqliteAuthManager database.

required
web_cache_path Path

Path to the HTTP response cache database.

required
max_rate float

Maximum number of requests per rate-limit window.

20.0
time_period float

Window duration in seconds for rate limiting.

1.0
Source code in src/pfmsoft/eve_link/esi_link.py
def __init__(
    self,
    auth_manager_db_path: Path,
    web_cache_path: Path,
    max_rate: float = 20.0,
    time_period: float = 1.0,
):
    """Configure an EsiLink instance.

    Args:
        auth_manager_db_path: Path to the SqliteAuthManager database.
        web_cache_path: Path to the HTTP response cache database.
        max_rate: Maximum number of requests per rate-limit window.
        time_period: Window duration in seconds for rate limiting.
    """
    self.api_requester: api_request.ApiRequester | None = None
    self.auth_manager: SqliteAuthManager | None = None
    self.auth_manager_db_path = auth_manager_db_path
    self.web_cache_path = web_cache_path
    self.max_rate = max_rate
    self.time_period = time_period

from_settings(settings: EsiLinkSettings) -> Self classmethod

Create an EsiLink instance from EsiLinkSettings.

Parameters:

Name Type Description Default
settings EsiLinkSettings

EsiLinkSettings object containing configuration parameters.

required

Returns:

Name Type Description
EsiLink Self

An initialized EsiLink instance.

Source code in src/pfmsoft/eve_link/esi_link.py
@classmethod
def from_settings(cls, settings: EsiLinkSettings) -> Self:
    """Create an EsiLink instance from EsiLinkSettings.

    Args:
        settings: EsiLinkSettings object containing configuration parameters.

    Returns:
        EsiLink: An initialized EsiLink instance.
    """
    return cls(
        auth_manager_db_path=settings.eve_auth_manager_settings.authorization_database_path,
        web_cache_path=settings.api_request_settings.web_cache_path,
        max_rate=settings.max_rate,
        time_period=settings.time_period,
    )

make_request(esi_request: EsiRequest, schema: EsiSchema) -> EsiResponse | FailedEsiResponse async

Validate, execute, and return a single ESI request response.

Parameters:

Name Type Description Default
esi_request EsiRequest

The ESI request to execute.

required
schema EsiSchema

Schema used for operation lookup and validation.

required

Returns:

Type Description
EsiResponse | FailedEsiResponse

EsiResponse | FailedEsiResponse: The response for the ESI request.

Raises:

Type Description
RuntimeError

If EsiLink is used outside async context manager scope.

EsiRequestValidationErrors

If the request fails schema validation.

Exception

Any backend execution exception from request or auth layers.

Source code in src/pfmsoft/eve_link/esi_link.py
async def make_request(
    self, esi_request: EsiRequest, schema: EsiSchema
) -> EsiResponse | FailedEsiResponse:
    """Validate, execute, and return a single ESI request response.

    Args:
        esi_request: The ESI request to execute.
        schema: Schema used for operation lookup and validation.

    Returns:
        EsiResponse | FailedEsiResponse: The response for the ESI request.

    Raises:
        RuntimeError: If EsiLink is used outside async context manager scope.
        EsiRequestValidationErrors: If the request fails schema validation.
        Exception: Any backend execution exception from request or auth layers.
    """
    request_group = EsiRequestGroup(
        name="single_request_group",
        description="A group containing a single ESI request.",
        requests={esi_request.request_id: esi_request},
    )
    response_group = await self.make_requests(request_group, schema)
    if esi_request.request_id in response_group.failed_responses:
        return response_group.failed_responses[esi_request.request_id]
    elif esi_request.request_id in response_group.successful_responses:
        return response_group.successful_responses[esi_request.request_id]
    else:
        raise RuntimeError(
            f"Response for request ID {esi_request.request_id} not found in response group."
        )

make_requests(esi_requests: EsiRequestGroup, schema: EsiSchema) -> EsiResponseGroup async

Validate, execute, and group responses for an ESI request batch.

Parameters:

Name Type Description Default
esi_requests EsiRequestGroup

ESI requests to execute.

required
schema EsiSchema

Schema used for operation lookup and validation.

required

Returns:

Type Description
EsiResponseGroup

Grouped successful and failed responses keyed by runtime request key.

Raises:

Type Description
RuntimeError

If EsiLink is used outside async context manager scope.

EsiRequestValidationErrors

If any request fails schema validation.

Exception

Any backend execution exception from request or auth layers.

Source code in src/pfmsoft/eve_link/esi_link.py
async def make_requests(
    self,
    esi_requests: EsiRequestGroup,
    schema: EsiSchema,
) -> EsiResponseGroup:
    """Validate, execute, and group responses for an ESI request batch.

    Args:
        esi_requests: ESI requests to execute.
        schema: Schema used for operation lookup and validation.

    Returns:
        Grouped successful and failed responses keyed by runtime request key.

    Raises:
        RuntimeError: If EsiLink is used outside async context manager scope.
        EsiRequestValidationErrors: If any request fails schema validation.
        Exception: Any backend execution exception from request or auth layers.
    """
    requester = self._check_api_requester_initialized()

    runtime_requests: dict[UUID, RuntimeEsiRequest] = {}
    for _, request in esi_requests.requests.items():
        self.validate_request(request, schema)
        runtime_request = build_runtime_esi_request(request, schema)
        await self._attach_access_token_if_required(request, runtime_request)
        runtime_requests[runtime_request.request_key] = runtime_request

    request_objects = {
        key: _make_request_from_runtime_request(request)
        for key, request in runtime_requests.items()
    }
    responses: Responses = await requester.process_requests(request_objects)
    esi_responses = _make_esi_response_group(
        responses, esi_requests, runtime_requests
    )
    return esi_responses

validate_request(esi_request: EsiRequest, schema: EsiSchema) -> None staticmethod

Validate one ESI request against the provided schema.

Parameters:

Name Type Description Default
esi_request EsiRequest

The ESI request to validate.

required
schema EsiSchema

Schema used for validation rules.

required

Raises:

Type Description
EsiRequestValidationErrors

If request data violates schema-derived rules.

Source code in src/pfmsoft/eve_link/esi_link.py
@staticmethod
def validate_request(
    esi_request: EsiRequest,
    schema: EsiSchema,
) -> None:
    """Validate one ESI request against the provided schema.

    Args:
        esi_request: The ESI request to validate.
        schema: Schema used for validation rules.

    Raises:
        EsiRequestValidationErrors: If request data violates schema-derived rules.
    """
    try:
        validate_esi_request(esi_request, schema)
    except EsiRequestValidationErrors as e:
        logger.error("Validation failed for request %s: %s", esi_request, e)
        raise

EsiLinkSettings dataclass

Configuration settings for the Eve ESI Link application.

Source code in src/pfmsoft/eve_link/settings.py
@dataclass(slots=True, kw_only=True)
class EsiLinkSettings:
    """Configuration settings for the Eve ESI Link application."""

    application_directory: Path
    logging_directory: Path
    schema_cache_directory: Path
    # Eve Auth Manager settings
    eve_auth_manager_settings: EveAuthManagerSettings
    # API Request settings
    api_request_settings: ApiRequestSettings
    max_rate: float = 50.0
    time_period: float = 1.0

EsiRequest dataclass

Represents a single ESI request to be executed.

Can be loaded from a file or created programmatically. The request_id is used to identify the request.

Requests can be be contained in a RequestGroup, and the request_id is used to link the Request to its RuntimeRequest, and to the final EsiResponse.

Source code in src/pfmsoft/eve_link/esi_request/models.py
@dataclass(slots=True, kw_only=True)
class EsiRequest:
    """Represents a single ESI request to be executed.

    Can be loaded from a file or created programmatically. The request_id is used to
    identify the request.

    Requests can be be contained in a RequestGroup, and the request_id is used
    to link the Request to its RuntimeRequest, and to the final EsiResponse.
    """

    request_id: UUID = field(default_factory=uuid4)
    """The unique identifier for the request. This is used to link the request to various 
        objects during the request lifecycle."""
    name: str | None = None
    """An optional name for the request. This is used for documentation purposes, 
        and can be used to provide context for the request when viewing it in a UI or in 
        logs."""
    description: str | None = None
    """An optional description of the request. This is used for documentation purposes, 
        and can be used to provide context for the request when viewing it in a UI or in 
        logs."""
    operation_id: str
    """The operation ID of the request, corresponding to the operationId in the ESI 
        OpenAPI schema."""
    path_parameters: dict[str, str | int | float] = field(
        default_factory=dict[str, str | int | float]
    )
    """The path parameters for the request, if applicable. This is used to fill in the 
        path parameters in the URL template."""
    query_parameters: dict[str, str | int | float] = field(
        default_factory=dict[str, str | int | float]
    )
    """The query parameters for the request, if applicable.

    This is used to fill in the query parameters in the URL template.

    NOTE: The page parameter is handled automatically by eve-link, and should not 
        be set manually. If it is set, it will raise a validation error. This is to help 
        normalize cache keys, which rely on predictable parameters.
    """
    header_parameters: dict[str, str] = field(default_factory=dict[str, str])
    """The header parameters for the request, if applicable. 

        Acceptable headers are:
        - Accept-Language
        - X-Tenant
        - X-Compatibility-Date

        Do not use this to set:

        - If-None-Match
        - If-Modified-Since headers. 

        Those are set at runtime during HTTP execution."""
    request_body: Any | None = None
    """The JSON payload of the request, if applicable. This is used for POST, PUT, and PATCH 
        requests."""
    auth_character_id: int | None = None
    """The character ID used for authorization."""
    auth_credential_id: UUID | None = None
    """The credential ID for authorization. This is used to link the authorization
        to the credential that was used to obtain it. This UUID is obtained from the 
        credential manager that provides the access token."""

    @property
    def has_authorization(self) -> bool:
        """Check if the request has an authorization."""
        return (
            self.auth_character_id is not None and self.auth_credential_id is not None
        )

    @property
    def authorization_slug(self) -> UUID:
        """Get the authorization slug for the authorization.

        This is a UUID that is generated from the character ID and credential ID, and is
        used as part of the cache key to differentiate between different cached
        authorized requests.

        Returns:
            The authorization slug for the authorization.
        """
        if not self.has_authorization:
            raise ValueError(
                "Cannot generate authorization key without both character_id and credential_id."
            )
        return uuid5(self.auth_credential_id, str(self.auth_character_id))  # type: ignore

auth_character_id: int | None = None class-attribute instance-attribute

The character ID used for authorization.

auth_credential_id: UUID | None = None class-attribute instance-attribute

The credential ID for authorization. This is used to link the authorization to the credential that was used to obtain it. This UUID is obtained from the credential manager that provides the access token.

authorization_slug: UUID property

Get the authorization slug for the authorization.

This is a UUID that is generated from the character ID and credential ID, and is used as part of the cache key to differentiate between different cached authorized requests.

Returns:

Type Description
UUID

The authorization slug for the authorization.

description: str | None = None class-attribute instance-attribute

An optional description of the request. This is used for documentation purposes, and can be used to provide context for the request when viewing it in a UI or in logs.

has_authorization: bool property

Check if the request has an authorization.

header_parameters: dict[str, str] = field(default_factory=dict[str, str]) class-attribute instance-attribute

The header parameters for the request, if applicable.

Acceptable headers are: - Accept-Language - X-Tenant - X-Compatibility-Date

Do not use this to set:

  • If-None-Match
  • If-Modified-Since headers.

Those are set at runtime during HTTP execution.

name: str | None = None class-attribute instance-attribute

An optional name for the request. This is used for documentation purposes, and can be used to provide context for the request when viewing it in a UI or in logs.

operation_id: str instance-attribute

The operation ID of the request, corresponding to the operationId in the ESI OpenAPI schema.

path_parameters: dict[str, str | int | float] = field(default_factory=dict[str, str | int | float]) class-attribute instance-attribute

The path parameters for the request, if applicable. This is used to fill in the path parameters in the URL template.

query_parameters: dict[str, str | int | float] = field(default_factory=dict[str, str | int | float]) class-attribute instance-attribute

The query parameters for the request, if applicable.

This is used to fill in the query parameters in the URL template.

The page parameter is handled automatically by eve-link, and should not

be set manually. If it is set, it will raise a validation error. This is to help normalize cache keys, which rely on predictable parameters.

request_body: Any | None = None class-attribute instance-attribute

The JSON payload of the request, if applicable. This is used for POST, PUT, and PATCH requests.

request_id: UUID = field(default_factory=uuid4) class-attribute instance-attribute

The unique identifier for the request. This is used to link the request to various objects during the request lifecycle.

EsiRequestGroup dataclass

Source code in src/pfmsoft/eve_link/esi_request/models.py
@dataclass(slots=True, kw_only=True)
class EsiRequestGroup:
    name: str | None = None
    """The name of this group of runtime ESI requests."""
    description: str | None = None
    """An optional description of this group of runtime ESI requests."""
    requests: dict[UUID, EsiRequest] = field(default_factory=dict[UUID, EsiRequest])
    """The dict of  ESI requests in this group."""

description: str | None = None class-attribute instance-attribute

An optional description of this group of runtime ESI requests.

name: str | None = None class-attribute instance-attribute

The name of this group of runtime ESI requests.

requests: dict[UUID, EsiRequest] = field(default_factory=dict[UUID, EsiRequest]) class-attribute instance-attribute

The dict of ESI requests in this group.

EsiResponse dataclass

Source code in src/pfmsoft/eve_link/esi_request/models.py
@dataclass(slots=True, kw_only=True, frozen=True)
class EsiResponse:
    esi_request: EsiRequest
    """The request that generated this response."""
    esi_runtime_request: RuntimeEsiRequest
    """The request that generated this response."""
    response: Response
    """The response associated with this EsiResponse."""

    def serialize(self, indent: int | None = None) -> str:
        """Serialize the EsiResponse."""
        return EsiResponseRoot(root=self).model_dump_json(indent=indent)

    @property
    def response_data(self) -> Any:
        """Return the JSON response data associated with this EsiResponse."""
        return self.response.json

    @property
    def received_at_instant(self) -> Instant:
        """Return the instant at which the response was received."""
        return self.response.metadata.received_at

    @property
    def expires_at_instant(self) -> Instant | None:
        """Return the instant at which the response expires, if any."""
        return (
            Instant.from_timestamp(self.response.metadata.expires_at)
            if self.response.metadata.expires_at
            else None
        )

    def simple_response(self) -> SimplifiedEsiResponse:
        """Return a simplified version of this EsiResponse.

        This is useful for serialization and deserialization, as it removes the
        response metadata and other details that are not needed for most use cases.
        """
        return SimplifiedEsiResponse(
            esi_request=self.esi_request,
            response_data=self.response_data,
            received_at_instant=self.received_at_instant,
            expires_at_instant=self.expires_at_instant,
        )

esi_request: EsiRequest instance-attribute

The request that generated this response.

esi_runtime_request: RuntimeEsiRequest instance-attribute

The request that generated this response.

expires_at_instant: Instant | None property

Return the instant at which the response expires, if any.

received_at_instant: Instant property

Return the instant at which the response was received.

response: Response instance-attribute

The response associated with this EsiResponse.

response_data: Any property

Return the JSON response data associated with this EsiResponse.

serialize(indent: int | None = None) -> str

Serialize the EsiResponse.

Source code in src/pfmsoft/eve_link/esi_request/models.py
def serialize(self, indent: int | None = None) -> str:
    """Serialize the EsiResponse."""
    return EsiResponseRoot(root=self).model_dump_json(indent=indent)

simple_response() -> SimplifiedEsiResponse

Return a simplified version of this EsiResponse.

This is useful for serialization and deserialization, as it removes the response metadata and other details that are not needed for most use cases.

Source code in src/pfmsoft/eve_link/esi_request/models.py
def simple_response(self) -> SimplifiedEsiResponse:
    """Return a simplified version of this EsiResponse.

    This is useful for serialization and deserialization, as it removes the
    response metadata and other details that are not needed for most use cases.
    """
    return SimplifiedEsiResponse(
        esi_request=self.esi_request,
        response_data=self.response_data,
        received_at_instant=self.received_at_instant,
        expires_at_instant=self.expires_at_instant,
    )

EsiResponseGroup dataclass

Source code in src/pfmsoft/eve_link/esi_request/models.py
@dataclass(slots=True, kw_only=True)
class EsiResponseGroup:
    name: str | None = None
    """The name of this group of runtime ESI responses."""
    description: str | None = None
    """An optional description of this group of runtime ESI responses."""
    successful_responses: dict[UUID, EsiResponse] = field(
        default_factory=dict[UUID, EsiResponse]
    )
    """The dict of successful ESI responses in this group."""
    failed_responses: dict[UUID, FailedEsiResponse] = field(
        default_factory=dict[UUID, FailedEsiResponse]
    )
    """The dict of failed ESI responses in this group."""

    def serialize(self, indent: int | None = None) -> str:
        """Purge secrets and serialize the EsiResponseGroup to a JSON string."""
        return EsiResponseGroupRoot(root=self).model_dump_json(indent=indent)

description: str | None = None class-attribute instance-attribute

An optional description of this group of runtime ESI responses.

failed_responses: dict[UUID, FailedEsiResponse] = field(default_factory=dict[UUID, FailedEsiResponse]) class-attribute instance-attribute

The dict of failed ESI responses in this group.

name: str | None = None class-attribute instance-attribute

The name of this group of runtime ESI responses.

successful_responses: dict[UUID, EsiResponse] = field(default_factory=dict[UUID, EsiResponse]) class-attribute instance-attribute

The dict of successful ESI responses in this group.

serialize(indent: int | None = None) -> str

Purge secrets and serialize the EsiResponseGroup to a JSON string.

Source code in src/pfmsoft/eve_link/esi_request/models.py
def serialize(self, indent: int | None = None) -> str:
    """Purge secrets and serialize the EsiResponseGroup to a JSON string."""
    return EsiResponseGroupRoot(root=self).model_dump_json(indent=indent)

EsiSchema dataclass

Represents the schema payload used for operation-level lookups.

Can have an optional timestamp associated with it, representing the timestamp when the schema was fetched in nanoseconds.

The model builds operation and tag indexes during initialization.

Source code in src/pfmsoft/eve_link/schema/models.py
@dataclass(slots=True, kw_only=True)
class EsiSchema:
    """Represents the schema payload used for operation-level lookups.

    Can have an optional timestamp associated with it, representing the timestamp when
    the schema was fetched in nanoseconds.

    The model builds operation and tag indexes during initialization.
    """

    dereferenced_schema: dict[str, Any]
    timestamp: str | None = None
    """The timestamp associated with the schema, representing the timestamp when the
        schema was fetched as an ISO 8601 string. This field is optional and can be None 
        if the timestamp is not available or not applicable."""
    _schema_operations: dict[str, SchemaOperation] = field(
        default_factory=dict[str, SchemaOperation], init=False, repr=False
    )
    _operations_id_by_tag: dict[str, list[str]] = field(
        default_factory=dict[str, list[str]], init=False, repr=False
    )

    @property
    def timestamp_instant(self) -> Instant | None:
        """Get the timestamp as an Instant object, if available."""
        if self.timestamp is not None:
            return Instant.parse_iso(self.timestamp)
        return None

    def __post_init__(self) -> None:
        """Ensure that the schema is valid."""
        if "openapi" not in self.dereferenced_schema:
            raise ValueError("Invalid schema: missing 'openapi' field")
        # fill the schema operations dictionary
        self._build_schema_operations()
        self._build_operation_id_by_tag()

    def serialize(self, indent: int | None = None) -> str:
        """Serialize as an EsiSchemaTD-compatible JSON string.

        Output keys:
        - dereferenced_schema
        - timestamp
        """
        return EsiSchemaRoot(root=self).model_dump_json(indent=indent)

    @classmethod
    def deserialize(cls, json_str: str) -> EsiSchema:
        """Deserialize an EsiSchema compatible JSON string into an EsiSchema instance."""
        model = EsiSchemaRoot.model_validate_json(json_str).root
        return model

    def _build_schema_operations(self) -> None:
        """Build the schema operations dictionary from the dereferenced schema."""
        paths = self.dereferenced_schema.get("paths", {})
        for path, methods in paths.items():
            for method, operation in methods.items():
                operation_id = operation.get("operationId")
                if operation_id:
                    self._schema_operations[operation_id] = SchemaOperation(
                        path=path,
                        method=HttpMethod(method.upper()),
                        operation_schema=deepcopy(operation),
                    )

    def _build_operation_id_by_tag(self) -> None:
        """Build the operation ID by tag mapping from the schema operations."""
        if not self._schema_operations:
            raise ValueError(
                "Schema operations must be built before building operation ID by tag mapping."
            )
        tag_mapping: dict[str, list[str]] = {}
        for operation in self._schema_operations.values():
            for tag in operation.tags:
                if tag not in tag_mapping:
                    tag_mapping[tag] = []
                tag_mapping[tag].append(operation.operation_id)
        # sort the tags alphabetically, and the operation IDs within each tag alphabetically as well
        self._operations_id_by_tag = {
            tag: sorted(operation_ids)
            for tag, operation_ids in sorted(tag_mapping.items())
        }

    @classmethod
    def from_raw_schema(
        cls, raw_schema: dict[str, Any], timestamp: str | None = None
    ) -> Self:
        """Factory method to create an EsiSchema instance from a raw OpenAPI schema.

        This method resolves internal ``$ref`` values before creating ``EsiSchema``.

        Args:
            raw_schema: The raw OpenAPI schema as a dictionary.
            timestamp: The timestamp associated with the schema, representing the timestamp when the
                schema was fetched as an ISO 8601 string. This field is optional and can be None if the
                timestamp is not available or not applicable.

        Returns:
            An instance of EsiSchema with the dereferenced schema.
        """
        dereferenced_schema = resolve_internal_refs(raw_schema, raw_schema)
        return cls(dereferenced_schema=dereferenced_schema, timestamp=timestamp)

    @property
    def operations(self) -> dict[str, SchemaOperation]:
        """Get a dictionary of all operations in the schema, keyed by operation ID."""
        return self._schema_operations

    @property
    def operations_id_by_tag(self) -> dict[str, list[str]]:
        """Get a dictionary mapping tags to lists of operation IDs."""
        return self._operations_id_by_tag

    @property
    def compatibility_date(self) -> str:
        """Get compatibility date, currently sourced from ``info.version``."""
        return self.version

    @property
    def version(self) -> str:
        """Get the version of the ESI schema based on the compatibility date."""
        version = cast(str, self.dereferenced_schema["info"]["version"])
        return version

    @property
    def base_url(self) -> str:
        """Get the base URL for the ESI API from the servers section of the schema."""
        return self.dereferenced_schema["servers"][0]["url"]

    def operation_url(self, operation_id: str) -> str:
        """Get full URL template for an operation ID.

        Raises:
            ValueError: If operation_id is not present in the schema.
        """
        operation = self.operations.get(operation_id)
        if operation is None:
            raise ValueError(f"Operation ID '{operation_id}' not found in ESI schema.")
        return f"{self.base_url}{operation.path}"

    @property
    def content_languages(self) -> set[str]:
        """Get the content languages supported by the ESI API from the schema."""
        return set(
            self.dereferenced_schema
            .get("components", {})
            .get("headers", {})
            .get("ContentLanguage", {})
            .get("schema", {})
            .get("enum", [])
        )

base_url: str property

Get the base URL for the ESI API from the servers section of the schema.

compatibility_date: str property

Get compatibility date, currently sourced from info.version.

content_languages: set[str] property

Get the content languages supported by the ESI API from the schema.

operations: dict[str, SchemaOperation] property

Get a dictionary of all operations in the schema, keyed by operation ID.

operations_id_by_tag: dict[str, list[str]] property

Get a dictionary mapping tags to lists of operation IDs.

timestamp: str | None = None class-attribute instance-attribute

The timestamp associated with the schema, representing the timestamp when the schema was fetched as an ISO 8601 string. This field is optional and can be None if the timestamp is not available or not applicable.

timestamp_instant: Instant | None property

Get the timestamp as an Instant object, if available.

version: str property

Get the version of the ESI schema based on the compatibility date.

__post_init__() -> None

Ensure that the schema is valid.

Source code in src/pfmsoft/eve_link/schema/models.py
def __post_init__(self) -> None:
    """Ensure that the schema is valid."""
    if "openapi" not in self.dereferenced_schema:
        raise ValueError("Invalid schema: missing 'openapi' field")
    # fill the schema operations dictionary
    self._build_schema_operations()
    self._build_operation_id_by_tag()

deserialize(json_str: str) -> EsiSchema classmethod

Deserialize an EsiSchema compatible JSON string into an EsiSchema instance.

Source code in src/pfmsoft/eve_link/schema/models.py
@classmethod
def deserialize(cls, json_str: str) -> EsiSchema:
    """Deserialize an EsiSchema compatible JSON string into an EsiSchema instance."""
    model = EsiSchemaRoot.model_validate_json(json_str).root
    return model

from_raw_schema(raw_schema: dict[str, Any], timestamp: str | None = None) -> Self classmethod

Factory method to create an EsiSchema instance from a raw OpenAPI schema.

This method resolves internal $ref values before creating EsiSchema.

Parameters:

Name Type Description Default
raw_schema dict[str, Any]

The raw OpenAPI schema as a dictionary.

required
timestamp str | None

The timestamp associated with the schema, representing the timestamp when the schema was fetched as an ISO 8601 string. This field is optional and can be None if the timestamp is not available or not applicable.

None

Returns:

Type Description
Self

An instance of EsiSchema with the dereferenced schema.

Source code in src/pfmsoft/eve_link/schema/models.py
@classmethod
def from_raw_schema(
    cls, raw_schema: dict[str, Any], timestamp: str | None = None
) -> Self:
    """Factory method to create an EsiSchema instance from a raw OpenAPI schema.

    This method resolves internal ``$ref`` values before creating ``EsiSchema``.

    Args:
        raw_schema: The raw OpenAPI schema as a dictionary.
        timestamp: The timestamp associated with the schema, representing the timestamp when the
            schema was fetched as an ISO 8601 string. This field is optional and can be None if the
            timestamp is not available or not applicable.

    Returns:
        An instance of EsiSchema with the dereferenced schema.
    """
    dereferenced_schema = resolve_internal_refs(raw_schema, raw_schema)
    return cls(dereferenced_schema=dereferenced_schema, timestamp=timestamp)

operation_url(operation_id: str) -> str

Get full URL template for an operation ID.

Raises:

Type Description
ValueError

If operation_id is not present in the schema.

Source code in src/pfmsoft/eve_link/schema/models.py
def operation_url(self, operation_id: str) -> str:
    """Get full URL template for an operation ID.

    Raises:
        ValueError: If operation_id is not present in the schema.
    """
    operation = self.operations.get(operation_id)
    if operation is None:
        raise ValueError(f"Operation ID '{operation_id}' not found in ESI schema.")
    return f"{self.base_url}{operation.path}"

serialize(indent: int | None = None) -> str

Serialize as an EsiSchemaTD-compatible JSON string.

Output keys: - dereferenced_schema - timestamp

Source code in src/pfmsoft/eve_link/schema/models.py
def serialize(self, indent: int | None = None) -> str:
    """Serialize as an EsiSchemaTD-compatible JSON string.

    Output keys:
    - dereferenced_schema
    - timestamp
    """
    return EsiSchemaRoot(root=self).model_dump_json(indent=indent)

FailedEsiResponse dataclass

Source code in src/pfmsoft/eve_link/esi_request/models.py
@dataclass(slots=True, kw_only=True, frozen=True)
class FailedEsiResponse:
    esi_request: EsiRequest
    """The request that generated this failed response."""
    esi_runtime_request: RuntimeEsiRequest
    """The request that generated this failed response."""
    failed_response: FailedResponse
    """The failed response associated with this FailedEsiResponse."""

    def serialize(self, indent: int | None = None) -> str:
        """Serialize the FailedEsiResponse."""
        return FailedEsiResponseRoot(root=self).model_dump_json(indent=indent)

esi_request: EsiRequest instance-attribute

The request that generated this failed response.

esi_runtime_request: RuntimeEsiRequest instance-attribute

The request that generated this failed response.

failed_response: FailedResponse instance-attribute

The failed response associated with this FailedEsiResponse.

serialize(indent: int | None = None) -> str

Serialize the FailedEsiResponse.

Source code in src/pfmsoft/eve_link/esi_request/models.py
def serialize(self, indent: int | None = None) -> str:
    """Serialize the FailedEsiResponse."""
    return FailedEsiResponseRoot(root=self).model_dump_json(indent=indent)

SchemaCacheManager

Manage persisted ESI schema cache files in a single directory.

The cache manager handles reading and writing schema files, as well as maintaining a list of valid compatibility dates. It provides methods to fetch and cache schemas for all known compatibility dates.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
class SchemaCacheManager:
    """Manage persisted ESI schema cache files in a single directory.

    The cache manager handles reading and writing schema files, as well as
    maintaining a list of valid compatibility dates. It provides methods to fetch
    and cache schemas for all known compatibility dates.
    """

    def __init__(self, *, cache_directory: Path) -> None:
        """Initialize a cache manager.

        Args:
            cache_directory: Directory containing cached schema JSON files.
        """
        self._cache_directory = cache_directory
        self._compatibility_dates: TimestampedCompatibilityDates | None = None
        self._load_compatibility_dates()

    def _compatibility_dates_path(self) -> Path:
        """Return the path to the cached compatibility dates file."""
        return self._cache_directory / "compatibility_dates.json"

    def _fetch_compatibility_dates(self, session: Client) -> None:
        """Fetch the list of compatibility dates from ESI.

        Args:
            session: An instance of httpx2.Client for making HTTP requests.
        """
        compatibility_dates = fetch_compatibility_dates(session=session)
        self._compatibility_dates_path().write_text(
            compatibility_dates.serialize(indent=2), encoding="utf-8"
        )
        self._compatibility_dates = compatibility_dates

    def _load_compatibility_dates(self) -> None:
        """Load and cache the list of compatibility dates from disk."""
        self._cache_directory.mkdir(parents=True, exist_ok=True)
        if self._compatibility_dates_path().exists():
            self._compatibility_dates = TimestampedCompatibilityDates.deserialize(
                self._compatibility_dates_path().read_text(encoding="utf-8")
            )
        else:
            self._compatibility_dates = None

    def _ensure_compatibility_dates(self, session: Client) -> None:
        """Ensure that compatibility dates are loaded and current, fetching from ESI if necessary.

        Args:
            session: An instance of httpx2.Client for making HTTP requests.
        """
        if self._compatibility_dates is None:
            self._fetch_compatibility_dates(session=session)
            return
        if self._compatibility_dates.timestamp_instant < previous_downtime():
            self._fetch_compatibility_dates(session=session)

    @property
    def valid_compatibility_dates(self) -> tuple[str, ...]:
        """Return the list of valid compatibility dates.

        Returns:
            Tuple of compatibility dates in YYYY-MM-DD format.
        """
        if self._compatibility_dates is None:
            raise RuntimeError(
                "Compatibility dates have not been loaded. Call "
                "fetch_updates(session) first."
            )
        return self._compatibility_dates.compatibility_dates

    @property
    def cache_directory(self) -> Path:
        """Return the configured cache directory path."""
        return self._cache_directory

    def save(self, *, schema: EsiSchema) -> SchemaCacheEntry:
        """Save a schema to cache, replacing any existing entry for its date.

        Args:
            schema: Schema to cache.

        Returns:
            Metadata for the saved cache entry.
        """
        self._cache_directory.mkdir(parents=True, exist_ok=True)

        for existing_path in self._files_for_compatibility_date(
            compatibility_date=schema.compatibility_date
        ):
            existing_path.unlink(missing_ok=True)

        output_name = default_file_name_for_cached_schema(schema)
        output_path = self._cache_directory / output_name
        output_path.write_text(schema.serialize(), encoding="utf-8")

        return SchemaCacheEntry(
            compatibility_date=schema.compatibility_date,
            timestamp=schema.timestamp,
        )

    def load(self, *, compatibility_date: str) -> EsiSchema:
        """Load a cached schema by compatibility date.

        Args:
            compatibility_date: Date key in YYYY-MM-DD format.

        Returns:
            Loaded EsiSchema.

        Raises:
            FileNotFoundError: If no cached schema exists for the date.
            ValueError: If multiple cached files exist for the same date.
        """
        matching_files = self._files_for_compatibility_date(
            compatibility_date=compatibility_date
        )
        if not matching_files:
            raise FileNotFoundError(
                f"No cached schema found for compatibility date {compatibility_date}."
            )
        if len(matching_files) > 1:
            raise ValueError(
                "Multiple cached schemas found for compatibility date "
                f"{compatibility_date}."
            )
        json_string = matching_files[0].read_text(encoding="utf-8")
        return EsiSchema.deserialize(json_string)

    def list_entries(self) -> list[SchemaCacheEntry]:
        """List all cached schema entries.

        Returns:
            Sorted list of cache entries by compatibility_date then timestamp.
        """
        entries: list[SchemaCacheEntry] = []
        for cache_file in self._iter_cache_files():
            parsed = self._parse_cache_file_name(cache_file.name)
            if parsed is None:
                continue
            entries.append(
                SchemaCacheEntry(
                    compatibility_date=parsed.compatibility_date,
                    timestamp=(
                        Instant.from_timestamp_nanos(parsed.timestamp).format_iso()
                        if parsed.timestamp is not None
                        else None
                    ),
                )
            )

        return sorted(
            entries,
            key=lambda item: (
                item.compatibility_date,
                item.timestamp is None,
                item.timestamp or "",
            ),
        )

    def latest_entry(self) -> SchemaCacheEntry:
        """Return the latest cached schema entry by compatibility date.

        Returns:
            The latest SchemaCacheEntry.

        Raises:
            ValueError: If no cached schema entries exist.
        """
        entries = self.list_entries()
        if not entries:
            raise ValueError(
                "No cached schema entries found. Please fetch and cache schemas first."
            )
        return max(entries, key=lambda entry: entry.compatibility_date)

    def latest_schema(self) -> EsiSchema:
        """Return the latest cached schema by compatibility date.

        Returns:
            The latest EsiSchema.

        Raises:
            ValueError: If no cached schema entries exist.
        """
        latest_entry = self.latest_entry()
        return self.load(compatibility_date=latest_entry.compatibility_date)

    def clear_date(self, *, compatibility_date: str) -> int:
        """Delete cached schema file(s) for one compatibility date.

        Args:
            compatibility_date: Date key in YYYY-MM-DD format.

        Returns:
            Number of deleted files.
        """
        deleted = 0
        for cache_file in self._files_for_compatibility_date(
            compatibility_date=compatibility_date
        ):
            cache_file.unlink(missing_ok=True)
            deleted += 1
        return deleted

    def clear_all(self) -> int:
        """Delete all recognized cached schema files.

        Returns:
            Number of deleted files.
        """
        deleted = 0
        for cache_file in self._iter_cache_files():
            if self._parse_cache_file_name(cache_file.name) is None:
                continue
            cache_file.unlink(missing_ok=True)
            deleted += 1
        return deleted

    def fetch_updates(
        self,
        session: Client,
    ) -> None:
        """Fetch and cache schemas by compatibility date, replacing existing cached files.

        This method ensures the latest available compatibility dates are fetched and
        cached, and then fetches and caches any missing schemas from the EVE Online API
        for those dates.

        Args:
            session: An instance of httpx2.Client for making HTTP requests.

        Raises:
            httpx2.HTTPError: If any HTTP request fails.
        """
        self._ensure_compatibility_dates(session=session)
        entries = self.list_entries()
        cached_dates = {entry.compatibility_date for entry in entries}

        for compatibility_date in self.valid_compatibility_dates:
            if compatibility_date in cached_dates:
                continue  # Skip already cached dates
            # Fetch the latest schema for the compatibility date
            timestamped_schema = fetch_schema(
                session=session, schema_as_of=compatibility_date
            )
            # Save the fetched schema to the cache
            self.save(
                schema=EsiSchema.from_raw_schema(
                    raw_schema=timestamped_schema.schema,
                    timestamp=timestamped_schema.timestamp,
                )
            )

    def _iter_cache_files(self) -> list[Path]:
        """Return sorted files in the cache directory.

        Missing directories produce an empty list.
        """
        if not self._cache_directory.exists():
            return []
        return sorted(
            path for path in self._cache_directory.iterdir() if path.is_file()
        )

    def _files_for_compatibility_date(self, *, compatibility_date: str) -> list[Path]:
        """Return cache files matching one compatibility date."""
        matching_files: list[Path] = []
        for cache_file in self._iter_cache_files():
            parsed = self._parse_cache_file_name(cache_file.name)
            if parsed is None:
                continue
            if parsed.compatibility_date == compatibility_date:
                matching_files.append(cache_file)
        return matching_files

    def _parse_cache_file_name(self, file_name: str) -> _ParsedCacheFileName | None:
        """Parse cache file names that follow the schema cache naming convention."""
        match = _SCHEMA_FILE_RE.match(file_name)
        if match is None:
            return None
        timestamp_string = match.group("timestamp")
        timestamp: int | None
        if timestamp_string == "None":
            timestamp = None
        else:
            timestamp = int(timestamp_string)

        return _ParsedCacheFileName(
            compatibility_date=match.group("compatibility_date"),
            timestamp=timestamp,
        )

cache_directory: Path property

Return the configured cache directory path.

valid_compatibility_dates: tuple[str, ...] property

Return the list of valid compatibility dates.

Returns:

Type Description
tuple[str, ...]

Tuple of compatibility dates in YYYY-MM-DD format.

__init__(*, cache_directory: Path) -> None

Initialize a cache manager.

Parameters:

Name Type Description Default
cache_directory Path

Directory containing cached schema JSON files.

required
Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def __init__(self, *, cache_directory: Path) -> None:
    """Initialize a cache manager.

    Args:
        cache_directory: Directory containing cached schema JSON files.
    """
    self._cache_directory = cache_directory
    self._compatibility_dates: TimestampedCompatibilityDates | None = None
    self._load_compatibility_dates()

clear_all() -> int

Delete all recognized cached schema files.

Returns:

Type Description
int

Number of deleted files.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def clear_all(self) -> int:
    """Delete all recognized cached schema files.

    Returns:
        Number of deleted files.
    """
    deleted = 0
    for cache_file in self._iter_cache_files():
        if self._parse_cache_file_name(cache_file.name) is None:
            continue
        cache_file.unlink(missing_ok=True)
        deleted += 1
    return deleted

clear_date(*, compatibility_date: str) -> int

Delete cached schema file(s) for one compatibility date.

Parameters:

Name Type Description Default
compatibility_date str

Date key in YYYY-MM-DD format.

required

Returns:

Type Description
int

Number of deleted files.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def clear_date(self, *, compatibility_date: str) -> int:
    """Delete cached schema file(s) for one compatibility date.

    Args:
        compatibility_date: Date key in YYYY-MM-DD format.

    Returns:
        Number of deleted files.
    """
    deleted = 0
    for cache_file in self._files_for_compatibility_date(
        compatibility_date=compatibility_date
    ):
        cache_file.unlink(missing_ok=True)
        deleted += 1
    return deleted

fetch_updates(session: Client) -> None

Fetch and cache schemas by compatibility date, replacing existing cached files.

This method ensures the latest available compatibility dates are fetched and cached, and then fetches and caches any missing schemas from the EVE Online API for those dates.

Parameters:

Name Type Description Default
session Client

An instance of httpx2.Client for making HTTP requests.

required

Raises:

Type Description
httpx2.HTTPError

If any HTTP request fails.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def fetch_updates(
    self,
    session: Client,
) -> None:
    """Fetch and cache schemas by compatibility date, replacing existing cached files.

    This method ensures the latest available compatibility dates are fetched and
    cached, and then fetches and caches any missing schemas from the EVE Online API
    for those dates.

    Args:
        session: An instance of httpx2.Client for making HTTP requests.

    Raises:
        httpx2.HTTPError: If any HTTP request fails.
    """
    self._ensure_compatibility_dates(session=session)
    entries = self.list_entries()
    cached_dates = {entry.compatibility_date for entry in entries}

    for compatibility_date in self.valid_compatibility_dates:
        if compatibility_date in cached_dates:
            continue  # Skip already cached dates
        # Fetch the latest schema for the compatibility date
        timestamped_schema = fetch_schema(
            session=session, schema_as_of=compatibility_date
        )
        # Save the fetched schema to the cache
        self.save(
            schema=EsiSchema.from_raw_schema(
                raw_schema=timestamped_schema.schema,
                timestamp=timestamped_schema.timestamp,
            )
        )

latest_entry() -> SchemaCacheEntry

Return the latest cached schema entry by compatibility date.

Returns:

Type Description
SchemaCacheEntry

The latest SchemaCacheEntry.

Raises:

Type Description
ValueError

If no cached schema entries exist.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def latest_entry(self) -> SchemaCacheEntry:
    """Return the latest cached schema entry by compatibility date.

    Returns:
        The latest SchemaCacheEntry.

    Raises:
        ValueError: If no cached schema entries exist.
    """
    entries = self.list_entries()
    if not entries:
        raise ValueError(
            "No cached schema entries found. Please fetch and cache schemas first."
        )
    return max(entries, key=lambda entry: entry.compatibility_date)

latest_schema() -> EsiSchema

Return the latest cached schema by compatibility date.

Returns:

Type Description
EsiSchema

The latest EsiSchema.

Raises:

Type Description
ValueError

If no cached schema entries exist.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def latest_schema(self) -> EsiSchema:
    """Return the latest cached schema by compatibility date.

    Returns:
        The latest EsiSchema.

    Raises:
        ValueError: If no cached schema entries exist.
    """
    latest_entry = self.latest_entry()
    return self.load(compatibility_date=latest_entry.compatibility_date)

list_entries() -> list[SchemaCacheEntry]

List all cached schema entries.

Returns:

Type Description
list[SchemaCacheEntry]

Sorted list of cache entries by compatibility_date then timestamp.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def list_entries(self) -> list[SchemaCacheEntry]:
    """List all cached schema entries.

    Returns:
        Sorted list of cache entries by compatibility_date then timestamp.
    """
    entries: list[SchemaCacheEntry] = []
    for cache_file in self._iter_cache_files():
        parsed = self._parse_cache_file_name(cache_file.name)
        if parsed is None:
            continue
        entries.append(
            SchemaCacheEntry(
                compatibility_date=parsed.compatibility_date,
                timestamp=(
                    Instant.from_timestamp_nanos(parsed.timestamp).format_iso()
                    if parsed.timestamp is not None
                    else None
                ),
            )
        )

    return sorted(
        entries,
        key=lambda item: (
            item.compatibility_date,
            item.timestamp is None,
            item.timestamp or "",
        ),
    )

load(*, compatibility_date: str) -> EsiSchema

Load a cached schema by compatibility date.

Parameters:

Name Type Description Default
compatibility_date str

Date key in YYYY-MM-DD format.

required

Returns:

Type Description
EsiSchema

Loaded EsiSchema.

Raises:

Type Description
FileNotFoundError

If no cached schema exists for the date.

ValueError

If multiple cached files exist for the same date.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def load(self, *, compatibility_date: str) -> EsiSchema:
    """Load a cached schema by compatibility date.

    Args:
        compatibility_date: Date key in YYYY-MM-DD format.

    Returns:
        Loaded EsiSchema.

    Raises:
        FileNotFoundError: If no cached schema exists for the date.
        ValueError: If multiple cached files exist for the same date.
    """
    matching_files = self._files_for_compatibility_date(
        compatibility_date=compatibility_date
    )
    if not matching_files:
        raise FileNotFoundError(
            f"No cached schema found for compatibility date {compatibility_date}."
        )
    if len(matching_files) > 1:
        raise ValueError(
            "Multiple cached schemas found for compatibility date "
            f"{compatibility_date}."
        )
    json_string = matching_files[0].read_text(encoding="utf-8")
    return EsiSchema.deserialize(json_string)

save(*, schema: EsiSchema) -> SchemaCacheEntry

Save a schema to cache, replacing any existing entry for its date.

Parameters:

Name Type Description Default
schema EsiSchema

Schema to cache.

required

Returns:

Type Description
SchemaCacheEntry

Metadata for the saved cache entry.

Source code in src/pfmsoft/eve_link/schema/cache/schema_cache_disk.py
def save(self, *, schema: EsiSchema) -> SchemaCacheEntry:
    """Save a schema to cache, replacing any existing entry for its date.

    Args:
        schema: Schema to cache.

    Returns:
        Metadata for the saved cache entry.
    """
    self._cache_directory.mkdir(parents=True, exist_ok=True)

    for existing_path in self._files_for_compatibility_date(
        compatibility_date=schema.compatibility_date
    ):
        existing_path.unlink(missing_ok=True)

    output_name = default_file_name_for_cached_schema(schema)
    output_path = self._cache_directory / output_name
    output_path.write_text(schema.serialize(), encoding="utf-8")

    return SchemaCacheEntry(
        compatibility_date=schema.compatibility_date,
        timestamp=schema.timestamp,
    )

SimpleRequests

Source code in src/pfmsoft/eve_link/esi_link.py
class SimpleRequests:
    def __init__(self, settings: EsiLinkSettings) -> None:
        """A simple wrapper for executing ESI requests with schema validation.

        The only state saved by this class is the EsiLinkSettings object, which is used
        to configure the EsiLink instance and schema cache manager. While the factory
        functions for creating EsiLink and SchemaCacheManager instances are useful for
        all situations, the other functions are intended for one-off scripts or commands
        that need to execute a single request or a small batch of requests without
        managing the EsiLink context directly.

        Longer lived applications that execute multiple requests should use the
        SchemaCacheManager and EsiLink class directly, as it is more efficient to keep
        the EsiLink instance open and reuse it for multiple requests.
        """
        self.settings = settings

    def get_schema(self, compatibility_date: str | None = None) -> EsiSchema:
        """Fetches an ESI schema from the EsiLink schema cache.

        This function retrieves the ESI schema from the local cache managed by the
        SchemaCacheManager. If a compatibility date is provided, it fetches the schema
        corresponding to that date; otherwise, it retrieves the latest available schema.

        This function will check for new schema updates from the ESI schema repository
        once per downtime before returning the schema.

        It is suitable for one-off scripts or commands. When executing multiple requests,
        consider using SchemaCacheManager directly to avoid repeated cache lookups.

        Args:
            compatibility_date: Optional compatibility date in YYYY-MM-DD format.
                If not provided, the latest schema is fetched.

        Returns:
            The EsiSchema object for the specified compatibility date or the latest
                schema if no date is provided.

        Raises:
            ValueError: If no cached schema entries exist.
        """
        schema_manager = self.schema_cache_manager_factory()
        with client_manager(USER_AGENT) as session:
            schema_manager.fetch_updates(session=session)
        if compatibility_date is not None:
            esi_schema = schema_manager.load(compatibility_date=compatibility_date)
        else:
            esi_schema = schema_manager.latest_schema()
        return esi_schema

    def schema_cache_manager_factory(self) -> SchemaCacheManager:
        """Factory function to create an instance of SchemaCacheManager from settings.

        Returns:
            SchemaCacheManager: An instance of the SchemaCacheManager class.
        """
        return SchemaCacheManager(cache_directory=self.settings.schema_cache_directory)

    def esi_link_factory(self) -> EsiLink:
        """Factory function to create an instance of EsiLink from settings.

        Returns:
            EsiLink: An instance of the EsiLink class.
        """
        return EsiLink(
            auth_manager_db_path=self.settings.eve_auth_manager_settings.authorization_database_path,
            web_cache_path=self.settings.api_request_settings.web_cache_path,
            max_rate=self.settings.max_rate,
            time_period=self.settings.time_period,
        )

    async def make_request(
        self, esi_request: EsiRequest, schema: EsiSchema
    ) -> EsiResponse | FailedEsiResponse:
        """Validate, execute, and return a single ESI request response.

        This function wraps the EsiLink class to provide a simple interface for executing
        a single ESI request. It creates a temporary EsiLink instance, validates the request
        against the provided schema, and executes it. The response is returned as either
        an EsiResponse or a FailedEsiResponse, depending on the outcome of the request.

        When used in situations where multiple requests need to be executed, consider using
        the EsiLink class directly to avoid the overhead of creating and closing multiple
        instances with their associated resources.

        Args:
            esi_request: The ESI request to execute.
            schema: The ESI schema for validation.

        Returns:
            EsiResponse if the request is successful, otherwise FailedEsiResponse.
        """
        async with self.esi_link_factory() as esi_link:
            response = await esi_link.make_request(
                esi_request=esi_request, schema=schema
            )
            return response

    async def make_requests(
        self, esi_requests: EsiRequestGroup, schema: EsiSchema
    ) -> EsiResponseGroup:
        """Validate, execute, and return a group of ESI request responses.

        This function wraps the EsiLink class to provide a simple interface for executing
        a group of ESI requests. It creates a temporary EsiLink instance, validates the
        requests against the provided schema, and executes them. The responses are returned
        as an EsiResponseGroup, containing both successful and failed responses.

        When used in situations where multiple requests need to be executed, consider using
        the EsiLink class directly to avoid the overhead of creating and closing multiple
        instances.

        Args:
            esi_requests: The group of ESI requests to execute.
            schema: The ESI schema for validation.

        Returns:
            EsiResponseGroup: The group of ESI request responses.
        """
        async with self.esi_link_factory() as esi_link:
            response_group = await esi_link.make_requests(esi_requests, schema)
            return response_group

__init__(settings: EsiLinkSettings) -> None

A simple wrapper for executing ESI requests with schema validation.

The only state saved by this class is the EsiLinkSettings object, which is used to configure the EsiLink instance and schema cache manager. While the factory functions for creating EsiLink and SchemaCacheManager instances are useful for all situations, the other functions are intended for one-off scripts or commands that need to execute a single request or a small batch of requests without managing the EsiLink context directly.

Longer lived applications that execute multiple requests should use the SchemaCacheManager and EsiLink class directly, as it is more efficient to keep the EsiLink instance open and reuse it for multiple requests.

Source code in src/pfmsoft/eve_link/esi_link.py
def __init__(self, settings: EsiLinkSettings) -> None:
    """A simple wrapper for executing ESI requests with schema validation.

    The only state saved by this class is the EsiLinkSettings object, which is used
    to configure the EsiLink instance and schema cache manager. While the factory
    functions for creating EsiLink and SchemaCacheManager instances are useful for
    all situations, the other functions are intended for one-off scripts or commands
    that need to execute a single request or a small batch of requests without
    managing the EsiLink context directly.

    Longer lived applications that execute multiple requests should use the
    SchemaCacheManager and EsiLink class directly, as it is more efficient to keep
    the EsiLink instance open and reuse it for multiple requests.
    """
    self.settings = settings

Factory function to create an instance of EsiLink from settings.

Returns:

Name Type Description
EsiLink EsiLink

An instance of the EsiLink class.

Source code in src/pfmsoft/eve_link/esi_link.py
def esi_link_factory(self) -> EsiLink:
    """Factory function to create an instance of EsiLink from settings.

    Returns:
        EsiLink: An instance of the EsiLink class.
    """
    return EsiLink(
        auth_manager_db_path=self.settings.eve_auth_manager_settings.authorization_database_path,
        web_cache_path=self.settings.api_request_settings.web_cache_path,
        max_rate=self.settings.max_rate,
        time_period=self.settings.time_period,
    )

get_schema(compatibility_date: str | None = None) -> EsiSchema

Fetches an ESI schema from the EsiLink schema cache.

This function retrieves the ESI schema from the local cache managed by the SchemaCacheManager. If a compatibility date is provided, it fetches the schema corresponding to that date; otherwise, it retrieves the latest available schema.

This function will check for new schema updates from the ESI schema repository once per downtime before returning the schema.

It is suitable for one-off scripts or commands. When executing multiple requests, consider using SchemaCacheManager directly to avoid repeated cache lookups.

Parameters:

Name Type Description Default
compatibility_date str | None

Optional compatibility date in YYYY-MM-DD format. If not provided, the latest schema is fetched.

None

Returns:

Type Description
EsiSchema

The EsiSchema object for the specified compatibility date or the latest schema if no date is provided.

Raises:

Type Description
ValueError

If no cached schema entries exist.

Source code in src/pfmsoft/eve_link/esi_link.py
def get_schema(self, compatibility_date: str | None = None) -> EsiSchema:
    """Fetches an ESI schema from the EsiLink schema cache.

    This function retrieves the ESI schema from the local cache managed by the
    SchemaCacheManager. If a compatibility date is provided, it fetches the schema
    corresponding to that date; otherwise, it retrieves the latest available schema.

    This function will check for new schema updates from the ESI schema repository
    once per downtime before returning the schema.

    It is suitable for one-off scripts or commands. When executing multiple requests,
    consider using SchemaCacheManager directly to avoid repeated cache lookups.

    Args:
        compatibility_date: Optional compatibility date in YYYY-MM-DD format.
            If not provided, the latest schema is fetched.

    Returns:
        The EsiSchema object for the specified compatibility date or the latest
            schema if no date is provided.

    Raises:
        ValueError: If no cached schema entries exist.
    """
    schema_manager = self.schema_cache_manager_factory()
    with client_manager(USER_AGENT) as session:
        schema_manager.fetch_updates(session=session)
    if compatibility_date is not None:
        esi_schema = schema_manager.load(compatibility_date=compatibility_date)
    else:
        esi_schema = schema_manager.latest_schema()
    return esi_schema

make_request(esi_request: EsiRequest, schema: EsiSchema) -> EsiResponse | FailedEsiResponse async

Validate, execute, and return a single ESI request response.

This function wraps the EsiLink class to provide a simple interface for executing a single ESI request. It creates a temporary EsiLink instance, validates the request against the provided schema, and executes it. The response is returned as either an EsiResponse or a FailedEsiResponse, depending on the outcome of the request.

When used in situations where multiple requests need to be executed, consider using the EsiLink class directly to avoid the overhead of creating and closing multiple instances with their associated resources.

Parameters:

Name Type Description Default
esi_request EsiRequest

The ESI request to execute.

required
schema EsiSchema

The ESI schema for validation.

required

Returns:

Type Description
EsiResponse | FailedEsiResponse

EsiResponse if the request is successful, otherwise FailedEsiResponse.

Source code in src/pfmsoft/eve_link/esi_link.py
async def make_request(
    self, esi_request: EsiRequest, schema: EsiSchema
) -> EsiResponse | FailedEsiResponse:
    """Validate, execute, and return a single ESI request response.

    This function wraps the EsiLink class to provide a simple interface for executing
    a single ESI request. It creates a temporary EsiLink instance, validates the request
    against the provided schema, and executes it. The response is returned as either
    an EsiResponse or a FailedEsiResponse, depending on the outcome of the request.

    When used in situations where multiple requests need to be executed, consider using
    the EsiLink class directly to avoid the overhead of creating and closing multiple
    instances with their associated resources.

    Args:
        esi_request: The ESI request to execute.
        schema: The ESI schema for validation.

    Returns:
        EsiResponse if the request is successful, otherwise FailedEsiResponse.
    """
    async with self.esi_link_factory() as esi_link:
        response = await esi_link.make_request(
            esi_request=esi_request, schema=schema
        )
        return response

make_requests(esi_requests: EsiRequestGroup, schema: EsiSchema) -> EsiResponseGroup async

Validate, execute, and return a group of ESI request responses.

This function wraps the EsiLink class to provide a simple interface for executing a group of ESI requests. It creates a temporary EsiLink instance, validates the requests against the provided schema, and executes them. The responses are returned as an EsiResponseGroup, containing both successful and failed responses.

When used in situations where multiple requests need to be executed, consider using the EsiLink class directly to avoid the overhead of creating and closing multiple instances.

Parameters:

Name Type Description Default
esi_requests EsiRequestGroup

The group of ESI requests to execute.

required
schema EsiSchema

The ESI schema for validation.

required

Returns:

Name Type Description
EsiResponseGroup EsiResponseGroup

The group of ESI request responses.

Source code in src/pfmsoft/eve_link/esi_link.py
async def make_requests(
    self, esi_requests: EsiRequestGroup, schema: EsiSchema
) -> EsiResponseGroup:
    """Validate, execute, and return a group of ESI request responses.

    This function wraps the EsiLink class to provide a simple interface for executing
    a group of ESI requests. It creates a temporary EsiLink instance, validates the
    requests against the provided schema, and executes them. The responses are returned
    as an EsiResponseGroup, containing both successful and failed responses.

    When used in situations where multiple requests need to be executed, consider using
    the EsiLink class directly to avoid the overhead of creating and closing multiple
    instances.

    Args:
        esi_requests: The group of ESI requests to execute.
        schema: The ESI schema for validation.

    Returns:
        EsiResponseGroup: The group of ESI request responses.
    """
    async with self.esi_link_factory() as esi_link:
        response_group = await esi_link.make_requests(esi_requests, schema)
        return response_group

schema_cache_manager_factory() -> SchemaCacheManager

Factory function to create an instance of SchemaCacheManager from settings.

Returns:

Name Type Description
SchemaCacheManager SchemaCacheManager

An instance of the SchemaCacheManager class.

Source code in src/pfmsoft/eve_link/esi_link.py
def schema_cache_manager_factory(self) -> SchemaCacheManager:
    """Factory function to create an instance of SchemaCacheManager from settings.

    Returns:
        SchemaCacheManager: An instance of the SchemaCacheManager class.
    """
    return SchemaCacheManager(cache_directory=self.settings.schema_cache_directory)

get_settings(application_directory: Path | None = None) -> EsiLinkSettings

Build runtime settings from a Pydantic settings model or application directory.

Parameters:

Name Type Description Default
application_directory Path | None

Optional application directory path. If not provided, the default application directory is used.

None

Returns:

Type Description
EsiLinkSettings

Runtime settings dataclass used by the application.

Raises:

Type Description
ValueError

If the provided application directory exists but is not a directory.

Source code in src/pfmsoft/eve_link/settings.py
def get_settings(
    application_directory: Path | None = None,
) -> EsiLinkSettings:
    """Build runtime settings from a Pydantic settings model or application directory.

    Args:
        application_directory (Path | None): Optional application directory path.
            If not provided, the default application directory is used.

    Returns:
        Runtime settings dataclass used by the application.

    Raises:
        ValueError: If the provided application directory exists but is not a directory.
    """
    if application_directory is None:
        # If the application directory is not provided, use the value from the Pydantic
        # settings model. This allows for environment variable overrides and .env file loading.
        application_directory = EsiLinkSettingsPydantic().application_directory
    application_directory = application_directory.expanduser().resolve()
    if application_directory.exists() and not application_directory.is_dir():
        raise ValueError(
            f"Application directory '{application_directory}' exists but is not a directory."
        )
    settings = _initialize_settings(application_directory)
    return settings