CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
dbfs.py497 linesDownload Raw Back to implementations
1from __future__ import annotations2 3import base644import urllib5 6import requests7from requests.adapters import HTTPAdapter, Retry8from typing_extensions import override9 10from fsspec import AbstractFileSystem11from fsspec.spec import AbstractBufferedFile12 13 14class DatabricksException(Exception):15    """16    Helper class for exceptions raised in this module.17    """18 19    def __init__(self, error_code, message, details=None):20        """Create a new DatabricksException"""21        super().__init__(message)22 23        self.error_code = error_code24        self.message = message25        self.details = details26 27 28class DatabricksFileSystem(AbstractFileSystem):29    """30    Get access to the Databricks filesystem implementation over HTTP.31    Can be used inside and outside of a databricks cluster.32    """33 34    def __init__(self, instance, token, **kwargs):35        """36        Create a new DatabricksFileSystem.37 38        Parameters39        ----------40        instance: str41            The instance URL of the databricks cluster.42            For example for an Azure databricks cluster, this43            has the form adb-<some-number>.<two digits>.azuredatabricks.net.44        token: str45            Your personal token. Find out more46            here: https://docs.databricks.com/dev-tools/api/latest/authentication.html47        """48        self.instance = instance49        self.token = token50        self.session = requests.Session()51        self.retries = Retry(52            total=10,53            backoff_factor=0.05,54            status_forcelist=[408, 429, 500, 502, 503, 504],55        )56 57        self.session.mount("https://", HTTPAdapter(max_retries=self.retries))58        self.session.headers.update({"Authorization": f"Bearer {self.token}"})59 60        super().__init__(**kwargs)61 62    @override63    def _ls_from_cache(self, path) -> list[dict[str, str | int]] | None:64        """Check cache for listing65 66        Returns listing, if found (may be empty list for a directory that67        exists but contains nothing), None if not in cache.68        """69        self.dircache.pop(path.rstrip("/"), None)70 71        parent = self._parent(path)72        if parent in self.dircache:73            for entry in self.dircache[parent]:74                if entry["name"] == path.rstrip("/"):75                    if entry["type"] != "directory":76                        return [entry]77                    return []78            raise FileNotFoundError(path)79 80    def ls(self, path, detail=True, **kwargs):81        """82        List the contents of the given path.83 84        Parameters85        ----------86        path: str87            Absolute path88        detail: bool89            Return not only the list of filenames,90            but also additional information on file sizes91            and types.92        """93        try:94            out = self._ls_from_cache(path)95        except FileNotFoundError:96            # This happens if the `path`'s parent was cached, but `path` is not97            # there. This suggests that `path` is new since the parent was98            # cached. Attempt to invalidate parent's cache before continuing.99            self.dircache.pop(self._parent(path), None)100            out = None101 102        if not out:103            try:104                r = self._send_to_api(105                    method="get", endpoint="list", json={"path": path}106                )107            except DatabricksException as e:108                if e.error_code == "RESOURCE_DOES_NOT_EXIST":109                    raise FileNotFoundError(e.message) from e110 111                raise112            files = r.get("files", [])113            out = [114                {115                    "name": o["path"],116                    "type": "directory" if o["is_dir"] else "file",117                    "size": o["file_size"],118                }119                for o in files120            ]121            self.dircache[path] = out122 123        if detail:124            return out125        return [o["name"] for o in out]126 127    def makedirs(self, path, exist_ok=True):128        """129        Create a given absolute path and all of its parents.130 131        Parameters132        ----------133        path: str134            Absolute path to create135        exist_ok: bool136            If false, checks if the folder137            exists before creating it (and raises an138            Exception if this is the case)139        """140        if not exist_ok:141            try:142                # If the following succeeds, the path is already present143                self._send_to_api(144                    method="get", endpoint="get-status", json={"path": path}145                )146                raise FileExistsError(f"Path {path} already exists")147            except DatabricksException as e:148                if e.error_code == "RESOURCE_DOES_NOT_EXIST":149                    pass150 151        try:152            self._send_to_api(method="post", endpoint="mkdirs", json={"path": path})153        except DatabricksException as e:154            if e.error_code == "RESOURCE_ALREADY_EXISTS":155                raise FileExistsError(e.message) from e156 157            raise158        self.invalidate_cache(self._parent(path))159 160    def mkdir(self, path, create_parents=True, **kwargs):161        """162        Create a given absolute path and all of its parents.163 164        Parameters165        ----------166        path: str167            Absolute path to create168        create_parents: bool169            Whether to create all parents or not.170            "False" is not implemented so far.171        """172        if not create_parents:173            raise NotImplementedError174 175        self.mkdirs(path, **kwargs)176 177    def rm(self, path, recursive=False, **kwargs):178        """179        Remove the file or folder at the given absolute path.180 181        Parameters182        ----------183        path: str184            Absolute path what to remove185        recursive: bool186            Recursively delete all files in a folder.187        """188        try:189            self._send_to_api(190                method="post",191                endpoint="delete",192                json={"path": path, "recursive": recursive},193            )194        except DatabricksException as e:195            # This is not really an exception, it just means196            # not everything was deleted so far197            if e.error_code == "PARTIAL_DELETE":198                self.rm(path=path, recursive=recursive)199            elif e.error_code == "IO_ERROR":200                # Using the same exception as the os module would use here201                raise OSError(e.message) from e202 203            raise204        self.invalidate_cache(self._parent(path))205 206    def mv(207        self, source_path, destination_path, recursive=False, maxdepth=None, **kwargs208    ):209        """210        Move a source to a destination path.211 212        A note from the original [databricks API manual]213        (https://docs.databricks.com/dev-tools/api/latest/dbfs.html#move).214 215        When moving a large number of files the API call will time out after216        approximately 60s, potentially resulting in partially moved data.217        Therefore, for operations that move more than 10k files, we strongly218        discourage using the DBFS REST API.219 220        Parameters221        ----------222        source_path: str223            From where to move (absolute path)224        destination_path: str225            To where to move (absolute path)226        recursive: bool227            Not implemented to far.228        maxdepth:229            Not implemented to far.230        """231        if recursive:232            raise NotImplementedError233        if maxdepth:234            raise NotImplementedError235 236        try:237            self._send_to_api(238                method="post",239                endpoint="move",240                json={"source_path": source_path, "destination_path": destination_path},241            )242        except DatabricksException as e:243            if e.error_code == "RESOURCE_DOES_NOT_EXIST":244                raise FileNotFoundError(e.message) from e245            elif e.error_code == "RESOURCE_ALREADY_EXISTS":246                raise FileExistsError(e.message) from e247 248            raise249        self.invalidate_cache(self._parent(source_path))250        self.invalidate_cache(self._parent(destination_path))251 252    def _open(self, path, mode="rb", block_size="default", **kwargs):253        """254        Overwrite the base class method to make sure to create a DBFile.255        All arguments are copied from the base method.256 257        Only the default blocksize is allowed.258        """259        return DatabricksFile(self, path, mode=mode, block_size=block_size, **kwargs)260 261    def _send_to_api(self, method, endpoint, json):262        """263        Send the given json to the DBFS API264        using a get or post request (specified by the argument `method`).265 266        Parameters267        ----------268        method: str269            Which http method to use for communication; "get" or "post".270        endpoint: str271            Where to send the request to (last part of the API URL)272        json: dict273            Dictionary of information to send274        """275        if method == "post":276            session_call = self.session.post277        elif method == "get":278            session_call = self.session.get279        else:280            raise ValueError(f"Do not understand method {method}")281 282        url = urllib.parse.urljoin(f"https://{self.instance}/api/2.0/dbfs/", endpoint)283 284        r = session_call(url, json=json)285 286        # The DBFS API will return a json, also in case of an exception.287        # We want to preserve this information as good as possible.288        try:289            r.raise_for_status()290        except requests.HTTPError as e:291            # try to extract json error message292            # if that fails, fall back to the original exception293            try:294                exception_json = e.response.json()295            except Exception:296                raise e from None297 298            raise DatabricksException(**exception_json) from e299 300        return r.json()301 302    def _create_handle(self, path, overwrite=True):303        """304        Internal function to create a handle, which can be used to305        write blocks of a file to DBFS.306        A handle has a unique identifier which needs to be passed307        whenever written during this transaction.308        The handle is active for 10 minutes - after that a new309        write transaction needs to be created.310        Make sure to close the handle after you are finished.311 312        Parameters313        ----------314        path: str315            Absolute path for this file.316        overwrite: bool317            If a file already exist at this location, either overwrite318            it or raise an exception.319        """320        try:321            r = self._send_to_api(322                method="post",323                endpoint="create",324                json={"path": path, "overwrite": overwrite},325            )326            return r["handle"]327        except DatabricksException as e:328            if e.error_code == "RESOURCE_ALREADY_EXISTS":329                raise FileExistsError(e.message) from e330 331            raise332 333    def _close_handle(self, handle):334        """335        Close a handle, which was opened by :func:`_create_handle`.336 337        Parameters338        ----------339        handle: str340            Which handle to close.341        """342        try:343            self._send_to_api(method="post", endpoint="close", json={"handle": handle})344        except DatabricksException as e:345            if e.error_code == "RESOURCE_DOES_NOT_EXIST":346                raise FileNotFoundError(e.message) from e347 348            raise349 350    def _add_data(self, handle, data):351        """352        Upload data to an already opened file handle353        (opened by :func:`_create_handle`).354        The maximal allowed data size is 1MB after355        conversion to base64.356        Remember to close the handle when you are finished.357 358        Parameters359        ----------360        handle: str361            Which handle to upload data to.362        data: bytes363            Block of data to add to the handle.364        """365        data = base64.b64encode(data).decode()366        try:367            self._send_to_api(368                method="post",369                endpoint="add-block",370                json={"handle": handle, "data": data},371            )372        except DatabricksException as e:373            if e.error_code == "RESOURCE_DOES_NOT_EXIST":374                raise FileNotFoundError(e.message) from e375            elif e.error_code == "MAX_BLOCK_SIZE_EXCEEDED":376                raise ValueError(e.message) from e377 378            raise379 380    def _get_data(self, path, start, end):381        """382        Download data in bytes from a given absolute path in a block383        from [start, start+length].384        The maximum number of allowed bytes to read is 1MB.385 386        Parameters387        ----------388        path: str389            Absolute path to download data from390        start: int391            Start position of the block392        end: int393            End position of the block394        """395        try:396            r = self._send_to_api(397                method="get",398                endpoint="read",399                json={"path": path, "offset": start, "length": end - start},400            )401            return base64.b64decode(r["data"])402        except DatabricksException as e:403            if e.error_code == "RESOURCE_DOES_NOT_EXIST":404                raise FileNotFoundError(e.message) from e405            elif e.error_code in ["INVALID_PARAMETER_VALUE", "MAX_READ_SIZE_EXCEEDED"]:406                raise ValueError(e.message) from e407 408            raise409 410    def invalidate_cache(self, path=None):411        if path is None:412            self.dircache.clear()413        else:414            self.dircache.pop(path, None)415        super().invalidate_cache(path)416 417 418class DatabricksFile(AbstractBufferedFile):419    """420    Helper class for files referenced in the DatabricksFileSystem.421    """422 423    DEFAULT_BLOCK_SIZE = 1 * 2**20  # only allowed block size424 425    def __init__(426        self,427        fs,428        path,429        mode="rb",430        block_size="default",431        autocommit=True,432        cache_type="readahead",433        cache_options=None,434        **kwargs,435    ):436        """437        Create a new instance of the DatabricksFile.438 439        The blocksize needs to be the default one.440        """441        if block_size is None or block_size == "default":442            block_size = self.DEFAULT_BLOCK_SIZE443 444        assert block_size == self.DEFAULT_BLOCK_SIZE, (445            f"Only the default block size is allowed, not {block_size}"446        )447 448        super().__init__(449            fs,450            path,451            mode=mode,452            block_size=block_size,453            autocommit=autocommit,454            cache_type=cache_type,455            cache_options=cache_options or {},456            **kwargs,457        )458 459    def _initiate_upload(self):460        """Internal function to start a file upload"""461        self.handle = self.fs._create_handle(self.path)462 463    def _upload_chunk(self, final=False):464        """Internal function to add a chunk of data to a started upload"""465        self.buffer.seek(0)466        data = self.buffer.getvalue()467 468        data_chunks = [469            data[start:end] for start, end in self._to_sized_blocks(len(data))470        ]471 472        for data_chunk in data_chunks:473            self.fs._add_data(handle=self.handle, data=data_chunk)474 475        if final:476            self.fs._close_handle(handle=self.handle)477            return True478 479    def _fetch_range(self, start, end):480        """Internal function to download a block of data"""481        return_buffer = b""482        length = end - start483        for chunk_start, chunk_end in self._to_sized_blocks(length, start):484            return_buffer += self.fs._get_data(485                path=self.path, start=chunk_start, end=chunk_end486            )487 488        return return_buffer489 490    def _to_sized_blocks(self, length, start=0):491        """Helper function to split a range from 0 to total_length into blocksizes"""492        end = start + length493        for data_chunk in range(start, end, self.blocksize):494            data_start = data_chunk495            data_end = min(end, data_chunk + self.blocksize)496            yield data_start, data_end497 
Aluode/PerceptionLabPortable · CoolFace