jsondb2.py

Version 0.1.0.1 · Source: modules/jsondb2.py

A JSON file wrapper with dictionary-style access, optional autosave, and lightweight async queue utilities.

Setup and storage model

Place jsondb2.py on the import path. JSON and queue operations use standard-library modules. Import also calls pmblue_update.self_update(); if the updater is absent, the bootstrap requires requests, downloads over the legacy HTTP URL, and writes pmblue_update.py in the working directory.

JsonFile(fp, **kwargs) reads the entire file into memory. A missing file is created with {}; its parent directory must already exist. Data is intended to be a JSON dictionary or list. Files are rewritten in full when saved, using compact JSON with ensure_ascii=False.

Explicit save example

from jsondb2 import JsonFile

settings = JsonFile("settings.json")
settings["theme"] = "dark"
settings["recent_files"] = ["report.csv"]
settings["recent_files"].append("summary.csv")
settings.save()

settings.reload()
print(settings["theme"])
print("theme" in settings)

Constructor options

OptionDefaultBehavior
auto_saveFalseSave on wrapper item assignment/deletion and data replacement. Existing nested dictionaries/lists are wrapped to intercept item assignment.
async_saveFalseSchedule save work through the internal asyncio queue; requires a running event loop. Disk writes still happen synchronously on that event loop.
reload_dataFalseReload the file whenever the data property is accessed. Can discard unsaved in-memory edits.
dumpBuilt-in compact serializerCustom callable taking (data, file_object). It replaces serialization, not loading.

Reading and writing

file.data exposes the loaded structure. Assigning data accepts only dictionaries/lists. file[key], assignment, deletion, membership, and iteration delegate to this structure. save() and reload() return None; reload() rereads disk without merging changes. Iteration uses the current memory state directly.

Autosave does not intercept every nested mutation. For example, file["items"].append(value) and nested deletion delegate directly to a list/dictionary and do not reliably trigger a write. Explicit save() after nested mutations is the predictable approach. Replacing the entire data value also does not recursively wrap the replacement immediately.

Current print(indent=2) only initializes its serialized value when auto_save=True; with defaults it raises an error. Use print(settings.data) or, when autosave is off, json.dumps(settings.data, indent=2).

Async queue utilities

Queue.add(coro) and Queue.add_sync(func, *args, **kwargs) return asyncio Tasks; await those tasks to observe completion and errors. await queue.add_and_wait(coro) runs a coroutine in the queue and returns its result. queue.qm() supplies a sync/async context manager that serializes entry within this process.

import asyncio
from jsondb2 import Queue

async def worker():
    await asyncio.sleep(0)
    return "finished"

async def main():
    queue = Queue()
    task = queue.add(worker())
    print(await task)

asyncio.run(main())

await queue waits for IDs that were registered when waiting started; await queue.wait_until_empty() waits while registered entries remain. Neither guarantees that newly scheduled tasks have entered the queue yet. JsonFile.save() discards the internally created Task when async_save=True, so it does not expose a reliable public flush/completion handle. Prefer the default synchronous save for code that must know persistence has completed.

AsyncProperty is a descriptor for async getters accessed with await obj.property; its wrapper also supports indexing before awaiting. Descriptor coroutine state is shared on the descriptor, so overlapping accesses are not a concurrency guarantee.

Persistence limits

Writes truncate and rewrite the live JSON file. There is no atomic replacement, transaction rollback, cross-process file lock, or merge between independent instances. Invalid JSON and file/serialization errors propagate to the caller. Use this wrapper for small local state with a single writer; the module updater's transactional installation does not change JsonFile's persistence behavior.

For relational data, see sqliteObj and aiosqliteObj.

Objects and method contracts

JsonFile is a file-backed container. Its fp identifies the file; auto_save, async_save, and reload_data store the constructor options. The data property exposes the actual in-memory dictionary or list, so changing it changes the wrapper's state. With autosave enabled, nested values can be internal container wrappers rather than plain dict/list objects.

OperationReturn / persistence
file[key]Returns the value (possibly a nested wrapper). Missing dictionary keys raise KeyError. In autosave mode, reads may also trigger a save after detecting changed in-memory state.
file[key] = value, del file[key]Mutate the loaded structure; save immediately only when autosave is enabled.
file.data = { ... }Replaces the entire root; accepts only dict or list. With autosave, saves the replacement.
file.save()Returns None. Default mode completes the full file rewrite before returning; async_save schedules it without returning the task.
file.reload()Returns None and replaces memory with file contents. Unsaved local changes are lost.
for key in fileIterates the current root: dictionary keys or list values. It does not implicitly merge external file edits.

Queue and QueueManager

Queue() tracks active entry IDs in q. active is a boolean property and len(queue) counts registered entries. The ID is registered when a scheduled task actually enters its QueueManager, so these values may still be empty immediately after queue.add(). QueueManager is normally obtained through queue.qm(); both with and async with serialize access, and waiting too long raises TimeoutError.

add(coro) takes an already-created coroutine; add_sync(func, *args, **kwargs) takes a callable plus its arguments. Both return Tasks, and awaited task results are the callable/coroutine's return value. Synchronous work is executed on the event-loop thread, so the queue does not make file writes nonblocking.

Worked example: autosave plus nested edits

This uses a temporary file and explicitly saves after append. That final save is deliberate: autosave reliably intercepts top-level assignments, but not all delegated list methods.

import os
import tempfile
from jsondb2 import JsonFile

with tempfile.TemporaryDirectory() as directory:
    path = os.path.join(directory, "preferences.json")
    preferences = JsonFile(path, auto_save=True)
    preferences["theme"] = "dark"
    preferences["recent"] = ["report.csv"]
    preferences["recent"].append("summary.csv")
    preferences.save()

    reread = JsonFile(path)
    print(reread.data)
    # {'theme': 'dark', 'recent': ['report.csv', 'summary.csv']}
    del preferences["theme"]
    reread.reload()
    print("theme" in reread)  # False

Worked example: await all queued work and errors

Keep the Task objects and await them with asyncio.gather. This observes completion even before those tasks have registered queue IDs, and propagates failures to the caller.

import asyncio
from jsondb2 import Queue

async def main():
    queue = Queue()
    events = []

    async def record(label):
        events.append("start " + label)
        await asyncio.sleep(0)
        events.append("end " + label)
        return label.upper()

    tasks = [queue.add(record(label)) for label in ("a", "b", "c")]
    results = await asyncio.gather(*tasks)
    print(results)  # ['A', 'B', 'C']
    print(events)  # Each task finishes its critical section before the next enters.

asyncio.run(main())

Custom serialization

A dump callback is useful with the default auto_save=False: pass dump=lambda data, stream: json.dump(data, stream, indent=2) for readable files. When autosave is enabled, a custom callback receives the internal nested wrappers and must handle them itself. Loading always uses json.load, so the callback must still produce valid JSON.

Complete source API

Generated from modules/jsondb2.py; version 0.1.0.1. Includes public functions, classes, directly declared methods, properties, and Python protocol methods. Conditional APIs may require optional dependencies. Inherited members and dynamically assigned attributes are explained in the guide where relevant.

Signatures and docstrings are extracted without importing or running the module. A property or AsyncProperty decorator changes how a member is accessed; see its decorators and the guide.

Classes and objects

class AsyncProperty

Source line 32

Construct: AsyncProperty(func)

Fields assigned by the constructor: coro, func. Some assignments may be conditional; see the object guide for meaning and lifecycle.

Declared functions, properties, and nested objects:

AsyncProperty.__init__(self, func)

Source line 33

ParameterPassing conventionDefault / required
funcpositional or keywordrequired
AsyncProperty.__await__(self)

Source line 37

No caller-supplied parameters are declared.

AsyncProperty.__get__(self, instance, owner)

Source line 39

ParameterPassing conventionDefault / required
instancepositional or keywordrequired
ownerpositional or keywordrequired
AsyncProperty.__set__(self, instance, value)

Source line 42

ParameterPassing conventionDefault / required
instancepositional or keywordrequired
valuepositional or keywordrequired
AsyncProperty.setter(self, func)

Source line 44

ParameterPassing conventionDefault / required
funcpositional or keywordrequired
class JsonFile

Source line 70

Construct: JsonFile(fp, **kwargs)

Fields assigned by the constructor: async_save, auto_save, dump, fp, reload_data. Some assignments may be conditional; see the object guide for meaning and lifecycle.

Declared functions, properties, and nested objects:

JsonFile.__init__(self, fp, **kwargs)

Source line 109

ParameterPassing conventionDefault / required
fppositional or keywordrequired
kwargsextra keyword arguments (**kwargs)optional collection
JsonFile.data(self)

Source line 138

Decorators: @property

No caller-supplied parameters are declared.

JsonFile.data(self, value)

Source line 143

Decorators: @data.setter

ParameterPassing conventionDefault / required
valuepositional or keywordrequired
JsonFile.save(self)

Source line 154

No caller-supplied parameters are declared.

JsonFile.reload(self)

Source line 159

No caller-supplied parameters are declared.

JsonFile.__contains__(self, value)

Source line 163

ParameterPassing conventionDefault / required
valuepositional or keywordrequired
JsonFile.__getitem__(self, key)

Source line 165

ParameterPassing conventionDefault / required
keypositional or keywordrequired
JsonFile.__setitem__(self, key, value)

Source line 171

ParameterPassing conventionDefault / required
keypositional or keywordrequired
valuepositional or keywordrequired
JsonFile.__delitem__(self, key)

Source line 178

ParameterPassing conventionDefault / required
keypositional or keywordrequired
JsonFile.__iter__(self)

Source line 182

No caller-supplied parameters are declared.

JsonFile.print(self, indent=2)

Source line 184

ParameterPassing conventionDefault / required
indentpositional or keyword2
class Queue

Source line 206

Construct: Queue()

Fields assigned by the constructor: q. Some assignments may be conditional; see the object guide for meaning and lifecycle.

Declared functions, properties, and nested objects:

Queue.__init__(self)

Source line 207

No caller-supplied parameters are declared.

Queue.qm(self)

Source line 209

No caller-supplied parameters are declared.

Queue.active(self)

Source line 212

Decorators: @property

No caller-supplied parameters are declared.

Queue.__len__(self)

Source line 214

No caller-supplied parameters are declared.

Queue.add(self, coro)

Source line 216

ParameterPassing conventionDefault / required
coropositional or keywordrequired
async Queue.add_and_wait(self, coro)

Source line 222

ParameterPassing conventionDefault / required
coropositional or keywordrequired

Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.

Queue.add_sync(self, func, *args, **kwargs)

Source line 226

ParameterPassing conventionDefault / required
funcpositional or keywordrequired
argsextra positional arguments (*args)optional collection
kwargsextra keyword arguments (**kwargs)optional collection
Queue.__await__(self)

Source line 232

No caller-supplied parameters are declared.

async Queue.wait_until_empty(self)

Source line 245

No caller-supplied parameters are declared.

Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.

class QueueManager

Source line 249

Construct: QueueManager(q)

Fields assigned by the constructor: q. Some assignments may be conditional; see the object guide for meaning and lifecycle.

Declared functions, properties, and nested objects:

QueueManager.__init__(self, q)

Source line 250

ParameterPassing conventionDefault / required
qpositional or keywordrequired
QueueManager.__enter__(self)

Source line 252

No caller-supplied parameters are declared.

QueueManager.__exit__(self, *args)

Source line 266

ParameterPassing conventionDefault / required
argsextra positional arguments (*args)optional collection
async QueueManager.__aenter__(self)

Source line 268

No caller-supplied parameters are declared.

Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.

async QueueManager.__aexit__(self, *args)

Source line 282

ParameterPassing conventionDefault / required
argsextra positional arguments (*args)optional collection

Declared with async def. Normal calls return an awaitable, or an async generator if the body yields. Decorated properties may be accessed differently; follow the object guide.