Lightweight Python application resource manager for configs, caches, assets, and files under safe roots.
Project description
Coryl
A lightweight Python application resource manager for configs, caches, assets, and files under safe roots.
If your project has started to grow little path rules such as config/, .cache/,
assets/, or data/, Coryl gives you one small, explicit layer to manage them
without turning your application into a framework.
It is built for the common local-app case: keep files under a root you control,
register the resources you care about, and read or write them through clear helpers.
The core install stays intentionally small, while optional extras add things like
YAML support, typed config validation, diskcache, file watching, fsspec,
platform-specific app directories, and file locks.
Start Here
For most new users, the fastest path is:
- Install the core package with
python -m pip install coryl. - Create one manager with
Coryl(root=".")in a project folder you control. - Register the resources your app needs, such as config files, caches, and assets.
- Add extras only when you actually need them, such as
coryl[yaml]orcoryl[all].
The smallest useful setup looks like this:
from coryl import Coryl
app = Coryl(root=".")
settings = app.configs.add("settings", "config/settings.toml")
settings.save({"app_name": "Coryl Demo", "debug": True})
print(settings.load()["app_name"])
That gives you a managed config file under a safe root, structured TOML reads and
writes, and a clean place to grow into app.caches, app.assets, app.data, and
app.logs when the project needs them.
Why Coryl?
Many Python applications eventually need the same small set of file concerns:
- a config file or two
- a local cache
- some bundled assets
- maybe runtime data and logs
The hard part is usually not file I/O itself. The hard part is keeping paths predictable, avoiding accidental path traversal, and not letting app-specific conventions spread across the codebase.
Coryl keeps that layer small. The default path is intentionally simple: Coryl(root=".") plus a few focused namespaces such as configs, caches, and assets. When you need more, optional extras add typed config validation, layered config, file watching, diskcache, fsspec, platform-specific app directories, and file locks without turning the core package into a framework.
Release Status
Coryl is a small beta library. The default local-filesystem flow, Coryl(root="."),
is the most exercised path today. Optional integrations are covered by targeted tests,
but they remain intentionally conservative and some helpers are local-filesystem-only.
In short:
- start with the local filesystem manager unless you know you need more
- expect explicit, predictable file handling rather than framework magic
- treat
fsspec, watching, locks, anddiskcacheas opt-in features with narrower behavior boundaries - use Coryl when you want a safe resource layer, not a larger application architecture
Installation
Core install:
python -m pip install coryl
Requirements:
- Python
3.10+ - a local project directory you control if you want the default
Coryl(root=".")workflow
The core install includes:
- the default local filesystem manager
- JSON and TOML structured I/O
- the built-in file-backed cache
- filesystem and package asset helpers
- the
coryldiagnostics CLI
Optional extras:
| Extra | Install | What it enables | Notes |
|---|---|---|---|
lock |
python -m pip install coryl[lock] |
Resource.lock() |
Local filesystem only. |
diskcache |
python -m pip install coryl[diskcache] |
app.caches.diskcache(...) and backend="diskcache" |
Local filesystem only. |
fsspec |
python -m pip install coryl[fsspec] |
Coryl.with_fs(...) and filesystem="fsspec" |
Opt-in, conservative support. |
platform |
python -m pip install coryl[platform] |
Coryl.for_app(...) |
Uses platformdirs for config/cache/data/log roots. |
pydantic |
python -m pip install coryl[pydantic] |
load_typed() and save_typed() |
Expects a Pydantic v2-style interface. |
watch |
python -m pip install coryl[watch] |
watch(), watch_reload(), on_change() |
Local filesystem only and blocking. |
yaml |
python -m pip install coryl[yaml] |
read_yaml(), write_yaml(), YAML manifests, and YAML layered-config files |
Loaded lazily when you touch YAML files. |
cli |
python -m pip install coryl[cli] |
No extra dependencies today | The CLI already ships with python -m pip install coryl. |
all |
python -m pip install coryl[all] |
Full optional feature set | Includes the declared extras, including YAML support. |
Missing optional dependencies fail lazily. Coryl imports them only when you use the
related feature, then raises CorylOptionalDependencyError with the matching
pip install coryl[...] hint.
Quick guidance:
- Use the core install if you want local configs, caches, assets, JSON/TOML I/O, and the CLI.
- Add
coryl[yaml]if your project uses YAML files or YAML manifests. - Add
coryl[platform]if you want OS-specific config/cache/data/log directories throughCoryl.for_app(...). - Add
coryl[all]if you are evaluating the full optional feature set locally.
More detail:
Quick Start
Run this from a disposable working directory or a project folder you control. Coryl creates files relative to the root you pass in. This example shows the common pattern: one config, one cache, and one asset group under the same manager.
from coryl import Coryl
app = Coryl(root=".")
settings = app.configs.add("settings", "config/settings.toml")
cache = app.caches.add("http", ".cache/http")
assets = app.assets.add("ui", "assets/ui")
settings.save({"app_name": "Coryl Demo", "debug": True})
loaded_settings = settings.load()
user = cache.remember_json(
"users/42.json",
lambda: {"id": 42, "name": "Ada"},
ttl=300,
)
logo = assets.file("images", "logo.svg", create=True)
logo.write_text("<svg />")
same_logo = assets.require("images", "logo.svg")
print(loaded_settings["app_name"])
print(user["name"])
print(same_logo.path)
Core Concepts
root: the managed base path. Local resources are resolved under this root and must stay inside it.resources: named files or directories you register withregister_file(...),register_directory(...), or one of the typed namespaces.configs: structured file resources exposed throughapp.configs.caches: managed cache directories exposed throughapp.caches.assets: safe asset directories exposed throughapp.assets.data/logs: plain resources exposed throughapp.dataandapp.logs.manifests: JSON, TOML, or YAML declarations that register multiple resources at startup.
Most applications can stay inside one root. If you later need platform-specific config/cache/data/log routing, Coryl.for_app(...) adds that without changing the basic model.
Safety Model
- Root confinement: registered paths must stay inside the manager root or the named root chosen by
Coryl.for_app(...). - Child path confinement: child helpers such as
assets.file(...),cache.file(...), andresource.joinpath(...)must stay inside their parent resource. - Traversal rejection: Coryl rejects path traversal segments such as
.., even when the normalized path would appear to land back inside the root. - Symlink behavior: local directory symlinks or junctions that would escape the allowed root are rejected during registration and during child lookups.
- Read-only resources: files, configs, caches, and assets can be marked
readonly=True; reads still work, writes do not. - Atomic writes: local
write_text(),write_bytes(),write_json(),write_toml(),write_yaml(), and configsave()operations use atomic replacement by default. - fsspec limitations: Coryl's fsspec backend does not support local-only helpers such as
open(),lock(),watch(), layered config, or thediskcachebackend, and fsspec writes fall back to regular writes even whenatomic=True.
Files and Structured Data
Coryl keeps generic file I/O small and explicit:
| Type | Read | Write | Notes |
|---|---|---|---|
| Text | read_text() |
write_text() |
Uses the resource encoding, default utf-8. |
| Bytes | read_bytes() |
write_bytes() |
Raw binary I/O. |
| JSON | read_json() |
write_json() |
Core install. |
| TOML | read_toml() |
write_toml() |
Core install. |
| YAML | read_yaml() |
write_yaml() |
Requires coryl[yaml]. |
read_data() and write_data() infer the structured format from the file extension. content() and write() provide one higher-level layer that auto-detects structured formats, text, and bytes.
from coryl import Coryl
app = Coryl(root=".")
notes = app.register_file("notes", "data/notes.txt")
notes.write_text("hello")
blob = app.register_file("blob", "data/blob.bin")
blob.write_bytes(b"\x00\x01coryl")
state = app.register_file("state", "data/state.json")
state.write_json({"count": 1})
settings = app.register_file("settings", "config/app.toml")
settings.write_toml({"debug": True, "theme": "dark"})
The sections below cover optional or more advanced workflows. Core usage stays intentionally smaller than the full surface area.
Configs
Basic config
ConfigResource adds structured config helpers on top of a normal file resource.
from coryl import Coryl
app = Coryl(root=".")
settings = app.configs.add("settings", "config/settings.toml")
settings.save(
{
"debug": False,
"database": {"host": "localhost", "ports": [5432, 5433]},
}
)
print(settings.get("database.host"))
print(settings.get("database.ports.1"))
updated = settings.update({"debug": True}, timezone="Europe/Rome")
print(updated["timezone"])
Typed config
Requires coryl[pydantic].
from importlib.util import find_spec
if find_spec("pydantic") is not None:
from pydantic import BaseModel
from coryl import Coryl
class SettingsModel(BaseModel):
host: str
port: int
debug: bool = False
app = Coryl(root=".")
settings = app.configs.add(
"settings", "config/settings.toml", schema=SettingsModel
)
settings.save_typed(SettingsModel(host="localhost", port=5432, debug=True))
typed_settings = settings.load_typed()
print(typed_settings.host)
print(typed_settings.port)
Layered config
Requires the default local filesystem. This is a separate resource type exposed through app.configs.layered(...).
import os
from pathlib import Path
from coryl import Coryl
Path("config").mkdir(exist_ok=True)
Path("config/defaults.toml").write_text(
'debug = false\n[database]\nhost = "db"\n',
encoding="utf-8",
)
Path("config/local.toml").write_text('theme = "local"\n', encoding="utf-8")
previous_host = os.environ.get("MYAPP_DATABASE__HOST")
os.environ["MYAPP_DATABASE__HOST"] = "env-db"
try:
app = Coryl(root=".")
settings = app.configs.layered(
"settings",
files=["config/defaults.toml", "config/local.toml"],
env_prefix="MYAPP",
)
settings.override({"debug": True})
print(settings.as_dict())
finally:
if previous_host is None:
os.environ.pop("MYAPP_DATABASE__HOST", None)
else:
os.environ["MYAPP_DATABASE__HOST"] = previous_host
Env overrides
With env_prefix="MYAPP", environment variables map onto dotted config keys:
MYAPP_DEBUG=true->debug = TrueMYAPP_DATABASE__HOST=localhost->database.host = "localhost"MYAPP_DATABASE__PORT=5432->database.port = 5432
Parsing is conservative. Booleans, integers, floats, JSON-like arrays, and JSON-like objects are converted when possible; otherwise values stay as strings.
Secrets
Layered config can merge secrets from:
secrets="config/.secrets.toml"for one structured secrets filesecrets_dir="run/secrets"for a directory where each filename becomes a top-level key
Use required=True when missing layers or secrets inputs should fail instead of being skipped.
Migrations
Versioned config migrations stay local to one config resource.
from coryl import Coryl
app = Coryl(root=".")
settings = app.configs.add("settings", "config/settings.toml", version=2)
settings.save({"version": 1, "theme": "light"})
@settings.migration(from_version=1, to_version=2)
def migrate_to_v2(document: dict[str, object]) -> dict[str, object]:
document["appearance"] = {"theme": document.pop("theme")}
return document
print(settings.migrate())
watch_reload()
Requires coryl[watch] and the default local filesystem.
# Conceptual: this blocks and yields whenever the config changes on disk.
for updated in settings.watch_reload():
print(updated)
on_change(callback) is the callback-oriented equivalent.
Caches
Built-in file cache
The default cache is a managed directory with inspectable files on disk.
from coryl import Coryl
app = Coryl(root=".")
cache = app.caches.add("http", ".cache/http")
cache.set("tokens/state.txt", "ready", ttl=60)
user = cache.remember_json(
"users/42.json",
lambda: {"id": 42, "name": "Ada"},
ttl=300,
)
print(cache.get("tokens/state.txt"))
print(user["name"])
TTL metadata for the built-in cache is stored in a reserved file named .coryl-cache-index.json inside the cache directory.
TTL and remember helpers
set(key, value, ttl=...)stores a value with an optional expirationget(key, default=...)returns the value or the defaulthas(key)checks existence and expirationexpire()removes expired entriesremember(...),remember_json(...), andremember_text(...)compute and store a value only when needed
DiskCache backend
Requires coryl[diskcache] and the default local filesystem.
from importlib.util import find_spec
if find_spec("diskcache") is not None:
from coryl import Coryl
app = Coryl(root=".")
cache = app.caches.diskcache("api", ".cache/api")
@cache.memoize(ttl=60)
def load_user(user_id: int) -> dict[str, object]:
return {"id": user_id}
print(load_user(42))
You can also register it with app.caches.add(..., backend="diskcache").
Which cache should you use?
- Use the built-in cache when you want inspectable files, path-based entries, or fsspec support.
- Use the
diskcachebackend when you want opaque Python object caching and decorator-style memoization on one local machine.
Assets
Filesystem assets
AssetGroup is for safe file and directory lookups under a managed asset root.
from coryl import Coryl
app = Coryl(root=".")
assets = app.assets.add("ui", "assets/ui")
logo = assets.file("images", "logo.svg", create=True)
logo.write_text("<svg />")
print(assets.require("images", "logo.svg").path)
print(assets.files("**/*"))
Use file(...), directory(...), and require(...) for lookups. Use files(pattern=...) when you want files only, and glob(pattern) when you want both files and directories.
Package assets
Package-backed assets are read-only and use importlib.resources, so they work from a source tree, a normal install, or a zipped import.
# Conceptual: assumes myapp.assets_pkg is an importable package with bundled files.
from coryl import Coryl
app = Coryl(root=".")
bundled = app.assets.from_package("bundled", "myapp.assets_pkg", "assets")
print(bundled.read_text("templates", "email.html"))
copy_to() bootstrap
Package assets do not promise a stable on-disk path. When you need real files on disk, use as_file() for one file or copy_to(...) for a whole tree.
# Conceptual: bootstrap bundled assets into a writable directory.
target_root = bundled.copy_to("bootstrap-assets", overwrite=True)
print(target_root)
Installed Apps
Requires coryl[platform].
Coryl.for_app(...) builds a manager around platformdirs and routes resource types to the expected OS-specific roots:
configs-> config rootcaches-> cache rootdata-> data rootlogs-> log rootassets-> data root
from importlib.util import find_spec
if find_spec("platformdirs") is not None:
from coryl import Coryl
app = Coryl.for_app("MyTool", app_author="Acme", version="1.2.3", ensure=True)
settings = app.configs.add("settings", "settings.toml")
cache = app.caches.add("http", "http")
state = app.data.add("state", "state.json")
log = app.logs.add("main", "app.log")
assets = app.assets.add("ui", "assets/ui")
Use ensure=True when you want the platform roots created immediately. With ensure=False, roots are created as resource creation requires them.
Manifests
Version 2 is the preferred manifest schema. It carries explicit path, kind, role, and optional metadata such as readonly, required, format, schema, and backend.
Load a manifest at startup:
from pathlib import Path
from coryl import Coryl
Path("app.toml").write_text(
'version = 2\n\n[resources.settings]\npath = "config/settings.toml"\nkind = "file"\nrole = "config"\ncreate = false\n',
encoding="utf-8",
)
app = Coryl(root=".", manifest_path="app.toml", create_missing=False)
print(sorted(app.resources))
TOML v2
version = 2
[resources.settings]
path = "config/settings.toml"
kind = "file"
role = "config"
create = false
[resources.http_cache]
path = ".cache/http"
kind = "directory"
role = "cache"
create = false
JSON v2
{
"version": 2,
"resources": {
"settings": {
"path": "config/settings.toml",
"kind": "file",
"role": "config",
"create": false
},
"ui": {
"path": "assets/ui",
"kind": "directory",
"role": "assets",
"create": false
}
}
}
YAML v2
Requires coryl[yaml].
version: 2
resources:
settings:
path: config/settings.toml
kind: file
role: config
create: false
ui:
path: assets/ui
kind: directory
role: assets
create: false
Legacy schema compatibility
Legacy manifests still load for compatibility:
{
"paths": {
"files": {
"settings": "config/settings.toml"
},
"directories": {
"http_cache": ".cache/http",
"ui": "assets/ui"
}
}
}
Use v2 for new work. The legacy schema only describes file and directory paths; it does not carry typed resource roles such as config, cache, or assets.
Diagnostics CLI
The CLI is meant for inspection and maintenance, not for defining your application architecture.
Commands:
coryl resources list --manifest app.toml --root .coryl resources check --manifest app.toml --root .coryl config show settings --manifest app.toml --root .coryl cache clear http_cache --manifest app.toml --root .coryl assets list ui --manifest app.toml --root .
Use --json on every command when you want machine-readable output.
Behavior notes:
resources listloads the manifest and prints resource role, kind, existence, safety, and path.resources checkreturns exit code1when any resource is missing or unsafe.config showprints one config resource from the manifest.cache clearclears one cache resource from the manifest.assets listlists files from filesystem assets or package assets.
Advanced Filesystems
Requires coryl[fsspec].
fsspec support is opt-in:
Coryl.with_fs(root, protocol=...)Coryl(root=..., filesystem="fsspec", protocol=...)
Memory filesystem example:
from importlib.util import find_spec
if find_spec("fsspec") is not None:
from coryl import Coryl
app = Coryl.with_fs("memory://demo", protocol="memory")
settings = app.configs.add("settings", "config/settings.json")
cache = app.caches.add("api", ".cache/api")
settings.save({"theme": "light"})
user = cache.remember_json("users/42.json", lambda: {"id": 42})
print(settings.load())
print(user)
Current limitations:
- local-only helpers such as
open(),lock(), andwatch()do not work - layered config is local-only
- the
diskcachebackend is local-only - local atomic replace semantics are not available; writes fall back to regular writes
See Limitations for the current boundary.
API Overview
The public surface is small enough to skim and broad enough that the full reference is still useful. See API reference for signatures and behavior details.
The main pieces are:
Coryl/ResourceManager: root management, registration, manifests, and lookupsResource: generic file or directory helpersConfigResource: structured config load/save/get/update plus migrations, typed config, and watch helpersLayeredConfigResource: local layered config with files, environment overrides, secrets, and runtime overridesCacheResource/DiskCacheResource: built-in file-backed caching or optionaldiskcacheAssetGroup/PackageAssetGroup: filesystem assets or package-backed assetsResourceSpec: declarative resource definitions for registration and manifests
Examples
See examples/ for runnable scripts validated by the test suite, and see examples/README.md for dependency notes and expected output summaries.
Optional examples such as typed_config.py, cache_diskcache.py, and
fsspec_memory.py still run from a fresh checkout. When the extra is not installed,
they print a small JSON payload with available: false and skipped: true instead of
failing.
- examples/simple_local_app.py:
Coryl(root=...)with TOML config, JSON config, cache, and asset lookup - examples/cli_tool_config.py: create a default config, update it, and print a value
- examples/api_cache.py: fake API caching with
remember_json(..., ttl=...) - examples/desktop_app_assets.py: filesystem assets with
require(),files(), andglob() - examples/manifest_startup.py: write
app.toml, load it, use configs/caches/assets, and inspectaudit_paths() - examples/package_assets.py: package assets with
read_text(),read_bytes(), andcopy_to() - examples/typed_config.py: typed config loading with Pydantic when available
- examples/layered_config.py: defaults, local overrides, environment overrides, and runtime overrides
- examples/cache_diskcache.py: optional
diskcachecache backend - examples/fsspec_memory.py: optional
fsspecmemory filesystem backend - examples/diagnostics_cli.py:
resources list,resources check, andconfig show
Compatibility Notes
- Legacy
FileManager-style path helpers still exist, includingroot_folder_path,config_file_path, and dynamic aliases such as<name>_file_pathand<name>_directory_pathwhen the resource kind matches. app.configis reserved for loaded manifest content. It is not a shortcut to an application settings resource.load_config()reloads the current manifest file and updatesapp.config.
Design Principles
- Simple by default: the core path is
Coryl(root=".")plus a few focused namespaces. - Safe by default: Coryl rejects unsafe paths instead of trying to recover from them.
- Optional integrations: typed configs, platformdirs, watching, locks,
diskcache,fsspec, and YAML stay opt-in. - No framework lock-in: Coryl manages application resources; it does not impose a larger app structure.
- Lazy optional imports: optional dependencies are only imported when you actually use the related feature.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file coryl-0.0.2.tar.gz.
File metadata
- Download URL: coryl-0.0.2.tar.gz
- Upload date:
- Size: 119.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf27d00db899bb115b9e102560582da02caf80d056070a88ebeb26c5f1fd10eb
|
|
| MD5 |
c26efaba4816e1bd8ccc77f74b83940d
|
|
| BLAKE2b-256 |
0a025502cda001c5c957b78bba060f48312a59608c18ed7a4b3a28306d150cf2
|
File details
Details for the file coryl-0.0.2-py3-none-any.whl.
File metadata
- Download URL: coryl-0.0.2-py3-none-any.whl
- Upload date:
- Size: 45.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e613513ec4c3a71921e458a61ebef2de3d614f01404ce7432ea44574c325fae
|
|
| MD5 |
f5131521e229d39dcbda8019e754b906
|
|
| BLAKE2b-256 |
722ff2d574cd44c90112e444c1ccadcdf98563c5ec6887cc5d32cc2e122728d4
|