CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
http_sync.py938 linesDownload Raw Back to implementations
1"""This file is largely copied from http.py"""2 3import io4import logging5import re6import urllib.error7import urllib.parse8from copy import copy9from json import dumps, loads10from urllib.parse import urlparse11 12try:13    import yarl14except (ImportError, ModuleNotFoundError, OSError):15    yarl = False16 17from fsspec.callbacks import _DEFAULT_CALLBACK18from fsspec.registry import register_implementation19from fsspec.spec import AbstractBufferedFile, AbstractFileSystem20from fsspec.utils import DEFAULT_BLOCK_SIZE, isfilelike, nullcontext, tokenize21 22from ..caching import AllBytes23 24# https://stackoverflow.com/a/15926317/382115425ex = re.compile(r"""<(a|A)\s+(?:[^>]*?\s+)?(href|HREF)=["'](?P<url>[^"']+)""")26ex2 = re.compile(r"""(?P<url>http[s]?://[-a-zA-Z0-9@:%_+.~#?&/=]+)""")27logger = logging.getLogger("fsspec.http")28 29 30class JsHttpException(urllib.error.HTTPError): ...31 32 33class StreamIO(io.BytesIO):34    # fake class, so you can set attributes on it35    # will eventually actually stream36    ...37 38 39class ResponseProxy:40    """Looks like a requests response"""41 42    def __init__(self, req, stream=False):43        self.request = req44        self.stream = stream45        self._data = None46        self._headers = None47 48    @property49    def raw(self):50        if self._data is None:51            b = self.request.response.to_bytes()52            if self.stream:53                self._data = StreamIO(b)54            else:55                self._data = b56        return self._data57 58    def close(self):59        if hasattr(self, "_data"):60            del self._data61 62    @property63    def headers(self):64        if self._headers is None:65            self._headers = dict(66                [67                    _.split(": ")68                    for _ in self.request.getAllResponseHeaders().strip().split("\r\n")69                ]70            )71        return self._headers72 73    @property74    def status_code(self):75        return int(self.request.status)76 77    def raise_for_status(self):78        if not self.ok:79            raise JsHttpException(80                self.url, self.status_code, self.reason, self.headers, None81            )82 83    def iter_content(self, chunksize, *_, **__):84        while True:85            out = self.raw.read(chunksize)86            if out:87                yield out88            else:89                break90 91    @property92    def reason(self):93        return self.request.statusText94 95    @property96    def ok(self):97        return self.status_code < 40098 99    @property100    def url(self):101        return self.request.response.responseURL102 103    @property104    def text(self):105        # TODO: encoding from headers106        return self.content.decode()107 108    @property109    def content(self):110        self.stream = False111        return self.raw112 113    def json(self):114        return loads(self.text)115 116 117class RequestsSessionShim:118    def __init__(self):119        self.headers = {}120 121    def request(122        self,123        method,124        url,125        params=None,126        data=None,127        headers=None,128        cookies=None,129        files=None,130        auth=None,131        timeout=None,132        allow_redirects=None,133        proxies=None,134        hooks=None,135        stream=None,136        verify=None,137        cert=None,138        json=None,139    ):140        from js import Blob, XMLHttpRequest141 142        logger.debug("JS request: %s %s", method, url)143 144        if cert or verify or proxies or files or cookies or hooks:145            raise NotImplementedError146        if data and json:147            raise ValueError("Use json= or data=, not both")148        req = XMLHttpRequest.new()149        extra = auth if auth else ()150        if params:151            url = f"{url}?{urllib.parse.urlencode(params)}"152        req.open(method, url, False, *extra)153        if timeout:154            req.timeout = timeout155        if headers:156            for k, v in headers.items():157                req.setRequestHeader(k, v)158 159        req.setRequestHeader("Accept", "application/octet-stream")160        req.responseType = "arraybuffer"161        if json:162            blob = Blob.new([dumps(data)], {type: "application/json"})163            req.send(blob)164        elif data:165            if isinstance(data, io.IOBase):166                data = data.read()167            blob = Blob.new([data], {type: "application/octet-stream"})168            req.send(blob)169        else:170            req.send(None)171        return ResponseProxy(req, stream=stream)172 173    def get(self, url, **kwargs):174        return self.request("GET", url, **kwargs)175 176    def head(self, url, **kwargs):177        return self.request("HEAD", url, **kwargs)178 179    def post(self, url, **kwargs):180        return self.request("POST}", url, **kwargs)181 182    def put(self, url, **kwargs):183        return self.request("PUT", url, **kwargs)184 185    def patch(self, url, **kwargs):186        return self.request("PATCH", url, **kwargs)187 188    def delete(self, url, **kwargs):189        return self.request("DELETE", url, **kwargs)190 191 192class HTTPFileSystem(AbstractFileSystem):193    """194    Simple File-System for fetching data via HTTP(S)195 196    This is the BLOCKING version of the normal HTTPFileSystem. It uses197    requests in normal python and the JS runtime in pyodide.198 199    ***This implementation is extremely experimental, do not use unless200    you are testing pyodide/pyscript integration***201    """202 203    protocol = ("http", "https", "sync-http", "sync-https")204    sep = "/"205 206    def __init__(207        self,208        simple_links=True,209        block_size=None,210        same_scheme=True,211        cache_type="readahead",212        cache_options=None,213        client_kwargs=None,214        encoded=False,215        **storage_options,216    ):217        """218 219        Parameters220        ----------221        block_size: int222            Blocks to read bytes; if 0, will default to raw requests file-like223            objects instead of HTTPFile instances224        simple_links: bool225            If True, will consider both HTML <a> tags and anything that looks226            like a URL; if False, will consider only the former.227        same_scheme: True228            When doing ls/glob, if this is True, only consider paths that have229            http/https matching the input URLs.230        size_policy: this argument is deprecated231        client_kwargs: dict232            Passed to aiohttp.ClientSession, see233            https://docs.aiohttp.org/en/stable/client_reference.html234            For example, ``{'auth': aiohttp.BasicAuth('user', 'pass')}``235        storage_options: key-value236            Any other parameters passed on to requests237        cache_type, cache_options: defaults used in open238        """239        super().__init__(self, **storage_options)240        self.block_size = block_size if block_size is not None else DEFAULT_BLOCK_SIZE241        self.simple_links = simple_links242        self.same_schema = same_scheme243        self.cache_type = cache_type244        self.cache_options = cache_options245        self.client_kwargs = client_kwargs or {}246        self.encoded = encoded247        self.kwargs = storage_options248 249        try:250            import js  # noqa: F401251 252            logger.debug("Starting JS session")253            self.session = RequestsSessionShim()254            self.js = True255        except Exception as e:256            import requests257 258            logger.debug("Starting cpython session because of: %s", e)259            self.session = requests.Session(**(client_kwargs or {}))260            self.js = False261 262        request_options = copy(storage_options)263        self.use_listings_cache = request_options.pop("use_listings_cache", False)264        request_options.pop("listings_expiry_time", None)265        request_options.pop("max_paths", None)266        request_options.pop("skip_instance_cache", None)267        self.kwargs = request_options268 269    @property270    def fsid(self):271        return "sync-http"272 273    def encode_url(self, url):274        if yarl:275            return yarl.URL(url, encoded=self.encoded)276        return url277 278    @classmethod279    def _strip_protocol(cls, path: str) -> str:280        """For HTTP, we always want to keep the full URL"""281        path = path.replace("sync-http://", "http://").replace(282            "sync-https://", "https://"283        )284        return path285 286    @classmethod287    def _parent(cls, path):288        # override, since _strip_protocol is different for URLs289        par = super()._parent(path)290        if len(par) > 7:  # "http://..."291            return par292        return ""293 294    def _ls_real(self, url, detail=True, **kwargs):295        # ignoring URL-encoded arguments296        kw = self.kwargs.copy()297        kw.update(kwargs)298        logger.debug(url)299        r = self.session.get(self.encode_url(url), **self.kwargs)300        self._raise_not_found_for_status(r, url)301        text = r.text302        if self.simple_links:303            links = ex2.findall(text) + [u[2] for u in ex.findall(text)]304        else:305            links = [u[2] for u in ex.findall(text)]306        out = set()307        parts = urlparse(url)308        for l in links:309            if isinstance(l, tuple):310                l = l[1]311            if l.startswith("/") and len(l) > 1:312                # absolute URL on this server313                l = parts.scheme + "://" + parts.netloc + l314            if l.startswith("http"):315                if self.same_schema and l.startswith(url.rstrip("/") + "/"):316                    out.add(l)317                elif l.replace("https", "http").startswith(318                    url.replace("https", "http").rstrip("/") + "/"319                ):320                    # allowed to cross http <-> https321                    out.add(l)322            else:323                if l not in ["..", "../"]:324                    # Ignore FTP-like "parent"325                    out.add("/".join([url.rstrip("/"), l.lstrip("/")]))326        if not out and url.endswith("/"):327            out = self._ls_real(url.rstrip("/"), detail=False)328        if detail:329            return [330                {331                    "name": u,332                    "size": None,333                    "type": "directory" if u.endswith("/") else "file",334                }335                for u in out336            ]337        else:338            return sorted(out)339 340    def ls(self, url, detail=True, **kwargs):341        if self.use_listings_cache and url in self.dircache:342            out = self.dircache[url]343        else:344            out = self._ls_real(url, detail=detail, **kwargs)345            self.dircache[url] = out346        return out347 348    def _raise_not_found_for_status(self, response, url):349        """350        Raises FileNotFoundError for 404s, otherwise uses raise_for_status.351        """352        if response.status_code == 404:353            raise FileNotFoundError(url)354        response.raise_for_status()355 356    def cat_file(self, url, start=None, end=None, **kwargs):357        kw = self.kwargs.copy()358        kw.update(kwargs)359        logger.debug(url)360 361        if start is not None or end is not None:362            if start == end:363                return b""364            headers = kw.pop("headers", {}).copy()365 366            headers["Range"] = self._process_limits(url, start, end)367            kw["headers"] = headers368        r = self.session.get(self.encode_url(url), **kw)369        self._raise_not_found_for_status(r, url)370        return r.content371 372    def get_file(373        self, rpath, lpath, chunk_size=5 * 2**20, callback=_DEFAULT_CALLBACK, **kwargs374    ):375        kw = self.kwargs.copy()376        kw.update(kwargs)377        logger.debug(rpath)378        r = self.session.get(self.encode_url(rpath), **kw)379        try:380            size = int(381                r.headers.get("content-length", None)382                or r.headers.get("Content-Length", None)383            )384        except (ValueError, KeyError, TypeError):385            size = None386 387        callback.set_size(size)388        self._raise_not_found_for_status(r, rpath)389        if not isfilelike(lpath):390            lpath = open(lpath, "wb")391        for chunk in r.iter_content(chunk_size, decode_unicode=False):392            lpath.write(chunk)393            callback.relative_update(len(chunk))394 395    def put_file(396        self,397        lpath,398        rpath,399        chunk_size=5 * 2**20,400        callback=_DEFAULT_CALLBACK,401        method="post",402        **kwargs,403    ):404        def gen_chunks():405            # Support passing arbitrary file-like objects406            # and use them instead of streams.407            if isinstance(lpath, io.IOBase):408                context = nullcontext(lpath)409                use_seek = False  # might not support seeking410            else:411                context = open(lpath, "rb")412                use_seek = True413 414            with context as f:415                if use_seek:416                    callback.set_size(f.seek(0, 2))417                    f.seek(0)418                else:419                    callback.set_size(getattr(f, "size", None))420 421                chunk = f.read(chunk_size)422                while chunk:423                    yield chunk424                    callback.relative_update(len(chunk))425                    chunk = f.read(chunk_size)426 427        kw = self.kwargs.copy()428        kw.update(kwargs)429 430        method = method.lower()431        if method not in ("post", "put"):432            raise ValueError(433                f"method has to be either 'post' or 'put', not: {method!r}"434            )435 436        meth = getattr(self.session, method)437        resp = meth(rpath, data=gen_chunks(), **kw)438        self._raise_not_found_for_status(resp, rpath)439 440    def _process_limits(self, url, start, end):441        """Helper for "Range"-based _cat_file"""442        size = None443        suff = False444        if start is not None and start < 0:445            # if start is negative and end None, end is the "suffix length"446            if end is None:447                end = -start448                start = ""449                suff = True450            else:451                size = size or self.info(url)["size"]452                start = size + start453        elif start is None:454            start = 0455        if not suff:456            if end is not None and end < 0:457                if start is not None:458                    size = size or self.info(url)["size"]459                    end = size + end460            elif end is None:461                end = ""462            if isinstance(end, int):463                end -= 1  # bytes range is inclusive464        return f"bytes={start}-{end}"465 466    def exists(self, path, strict=False, **kwargs):467        kw = self.kwargs.copy()468        kw.update(kwargs)469        try:470            logger.debug(path)471            r = self.session.get(self.encode_url(path), **kw)472            if strict:473                self._raise_not_found_for_status(r, path)474            return r.status_code < 400475        except FileNotFoundError:476            return False477        except Exception:478            if strict:479                raise480            return False481 482    def isfile(self, path, **kwargs):483        return self.exists(path, **kwargs)484 485    def _open(486        self,487        path,488        mode="rb",489        block_size=None,490        autocommit=None,  # XXX: This differs from the base class.491        cache_type=None,492        cache_options=None,493        size=None,494        **kwargs,495    ):496        """Make a file-like object497 498        Parameters499        ----------500        path: str501            Full URL with protocol502        mode: string503            must be "rb"504        block_size: int or None505            Bytes to download in one request; use instance value if None. If506            zero, will return a streaming Requests file-like instance.507        kwargs: key-value508            Any other parameters, passed to requests calls509        """510        if mode != "rb":511            raise NotImplementedError512        block_size = block_size if block_size is not None else self.block_size513        kw = self.kwargs.copy()514        kw.update(kwargs)515        size = size or self.info(path, **kwargs)["size"]516        if block_size and size:517            return HTTPFile(518                self,519                path,520                session=self.session,521                block_size=block_size,522                mode=mode,523                size=size,524                cache_type=cache_type or self.cache_type,525                cache_options=cache_options or self.cache_options,526                **kw,527            )528        else:529            return HTTPStreamFile(530                self,531                path,532                mode=mode,533                session=self.session,534                **kw,535            )536 537    def ukey(self, url):538        """Unique identifier; assume HTTP files are static, unchanging"""539        return tokenize(url, self.kwargs, self.protocol)540 541    def info(self, url, **kwargs):542        """Get info of URL543 544        Tries to access location via HEAD, and then GET methods, but does545        not fetch the data.546 547        It is possible that the server does not supply any size information, in548        which case size will be given as None (and certain operations on the549        corresponding file will not work).550        """551        info = {}552        for policy in ["head", "get"]:553            try:554                info.update(555                    _file_info(556                        self.encode_url(url),557                        size_policy=policy,558                        session=self.session,559                        **self.kwargs,560                        **kwargs,561                    )562                )563                if info.get("size") is not None:564                    break565            except Exception as exc:566                if policy == "get":567                    # If get failed, then raise a FileNotFoundError568                    raise FileNotFoundError(url) from exc569                logger.debug(str(exc))570 571        return {"name": url, "size": None, **info, "type": "file"}572 573    def glob(self, path, maxdepth=None, **kwargs):574        """575        Find files by glob-matching.576 577        This implementation is idntical to the one in AbstractFileSystem,578        but "?" is not considered as a character for globbing, because it is579        so common in URLs, often identifying the "query" part.580        """581        import re582 583        ends = path.endswith("/")584        path = self._strip_protocol(path)585        indstar = path.find("*") if path.find("*") >= 0 else len(path)586        indbrace = path.find("[") if path.find("[") >= 0 else len(path)587 588        ind = min(indstar, indbrace)589 590        detail = kwargs.pop("detail", False)591 592        if not has_magic(path):593            root = path594            depth = 1595            if ends:596                path += "/*"597            elif self.exists(path):598                if not detail:599                    return [path]600                else:601                    return {path: self.info(path)}602            else:603                if not detail:604                    return []  # glob of non-existent returns empty605                else:606                    return {}607        elif "/" in path[:ind]:608            ind2 = path[:ind].rindex("/")609            root = path[: ind2 + 1]610            depth = None if "**" in path else path[ind2 + 1 :].count("/") + 1611        else:612            root = ""613            depth = None if "**" in path else path[ind + 1 :].count("/") + 1614 615        allpaths = self.find(616            root, maxdepth=maxdepth or depth, withdirs=True, detail=True, **kwargs617        )618        # Escape characters special to python regex, leaving our supported619        # special characters in place.620        # See https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html621        # for shell globbing details.622        pattern = (623            "^"624            + (625                path.replace("\\", r"\\")626                .replace(".", r"\.")627                .replace("+", r"\+")628                .replace("//", "/")629                .replace("(", r"\(")630                .replace(")", r"\)")631                .replace("|", r"\|")632                .replace("^", r"\^")633                .replace("$", r"\$")634                .replace("{", r"\{")635                .replace("}", r"\}")636                .rstrip("/")637            )638            + "$"639        )640        pattern = re.sub("[*]{2}", "=PLACEHOLDER=", pattern)641        pattern = re.sub("[*]", "[^/]*", pattern)642        pattern = re.compile(pattern.replace("=PLACEHOLDER=", ".*"))643        out = {644            p: allpaths[p]645            for p in sorted(allpaths)646            if pattern.match(p.replace("//", "/").rstrip("/"))647        }648        if detail:649            return out650        else:651            return list(out)652 653    def isdir(self, path):654        # override, since all URLs are (also) files655        try:656            return bool(self.ls(path))657        except (FileNotFoundError, ValueError):658            return False659 660 661class HTTPFile(AbstractBufferedFile):662    """663    A file-like object pointing to a remove HTTP(S) resource664 665    Supports only reading, with read-ahead of a predermined block-size.666 667    In the case that the server does not supply the filesize, only reading of668    the complete file in one go is supported.669 670    Parameters671    ----------672    url: str673        Full URL of the remote resource, including the protocol674    session: requests.Session or None675        All calls will be made within this session, to avoid restarting676        connections where the server allows this677    block_size: int or None678        The amount of read-ahead to do, in bytes. Default is 5MB, or the value679        configured for the FileSystem creating this file680    size: None or int681        If given, this is the size of the file in bytes, and we don't attempt682        to call the server to find the value.683    kwargs: all other key-values are passed to requests calls.684    """685 686    def __init__(687        self,688        fs,689        url,690        session=None,691        block_size=None,692        mode="rb",693        cache_type="bytes",694        cache_options=None,695        size=None,696        **kwargs,697    ):698        if mode != "rb":699            raise NotImplementedError("File mode not supported")700        self.url = url701        self.session = session702        self.details = {"name": url, "size": size, "type": "file"}703        super().__init__(704            fs=fs,705            path=url,706            mode=mode,707            block_size=block_size,708            cache_type=cache_type,709            cache_options=cache_options,710            **kwargs,711        )712 713    def read(self, length=-1):714        """Read bytes from file715 716        Parameters717        ----------718        length: int719            Read up to this many bytes. If negative, read all content to end of720            file. If the server has not supplied the filesize, attempting to721            read only part of the data will raise a ValueError.722        """723        if (724            (length < 0 and self.loc == 0)  # explicit read all725            # but not when the size is known and fits into a block anyways726            and not (self.size is not None and self.size <= self.blocksize)727        ):728            self._fetch_all()729        if self.size is None:730            if length < 0:731                self._fetch_all()732        else:733            length = min(self.size - self.loc, length)734        return super().read(length)735 736    def _fetch_all(self):737        """Read whole file in one shot, without caching738 739        This is only called when position is still at zero,740        and read() is called without a byte-count.741        """742        logger.debug(f"Fetch all for {self}")743        if not isinstance(self.cache, AllBytes):744            r = self.session.get(self.fs.encode_url(self.url), **self.kwargs)745            r.raise_for_status()746            out = r.content747            self.cache = AllBytes(size=len(out), fetcher=None, blocksize=None, data=out)748            self.size = len(out)749 750    def _parse_content_range(self, headers):751        """Parse the Content-Range header"""752        s = headers.get("Content-Range", "")753        m = re.match(r"bytes (\d+-\d+|\*)/(\d+|\*)", s)754        if not m:755            return None, None, None756 757        if m[1] == "*":758            start = end = None759        else:760            start, end = [int(x) for x in m[1].split("-")]761        total = None if m[2] == "*" else int(m[2])762        return start, end, total763 764    def _fetch_range(self, start, end):765        """Download a block of data766 767        The expectation is that the server returns only the requested bytes,768        with HTTP code 206. If this is not the case, we first check the headers,769        and then stream the output - if the data size is bigger than we770        requested, an exception is raised.771        """772        logger.debug(f"Fetch range for {self}: {start}-{end}")773        kwargs = self.kwargs.copy()774        headers = kwargs.pop("headers", {}).copy()775        headers["Range"] = f"bytes={start}-{end - 1}"776        logger.debug("%s : %s", self.url, headers["Range"])777        r = self.session.get(self.fs.encode_url(self.url), headers=headers, **kwargs)778        if r.status_code == 416:779            # range request outside file780            return b""781        r.raise_for_status()782 783        # If the server has handled the range request, it should reply784        # with status 206 (partial content). But we'll guess that a suitable785        # Content-Range header or a Content-Length no more than the786        # requested range also mean we have got the desired range.787        cl = r.headers.get("Content-Length", r.headers.get("content-length", end + 1))788        response_is_range = (789            r.status_code == 206790            or self._parse_content_range(r.headers)[0] == start791            or int(cl) <= end - start792        )793 794        if response_is_range:795            # partial content, as expected796            out = r.content797        elif start > 0:798            raise ValueError(799                "The HTTP server doesn't appear to support range requests. "800                "Only reading this file from the beginning is supported. "801                "Open with block_size=0 for a streaming file interface."802            )803        else:804            # Response is not a range, but we want the start of the file,805            # so we can read the required amount anyway.806            cl = 0807            out = []808            for chunk in r.iter_content(2**20, False):809                out.append(chunk)810                cl += len(chunk)811            out = b"".join(out)[: end - start]812        return out813 814 815magic_check = re.compile("([*[])")816 817 818def has_magic(s):819    match = magic_check.search(s)820    return match is not None821 822 823class HTTPStreamFile(AbstractBufferedFile):824    def __init__(self, fs, url, mode="rb", session=None, **kwargs):825        self.url = url826        self.session = session827        if mode != "rb":828            raise ValueError829        self.details = {"name": url, "size": None}830        super().__init__(fs=fs, path=url, mode=mode, cache_type="readahead", **kwargs)831 832        r = self.session.get(self.fs.encode_url(url), stream=True, **kwargs)833        self.fs._raise_not_found_for_status(r, url)834        self.it = r.iter_content(1024, False)835        self.leftover = b""836 837        self.r = r838 839    def seek(self, *args, **kwargs):840        raise ValueError("Cannot seek streaming HTTP file")841 842    def read(self, num=-1):843        bufs = [self.leftover]844        leng = len(self.leftover)845        while leng < num or num < 0:846            try:847                out = self.it.__next__()848            except StopIteration:849                break850            if out:851                bufs.append(out)852            else:853                break854            leng += len(out)855        out = b"".join(bufs)856        if num >= 0:857            self.leftover = out[num:]858            out = out[:num]859        else:860            self.leftover = b""861        self.loc += len(out)862        return out863 864    def close(self):865        self.r.close()866        self.closed = True867 868 869def get_range(session, url, start, end, **kwargs):870    # explicit get a range when we know it must be safe871    kwargs = kwargs.copy()872    headers = kwargs.pop("headers", {}).copy()873    headers["Range"] = f"bytes={start}-{end - 1}"874    r = session.get(url, headers=headers, **kwargs)875    r.raise_for_status()876    return r.content877 878 879def _file_info(url, session, size_policy="head", **kwargs):880    """Call HEAD on the server to get details about the file (size/checksum etc.)881 882    Default operation is to explicitly allow redirects and use encoding883    'identity' (no compression) to get the true size of the target.884    """885    logger.debug("Retrieve file size for %s", url)886    kwargs = kwargs.copy()887    ar = kwargs.pop("allow_redirects", True)888    head = kwargs.get("headers", {}).copy()889    # TODO: not allowed in JS890    # head["Accept-Encoding"] = "identity"891    kwargs["headers"] = head892 893    info = {}894    if size_policy == "head":895        r = session.head(url, allow_redirects=ar, **kwargs)896    elif size_policy == "get":897        r = session.get(url, allow_redirects=ar, **kwargs)898    else:899        raise TypeError(f'size_policy must be "head" or "get", got {size_policy}')900    r.raise_for_status()901 902    # TODO:903    #  recognise lack of 'Accept-Ranges',904    #                 or 'Accept-Ranges': 'none' (not 'bytes')905    #  to mean streaming only, no random access => return None906    if "Content-Length" in r.headers:907        info["size"] = int(r.headers["Content-Length"])908    elif "Content-Range" in r.headers:909        info["size"] = int(r.headers["Content-Range"].split("/")[1])910    elif "content-length" in r.headers:911        info["size"] = int(r.headers["content-length"])912    elif "content-range" in r.headers:913        info["size"] = int(r.headers["content-range"].split("/")[1])914 915    for checksum_field in ["ETag", "Content-MD5", "Digest"]:916        if r.headers.get(checksum_field):917            info[checksum_field] = r.headers[checksum_field]918 919    return info920 921 922# importing this is enough to register it923def register():924    register_implementation("http", HTTPFileSystem, clobber=True)925    register_implementation("https", HTTPFileSystem, clobber=True)926    register_implementation("sync-http", HTTPFileSystem, clobber=True)927    register_implementation("sync-https", HTTPFileSystem, clobber=True)928 929 930register()931 932 933def unregister():934    from fsspec.implementations.http import HTTPFileSystem935 936    register_implementation("http", HTTPFileSystem, clobber=True)937    register_implementation("https", HTTPFileSystem, clobber=True)938 
Aluode/PerceptionLabPortable · CoolFace