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.
ResourceErroris the common provider error base.NotFoundalso inheritsFileNotFoundError, andAccessDeniedalso inheritsPermissionError.AuthenticationRequired,Conflict, andUnsupportedOperationdistinguish other failures. Ordinary stream IO errors retain their IO exception types.EffectUnknownmeans 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
writeto mutate content rather than the stream returned byopen. - 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-stateand.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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.