diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..7696fb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# vscode +.vscode/ \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 5d3f54e..518dded --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# tispost \ No newline at end of file +Tispost +======= + +[![Latest PyPI package version](https://badge.fury.io/py/tispost.svg)](https://pypi.org/project/tispost) + +Key Features +------------ + +- Supports asyncio. + +Getting started +--------------- + +`tispost` allows you to quickly use postgres as a nosql database. + +Example +```python +# import +from tispost import Server, Collection, Session + +# create connection +server = Server(dbname=database, user=postgres, password=postgres, host=dlnxiot001) + +# connect +server.connect() + +# get session +session = await server.session() + +# create new collection +session.create("collection") + +# delete a collection +session.delete("collection") + +# get a collection +collection = session.collection("collection") + +# insert document into a collection: +collection.save({'item':'value'...}) + +# get document from collection with id +collection.get(id="930f43ed-7bb5-46b9-a6d2-45c345ec959e") + +# query items +collection.query(filter={'key':'value'}, offset=0, limit=50) +``` + +Installation +------------ +It's very simple to install tispost: +```sh +pip install tispost +``` + +Notes +----- + + - The db user must have the create/drop table permission + + +Requirements +------------ + +- Python >= 3.6 +- [aiopg](https://pypi.python.org/pypi/aiopg) + +License +------- + +`tispost` is offered under the Apache 2 license. + +Source code +----------- + +The latest developer version is available in a GitHub repository: + diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..d440ea4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +coverage +flake8 +isort +pytest +pytest-asyncio +pytest-cov +pytest-sugar +pytest-timeout +aiopg \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100755 index 0000000..29b21fb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +license_file = LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4f622f7 --- /dev/null +++ b/setup.py @@ -0,0 +1,76 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +import sys +import pathlib +from setuptools import setup + +if sys.version_info < (3, 6): + raise RuntimeError("tispost 4.x requires Python 3.6+") + + +HERE = pathlib.Path(__file__).parent + + +txt = (HERE / 'tispost' / '__init__.py').read_text('utf-8') +try: + version = re.findall(r"^__version__ = '([^']+)'\r?$", + txt, re.M)[0] +except IndexError: + raise RuntimeError('Unable to determine version.') + + +with open(os.path.join(HERE, 'README.md')) as f: + README = f.read() + + +setup( + name='tispost', + version=version, + description='Lightweight library for using postgres as nosql database', + long_description=README, + long_description_content_type='text/markdown', + classifiers=[ + 'License :: OSI Approved :: Apache Software License', + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Development Status :: 3 - Alpha', + 'Operating System :: POSIX', + 'Operating System :: MacOS :: MacOS X', + 'Operating System :: Microsoft :: Windows', + 'Framework :: AsyncIO', + ], + author='Alessio Pinna', + author_email='alessio.pinna@aiselis.com', + maintainer='Alessio Pinna ', + url='https://github.com/aiselis/tispost', + project_urls={ + 'Bug Reports': 'https://github.com/aiselis/tispost/issues', + 'Source': 'https://github.com/aiselis/tispost', + }, + license='Apache 2', + packages=['tispost'], + python_requires='>=3.6', + install_requires=[ + 'aiopg' + ], + include_package_data=True, +) diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100755 index 0000000..1d844bd --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,36 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tispost.server import Server +from unittest.mock import patch, AsyncMock +import pytest + + +@patch('tispost.server.aiopg', new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_connect(mock): + server = Server() + assert not server.pool + await server.connect() + assert server.pool + + +@patch('tispost.server.aiopg', new_callable=AsyncMock) +@pytest.mark.asyncio +async def test_close_valid(mock): + server = Server() + await server.connect() + await server.close() + server.pool.close.assert_called() diff --git a/tispost/__init__.py b/tispost/__init__.py new file mode 100755 index 0000000..098f937 --- /dev/null +++ b/tispost/__init__.py @@ -0,0 +1,23 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tispost.server import Server +from tispost.collection import Collection +from tispost.session import Session + + +__all__ = ['Server', 'Session', 'Collection'] + +__version__ = '0.1.0' diff --git a/tispost/collection.py b/tispost/collection.py new file mode 100755 index 0000000..2ecccb8 --- /dev/null +++ b/tispost/collection.py @@ -0,0 +1,64 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from aiopg import Connection +from psycopg2.extras import Json + + +class Collection: + + def __init__(self, connection: Connection, collection: str): + self.connection = connection + self.table = collection + + async def save(self, value: dict): + id = value.pop('id', None) + async with self.connection.cursor() as cursor: + if id: + await cursor.execute(f"UPDATE {self.table} SET data=%s WHERE id=%s", (Json(value), id)) + else: + await cursor.execute(f"INSERT INTO {self.table} (data) VALUES (%s) RETURNING id", (Json(value),)) + id = (await cursor.fetchone())[0] + cursor.close() + value.update(id=id) + return value + + async def delete(self, id: str): + async with self.connection.cursor() as cursor: + await cursor.execute(f"DELETE FROM {self.table} WHERE id=%s", (id,)) + cursor.close() + + async def get(self, id: str) -> dict: + async with self.connection.cursor() as cursor: + await cursor.execute(f"SELECT data FROM {self.table} WHERE id=%s", (id,)) + result = await cursor.fetchone() + cursor.close() + if result: + result[0].update(id=id) + return result[0] + else: + return None + + async def query(self, filter: dict, offset=0, limit=50): + async with self.connection.cursor() as cursor: + result = list() + query = f"SELECT id, data FROM {self.table} WHERE data@>%s ORDER BY id LIMIT %s OFFSET %s", + await cursor.execute(query, (Json(filter), limit, offset)) + async for row in cursor: + item = row[1] + item.update(id=row[0]) + result.append(item) + cursor.close() + return result diff --git a/tispost/server.py b/tispost/server.py new file mode 100755 index 0000000..c52e023 --- /dev/null +++ b/tispost/server.py @@ -0,0 +1,37 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tispost.session import Session +from aiopg import Pool +import aiopg + + +class Server: + + pool: Pool = None + dns: str = None + + def __init__(self, **kwargs): + self.dsn = ' '.join(['{}={}'.format(k, v) for k, v in kwargs.items()]) + + async def connect(self): + self.pool = await aiopg.create_pool(self.dsn) + + async def close(self): + if self.pool: + self.pool.close() + + async def session(self) -> Session: + return Session(self.pool) diff --git a/tispost/session.py b/tispost/session.py new file mode 100755 index 0000000..aa4290a --- /dev/null +++ b/tispost/session.py @@ -0,0 +1,50 @@ +# +# Copyright 2020 Alessio Pinna +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tispost.collection import Collection +from aiopg import Pool, Connection + + +class Session: + + pool: Pool = None + connection: Connection = None + + def __init__(self, pool: Pool): + self.pool = pool + + async def get_connection(self): + if not self.connection: + self.connection = await self.pool.acquire() + return self.connection + + async def collection(self, table: str) -> Collection: + return Collection(await self.get_connection(), table) + + async def create(self, collection: str): + q = "CREATE TABLE IF NOT EXISTS {}(id UUID NOT NULL DEFAULT uuid_generate_v4(), data JSON, PRIMARY KEY (id))" + async with (await self.get_connection()).cursor() as cursor: + await cursor.execute(q.format(collection)) + cursor.close() + + async def delete(self, collection: str): + q = "DROP TABLE IF EXISTS {}" + async with (await self.get_connection()).cursor() as cursor: + await cursor.execute(q.format(collection)) + cursor.close() + + async def close(self): + if self.connection: + await self.pool.release(self.connection)