Source code for httk.core.datastream.textstream_url

import io
import urllib.parse
import urllib.request
from typing import Any, Self, cast

from .compression import open_compressed, validate_compression
from .network_policy import NETWORK_SCHEMES, require_network_consent, resolve_timeout
from .textstream_backend import TextstreamBackend
from .textstream_common import TextstreamCommon

_URL_SCHEMES = ("http", "https", "ftp", "file")


[docs] class TextstreamURL(TextstreamCommon, TextstreamBackend): r""" Backend for streaming text fetched from a URL string. A bare string is interpreted as a URL when its scheme is one of http, https, ftp, or file, or when an explicit kind="url" hint is given. Network access from an implicit bare network URL requires explicit consent before opening; URL views and ``kind="url"`` provide that consent. Content is transparently decompressed before text decoding. :param url: URL to fetch lazily when text is first read. :param \**hints: Backend-selection, consent, encoding, timeout, and compression hints. :raises ValueError: If the compression hint is unknown. """ _url: str _timeout: float | None _needs_consent: bool _encoding: str | None _compression: str _f: io.TextIOBase | None _underlying: io.IOBase | None _closed: bool @classmethod def _backend_adopt(cls, obj: Any, **hints: Any) -> Self | None: r"""Adopt a URL string when its scheme and hints match this backend. :param obj: The object to adopt. :param \**hints: Backend-selection and consent hints. :return: An initialized backend, or ``None`` when ``obj`` is not accepted. """ if not isinstance(obj, str): return None kind = hints.get("kind") if kind == "url": if not urllib.parse.urlsplit(obj).scheme: return None return cls(obj, **hints) if kind is None and urllib.parse.urlsplit(obj).scheme in _URL_SCHEMES: return cls(obj, **hints) return None def __init__(self, url: str, **hints: Any) -> None: self._url = url self._timeout = hints.get("timeout") self._needs_consent = hints.get("kind") != "url" and urllib.parse.urlsplit(url).scheme in NETWORK_SCHEMES self._encoding = hints.get("encoding") self._compression = hints.get("compression", "auto") validate_compression(self._compression) self._f = None self._underlying = None self._closed = False def _ensure_f(self) -> io.TextIOBase: if self._closed: raise ValueError("I/O operation on closed stream") if self._f is None: if self._needs_consent: require_network_consent(self._url) resp = urllib.request.urlopen(self._url, timeout=resolve_timeout(self._timeout)) encoding = self._encoding or resp.headers.get_content_charset() or "utf-8" raw = cast(io.IOBase, resp) name = urllib.parse.urlsplit(self._url).path decompressed = open_compressed(raw, compression=self._compression, name=name) self._underlying = raw if decompressed is not raw else None self._f = io.TextIOWrapper(cast(io.BufferedReader, decompressed), encoding=encoding) return self._f @property
[docs] def name(self) -> str | None: """Report that a URL backend has no filename.""" return None
@property
[docs] def url(self) -> str: """Return the source URL.""" return self._url
@property
[docs] def closed(self) -> bool: """Report whether the URL backend is closed.""" return self._closed