Skip to content

Commit 0f7ac60

Browse files
bug/Permit deleting db entries with leading slashes (#218)
* poetry lock * Formatting * Avoid object has no attribute '_refresh_timer' When called without a refresh function we end up getting AttributeError: 'AsyncDatabase' object has no attribute '_refresh_timer' on attempted close. Fix this. * Revert #172 This was a good first attempt, but still prevented users from deleting the keys they'd created * Switch delete method to use new multipart/form-data delete method that supports deleting keys with leading slashes * Adjusting tests to reflect the gap in getting slashy keys
1 parent f8651c7 commit 0f7ac60

File tree

9 files changed

+962
-712
lines changed

9 files changed

+962
-712
lines changed

poetry.lock

Lines changed: 898 additions & 684 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/replit/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""CLI for interacting with your Repl's DB. Written as top-level script."""
2+
23
import json
34

45
import click

src/replit/database/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Interface with the Replit Database."""
2+
23
from typing import Any
34

45
from . import default_db

src/replit/database/database.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from aiohttp_retry import ExponentialRetry, RetryClient # type: ignore
2121
import requests
2222
from requests.adapters import HTTPAdapter, Retry
23+
from urllib3.filepost import encode_multipart_formdata
2324

2425

2526
def to_primitive(o: Any) -> Any:
@@ -62,18 +63,6 @@ def dumps(val: Any) -> str:
6263
_dumps = dumps
6364

6465

65-
def _sanitize_key(key: str) -> str:
66-
"""Strip slashes from the beginning of keys.
67-
68-
Args:
69-
key (str): The key to strip
70-
71-
Returns:
72-
str: The stripped key
73-
"""
74-
return key.lstrip("/")
75-
76-
7766
class AsyncDatabase:
7867
"""Async interface for Replit Database.
7968
@@ -123,6 +112,8 @@ def __init__(
123112
if self._get_db_url:
124113
self._refresh_timer = threading.Timer(3600, self._refresh_db)
125114
self._refresh_timer.start()
115+
else:
116+
self._refresh_timer = None
126117
watched_thread = threading.main_thread()
127118
self._watchdog_timer = threading.Timer(1, self._watchdog, args=[watched_thread])
128119
self._watchdog_timer.start()
@@ -228,7 +219,6 @@ async def set_bulk_raw(self, values: Dict[str, str]) -> None:
228219
Args:
229220
values (Dict[str, str]): The key-value pairs to set.
230221
"""
231-
values = {_sanitize_key(k): v for k, v in values.items()}
232222
async with self.client.post(self.db_url, data=values) as response:
233223
response.raise_for_status()
234224

@@ -241,8 +231,9 @@ async def delete(self, key: str) -> None:
241231
Raises:
242232
KeyError: Key does not exist
243233
"""
234+
body, content_type = encode_multipart_formdata({"key": key})
244235
async with self.client.delete(
245-
self.db_url + "/" + urllib.parse.quote(key)
236+
self.db_url, data=body, headers={"Content-Type": content_type}
246237
) as response:
247238
if response.status == 404:
248239
raise KeyError(key)
@@ -550,6 +541,8 @@ def __init__(
550541
if self._get_db_url:
551542
self._refresh_timer = threading.Timer(3600, self._refresh_db)
552543
self._refresh_timer.start()
544+
else:
545+
self._refresh_timer = None
553546
watched_thread = threading.main_thread()
554547
self._watchdog_timer = threading.Timer(1, self._watchdog, args=[watched_thread])
555548
self._watchdog_timer.start()
@@ -685,7 +678,6 @@ def set_bulk_raw(self, values: Dict[str, str]) -> None:
685678
Args:
686679
values (Dict[str, str]): The key-value pairs to set.
687680
"""
688-
values = {_sanitize_key(k): v for k, v in values.items()}
689681
r = self.sess.post(self.db_url, data=values)
690682
r.raise_for_status()
691683

@@ -698,7 +690,10 @@ def __delitem__(self, key: str) -> None:
698690
Raises:
699691
KeyError: Key is not set
700692
"""
701-
r = self.sess.delete(self.db_url + "/" + urllib.parse.quote(key))
693+
body, content_type = encode_multipart_formdata({"key": key})
694+
r = self.sess.delete(
695+
self.db_url, data=body, headers={"Content-Type": content_type}
696+
)
702697
if r.status_code == 404:
703698
raise KeyError(key)
704699

src/replit/database/default_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""A module containing the default database."""
2+
23
import os
34
import os.path
45
from typing import Any, Optional

src/replit/database/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""A module containing a database proxy implementation."""
2+
23
from typing import Any
34
from urllib.parse import quote
45

src/replit/info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Information about your repl."""
2+
23
import os
34
from typing import Optional
45

src/replit/web/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Utilities for working with user mappings."""
2+
23
from collections.abc import Mapping, MutableMapping
34
from typing import Any, Iterator, Optional
45

tests/test_database.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,30 +129,48 @@ async def test_slash_keys(self) -> None:
129129
"""Test that slash keys work."""
130130
k = "/key"
131131
# set
132-
await self.db.set(k,"val1")
133-
self.assertEqual(await self.db.get(k), "val1")
132+
await self.db.set(k, "val1")
133+
# TODO: Getting slash keys is currently not supported
134+
# See https://github.com/replit/replit-py/pull/218#discussion_r1588295348
135+
# self.assertEqual(await self.db.get(k), "val1")
136+
self.assertEqual(list(await self.db.list("/")), [k])
134137
await self.db.delete(k)
138+
# TODO: Getting slash keys is currently not supported
139+
# KeyError is the same though, so it can stay.
135140
with self.assertRaises(KeyError):
136141
await self.db.get(k)
137142
# set_raw
138-
await self.db.set_raw(k,"val1")
139-
self.assertEqual(await self.db.get_raw(k), "val1")
143+
await self.db.set_raw(k, "val1")
144+
# TODO: Getting slash keys is currently not supported
145+
# self.assertEqual(await self.db.get_raw(k), "val1")
146+
self.assertEqual(list(await self.db.list("/")), [k])
140147
await self.db.delete(k)
148+
# TODO: Getting slash keys is currently not supported.
149+
# KeyError is the same though, so it can stay.
141150
with self.assertRaises(KeyError):
142151
await self.db.get(k)
143152
# set_bulk
144153
await self.db.set_bulk({k: "val1"})
145-
self.assertEqual(await self.db.get(k), "val1")
154+
# TODO: Getting slash keys is currently not supported
155+
# self.assertEqual(await self.db.get(k), "val1")
156+
self.assertEqual(list(await self.db.list("/")), [k])
146157
await self.db.delete(k)
158+
# TODO: Getting slash keys is currently not supported
159+
# KeyError is the same though, so it can stay.
147160
with self.assertRaises(KeyError):
148161
await self.db.get(k)
149162
# set_bulk_raw
150163
await self.db.set_bulk_raw({k: "val1"})
151-
self.assertEqual(await self.db.get_raw(k), "val1")
164+
# TODO: Getting slash keys is currently not supported
165+
# self.assertEqual(await self.db.get_raw(k), "val1")
166+
self.assertEqual(list(await self.db.list("/")), [k])
152167
await self.db.delete(k)
168+
# TODO: Getting slash keys is currently not supported
169+
# KeyError is the same though, so it can stay.
153170
with self.assertRaises(KeyError):
154171
await self.db.get(k)
155172

173+
156174
class TestDatabase(unittest.TestCase):
157175
"""Tests for replit.database.Database."""
158176

@@ -291,26 +309,43 @@ def test_slash_keys(self) -> None:
291309
"""Test that slash keys work."""
292310
k = "/key"
293311
# set
294-
self.db.set(k,"val1")
295-
self.assertEqual(self.db[k], "val1")
312+
self.db.set(k, "val1")
313+
# TODO: Getting slash keys is currently not supported
314+
# See https://github.com/replit/replit-py/pull/218#discussion_r1588295348
315+
# self.assertEqual(self.db[k], "val1")
316+
self.assertEqual(list(self.db.keys()), [k])
296317
del self.db[k]
318+
# TODO: Getting slash keys is currently not supported
319+
# KeyError is the same though, so it can stay.
297320
with self.assertRaises(KeyError):
298321
self.db[k]
299322
# set_raw
300-
self.db.set_raw(k,"val1")
301-
self.assertEqual(self.db.get_raw(k), "val1")
323+
self.db.set_raw(k, "val1")
324+
# TODO: Getting slash keys is currently not supported
325+
# self.assertEqual(self.db.get_raw(k), "val1")
326+
self.assertEqual(list(self.db.keys()), [k])
302327
del self.db[k]
328+
# TODO: Getting slash keys is currently not supported
329+
# KeyError is the same though, so it can stay.
303330
with self.assertRaises(KeyError):
304331
self.db[k]
305332
# set_bulk
306333
self.db.set_bulk({k: "val1"})
307-
self.assertEqual(self.db.get(k), "val1")
334+
# TODO: Getting slash keys is currently not supported
335+
# self.assertEqual(self.db.get(k), "val1")
336+
self.assertEqual(list(self.db.keys()), [k])
308337
del self.db[k]
338+
# TODO: Getting slash keys is currently not supported
339+
# KeyError is the same though, so it can stay.
309340
with self.assertRaises(KeyError):
310341
self.db[k]
311342
# set_bulk_raw
312343
self.db.set_bulk_raw({k: "val1"})
313-
self.assertEqual(self.db.get_raw(k), "val1")
344+
# TODO: Getting slash keys is currently not supported
345+
# self.assertEqual(self.db.get_raw(k), "val1")
346+
self.assertEqual(list(self.db.keys()), [k])
314347
del self.db[k]
348+
# TODO: Getting slash keys is currently not supported
349+
# KeyError is the same though, so it can stay.
315350
with self.assertRaises(KeyError):
316351
self.db[k]

0 commit comments

Comments
 (0)