├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── json_store_client └── __init__.py ├── requirements.txt ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | #Pycharm 107 | *.idea 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - '3.7' 5 | - '3.6' 6 | - nightly 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install codecov coverage 10 | after_script: 11 | - codecov 12 | script: 13 | - coverage run tests.py 14 | deploy: 15 | provider: pypi 16 | user: leon332157 17 | password: 18 | secure: lE4VFW2tsMXjH1aIDKkEufLL60+4qRlTmKJC2iWI3bX9+N02DWEVhevOvhDJL1QWYMtVhReFoJoAZpaWosB+/lAjKSX9ZEKBT6StLLaboYBH51hWF9G6Fji6CVa6+QEHZ51HEaiw4SjkHX6npfdHyxXqFPjsyZyfE3d+D/tWMyvVVt6WUe3zmfJvHlGTfwdq9Y0IlsRi2Kz9L2SoUBmTBsBzIwmZIlSrgAT/r9Owh7W10oCKHmLQbcrKdAtsqbSvO0O5eOQ0Vs/T8Ie/M332Ly8TDtHVexZqTxClpBIp2r9VwDKPPUTtMjp/ppoLrwavRiwM8CrfeVnUdFyoT+m+xxLV481M4FpsgjQoc5cAK0HTFUTLU6+l0cDx+AGT9g7y8N/TZga+3Mp1Z3JizAWxFfj0Ghs33aqFSviRpVyfP0UkAkztD5UBjadalHeObHlMoqKKUm++a1ziPDRioXT1QhtvISdtYs/rsR1nxI7chYolEJTF4h2U2dlvuOEID22IP8CPVRVCpn8V069uYUKMmpfSSnQSooA1ZIkn8Ca/DbuVknFXWHE+B3OU5sZCgMh+G6FKKKAfF6VvmbMEscL+PP6KF4oLZPjs3c7fHib1/OTj+E0iLxR+ek0Fo0XQRZZkyDPWEPa3n8szE0T8ECaqVOdLGbClYDzOM11IiD/PPbs= 19 | on: 20 | tags: true 21 | skip_existing: true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) leon332157 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-store-client 2 | A Python client for [jsonstore.io](https://www.jsonstore.io/) 3 | 4 | ## Installation 5 | ```bash 6 | pip install json-store-client 7 | ``` 8 | #### An optional installation of [ujson](https://pypi.org/project/ujson/) is recommended for faster json processing. 9 | ##### Also installing [cchardet](https://pypi.org/project/cchardet/) and [aiodns](https://pypi.org/project/aiodns/) are recommended by aiohttp for faster performance. 10 | 11 | ## Usage 12 | 13 | #### Demo of storing a Python json-friendly object with json-store-client in async on [repl.it](https://repl.it/@leon332157/json-store-client-demo). 14 | 15 | ```python 16 | from json_store_client import * 17 | 18 | jsonStoreToken = '...' # Insert your token here in place of the three periods(...). 19 | client = AsyncClient(jsonStoreToken) 20 | 21 | async def demo_function(): 22 | 23 | # Save data to the 'foo' key. 24 | await client.store('foo', {'alfa': 'bravo', 'charlie': 'delta'}) 25 | 26 | # Save data with dict mapping 27 | await client.save_multiple({'foo':{'alfa': 'bravo', 'charlie': 'delta'}}) 28 | 29 | # Get the data from the 'foo' key. 30 | data = await client.retrieve('foo') 31 | 32 | print(data) # => {'alfa': 'bravo', 'charlie': 'delta'} 33 | print(data['alfa']) # => 'bravo' 34 | 35 | # Deletes the data after printing parts of it. 36 | await client.delete('foo') 37 | ``` 38 | 39 | ## Importing 40 | 41 | Before starting to use the API, you will need to import the client classes into your program. The following line of code will simply import everything from the package: - 42 | ```python 43 | from json_store_client import * 44 | ``` 45 | 46 | 47 | ## json-store-client API 48 | 49 | ### Client 50 | **The synchronous Client.** 51 | This client handles the API features synchronously using normal functions. 52 | #### Constructor 53 | ```python 54 | my_client = Client(token) 55 | ``` 56 | 57 | ### AsyncClient 58 | **The asynchronous Client.** 59 | This client handles the API features asynchronously using coroutines. 60 | #### Constructor 61 | ```python 62 | my_async_client = AsyncClient(token) 63 | ``` 64 | 65 | Both return the client to use for data operations. 66 | 67 | ###### token (str): The API token from [jsonstore.io](https://www.jsonstore.io) 68 | 69 | 70 | ## Storing data 71 | 72 | - `client.store(key, data[, timeout]) # Synchronously` 73 | - `await client.store(key, data[, timeout]) # Asynchronously` 74 | 75 | Storing data in jsonstore with a key. 76 | 77 | ###### key (str): The key to be stored on jsonstore 78 | ###### data (any): The data to be stored under the key. It can be any Python objects. 79 | ###### timeout (int): The timeout for the http request. Default 5 seconds 80 | 81 | 82 | - `client.store_multiple(data[, timeout])` 83 | - `await client.store_multiple(data[, timeout])` 84 | 85 | Storing data in jsonstore with a dictionary mapping. 86 | 87 | ###### data (dict): A dict of {key(s):value(s)} to be updated. 88 | ###### timeout (int): The timeout for the http request. Default 5 seconds 89 | 90 | > **Note:** If there is already some data stored under the key, it will be overwritten. 91 | 92 | 93 | ## Fetching stored data 94 | 95 | - `client.retrieve(key[, timeout])` 96 | - `await clent.retrieve(key[, timeout])` 97 | 98 | Retrieve data in jsonstore with a key. 99 | 100 | ##### If nothing is saved under the key, it will return `None`. 101 | 102 | ###### key (str): The key to get on jsonstore 103 | ###### timeout (int): The timeout for the http request. Default 5 seconds 104 | 105 | 106 | ## Deleting stored data 107 | 108 | - `client.delete(key[, timeout])` 109 | - `await client.delete(key[, timeout])` 110 | 111 | Delete data in jsonstore with a key 112 | 113 | ###### key (str): The key to get on jsonstore 114 | ###### timeout (int): The timeout for the http request. Default 5 seconds 115 | -------------------------------------------------------------------------------- /json_store_client/__init__.py: -------------------------------------------------------------------------------- 1 | """Http client for the www.jsonstore.io API 2 | 3 | Usage (sync client): 4 | 5 | import json_store_client 6 | 7 | #Initialize the sync client class 8 | 9 | client = json_store_client.Client('insert your token/url here') 10 | 11 | Save/Change data in jsonstore with a key 12 | 13 | client.store('test_key', {'a':'B'}) 14 | client.save({'a':'B'}) # Using dict key and value mapping, a is the key and B is the value. 15 | 16 | Get data in jsonstore with a key 17 | 18 | test_dict=client.get('test_key') 19 | test_dict => {'a':'B'} 20 | 21 | Delete data in jsonstore with a key 22 | 23 | client.delete('test_key') 24 | 25 | Usage (async client): 26 | 27 | import json_store_client 28 | 29 | #Initialize the sync client class 30 | 31 | client = json_store_client.AsyncClient('insert your token/url here') 32 | 33 | Save/Change data in jsonstore with a key 34 | 35 | await client.store('test_key', {'a':'B'}) 36 | await client.save({'test_key:{'a':'B'}) # Using dict key and value mapping. 37 | 38 | Get data in jsonstore with a key 39 | 40 | test_dict = await client.get('test_key') 41 | test_dict => {'a':'B'} 42 | 43 | Delete data in jsonstore with a key 44 | 45 | await client.delete('test_key') 46 | """ 47 | try: 48 | import ujson as json 49 | except ImportError: 50 | import json 51 | from asyncio import get_event_loop 52 | from warnings import warn 53 | 54 | import aiohttp 55 | import requests 56 | 57 | DEFAULT_TIMEOUT_SECONDS = 5 58 | VERSION = '1.1.0' 59 | __all__ = ['JsonstoreError', 'Client', 'AsyncClient', 'EmptyResponseWarning'] 60 | 61 | 62 | class JsonstoreError(Exception): 63 | """Exception for errors occurring in calls to Jsonstore""" 64 | pass 65 | 66 | 67 | class EmptyResponseWarning(Warning): 68 | """Warning for empty response from Jsonstore""" 69 | pass 70 | 71 | 72 | class Client: 73 | 74 | def __init__(self, token: str): 75 | self.session = requests.Session() 76 | self.version = VERSION 77 | self.session.headers.update({ 78 | 'Accept': 'application/json', 79 | 'Content-type': 'application/json', 80 | 'User-Agent': f'Mozilla/5.0 Python/json-store-client/{self.version}' 81 | }) 82 | if not isinstance(token, str): 83 | raise TypeError("Token must be str, not {}".format(token.__class__.__name__)) 84 | if token.startswith('https://'): 85 | token = token.split('/')[-1] 86 | self.__base_url = f'https://www.jsonstore.io/{token}' 87 | 88 | def retrieve(self, key: str, timeout: int = DEFAULT_TIMEOUT_SECONDS): 89 | """Get gets value from jsonstore. 90 | 91 | :param key:str Name of key to a resource 92 | :param timeout:int Timeout of the request in seconds 93 | :return: The object that was stored 94 | """ 95 | if not isinstance(key, str): 96 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 97 | url = self.__finalize_url(key) 98 | try: 99 | resp = self.session.get(url, timeout=timeout) 100 | json_resp = self.__check_response(resp) 101 | if not json_resp or not json_resp['result']: 102 | warn('Jsonstore returned null, please make sure something is saved under this key.', EmptyResponseWarning) 103 | return None 104 | return json_resp['result'] 105 | except (ValueError, KeyError) as e: 106 | raise JsonstoreError(str(e)) 107 | 108 | get = retrieve 109 | 110 | def store(self, key: str, data, timeout: int = DEFAULT_TIMEOUT_SECONDS): 111 | """Save data in jsonstore under a key. 112 | 113 | :param key:str Name of key to a resource 114 | :param data Data to be updated/saved.. 115 | :param timeout:int Timeout of the request in seconds 116 | """ 117 | if not isinstance(key, str): 118 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 119 | url = self.__finalize_url(key) 120 | json_data = json.dumps(data) 121 | resp = self.session.post(url, data=json_data, timeout=timeout) 122 | self.__check_response(resp) 123 | return json_data 124 | 125 | save = store 126 | 127 | def store_multiple(self, data: dict, timeout: int = DEFAULT_TIMEOUT_SECONDS): 128 | """Save data in jsonstore with a dict mapping. 129 | 130 | :param data:dict A dict of {key(s):value(s)} to be updated. Value(s) can be any python object, will be processed with jsonpickle. 131 | :param timeout:int Timeout of the request in seconds 132 | """ 133 | if not isinstance(data, dict): 134 | raise TypeError("The data must be dict, not {}".format(data.__class__.__name__)) 135 | for key, value in data.items(): 136 | self.store(key, value, timeout) 137 | 138 | save_multiple = store_multiple 139 | 140 | def delete(self, key: str, timeout: int = DEFAULT_TIMEOUT_SECONDS): 141 | """Deletes data in jsonstore under a key. 142 | 143 | :param key:str Name of key to a resource 144 | :param timeout:int Timeout of the request in seconds 145 | """ 146 | if not isinstance(key, str): 147 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 148 | url = self.__finalize_url(key) 149 | resp = self.session.delete(url, timeout=timeout) 150 | self.__check_response(resp) 151 | return key 152 | 153 | def __check_response(self, response): 154 | """Checks if a response is successful raises a JsonstoreError if not. 155 | 156 | :param response: Response to check 157 | :return: Deserialized json response 158 | """ 159 | if not isinstance(response, requests.Response): 160 | raise TypeError('Unexpected type {}'.format(type(response.__class__.__name__))) 161 | response.raise_for_status() 162 | resp = response.json() 163 | if 'ok' not in resp: 164 | raise JsonstoreError('Call to jsonstore failed') 165 | return resp 166 | 167 | def __finalize_url(self, key): 168 | """Creates url for a given key. 169 | 170 | :param key: Key to append to the base url 171 | :return: URL to resource 172 | """ 173 | return '{}/{}'.format(self.__base_url, key) 174 | 175 | 176 | class AsyncClient: 177 | def __init__(self, token: str): 178 | self.version = VERSION 179 | self.session = get_event_loop().run_until_complete(self.__create_session()) 180 | if not isinstance(token, str): 181 | raise TypeError("Token must be str, not {}".format(token.__class__.__name__)) 182 | if token.startswith('https://'): 183 | token = token.split('/')[-1] 184 | self.__base_url = f'https://www.jsonstore.io/{token}' 185 | 186 | async def __create_session(self): 187 | return aiohttp.ClientSession(headers={ 188 | 'Accept': 'application/json', 189 | 'Content-type': 'application/json', 190 | 'User-Agent': f'Mozilla/5.0 Python/json-store-client/{self.version}' 191 | }) 192 | 193 | async def retrieve(self, key: str, timeout: int = DEFAULT_TIMEOUT_SECONDS): 194 | """Get gets value from jsonstore. 195 | 196 | :param key:str Name of key to a resource 197 | :param timeout:int Timeout of the request in seconds 198 | :return: The object that was stored 199 | """ 200 | if not isinstance(key, str): 201 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 202 | url = await self.__finalize_url(key) 203 | try: 204 | async with self.session.get(url, timeout=timeout) as s: 205 | json_resp = json.loads(await s.text()) 206 | if not json_resp or not json_resp['result']: 207 | warn('JSONSTORE WARNING: Jsonstore returned null, please make sure something is saved under this key.', 208 | EmptyResponseWarning) 209 | return None 210 | return json_resp['result'] 211 | except (ValueError, KeyError) as e: 212 | raise JsonstoreError(str(e)) 213 | 214 | get = retrieve 215 | 216 | async def store(self, key: str, data, timeout: int = DEFAULT_TIMEOUT_SECONDS): 217 | """Save data in jsonstore under a key. 218 | 219 | :param key:str Name of key to a resource 220 | :param data Data to be updated/saved. 221 | :param timeout:int Timeout of the request in seconds 222 | """ 223 | if not isinstance(key, str): 224 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 225 | url = await self.__finalize_url(key) 226 | json_data = json.dumps(data) 227 | async with self.session.post(url, data=json_data, timeout=timeout) as s: 228 | s.raise_for_status() 229 | return json_data 230 | 231 | save = store 232 | 233 | async def store_multiple(self, data: dict, timeout: int = DEFAULT_TIMEOUT_SECONDS): 234 | """Save data in jsonstore with a dict mapping. 235 | 236 | :param data:dict A dict of {key(s):value(s)} to be updated. 237 | :param timeout:int Timeout of the request in seconds 238 | """ 239 | if not isinstance(data, dict): 240 | raise TypeError("The data must be dict, not {}".format(data.__class__.__name__)) 241 | for key, value in data.items(): 242 | await self.store(key, value, timeout) 243 | 244 | save_multiple = store_multiple 245 | 246 | async def delete(self, key: str, timeout: int = DEFAULT_TIMEOUT_SECONDS): 247 | """Deletes data in jsonstore under a key. 248 | 249 | :param key:str Name of key to a resource 250 | :param timeout:int Timeout of the request in seconds 251 | """ 252 | if not isinstance(key, str): 253 | raise TypeError("Key must be str, not {}".format(key.__class__.__name__)) 254 | url = await self.__finalize_url(key) 255 | async with self.session.delete(url, timeout=timeout) as s: 256 | s.raise_for_status() 257 | return key 258 | 259 | async def __finalize_url(self, key): 260 | """Creates url for a given key. 261 | 262 | :param key: Key to append to the base url 263 | :return: URL to resource 264 | """ 265 | return '{}/{}'.format(self.__base_url, key) 266 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.22.0 2 | aiohttp>=3.5.4 3 | jsonpickle==1.1 4 | ujson==1.35 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | requires = [ 6 | 'requests', 'aiohttp' 7 | ] 8 | setuptools.setup( 9 | name="json-store-client", 10 | version="1.1.0", 11 | author="leon332157", 12 | author_email="leon332157@gmail.com", 13 | description="A client library for jsonstore", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/leon332157/json-store-client", 17 | packages=setuptools.find_packages(), 18 | classifiers=[ 19 | "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", "Topic :: Internet" 22 | ], install_requires=requires, 23 | ) 24 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import unittest 4 | 5 | import json_store_client 6 | 7 | TOKEN = "4aca8a426a3d8f3b0230f8dd83806b10d25237e22393bdcddb710f548c373d7e" 8 | KEY = 'testKey' 9 | DATA = {'testDataKey': 'testDataValue' + '😀'} 10 | 11 | 12 | def async_test(f): 13 | def wrapper(*args, **kwargs): 14 | coro = asyncio.coroutine(f) 15 | future = coro(*args, **kwargs) 16 | loop = asyncio.get_event_loop() 17 | loop.run_until_complete(future) 18 | 19 | return wrapper 20 | 21 | 22 | class TestSyncClient(unittest.TestCase): 23 | def setUp(self): 24 | self.client = json_store_client.Client(TOKEN) 25 | 26 | def testInit(self): 27 | self.assertIsInstance(self.client, json_store_client.Client) 28 | 29 | def testEmptyKey(self): 30 | with self.assertWarns(json_store_client.EmptyResponseWarning): 31 | self.client.get(str(random.randint(0, 10))) 32 | 33 | def testSave(self): 34 | self.assertEqual(self.client.store(KEY, DATA), '{"testDataKey":"testDataValue\\ud83d\\ude00"}') 35 | self.assertEqual(self.client.get(KEY), DATA) 36 | 37 | def testSaveMultiple(self): 38 | self.assertIsNone(self.client.store_multiple(DATA)) 39 | self.assertEqual(self.client.get('testDataKey'), 'testDataValue😀') 40 | 41 | def testDelete(self): 42 | self.assertEqual(self.client.delete(KEY), KEY) 43 | 44 | def doCleanups(self): 45 | self.client.session.close() 46 | 47 | 48 | class TestAsyncClient(unittest.TestCase): 49 | def setUp(self): 50 | self.client = json_store_client.AsyncClient(TOKEN) 51 | 52 | def testInit(self): 53 | self.assertIsInstance(self.client, json_store_client.AsyncClient) 54 | 55 | @async_test 56 | async def testEmptyKey(self): 57 | with self.assertWarns(json_store_client.EmptyResponseWarning): 58 | await self.client.get(str(random.randint(0, 10))) 59 | 60 | @async_test 61 | async def testSave(self): 62 | self.assertEqual(await self.client.store(KEY, DATA), '{"testDataKey":"testDataValue\\ud83d\\ude00"}') 63 | self.assertEqual(await self.client.get(KEY), DATA) 64 | 65 | @async_test 66 | async def testSaveMultiple(self): 67 | self.assertIsNone(await self.client.store_multiple(DATA)) 68 | self.assertEqual(await self.client.get('testDataKey'), 'testDataValue😀') 69 | 70 | @async_test 71 | async def testDelete(self): 72 | self.assertEqual(await self.client.delete(KEY), KEY) 73 | 74 | @async_test 75 | async def doCleanups(self): 76 | await self.client.session.close() 77 | 78 | 79 | if __name__ == '__main__': 80 | unittest.main() 81 | --------------------------------------------------------------------------------