├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── example └── orbitdb_test.ipynb ├── orbitdbapi ├── __init__.py ├── client.py ├── db.py └── version.py ├── setup.py └── tests └── test.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 | # custom 107 | debug/ 108 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabCompletion": "on", 3 | "diffEditor.codeLens": true 4 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | WORKDIR /py-orbit-db-http-client 4 | RUN curl -L https://github.com/orbitdb/py-orbit-db-http-client/tarball/develop | tar -xz --strip-components 1 \ 5 | && pip install . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 phillmac 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 | # py-orbit-db-http-client 2 | 3 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/orbitdb/Lobby) [![Matrix](https://img.shields.io/badge/matrix-%23orbitdb%3Apermaweb.io-blue.svg)](https://riot.permaweb.io/#/room/#orbitdb:permaweb.io) 4 | 5 | > A python client for the Orbitdb HTTP API 6 | 7 | Status: Proof-of-concept 8 | 9 | ------------------------------ 10 | ## Install 11 | 12 | ### Install from `pip` 13 | ```sh 14 | pip install orbitdbapi 15 | ``` 16 | 17 | ### Install using source 18 | Clone, and then run: 19 | ```sh 20 | git clone https://github.com/orbitdb/py-orbit-db-http-client.git 21 | cd py-orbit-db-http-client 22 | py setup.py 23 | ``` 24 | ----------------------------- 25 | ## Usage 26 | > 27 | ``` 28 | from orbitdbapi import OrbitDbAPI 29 | client = OrbitDbAPI(base_url='http://localhost:3000') 30 | ``` 31 | You can then use the client object to list all the databases available on the API: 32 | ``` 33 | dbs = client.list_dbs() 34 | print(dbs) 35 | ``` 36 | To open a specific database, you can use the db() method, providing the name of the database as an argument: 37 | ``` 38 | db = client.db('mydb') 39 | ``` 40 | Once you have a DB object, you can use it to perform CRUD operations on the database. For example, to add an entry to the database: 41 | ``` 42 | db.add({'key': 'value'}) 43 | ``` 44 | To retrieve all the entries in the database: 45 | ``` 46 | entries = db.all() 47 | print(entries) 48 | ``` 49 | To retrieve a specific entry by its key: 50 | ``` 51 | entry = db.get('key') 52 | print(entry) 53 | ``` 54 | To update an entry: 55 | ``` 56 | db.update('key', {'key': 'new_value'}) 57 | ``` 58 | To remove an entry: 59 | ``` 60 | db.remove('key') 61 | ``` 62 | 63 | check the Jupyter Notebook example for local testing : [orbitdb_test.ipynb](./example/orbitdb_test.ipynb) 64 | 65 | ----------------------- 66 | 67 | ## Contributing 68 | 69 | This is a work-in-progress. Feel free to contribute by opening issues or pull requests. 70 | 71 | -------------------------- 72 | 73 | ## License 74 | 75 | [MIT Copyright (c) 2019 phillmac](LICENSE) 76 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Docker 2 | # Build a Docker image 3 | # https://docs.microsoft.com/azure/devops/pipelines/languages/docker 4 | 5 | trigger: 6 | - master 7 | 8 | resources: 9 | - repo: self 10 | 11 | variables: 12 | tag: '$(Build.BuildId)' 13 | 14 | stages: 15 | - stage: Build 16 | displayName: Build image 17 | jobs: 18 | - job: Build 19 | displayName: Build 20 | pool: 21 | vmImage: 'ubuntu-latest' 22 | steps: 23 | - task: Docker@2 24 | displayName: Build an image 25 | inputs: 26 | containerRegistry: 'Docker hub' 27 | repository: 'orbitdb/py-orbit-db-http-client' 28 | command: build 29 | dockerfile: '**/Dockerfile' 30 | tags: | 31 | latest 32 | $(tag) 33 | - task: Docker@2 34 | displayName: Push image 35 | inputs: 36 | containerRegistry: 'Docker hub' 37 | repository: 'orbitdb/py-orbit-db-http-client' 38 | command: 'push' 39 | tags: | 40 | latest 41 | $(tag) -------------------------------------------------------------------------------- /example/orbitdb_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1c0583dc", 6 | "metadata": { 7 | "toc": true 8 | }, 9 | "source": [ 10 | "

Table of Contents

\n", 11 | "
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "f65bab3c", 17 | "metadata": {}, 18 | "source": [ 19 | "# OrbitDB Test" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "d8a09e5a", 25 | "metadata": {}, 26 | "source": [ 27 | "## Install OrbitDB" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "id": "abe8087c", 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "pip install orbitdbapi" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "id": "2a8a5f2c", 43 | "metadata": {}, 44 | "source": [ 45 | "## Initialize the client with the base URL of the OrbitDB API" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "id": "22d80239", 52 | "metadata": { 53 | "ExecuteTime": { 54 | "end_time": "2023-01-20T13:14:34.650165Z", 55 | "start_time": "2023-01-20T13:14:34.309982Z" 56 | } 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "from orbitdbapi import OrbitDbAPI\n", 61 | "client = OrbitDbAPI(base_url='http://localhost:3000')" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "baee9316", 67 | "metadata": {}, 68 | "source": [ 69 | "## Listing all databases" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "id": "cdce6b1e", 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "dbs = client.list_dbs()\n", 80 | "print(dbs)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "id": "49c076f5", 86 | "metadata": {}, 87 | "source": [ 88 | "## Opening a database by name" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "id": "efc391f4", 95 | "metadata": { 96 | "ExecuteTime": { 97 | "end_time": "2023-01-20T13:16:15.737586Z", 98 | "start_time": "2023-01-20T13:16:13.630056Z" 99 | }, 100 | "scrolled": true 101 | }, 102 | "outputs": [], 103 | "source": [ 104 | "db = client.db('mydb')" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "id": "20fe1870", 110 | "metadata": {}, 111 | "source": [ 112 | "## Add a record to the database" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "id": "9d09b430", 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "db.add({'name': 'John Doe', 'age': 30})" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "d9a22fcd", 128 | "metadata": {}, 129 | "source": [ 130 | "## Retrieve a record from the database" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "9b249099", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "record = db.get('John Doe')\n", 141 | "print(record)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "id": "68990bf6", 147 | "metadata": {}, 148 | "source": [ 149 | "## Update a record in the database" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "id": "f8387ae3", 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "db.update('John Doe', {'name': 'Jane Doe', 'age': 25})" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "id": "c7e31479", 165 | "metadata": {}, 166 | "source": [ 167 | "## Delete a record from the database" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "id": "3ee23b16", 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "db.delete('Jane Doe')" 178 | ] 179 | }, 180 | { 181 | "cell_type": "markdown", 182 | "id": "688eb00d", 183 | "metadata": {}, 184 | "source": [ 185 | "## To retrieve all the entries in the database" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "id": "d2192c43", 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "entries = db.all()\n", 196 | "print(entries)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "id": "d8c748e2", 202 | "metadata": {}, 203 | "source": [ 204 | "## To query the database for specific entries" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "id": "4877bc66", 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "# Retrieve all entries where 'age' is greater than 25\n", 215 | "result = db.query([{'$gt': {'age': 25}}])\n", 216 | "print(result)" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "id": "c6b92bc2", 222 | "metadata": {}, 223 | "source": [ 224 | "## This will print a list of all the searches that are currently running on the OrbitDB API." 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "id": "be9dc359", 231 | "metadata": {}, 232 | "outputs": [], 233 | "source": [ 234 | "searches = client.searches()\n", 235 | "print(searches)" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": null, 241 | "id": "b42947ba", 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [] 245 | } 246 | ], 247 | "metadata": { 248 | "hide_input": false, 249 | "kernelspec": { 250 | "display_name": "Python 3.10", 251 | "language": "python", 252 | "name": "python310" 253 | }, 254 | "language_info": { 255 | "codemirror_mode": { 256 | "name": "ipython", 257 | "version": 3 258 | }, 259 | "file_extension": ".py", 260 | "mimetype": "text/x-python", 261 | "name": "python", 262 | "nbconvert_exporter": "python", 263 | "pygments_lexer": "ipython3", 264 | "version": "3.10.9" 265 | }, 266 | "toc": { 267 | "base_numbering": 1, 268 | "nav_menu": {}, 269 | "number_sections": true, 270 | "sideBar": false, 271 | "skip_h1_title": false, 272 | "title_cell": "Table of Contents", 273 | "title_sidebar": "Contents", 274 | "toc_cell": true, 275 | "toc_position": {}, 276 | "toc_section_display": false, 277 | "toc_window_display": false 278 | }, 279 | "varInspector": { 280 | "cols": { 281 | "lenName": 16, 282 | "lenType": 16, 283 | "lenVar": 40 284 | }, 285 | "kernels_config": { 286 | "python": { 287 | "delete_cmd_postfix": "", 288 | "delete_cmd_prefix": "del ", 289 | "library": "var_list.py", 290 | "varRefreshCmd": "print(var_dic_list())" 291 | }, 292 | "r": { 293 | "delete_cmd_postfix": ") ", 294 | "delete_cmd_prefix": "rm(", 295 | "library": "var_list.r", 296 | "varRefreshCmd": "cat(var_dic_list()) " 297 | } 298 | }, 299 | "types_to_exclude": [ 300 | "module", 301 | "function", 302 | "builtin_function_or_method", 303 | "instance", 304 | "_Feature" 305 | ], 306 | "window_display": false 307 | } 308 | }, 309 | "nbformat": 4, 310 | "nbformat_minor": 5 311 | } 312 | -------------------------------------------------------------------------------- /orbitdbapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import version, version_info 2 | from .client import OrbitDbAPI 3 | from .db import DB 4 | __version__ = version 5 | -------------------------------------------------------------------------------- /orbitdbapi/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from pprint import pformat 4 | from urllib.parse import quote as urlquote 5 | 6 | import httpx 7 | 8 | from .db import DB 9 | 10 | 11 | class OrbitDbAPI (): 12 | """ 13 | A client for interacting with the OrbitDB HTTP API. 14 | """ 15 | def __init__ (self, **kwargs): 16 | """ 17 | Initialize the client with the provided configuration options. 18 | 19 | Args: 20 | kwargs (dict): A dictionary of configuration options. 21 | - 'base_url': The base URL of the OrbitDB API (str). 22 | - 'use_db_cache': Whether to use a cache for database objects (bool, default=True). 23 | - 'timeout': Timeout for API requests (int, default=30). 24 | Example: 25 | client = OrbitDbAPI(base_url='http://localhost:3000', use_db_cache=True, timeout=30) 26 | """ 27 | self.logger = logging.getLogger(__name__) 28 | self.__config = kwargs 29 | self.__base_url = self.__config.get('base_url') 30 | self.__use_db_cache = self.__config.get('use_db_cache', True) 31 | self.__timeout = self.__config.get('timeout', 30) 32 | self.__session = httpx.Client() 33 | self.logger.debug('Base url: ' + self.__base_url) 34 | 35 | @property 36 | def session(self): 37 | """ 38 | Returns the underlying HTTP session used by the client. 39 | """ 40 | return self.__session 41 | 42 | @property 43 | def base_url(self): 44 | """ 45 | Returns the base URL of the OrbitDB API. 46 | """ 47 | return self.__base_url 48 | 49 | @property 50 | def use_db_cache(self): 51 | """ 52 | Returns whether the client uses a cache for database objects. 53 | """ 54 | return self.__use_db_cache 55 | 56 | def _do_request(self, *args, **kwargs): 57 | """ 58 | Perform a raw request to the OrbitDB API. 59 | Args: 60 | *args: Positional arguments to pass to the session's request() method. 61 | **kwargs: Keyword arguments to pass to the session's request() method. 62 | """ 63 | self.logger.log(15, json.dumps([args, kwargs])) 64 | kwargs['timeout'] = kwargs.get('timeout', self.__timeout) 65 | try: 66 | return self.__session.request(*args, **kwargs) 67 | except: 68 | self.logger.exception('Exception during api call') 69 | raise 70 | 71 | def _call_raw(self, method, endpoint, **kwargs): 72 | """ 73 | Perform a raw API call and return the raw response. 74 | Args: 75 | method (str): HTTP method to use. 76 | endpoint (str): Endpoint to call. 77 | **kwargs: Additional keyword arguments to pass to the request. 78 | """ 79 | url = '/'.join([self.__base_url, endpoint]) 80 | return self._do_request(method, url, **kwargs) 81 | 82 | def _call(self, method, endpoint, **kwargs): 83 | """ 84 | Perform an API call and return the parsed JSON response. 85 | Args: 86 | method (str): HTTP method to use. 87 | endpoint (str): Endpoint to call. 88 | **kwargs: Additional keyword arguments to pass to the request. 89 | """ 90 | res = self._call_raw(method, endpoint, **kwargs) 91 | try: 92 | result = res.json() 93 | except: 94 | self.logger.warning('Json decode error', exc_info=True) 95 | self.logger.log(15, res.text) 96 | raise 97 | try: 98 | res.raise_for_status() 99 | except: 100 | self.logger.exception('Server Error') 101 | self.logger.error(pformat(result)) 102 | raise 103 | return result 104 | 105 | def list_dbs(self): 106 | """ 107 | Retrieve a list of all databases on the OrbitDB API. 108 | """ 109 | return self._call('GET', 'dbs') 110 | 111 | def db(self, dbname, local_options=None, **kwargs): 112 | """ 113 | Open a database by name and return a DB object. 114 | Args: 115 | dbname (str): The name of the database to open. 116 | local_options (dict): A dictionary of options to pass to the DB object. 117 | **kwargs: Additional keyword arguments to pass to the open_db() method. 118 | Example: 119 | client = OrbitDbAPI() 120 | mydb = client.db("mydbname") 121 | """ 122 | if local_options is None: local_options = {} 123 | return DB(self, self.open_db(dbname, **kwargs), **{**self.__config, **local_options}) 124 | 125 | def open_db(self, dbname, **kwargs): 126 | """ 127 | Open a database by name and return the raw JSON response. 128 | Args: 129 | dbname (str): The name of the database to open. 130 | **kwargs: Additional keyword arguments to pass to the request. 131 | """ 132 | endpoint = '/'.join(['db', urlquote(dbname, safe='')]) 133 | return self._call('POST', endpoint, **kwargs) 134 | 135 | def searches(self): 136 | """ 137 | Retrieve a list of all searches on the OrbitDB API. 138 | """ 139 | endpoint = '/'.join(['peers', 'searches']) 140 | return self._call('GET', endpoint) 141 | -------------------------------------------------------------------------------- /orbitdbapi/db.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from collections.abc import Hashable, Iterable 4 | from copy import deepcopy 5 | from urllib.parse import quote as urlquote 6 | 7 | from sseclient import SSEClient 8 | 9 | 10 | class DB (): 11 | """ 12 | A class for interacting with a specific OrbitDB database. 13 | """ 14 | def __init__(self, client, params, **kwargs): 15 | """ 16 | Initialize a DB object. 17 | Args: 18 | client (OrbitDbAPI): The client object used to interact with the API. 19 | params (dict): A dictionary of parameters for the database. 20 | **kwargs: Additional keyword arguments. 21 | - 'use_db_cache': Whether to use a cache for database objects (bool, default=True). 22 | - 'enforce_caps': Whether to enforce the presence of capabilities in the database (bool, default=True). 23 | - 'enforce_indexby': Whether to enforce the presence of an indexBy option in the database (bool, default=True). 24 | """ 25 | self.__cache = {} 26 | self.__client = client 27 | self.__params = params 28 | self.__db_options = params.get('options', {}) 29 | self.__dbname = params['dbname'] 30 | self.__id = params['id'] 31 | self.__id_safe = urlquote(self.__id, safe='') 32 | self.__type = params['type'] 33 | self.__use_cache = kwargs.get('use_db_cache', client.use_db_cache) 34 | self.__enforce_caps = kwargs.get('enforce_caps', True) 35 | self.__enforce_indexby = kwargs.get('enforce_indexby', True) 36 | 37 | self.logger = logging.getLogger(__name__) 38 | self.__index_by = self.__db_options.get('indexBy') 39 | 40 | 41 | def clear_cache(self): 42 | """ 43 | Clear the cache of the database. 44 | """ 45 | self.__cache = {} 46 | 47 | def cache_get(self, item): 48 | """ 49 | Retrieve an item from the cache of the database. 50 | Args: 51 | item: The item to retrieve from the cache. 52 | """ 53 | item = str(item) 54 | return deepcopy(self.__cache.get(item)) 55 | 56 | def cache_remove(self, item): 57 | """ 58 | Remove an item from the cache of the database. 59 | Args: 60 | item: The item to remove from the cache. 61 | """ 62 | item = str(item) 63 | if item in self.__cache: 64 | del self.__cache[item] 65 | 66 | @property 67 | def cached(self): 68 | """ 69 | Returns whether the client uses a cache for database objects. 70 | """ 71 | return self.__use_cache 72 | 73 | @property 74 | def index_by(self): 75 | """ 76 | Returns the indexBy option of the database. 77 | """ 78 | return self.__index_by 79 | 80 | @property 81 | def cache(self): 82 | """ 83 | Returns the cache of the database. 84 | """ 85 | return deepcopy(self.__cache) 86 | 87 | @property 88 | def params(self): 89 | """ 90 | Returns the parameters of the database. 91 | """ 92 | return deepcopy(self.__params) 93 | 94 | @property 95 | def dbname(self): 96 | """ 97 | Returns the name of the database. 98 | """ 99 | return self.__dbname 100 | 101 | @property 102 | def id(self): 103 | """ 104 | Returns the id of the database. 105 | """ 106 | return self.__id 107 | 108 | @property 109 | def dbtype(self): 110 | """ 111 | Returns the type of the database. 112 | """ 113 | return self.__type 114 | 115 | @property 116 | def capabilities(self): 117 | """ 118 | Returns the capabilities of the database. 119 | """ 120 | return deepcopy(self.__params.get('capabilities', [])) 121 | 122 | @property 123 | def queryable(self): 124 | """ 125 | Returns whether the database is queryable. 126 | """ 127 | return 'query' in self.__params.get('capabilities', []) 128 | 129 | @property 130 | def putable(self): 131 | """ 132 | Returns whether the database is putable. 133 | """ 134 | return 'put' in self.__params.get('capabilities', []) 135 | 136 | @property 137 | def removeable(self): 138 | """ 139 | Returns whether the database is removeable. 140 | """ 141 | return 'remove' in self.__params.get('capabilities', []) 142 | 143 | @property 144 | def iterable(self): 145 | """ 146 | Returns whether the database is iterable. 147 | """ 148 | return 'iterator' in self.__params.get('capabilities', []) 149 | 150 | @property 151 | def addable(self): 152 | """ 153 | Returns whether the database is addable. 154 | """ 155 | return 'add' in self.__params.get('capabilities', []) 156 | 157 | @property 158 | def valuable(self): 159 | """ 160 | Returns whether the database is valuable. 161 | """ 162 | return 'value' in self.__params.get('capabilities', []) 163 | 164 | @property 165 | def incrementable(self): 166 | return 'inc' in self.__params.get('capabilities', []) 167 | 168 | @property 169 | def indexed(self): 170 | return 'indexBy' in self.__db_options 171 | 172 | @property 173 | def can_append(self): 174 | return self.__params.get('canAppend') 175 | 176 | @property 177 | def write_access(self): 178 | return deepcopy(self.__params.get('write')) 179 | 180 | def info(self): 181 | endpoint = '/'.join(['db', self.__id_safe]) 182 | return self.__client._call('GET', endpoint) 183 | 184 | def get(self, item, cache=None, unpack=False): 185 | if cache is None: cache = self.__use_cache 186 | item = str(item) 187 | if cache and item in self.__cache: 188 | result = self.__cache[item] 189 | else: 190 | endpoint = '/'.join(['db', self.__id_safe, item]) 191 | result = self.__client._call('GET', endpoint) 192 | if cache: self.__cache[item] = result 193 | if isinstance(result, Hashable): return deepcopy(result) 194 | if isinstance(result, Iterable): return deepcopy(result) 195 | if unpack: 196 | if isinstance(result, Iterable): return deepcopy(next(result, {})) 197 | if isinstance(result, list): return deepcopy(next(iter(result), {})) 198 | return result 199 | 200 | def get_raw(self, item): 201 | endpoint = '/'.join(['db', self.__id_safe, 'raw', str(item)]) 202 | return (self.__client._call('GET', endpoint)) 203 | 204 | def put(self, item, cache=None): 205 | if self.__enforce_caps and not self.putable: 206 | raise CapabilityError(f'Db {self.__dbname} does not have put capability') 207 | if self.indexed and (not self.__index_by in item) and self.__enforce_indexby: 208 | raise MissingIndexError(f"The provided document {item} doesn't contain field '{self.__index_by}'") 209 | 210 | if cache is None: cache = self.__use_cache 211 | if cache: 212 | if self.indexed and hasattr(item, self.__index_by): 213 | index_val = getattr(item, self.__index_by) 214 | else: 215 | index_val = item.get('key') 216 | if index_val: 217 | self.__cache[index_val] = item 218 | endpoint = '/'.join(['db', self.__id_safe, 'put']) 219 | entry_hash = self.__client._call('POST', endpoint, json=item).get('hash') 220 | if cache and entry_hash: self.__cache[entry_hash] = item 221 | return entry_hash 222 | 223 | def add(self, item, cache=None): 224 | if self.__enforce_caps and not self.addable: 225 | raise CapabilityError(f'Db {self.__dbname} does not have add capability') 226 | if cache is None: cache = self.__use_cache 227 | endpoint = '/'.join(['db', self.__id_safe, 'add']) 228 | entry_hash = self.__client._call('POST', endpoint, json=item).get('hash') 229 | if cache and entry_hash: self.__cache[entry_hash] = item 230 | return entry_hash 231 | 232 | def inc(self, val): 233 | val = int(val) 234 | endpoint = '/'.join(['db', self.__id_safe, 'inc']) 235 | return self.__client._call('POST', endpoint, json={'val':val}) 236 | 237 | def value(self): 238 | endpoint = '/'.join(['db', self.__id_safe, 'value']) 239 | return self.__client._call('GET', endpoint) 240 | 241 | def iterator_raw(self, **kwargs): 242 | if self.__enforce_caps and not self.iterable: 243 | raise CapabilityError(f'Db {self.__dbname} does not have iterator capability') 244 | endpoint = '/'.join(['db', self.__id_safe, 'rawiterator']) 245 | return self.__client._call('GET', endpoint, json=kwargs) 246 | 247 | def iterator(self, **kwargs): 248 | if self.__enforce_caps and not self.iterable: 249 | raise CapabilityError(f'Db {self.__dbname} does not have iterator capability') 250 | endpoint = '/'.join(['db', self.__id_safe, 'iterator']) 251 | return self.__client._call('GET', endpoint, json=kwargs) 252 | 253 | def index(self): 254 | endpoint = '/'.join(['db', self.__id_safe, 'index']) 255 | result = self.__client._call('GET', endpoint) 256 | return result 257 | 258 | def all(self): 259 | endpoint = '/'.join(['db', self.__id_safe, 'all']) 260 | result = self.__client._call('GET', endpoint) 261 | if isinstance(result, Hashable): 262 | self.__cache = result 263 | return result 264 | 265 | def remove(self, item): 266 | if self.__enforce_caps and not self.removeable: 267 | raise CapabilityError(f'Db {self.__dbname} does not have remove capability') 268 | item = str(item) 269 | endpoint = '/'.join(['db', self.__id_safe, item]) 270 | return self.__client._call('DELETE', endpoint) 271 | 272 | def unload(self): 273 | endpoint = '/'.join(['db', self.__id_safe]) 274 | return self.__client._call('DELETE', endpoint) 275 | 276 | def events(self, eventname): 277 | endpoint = '/'.join(['db', self.__id_safe, 'events', urlquote(eventname, safe='')]) 278 | res = self.__client._call_raw('GET', endpoint, stream=True) 279 | res.raise_for_status() 280 | return SSEClient(res.stream()).events() 281 | 282 | def findPeers(self, **kwargs): 283 | endpoint = '/'.join(['peers','searches','db', self.__id_safe]) 284 | return self.__client._call('POST', endpoint, json=kwargs) 285 | 286 | def getPeers(self): 287 | endpoint = '/'.join(['db', self.__id_safe, 'peers']) 288 | return self.__client._call('GET', endpoint) 289 | 290 | 291 | class CapabilityError(Exception): 292 | pass 293 | 294 | class MissingIndexError(Exception): 295 | pass 296 | -------------------------------------------------------------------------------- /orbitdbapi/version.py: -------------------------------------------------------------------------------- 1 | version = '0.4.1-dev0' 2 | version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | version = None 6 | exec(open('orbitdbapi/version.py').read()) 7 | 8 | setup( 9 | name='orbitdbapi', 10 | version=version, 11 | description='A Python HTTP Orbitdb API Client', 12 | author='Phillip Mackintosh', 13 | url='https://github.com/orbitdb/py-orbit-db-http-client', 14 | packages=find_packages(), 15 | install_requires=[ 16 | 'httpx >= 0.7.5', 17 | 'sseclient==0.0.24' 18 | ], 19 | classifiers=[ 20 | 'Programming Language :: Python :: 3', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import json 3 | import logging 4 | import os 5 | import random 6 | import string 7 | import sys 8 | import unittest 9 | from time import sleep 10 | 11 | from orbitdbapi.client import OrbitDbAPI 12 | 13 | base_url=os.environ.get('ORBIT_DB_HTTP_API_URL') 14 | 15 | def randString(k=5, lowercase=False, both=False): 16 | if both: 17 | return ''.join(random.choices(string.ascii_letters + string.digits, k=k)) 18 | if lowercase: 19 | return ''.join(random.choices(string.ascii_lowercase + string.digits, k=k)) 20 | return ''.join(random.choices(string.ascii_uppercase + string.digits, k=k)) 21 | 22 | class CapabilitiesTestCase(unittest.TestCase): 23 | def setUp(self): 24 | client = OrbitDbAPI(base_url=base_url) 25 | self.kevalue_test = client.db('keyvalue_test', json={'create':True, 'type': 'keyvalue'}) 26 | self.feed_test = client.db('feed_test', json={'create':True, 'type': 'feed'}) 27 | self.event_test = client.db('event_test', json={'create':True, 'type': 'eventlog'}) 28 | self.docstore_test = client.db('docstore_test', json={'create':True, 'type': 'docstore'}) 29 | self.counter_test = client.db('counter_test', json={'create':True, 'type': 'counter'}) 30 | 31 | def runTest(self): 32 | self.assertEqual(set(['get', 'put', 'remove']), set(self.kevalue_test.capabilities)) 33 | self.assertEqual(set(['add', 'get', 'iterator', 'remove']), set(self.feed_test.capabilities)) 34 | self.assertEqual(set(['add', 'get', 'iterator']), set(self.event_test.capabilities)) 35 | self.assertEqual(set(['get', 'put', 'query', 'remove']), set(self.docstore_test.capabilities)) 36 | self.assertEqual(set(['inc', 'value']), set(self.counter_test.capabilities)) 37 | 38 | def tearDown(self): 39 | self.kevalue_test.unload() 40 | self.feed_test.unload() 41 | self.event_test.unload() 42 | self.docstore_test.unload() 43 | self.counter_test.unload() 44 | 45 | class CounterIncrementTestCase(unittest.TestCase): 46 | def setUp(self): 47 | client = OrbitDbAPI(base_url=base_url) 48 | self.counter_test = client.db('counter_test', json={'create':True, 'type': 'counter'}) 49 | 50 | def runTest(self): 51 | localVal = self.counter_test.value() 52 | self.assertEqual(localVal, self.counter_test.value()) 53 | for _c in range(1,100): 54 | incVal = random.randrange(1,100) 55 | localVal += incVal 56 | self.counter_test.inc(incVal) 57 | self.assertEqual(localVal, self.counter_test.value()) 58 | 59 | def tearDown(self): 60 | self.counter_test.unload() 61 | 62 | 63 | class KVStoreGetPutTestCase(unittest.TestCase): 64 | def setUp(self): 65 | client = OrbitDbAPI(base_url=base_url, use_db_cache=False) 66 | self.kevalue_test = client.db('keyvalue_test', json={'create':True, 'type': 'keyvalue'}) 67 | 68 | def runTest(self): 69 | self.assertFalse(self.kevalue_test.cached) 70 | localKV = {} 71 | for _c in range(1,100): 72 | k = randString() 73 | v = randString(k=100, both=True) 74 | localKV[k] = v 75 | self.kevalue_test.put({'key':k, 'value':v}) 76 | self.assertEqual(localKV.get(k), self.kevalue_test.get(k)) 77 | self.assertDictContainsSubset(localKV, self.kevalue_test.all()) 78 | 79 | def tearDown(self): 80 | self.kevalue_test.unload() 81 | 82 | class DocStoreGetPutTestCase(unittest.TestCase): 83 | def setUp(self): 84 | client = OrbitDbAPI(base_url=base_url, use_db_cache=False) 85 | self.docstore_test = client.db('docstore_test', json={'create':True, 'type': 'docstore'}) 86 | 87 | def runTest(self): 88 | self.assertFalse(self.docstore_test.cached) 89 | localDocs = [] 90 | for _c in range(1,100): 91 | k = randString() 92 | v = randString(k=100, both=True) 93 | item = {'_id':k, 'value':v} 94 | localDocs.append(item) 95 | self.docstore_test.put(item) 96 | self.assertDictContainsSubset(item, self.docstore_test.get(k)[0]) 97 | self.assertTrue(all(item in self.docstore_test.all() for item in localDocs)) 98 | 99 | def tearDown(self): 100 | self.docstore_test.unload() 101 | 102 | 103 | class SearchesTestCase(unittest.TestCase): 104 | def setUp(self): 105 | self.client = OrbitDbAPI(base_url=base_url) 106 | self.kevalue_test = self.client.db('keyvalue_test', json={'create':True, 'type': 'keyvalue'}) 107 | 108 | 109 | def runTest(self): 110 | self.kevalue_test.findPeers() 111 | searches = self.client.searches() 112 | self.assertGreater(len(searches), 0) 113 | self.assertGreater(len([s for s in searches if s.get('searchID') == self.kevalue_test.id]), 0) 114 | 115 | def tearDown(self): 116 | self.kevalue_test.unload() 117 | 118 | class SearchPeersTestCase(unittest.TestCase): 119 | def setUp(self): 120 | self.client = OrbitDbAPI(base_url=base_url) 121 | self.kevalue_test = self.client.db('zdpuAuSAkDDRm9KTciShAcph2epSZsNmfPeLQmxw6b5mdLmq5/keyvalue_test') 122 | 123 | def runTest(self): 124 | self.kevalue_test.findPeers(useCustomFindProvs=True) 125 | dbPeers = [] 126 | count = 0 127 | while len(dbPeers) < 1: 128 | sleep(5) 129 | dbPeers = self.kevalue_test.getPeers() 130 | if count > 60: break 131 | self.assertGreater(len(dbPeers), 0) 132 | 133 | 134 | # def tearDown(self): 135 | # self.kevalue_test.unload() 136 | 137 | 138 | 139 | if __name__ == '__main__': 140 | loglvl = int(os.environ.get('LOG_LEVEL',15)) 141 | print(f'Log level: {loglvl}') 142 | logfmt = '%(asctime)s - %(levelname)s - %(message)s' 143 | logging.basicConfig(format=logfmt, stream=sys.stdout, level=loglvl) 144 | unittest.main() 145 | --------------------------------------------------------------------------------