├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── demo.gif ├── orm.py ├── setup.cfg ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Flask-Migrate 61 | # migrations/ 62 | 63 | # sqlite 64 | *.db 65 | db.* 66 | 67 | TODO* 68 | **.sqlite 69 | **.sqlite-journal 70 | **.env -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. 4 | 5 | See [the GitHub guide](https://guides.github.com/activities/contributing-to-open-source/) to contributing to this project. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Fernando Felix do Nascimento Junior 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include CONTRIBUTING.md 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Some useful commands to create a simple readme page with mkdocs. 2 | # 3 | # Author: Fernando Felix do Nascimento Junior 4 | # License: MIT License 5 | 6 | help: 7 | @echo 'Usage: make [command]' 8 | @echo 'Commands:' 9 | @echo ' env Create an isolated development environment.' 10 | @echo ' deps Install dependencies.' 11 | @echo ' build Create a dist package.' 12 | @echo ' install Install a local dist package with pip.' 13 | @echo ' clean Remove all Python, test and build artifacts.' 14 | @echo ' clean-build Remove build artifacts.' 15 | @echo ' clean-pyc Remove Python file artifacts.' 16 | @echo ' clean-test Remove test and coverage artifacts.' 17 | 18 | env: 19 | virtualenv env && . env/bin/activate && make deps 20 | 21 | deps: 22 | test -f requirements.txt && \ 23 | pip install -r requirements.txt || echo "requirements.txt doesn't exists" 24 | 25 | build: 26 | python setup.py egg_info sdist bdist_wheel 27 | ls -l dist 28 | 29 | install: build 30 | pip install dist/*.tar.gz 31 | 32 | clean: clean-pyc clean-test clean-build 33 | 34 | clean-build: 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr .eggs/ 38 | find . -name '*.egg-info' -exec rm -fr {} + 39 | find . -name '*.egg' -exec rm -f {} + 40 | 41 | clean-pyc: 42 | find . -name '*.pyc' -exec rm -f {} + 43 | find . -name '*.pyo' -exec rm -f {} + 44 | find . -name '*~' -exec rm -f {} + 45 | find . -name '__pycache__' -exec rm -fr {} + 46 | 47 | clean-test: 48 | rm -fr .cache/ 49 | rm -fr .tox/ 50 | rm -fr htmlcov/ 51 | rm -f .coverage 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-sqlite-orm 2 | 3 | A Python object relational mapper for SQLite. 4 | 5 | ## Install 6 | 7 | ```sh 8 | pip install sqlite-orm 9 | ``` 10 | 11 | ## Usage 12 | 13 | Following a basic tutorial to demonstrate how to use the ORM. 14 | 15 | 1. Define a `Post` model in a `post.py` file. 16 | 17 | ```py 18 | # post.py 19 | from orm import Model 20 | 21 | class Post(Model): 22 | 23 | text = str # other datatypes: int, float 24 | 25 | def __init__(self, text): 26 | self.text = text 27 | 28 | ``` 29 | 30 | 2. Import `Database` to create a data access object. 31 | 32 | ```py 33 | >>> from orm import Database 34 | >>> db = Database('db.sqlite') # indicating a database file. 35 | ``` 36 | 37 | 3. Import the `Post` model and link it to the database. 38 | 39 | ```py 40 | >>> from post import Post 41 | >>> Post.db = db # see another approach in tests.py 42 | ``` 43 | 44 | 4. Create a post and save it in the staging area (without commit) of database. 45 | 46 | ```py 47 | >>> post = Post('Hello World').save() 48 | >>> print(post.id) # auto generated id 49 | 1 50 | ``` 51 | 52 | 5. Change the hello world post and update it in the database. 53 | 54 | ```py 55 | >>> post = Post.manager().get(id=1) 56 | >>> post.text = 'Hello Mundo' 57 | >>> post.update() 58 | >>> post.text 59 | Hello Mundo 60 | ``` 61 | 62 | 6. Commit all staged operations (`save` and `update`) to the database. 63 | 64 | ```py 65 | >>> db.commit() 66 | ``` 67 | 68 | 7. Delete the object and commit. 69 | 70 | ```py 71 | >>> post.delete() 72 | >>> db.commit() 73 | ``` 74 | 75 | 8. Create a manager that can perform CRUD operations in the database. 76 | 77 | ```py 78 | >>> objects = Post.manager(db) 79 | ``` 80 | 81 | 9. Save and get a post. 82 | 83 | ```py 84 | >>> objects.save(Post('Hello', 'World')) 85 | >>> objects.get(2) # get by id from the staging area 86 | {'text': 'World', 'id': 2, 'title': 'Hello'} 87 | ``` 88 | 89 | 10. Close the database without commit the changes 90 | 91 | ```py 92 | >>> db.close() 93 | ``` 94 | 95 | 11. Get all posts from database. 96 | 97 | ```py 98 | >>> list(objects.all()) # return a "empty" generator 99 | [] 100 | ``` 101 | 102 | ## Linter 103 | 104 | Check code lint: 105 | 106 | ```sh 107 | pip install pylint 108 | pylint orm.py 109 | ``` 110 | 111 | ## Contributing 112 | 113 | See [CONTRIBUTING](/CONTRIBUTING.md). 114 | 115 | ## License 116 | 117 | [![CC0](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)](https://creativecommons.org/licenses/by-nc-sa/4.0/) 118 | 119 | The MIT License. 120 | 121 | - 122 | 123 | Copyright (c) 2014-2016 [Fernando Felix do Nascimento Junior](https://github.com/fernandojunior/). 124 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fernandojunior/python-sqlite-orm/7c7969437b22e0c11c27f449719d6561772e4cee/demo.gif -------------------------------------------------------------------------------- /orm.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A Python object relational mapper for SQLite. 3 | 4 | Reference: https://www.sqlite.org/lang.html 5 | 6 | Author: Fernando Felix do Nascimento Junior 7 | License: MIT License 8 | Homepage: https://github.com/fernandojunior/python-sqlite-orm 9 | ''' 10 | import sqlite3 11 | 12 | 13 | #: Dictionary to map Python and SQLite data types 14 | DATA_TYPES = {str: 'TEXT', int: 'INTEGER', float: 'REAL'} 15 | 16 | 17 | def attrs(obj): 18 | ''' Return attribute values dictionary for an object ''' 19 | return dict(i for i in vars(obj).items() if i[0][0] != '_') 20 | 21 | 22 | def copy_attrs(obj, remove=None): 23 | ''' Copy attribute values for an object ''' 24 | if remove is None: 25 | remove = [] 26 | return dict(i for i in attrs(obj).items() if i[0] not in remove) 27 | 28 | 29 | def render_column_definitions(model): 30 | ''' Create SQLite column definitions for an entity model ''' 31 | model_attrs = attrs(model).items() 32 | model_attrs = {k: v for k, v in model_attrs if k != 'db'} 33 | return ['%s %s' % (k, DATA_TYPES[v]) for k, v in model_attrs.items()] 34 | 35 | 36 | def render_create_table_stmt(model): 37 | ''' Render a SQLite statement to create a table for an entity model ''' 38 | sql = 'CREATE TABLE {table_name} (id integer primary key autoincrement, {column_def});' # noqa 39 | column_definitions = ', '.join(render_column_definitions(model)) 40 | params = {'table_name': model.__name__, 'column_def': column_definitions} 41 | return sql.format(**params) 42 | 43 | 44 | class Database(object): # pylint: disable=R0205 45 | ''' Proxy class to access sqlite3.connect method ''' 46 | 47 | def __init__(self, *args, **kwargs): 48 | self.args = args 49 | self.kwargs = kwargs 50 | self._connection = None 51 | self.connected = False 52 | self.Model = type('Model%s' % str(self), (Model,), {'db': self}) # pylint: disable=C0103 53 | 54 | @property 55 | def connection(self): 56 | ''' Create SQL connection ''' 57 | if self.connected: 58 | return self._connection 59 | self._connection = sqlite3.connect(*self.args, **self.kwargs) 60 | self._connection.row_factory = sqlite3.Row 61 | self.connected = True 62 | return self._connection 63 | 64 | def close(self): 65 | ''' Close SQL connection ''' 66 | if self.connected: 67 | self.connection.close() 68 | self.connected = False 69 | 70 | def commit(self): 71 | ''' Commit SQL changes ''' 72 | self.connection.commit() 73 | 74 | def execute(self, sql, *args): 75 | ''' Execute SQL ''' 76 | return self.connection.execute(sql, args) 77 | 78 | def executescript(self, script): 79 | ''' Execute SQL script ''' 80 | self.connection.cursor().executescript(script) 81 | self.commit() 82 | 83 | 84 | class Manager(object): # pylint: disable=R0205 85 | ''' Data mapper interface (generic repository) for models ''' 86 | 87 | def __init__(self, db, model, type_check=True): 88 | self.db = db 89 | self.model = model 90 | self.table_name = model.__name__ 91 | self.type_check = type_check 92 | if not self._hastable(): 93 | self.db.executescript(render_create_table_stmt(self.model)) 94 | 95 | def all(self): 96 | ''' Get all model objects from database ''' 97 | result = self.db.execute('SELECT * FROM %s' % self.table_name) 98 | return (self.create(**row) for row in result.fetchall()) 99 | 100 | def create(self, **kwargs): 101 | ''' Create a model object ''' 102 | obj = object.__new__(self.model) 103 | obj.__dict__ = kwargs 104 | return obj 105 | 106 | def delete(self, obj): 107 | ''' Delete a model object from database ''' 108 | sql = 'DELETE from %s WHERE id = ?' 109 | self.db.execute(sql % self.table_name, obj.id) 110 | 111 | def get(self, id): 112 | ''' Get a model object from database by its id ''' 113 | sql = 'SELECT * FROM %s WHERE id = ?' % self.table_name 114 | result = self.db.execute(sql, id) 115 | row = result.fetchone() 116 | if not row: 117 | msg = 'Object%s with id does not exist: %s' % (self.model, id) 118 | raise ValueError(msg) 119 | return self.create(**row) 120 | 121 | def has(self, id): 122 | ''' Check if a model object exists in database by its id ''' 123 | sql = 'SELECT id FROM %s WHERE id = ?' % self.table_name 124 | result = self.db.execute(sql, id) 125 | return True if result.fetchall() else False 126 | 127 | def save(self, obj): 128 | ''' Save a model object ''' 129 | if 'id' in obj.__dict__ and self.has(obj.id): 130 | msg = 'Object%s id already registred: %s' % (self.model, obj.id) 131 | raise ValueError(msg) 132 | clone = copy_attrs(obj, remove=['id']) 133 | self.type_check and self._isvalid(clone) 134 | column_names = '%s' % ', '.join(clone.keys()) 135 | column_references = '%s' % ', '.join('?' for i in range(len(clone))) 136 | sql = 'INSERT INTO %s (%s) VALUES (%s)' 137 | sql = sql % (self.table_name, column_names, column_references) 138 | result = self.db.execute(sql, *clone.values()) 139 | obj.id = result.lastrowid 140 | return obj 141 | 142 | def update(self, obj): 143 | ''' Update a model object ''' 144 | clone = copy_attrs(obj, remove=['id']) 145 | self.type_check and self._isvalid(clone) 146 | where_expressions = '= ?, '.join(clone.keys()) + '= ?' 147 | sql = 'UPDATE %s SET %s WHERE id = ?' % (self.table_name, where_expressions) # noqa 148 | self.db.execute(sql, *(list(clone.values()) + [obj.id])) 149 | 150 | def _hastable(self): 151 | ''' Check if entity model already has a database table ''' 152 | sql = 'SELECT name len FROM sqlite_master WHERE type = ? AND name = ?' 153 | result = self.db.execute(sql, 'table', self.table_name) 154 | return True if result.fetchall() else False 155 | 156 | def _isvalid(self, attr_values): 157 | ''' Check if an attr values dict are valid as specificated in model ''' 158 | attr_types = attrs(self.model) 159 | value_types = {a: v.__class__ for a, v in attr_values.items()} 160 | 161 | for attr, value_type in value_types.items(): 162 | if value_type is not attr_types[attr]: 163 | msg = "%s value should be type %s not %s" 164 | raise TypeError(msg % (attr, attr_types[attr], value_type)) 165 | 166 | 167 | class Model(object): # pylint: disable=R0205 168 | ''' Abstract entity model with an active record interface ''' 169 | 170 | db = None 171 | 172 | def delete(self, type_check=True): 173 | ''' Delete this model object ''' 174 | return self.__class__.manager(type_check=type_check).delete(self) 175 | 176 | def save(self, type_check=True): 177 | ''' Save this model object ''' 178 | return self.__class__.manager(type_check=type_check).save(self) 179 | 180 | def update(self, type_check=True): 181 | ''' Update this model object ''' 182 | return self.__class__.manager(type_check=type_check).update(self) 183 | 184 | @property 185 | def public(self): 186 | ''' Return the public model attributes ''' 187 | return attrs(self) 188 | 189 | def __repr__(self): 190 | return str(self.public) 191 | 192 | @classmethod 193 | def manager(cls, db=None, type_check=True): 194 | ''' Create a database managet ''' 195 | return Manager(db if db else cls.db, cls, type_check) 196 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Contains informations necessaries to build, release and install a distribution. 3 | ''' 4 | from setuptools import setup 5 | 6 | setup( 7 | name='sqlite-orm', 8 | version='0.0.2-beta', 9 | author='Fernando Felix do Nascimento Junior', 10 | url='https://github.com/fernandojunior/python-sqlite-orm', 11 | license='MIT License', 12 | description='A Python object relational mapper for SQLite.', 13 | py_modules=['orm'], 14 | classifiers=[ 15 | 'Development Status :: 4 - Beta', 16 | 'License :: OSI Approved :: MIT License', 17 | 'Natural Language :: English', 18 | 'Operating System :: OS Independent', 19 | "Programming Language :: Python :: 2", 20 | 'Programming Language :: Python :: 3', 21 | ], # see more at https://pypi.python.org/pypi?%3Aaction=list_classifiers 22 | zip_safe=False 23 | ) 24 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | from random import random 3 | from orm import Database 4 | 5 | db = Database('db.sqlite.test') 6 | 7 | 8 | class Post(db.Model): 9 | random = float 10 | text = str 11 | 12 | def __init__(self, text): 13 | self.text = text 14 | self.random = random() 15 | 16 | try: 17 | post = Post('Hello World').save() 18 | assert(post.id == 1) 19 | post.text = 'Hello Mundo' 20 | post.update() 21 | db.commit() 22 | post = Post.manager().get(id=1) 23 | assert(post.text == 'Hello Mundo') 24 | post.delete() 25 | db.commit() 26 | try: 27 | invalid_post = Post(None).save(type_check=False) 28 | except TypeError as e: 29 | assert(False) 30 | else: 31 | assert(True) 32 | objects = Post.manager() 33 | objects.save(Post('Hello World')) 34 | assert(set(objects.get(2).public.keys()) == set(['id', 'text', 'random'])) 35 | assert(isinstance(objects.get(2).random, float)) 36 | db.close() 37 | assert(list(objects.all()) == []) 38 | finally: 39 | os.remove('db.sqlite.test') 40 | --------------------------------------------------------------------------------