Python · Async · v4.0.0

eaf_base_api

The version 4 asynchronous networking and media-loading engine for EchterAlsFake API wrappers. It provides explicit request APIs, bounded retries, byte-limited TTL caches, source-aware media models, structured scrape streams, and HLS/RAW downloads.

$ pip install eaf_base_api 📋
GitHub →

🏗️ Overview & Version 4

Version 4 is intentionally breaking
The legacy multi-mode BaseCore.fetch(), BaseMedia.load(api=..., html=...), mutable ScrapeResult.video/is_success contract, and boolean retry callbacks were removed. Use the explicit interfaces documented below.

eaf_base_api is the shared engine used by all site-specific API packages. Version 4 separates each responsibility into an explicit, typed interface:

  • BaseCore — HTTP sessions, request retries, response decoding, caching, proxy/interface binding, HLS inspection, and downloads
  • BaseMedia + media_field — source-aware, atomic lazy loading with deterministic source precedence
  • Helper + IteratorConfig — bounded page/item scheduling, ordering, retries, error handlers, and load selection
  • ScrapeStream + ScrapeResult — deterministic stream cleanup and immutable success/failure results
  • CacheBackend — replaceable storage contract; the built-in cache uses byte limits and TTL expiry

📦 Installation

shell
pip install eaf_base_api

# Install optional HLS parsing/remux dependencies as well
pip install "eaf_base_api[hls]"

Version 4 requires Python 3.12 or newer. The package ships a py.typed marker, so type checkers can consume its inline annotations.

🎛️ RuntimeConfig

Create a RuntimeConfig for each independently configured BaseCore. A process-wide config instance remains available as the default, but a dedicated instance avoids unrelated clients changing one another.

AttributeTypeDefaultDescription
response_cache_size_bytesint32 MiBMaximum encoded size of cached text responses; set to 0 to disable
response_cache_ttlfloat300.0Text-response lifetime in seconds
segment_cache_size_bytesint8 MiBMaximum encoded size of cached HLS segment URL lists
segment_cache_ttlfloat300.0HLS segment-list lifetime in seconds
request_attemptsint4Total request attempts, including the first call
request_retry_initial_delayfloat0.5Initial exponential retry delay in seconds
request_retry_max_delayfloat30.0Maximum exponential retry delay
request_multiplierfloat2.0Exponential base for BaseCore request backoff and multiplier for derived page/item retry policies
request_retry_jitterfloat0.5Maximum random jitter added to retry delays
request_delayint0Minimum delay between requests made by this core
timeoutint20Default request timeout in seconds
max_bandwidth_mbfloat | NoneNoneAggregate download receive limit in MB/s
proxystr | NoneNoneOne HTTP, HTTPS, or SOCKS proxy URL
proxy_authstr | NoneNoneProxy credentials as "username:password"
interfacestr | NoneNoneLocal interface IP address to bind
http_versionstr"v2""v1", "v2", or "v3"
dns_over_httpsstr | NoneNoneDNS-over-HTTPS endpoint
impersonationstr"chrome"curl_cffi browser impersonation profile
custom_ja3str | NoneNoneAdvanced custom TLS JA3 fingerprint
verify_sslboolTrueVerify TLS certificates
trust_envboolFalseUse proxy and CA settings from the environment
cookiesdict[str, str] | NoneNoneInitial cookie mapping applied when each new HTTP session is created
localestr"en-US,en;q=0.9"Default Accept-Language; changing it can affect site parsers
max_workers_downloadint20HLS download worker count for this core
videos_concurrencyint5Fallback item concurrency for resolved iterator configs
pages_concurrencyint2Fallback page concurrency for resolved iterator configs

When changes take effect

Cache limits/TTLs and the initial locale header are captured when BaseCore is constructed. Proxy, interface, TLS, HTTP, DoH, impersonation, cookies, and bandwidth options are captured whenever its session is created. Request budgets, retry delays/multiplier, and request delay are read for each request; HLS timeout/workers are read when a download begins; and omitted IteratorConfig values are resolved whenever a new stream is created.

Applying changed session settings
initialize_session() is idempotent: it creates a session only while core.session is None. To apply changed session-bound settings such as proxy, cookies, TLS, or interface, call await core.close(); the next request, context-manager entry, or explicit initialize_session() creates a fresh session from the current configuration.

🔗 Client Integration & Lifecycle

python
from base_api import BaseCore
from base_api.modules.config import RuntimeConfig
from xvideos_api import Client

runtime = RuntimeConfig()
runtime.timeout = 60
runtime.request_attempts = 5
runtime.request_multiplier = 2.0
runtime.proxy = "socks5://127.0.0.1:9050"
runtime.interface = None  # Or a local interface IP
runtime.cookies = {"session": "value"}

core = BaseCore(configuration=runtime)
client = Client(core=core)

try:
    video = await client.get_video("https://www.xvideos.com/video...")
finally:
    await core.close()

BaseCore starts without an HTTP session. Context-manager entry, the first request, or an explicit initialize_session() creates it lazily; further initialization calls are no-ops while that session remains live. Prefer async with BaseCore(configuration=runtime) as core: for direct use. await core.close() closes and clears the current session, and a later request safely recreates it from the core's current configuration.

🌐 Explicit Request API

Version 4 replaces the flag-driven fetch() method with one method per representation:

MethodReturnsCache behavior
await core.request(url, ...)curl_cffi.ResponseNever uses the text cache
await core.fetch_text(url, ...)strSuccessful GET text can use the configured cache
await core.fetch_bytes(url, ...)bytesNever uses the text cache
python
from base_api import BaseCore, CachePolicy

async with BaseCore() as core:
    response = await core.request(url)
    text = await core.fetch_text(url)
    data = await core.fetch_bytes(download_url)

    refreshed = await core.fetch_text(
        url,
        cache_policy=CachePolicy.REFRESH,
    )
    uncached = await core.fetch_text(
        url,
        cache_policy=CachePolicy.BYPASS,
    )

CachePolicy.USE reads and writes, REFRESH skips the read and replaces the cached value, and BYPASS neither reads nor writes. All three request methods accept timeout, cookies, redirects, data, HTTP method, headers, JSON, parameters, and retry_non_idempotent.

🔁 BaseCore Request Retries

RuntimeConfig.request_attempts includes the first request. Backoff uses request_retry_initial_delay, request_multiplier as Tenacity's exponential base, request_retry_max_delay, and request_retry_jitter. Idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, and TRACE) retry network failures plus HTTP 408, 425, 429, and 5xx responses. A 429 response honors Retry-After when available.

  • POST/PATCH and other non-idempotent operations are attempted once unless retry_non_idempotent=True is explicitly safe.
  • Non-retryable HTTP failures—including 401/403, 404, 410, and 4xx responses other than 408, 425, and 429—are terminal.
  • Exhaustion raises RequestRetriesExhausted with .url, .attempts, and .last_error.
Two separate retry layers
RuntimeConfig controls individual HTTP requests. RetryPolicy controls complete page extraction or media-item loading inside Helper. When those policies are derived, both layers use RuntimeConfig.request_multiplier; avoid multiplying both budgets unnecessarily.

🔒 Proxy & Interface Binding

python
from base_api.modules.config import RuntimeConfig

runtime = RuntimeConfig()
runtime.proxy = "socks5://127.0.0.1:9050"
runtime.proxy_auth = "username:password"
runtime.interface = "192.0.2.10"

# Only disable verification for a proxy you control and trust.
runtime.verify_ssl = False

The old proxies mapping was replaced by the singular proxy URL. interface is passed to curl_cffi.AsyncSession and must be a local interface IP address.

💾 Caching

The built-in Cache uses separate, thread-safe TTL caches for text responses and HLS segment lists. Limits are measured in UTF-8 bytes, not item counts. Request keys include method, URL, redirects, parameters, body, headers, and cookies; sensitive values are fingerprinted rather than stored in plaintext keys.

  • Only successful GET text responses are stored.
  • Concurrent cache misses for the same request share one in-flight network operation.
  • core.cache.clear() clears both built-in caches.
  • Pass a custom CacheBackend to BaseCore(cache=...) to replace storage.

🧩 Source-aware BaseMedia

Remote fields are declared with media_field(). The first source has highest precedence. A loader is async, returns a complete mapping, and never mutates the object directly; results are validated and committed atomically.

python
from dataclasses import dataclass
from typing import ClassVar
from base_api import BaseMedia, media_field

@dataclass(kw_only=True, slots=True)
class Video(BaseMedia):
    title: str | None = media_field("html", "api")
    duration: int | None = media_field("api")

    loader_methods: ClassVar[dict[str, str]] = {
        "html": "_load_html",
        "api": "_load_api",
    }

    async def _load_html(self) -> dict[str, object]:
        data = await fetch_html(self.url)
        return {"title": data.get("title")}

    async def _load_api(self) -> dict[str, object]:
        data = await fetch_api(self.url)
        return {"title": data.get("title"), "duration": data.get("duration")}
OperationMeaning
await media.load_sources("html", "api")Load named sources concurrently; repeated calls are idempotent
await media.load_fields("title")Select the smallest useful source set for unresolved fields
await media.get_field("title")Load one field if necessary and return it
media.loaded_sourcesImmutable set of successful source names
media.source_state("html")NOT_LOADED, LOADING, LOADED, or FAILED
media.source_errorsCopy of the latest failure per source
media.is_field_loaded(name)True even when a loader deliberately returned None
media.unloaded_fields()Names still holding the private unloaded marker
media.to_dict()Serialize loaded public fields without triggering lazy-field errors

Direct access to an unresolved field raises DataNotLoadedError. Use retry_failed=False with a load method when a previously failed source should not be attempted again.

⚙️ IteratorConfig

Site-specific iterator methods now accept one IteratorConfig instead of many concurrency, ordering, loading, and callback parameters. Values left as None are resolved from the active core's RuntimeConfig.

AttributeDefaultDescription
max_page_concurrencyRuntimeConfig.pages_concurrencyConcurrent page operations
max_item_concurrencyRuntimeConfig.videos_concurrencyConcurrent media-item operations
max_pending_items4 × item concurrencyBackpressure limit for extracted items awaiting work
extract_in_threadTrueRun the synchronous extractor and its iteration in a worker thread
orderResultOrder.COMPLETIONYield fastest results first or restore original page/item order
page_error_modeErrorMode.YIELDTerminal page failure behavior
item_error_modeErrorMode.YIELDTerminal item failure behavior
page_retryderived from RuntimeConfigBounded retry policy for a complete page operation
item_retryderived from RuntimeConfigBounded retry policy for construction/loading of one item
page_error_handlerNoneOptional sync/async handler receiving ScrapeErrorContext
item_error_handlerNoneOptional sync/async handler receiving ScrapeErrorContext
load_specific_fields()Fields each constructed media item must load
load_specific_sources()Sources each constructed media item must load before fields
Preserve package loading defaults
Passing a custom IteratorConfig replaces that API method's default config. Include the method's required load_specific_sources or load_specific_fields; most site packages use ("html",), while some dual-source APIs use both "api" and "html".

🛡️ RetryPolicy & Custom Error Handling

RetryPolicy.max_attempts includes the first attempt. Without a custom handler, eligible exception classes are selected with retry_for; the delay is bounded exponential backoff plus uniformly random jitter.

Resolved defaults and nested retry budgets
A standalone RetryPolicy() performs one stage attempt. When an IteratorConfig leaves page_retry or item_retry unset, resolve() instead creates both policies from the active RuntimeConfig; the shipped defaults are four stage attempts, base_delay=0.5, multiplier=2.0, max_delay=30.0, and jitter=0.5. A stage attempt may call BaseCore, whose separate four-attempt request budget can therefore multiply the number of HTTP calls. Set both layers deliberately for your workload.
python
from base_api import (
    ErrorAction,
    ErrorMode,
    MediaLoadError,
    MediaLoadErrors,
    ResultOrder,
    RetryPolicy,
    ScrapeErrorContext,
)
from base_api.modules.config import IteratorConfig
from base_api.modules.errors import ResourceGone

def resource_is_gone(error: BaseException) -> bool:
    if isinstance(error, ResourceGone):
        return True
    if isinstance(error, MediaLoadError):
        return resource_is_gone(error.original_error)
    if isinstance(error, MediaLoadErrors):
        return any(resource_is_gone(item) for item in error.errors)
    return False

def handle_page_error(context: ScrapeErrorContext) -> ErrorAction:
    print("page", context.url, context.attempt, context.error)
    return ErrorAction.RETRY

async def handle_item_error(context: ScrapeErrorContext) -> ErrorAction:
    print("item", context.url, context.attempt, context.error)
    if resource_is_gone(context.error):
        return ErrorAction.SKIP
    return ErrorAction.RETRY

iterator_config = IteratorConfig(
    max_page_concurrency=2,
    max_item_concurrency=8,
    order=ResultOrder.ORIGINAL,
    item_retry=RetryPolicy(
        max_attempts=3,
        base_delay=0.5,
        multiplier=2.0,
        max_delay=8.0,
        jitter=0.25,
        retry_for=(Exception,),
    ),
    page_error_handler=handle_page_error,
    item_error_handler=handle_item_error,
    item_error_mode=ErrorMode.YIELD,
    page_error_mode=ErrorMode.SKIP,
    load_specific_sources=("html",),
)

Page and item handlers are independent; each receives only failures from its own stage, and either field may be left unset to use automatic policy for that stage. A sync or async handler runs on every failed stage attempt and may return RETRY, RAISE, YIELD, or SKIP. An explicit RETRY can override retry_for, but it cannot exceed the policy's hard limit; on the final attempt it falls back to the configured error mode. Page failures containing a nested HTTP 404 remain terminal. A handler that raises or returns another value produces a fatal ErrorHandlerError.

🌊 ScrapeStream & ScrapeResult

Helper.iterator() returns a lazily started ScrapeStream. Exhausting it cleans up normally; use it as an async context manager whenever the consumer may stop early.

python
stream = helper.iterator(
    target_page_urls=page_urls,
    item_extractor=extractor,
    iterator_config=iterator_config,
)

async with stream:
    async for result in stream:
        if not result.succeeded:
            print(result.stage, result.url, result.error)
            continue
        media = result.unwrap()  # Same object as result.item after the check
ScrapeResult fieldDescription
stageScrapeStage.PAGE or ScrapeStage.ITEM
urlPage or item URL associated with this outcome
page_index / item_indexStable original ordering coordinates
attemptsNumber of attempts consumed
itemLoaded media on success, otherwise None
errorTyped PageFetchError/ItemFetchError on yielded failure
succeededTrue exactly when the result contains an item
unwrap()Return the item or raise the stored typed scrape error

⬇️ Download Configurations

Shared fields

DownloadConfigHLS and DownloadConfigRAW share quality, path, callback, no_title, and stop_event. Quality accepts a supported integer height or "best", "half", "worst", and common "720p"-style values.

DownloadConfigHLS

FieldDefaultDescription
m3u8_base_urlNoneMaster playlist URL, awaitable, or callable used by BaseCore.download
remuxFalseRemux concatenated transport stream to MP4
start_segment0First segment index
segment_state_pathNoneJSON resume-state file
segment_dirNoneDirectory for downloaded segments
return_reportFalseReturn a DownloadReport instead of only a boolean
cleanup_on_stopTrueRemove temporary state when cancelled
keep_segment_dirFalseRetain segment files after completion
callback_remuxNoneProgress callback for remuxing
ios_supportFalseEnable the iOS-compatible remux path

BaseCore.download() reads RuntimeConfig.timeout and RuntimeConfig.max_workers_download from that core when dispatching each HLS download. Dedicated cores therefore keep their HLS timeout and worker settings isolated from the process-wide default configuration.

DownloadConfigRAW

Direct-file downloads add allow_multipart=True, max_workers=5, read_timeout=120.0, chunk_size=1024, and max_retries=5. These downloader retries are separate from RuntimeConfig.request_attempts.

📝 Logging & Cleanup

python
import logging

core.enable_logging(level=logging.DEBUG)
core.enable_logging(log_file="api.log", level=logging.INFO)
core.enable_logging(
    log_ip="192.168.1.100",
    log_port=8080,
    level=logging.DEBUG,
)

# Always release the curl_cffi connection pool.
await core.close()

🚚 Migration from 3.x

3.x4.0 replacement
fetch(url)fetch_text(url)
fetch(..., get_bytes=True)fetch_bytes(...)
fetch(..., get_response=True)request(...)
fetch(..., save_cache=False)fetch_text(..., cache_policy=CachePolicy.BYPASS)
max_cache_itemsresponse_cache_size_bytes + segment_cache_size_bytes
max_retriesrequest_attempts
proxiesproxy
load(api=True, html=False)load_sources("api") or load_fields(...)
on_error_hint returning boolErrorHandler returning ErrorAction
keep_original_order=TrueIteratorConfig(order=ResultOrder.ORIGINAL)
max_video_concurrencyIteratorConfig(max_item_concurrency=...)
result.is_successresult.succeeded
result.videoresult.unwrap() or checked result.item

🚨 Error Reference

Errors live in base_api.modules.errors; frequently used version 4 errors are also exported from base_api.

Loader exceptions are wrapped
An exception raised by one source loader is exposed as MediaLoadError; inspect its original_error for a site package's NotFound, RegionBlocked, or similar exception. If several requested sources fail together, MediaLoadErrors.errors contains each failure. Helper item failures add one more typed ItemFetchError layer whose original_error is the media-load error.
FamilyExceptions and meaning
HTTP/networkNetworkRequestError, HTTPStatusError, RateLimitError, RequestRetriesExhausted, ResourceGone, AccessDeniedError, InvalidProxy, ProxySSLError
Media fieldsUnknownMediaFieldError, FieldNotLoadableError, DataNotLoadedError
Media loadersLoaderConfigurationError, LoaderContractError, MediaLoadError, MediaLoadErrors
Scrape operationsPageFetchError, ItemFetchError, ErrorHandlerError
Downloads/playlistsDownloadCancelled, SegmentError, PlaylistExtractionError, StateLoadError, MaxRetriesExceeded
Bot challengesBotProtectionDetected, ChallengeRegexError, ChallengeMathError, SecurityAbort

📋 Changelog

Date / VersionChanges since 3.3.3
2026-08-11 — 4.0.0Added the py.typed marker; finalized runtime-resolved IteratorConfig values; expanded typed exports; improved page/item exception logging and terminal error context. Commits 13b4105184df57.
2026-08-08 — 4.0.0Centralized page/item concurrency, ordering, loading, retry policies, and handlers in IteratorConfig; resolved omitted policies from RuntimeConfig; expanded inline API documentation. Commits 2705faabaccdfd.
2026-08-07 — 4.0.0Breaking v4 redesign: explicit request methods and cache policies; byte-bounded TTL caches and single-flight text fetches; source-aware atomic media loading; bounded dynamic Helper scheduling; immutable ScrapeResult; owned ScrapeStream; structured retry/error policies. Commits 37176cb, 7294115.
2026-08-07 — 3.3.6Improved legacy iterator ordering and ensured worker cancellation/cleanup when iteration finishes or the consumer exits early. Commits 34286d0, 76b3023.
2026-08-04 — 3.3.5Added local interface binding and corrected proxy initialization to use a single proxy URL. Commit a3ca95e.
2026-07-27 — 3.3.4Hardened output filename sanitization against path traversal and invalid filename components. Commit eec47e3.
2026-07-26 — 3.3.3Baseline requested for this documentation update; licensing changed to AGPL-3.0-or-later. Commit 3ee47f7.