Skip to main content

Duru

Access local files, Google Drive documents, and Slack messages through small, URI-based Python interfaces. Implement one provider contract and opt into the capabilities your storage supports.

English · 한국어

Status

This library is under development and requires Python 3.12 or later. The package and Python import are both duru. It uses the BSD 3-Clause license.

The package has no runtime dependencies. Applications supply credentials and configuration explicitly. It can be used independently of any framework.

Installation

Install from PyPI:

python -m pip install duru

For a local checkout, use python -m pip install . instead.

Quick start

This example creates its own temporary directory and sample file. All mutations stay inside that directory.

from pathlib import Path
from tempfile import TemporaryDirectory

from duru import LocalProvider, Registry

with TemporaryDirectory() as directory:
    root = Path(directory) / "documents"
    root.mkdir()
    (root / "guide.md").write_text("Hello, resources!", encoding="utf-8")

    provider = LocalProvider(root)
    registry = Registry()
    registry.register("local", provider)

    uri = provider.uri("guide.md")
    selected = registry.resolve(uri)
    info = selected.stat(uri)
    with selected.open(uri, expected_revision=info.revision) as content:
        print(info.name, content.read().decode("utf-8"))

    updated = provider.write(uri, b"Updated guide", expected_revision=info.revision)
    renamed = provider.rename(updated.uri, "manual.md")
    print(renamed.name)

Expected output:

guide.md Hello, resources!
manual.md

Use the URI returned by a mutation for subsequent calls. A URI identifies a resource; its display name is a separate value. Their relationship belongs to the provider. The registry only dispatches by URI scheme.

Providers and capabilities

Provider Read, stat, list Mutations
ReadOnlyLocalProvider Yes None
LocalProvider Yes File create, write, rename, move, delete
GoogleDriveProvider Yes None
SlackProvider Yes None

Applications inject tokens and resource IDs. Providers do not load framework settings, discover credentials, or acquire and renew OAuth tokens.

from duru import GoogleDriveProvider, SlackProvider

def configure_providers(drive_token, folder_id, slack_token, workspace_id, channel_id):
    drive = GoogleDriveProvider(
        access_token=drive_token,
        account_id="work",
        folder_id=folder_id,
        export_formats={"application/vnd.google-apps.document": "text/plain"},
    )
    slack = SlackProvider(
        token=slack_token,
        workspace_id=workspace_id,
        channel_ids=(channel_id,),
    )
    return drive, slack

Drive native documents require an explicit export format. Slack messages are JSON resources. Configured folders and channel IDs are starting locations, not authorization boundaries. Read the provider guide for URI conventions, pagination, API costs, transport limits, and upstream links.

Extend the contract

Subclass Provider and implement identity, stat(uri), and open(uri, expected_revision=...). Return ResourceInfo for metadata and a caller-owned binary stream for content. The caller closes the stream, preferably with with.

A checked open must verify the actual returned bytes against the requested revision or reject the operation. Checking metadata before returning an unchecked, mutable stream is insufficient. Providers define resource identity and revision semantics explicitly.

Inherit additional ABCs only for supported operations:

Contract Operation
Listable list(uri, cursor=None, limit=100) -> Page
Writable write(uri, content, expected_revision=None)
ConditionalWritable Compare-and-replace writes among cooperating clients
Creatable create(parent_uri, name, content)
Renamable rename(uri, name)
Movable move(uri, destination_uri) into a parent collection
Removable delete(uri)

capabilities(provider) reports implemented contracts, not access permissions. Unsupported operations fail explicitly. Mutations that return a resource return its resulting URI and display name. Cross-provider moves are unsupported; the library does not emulate them with a non-atomic copy and delete.

For example, register a read-only provider serving one immutable document:

import hashlib
import io

from duru import Conflict, NotFound, Provider, Registry, ResourceInfo

class GuideProvider(Provider):
    identity = "guide-demo:v1"
    uri = "guide://manual"
    content = b"A document supplied by your application."

    def stat(self, uri):
        if uri != self.uri:
            raise NotFound("Unknown guide")
        snapshot = self.content
        return ResourceInfo(
            uri, "manual.txt",
            revision=hashlib.sha256(snapshot).hexdigest(),
            size=len(snapshot),
        )

    def open(self, uri, *, expected_revision=None):
        if uri != self.uri:
            raise NotFound("Unknown guide")
        snapshot = self.content
        revision = hashlib.sha256(snapshot).hexdigest()
        if expected_revision is not None and expected_revision != revision:
            raise Conflict("Guide revision changed")
        return io.BufferedReader(io.BytesIO(snapshot))

registry = Registry()
registry.register("guide", GuideProvider())
provider = registry.resolve("guide://manual")
info = provider.stat("guide://manual")
with provider.open(info.uri, expected_revision=info.revision) as content:
    print(content.read().decode("utf-8"))

Consistency and limits

  • Listing cursors are opaque and collection-scoped. Listings are not snapshots; a missing item is not evidence of deletion.
  • ResourceError is the common provider error base. NotFound also inherits FileNotFoundError, and AccessDenied also inherits PermissionError. AuthenticationRequired, Conflict, and UnsupportedOperation distinguish other failures. Ordinary stream IO errors retain their IO exception types.
  • EffectUnknown means a mutation may already have occurred. Reconcile the resource state before retrying; an exception alone does not prove rollback.
  • Local revisions are SHA256 digests. Reads buffer the entire file into a read-only snapshot. Memory use grows with file size; use write to mutate content rather than the stream returned by open.
  • Linux access uses no-follow descriptors for every absolute directory component and rejects symlinks and non-regular file content. Writes publish fully written temporary files atomically; durability failures after a mutation raise EffectUnknown. Locks coordinate cooperating providers, not unrelated OS writers. Windows uses checked paths without equivalent protection against concurrent directory replacement. Linux is the hardened deployment target.
  • Local collection mutation is unsupported. Local URIs are path-derived and may change after rename or move. Reserved .framework-state and .framework-write- names are excluded from listings.

Scheduling, durable checkpoints, retry policy, document permissions, parsing, and indexing belong to the application. Native watches, copy, mkdir, locking APIs, POSIX attributes, and network mount protocols are outside the common interface.

Development

From this checkout, install development tools and run the checks:

python -m pip install -e '.[test]'
python -m pytest
python -m ruff check src tests
python -m ruff format --check src tests

Tests cover provider substitution, names and URIs, checked snapshots, cursor scope, mutation conflicts, concurrent conditional writers, local confinement, and unknown mutation outcomes. Linux-specific security and locking tests are skipped on Windows. Service tests use local HTTP fixtures; they do not verify live Google Drive or Slack accounts.

Contributing

For a new provider or capability, describe its URI and revision semantics, supported operations, and behavior after an ambiguous mutation. Keep credentials injected and provider API code separate from application scheduling and policy. Include focused contract tests and update the provider guide and both README translations when public behavior changes. Use the checks above before sharing a change. Contributions use the BSD 3-Clause license.

See contributing, security reporting, and the release procedure.

Metadata

Release files for duru 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for duru 0.1.1
File Size Uploaded Provenance Actions
duru-0.1.1.tar.gz 44.6 kB With provenance Details for duru-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for duru 0.1.1
File Interpreter ABI Platform Actions
duru-0.1.1-py3-none-any.whl Python 3 none any Details for duru-0.1.1-py3-none-any.whl

Total release size: 73.7 kB

Release files / duru-0.1.1.tar.gz

Download URL duru-0.1.1.tar.gz
Size 44.6 kB
Tags Source
SHA-256 checksum
How to use checksums
08390c995f1e7a0b2089bb4c92cdf964b81052cc28dd6c00c0b03844e5260ffc
BLAKE2b-256 checksum
How to use checksums
52893e7c0a82724a21d2deb1bbc51d4000a81783b7a72019605e1dd0ce557e10
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / duru-0.1.1-py3-none-any.whl

Download URL duru-0.1.1-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cc7d403d79ece4cfd8ec60e32e8b603e32f6a0399f2fe19d8d4e5997611ea93c
BLAKE2b-256 checksum
How to use checksums
6232eb214de7d23cbaa84bfee8b6772a8500089abbb30843d0107e2f0351f99f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for duru 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for duru-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page