memorydb-django 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ """Django integration for MemoryDB: cache and session backends that drop into settings.py.
2
+
3
+ CACHES = {"default": {"BACKEND": "memorydb_django.MemoryDBCache",
4
+ "LOCATION": "memorydb://TOKEN@127.0.0.1:8080"}}
5
+ SESSION_ENGINE = "django.contrib.sessions.backends.memorydb" # via this package's session module
6
+
7
+ Values expire through the engine's own record expiry, not a swept key table:
8
+ an expired record stops being readable server-side.
9
+ """
10
+ from memorydb_django.cache import MemoryDBCache
11
+ from memorydb_django.sessions import SessionStore
12
+
13
+ __all__ = ["MemoryDBCache", "SessionStore"]
@@ -0,0 +1,118 @@
1
+ """Cache backend: Django's cache API on top of MemoryDB records.
2
+
3
+ settings.py:
4
+ CACHES = {"default": {
5
+ "BACKEND": "memorydb_django.MemoryDBCache",
6
+ "LOCATION": "memorydb://TOKEN@127.0.0.1:8080", # or set MEMORYDB_URL
7
+ "OPTIONS": {"collection": "django_cache"},
8
+ }}
9
+ """
10
+ import base64
11
+ import pickle
12
+ from datetime import datetime, timedelta, timezone
13
+
14
+ from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
15
+ from memorydb import MemoryDB, MemoryDBError
16
+
17
+ MAX_PAGE = 500
18
+
19
+
20
+ class MemoryDBCache(BaseCache):
21
+ def __init__(self, server, params):
22
+ super().__init__(params)
23
+ options = params.get("OPTIONS", {})
24
+ location = server if isinstance(server, str) else options.get("url", "")
25
+ if location.startswith("memorydb"):
26
+ self.db = MemoryDB.from_url(location)
27
+ elif location:
28
+ self.db = MemoryDB(location, token=options.get("token"))
29
+ else:
30
+ self.db = MemoryDB() # reads MEMORYDB_URL
31
+ self.collection = options.get("collection", "django_cache")
32
+ self._ensure_collection()
33
+
34
+ # -- engine plumbing ----------------------------------------------------
35
+ def _ensure_collection(self):
36
+ try:
37
+ self.db.collections.create(self.collection,
38
+ fields={"cache_key": {"type": "string", "required": True},
39
+ "payload": {"type": "string"}},
40
+ unique=["cache_key"], indexes=["cache_key"])
41
+ except MemoryDBError as exc:
42
+ if exc.status != 409: # already exists
43
+ raise
44
+
45
+ def _find(self, key):
46
+ page = self.db.request("POST", "query/" + self.collection + "/",
47
+ {"where": [{"field": "cache_key", "op": "eq", "value": key}], "limit": 1})
48
+ return page["results"][0] if page["results"] else None
49
+
50
+ def _write(self, key, value, timeout):
51
+ payload = base64.b64encode(pickle.dumps(value)).decode()
52
+ expires = None
53
+ deadline = self.get_backend_timeout(timeout)
54
+ if deadline:
55
+ expires = datetime.fromtimestamp(deadline, timezone.utc).isoformat()
56
+ for attempt in range(3):
57
+ record = self._find(key)
58
+ try:
59
+ if record is None:
60
+ self.db.request("POST", "documents/" + self.collection + "/",
61
+ {"data": {"cache_key": key, "payload": payload}, "expires": expires})
62
+ else:
63
+ updated = dict(record)
64
+ updated["data"] = {"cache_key": key, "payload": payload}
65
+ updated["expires"] = expires
66
+ self.db.request("PUT", "documents/" + self.collection + "/" + record["id"] + "/", updated)
67
+ return
68
+ except MemoryDBError as exc:
69
+ if exc.status != 409 or attempt == 2:
70
+ raise # concurrent writer bumped the version; re-read and retry
71
+
72
+ def _read(self, key):
73
+ record = self._find(key)
74
+ if record is None:
75
+ return None
76
+ return pickle.loads(base64.b64decode(record["data"]["payload"]))
77
+
78
+ # -- BaseCache contract ---------------------------------------------------
79
+ def get(self, key, default=None, version=None):
80
+ envelope = self._read(self.make_key(key, version=version))
81
+ if envelope is None:
82
+ return default
83
+ value, flags = envelope
84
+ return value if flags == "c" else default
85
+
86
+ def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
87
+ self._write(self.make_key(key, version=version), (value, "c"), timeout)
88
+
89
+ def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
90
+ full = self.make_key(key, version=version)
91
+ if self._read(full) is None:
92
+ self.set(key, value, timeout=timeout, version=version)
93
+ return True
94
+ return False
95
+
96
+ def delete(self, key, version=None):
97
+ record = self._find(self.make_key(key, version=version))
98
+ if record is None:
99
+ return
100
+ self.db.request("DELETE", "documents/" + self.collection + "/" + record["id"] + "/",
101
+ headers={"If-Match": str(record["version"])})
102
+
103
+ def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
104
+ full = self.make_key(key, version=version)
105
+ envelope = self._read(full)
106
+ if envelope is None:
107
+ return False
108
+ self._write(full, envelope, timeout)
109
+ return True
110
+
111
+ def clear(self):
112
+ while True:
113
+ page = self.db.request("POST", "query/" + self.collection + "/", {"limit": MAX_PAGE})
114
+ if not page["results"]:
115
+ return
116
+ for record in page["results"]:
117
+ self.db.request("DELETE", "documents/" + self.collection + "/" + record["id"] + "/",
118
+ headers={"If-Match": str(record["version"])})
@@ -0,0 +1,103 @@
1
+ """Session backend: django.contrib.sessions storage on MemoryDB records.
2
+
3
+ settings.py:
4
+ SESSION_ENGINE = "memorydb_django.sessions"
5
+
6
+ Sessions expire through the engine's own record expiry (SESSION_COOKIE_AGE);
7
+ expired sessions stop being readable server-side with no sweep cron.
8
+ """
9
+ from datetime import datetime, timedelta, timezone
10
+
11
+ from django.conf import settings
12
+ from django.contrib.sessions.backends.base import CreateError, SessionBase
13
+ from memorydb import MemoryDB, MemoryDBError
14
+
15
+
16
+ def _client():
17
+ location = getattr(settings, "MEMORYDB_URL", "")
18
+ if location.startswith("memorydb"):
19
+ return MemoryDB.from_url(location)
20
+ if location:
21
+ return MemoryDB(location, token=getattr(settings, "MEMORYDB_TOKEN", None))
22
+ return MemoryDB() # reads the MEMORYDB_URL env var
23
+
24
+
25
+ def _collection():
26
+ return getattr(settings, "MEMORYDB_SESSIONS_COLLECTION", "django_sessions")
27
+
28
+
29
+ class SessionStore(SessionBase):
30
+ def __init__(self, session_key=None):
31
+ super().__init__(session_key)
32
+ self.db = _client()
33
+ self.collection = _collection()
34
+ try:
35
+ self.db.collections.create(self.collection,
36
+ fields={"session_key": {"type": "string", "required": True},
37
+ "payload": {"type": "string"}},
38
+ unique=["session_key"], indexes=["session_key"])
39
+ except MemoryDBError as exc:
40
+ if exc.status != 409:
41
+ raise
42
+
43
+ def _find(self, session_key):
44
+ page = self.db.request("POST", "query/" + self.collection + "/",
45
+ {"where": [{"field": "session_key", "op": "eq", "value": session_key}], "limit": 1})
46
+ return page["results"][0] if page["results"] else None
47
+
48
+ def load(self):
49
+ record = self._find(self.session_key) if self.session_key else None
50
+ if record is None:
51
+ self._session_key = None
52
+ return {}
53
+ return self.decode(record["data"]["payload"])
54
+
55
+ def exists(self, session_key):
56
+ return self._find(session_key) is not None
57
+
58
+ def create(self):
59
+ while True:
60
+ self._session_key = self._get_new_session_key()
61
+ try:
62
+ self.save(must_create=True)
63
+ except CreateError:
64
+ continue # key collision; try another
65
+ self.modified = True
66
+ return
67
+
68
+ def save(self, must_create=False):
69
+ if self.session_key is None:
70
+ return self.create()
71
+ payload = self.encode(self._get_session(no_load=must_create))
72
+ expires = (datetime.now(timezone.utc) + timedelta(seconds=self.get_expiry_age())).isoformat()
73
+ for attempt in range(3):
74
+ record = self._find(self.session_key)
75
+ try:
76
+ if record is None:
77
+ self.db.request("POST", "documents/" + self.collection + "/",
78
+ {"data": {"session_key": self.session_key, "payload": payload}, "expires": expires})
79
+ return
80
+ if must_create:
81
+ raise CreateError
82
+ updated = dict(record)
83
+ updated["data"] = {"session_key": self.session_key, "payload": payload}
84
+ updated["expires"] = expires
85
+ self.db.request("PUT", "documents/" + self.collection + "/" + record["id"] + "/", updated)
86
+ return
87
+ except MemoryDBError as exc:
88
+ if exc.status != 409 or attempt == 2:
89
+ raise
90
+
91
+ def delete(self, session_key=None):
92
+ if session_key is None:
93
+ if self.session_key is None:
94
+ return
95
+ session_key = self.session_key
96
+ record = self._find(session_key)
97
+ if record is not None:
98
+ self.db.request("DELETE", "documents/" + self.collection + "/" + record["id"] + "/",
99
+ headers={"If-Match": str(record["version"])})
100
+
101
+ @classmethod
102
+ def clear_expired(cls):
103
+ pass # expiry is enforced by the engine at read time; nothing to sweep
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: memorydb-django
3
+ Version: 0.2.1
4
+ Summary: Django cache and session backends for MemoryDB (SQLite-style drop-in for the settings file)
5
+ Author: humaniods
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/humaniods/MemoryDB
8
+ Project-URL: Repository, https://github.com/humaniods/MemoryDB
9
+ Project-URL: Issues, https://github.com/humaniods/MemoryDB/issues
10
+ Keywords: django,cache,sessions,database,agent-memory
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: Django
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Database
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: superb-memory
20
+ Requires-Dist: Django>=4.2
21
+
22
+ # memorydb-django
23
+
24
+ Django cache and session backends for [MemoryDB](https://github.com/humaniods/MemoryDB).
25
+ Two lines in `settings.py`, the way `sqlite3` is a drop-in for Django itself — no models, no
26
+ migrations, no new code in your views.
27
+
28
+ ```bash
29
+ pip install memorydb-django
30
+ ```
31
+
32
+ ```python
33
+ # settings.py
34
+ CACHES = {"default": {
35
+ "BACKEND": "memorydb_django.MemoryDBCache",
36
+ "LOCATION": "memorydb://TOKEN@127.0.0.1:8080", # or leave empty and set MEMORYDB_URL
37
+ }}
38
+
39
+ SESSION_ENGINE = "memorydb_django.sessions"
40
+ ```
41
+
42
+ Then use Django's own API:
43
+
44
+ ```python
45
+ from django.core.cache import cache
46
+ cache.set("hero", {"name": "Karna"}, timeout=30) # one record; the TTL is the record's expiry
47
+ cache.get("hero")
48
+
49
+ request.session["user_id"] = 42 # SESSION_COOKIE_AGE enforced server-side
50
+ request.session.save()
51
+ ```
52
+
53
+ Details worth knowing:
54
+
55
+ - Each cache key and each session is one MemoryDB document in the `django_cache` /
56
+ `django_sessions` collection (typed, unique and indexed; created on first use).
57
+ - `timeout=` and `SESSION_COOKIE_AGE` are stored as the record's own `expires` field, so the
58
+ server stops serving them at the deadline. No cleanup cron, and `clear_expired()` is a no-op.
59
+ - Concurrent writers use optimistic versions and retry, so there are no row locks or deadlocks.
60
+ - Set `MEMORYDB_SESSIONS_COLLECTION` / `OPTIONS["collection"]` to move either store elsewhere, so
61
+ two Django projects on one server stay apart.
62
+ - Session values are stored through Django's own `SESSION_SERIALIZER` and signed with your
63
+ `SECRET_KEY`, exactly as the database-backed store signs them, so sessions your site already
64
+ issued keep working when you switch engines.
65
+
66
+ This is a cache/session backend, **not** a `django.db` backend — models, migrations, the admin
67
+ and ORM queries still need Postgres or SQLite.
@@ -0,0 +1,7 @@
1
+ memorydb_django/__init__.py,sha256=fU3YdPMQKObshfr9EJjC8eXgl4iTlaMJqzgm0PsopqM,615
2
+ memorydb_django/cache.py,sha256=XqyocQ5ONb4fBH_dOGx-Gua7EdTFbSTS62qzg3_DnLY,4978
3
+ memorydb_django/sessions.py,sha256=xF2LfiJ7gcKIGgvYuMQQBPjOWFE8Uekm77Tj2wk_Yis,4116
4
+ memorydb_django-0.2.1.dist-info/METADATA,sha256=586LRfQt-LC2jLVJsS5dS1X9NAo5CvHSWix5p8ws9Xg,2633
5
+ memorydb_django-0.2.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ memorydb_django-0.2.1.dist-info/top_level.txt,sha256=cRI1VYHXafs6ultCQqUNJguGnMIv4dDnDJVbPSz4xu0,16
7
+ memorydb_django-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ memorydb_django