Skip to content

typerdrive Files modules

typerdrive.files.attach

Provide a decorator that attaches the typerdrive files to a typer command function.

Attributes

Classes

Functions

attach_files

attach_files(
    show: bool = False,
) -> Callable[
    [ContextFunction[P, T]], ContextFunction[P, T]
]

Attach the typerdrive files to the decorated typer command function.

Parameters:

Name Type Description Default
show bool

If set, show the files directory after the function runs.

False
Source code in src/typerdrive/files/attach.py
def attach_files(show: bool = False) -> Callable[[ContextFunction[P, T]], ContextFunction[P, T]]:
    """
    Attach the `typerdrive` files to the decorated `typer` command function.

    Parameters:
        show: If set, show the files directory after the function runs.
    """

    def _decorate(func: ContextFunction[P, T]) -> ContextFunction[P, T]:
        manager_param_key: str | None = None
        for key in func.__annotations__.keys():
            if func.__annotations__[key] is FilesManager:
                func.__annotations__[key] = Annotated[FilesManager | None, CloakingDevice]
                manager_param_key = key

        @wraps(func)
        def wrapper(ctx: typer.Context, *args: P.args, **kwargs: P.kwargs) -> T:
            manager: FilesManager = FilesManager()
            to_context(ctx, "files_manager", manager)

            if manager_param_key:
                kwargs_dict = cast(dict[str, Any], kwargs)
                kwargs_dict[manager_param_key] = manager

            ret_val = func(ctx, *args, **kwargs)

            if show:
                show_directory(manager.files_dir, subject="Current files")

            return ret_val

        return wrapper

    return _decorate

get_files_manager

get_files_manager(ctx: Context) -> FilesManager

Retrieve the FilesManager from the TyperdriveContext.

Source code in src/typerdrive/files/attach.py
def get_files_manager(ctx: typer.Context) -> FilesManager:
    """
    Retrieve the `FilesManager` from the `TyperdriveContext`.
    """
    with FilesError.handle_errors("Files is not bound to the context. Use the @attach_files() decorator"):
        mgr: Any = from_context(ctx, "files_manager")
    return FilesError.ensure_type(
        mgr,
        FilesManager,
        "Item in user context at `files_manager` was not a FilesManager",
    )

typerdrive.files.commands

Provide commands that can be added to a typer app to interact with files.

Classes

Functions

add_files_subcommand

add_files_subcommand(cli: Typer)

Add all files subcommands to the given app.

Source code in src/typerdrive/files/commands.py
def add_files_subcommand(cli: typer.Typer):
    """
    Add all `files` subcommands to the given app.
    """
    files_cli = typer.Typer(help="Manage files for the app")

    add_show(files_cli)

    cli.add_typer(files_cli, name="files")

add_show

add_show(cli: Typer)

Add the show command to the given typer app.

Source code in src/typerdrive/files/commands.py
def add_show(cli: typer.Typer):
    """
    Add the `show` command to the given `typer` app.
    """
    cli.command()(show)

show

show(ctx: Context)

Show the current files directory.

Source code in src/typerdrive/files/commands.py
@handle_errors("Failed to show files", handle_exc_class=FilesError)
@attach_files(show=True)
def show(ctx: typer.Context):
    """
    Show the current files directory.
    """
    pass

typerdrive.files.exceptions

Provide exceptions specific to the files feature of typerdrive.

Classes

FilesClearError

Bases: FilesError

Indicate that there was a problem deleting a file.

Source code in src/typerdrive/files/exceptions.py
class FilesClearError(FilesError):
    """
    Indicate that there was a problem deleting a file.
    """

FilesError

Bases: TyperdriveError

The base exception for files errors.

Source code in src/typerdrive/files/exceptions.py
class FilesError(TyperdriveError):
    """
    The base exception for files errors.
    """

    exit_code: ExitCode = ExitCode.GENERAL_ERROR
Attributes
exit_code class-attribute instance-attribute
exit_code: ExitCode = GENERAL_ERROR

FilesInitError

Bases: FilesError

Indicate that there was a problem initializing the files storage.

Source code in src/typerdrive/files/exceptions.py
class FilesInitError(FilesError):
    """
    Indicate that there was a problem initializing the files storage.
    """

FilesLoadError

Bases: FilesError

Indicate that there was a problem loading a file.

Source code in src/typerdrive/files/exceptions.py
class FilesLoadError(FilesError):
    """
    Indicate that there was a problem loading a file.
    """

FilesStoreError

Bases: FilesError

Indicate that there was a problem storing a file.

Source code in src/typerdrive/files/exceptions.py
class FilesStoreError(FilesError):
    """
    Indicate that there was a problem storing a file.
    """

typerdrive.files.manager

Provide a class for managing the typerdrive files feature.

Classes

FilesManager

Manage the typerdrive files feature.

This manager provides a simple key-value file storage system where files are stored in a directory structure. Files can be stored as bytes, text, or JSON and retrieved the same way.

Source code in src/typerdrive/files/manager.py
class FilesManager:
    """
    Manage the `typerdrive` files feature.

    This manager provides a simple key-value file storage system where files are stored
    in a directory structure. Files can be stored as bytes, text, or JSON and retrieved
    the same way.
    """

    files_dir: Path
    """ The directory where files are stored. """

    def __init__(self):
        config: TyperdriveConfig = get_typerdrive_config()

        self.files_dir = config.files_dir

        with FilesInitError.handle_errors("Failed to initialize files storage"):
            self.files_dir.mkdir(parents=True, exist_ok=True)

    def resolve_path(self, path: Path | str, mkdir: bool = False) -> Path:
        """
        Resolve a given file key path to an absolute path within the files directory.

        If the resolved path is outside the files directory, an exception will be raised.
        If the resolved path is the same as the files directory, an exception will be raised.

        Parameters:
            path:  The file key
            mkdir: If set, create the directory for the file key if it does not exist.
        """
        if isinstance(path, str):
            path = Path(path)
        full_path = self.files_dir / path
        full_path = full_path.resolve()
        FilesError.require_condition(
            is_child(full_path, self.files_dir),
            f"Resolved file path {str(full_path)} is not within files directory {str(self.files_dir)}",
        )
        FilesError.require_condition(
            full_path != self.files_dir,
            f"Resolved file path {str(full_path)} must not be the same as files directory {str(self.files_dir)}",
        )
        if mkdir:
            full_path.parent.mkdir(parents=True, exist_ok=True)
        return full_path

    def list_items(self, path: Path | str) -> list[str]:
        """
        List files at a given path.

        Items will be all non-directory entries in the given path.

        If the path doesn't exist or is not a directory, an exception will be raised.
        """
        full_path = self.resolve_path(path)
        FilesError.require_condition(
            full_path.exists(),
            f"Resolved file path {str(full_path)} does not exist",
        )
        FilesError.require_condition(
            full_path.is_dir(),
            f"Resolved file path {str(full_path)} is not a directory",
        )
        items: list[str] = []
        for path in full_path.iterdir():
            if path.is_dir():
                continue
            items.append(str(path.name))
        return items

    def store_bytes(self, data: bytes, path: Path | str, mode: int | None = None):
        """
        Store binary data as a file at the given key.

        Parameters:
            data: The binary data to store
            path: The file key where the data should be stored
            mode: The file mode to use when creating the file
        """
        full_path = self.resolve_path(path, mkdir=True)

        logger.debug(f"Storing file at {full_path}")

        with FilesStoreError.handle_errors(f"Failed to store file at {str(path)}"):
            full_path.write_bytes(data)
        if mode:
            with FilesStoreError.handle_errors(f"Failed to set mode for file at {str(path)} to {mode=}"):
                full_path.chmod(mode)

    def store_text(self, text: str, path: Path | str, mode: int | None = None):
        """
        Store text as a file at the given key.

        Parameters:
            text: The text to store
            path: The file key where the text should be stored
            mode: The file mode to use when creating the file
        """
        self.store_bytes(text.encode("utf-8"), path, mode=mode)

    def store_json(self, data: dict[str, Any], path: Path | str, mode: int | None = None):
        """
        Store a dictionary as a JSON file at the given key.

        The dictionary must be json serializable.

        Parameters:
            data: The dict to store as JSON
            path: The file key where the JSON should be stored
            mode: The file mode to use when creating the file
        """
        self.store_bytes(json.dumps(data, indent=2).encode("utf-8"), path, mode=mode)

    def load_bytes(self, path: Path | str) -> bytes:
        """
        Load binary data from a file at the given key.

        If there is no file at the given key, an exception will be raised.
        If there is an error reading the file, an exception will be raised.

        Parameters:
            path: The file key where the data should be loaded from
        """
        full_path = self.resolve_path(path, mkdir=False)

        logger.debug(f"Loading file from {full_path}")

        FilesLoadError.require_condition(full_path.exists(), f"File at {str(path)} does not exist")
        with FilesLoadError.handle_errors(f"Failed to load file from {str(path)}"):
            return full_path.read_bytes()

    def load_text(self, path: Path | str) -> str:
        """
        Load text from a file at the given key.

        Parameters:
            path: The file key where the text should be loaded from
        """
        return self.load_bytes(path).decode("utf-8")

    def load_json(self, path: Path | str) -> dict[str, Any]:
        """
        Load a dictionary from a JSON file at the given key.

        The file at the given key must be valid JSON.

        Parameters:
            path: The file key where the JSON should be loaded from
        """
        text = self.load_bytes(path).decode("utf-8")
        with FilesLoadError.handle_errors(f"Failed to parse JSON from file at {str(path)}"):
            return json.loads(text)

    def delete(self, path: Path | str) -> Path:
        """
        Delete a file at the given key.

        Parameters:
            path: The file key where the file should be deleted
        """
        full_path = self.resolve_path(path)

        logger.debug(f"Deleting file at {full_path}")

        with FilesClearError.handle_errors(f"Failed to delete file at {str(path)}"):
            full_path.unlink()

        # Clean up empty parent directories
        if len([p for p in full_path.parent.iterdir()]) == 0:
            with FilesClearError.handle_errors(f"Failed to remove empty directory {str(full_path.parent)}"):
                full_path.parent.rmdir()
        return full_path
Attributes
files_dir instance-attribute
files_dir: Path = files_dir

The directory where files are stored.

Functions
__init__
__init__()
Source code in src/typerdrive/files/manager.py
def __init__(self):
    config: TyperdriveConfig = get_typerdrive_config()

    self.files_dir = config.files_dir

    with FilesInitError.handle_errors("Failed to initialize files storage"):
        self.files_dir.mkdir(parents=True, exist_ok=True)
delete
delete(path: Path | str) -> Path

Delete a file at the given key.

Parameters:

Name Type Description Default
path Path | str

The file key where the file should be deleted

required
Source code in src/typerdrive/files/manager.py
def delete(self, path: Path | str) -> Path:
    """
    Delete a file at the given key.

    Parameters:
        path: The file key where the file should be deleted
    """
    full_path = self.resolve_path(path)

    logger.debug(f"Deleting file at {full_path}")

    with FilesClearError.handle_errors(f"Failed to delete file at {str(path)}"):
        full_path.unlink()

    # Clean up empty parent directories
    if len([p for p in full_path.parent.iterdir()]) == 0:
        with FilesClearError.handle_errors(f"Failed to remove empty directory {str(full_path.parent)}"):
            full_path.parent.rmdir()
    return full_path
list_items
list_items(path: Path | str) -> list[str]

List files at a given path.

Items will be all non-directory entries in the given path.

If the path doesn't exist or is not a directory, an exception will be raised.

Source code in src/typerdrive/files/manager.py
def list_items(self, path: Path | str) -> list[str]:
    """
    List files at a given path.

    Items will be all non-directory entries in the given path.

    If the path doesn't exist or is not a directory, an exception will be raised.
    """
    full_path = self.resolve_path(path)
    FilesError.require_condition(
        full_path.exists(),
        f"Resolved file path {str(full_path)} does not exist",
    )
    FilesError.require_condition(
        full_path.is_dir(),
        f"Resolved file path {str(full_path)} is not a directory",
    )
    items: list[str] = []
    for path in full_path.iterdir():
        if path.is_dir():
            continue
        items.append(str(path.name))
    return items
load_bytes
load_bytes(path: Path | str) -> bytes

Load binary data from a file at the given key.

If there is no file at the given key, an exception will be raised. If there is an error reading the file, an exception will be raised.

Parameters:

Name Type Description Default
path Path | str

The file key where the data should be loaded from

required
Source code in src/typerdrive/files/manager.py
def load_bytes(self, path: Path | str) -> bytes:
    """
    Load binary data from a file at the given key.

    If there is no file at the given key, an exception will be raised.
    If there is an error reading the file, an exception will be raised.

    Parameters:
        path: The file key where the data should be loaded from
    """
    full_path = self.resolve_path(path, mkdir=False)

    logger.debug(f"Loading file from {full_path}")

    FilesLoadError.require_condition(full_path.exists(), f"File at {str(path)} does not exist")
    with FilesLoadError.handle_errors(f"Failed to load file from {str(path)}"):
        return full_path.read_bytes()
load_json
load_json(path: Path | str) -> dict[str, Any]

Load a dictionary from a JSON file at the given key.

The file at the given key must be valid JSON.

Parameters:

Name Type Description Default
path Path | str

The file key where the JSON should be loaded from

required
Source code in src/typerdrive/files/manager.py
def load_json(self, path: Path | str) -> dict[str, Any]:
    """
    Load a dictionary from a JSON file at the given key.

    The file at the given key must be valid JSON.

    Parameters:
        path: The file key where the JSON should be loaded from
    """
    text = self.load_bytes(path).decode("utf-8")
    with FilesLoadError.handle_errors(f"Failed to parse JSON from file at {str(path)}"):
        return json.loads(text)
load_text
load_text(path: Path | str) -> str

Load text from a file at the given key.

Parameters:

Name Type Description Default
path Path | str

The file key where the text should be loaded from

required
Source code in src/typerdrive/files/manager.py
def load_text(self, path: Path | str) -> str:
    """
    Load text from a file at the given key.

    Parameters:
        path: The file key where the text should be loaded from
    """
    return self.load_bytes(path).decode("utf-8")
resolve_path
resolve_path(path: Path | str, mkdir: bool = False) -> Path

Resolve a given file key path to an absolute path within the files directory.

If the resolved path is outside the files directory, an exception will be raised. If the resolved path is the same as the files directory, an exception will be raised.

Parameters:

Name Type Description Default
path Path | str

The file key

required
mkdir bool

If set, create the directory for the file key if it does not exist.

False
Source code in src/typerdrive/files/manager.py
def resolve_path(self, path: Path | str, mkdir: bool = False) -> Path:
    """
    Resolve a given file key path to an absolute path within the files directory.

    If the resolved path is outside the files directory, an exception will be raised.
    If the resolved path is the same as the files directory, an exception will be raised.

    Parameters:
        path:  The file key
        mkdir: If set, create the directory for the file key if it does not exist.
    """
    if isinstance(path, str):
        path = Path(path)
    full_path = self.files_dir / path
    full_path = full_path.resolve()
    FilesError.require_condition(
        is_child(full_path, self.files_dir),
        f"Resolved file path {str(full_path)} is not within files directory {str(self.files_dir)}",
    )
    FilesError.require_condition(
        full_path != self.files_dir,
        f"Resolved file path {str(full_path)} must not be the same as files directory {str(self.files_dir)}",
    )
    if mkdir:
        full_path.parent.mkdir(parents=True, exist_ok=True)
    return full_path
store_bytes
store_bytes(
    data: bytes, path: Path | str, mode: int | None = None
)

Store binary data as a file at the given key.

Parameters:

Name Type Description Default
data bytes

The binary data to store

required
path Path | str

The file key where the data should be stored

required
mode int | None

The file mode to use when creating the file

None
Source code in src/typerdrive/files/manager.py
def store_bytes(self, data: bytes, path: Path | str, mode: int | None = None):
    """
    Store binary data as a file at the given key.

    Parameters:
        data: The binary data to store
        path: The file key where the data should be stored
        mode: The file mode to use when creating the file
    """
    full_path = self.resolve_path(path, mkdir=True)

    logger.debug(f"Storing file at {full_path}")

    with FilesStoreError.handle_errors(f"Failed to store file at {str(path)}"):
        full_path.write_bytes(data)
    if mode:
        with FilesStoreError.handle_errors(f"Failed to set mode for file at {str(path)} to {mode=}"):
            full_path.chmod(mode)
store_json
store_json(
    data: dict[str, Any],
    path: Path | str,
    mode: int | None = None,
)

Store a dictionary as a JSON file at the given key.

The dictionary must be json serializable.

Parameters:

Name Type Description Default
data dict[str, Any]

The dict to store as JSON

required
path Path | str

The file key where the JSON should be stored

required
mode int | None

The file mode to use when creating the file

None
Source code in src/typerdrive/files/manager.py
def store_json(self, data: dict[str, Any], path: Path | str, mode: int | None = None):
    """
    Store a dictionary as a JSON file at the given key.

    The dictionary must be json serializable.

    Parameters:
        data: The dict to store as JSON
        path: The file key where the JSON should be stored
        mode: The file mode to use when creating the file
    """
    self.store_bytes(json.dumps(data, indent=2).encode("utf-8"), path, mode=mode)
store_text
store_text(
    text: str, path: Path | str, mode: int | None = None
)

Store text as a file at the given key.

Parameters:

Name Type Description Default
text str

The text to store

required
path Path | str

The file key where the text should be stored

required
mode int | None

The file mode to use when creating the file

None
Source code in src/typerdrive/files/manager.py
def store_text(self, text: str, path: Path | str, mode: int | None = None):
    """
    Store text as a file at the given key.

    Parameters:
        text: The text to store
        path: The file key where the text should be stored
        mode: The file mode to use when creating the file
    """
    self.store_bytes(text.encode("utf-8"), path, mode=mode)

Functions