<head>
<meta charset="UTF-8">
<title>mongoKV – Tiny Redis-style Key-Value Store for MongoDB (Python)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- SEO Meta Tags -->
<meta name="description" content="mongoKV is a tiny async/sync key-value store wrapper for MongoDB using PyMongo. One collection per instance, works synchronously and asynchronously with the same API.">
<meta name="keywords" content="mongodb, key-value store, pymongo, python, async, sync, database, nosql, kv store">
<meta name="author" content="Harrison">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://patx.github.io/mongokv/">
<meta property="og:title" content="mongoKV – Tiny Key-Value Store for MongoDB">
<meta property="og:description" content="Async/sync key-value store on top of PyMongo. One collection per instance, one document per key. Intentionally small, stable, and boring.">
<meta property="og:image" content="https://raw.githubusercontent.com/patx/mongokv/refs/heads/main/docs/logo.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://patx.github.io/mongokv/">
<meta name="twitter:title" content="mongoKV – Tiny Key-Value Store for MongoDB">
<meta name="twitter:description" content="Async/sync key-value store on top of PyMongo. One collection per instance, one document per key.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/patx/mongokv/refs/heads/main/docs/logo.png">
<meta name="twitter:creator" content="@harrisonerd">
<!-- Favicon (using your logo) -->
<link rel="icon" type="image/png" href="https://raw.githubusercontent.com/patx/mongokv/refs/heads/main/docs/logo.png">
<link rel="apple-touch-icon" href="https://raw.githubusercontent.com/patx/mongokv/refs/heads/main/docs/logo.png">
<!-- Canonical URL -->
<link rel="canonical" href="https://patx.github.io/mongokv/">
<!-- Theme Color -->
<meta name="theme-color" content="#24663c">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
<!-- highlight.js -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github.min.css">
<script defer
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
<script defer
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/python.min.js"></script>
<script defer
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/bash.min.js"></script>
<script defer
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/languages/json.min.js"></script>
<script defer>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("pre code").forEach(el => hljs.highlightElement(el));
const nav = document.getElementById("topnav");
const onScroll = () => nav.classList.toggle("scrolled", window.scrollY > 2);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
});
</script>
<style>
:root {
--bg: #f5f5f5;
--fg: #111827;
--muted: #6b7280;
--accent: #24663c;
--border: #e5e7eb;
--code-bg: #c4cfc6;
--code-fg: black;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html { scroll-behavior: smooth; }
body {
margin: 0;
padding: 0;
font-family: var(--sans);
background: var(--bg);
color: var(--fg);
line-height: 1.6;
}
.page {
max-width: 840px;
margin: 0 auto;
padding: 32px 16px 64px;
}
header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
h1, h2, h3, h4 {
font-weight: 700;
letter-spacing: -0.02em;
}
h1 {
font-size: 4rem;
margin: 0 0 4px;
}
.logo {
font-family: 'Pacifico', cursive;
color: var(--accent);
letter-spacing: 3px;
}
.subtitle {
color: var(--muted);
margin: 0 0 12px;
}
.pronounce {
color: var(--muted);
font-size: 18px;
font-weight: 400;
white-space: nowrap;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
p { margin: 0 0 1rem; }
ul, ol {
margin: 0 0 1rem 1.5rem;
padding: 0;
}
code {
font-family: var(--mono);
font-size: 0.9em;
background: var(--border);
padding: 0.1em 0.3em;
border-radius: 4px;
}
pre {
margin: 0 0 1.25rem;
padding: 12px 14px;
background: var(--code-bg);
color: var(--code-fg);
border-radius: 8px;
overflow-x: auto;
font-family: var(--mono);
font-size: 0.9rem;
}
pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
pre code.hljs,
.hljs {
background: var(--code-bg) !important;
color: var(--code-fg) !important;
}
.section {
margin-bottom: 32px;
border-left: 3px solid var(--border);
padding: 10px;
}
.tagline {
font-size: 0.95rem;
color: var(--muted);
margin-top: 8px;
}
.api-heading {
margin-top: 32px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.callout-list {
list-style: none;
margin: 0 0 1rem;
padding: 0;
}
.callout-list li::before {
content: "•";
margin-right: 0.5rem;
color: var(--accent);
}
.callout-list li { margin-bottom: 0.25rem; }
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 24px;
}
.header-content nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 24px;
}
.header-content nav a {
color: var(--fg);
text-decoration: none;
font-weight: 500;
}
.header-content nav a:hover {
color: var(--accent);
text-decoration: underline;
}
.navbar{
position: sticky;
top: 0;
z-index: 9999;
background: rgba(245,245,245,.82);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
.nav-inner{
max-width: 840px;
margin: 0 auto;
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.brand{
display: inline-flex;
align-items: baseline;
gap: 10px;
text-decoration: none;
}
.brand-logo{
font-family: 'Pacifico', cursive;
color: var(--accent);
letter-spacing: 2px;
font-size: 1.25rem;
line-height: 1;
}
.nav-links{
list-style: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.nav-links a{
display: inline-flex;
align-items: center;
height: 36px;
padding: 0 12px;
border-radius: 999px;
color: var(--fg);
text-decoration: none;
font-weight: 600;
font-size: 0.95rem;
transition: transform .15s ease, background .15s ease, color .15s ease, box-shadow .15s ease;
}
.nav-links a:hover{
background: rgba(36,102,60,.10);
color: var(--accent);
box-shadow: 0 4px 14px rgba(0,0,0,.06);
transform: translateY(-1px);
}
/* NEW: active section highlight */
.nav-links a.active{
background: rgba(36,102,60,.16);
color: var(--accent);
box-shadow: 0 8px 18px rgba(0,0,0,.08);
}
.navbar.scrolled{
box-shadow: 0 10px 24px rgba(0,0,0,.08);
}
/* Hide brand logo until scrolled past header */
.brand-logo {
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
}
/* Show brand logo after scrolling */
.navbar.show-logo .brand-logo {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
</style>
</head>
<body>
<nav class="navbar" id="topnav">
<div class="nav-inner">
<a class="brand" href="#top">
<span class="brand-logo">mongoKV</span>
</a>
<ul class="nav-links">
<li><a href="#guide" data-spy="guide">Guide</a></li>
<li><a href="#api" data-spy="api">API</a></li>
<li><a href="https://github.com/patx/mongokv" target="_blank" rel="noreferrer">GitHub</a></li>
<li><a href="https://pypi.org/project/mongokv/" target="_blank" rel="noreferrer">PyPI</a></li>
</ul>
</div>
</nav>
<div class="page" id="top">
<header>
<div class="header-content">
<div>
<h1><span class="logo">mongoKV</span> <span class="pronounce">/ˈmɑːŋɡoʊ kiː ˈvæljuː/</span></h1>
<p class="subtitle">Tiny async/sync key–value store on top of PyMongo.</p>
<p class="tagline">
<a href="https://harrisonerd.com/" target="_blank" rel="noopener noreferrer">Harrison Erd</a>
· BSD 3-Clause
</p>
</div>
</div>
</header>
<section class="section" style="border: 0px;">
<h2 id="guide">User Guide</h2>
<p><code>mongoKV</code> is a tiny key–value store wrapper around MongoDB using PyMongo.</p>
<ul class="callout-list">
<li>One collection per instance, one document per key</li>
<li>Works synchronously and asynchronously with the same API</li>
<li>Stores any BSON-serializable Python object as the value, keys are strings</li>
<li>Automatically generates IDs when you don’t care about the key</li>
<li>Fully tested. MongoDB handles locking, durability, and concurrency.</li>
<li>This project is intentionally small, stable, and boring. Breaking changes are avoided</li>
<li>mongoKV is not a cache or a Redis replacement — it’s a simple, durable KV API on top of MongoDB</li>
</ul>
<p>Think of it as <em>MongoDB as a dict instead of a database, for lazy programmers who just want to save some simple data.</em></p>
</section>
<section class="section" style="border: 0px;">
<h3>Installation</h3>
<pre><code class="language-bash">pip install mongokv</code></pre>
<p>You’ll also need a running MongoDB instance and a valid connection URI, for example:</p>
<pre><code class="language-bash">mongodb://localhost:27017</code></pre>
<p>Check out <a href="https://www.mongodb.com/resources/products/fundamentals/mongodb-connection-string">An Introduction to MongoDB Connection Strings</a>
for help on getting a MongoDB instance up and running. A super easy way to do this is to use <a href="https://www.mongodb.com/products/platform/atlas-database">Atlas</a>,
that way your instance is in the cloud and you won't need a Mongo server running on your local device.</p>
</section>
<section class="section" style="border: 0px;">
<h3>Core Concepts</h3>
<p>Each key is stored as a single MongoDB document:</p>
<pre><code class="language-json">{
"_id": "<str(key)>",
"value": <your_data>
}</code></pre>
<ul>
<li>Keys are always stored as strings (<code>str(key)</code>), but you can pass any object with a sensible <code>str()</code>.</li>
<li>Values must be BSON-serializable (anything PyMongo can store: dicts, lists, ints, strings, datetimes, etc.).</li>
<li>This is a normal MongoDB collection. You can always access the same data directly using PyMongo as your needs grow — no migrations required.</li>
<li>Safe across threads, processes, and ASGI workers — MongoDB handles locking and atomicity.</li>
<li>The library auto-detects whether it’s running inside an <code>asyncio</code> event loop:
<ul>
<li><strong>Inside async code</strong>: methods return <strong>coroutines</strong> → you must <code>await</code> them.</li>
<li><strong>Outside async code</strong>: methods run <strong>synchronously</strong> and return results directly.</li>
</ul>
</li>
</ul>
<p>Same methods, two modes. Start simple, grow into MongoDB when you need more.</p>
</section>
<section class="section" style="border: 0px;">
<h3>Quickstart</h3>
<h4>Synchronous usage</h4>
<pre><code class="language-python">from mongokv import Mkv
db = Mkv("mongodb://localhost:27017")
# Set a value with an explicit key
db.set("username", "harrison")
# Get it back
print(db.get("username")) # -> "harrison"
# Set a value with an auto-generated key
note_id = db.set(None, {"title": "mongoKV", "tags": ["mongo", "kv"]})
print(note_id) # -> e.g. "677eec65e92eeca23513fe99"
# List all keys
print(db.all()) # -> ["username", "677eec65e92eeca23513fe99", ...]
# Remove a key
db.remove("username")
# Clear everything
db.purge()
# Close connections when you’re done
db.close()</code></pre>
<h4>Asynchronous usage</h4>
<pre><code class="language-python">import asyncio
from mongokv import Mkv
db = Mkv("mongodb://localhost:27017")
async def main():
# Set a value with explicit key
await db.set("counter", 1)
# Increment and save again
current = await db.get("counter", 0)
await db.set("counter", current + 1)
# Auto-generated key
new_id = await db.set(None, {"hello": "world"})
print("New id:", new_id)
# List all keys
keys = await db.all()
print("Keys:", keys)
# Remove key
deleted = await db.remove("counter")
print("Deleted:", deleted)
# Purge all keys
await db.purge()
# Close connections
await db.close()
asyncio.run(main())</code></pre>
</section>
<section class="section api-heading" style="border: 0px;">
<h2 id="api">API Reference</h2>
</section>
<section class="section">
<h3><code>class Mkv</code></h3>
<p>Create a new mongoKV instance.</p>
<h4>Parameters</h4>
<ul>
<li><code>mongo_uri</code> (<code>str</code>): MongoDB connection string.</li>
<li><code>db_name</code> (<code>str</code>, default <code>"mkv"</code>): Database name.</li>
<li><code>collection_name</code> (<code>str</code>, default <code>"kv"</code>): Collection name.</li>
</ul>
<p>Internally it maintains:</p>
<ul>
<li>An async client & collection:
<ul>
<li><code>self._async_client = AsyncMongoClient(mongo_uri)</code></li>
<li><code>self.db</code>, <code>self.collection</code></li>
</ul>
</li>
<li>A sync client & collection:
<ul>
<li><code>self._sync_client = MongoClient(mongo_uri)</code></li>
<li><code>self._sync_db</code>, <code>self._sync_collection</code></li>
</ul>
</li>
</ul>
<p>These are used automatically based on sync/async context.</p>
</section>
<section class="section">
<h3><code>set</code></h3>
<p>Set a value for a given key. If <code>key</code> is <code>None</code>, a new ObjectId-based key will be generated.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>: executes immediately, returns the string key.</li>
<li><strong>Async</strong>: returns a coroutine; awaiting it yields the string key.</li>
</ul>
<h4>Parameters</h4>
<ul>
<li><code>key</code> (<code>str</code> or <code>None</code>):
<ul>
<li>If not <code>None</code>: converted to <code>str(key)</code> and used as <code>_id</code>.</li>
<li>If <code>None</code>: a new <code>ObjectId()</code> is generated and used as <code>_id</code>.</li>
</ul>
</li>
<li><code>value</code> (<code>Any</code>): Any BSON-serializable object to store.</li>
</ul>
<h4>Returns</h4>
<p><code>str</code>: The key used for this record.</p>
<h4>Examples (sync)</h4>
<pre><code class="language-python">db.set("user:1", {"name": "Alice"})
auto_id = db.set(None, {"name": "Bob"})</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python">key = await db.set("session:123", {"state": "active"})
new_id = await db.set(None, {"foo": "bar"})</code></pre>
</section>
<section class="section">
<h3><code>get</code></h3>
<p>Fetch the value for a given key.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>: executes immediately.</li>
<li><strong>Async</strong>: returns a coroutine; awaiting it yields the result.</li>
<li><strong>Missing keys</strong>:
<ul>
<li>If <code>default</code> is <em>not</em> provided, a missing key raises <code>KeyError</code>.</li>
<li>If <code>default</code> <em>is</em> provided (even <code>None</code>), that value is returned when missing.</li>
</ul>
</li>
</ul>
<h4>Parameters</h4>
<ul>
<li><code>key</code> (<code>str</code>): Key to look up (converted to <code>str(key)</code> for <code>_id</code>).</li>
<li><code>default</code> (<code>Any</code>, optional): Fallback to return if the key does not exist. If omitted, missing keys raise <code>KeyError</code>.</li>
</ul>
<h4>Returns</h4>
<p>The stored value. If the key is missing, returns <code>default</code> if provided, otherwise raises <code>KeyError</code>.</p>
<h4>Examples (sync)</h4>
<pre><code class="language-python"># strict (dict-like)
try:
user = db.get("user:1")
except KeyError:
user = None
# fallback behavior
user = db.get("user:1", default={})
missing = db.get("nope", default="N/A") # -> "N/A"
none_ok = db.get("nope", default=None) # -> None</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python"># strict (dict-like)
try:
user = await db.get("user:1")
except KeyError:
user = None
# fallback behavior
user = await db.get("user:1", default={})
none_ok = await db.get("nope", default=None)</code></pre>
</section>
<section class="section">
<h3><code>remove</code></h3>
<p>Delete a single document by key.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>: executes immediately, returns <code>True</code> if a document was deleted, <code>False</code> otherwise.</li>
<li><strong>Async</strong>: returns a coroutine; awaiting it yields <code>True</code> or <code>False</code>.</li>
</ul>
<h4>Parameters</h4>
<ul>
<li><code>key</code> (<code>str</code>): Key to remove (converted to <code>str(key)</code> for <code>_id</code>).</li>
</ul>
<h4>Returns</h4>
<p><code>bool</code>: <code>True</code> if a document was deleted, <code>False</code> otherwise.</p>
<h4>Examples (sync)</h4>
<pre><code class="language-python">deleted = db.remove("user:1")
if deleted:
print("User removed")</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python">deleted = await db.remove("user:1")</code></pre>
</section>
<section class="section">
<h3><code>all</code></h3>
<p>List all keys (all <code>_id</code> values) in the collection.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>: executes immediately, returns a <code>list[str]</code>.</li>
<li><strong>Async</strong>: returns a coroutine; awaiting it yields a <code>list[str]</code>.</li>
</ul>
<h4>Returns</h4>
<p><code>list[str]</code>: All keys currently stored.</p>
<h4>Examples (sync)</h4>
<pre><code class="language-python">keys = db.all()
print(keys) # -> ["user:1", "note:abc", ...]</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python">keys = await db.all()</code></pre>
</section>
<section class="section">
<h3><code>purge</code></h3>
<p>Delete <strong>all</strong> documents in the collection. Use with care.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>: executes immediately, returns <code>True</code> once the operation completes.</li>
<li><strong>Async</strong>: returns a coroutine; awaiting it yields <code>True</code>.</li>
</ul>
<h4>Returns</h4>
<p><code>bool</code>: Always <code>True</code> if no exception is raised.</p>
<h4>Examples (sync)</h4>
<pre><code class="language-python">db.purge() # All keys gone</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python">await db.purge()</code></pre>
</section>
<section class="section">
<h3><code>close</code></h3>
<p>Close both the async and sync MongoDB clients.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Sync</strong>:
<ul>
<li>Calls <code>self._async_client.close()</code> using <code>asyncio.run()</code>.</li>
<li>Calls <code>self._sync_client.close()</code>.</li>
<li>Returns <code>None</code>.</li>
</ul>
</li>
<li><strong>Async</strong>: returns a coroutine that:
<ul>
<li><code>await self._async_client.close()</code></li>
<li><code>self._sync_client.close()</code></li>
<li>Awaited result is <code>None</code>.</li>
</ul>
</li>
</ul>
<h4>Returns</h4>
<p><code>None</code></p>
<h4>Examples (sync)</h4>
<pre><code class="language-python">db.close()</code></pre>
<h4>Examples (async)</h4>
<pre><code class="language-python">await db.close()</code></pre>
<p>Closing is optional in short-lived scripts, but recommended in long-running apps and tests.</p>
</section>
<section class="section">
<h3>When is it sync vs async?</h3>
<p>The library uses:</p>
<pre><code class="language-python">def in_async() -> bool:
try:
asyncio.get_running_loop()
return True
except RuntimeError:
return False</code></pre>
<ul>
<li>If there is a running event loop, methods like <code>set</code>, <code>get</code>, etc. return <strong>coroutines</strong>.</li>
<li>If there is no running loop, they execute synchronously.</li>
<li>If you do not await your method calls in an async context there will be an error because your a returning a non-awaited coroutine.</li>
</ul>
</section>
<section class="section">
<h3>Error Handling</h3>
<p>The current implementation doesn’t wrap MongoDB errors – any connection or operation issue will bubble up as a PyMongo/AsyncMongo exception. Typical things you might see:</p>
<ul>
<li>Connection failures</li>
<li>Serialization errors for non-BSON-compatible values</li>
<li>Authentication/authorization errors if your URI is locked down</li>
</ul>
<p>You can catch them at the call site:</p>
<pre><code class="language-python">from pymongo.errors import PyMongoError
try:
db.set("x", object())
except PyMongoError as e:
print("Mongo blew up:", e)</code></pre>
</section>
</div>
<script defer>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("pre code").forEach(el => hljs.highlightElement(el));
const nav = document.getElementById("topnav");
const header = document.querySelector("header");
const guideEl = document.getElementById("guide");
const apiEl = document.getElementById("api");
const guideLink = document.querySelector('.nav-links a[href="#guide"]');
const apiLink = document.querySelector('.nav-links a[href="#api"]');
const clearActive = () => {
[guideLink, apiLink].forEach(a => a && a.classList.remove("active"));
};
const setActive = (which) => {
clearActive();
if (which === "guide" && guideLink) guideLink.classList.add("active");
if (which === "api" && apiLink) apiLink.classList.add("active");
};
// Decide which section should be active RIGHT NOW (when spy is enabled)
const computeActiveSection = () => {
if (!guideEl || !apiEl) return null;
const y = 90; // "reading line" just below sticky nav
const g = guideEl.getBoundingClientRect();
const a = apiEl.getBoundingClientRect();
const guidePassed = g.top <= y;
const apiPassed = a.top <= y;
if (apiPassed) return "api";
if (guidePassed) return "guide";
return null;
};
let spyEnabled = false;
const syncNavState = () => {
const headerBottom = header.getBoundingClientRect().bottom;
const shouldEnable = headerBottom <= 0;
nav.classList.toggle("scrolled", window.scrollY > 2);
nav.classList.toggle("show-logo", shouldEnable);
if (!shouldEnable) {
spyEnabled = false;
clearActive();
return;
}
spyEnabled = true;
const which = computeActiveSection();
if (which) setActive(which);
else clearActive();
};
syncNavState();
window.addEventListener("scroll", syncNavState, { passive: true });
window.addEventListener("resize", syncNavState, { passive: true });
const onNavClick = (which) => (e) => {
if (!spyEnabled) return;
setActive(which);
};
if (guideLink) guideLink.addEventListener("click", onNavClick("guide"));
if (apiLink) apiLink.addEventListener("click", onNavClick("api"));
if ("IntersectionObserver" in window && guideEl && apiEl) {
const observer = new IntersectionObserver(() => {
if (!spyEnabled) return;
const which = computeActiveSection();
if (which) setActive(which);
else clearActive();
}, {
root: null,
rootMargin: "-72px 0px -55% 0px",
threshold: [0, 0.1, 0.25, 0.5]
});
observer.observe(guideEl);
observer.observe(apiEl);
}
});
</script>
</body>
</html>