Skip to content

Storage

m365_brain.storage

Storage backends for m365-brain, and the addressing that surrounds them.

A storage key is relative by contract -- see vault/paths.py. That holds inside the library, where StorageBackend addresses a blob container and a filesystem path would be meaningless. It stops holding the moment a key leaves the process: on stdout, in a caller's hands, a relative key needs a base the caller has no way to obtain. resolve_key and storage_key are that boundary, in one place, so the join rule cannot be spelled two ways.

create_storage

create_storage(config: StorageConfig) -> StorageBackend

Create a storage backend from config. Crashes on unknown or misconfigured backend.

Source code in m365_brain/storage/__init__.py
def create_storage(config: StorageConfig) -> StorageBackend:
    """Create a storage backend from config. Crashes on unknown or misconfigured backend."""
    if config.backend == "local":
        if config.local is None:
            raise ConfigError("backend is 'local' but storage.local section is missing")
        from m365_brain.storage.local import LocalBackend

        return LocalBackend(config.local.base_path)

    if config.backend == "azure_blob":
        if config.azure_blob is None:
            raise ConfigError("backend is 'azure_blob' but storage.azure_blob section is missing")
        from m365_brain.storage.azure_blob import AzureBlobBackend

        return AzureBlobBackend(
            connection_string=config.azure_blob.connection_string,
            container_name=config.azure_blob.container_name,
            prefix=config.azure_blob.prefix,
        )

    raise ConfigError(f"unknown storage backend '{config.backend}'")

create_user_storage

create_user_storage(config: StorageConfig, user_id: str) -> StorageBackend

Create a storage backend with per-user isolation.

Appends user_id to the storage prefix (azure_blob) or base_path (local) so each user's synced data lives in its own subdirectory.

Source code in m365_brain/storage/__init__.py
def create_user_storage(config: StorageConfig, user_id: str) -> StorageBackend:
    """Create a storage backend with per-user isolation.

    Appends ``user_id`` to the storage prefix (azure_blob) or base_path (local)
    so each user's synced data lives in its own subdirectory.
    """
    validate_user_id(user_id)
    if config.backend == "local":
        if config.local is None:
            raise ConfigError("backend is 'local' but storage.local section is missing")
        from m365_brain.storage.local import LocalBackend

        user_path = str(Path(config.local.base_path) / user_id)
        return LocalBackend(user_path)

    if config.backend == "azure_blob":
        if config.azure_blob is None:
            raise ConfigError("backend is 'azure_blob' but storage.azure_blob section is missing")
        from m365_brain.storage.azure_blob import AzureBlobBackend

        base_prefix = config.azure_blob.prefix.rstrip("/")
        user_prefix = f"{base_prefix}/{user_id}"
        return AzureBlobBackend(
            connection_string=config.azure_blob.connection_string,
            container_name=config.azure_blob.container_name,
            prefix=user_prefix,
        )

    raise ConfigError(f"unknown storage backend '{config.backend}'")

local_base_path

local_base_path(config: StorageConfig) -> Path

storage.local.base_path, as the filesystem path stored keys hang off.

Raises rather than guessing for a blob-backed vault: StorageBackend has write_bytes and no read_bytes, so there is no backend-agnostic way to hand a stored object back to something that wants a file. Say so instead of failing later inside a storage client's constructor.

Source code in m365_brain/storage/__init__.py
def local_base_path(config: StorageConfig) -> Path:
    """`storage.local.base_path`, as the filesystem path stored keys hang off.

    Raises rather than guessing for a blob-backed vault: `StorageBackend` has
    `write_bytes` and no `read_bytes`, so there is no backend-agnostic way to
    hand a stored object back to something that wants a file. Say so instead of
    failing later inside a storage client's constructor.
    """
    if config.backend != "local" or config.local is None:
        raise ConfigError(
            f"this reads a stored file back off disk, and storage.backend is {config.backend!r}. "
            f"StorageBackend has write_bytes but no read_bytes, so there is nothing to read from "
            f"-- run it against a local vault."
        )
    return Path(config.local.base_path)

resolve_key

resolve_key(config: StorageConfig, key: str) -> str

One storage-relative key as an address that needs no base from the caller.

An absolute filesystem path for local, a blob URL for azure_blob. This is the only conversion the library performs in that direction, and it happens at the process boundary -- keys stay relative everywhere inside.

Source code in m365_brain/storage/__init__.py
def resolve_key(config: StorageConfig, key: str) -> str:
    """One storage-relative key as an address that needs no base from the caller.

    An absolute filesystem path for `local`, a blob URL for `azure_blob`. This
    is the only conversion the library performs in that direction, and it
    happens at the process boundary -- keys stay relative everywhere inside.
    """
    if _carries_its_own_base(key):
        return key
    return _address_base(config) + key

storage_key

storage_key(config: StorageConfig, address: str) -> str

The inverse of resolve_key: an address back to the key it names.

An address that does not sit under the configured base comes back unchanged. It is then simply a key that matches nothing, which is the honest answer for a path belonging to some other vault -- better than a substring guess that would resolve it to the wrong row.

Source code in m365_brain/storage/__init__.py
def storage_key(config: StorageConfig, address: str) -> str:
    """The inverse of `resolve_key`: an address back to the key it names.

    An address that does not sit under the configured base comes back
    unchanged. It is then simply a key that matches nothing, which is the
    honest answer for a path belonging to some other vault -- better than a
    substring guess that would resolve it to the wrong row.
    """
    if not _carries_its_own_base(address):
        return address
    base = _address_base(config)
    return address[len(base) :] if address.startswith(base) else address