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.
Overview & Version 4
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
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.
| Attribute | Type | Default | Description |
|---|---|---|---|
response_cache_size_bytes | int | 32 MiB | Maximum encoded size of cached text responses; set to 0 to disable |
response_cache_ttl | float | 300.0 | Text-response lifetime in seconds |
segment_cache_size_bytes | int | 8 MiB | Maximum encoded size of cached HLS segment URL lists |
segment_cache_ttl | float | 300.0 | HLS segment-list lifetime in seconds |
request_attempts | int | 4 | Total request attempts, including the first call |
request_retry_initial_delay | float | 0.5 | Initial exponential retry delay in seconds |
request_retry_max_delay | float | 30.0 | Maximum exponential retry delay |
request_multiplier | float | 2.0 | Exponential base for BaseCore request backoff and multiplier for derived page/item retry policies |
request_retry_jitter | float | 0.5 | Maximum random jitter added to retry delays |
request_delay | int | 0 | Minimum delay between requests made by this core |
timeout | int | 20 | Default request timeout in seconds |
max_bandwidth_mb | float | None | None | Aggregate download receive limit in MB/s |
proxy | str | None | None | One HTTP, HTTPS, or SOCKS proxy URL |
proxy_auth | str | None | None | Proxy credentials as "username:password" |
interface | str | None | None | Local interface IP address to bind |
http_version | str | "v2" | "v1", "v2", or "v3" |
dns_over_https | str | None | None | DNS-over-HTTPS endpoint |
impersonation | str | "chrome" | curl_cffi browser impersonation profile |
custom_ja3 | str | None | None | Advanced custom TLS JA3 fingerprint |
verify_ssl | bool | True | Verify TLS certificates |
trust_env | bool | False | Use proxy and CA settings from the environment |
cookies | dict[str, str] | None | None | Initial cookie mapping applied when each new HTTP session is created |
locale | str | "en-US,en;q=0.9" | Default Accept-Language; changing it can affect site parsers |
max_workers_download | int | 20 | HLS download worker count for this core |
videos_concurrency | int | 5 | Fallback item concurrency for resolved iterator configs |
pages_concurrency | int | 2 | Fallback 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.
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
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:
| Method | Returns | Cache behavior |
|---|---|---|
await core.request(url, ...) | curl_cffi.Response | Never uses the text cache |
await core.fetch_text(url, ...) | str | Successful GET text can use the configured cache |
await core.fetch_bytes(url, ...) | bytes | Never uses the text cache |
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=Trueis explicitly safe. - Non-retryable HTTP failures—including 401/403, 404, 410, and 4xx responses other than 408, 425, and 429—are terminal.
- Exhaustion raises
RequestRetriesExhaustedwith.url,.attempts, and.last_error.
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
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
CacheBackendtoBaseCore(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.
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")}
| Operation | Meaning |
|---|---|
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_sources | Immutable set of successful source names |
media.source_state("html") | NOT_LOADED, LOADING, LOADED, or FAILED |
media.source_errors | Copy 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.
| Attribute | Default | Description |
|---|---|---|
max_page_concurrency | RuntimeConfig.pages_concurrency | Concurrent page operations |
max_item_concurrency | RuntimeConfig.videos_concurrency | Concurrent media-item operations |
max_pending_items | 4 × item concurrency | Backpressure limit for extracted items awaiting work |
extract_in_thread | True | Run the synchronous extractor and its iteration in a worker thread |
order | ResultOrder.COMPLETION | Yield fastest results first or restore original page/item order |
page_error_mode | ErrorMode.YIELD | Terminal page failure behavior |
item_error_mode | ErrorMode.YIELD | Terminal item failure behavior |
page_retry | derived from RuntimeConfig | Bounded retry policy for a complete page operation |
item_retry | derived from RuntimeConfig | Bounded retry policy for construction/loading of one item |
page_error_handler | None | Optional sync/async handler receiving ScrapeErrorContext |
item_error_handler | None | Optional 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 |
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.
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.
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.
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 field | Description |
|---|---|
stage | ScrapeStage.PAGE or ScrapeStage.ITEM |
url | Page or item URL associated with this outcome |
page_index / item_index | Stable original ordering coordinates |
attempts | Number of attempts consumed |
item | Loaded media on success, otherwise None |
error | Typed PageFetchError/ItemFetchError on yielded failure |
succeeded | True 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
| Field | Default | Description |
|---|---|---|
m3u8_base_url | None | Master playlist URL, awaitable, or callable used by BaseCore.download |
remux | False | Remux concatenated transport stream to MP4 |
start_segment | 0 | First segment index |
segment_state_path | None | JSON resume-state file |
segment_dir | None | Directory for downloaded segments |
return_report | False | Return a DownloadReport instead of only a boolean |
cleanup_on_stop | True | Remove temporary state when cancelled |
keep_segment_dir | False | Retain segment files after completion |
callback_remux | None | Progress callback for remuxing |
ios_support | False | Enable 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
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.x | 4.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_items | response_cache_size_bytes + segment_cache_size_bytes |
max_retries | request_attempts |
proxies | proxy |
load(api=True, html=False) | load_sources("api") or load_fields(...) |
on_error_hint returning bool | ErrorHandler returning ErrorAction |
keep_original_order=True | IteratorConfig(order=ResultOrder.ORIGINAL) |
max_video_concurrency | IteratorConfig(max_item_concurrency=...) |
result.is_success | result.succeeded |
result.video | result.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.
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.
| Family | Exceptions and meaning |
|---|---|
| HTTP/network | NetworkRequestError, HTTPStatusError, RateLimitError, RequestRetriesExhausted, ResourceGone, AccessDeniedError, InvalidProxy, ProxySSLError |
| Media fields | UnknownMediaFieldError, FieldNotLoadableError, DataNotLoadedError |
| Media loaders | LoaderConfigurationError, LoaderContractError, MediaLoadError, MediaLoadErrors |
| Scrape operations | PageFetchError, ItemFetchError, ErrorHandlerError |
| Downloads/playlists | DownloadCancelled, SegmentError, PlaylistExtractionError, StateLoadError, MaxRetriesExceeded |
| Bot challenges | BotProtectionDetected, ChallengeRegexError, ChallengeMathError, SecurityAbort |
Changelog
| Date / Version | Changes since 3.3.3 |
|---|---|
| 2026-08-11 — 4.0.0 | Added the py.typed marker; finalized runtime-resolved IteratorConfig values; expanded typed exports; improved page/item exception logging and terminal error context. Commits 13b4105–184df57. |
| 2026-08-08 — 4.0.0 | Centralized page/item concurrency, ordering, loading, retry policies, and handlers in IteratorConfig; resolved omitted policies from RuntimeConfig; expanded inline API documentation. Commits 2705faa–baccdfd. |
| 2026-08-07 — 4.0.0 | Breaking 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.6 | Improved legacy iterator ordering and ensured worker cancellation/cleanup when iteration finishes or the consumer exits early. Commits 34286d0, 76b3023. |
| 2026-08-04 — 3.3.5 | Added local interface binding and corrected proxy initialization to use a single proxy URL. Commit a3ca95e. |
| 2026-07-27 — 3.3.4 | Hardened output filename sanitization against path traversal and invalid filename components. Commit eec47e3. |
| 2026-07-26 — 3.3.3 | Baseline requested for this documentation update; licensing changed to AGPL-3.0-or-later. Commit 3ee47f7. |