├── .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 | [](https://gitter.im/orbitdb/Lobby) [](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 |
--------------------------------------------------------------------------------