kydb — the Kinyu Database
One intuitive, Pythonic interface. Many NoSQL backends. Change your database by changing a URL.
kydb is an abstraction layer over NoSQL databases, built for the kind of workloads found in financial services: object hierarchies, layered caches, market-data invalidation, and objects that must survive years of refactoring.
import kydb
db = kydb.connect('dynamodb://my-table')
db['/trading/books/MYBOOK'] = my_book_object
That is the whole API surface you need to learn. Everything below is the
same db object, pointed at a different URL.
Installation
pip install kydb
Why kydb
| A dict, backed by anything | db[key] = value works identically on S3, DynamoDB, Redis, the filesystem, memory or HTTP. |
| The URL is the config | s3://bucket, redis://host:6379, dynamodb://table. Swap backends without touching a line of application code. |
| Filesystem hierarchy | mkdir, ls, rmdir, rm_tree — on databases that have no concept of a folder. |
| Unions | redis://cache;s3://prod — read from the first database that has the key, always write to the front. |
| Cache DB | redis://cache|dynamodb://table — a read-through cache in front of a persistent store. |
| Cache context | Scope in-memory caching to a block, so live market data reprices while static data stays cached. |
| Store any Python object | Anything pickleable, or use the DbObj decorators for refactor-proof, field-level control. |
| Recency queries | db.recent('/articles', limit=10) — server-side ordered indexes on DynamoDB and Redis. |
| Business keys | db.set(key, v, index={'class_date': 20260905}), then db.folder(f).by('class_date') — order by what an object means, not when it was written. |
Databases in one line each
db = kydb.connect('s3://my-kydb-bucket') # AWS S3
db = kydb.connect('dynamodb://kydb') # AWS DynamoDB
db = kydb.connect('redis://cache.epythoncloud.io') # Redis (port optional)
db = kydb.connect('memory://cache001') # In-memory
db = kydb.connect('files://tmp/foo/bar') # Local filesystem
db = kydb.connect('http://my-source-host') # HTTP (read-only source)
db = kydb.connect('https://my-source-host') # HTTPS (read-only source)
Every one of them returns the same
KYDBInterface. Your code
does not know, and does not care, which one it got.
def save_report(db, name, report):
db[f'/reports/{name}'] = report # works against all of the above
Prefer paths relative to a root? Set a base path in the URL:
db = kydb.connect('dynamodb://my-source-db/home/tony.yum')
db['foo'] = 'Hello World!' # writes to /home/tony.yum/foo
Reading and writing
db['/mytest/foo'] = 123
db['/mytest/foo'] # 123 — served from cache, no round trip
db.read('/mytest/foo', reload=True) # force a read from the database
db.exists('/mytest/foo') # True
db.delete('/mytest/foo')
Any pickleable object goes in as-is:
from datetime import datetime
db['/mytest/bar'] = {
'my_int': 123,
'my_float': 123.456,
'my_str': 'hello',
'my_list': [1, 2, 3],
'my_datetime': datetime.now(),
}
A filesystem on a document store
Document databases have no folders. kydb gives you them anyway — including empty ones — on every backend.
db.mkdir('/empty-folder')
db.ls('/') # ['empty-folder/']
db['/foo/bar/baz'] = 123 # intermediate folders are created for you
db.ls('/') # ['empty-folder/', 'foo/']
db.ls('/foo') # ['bar/']
db.rmdir('/empty-folder') # removes an empty folder
db.rm_tree('/foo') # recursive, like rm -rf
db.ls('/') # []
Folders always end in /; objects never do. list_dir is the lazy,
paginated generator behind ls.
Unions: layer databases with a URL
Separate the databases with ;. Reads try each database in order and return
the first hit; writes always go to the front.
db = kydb.connect('memory://scratch;s3://team-shared;s3://firm-golden')
db['/curves/USD.OIS'] # found in whichever layer has it first
db['/curves/USD.OIS'] = c # always written to memory://scratch
That is a personal overlay on top of shared data, with no branching logic in your code — perfect for "what if I tweak this one curve?" research against a production dataset you must not touch.
CacheDB: a fast cache in front of a slow store
Separate two databases with |. Reads hit the cache first and fall back to
the persistent store; writes go to both.
db = kydb.connect('redis://my-redis-host|s3://my-s3-prod-source')
db['/refdata/instruments'] = instruments # written to Redis *and* S3
db['/refdata/instruments'] # served from Redis
Cache context: invalidate exactly what you mean to
kydb caches reads in memory, so repeatedly pricing a book does not hammer the
database. But live market data must not be cached across ticks. refresh()
is too blunt — it drops the positions too — and tracking invalidation by hand
is error-prone. Scope the cache to a with block instead:
db = kydb.connect('dynamodb://tradingdb')
book = db['/Books/MyBook']
with db.cache_context():
positions = book.Positions() # loaded once, cached for the outer block
while keep_pricing():
with db.cache_context():
for qty, inst in positions.items():
print(qty, inst, inst.Price())
wait_for_next_marketdata_tick()
# inner block exited: all market-data cache is gone
# outer block exited: the position cache is gone too
Recency queries
List a folder in modification-time order, newest first. DynamoDB uses a
folder-time-index GSI and Redis a native sorted set — the ordering is done
by the database, not in your process.
newest = db.recent('/articles', limit=10) # lazy generator of names
For more control, build a query. Each call returns a new query, so a partly built one can be reused and branched:
base = db.folder('/articles').by('mtime')
names = base.desc().limit(10) # ['2026-09-02-launch', ...]
pairs = base.since(timestamp_ns).items() # (name, value) pairs
entries = base.desc().entries() # Entry: .key, .mtime, .ctime
Timestamps are integer nanoseconds since the Unix epoch. Backends with no server-side ordering index (Memory, Files, S3) refuse to silently run an expensive scan — you have to ask for it:
db.recent('/articles', limit=10) # raises IndexNotSupported
db.recent('/articles', limit=10, allow_scan=True) # client-side sort, opt-in
Objects written before the index existed are reported honestly at
mtime == 0 rather than being guessed at, and move into place the first time
they are rewritten. No migration is required. See the
recency documentation
for the full ordering contract and the DynamoDB schema.
Business keys: order by what an object means
mtime answers when was this written. A gym takes a booking on Tuesday for
Saturday's 18:30 HIIT class — and the roster the front desk needs every
morning is ordered by the class date, not by when the phone rang.
Record a business key on the write, and query it with the same builder:
db.set('/signups/anna', booking, index={'class_date': 20260905})
# who is booked into Saturday's classes — one indexed query
db.folder('/signups').by('class_date').since(20260905).until(20260905).items()
# rebooking Anna to Monday: one write, and her key never moves
db.set('/signups/anna', booking, index={'class_date': 20260907})
# and mtime still answers its own question, unchanged
db.recent('/signups', limit=10) # who booked most recently
Values are int (a date is int(d.strftime('%Y%m%d'))), both bounds are
inclusive, and a rewrite that does not mention an index preserves it —
index={'class_date': None} is how you clear one. The index is strictly
sparse: an object with no class_date is not a badly-dated booking, it is
not a booking, so it appears in no roster. DynamoDB serves each business key
from its own folder-<name>-index GSI; Redis and Memory need no schema at
all.
Refactor-proof objects with DbObj
Pickling a class ties your stored data to the module path it was written
from. DbObj breaks that link: the database holds a name, and a config maps
that name to wherever the class lives today. Move the class, and old objects
still deserialise.
Register the classes once:
db = kydb.connect('memory://decorated_py_obj')
db.upload_objdb_config({
'Greeter': {
'module_path': 'path.to.module',
'class_name': 'Greeter',
}
})
Declare which attributes are persisted with @kydb.stored — everything else
is transient state, rebuilt by init() on each load:
import kydb
class Greeter(kydb.DbObj):
def init(self):
self.greet_count = 0 # transient
@kydb.stored
def name(self):
return 'John' # the default value
def greet(self):
self.greet_count += 1
return 'Hello ' + self.name()
key = '/hello-world/greeter001'
greeter = db.new('Greeter', key)
greeter.name() # 'John' — the default
greeter.greet() # 'Hello John'
greeter = db.new('Greeter', key, name='Tony')
greeter.greet() # 'Hello Tony'
greeter.name.setvalue('Jane')
greeter.greet() # 'Hello Jane'
greeter.get_stored_dict() # {'name': 'Jane'} — only `name` persists
Persist it and read it back; greet_count resets because it was never
stored:
db[key] = greeter
db.read(key, reload=True).greet_count # 0
db.read(key, reload=True).name() # 'Jane'
Secured Redis
Redis connection details, including a password encrypted with AWS KMS, can be
supplied through a config file pointed at by KYDB_CONFIG_PATH:
dbs:
my-redis-db:
host: cache.epythoncloud.io
port: 6379
password:
encryption-method: kms # or `plain`
encryption-key: <kms-key-id>
env_var: MY_REDIS_PASSWORD # holds the base64 ciphertext
The same file switches off the recency index per database with
mtime-index: false.
Documentation
Full documentation, including the complete API reference and the DynamoDB table schema, lives at kydb.readthedocs.io.
License
MIT — see LICENSE.
Metadata
Release files for kydb 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kydb-1.1.0.tar.gz | 58.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kydb-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.2 kB
Release files / kydb-1.1.0.tar.gz
| Download URL | kydb-1.1.0.tar.gz |
|---|---|
| Size | 58.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3ca14f4ccc68af870574a7dd4568511854f7515f6e6a228e569aaddc19ea2f92
|
|
BLAKE2b-256 checksum How to use checksums |
4cffc343265af60cf3803a9a946fba3ec04864d3e9c48361ef4d004432a17542
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / kydb-1.1.0-py3-none-any.whl
| Download URL | kydb-1.1.0-py3-none-any.whl |
|---|---|
| Size | 64.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
44599c866578dd8545a7a10e6551fe0df87bfbfb76d19155280e295c3e6e0bb7
|
|
BLAKE2b-256 checksum How to use checksums |
dbfd3c60b92d96f1511e529592e5185f01426156cf513ebb5826efa976c47df0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|