Source code for aeat.core._optional_extras
"""Capability-gated optional package extras and their import guard.
The shipped package is lean: a bare ``pip install aeat`` omits the optional
integration stacks (Google export, the live-AEAT browser, the Anthropic-API LLM
provider). Each maps to a ``[project.optional-dependencies]`` extra and is
imported lazily, so the core CLI builds and runs without it.
This module is the single source of truth for those extras. It lives in ``core``
— the innermost layer — so an adapter can guard its own external-library import
without importing the application layer, and the application doctor can probe the
same :data:`OPTIONAL_EXTRAS` registry through
:func:`application.provisioning.probe_optional_extra`. :func:`require_optional_extra`
is the seam every feature boundary calls before its lazy import so a missing
extra becomes one instructive :class:`MissingOptionalExtraError` naming
``pip install aeat-cli[<extra>]`` instead of a raw deep-stack
``ModuleNotFoundError``.
These records describe package availability only. They do not decide whether an
operator has opted into Google export, browser automation, or hosted LLM usage;
that consent surface is represented separately by :class:`~core.ServiceCapability`.
"""
from __future__ import annotations
import importlib.util
from pydantic import BaseModel, Field
from ._models import STRICT_FROZEN_CONFIG
from .errors import CoreError
__all__ = [
"ANTHROPIC_EXTRA",
"BROWSER_EXTRA",
"GOOGLE_EXTRA",
"OPTIONAL_EXTRAS",
"MissingOptionalExtraError",
"OptionalExtra",
"optional_extra_available",
"require_optional_extra",
]
[docs]
class OptionalExtra(BaseModel):
"""A capability-gated optional package extra and how to probe/install it.
Attributes:
extra: The ``[project.optional-dependencies]`` key.
import_name: Importable package/module name used by the spec-only probe.
feature: Human-readable feature label used in refusals and doctor rows.
"""
model_config = STRICT_FROZEN_CONFIG
extra: str = Field(min_length=1)
import_name: str = Field(min_length=1)
feature: str = Field(min_length=1)
@property
def install_hint(self) -> str:
"""Return the exact package-install command for this extra.
This is a dependency remediation hint, not a runtime provisioning command
such as ``playwright install chromium``.
"""
return f"pip install aeat-cli[{self.extra}]"
# The capability-mapped optional extras declared in
# ``[project.optional-dependencies]``. Each adapter family guards its own entry
# with the matching constant; the doctor enumerates the tuple.
GOOGLE_EXTRA = OptionalExtra(extra="google", import_name="googleapiclient", feature="Google Drive / Sheets export")
BROWSER_EXTRA = OptionalExtra(extra="browser", import_name="playwright", feature="live AEAT browser automation")
ANTHROPIC_EXTRA = OptionalExtra(extra="anthropic", import_name="anthropic", feature="the Anthropic-API LLM provider")
OPTIONAL_EXTRAS: tuple[OptionalExtra, ...] = (GOOGLE_EXTRA, BROWSER_EXTRA, ANTHROPIC_EXTRA)
[docs]
class MissingOptionalExtraError(CoreError, ImportError):
"""Raised when a feature is reached but its optional extra is not installed.
Descends from :class:`~core.errors.CoreError` so the project-wide
:class:`~core.errors.AeatError` boundary sees the refusal, and from
:class:`ImportError` so adapters that already catch import failures keep
working. Application probes report the same missing package as a
:class:`application.provisioning.DependencyStatus`; feature guards raise
this exception only when the operator reaches the guarded boundary.
Attributes:
extra: Optional-extra registry record that failed the spec-only probe.
install_hint: Exact ``pip install aeat-cli[<extra>]`` remediation command.
"""
def __init__(self, extra: OptionalExtra) -> None:
self.extra = extra
self.install_hint = extra.install_hint
message = f"{extra.feature} requires the optional '{extra.extra}' extra. Install it with: {extra.install_hint}"
super().__init__(
message,
context={"extra": extra.extra, "import_name": extra.import_name, "feature": extra.feature},
suggestion=extra.install_hint,
)
self.name = extra.import_name
self.path = None
[docs]
def optional_extra_available(extra: OptionalExtra) -> bool:
"""Return whether ``extra``'s package is importable, without importing it.
A spec-only check (:func:`importlib.util.find_spec`) — no side effects, no
heavy module load. Never raises: a missing parent package resolves to
``False``. This helper intentionally does not call
:func:`require_optional_extra`; probes should report dependency status, not
raise feature-boundary refusals.
Args:
extra: The :class:`OptionalExtra` registry record to probe.
Returns:
``True`` when ``extra.import_name`` has an import spec; otherwise
``False``.
"""
try:
return importlib.util.find_spec(extra.import_name) is not None
except ModuleNotFoundError:
return False
[docs]
def require_optional_extra(extra: OptionalExtra) -> None:
"""Raise :class:`MissingOptionalExtraError` when ``extra`` is absent; a no-op when present.
Call this at a feature boundary, immediately before the lazy import of the
extra's package, so a missing extra becomes a single actionable message
instead of a raw deep-stack ``ModuleNotFoundError``.
Args:
extra: The :class:`OptionalExtra` required by the feature boundary.
Raises:
MissingOptionalExtraError: If ``extra.import_name`` is not importable.
"""
if not optional_extra_available(extra):
raise MissingOptionalExtraError(extra)