diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index b85bdcd..0012969 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -5,8 +5,10 @@ agent: type: e1-standard-2 os_image: ubuntu1804 blocks: - - name: Lint + - name: Lint and test task: + secrets: + - name: Repl.it Database jobs: - name: flake8 commands: @@ -15,3 +17,11 @@ blocks: - python -m pip install --upgrade poetry - poetry install - git diff origin/master | poetry run flake8 --diff + - name: unittest + commands: + - sem-version python 3.8 + - checkout --use-cache + - python -m pip install --upgrade poetry + - poetry install + - poetry run coverage run -m unittest src/replit/test_database.py + - poetry run coverage report -m diff --git a/poetry.lock b/poetry.lock index b82aa2b..ded8da5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -76,9 +76,9 @@ version = "1.6.2" [package.dependencies] GitPython = ">=1.0.1" PyYAML = ">=3.13" +colorama = ">=0.3.9" six = ">=1.10.0" stevedore = ">=1.20.0" -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} [[package]] category = "dev" @@ -127,11 +127,23 @@ version = "7.1.2" [[package]] category = "dev" description = "Cross-platform colored terminal text." +marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" name = "colorama" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.4.3" +[[package]] +category = "main" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.2.1" + +[package.extras] +toml = ["toml"] + [[package]] category = "dev" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." @@ -232,6 +244,7 @@ version = "0.18.1" [package.dependencies] pycodestyle = "*" +setuptools = "*" [[package]] category = "dev" @@ -505,10 +518,12 @@ Jinja2 = ">=2.3" Pygments = ">=2.0" alabaster = ">=0.7,<0.8" babel = ">=1.3" +colorama = ">=0.3.5" docutils = ">=0.12" imagesize = "*" packaging = "*" requests = ">=2.5.0" +setuptools = "*" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -516,7 +531,6 @@ sphinxcontrib-htmlhelp = "*" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = "*" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -696,7 +710,8 @@ idna = ">=2.0" multidict = ">=4.0" [metadata] -content-hash = "c92c18a07248bbd0df18fba9327c2387af07b06a9a489123e3276810fb4e89fc" +content-hash = "9ceb6e910237c20a8cf4b40b1c12620752f1e9fe3f0417cba404e4ce2ef10752" +lock-version = "1.0" python-versions = "^3.8" [metadata.files] @@ -758,6 +773,42 @@ colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] +coverage = [ + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, + {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, + {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, + {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, + {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, + {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, + {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, + {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, + {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, + {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, + {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, + {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, + {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, + {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, + {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, + {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, + {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, + {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, + {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, +] darglint = [ {file = "darglint-1.5.2-py3-none-any.whl", hash = "sha256:049a98cf3aec8cf6ea344a863c68112d80b7f8de214459b5fa6853371f89c3e7"}, {file = "darglint-1.5.2.tar.gz", hash = "sha256:6b9461f96694c2cf1d8edb1597a783fe6840953b0eb18cc6cc1e72a26f196d79"}, diff --git a/pyproject.toml b/pyproject.toml index 8d7a1d0..e6a1205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ flask = "^1.1.2" werkzeug = "^1.0.1" aiohttp = "^3.6.2" nest_asyncio = "^1.4.0" +coverage = "^5.2.1" [tool.poetry.dev-dependencies] flake8 = "^3.8.3" diff --git a/src/replit/database/__init__.py b/src/replit/database/__init__.py index e716634..7b93d05 100644 --- a/src/replit/database/__init__.py +++ b/src/replit/database/__init__.py @@ -5,6 +5,7 @@ import json import os from sys import stderr from typing import Any, Callable, Dict, Tuple, Union +import urllib import aiohttp import nest_asyncio @@ -20,7 +21,7 @@ class AsyncJSONKey: you don't have to do it manually. """ - __slots__ = ("db", "key", "dtype", "get_default", "discard_bad_data") + __slots__ = ("db", "key", "dtype", "get_default", "discard_bad_data", "do_raise") def __init__( self, @@ -216,14 +217,15 @@ class AsyncReplitDb: Returns: Tuple[str]: The keys found. """ + params = {"prefix": prefix, "encode": "true"} async with aiohttp.ClientSession() as session: - async with session.get(self.db_url + "?prefix=" + prefix) as response: + async with session.get(self.db_url, params=params) as response: response.raise_for_status() text = await response.text() if not text: return tuple() else: - return tuple(text.split("\n")) + return tuple(urllib.parse.unquote(k) for k in text.split("\n")) async def to_dict(self, prefix: str = "") -> Dict[str, str]: """Dump all data in the database into a dictionary. @@ -238,7 +240,7 @@ class AsyncReplitDb: ret = {} keys = await self.list(prefix=prefix) for i in keys: - ret[i] = await self.view(i) + ret[i] = await self.get(i) return ret async def keys(self) -> Tuple[str]: @@ -263,6 +265,7 @@ class AsyncReplitDb: dtype: JSON_TYPE, get_default: Callable = None, discard_bad_data: bool = False, + do_raise: bool = False, ) -> AsyncJSONKey: """Initialize an AsyncJSONKey instance. @@ -277,6 +280,7 @@ class AsyncReplitDb: argument is used. discard_bad_data (bool): Don't prompt if bad data is read, overwrite it with the default. Defaults to False. + do_raise (bool): Whether to raise exceptions when errors are encountered. Returns: AsyncJSONKey: The initialized AsyncJSONKey instance. @@ -287,6 +291,7 @@ class AsyncReplitDb: dtype=dtype, get_default=get_default, discard_bad_data=discard_bad_data, + do_raise=do_raise, ) def __repr__(self) -> str: diff --git a/src/replit/test_database.py b/src/replit/test_database.py new file mode 100644 index 0000000..1ffb4b9 --- /dev/null +++ b/src/replit/test_database.py @@ -0,0 +1,128 @@ +"""Tests for replit.database.""" + +import os +import unittest + +from replit.database import AsyncReplitDb, ReplitDb +import requests + + +class TestAsyncDatabase(unittest.IsolatedAsyncioTestCase): + """Tests for replit.database.AsyncReplitDb.""" + + async def asyncSetUp(self) -> None: + """Grab a JWT for all the tests to share.""" + password = os.environ["PASSWORD"] + req = requests.get( + "https://database-test-jwt.kochman.repl.co", auth=("test", password) + ) + url = req.text + self.db = AsyncReplitDb(url) + + async def asyncTearDown(self) -> None: + """Nuke whatever the test added.""" + for k in await self.db.keys(): + await self.db.delete(k) + + async def test_get_set_delete(self) -> None: + """Test that we can get, set, and delete a key.""" + await self.db.set("test-key", "value") + + val = await self.db.get("test-key") + self.assertEqual(val, "value") + + await self.db.delete("test-key") + with self.assertRaises(KeyError): + await self.db.get("test-key") + + async def test_list_keys(self) -> None: + """Test that we can list keys.""" + key = "test-list-keys-with\nnewline" + await self.db.set(key, "value") + + val = await self.db.get(key) + self.assertEqual(val, "value") + + keys = await self.db.list(key) + self.assertEqual(keys, (key,)) + + await self.db.delete(key) + with self.assertRaises(KeyError): + await self.db.get(key) + + async def test_list_values(self) -> None: + """Test that we can get all values.""" + key = "test-list-values" + await self.db.set(key + "1", "value1") + await self.db.set(key + "2", "value2") + + vals = await self.db.values() + self.assertTupleEqual(vals, ("value1", "value2")) + + async def test_dict(self) -> None: + """Test that we can get a dict.""" + await self.db.set("key1", "value") + await self.db.set("key2", "value") + d = await self.db.to_dict() + self.assertDictEqual(d, {"key1": "value", "key2": "value"}) + + async def test_jsonkey(self) -> None: + """Test replit.database.AsyncJSONKey.""" + key = "test-jsonkey" + + jk = self.db.jsonkey(key, dtype=str, do_raise=True) + with self.assertRaises(KeyError): + await jk.get() + await jk.set("value") + val = await jk.get() + self.assertEqual(val, "value") + + async def test_jsonkey_default(self) -> None: + """Test replit.database.AsyncJSONKey with a default callable.""" + key = "test-jsonkey" + + jk = self.db.jsonkey(key, dtype=str, get_default=lambda: "value") + val = await jk.get() + self.assertEqual(val, "value") + + +class TestDatabase(unittest.IsolatedAsyncioTestCase): + """Tests for replit.database.ReplitDb.""" + + def setUp(self) -> None: + """Grab a JWT for all the tests to share.""" + password = os.environ["PASSWORD"] + req = requests.get( + "https://database-test-jwt.kochman.repl.co", auth=("test", password) + ) + url = req.text + self.db = ReplitDb(url) + + async def tearDown(self) -> None: + """Nuke whatever the test added.""" + for k in await self.db.keys(): + await self.db.delete(k) + + def test_get_set_delete(self) -> None: + """Test get, set, and delete.""" + with self.assertRaises(KeyError): + self.db.get("key") + + self.db.set("key", "value") + val = self.db.get("key") + self.assertEqual(val, "value") + + self.db.delete("key") + + def test_dict(self) -> None: + """Test using the database as a dict.""" + with self.assertRaises(KeyError): + val = self.db["hi"] + + self.db["hi"] = "there" + val = self.db.get("hi") + self.assertEqual(val, "there") + + del self.db["hi"] + with self.assertRaises(KeyError): + val = self.db["hi"]