├── .github └── workflows │ └── publish.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bottle_postgresql.py ├── requirements.txt ├── samples ├── advanced │ ├── .editorconfig │ ├── .flake8 │ ├── requirements-development.txt │ ├── requirements.txt │ └── sources │ │ ├── __init__.py │ │ ├── application.py │ │ ├── business │ │ ├── __init__.py │ │ └── entity.py │ │ ├── commons │ │ ├── __init__.py │ │ ├── base.py │ │ ├── converter.py │ │ ├── schema.py │ │ └── singleton.py │ │ ├── domains │ │ ├── __init__.py │ │ ├── database.py │ │ └── entity.py │ │ ├── hook.py │ │ ├── log.py │ │ ├── main.py │ │ ├── resources │ │ ├── __init__.py │ │ └── entity │ │ │ ├── __init__.py │ │ │ ├── resource.py │ │ │ └── schema.py │ │ ├── route.py │ │ └── utils │ │ ├── __init__.py │ │ └── environment.py └── basic │ ├── requirements.txt │ └── sources │ ├── __init__.py │ └── main.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── bottle_postgresql_test.py ├── configurations └── configuration.json └── queries ├── test.find_all_with_filter.sql ├── test.find_by_id.sql └── test.save.sql /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | pip install setuptools wheel twine 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | python setup.py sdist bdist_wheel 27 | twine upload dist/* 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | bottle_postgresql.egg-info/ 3 | build/ 4 | dist/ 5 | 6 | # Byte Compiled 7 | __pycache__/ 8 | 9 | # MacOS 10 | .DS_Store 11 | 12 | # PyCharm 13 | .idea/ 14 | 15 | # Virtual Environment 16 | venv/ 17 | 18 | # Visual Code Studio 19 | .vscode 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cropland 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 | # Bottle PostgreSQL 2 | ![PyPI - License](https://img.shields.io/pypi/l/bottle-postgresql?color=4B8BBE&label=License) 3 | ![PyPI - Version](https://img.shields.io/pypi/v/bottle-postgresql?color=FFE873&label=PyPI) 4 | 5 | Bottle PostgreSQL is a simple adapter for PostgreSQL with connection pooling. 6 | 7 | ## Configuration 8 | The configuration can be done through **JSON** file or by **Dict** following the pattern described below: 9 | ```json 10 | { 11 | "connect_timeout": 30, 12 | "dbname": "postgres", 13 | "host": "localhost", 14 | "maxconnections": 5, 15 | "password": "postgres", 16 | "port": 5432, 17 | "print_sql": true, 18 | "user": "postgres 19 | } 20 | ``` 21 | 22 | Create the `queries` directory. This should contain all the `.sql` files that the library will use. 23 | 24 | ## Usage 25 | PyPostgreSQLWrapper usage description: 26 | 27 | ### Delete 28 | 29 | #### Delete with where 30 | ```python 31 | from bottle_postgresql import Database 32 | 33 | with Database() as connection: 34 | ( 35 | connection 36 | .delete('test') 37 | .where('id', 1) 38 | .execute() 39 | ) 40 | ``` 41 | 42 | #### Delete with where condition 43 | ```python 44 | from bottle_postgresql import Database 45 | 46 | with Database() as connection: 47 | ( 48 | connection 49 | .delete('test') 50 | .where('description', 'Test%', operator='like') 51 | .execute() 52 | ) 53 | ``` 54 | 55 | ### Execute 56 | ```python 57 | from bottle_postgresql import Database 58 | 59 | with Database() as connection: 60 | ( 61 | connection 62 | .execute( 63 | ''' 64 | create table if not exists test ( 65 | id bigserial not null, 66 | name varchar(100), 67 | description varchar(255), 68 | constraint test_primary_key primary key (id) 69 | ) 70 | ''', 71 | skip_load_query=True 72 | ) 73 | ) 74 | ``` 75 | 76 | ### Insert 77 | ```python 78 | from bottle_postgresql import Database 79 | 80 | with Database() as connection: 81 | ( 82 | connection 83 | .insert('test') 84 | .set('id', 1) 85 | .set('name', 'Name') 86 | .set('description', 'Description') 87 | .execute() 88 | ) 89 | ``` 90 | 91 | ### Paging 92 | 93 | #### Paging with where condition 94 | ```python 95 | from bottle_postgresql import Database 96 | 97 | with Database() as connection: 98 | ( 99 | connection 100 | .select('test') 101 | .fields('id', 'name', 'description') 102 | .where('id', 1, operator='>') 103 | .order_by('id') 104 | .paging(0, 2) 105 | ) 106 | ``` 107 | 108 | #### Paging without where condition 109 | ```python 110 | from bottle_postgresql import Database 111 | 112 | with Database() as connection: 113 | ( 114 | connection 115 | .select('test') 116 | .paging(0, 10) 117 | ) 118 | ``` 119 | 120 | ### Select 121 | 122 | #### Fetch all 123 | ```python 124 | from bottle_postgresql import Database 125 | 126 | with Database() as connection: 127 | ( 128 | connection 129 | .select('test') 130 | .execute() 131 | .fetch_all() 132 | ) 133 | ``` 134 | 135 | #### Fetch many 136 | ```python 137 | from bottle_postgresql import Database 138 | 139 | with Database() as connection: 140 | ( 141 | connection 142 | .select('test') 143 | .execute() 144 | .fetch_many(1) 145 | ) 146 | ``` 147 | 148 | #### Fetch one 149 | ```python 150 | from bottle_postgresql import Database 151 | 152 | with Database() as connection: 153 | ( 154 | connection 155 | .select('test') 156 | .execute() 157 | .fetch_one() 158 | ) 159 | ``` 160 | 161 | #### Select by file 162 | ```python 163 | from bottle_postgresql import Database 164 | 165 | with Database() as connection: 166 | ( 167 | connection 168 | .execute('find_by_id', {'id': 1}) 169 | .fetch_one() 170 | ) 171 | ``` 172 | 173 | #### Select by query 174 | ```python 175 | from bottle_postgresql import Database 176 | 177 | with Database() as connection: 178 | ( 179 | connection 180 | .execute('select id, name, description from test where id = %(id)s', {'id': 1}) 181 | .fetch_one() 182 | ) 183 | ``` 184 | 185 | ### Update 186 | 187 | #### Update with where 188 | ```python 189 | from bottle_postgresql import Database 190 | 191 | with Database() as connection: 192 | ( 193 | connection 194 | .update('test') 195 | .set('name', 'New Name') 196 | .set('description', 'New Description') 197 | .where('id', 1) 198 | .execute() 199 | ) 200 | ``` 201 | 202 | #### Update with where all 203 | ```python 204 | from bottle_postgresql import Database 205 | 206 | with Database() as connection: 207 | ( 208 | connection 209 | .update('test') 210 | .set('name', 'New Name') 211 | .set('description', 'New Description') 212 | .where_all({'id': 1, 'name': 'Name', 'description': 'Description'}) 213 | .execute() 214 | ) 215 | ``` 216 | 217 | ### Using filters 218 | 219 | #### SQL 220 | ```sql 221 | select 222 | id, 223 | name, 224 | description 225 | from test 226 | where 1 = 1 227 | {{#id}} 228 | and id = %(id)s 229 | {{/id}} 230 | {{#name}} 231 | and name like %(name)s 232 | {{/name}} 233 | ``` 234 | 235 | #### Select with filters 236 | ```python 237 | from bottle_postgresql import Database 238 | 239 | with Database() as connection: 240 | ( 241 | connection 242 | .execute('test.find_all_with_filter', parameters={'id': 1, 'name': 'Name'}) 243 | .fetch_one() 244 | ) 245 | ``` 246 | -------------------------------------------------------------------------------- /bottle_postgresql.py: -------------------------------------------------------------------------------- 1 | from dbutils.pooled_db import PooledDB 2 | 3 | import errno 4 | import json 5 | import os 6 | import psycopg2 7 | import psycopg2.extras 8 | import pystache 9 | 10 | __author__ = 'Bernardo Couto' 11 | __author_email__ = 'bernardocouto.py@gmail.com' 12 | __version__ = '1.0.1' 13 | 14 | QUERIES_DIRECTORY = os.path.realpath(os.path.curdir) + '/queries/' 15 | 16 | 17 | class Configuration(object): 18 | 19 | __instance__ = None 20 | 21 | def __init__(self, configuration_dict=None, configuration_file=None): 22 | if configuration_dict: 23 | self.data = configuration_dict 24 | elif configuration_file: 25 | if not os.path.exists(configuration_file): 26 | raise ConfigurationNotFoundException() 27 | with open(configuration_file, 'r') as file: 28 | try: 29 | self.data = json.loads(file.read()) 30 | except json.decoder.JSONDecoderError as exception: 31 | raise ConfigurationInvalidException(exception) 32 | self.print_sql = self.data.pop('print_sql') if 'print_sql' in self.data else False 33 | self.pool = PooledDB(psycopg2, **self.data) 34 | 35 | @staticmethod 36 | def instance(configuration_dict=None, configuration_file='/etc/bottle_postgresql/configuration.json'): 37 | if Configuration.__instance__ is None: 38 | Configuration.__instance__ = Configuration(configuration_dict, configuration_file) 39 | return Configuration.__instance__ 40 | 41 | 42 | class ConfigurationInvalidException(Exception): 43 | 44 | pass 45 | 46 | 47 | class ConfigurationNotFoundException(Exception): 48 | 49 | pass 50 | 51 | 52 | class CursorWrapper(object): 53 | 54 | def __init__(self, cursor): 55 | self.cursor = cursor 56 | 57 | def __iter__(self): 58 | return self 59 | 60 | def __next__(self): 61 | return self.next() 62 | 63 | def close(self): 64 | self.cursor.close() 65 | 66 | def fetch_all(self): 67 | return [DictWrapper(row) for row in self.cursor.fetchall()] 68 | 69 | def fetch_many(self, size): 70 | return [DictWrapper(row) for row in self.cursor.fetchmany(size)] 71 | 72 | def fetch_one(self): 73 | row = self.cursor.fetchone() 74 | if row is not None: 75 | return DictWrapper(row) 76 | else: 77 | self.close() 78 | return row 79 | 80 | def next(self): 81 | row = self.fetch_one() 82 | if row is None: 83 | raise StopIteration() 84 | return row 85 | 86 | def row_count(self): 87 | return self.cursor.rowcount 88 | 89 | 90 | class Database(object): 91 | 92 | def __enter__(self): 93 | return self 94 | 95 | def __exit__(self, exception_type, exception_value, exception_traceback): 96 | if exception_type is None and exception_value is None and exception_traceback is None: 97 | self.connection.commit() 98 | else: 99 | self.connection.rollback() 100 | self.disconnect() 101 | 102 | def __init__(self, configuration=None): 103 | self.configuration = Configuration.instance() if configuration is None else configuration 104 | self.connection = self.configuration.pool.connection() 105 | self.print_sql = self.configuration.print_sql 106 | 107 | def delete(self, table): 108 | return DeleteBuilder(self, table) 109 | 110 | def disconnect(self): 111 | self.connection.close() 112 | 113 | def execute(self, sql, parameters=None, skip_load_query=False): 114 | cursor = self.connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) 115 | if self.print_sql: 116 | print('Query: {} - Parameters: {}'.format(sql, parameters)) 117 | if skip_load_query: 118 | sql = sql 119 | else: 120 | sql = self.load_query(sql, parameters) 121 | cursor.execute(sql, parameters) 122 | return CursorWrapper(cursor) 123 | 124 | def insert(self, table): 125 | return InsertBuilder(self, table) 126 | 127 | def update(self, table): 128 | return UpdateBuilder(self, table) 129 | 130 | @staticmethod 131 | def load_query(query_name, parameters=None): 132 | try: 133 | with open(QUERIES_DIRECTORY + query_name + '.sql') as file: 134 | query = file.read() 135 | if not parameters: 136 | return query 137 | else: 138 | return pystache.render(query, parameters) 139 | except IOError as exception: 140 | if exception.errno == errno.ENOENT: 141 | return query_name 142 | else: 143 | raise exception 144 | 145 | def paging(self, sql, page=0, parameters=None, size=10, skip_load_query=True): 146 | if skip_load_query: 147 | sql = sql 148 | else: 149 | sql = self.load_query(sql, parameters) 150 | sql = '{} limit {} offset {}'.format(sql, size + 1, page * size) 151 | data = self.execute(sql, parameters, skip_load_query=True).fetch_all() 152 | last = len(data) <= size 153 | return Page(page, size, data[:-1] if not last else data, last) 154 | 155 | def select(self, table): 156 | return SelectBuilder(self, table) 157 | 158 | 159 | class DictWrapper(dict): 160 | 161 | def __getattr__(self, item): 162 | if item in self: 163 | if isinstance(self[item], dict) and not isinstance(self[item], DictWrapper): 164 | self[item] = DictWrapper(self[item]) 165 | return self[item] 166 | raise AttributeError('{} is not a valid attribute'.format(item)) 167 | 168 | def __init__(self, data): 169 | self.update(data) 170 | 171 | def __setattr__(self, key, value): 172 | self[key] = value 173 | 174 | def as_dict(self): 175 | return self 176 | 177 | 178 | class Migration(object): 179 | 180 | pass 181 | 182 | 183 | class Page(dict): 184 | 185 | def __init__(self, page_number, page_size, data, last): 186 | self['data'] = self.data = data 187 | self['last'] = self.last = last 188 | self['page_number'] = self.page_number = page_number 189 | self['page_size'] = self.page_size = page_size 190 | 191 | 192 | class SQLBuilder(object): 193 | 194 | def __init__(self, database, table): 195 | self.database = database 196 | self.parameters = {} 197 | self.table = table 198 | self.where_conditions = [] 199 | 200 | def execute(self): 201 | return self.database.execute(self.sql(), self.parameters, True) 202 | 203 | def sql(self): 204 | pass 205 | 206 | def where_all(self, data): 207 | for value in data.keys(): 208 | self.where(value, data[value]) 209 | return self 210 | 211 | def where_build(self): 212 | if len(self.where_conditions) > 0: 213 | conditions = ' and '.join(self.where_conditions) 214 | return 'where {}'.format(conditions) 215 | else: 216 | return '' 217 | 218 | def where(self, field, value, constant=False, operator='='): 219 | if constant: 220 | self.where_conditions.append('{} {} {}'.format(field, operator, value)) 221 | else: 222 | self.parameters[field] = value 223 | self.where_conditions.append('{0} {1} %({0})s'.format(field, operator)) 224 | return self 225 | 226 | 227 | class DeleteBuilder(SQLBuilder): 228 | 229 | def sql(self): 230 | return 'delete from {} {}'.format(self.table, self.where_build()) 231 | 232 | 233 | class InsertBuilder(SQLBuilder): 234 | 235 | def __init__(self, database, table): 236 | super(InsertBuilder, self).__init__(database, table) 237 | self.constants = {} 238 | 239 | def set(self, field, value, constant=False): 240 | if constant: 241 | self.constants[field] = value 242 | else: 243 | self.parameters[field] = value 244 | return self 245 | 246 | def set_all(self, data): 247 | for value in data.keys(): 248 | self.set(value, data[value]) 249 | return self 250 | 251 | def sql(self): 252 | if len(set(list(self.parameters.keys()) + list(self.constants.keys()))) == len(self.parameters.keys()) + len(self.constants.keys()): 253 | columns = [] 254 | values = [] 255 | for field in self.constants: 256 | columns.append(field) 257 | values.append(self.constants[field]) 258 | for field in self.parameters: 259 | columns.append(field) 260 | values.append('%({})s'.format(field)) 261 | return 'insert into {} ({}) values ({}) returning *'.format(self.table, ', '.join(columns), ', '.join(values)) 262 | else: 263 | raise ValueError('There are repeated keys in constants and values') 264 | 265 | 266 | class SelectBuilder(SQLBuilder): 267 | 268 | def __init__(self, database, table): 269 | super(SelectBuilder, self).__init__(database, table) 270 | self.select_fields = ['*'] 271 | self.select_group_by = [] 272 | self.select_order_by = [] 273 | self.select_page = '' 274 | 275 | def fields(self, *fields): 276 | self.select_fields = fields 277 | return self 278 | 279 | def group_by(self, *fields): 280 | self.select_group_by = fields 281 | return self 282 | 283 | def order_by(self, *fields): 284 | self.select_order_by = fields 285 | return self 286 | 287 | def paging(self, page=0, size=10): 288 | self.select_page = 'limit {} offset {}'.format(size + 1, page * size) 289 | data = self.execute().fetch_all() 290 | last = len(data) <= size 291 | return Page(page, size, data[:-1] if not last else data, last) 292 | 293 | def sql(self): 294 | group_by = ', '.join(self.select_group_by) 295 | if group_by != '': 296 | group_by = 'group by {}'.format(group_by) 297 | order_by = ', '.join(self.select_order_by) 298 | if order_by != '': 299 | order_by = 'order by {}'.format(order_by) 300 | return 'select {} from {} {} {} {} {}'.format( 301 | ', '.join(self.select_fields), 302 | self.table, 303 | self.where_build(), 304 | group_by, 305 | order_by, 306 | self.select_page 307 | ) 308 | 309 | 310 | class UpdateBuilder(SQLBuilder): 311 | 312 | def __init__(self, database, table): 313 | super(UpdateBuilder, self).__init__(database, table) 314 | self.statements = [] 315 | 316 | def set(self, field, value, constant=False): 317 | if constant: 318 | self.statements.append('{} = {}'.format(field, value)) 319 | else: 320 | self.statements.append('{0} = %({0})s'.format(field)) 321 | self.parameters[field] = value 322 | return self 323 | 324 | def set_all(self, data): 325 | for value in data.keys(): 326 | self.set(value, data[value]) 327 | return self 328 | 329 | def set_build(self): 330 | if len(self.statements) > 0: 331 | statements = ', '.join(self.statements) 332 | return 'set {}'.format(statements) 333 | else: 334 | return '' 335 | 336 | def sql(self): 337 | return 'update {} {} {}'.format(self.table, self.set_build(), self.where_build()) 338 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dbutils==2.0.1 2 | psycopg2-binary==2.8.6 3 | pystache==0.5.4 4 | -------------------------------------------------------------------------------- /samples/advanced/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.py] 12 | max_line_length = 120 13 | 14 | [*.txt] 15 | max_line_length = 80 16 | -------------------------------------------------------------------------------- /samples/advanced/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | application-import-names = flake8 3 | exclude = 4 | .git/, 5 | .github/, 6 | .idea/, 7 | .vscode/, 8 | __pycache__/, 9 | venv/ 10 | max-complexity = 15 11 | max-line-length = 120 12 | -------------------------------------------------------------------------------- /samples/advanced/requirements-development.txt: -------------------------------------------------------------------------------- 1 | pre-commit==2.12.0 2 | -------------------------------------------------------------------------------- /samples/advanced/requirements.txt: -------------------------------------------------------------------------------- 1 | bottle==0.12.19 2 | bottle-cerberus==1.1.3 3 | bottle-postgresql==1.0.0 4 | -------------------------------------------------------------------------------- /samples/advanced/sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/application.py: -------------------------------------------------------------------------------- 1 | from bottle import Bottle, JSONPlugin 2 | 3 | import datetime 4 | import decimal 5 | import json 6 | 7 | 8 | class JSONEncoder(json.JSONEncoder): 9 | 10 | def default(self, entity): 11 | if isinstance(entity, datetime.date): 12 | return entity.isoformat() 13 | if isinstance(entity, datetime.datetime): 14 | return entity.isoformat() 15 | if isinstance(entity, decimal.Decimal): 16 | return float(entity) 17 | return json.JSONEncoder.default(self, entity) 18 | 19 | 20 | def application_factory(): 21 | application = Bottle() 22 | application.install(JSONPlugin(json_dumps=lambda entity: json.dumps(entity, cls=JSONEncoder))) 23 | return application 24 | -------------------------------------------------------------------------------- /samples/advanced/sources/business/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/business/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/business/entity.py: -------------------------------------------------------------------------------- 1 | from samples.advanced.sources.commons.converter import ObjectConverter 2 | from samples.advanced.sources.domains.entity import EntityModel, EntityRepository 3 | 4 | 5 | class EntityBusiness: 6 | 7 | @staticmethod 8 | def delete_entity(id): 9 | with EntityRepository() as repository: 10 | entity = repository.select_entity_by_id(id) 11 | entity = EntityModel( 12 | ObjectConverter({ 13 | 'id': entity.get('id'), 14 | 'field': entity.get('field') 15 | }) 16 | ) 17 | repository.delete_entity(entity) 18 | 19 | @staticmethod 20 | def insert_entity(field): 21 | entity = EntityModel( 22 | ObjectConverter({ 23 | 'field': field 24 | }) 25 | ) 26 | with EntityRepository() as repository: 27 | return repository.insert_entity(entity) 28 | 29 | @staticmethod 30 | def select_entity_all(): 31 | with EntityRepository() as repository: 32 | return repository.select_entity_all() 33 | 34 | @staticmethod 35 | def update_entity(id, field): 36 | with EntityRepository() as repository: 37 | entity = repository.select_entity_by_id(id) 38 | entity.field = field 39 | return repository.update_entity(entity) 40 | -------------------------------------------------------------------------------- /samples/advanced/sources/commons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/commons/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/commons/base.py: -------------------------------------------------------------------------------- 1 | from bottle import Bottle 2 | from bottle_cerberus import CerberusPlugin 3 | from samples.advanced.sources.commons.singleton import Singleton 4 | 5 | 6 | class Base(Bottle, metaclass=Singleton): 7 | 8 | def __init__(self): 9 | super(Base, self).__init__() 10 | self.install(CerberusPlugin()) 11 | -------------------------------------------------------------------------------- /samples/advanced/sources/commons/converter.py: -------------------------------------------------------------------------------- 1 | class ObjectConverter: 2 | 3 | def __init__(self, entity): 4 | for key, value in entity.items(): 5 | if isinstance(value, (list, tuple)): 6 | setattr(self, key, [ObjectConverter(child) if isinstance(child, dict) else child for child in value]) 7 | else: 8 | setattr(self, key, ObjectConverter(value) if isinstance(value, dict) else value) 9 | -------------------------------------------------------------------------------- /samples/advanced/sources/commons/schema.py: -------------------------------------------------------------------------------- 1 | class Schema: 2 | 3 | BODY = 'body' 4 | QUERY_PARAMETER = 'query_parameter' 5 | URL_PARAMETER = 'url_parameter' 6 | 7 | @staticmethod 8 | def validate_body(schema): 9 | return {Schema.BODY: schema()} 10 | 11 | @staticmethod 12 | def validate_query_parameter(schema): 13 | return {Schema.QUERY_PARAMETER: schema()} 14 | 15 | @staticmethod 16 | def validate_url_parameter(schema): 17 | return {Schema.URL_PARAMETER: schema()} 18 | -------------------------------------------------------------------------------- /samples/advanced/sources/commons/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(type): 2 | 3 | instance = {} 4 | 5 | def __call__(cls, *args, **kwargs): 6 | if cls not in cls.instance: 7 | cls.instance[cls] = super(Singleton, cls).__call__(*args, **kwargs) 8 | return cls.instance[cls] 9 | 10 | @staticmethod 11 | def clear(): 12 | Singleton.instance.clear() 13 | -------------------------------------------------------------------------------- /samples/advanced/sources/domains/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/domains/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/domains/database.py: -------------------------------------------------------------------------------- 1 | from samples.advanced.sources.log import get_logger 2 | from samples.advanced.sources.utils import environment 3 | 4 | import bottle_postgresql 5 | 6 | logger = get_logger(__name__) 7 | 8 | 9 | class Database(bottle_postgresql.Database): 10 | 11 | def __init__(self, table_name, configuration=None): 12 | if configuration: 13 | super(Database, self).__init__(configuration) 14 | else: 15 | super(Database, self).__init__() 16 | self.table_name = table_name 17 | 18 | @staticmethod 19 | def configuration(): 20 | configuration_dict = { 21 | 'connect_timeout': environment.DATABASE_CONNECTION_TIMEOUT, 22 | 'dbname': environment.DATABASE_NAME, 23 | 'host': environment.DATABASE_HOST, 24 | 'maxconnections': environment.DATABASE_MAX_CONNECTION, 25 | 'password': environment.DATABASE_PASSWORD, 26 | 'port': environment.DATABASE_PORT, 27 | 'print_sql': environment.DATABASE_PRINT_SQL, 28 | 'user': environment.DATABASE_USERNAME 29 | } 30 | bottle_postgresql.Configuration.instance(configuration_dict=configuration_dict, configuration_file=None) 31 | -------------------------------------------------------------------------------- /samples/advanced/sources/domains/entity.py: -------------------------------------------------------------------------------- 1 | from samples.advanced.sources.domains.database import Database 2 | from samples.advanced.sources.log import get_logger 3 | 4 | logger = get_logger(__name__) 5 | 6 | 7 | class EntityModel: 8 | 9 | def __init__(self, entity): 10 | self.id = entity.id 11 | self.name = entity.name 12 | 13 | 14 | class EntityRepository(Database): 15 | 16 | def __init__(self): 17 | super(EntityRepository, self).__init__('entities') 18 | 19 | def delete_entity(self, entity): 20 | ( 21 | self.delete(self.table_name) 22 | .where('id', entity.get('id'), operator='=') 23 | .execute() 24 | ) 25 | 26 | def insert_entity(self, entity): 27 | return ( 28 | self.insert(self.table_name) 29 | .set('id', entity.id) 30 | .set('field', entity.field) 31 | .execute() 32 | .fetch_one() 33 | ) 34 | 35 | def select_entity_all(self): 36 | return ( 37 | self.select(self.table_name) 38 | .execute() 39 | .fetch_all() 40 | ) 41 | 42 | def select_entity_by_id(self, id): 43 | return ( 44 | self.select(self.table_name) 45 | .where('id', id, operator='=') 46 | .execute() 47 | .fetch_all() 48 | ) 49 | 50 | def update_entity(self, entity): 51 | return ( 52 | self.update(self.table_name) 53 | .set('field', entity.field) 54 | .where('id', entity.id, operator='=') 55 | .execute() 56 | .fetch_one() 57 | ) 58 | -------------------------------------------------------------------------------- /samples/advanced/sources/hook.py: -------------------------------------------------------------------------------- 1 | from samples.advanced.sources.commons.singleton import Singleton 2 | 3 | 4 | def after_request(): 5 | Singleton.clear() 6 | 7 | 8 | def before_request(): 9 | pass 10 | -------------------------------------------------------------------------------- /samples/advanced/sources/log.py: -------------------------------------------------------------------------------- 1 | from samples.advanced.sources.utils import environment 2 | 3 | import logging 4 | import logging.config 5 | 6 | __logger__ = {} 7 | 8 | 9 | def get_logger(module): 10 | global __logger__ 11 | if module not in __logger__: 12 | logger = logging.getLogger(module) 13 | logger.setLevel(logging.DEBUG if environment.APPLICATION_DEBUG else logging.WARNING) 14 | __logger__[module] = logger 15 | return __logger__[module] 16 | -------------------------------------------------------------------------------- /samples/advanced/sources/main.py: -------------------------------------------------------------------------------- 1 | from bottle import response, run 2 | from samples.advanced.sources.application import application_factory 3 | from samples.advanced.sources.domains.database import Database 4 | from samples.advanced.sources.hook import after_request, before_request 5 | from samples.advanced.sources.log import get_logger 6 | from samples.advanced.sources.route import route 7 | from samples.advanced.sources.utils import environment 8 | 9 | logger = get_logger(__name__) 10 | 11 | application = application_factory() 12 | application.add_hook('after_request', after_request) 13 | application.add_hook('before_request', before_request) 14 | application.merge(route) 15 | 16 | Database.configuration() 17 | 18 | 19 | @application.hook('after_request') 20 | def content_type(): 21 | response.content_type = 'application/json; charset=utf-8' 22 | 23 | 24 | def server(): 25 | run( 26 | app=application, 27 | debug=environment.APPLICATION_DEBUG, 28 | host=environment.APPLICATION_HOST, 29 | port=environment.APPLICATION_PORT, 30 | reloader=environment.APPLICATION_RELOADER 31 | ) 32 | 33 | 34 | if __name__ == '__main__': 35 | server() 36 | -------------------------------------------------------------------------------- /samples/advanced/sources/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/resources/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/resources/entity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/resources/entity/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/resources/entity/resource.py: -------------------------------------------------------------------------------- 1 | from bottle import request 2 | from samples.advanced.sources.business.entity import EntityBusiness 3 | from samples.advanced.sources.commons.base import Base 4 | from samples.advanced.sources.commons.schema import Schema 5 | from samples.advanced.sources.log import get_logger 6 | from samples.advanced.sources.resources.entity.schema import EntitySchema 7 | 8 | import json 9 | 10 | logger = get_logger(__name__) 11 | 12 | entity_business = EntityBusiness() 13 | entity_resource = Base() 14 | 15 | 16 | @entity_resource.delete('/api/v1/entity/') 17 | def delete_entity(id): 18 | entity_business.delete_entity(id) 19 | 20 | 21 | @entity_resource.post('/api/v1/entity', schemas={ 22 | Schema.BODY: EntitySchema() 23 | }) 24 | def insert_entity(): 25 | data = dict(request.json) 26 | entity = entity_business.insert_entity(data.get('field')) 27 | return json.dumps({'data': entity}) 28 | 29 | 30 | @entity_resource.get('/api/v1/entity') 31 | def select_entity_all(): 32 | entities = entity_business.select_entity_all() 33 | return json.dumps({'data': entities}) 34 | 35 | 36 | @entity_resource.put('/api/v1/entity/', schemas={ 37 | Schema.BODY: EntitySchema() 38 | }) 39 | def update_entity(id): 40 | data = dict(request.json) 41 | entity = entity_business.update_entity(id, data.get('field')) 42 | return json.dumps({'data': entity}) 43 | -------------------------------------------------------------------------------- /samples/advanced/sources/resources/entity/schema.py: -------------------------------------------------------------------------------- 1 | from bottle_cerberus import Schema 2 | 3 | 4 | class EntitySchema(Schema): 5 | 6 | @staticmethod 7 | def schema(self): 8 | return { 9 | 'field': { 10 | 'empty': False, 11 | 'nullable': False, 12 | 'required': True, 13 | 'type': 'string' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/advanced/sources/route.py: -------------------------------------------------------------------------------- 1 | from bottle import Bottle 2 | from samples.advanced.sources.log import get_logger 3 | from samples.advanced.sources.resources.entity.resource import entity_resource 4 | 5 | logger = get_logger(__name__) 6 | 7 | route = Bottle() 8 | 9 | resources = [ 10 | entity_resource 11 | ] 12 | 13 | for resource in resources: 14 | route.merge(resource) 15 | -------------------------------------------------------------------------------- /samples/advanced/sources/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/advanced/sources/utils/__init__.py -------------------------------------------------------------------------------- /samples/advanced/sources/utils/environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | APPLICATION_DEBUG = os.environ.get('APPLICATION_DEBUG') 4 | APPLICATION_HOST = os.environ.get('APPLICATION_HOST') 5 | APPLICATION_PORT = os.environ.get('APPLICATION_PORT') 6 | APPLICATION_RELOADER = os.environ.get('APPLICATION_RELOADER') 7 | 8 | DATABASE_CONNECTION_TIMEOUT = os.environ.get('DATABASE_CONNECTION_TIMEOUT') 9 | DATABASE_HOST = os.environ.get('DATABASE_HOST') 10 | DATABASE_MAX_CONNECTION = os.environ.get('DATABASE_MAX_CONNECTION') 11 | DATABASE_NAME = os.environ.get('DATABASE_NAME') 12 | DATABASE_PASSWORD = os.environ.get('DATABASE_PASSWORD') 13 | DATABASE_PORT = os.environ.get('DATABASE_PORT') 14 | DATABASE_PRINT_SQL = os.environ.get('DATABASE_PRINT_SQL') 15 | DATABASE_USERNAME = os.environ.get('DATABASE_USERNAME') 16 | -------------------------------------------------------------------------------- /samples/basic/requirements.txt: -------------------------------------------------------------------------------- 1 | bottle==0.12.19 2 | bottle-postgresql==1.0.0 3 | -------------------------------------------------------------------------------- /samples/basic/sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/samples/basic/sources/__init__.py -------------------------------------------------------------------------------- /samples/basic/sources/main.py: -------------------------------------------------------------------------------- 1 | from bottle import Bottle, JSONPlugin, request, response, run 2 | from bottle_postgresql import Configuration, Database 3 | 4 | import datetime 5 | import decimal 6 | import json 7 | import os 8 | 9 | 10 | class JSONEncoder(json.JSONEncoder): 11 | 12 | def default(self, entity): 13 | if isinstance(entity, datetime.date): 14 | return entity.isoformat() 15 | if isinstance(entity, datetime.datetime): 16 | return entity.isoformat() 17 | if isinstance(entity, decimal.Decimal): 18 | return float(entity) 19 | return json.JSONEncoder.default(self, entity) 20 | 21 | 22 | application = Bottle() 23 | application.install(JSONPlugin(json_dumps=lambda entity: json.dumps(entity, cls=JSONEncoder))) 24 | 25 | configuration_dict = { 26 | 'connect_timeout': os.environ.get('DATABASE_CONNECTION_TIMEOUT'), 27 | 'dbname': os.environ.get('DATABASE_NAME'), 28 | 'host': os.environ.get('DATABASE_HOST'), 29 | 'maxconnections': os.environ.get('DATABASE_MAX_CONNECTION'), 30 | 'password': os.environ.get('DATABASE_PASSWORD'), 31 | 'port': os.environ.get('DATABASE_PORT'), 32 | 'print_sql': os.environ.get('DATABASE_PRINT_SQL'), 33 | 'user': os.environ.get('DATABASE_USERNAME') 34 | } 35 | 36 | configuration = Configuration(configuration_dict=configuration_dict) 37 | 38 | 39 | def connect(): 40 | return Database(configuration) 41 | 42 | 43 | @application.hook('after_request') 44 | def content_type(): 45 | response.content_type = 'application/json; charset=utf-8' 46 | 47 | 48 | @application.delete('/api/v1/entity/') 49 | def delete(id): 50 | with connect() as connection: 51 | entities = ( 52 | connection 53 | .delete('entities') 54 | .where('id', id, operator='=') 55 | .execute() 56 | ) 57 | return json.dumps({'data': entities}) 58 | 59 | 60 | @application.get('/api/v1/entity') 61 | def find_all(): 62 | with connect() as connection: 63 | entities = ( 64 | connection 65 | .select('entities') 66 | .execute() 67 | .fetch_all() 68 | ) 69 | return json.dumps({'data': entities}) 70 | 71 | 72 | @application.post('/api/v1/entity') 73 | def save(): 74 | data = dict(request.json) 75 | with connect() as connection: 76 | entities = ( 77 | connection 78 | .insert('entities') 79 | .set('field', data.get('field')) 80 | .execute() 81 | .fetch_one() 82 | ) 83 | return json.dumps({'data': entities}) 84 | 85 | 86 | @application.put('/api/v1/entity/') 87 | def update(id): 88 | data = dict(request.json) 89 | with connect() as connection: 90 | entities = ( 91 | connection 92 | .update('entities') 93 | .set('field', data.get('field')) 94 | .where('id', id, operator='=') 95 | .execute() 96 | .fetch_one() 97 | ) 98 | return json.dumps({'data': entities}) 99 | 100 | 101 | def server(): 102 | run( 103 | app=application, 104 | debug=True, 105 | host='0.0.0.0', 106 | port=8080, 107 | reloader=True 108 | ) 109 | 110 | 111 | if __name__ == '__main__': 112 | server() 113 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = true 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | 3 | import bottle_postgresql 4 | import os 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | with open(os.path.abspath('README.md')) as file: 12 | long_description = file.read() 13 | 14 | setup( 15 | author=bottle_postgresql.__author__, 16 | author_email=bottle_postgresql.__author_email__, 17 | classifiers=[ 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3', 22 | 'Topic :: Database' 23 | ], 24 | description='Bottle PostgreSQL is a simple adapter for PostgreSQL with connection pooling.', 25 | include_package_data=True, 26 | install_requires=[ 27 | 'dbutils', 28 | 'psycopg2-binary', 29 | 'pystache' 30 | ], 31 | keywords='bottle database postgresql psycopg2', 32 | license='MIT', 33 | long_description=long_description, 34 | long_description_content_type='text/markdown', 35 | name='bottle-postgresql', 36 | packages=find_packages(), 37 | platforms='any', 38 | py_modules=[ 39 | 'bottle_postgresql' 40 | ], 41 | url='https://github.com/bernardocouto/bottle-postgresql', 42 | version=bottle_postgresql.__version__ 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bernardocouto/bottle-postgresql/e1b5a9d09565ebb21f59ae0d41ea2f67319ee53f/tests/__init__.py -------------------------------------------------------------------------------- /tests/bottle_postgresql_test.py: -------------------------------------------------------------------------------- 1 | from bottle_postgresql import Configuration, Database, Page 2 | 3 | import unittest 4 | 5 | CONFIGURATION = Configuration.instance(configuration_file='configurations/configuration.json') 6 | 7 | 8 | class BottlePostgreSQLTest(unittest.TestCase): 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.database = Database(CONFIGURATION) 13 | with cls.database as connection: 14 | ( 15 | connection 16 | .execute( 17 | ''' 18 | create table if not exists test ( 19 | id bigserial not null, 20 | name varchar(100), 21 | description varchar(255), 22 | constraint test_primary_key primary key (id) 23 | ) 24 | ''', 25 | skip_load_query=True 26 | ) 27 | ) 28 | 29 | @classmethod 30 | def tearDownClass(cls): 31 | cls.database = Database(CONFIGURATION) 32 | with cls.database as connection: 33 | ( 34 | connection 35 | .execute('drop table if exists test', skip_load_query=True) 36 | ) 37 | 38 | def setUp(self): 39 | self.database = Database(CONFIGURATION) 40 | 41 | def tearDown(self): 42 | pass 43 | 44 | def test_delete(self): 45 | pass 46 | 47 | def test_find_all(self): 48 | with self.database as connection: 49 | ( 50 | connection 51 | .select('test') 52 | .fields('id', 'name', 'description') 53 | .execute() 54 | .fetch_all() 55 | ) 56 | 57 | def test_find_all_with_filter(self): 58 | with self.database as connection: 59 | test = ( 60 | connection 61 | .execute('test.find_all_with_filter', parameters={'id': 100}) 62 | .fetch_one() 63 | ) 64 | self.assertEqual(test, None) 65 | test = ( 66 | connection 67 | .execute('test.find_all_with_filter', parameters={'name': 'Test Name'}) 68 | .fetch_all() 69 | ) 70 | self.assertEqual(len(test), 0) 71 | 72 | def test_find_all_with_paging(self): 73 | with self.database as connection: 74 | ( 75 | connection 76 | .select('test') 77 | .fields('id', 'name', 'description') 78 | .paging(0, 1) 79 | ) 80 | 81 | def test_find_by_id(self): 82 | with self.database as connection: 83 | ( 84 | connection 85 | .select('test') 86 | .fields('id', 'name', 'description') 87 | .where('id', 1, operator='=') 88 | ) 89 | 90 | def test_find_by_id_with_file(self): 91 | with self.database as connection: 92 | ( 93 | connection 94 | .execute('test.find_by_id', parameters={'id': 1}) 95 | .fetch_one() 96 | ) 97 | 98 | def test_insert(self): 99 | with self.database as connection: 100 | test = ( 101 | connection 102 | .insert('test') 103 | .set('name', 'Test Name') 104 | .set('description', 'Test Description') 105 | .execute() 106 | .fetch_one() 107 | ) 108 | self.assertEqual(test['name'], 'Test Name') 109 | self.assertEqual(test['description'], 'Test Description') 110 | 111 | def test_insert_with_file(self): 112 | with self.database as connection: 113 | test = ( 114 | connection 115 | .execute('test.save', {'name': 'Test Name', 'description': 'Test Description'}) 116 | .fetch_one() 117 | ) 118 | self.assertEqual(test['name'], 'Test Name') 119 | self.assertEqual(test['description'], 'Test Description') 120 | 121 | 122 | if __name__ == '__main__': 123 | unittest.main() 124 | -------------------------------------------------------------------------------- /tests/configurations/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": "bottle-postgresql", 3 | "host": "localhost", 4 | "max_connection": 10, 5 | "password": "bottle-postgresql", 6 | "port": 5432, 7 | "print_sql": true, 8 | "username": "bottle-postgresql" 9 | } 10 | -------------------------------------------------------------------------------- /tests/queries/test.find_all_with_filter.sql: -------------------------------------------------------------------------------- 1 | select 2 | id, 3 | name, 4 | description 5 | from test 6 | where 1 = 1 7 | {{#id}} 8 | and id = %(id)s 9 | {{/id}} 10 | {{#name}} 11 | and name like %(name)s 12 | {{/name}} 13 | -------------------------------------------------------------------------------- /tests/queries/test.find_by_id.sql: -------------------------------------------------------------------------------- 1 | select 2 | id, 3 | name, 4 | description 5 | from test 6 | where id = %(id)s 7 | -------------------------------------------------------------------------------- /tests/queries/test.save.sql: -------------------------------------------------------------------------------- 1 | insert into test ( 2 | name, 3 | description 4 | ) values ( 5 | %(name)s, 6 | %(description)s 7 | ) 8 | returning * 9 | --------------------------------------------------------------------------------