├── tests ├── __init__.py ├── conftest.py ├── test_operations.py ├── test_tables.py ├── test_middlewares.py ├── test_utils.py ├── test_flata.py ├── test_storages.py ├── test_queries.py └── test_crud.py ├── .coveragerc ├── tox.ini ├── .gitignore ├── flata ├── operations.py ├── __init__.py ├── storages.py ├── middlewares.py ├── utils.py ├── queries.py └── database.py ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── setup.py ├── CONTRIBUTING.rst └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: no cover 4 | raise NotImplementedError.* 5 | warnings\.warn.* 6 | def __repr__ 7 | def __str__ 8 | def main() 9 | if __name__ == .__main__.: 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py{26,27,33,34,35,36,py,py3} 8 | 9 | [testenv] 10 | commands = pytest -v 11 | deps = 12 | . 13 | pytest 14 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flata.middlewares import CachingMiddleware 4 | from flata.storages import MemoryStorage 5 | from flata import Flata 6 | 7 | 8 | @pytest.fixture 9 | def db(): 10 | db_ = Flata(storage=MemoryStorage) 11 | db_.purge_tables() 12 | db_.table('t').insert_multiple({'int': 1, 'char': c} for c in 'abc') 13 | return db_ 14 | 15 | 16 | @pytest.fixture 17 | def storage(): 18 | _storage = CachingMiddleware(MemoryStorage) 19 | return _storage() # Initialize MemoryStorage 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Pycharm 39 | .idea 40 | 41 | *.db.yml 42 | -------------------------------------------------------------------------------- /flata/operations.py: -------------------------------------------------------------------------------- 1 | def delete(field): 2 | """ 3 | Delete a given field from the element. 4 | """ 5 | def transform(element): 6 | del element[field] 7 | 8 | return transform 9 | 10 | 11 | def increment(field): 12 | """ 13 | Increment a given field in the element. 14 | """ 15 | def transform(element): 16 | element[field] += 1 17 | 18 | return transform 19 | 20 | 21 | def decrement(field): 22 | """ 23 | Decrement a given field in the element. 24 | """ 25 | def transform(element): 26 | element[field] -= 1 27 | 28 | return transform 29 | -------------------------------------------------------------------------------- /tests/test_operations.py: -------------------------------------------------------------------------------- 1 | from flata import where 2 | from flata.operations import delete, increment, decrement 3 | 4 | 5 | def test_delete(db): 6 | db.table('t').update(delete('int'), where('char') == 'a') 7 | assert 'int' not in db.table('t').get(where('char') == 'a') 8 | 9 | 10 | def test_increment(db): 11 | db.table('t').update(increment('int'), where('char') == 'a') 12 | assert db.table('t').get(where('char') == 'a')['int'] == 2 13 | 14 | 15 | def test_decrement(db): 16 | db.table('t').update(decrement('int'), where('char') == 'a') 17 | assert db.table('t').get(where('char') == 'a')['int'] == 0 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "pypy-5.4.1" 5 | - "pypy3.3-5.2-alpha1" 6 | - "3.6" 7 | - "3.5" 8 | - "3.4" 9 | - "3.3" 10 | - "2.7" 11 | - "2.6" 12 | matrix: 13 | allow_failures: 14 | - python: nightly 15 | install: 16 | - pip install -U pytest 17 | - pip install coverage 18 | - pip install coveralls 19 | - pip install pytest-cov 20 | script: 21 | py.test -v --cov flata 22 | after_success: 23 | coveralls 24 | notifications: 25 | email: 26 | recipients: 27 | - harry.ho_long@yahoo.com 28 | on_success: never # default: change 29 | on_failure: always # default: always -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python", 6 | "type": "python", 7 | "request": "launch", 8 | "stopOnEntry": true, 9 | "pythonPath": "${config:python.pythonPath}", 10 | "program": "${file}", 11 | "cwd": "${workspaceRoot}", 12 | "env": {}, 13 | "envFile": "${workspaceRoot}/.env", 14 | "debugOptions": [ 15 | "WaitOnAbnormalExit", 16 | "WaitOnNormalExit", 17 | "RedirectOutput" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 Harry Ho 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /flata/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Flata (Version: 3.3.1) 4 | 5 | Flata is a tiny, document oriented database optimized for your happiness :) 6 | 7 | Flata stores differrent types of python data types using a configurable 8 | backend. It has support for handy querying and tables. 9 | 10 | .. codeauthor:: Harry Ho 11 | 12 | Usage example: 13 | 14 | >>> from flata. import Flata, where 15 | >>> from flata.storages import MemoryStorage 16 | >>> db = Flata(storage=MemoryStorage) 17 | >>> tb = db.table('table1') 18 | >>> tb.insert({'data': 0}) 19 | >>> tb.search(where('data') == 5) 20 | [{'data': 5, 'id': 1}] 21 | >>> # Now let's create a new table 22 | >>> tbl = db.table('our_table') 23 | >>> for i in range(10): 24 | ... tbl.insert({'data': i}) 25 | ... 26 | >>> len(tbl.search(where('data') < 5)) 27 | 5 28 | """ 29 | 30 | from .queries import Query, where 31 | from .storages import Storage, JSONStorage, MemoryStorage 32 | from .middlewares import Middleware, CachingMiddleware 33 | from .database import Flata 34 | 35 | __all__ = ('Flata', 'Storage', 'JSONStorage', 'MemoryStorage', 'Middleware', 'CachingMiddleware', 'Query', 'where') 36 | 37 | 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from setuptools import setup, find_packages 3 | from codecs import open 4 | import os 5 | 6 | 7 | def read(fname): 8 | path = os.path.join(os.path.dirname(__file__), fname) 9 | return open(path, encoding='utf-8').read() 10 | 11 | 12 | setup( 13 | name="flata", 14 | version="5.0.0", 15 | packages=find_packages(), 16 | 17 | # development metadata 18 | zip_safe=True, 19 | 20 | # metadata for upload to PyPI 21 | author="Harry Ho", 22 | author_email="harry.ho_long@yahoo.com", 23 | description="Flata is inspired by TinyDB and lowdb. It is a tiny, document oriented database optimized for " 24 | "FlatApi and fun :)", 25 | license="MIT", 26 | keywords="database json nosql", 27 | url="https://github.com/harryho/flata", 28 | classifiers=[ 29 | "Development Status :: 5 - Production/Stable", 30 | "Intended Audience :: Developers", 31 | "Intended Audience :: System Administrators", 32 | "License :: OSI Approved :: MIT License", 33 | "Topic :: Database", 34 | "Topic :: Database :: Database Engines/Servers", 35 | "Topic :: Utilities", 36 | "Programming Language :: Python :: 2.6", 37 | "Programming Language :: Python :: 2.7", 38 | "Programming Language :: Python :: 3.3", 39 | "Programming Language :: Python :: 3.4", 40 | "Programming Language :: Python :: 3.5", 41 | "Programming Language :: Python :: 3.6", 42 | "Programming Language :: Python :: Implementation :: PyPy", 43 | "Operating System :: OS Independent" 44 | ], 45 | 46 | long_description=read('README.rst'), 47 | ) 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ####################### 3 | 4 | Whether reporting bugs, discussing improvements and new ideas or writing 5 | extensions: Contributions to Flata are welcome! Here's how to get started: 6 | 7 | 1. Check for open issues or open a fresh issue to start a discussion around 8 | a feature idea or a bug 9 | 2. Fork `the repository `_ on GitHub, 10 | create a new branch off the `master` branch and start making your changes 11 | (known as `GitHub Flow `_) 12 | 3. Write a test which shows that the bug was fixed or that the feature works 13 | as expected 14 | 4. Send a pull request and bug the maintainer until it gets merged and 15 | published :) 16 | 17 | Philosophy of Flata 18 | ******************** 19 | 20 | Flata aims to be simple and fun to use. Therefore two key values are simplicity 21 | and elegance of interfaces and code. These values will contradict each other 22 | from time to time. In these cases , try using as little magic as possible. 23 | In any case don't forget documenting code that isn't clear at first glance. 24 | 25 | Code Conventions 26 | **************** 27 | 28 | In general the Flata source should always follow `PEP 8 `_. 29 | Exceptions are allowed in well justified and documented cases. However we make 30 | a small exception concerning docstrings: 31 | 32 | When using multiline docstrings, keep the opening and closing triple quotes 33 | on their own lines and add an empty line after it. 34 | 35 | .. code-block:: python 36 | 37 | def some_function(): 38 | """ 39 | Documentation ... 40 | """ 41 | 42 | # implementation ... 43 | 44 | Version Numbers 45 | *************** 46 | 47 | Flata follows the `SemVer versioning guidelines `_. 48 | This implies that backwards incompatible changes in the API will increment 49 | the major version. So think twice before making such changes. 50 | -------------------------------------------------------------------------------- /tests/test_tables.py: -------------------------------------------------------------------------------- 1 | from flata import where 2 | 3 | 4 | def test_tables_list(db): 5 | db.table('table1') 6 | db.table('table2') 7 | 8 | assert db.tables() == set(['t','table1', 'table2']) 9 | 10 | 11 | def test_one_table(db): 12 | table1 = db.table('table1') 13 | 14 | table1.insert_multiple({'int': 1, 'char': c} for c in 'abc') 15 | 16 | assert table1.get(where('int') == 1)['char'] == 'a' 17 | assert table1.get(where('char') == 'b')['char'] == 'b' 18 | 19 | 20 | def test_multiple_tables(db): 21 | table1 = db.table('table1') 22 | table2 = db.table('table2') 23 | table3 = db.table('table3') 24 | 25 | table1.insert({'int': 1, 'char': 'a'}) 26 | table2.insert({'int': 1, 'char': 'b'}) 27 | table3.insert({'int': 1, 'char': 'c'}) 28 | 29 | assert table1.count(where('char') == 'a') == 1 30 | assert table2.count(where('char') == 'b') == 1 31 | assert table3.count(where('char') == 'c') == 1 32 | 33 | db.purge_tables() 34 | 35 | assert len(table1) == 0 36 | assert len(table2) == 0 37 | assert len(table3) == 0 38 | 39 | 40 | def test_caching(db): 41 | table1 = db.table('table1') 42 | table2 = db.table('table1') 43 | 44 | assert table1 is table2 45 | 46 | 47 | def test_query_cache_size(db): 48 | table = db.table('table3', cache_size=1) 49 | query = where('int') == 1 50 | 51 | table.insert({'int': 1}) 52 | table.insert({'int': 1}) 53 | 54 | assert table.count(query) == 2 55 | assert table.count(where('int') == 2) == 0 56 | assert len(table._query_cache) == 1 57 | 58 | 59 | def test_lru_cache(db): 60 | # Test integration into Flata 61 | table = db.table('table3', cache_size=2) 62 | query = where('int') == 1 63 | 64 | table.search(query) 65 | table.search(where('int') == 2) 66 | table.search(where('int') == 3) 67 | assert query not in table._query_cache 68 | 69 | table.remove(where('int') == 1) 70 | assert not table._query_cache.lru 71 | 72 | table.search(query) 73 | 74 | assert len(table._query_cache) == 1 75 | table.clear_cache() 76 | assert len(table._query_cache) == 0 77 | 78 | 79 | def test_table_is_iterable(db): 80 | table = db.table('table1') 81 | 82 | table.insert_multiple({'int': i} for i in range(3)) 83 | 84 | assert [r for r in table] == table.all() 85 | -------------------------------------------------------------------------------- /tests/test_middlewares.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flata import Flata 4 | from flata.middlewares import CachingMiddleware 5 | from flata.storages import MemoryStorage, JSONStorage 6 | 7 | if 'xrange' not in dir(__builtins__): 8 | # noinspection PyShadowingBuiltins 9 | xrange = range # Python 3 support 10 | 11 | 12 | element = {'none': [None, None], 'int': 42, 'float': 3.1415899999999999, 13 | 'list': ['LITE', 'RES_ACID', 'SUS_DEXT'], 14 | 'dict': {'hp': 13, 'sp': 5}, 15 | 'bool': [True, False, True, False]} 16 | 17 | 18 | def test_caching(storage): 19 | # Write contents 20 | storage.write(element) 21 | 22 | # Verify contents 23 | assert element == storage.read() 24 | 25 | 26 | def test_caching_read(): 27 | db = Flata(storage=CachingMiddleware(MemoryStorage)) 28 | assert not db.all() 29 | 30 | 31 | def test_caching_write_many(storage): 32 | storage.WRITE_CACHE_SIZE = 3 33 | 34 | # Storage should be still empty 35 | assert storage.memory is None 36 | 37 | # Write contents 38 | for x in xrange(2): 39 | storage.write(element) 40 | assert storage.memory is None # Still cached 41 | 42 | storage.write(element) 43 | 44 | # Verify contents: Cache should be emptied and written to storage 45 | assert storage.memory 46 | 47 | 48 | def test_caching_flush(storage): 49 | # Write contents 50 | storage.write(element) 51 | 52 | storage.flush() 53 | 54 | # Verify contents: Cache should be emptied and written to storage 55 | assert storage.memory 56 | 57 | 58 | def test_caching_write(storage): 59 | # Write contents 60 | storage.write(element) 61 | 62 | storage.close() 63 | 64 | # Verify contents: Cache should be emptied and written to storage 65 | assert storage.storage.memory 66 | 67 | 68 | def test_nested(): 69 | storage = CachingMiddleware(MemoryStorage) 70 | storage() # Initialization 71 | 72 | # Write contents 73 | storage.write(element) 74 | 75 | # Verify contents 76 | assert element == storage.read() 77 | 78 | 79 | def test_caching_json_write(tmpdir): 80 | path = str(tmpdir.join('test.db')) 81 | 82 | with Flata(path, storage=CachingMiddleware(JSONStorage)) as db: 83 | db.table('t').insert({'key': 'value'}) 84 | 85 | # Verify database filesize 86 | statinfo = os.stat(path) 87 | assert statinfo.st_size != 0 88 | 89 | # Assert JSON file has been closed 90 | assert db._storage._handle.closed 91 | 92 | del db 93 | 94 | # Repoen database 95 | with Flata(path, storage=CachingMiddleware(JSONStorage)) as db: 96 | assert db.table('t').all() == [{'id':1, 'key': 'value'}] 97 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import pytest 3 | 4 | from flata.utils import LRUCache, catch_warning, freeze, FrozenDict 5 | 6 | 7 | def test_lru_cache(): 8 | cache = LRUCache(capacity=3) 9 | cache["a"] = 1 10 | cache["b"] = 2 11 | cache["c"] = 3 12 | _ = cache["a"] # move to front in lru queue 13 | cache["d"] = 4 # move oldest item out of lru queue 14 | 15 | try: 16 | _ = cache['f'] 17 | except KeyError: 18 | pass 19 | 20 | assert cache.lru == ["c", "a", "d"] 21 | 22 | 23 | def test_lru_cache_set_multiple(): 24 | cache = LRUCache(capacity=3) 25 | cache["a"] = 1 26 | cache["a"] = 2 27 | cache["a"] = 3 28 | cache["a"] = 4 29 | 30 | assert cache.lru == ["a"] 31 | 32 | 33 | def test_lru_cache_get(): 34 | cache = LRUCache(capacity=3) 35 | cache["a"] = 1 36 | cache["b"] = 1 37 | cache["c"] = 1 38 | cache.get("a") 39 | cache["d"] = 4 40 | 41 | assert cache.lru == ["c", "a", "d"] 42 | 43 | 44 | def test_lru_cache_delete(): 45 | cache = LRUCache(capacity=3) 46 | cache["a"] = 1 47 | cache["b"] = 2 48 | del cache["a"] 49 | 50 | try: 51 | del cache['f'] 52 | except KeyError: 53 | pass 54 | 55 | assert cache.lru == ["b"] 56 | 57 | 58 | def test_lru_cache_clear(): 59 | cache = LRUCache(capacity=3) 60 | cache["a"] = 1 61 | cache["b"] = 2 62 | cache.clear() 63 | 64 | assert cache.lru == [] 65 | 66 | 67 | def test_lru_cache_unlimited(): 68 | cache = LRUCache() 69 | for i in range(100): 70 | cache[i] = i 71 | 72 | assert len(cache.lru) == 100 73 | 74 | 75 | def test_lru_cache_unlimited_explicit(): 76 | cache = LRUCache(capacity=None) 77 | for i in range(100): 78 | cache[i] = i 79 | 80 | assert len(cache.lru) == 100 81 | 82 | 83 | def test_catch_warning(): 84 | class MyWarning(Warning): 85 | pass 86 | 87 | filters = warnings.filters[:] 88 | 89 | with pytest.raises(MyWarning): 90 | with catch_warning(MyWarning): 91 | warnings.warn("message", MyWarning) 92 | 93 | assert filters == warnings.filters 94 | 95 | 96 | def test_catch_warning_reset_filter(): 97 | class MyWarning(Warning): 98 | pass 99 | 100 | warnings.filterwarnings(action='once', category=MyWarning) 101 | 102 | with pytest.raises(MyWarning): 103 | with catch_warning(MyWarning): 104 | warnings.warn("message", MyWarning) 105 | 106 | filters = [f for f in warnings.filters if f[2] == MyWarning] 107 | assert filters 108 | assert filters[0][0] == 'once' 109 | 110 | 111 | def test_freeze(): 112 | frozen = freeze([0, 1, 2, {'a': [1, 2, 3]}]) 113 | assert isinstance(frozen, tuple) 114 | assert isinstance(frozen[3], FrozenDict) 115 | assert isinstance(frozen[3]['a'], tuple) 116 | 117 | with pytest.raises(TypeError): 118 | frozen[0] = 10 119 | 120 | with pytest.raises(TypeError): 121 | frozen[3]['a'] = 10 122 | -------------------------------------------------------------------------------- /tests/test_flata.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flata import Flata, where 4 | from flata.middlewares import CachingMiddleware 5 | from flata.storages import MemoryStorage, JSONStorage 6 | # from flata.middlewares import Middleware 7 | 8 | 9 | def test_purge(db): 10 | db.purge_tables() 11 | tb = db.table('t') 12 | tb.insert({}) 13 | db.purge_tables() 14 | 15 | assert len(db) == 0 16 | 17 | 18 | def test_table_all(db): 19 | db.purge_tables() 20 | 21 | tb = db.table('t') 22 | for i in range(10): 23 | tb.insert({}) 24 | 25 | # pp(list(db.all().values()).len()) 26 | assert len(db.all()) == 1 27 | assert len(tb.all()) == 10 28 | 29 | def test_purge_table(): 30 | table_name = 'some-other-table' 31 | db = Flata(storage=MemoryStorage) 32 | db.table(table_name) 33 | assert set([table_name]) == db.tables() 34 | 35 | db.purge_table(table_name) 36 | assert not set([table_name]) == db.tables() 37 | 38 | def test_storage_closed_once(): 39 | class Storage(object): 40 | def __init__(self): 41 | self.closed = False 42 | 43 | def read(self): 44 | return {} 45 | 46 | def write(self, data): 47 | pass 48 | 49 | def close(self): 50 | assert not self.closed 51 | self.closed = True 52 | 53 | with Flata(storage=Storage) as db: 54 | db.close() 55 | 56 | del db 57 | # If db.close() is called during cleanup, the assertion will fail and throw 58 | # and exception 59 | 60 | def test_flata_memory_storage(): 61 | with Flata('db.json', storage=MemoryStorage) as db: 62 | assert isinstance( db._storage, MemoryStorage) 63 | db.close() 64 | 65 | def test_flata_caching_memory_storage(): 66 | with Flata('db.json', storage=CachingMiddleware(MemoryStorage)) as db: 67 | assert isinstance( db._storage, (CachingMiddleware, MemoryStorage)) 68 | db.close() 69 | 70 | def test_flata_external_cache_memory_storage(): 71 | _cache = CachingMiddleware(MemoryStorage)() 72 | with Flata('db.json', cache=_cache) as db: 73 | assert isinstance( db._storage, (CachingMiddleware, MemoryStorage)) 74 | db.close() 75 | 76 | def test_flata_json_storage(): 77 | with Flata('db.json', storage=JSONStorage) as db: 78 | assert isinstance( db._storage, JSONStorage) 79 | db.close() 80 | 81 | def test_flata_caching_json_storage(): 82 | with Flata('db.json', storage=CachingMiddleware(JSONStorage)) as db: 83 | assert isinstance( db._storage, (CachingMiddleware,JSONStorage)) 84 | db.close() 85 | 86 | def test_flata_external_cache_json_storage(): 87 | _cache = CachingMiddleware(JSONStorage)('db.json') 88 | with Flata('db.json', cache=_cache) as db: 89 | assert isinstance( db._storage, (CachingMiddleware, JSONStorage)) 90 | db.close() 91 | 92 | def test_flata_default_storage(): 93 | with Flata('db.json', storage=JSONStorage) as db: 94 | assert isinstance( db._storage, JSONStorage) 95 | db.close() -------------------------------------------------------------------------------- /tests/test_storages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import tempfile 4 | 5 | import pytest 6 | 7 | 8 | random.seed() 9 | 10 | from flata import Flata, where 11 | from flata.storages import JSONStorage, MemoryStorage, Storage 12 | 13 | element = {'none': [None, None], 'int': 42, 'float': 3.1415899999999999, 14 | 'list': ['LITE', 'RES_ACID', 'SUS_DEXT'], 15 | 'dict': {'hp': 13, 'sp': 5}, 16 | 'bool': [True, False, True, False]} 17 | 18 | 19 | def test_json(tmpdir): 20 | # Write contents 21 | path = str(tmpdir.join('test.db')) 22 | storage = JSONStorage(path) 23 | storage.write(element) 24 | 25 | # Verify contents 26 | assert element == storage.read() 27 | storage.close() 28 | 29 | 30 | def test_json_kwargs(tmpdir): 31 | db_file = tmpdir.join('test.db.json') 32 | db = Flata(str(db_file), sort_keys=True, indent=4, separators=(',', ': ')) 33 | 34 | # Create table test_table 35 | tb = db.table('test_table') 36 | 37 | # Write contents 38 | tb.insert({'b': 1}) 39 | # tb.insert({'a': 1}) 40 | print(db_file.read()) 41 | 42 | assert db_file.read() == '''{ 43 | "test_table": [ 44 | { 45 | "b": 1, 46 | "id": 1 47 | } 48 | ] 49 | }''' 50 | db.close() 51 | 52 | 53 | def test_json_readwrite(tmpdir): 54 | """ 55 | Regression test for issue #1 56 | """ 57 | path = str(tmpdir.join('test.db.json')) 58 | 59 | # Create Flata instance 60 | db = Flata(path, storage=JSONStorage) 61 | 62 | # Create table test_table 63 | tb = db.table('test_table') 64 | 65 | item = {'data': 'data exists'} 66 | item2 = {'data': 'data not exists'} 67 | 68 | get = lambda s: tb.get(where('data') == s) 69 | 70 | tb.insert(item) 71 | assert dict(get('data exists'))['data'] == 'data exists' 72 | 73 | assert get('data not exists') is None 74 | 75 | tb.remove(where('data') == 'data exists') 76 | assert get('data exists') is None 77 | 78 | db.close() 79 | 80 | 81 | def test_create_dirs(): 82 | temp_dir = tempfile.gettempdir() 83 | db_dir = '' 84 | db_file = '' 85 | 86 | while True: 87 | dname = os.path.join(temp_dir, str(random.getrandbits(20))) 88 | if not os.path.exists(dname): 89 | db_dir = dname 90 | db_file = os.path.join(db_dir, 'db.json') 91 | break 92 | 93 | db_conn = JSONStorage(db_file, create_dirs=True) 94 | db_conn.close() 95 | 96 | db_exists = os.path.exists(db_file) 97 | 98 | os.remove(db_file) 99 | os.rmdir(db_dir) 100 | 101 | assert db_exists 102 | 103 | 104 | def test_json_invalid_directory(): 105 | with pytest.raises(IOError): 106 | with Flata('/this/is/an/invalid/path/db.json', storage=JSONStorage): 107 | pass 108 | 109 | 110 | def test_in_memory(): 111 | # Write contents 112 | storage = MemoryStorage() 113 | storage.write(element) 114 | 115 | # Verify contents 116 | assert element == storage.read() 117 | 118 | # Test case for #21 119 | other = MemoryStorage() 120 | other.write({}) 121 | assert other.read() != storage.read() 122 | 123 | 124 | def test_in_memory_close(): 125 | with Flata('', storage=MemoryStorage) as db: 126 | db.table('t').insert({}) 127 | 128 | 129 | def test_custom(): 130 | # noinspection PyAbstractClass 131 | class MyStorage(Storage): 132 | pass 133 | 134 | with pytest.raises(TypeError): 135 | MyStorage() 136 | -------------------------------------------------------------------------------- /flata/storages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the :class:`base class ` for storages and 3 | implementations. 4 | """ 5 | 6 | from abc import ABCMeta, abstractmethod 7 | import os 8 | 9 | from .utils import with_metaclass 10 | 11 | 12 | try: 13 | import ujson as json 14 | except ImportError: 15 | import json 16 | 17 | 18 | def touch(fname, times=None, create_dirs=False): 19 | if create_dirs: 20 | base_dir = os.path.dirname(fname) 21 | if not os.path.exists(base_dir): 22 | os.makedirs(base_dir) 23 | with open(fname, 'a'): 24 | os.utime(fname, times) 25 | 26 | 27 | class Storage(with_metaclass(ABCMeta, object)): 28 | """ 29 | The abstract base class for all Storages. 30 | 31 | A Storage (de)serializes the current state of the database and stores it in 32 | some place (memory, file on disk, ...). 33 | """ 34 | 35 | # Using ABCMeta as metaclass allows instantiating only storages that have 36 | # implemented read and write 37 | 38 | @abstractmethod 39 | def read(self): 40 | """ 41 | Read the last stored state. 42 | 43 | Any kind of deserialization should go here. 44 | Return ``None`` here to indicate that the storage is empty. 45 | 46 | :rtype: dict 47 | """ 48 | 49 | raise NotImplementedError('To be overridden!') 50 | 51 | @abstractmethod 52 | def write(self, data): 53 | """ 54 | Write the current state of the database to the storage. 55 | 56 | Any kind of serialization should go here. 57 | 58 | :param data: The current state of the database. 59 | :type data: dict 60 | """ 61 | 62 | raise NotImplementedError('To be overridden!') 63 | 64 | def close(self): 65 | """ 66 | Optional: Close open file handles, etc. 67 | """ 68 | 69 | pass 70 | 71 | 72 | class JSONStorage(Storage): 73 | """ 74 | Store the data in a JSON file. 75 | """ 76 | 77 | def __init__(self, path, create_dirs=False, **kwargs): 78 | """ 79 | Create a new instance. 80 | 81 | Also creates the storage file, if it doesn't exist. 82 | 83 | :param path: Where to store the JSON data. 84 | :type path: str 85 | """ 86 | 87 | super(JSONStorage, self).__init__() 88 | touch(path, create_dirs=create_dirs) # Create file if not exists 89 | self.kwargs = kwargs 90 | self._handle = open(path, 'r+') 91 | 92 | def close(self): 93 | self._handle.close() 94 | 95 | def read(self): 96 | # Get the file size 97 | self._handle.seek(0, os.SEEK_END) 98 | size = self._handle.tell() 99 | 100 | if not size: 101 | # File is empty 102 | return None 103 | else: 104 | self._handle.seek(0) 105 | return json.load(self._handle) 106 | 107 | def write(self, data): 108 | self._handle.seek(0) 109 | serialized = json.dumps(data, **self.kwargs) 110 | self._handle.write(serialized) 111 | self._handle.flush() 112 | self._handle.truncate() 113 | 114 | 115 | class MemoryStorage(Storage): 116 | """ 117 | Store the data as JSON in memory. 118 | """ 119 | 120 | def __init__(self,*args, **kwargs): 121 | """ 122 | Create a new instance. 123 | """ 124 | 125 | super(MemoryStorage, self).__init__() 126 | self.memory = None 127 | 128 | def read(self): 129 | return self.memory 130 | 131 | def write(self, data): 132 | self.memory = data 133 | -------------------------------------------------------------------------------- /flata/middlewares.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the :class:`base class ` for 3 | middlewares and implementations. 4 | """ 5 | from .database import Flata 6 | 7 | 8 | class Middleware(object): 9 | """ 10 | The base class for all Middlewares. 11 | 12 | Middlewares hook into the read/write process of Flata allowing you to 13 | extend the behaviour by adding caching, logging, ... 14 | 15 | Your middleware's ``__init__`` method has to accept exactly one 16 | argument which is the class of the "real" storage. It has to be stored as 17 | ``_storage_cls`` (see :class:`~flata.middlewares.CachingMiddleware` for an 18 | example). 19 | """ 20 | 21 | def __init__(self, storage_cls=Flata.DEFAULT_STORAGE): 22 | self._storage_cls = storage_cls 23 | self.storage = None 24 | 25 | def __call__(self, *args, **kwargs): 26 | """ 27 | Create the storage instance and store it as self.storage. 28 | 29 | Usually a user creates a new Flata instance like this:: 30 | 31 | Flata(storage=StorageClass) 32 | 33 | The storage kwarg is used by Flata this way:: 34 | 35 | self.storage = storage(*args, **kwargs) 36 | 37 | As we can see, ``storage(...)`` runs the constructor and returns the 38 | new storage instance. 39 | 40 | 41 | Using Middlewares, the user will call:: 42 | 43 | The 'real' storage class 44 | v 45 | Flata(storage=Middleware(StorageClass)) 46 | ^ 47 | Already an instance! 48 | 49 | So, when running ``self.storage = storage(*args, **kwargs)`` Python 50 | now will call ``__call__`` and Flata will expect the return value to 51 | be the storage (or Middleware) instance. Returning the instance is 52 | simple, but we also got the underlying (*real*) StorageClass as an 53 | __init__ argument that still is not an instance. 54 | So, we initialize it in __call__ forwarding any arguments we recieve 55 | from Flata (``Flata(arg1, kwarg1=value, storage=...)``). 56 | 57 | In case of nested Middlewares, calling the instance as if it was an 58 | class results in calling ``__call__`` what initializes the next 59 | nested Middleware that itself will initialize the next Middleware and 60 | so on. 61 | """ 62 | 63 | self.storage = self._storage_cls(*args, **kwargs) 64 | 65 | return self 66 | 67 | def __getattr__(self, name): 68 | """ 69 | Forward all unknown attribute calls to the underlying storage so we 70 | remain as transparent as possible. 71 | """ 72 | 73 | return getattr(self.__dict__['storage'], name) 74 | 75 | 76 | class CachingMiddleware(Middleware): 77 | """ 78 | Add some caching to Flata. 79 | 80 | This Middleware aims to improve the performance of Flata by writing only 81 | the last DB state every :attr:`WRITE_CACHE_SIZE` time and reading always 82 | from cache. 83 | """ 84 | 85 | #: The number of write operations to cache before writing to disc 86 | WRITE_CACHE_SIZE = 1000 87 | 88 | def __init__(self, storage_cls=Flata.DEFAULT_STORAGE): 89 | super(CachingMiddleware, self).__init__(storage_cls) 90 | 91 | self.cache = None 92 | self._cache_modified_count = 0 93 | 94 | def read(self): 95 | if self.cache is None: 96 | self.cache = self.storage.read() 97 | return self.cache 98 | 99 | def write(self, data): 100 | self.cache = data 101 | self._cache_modified_count += 1 102 | 103 | if self._cache_modified_count >= self.WRITE_CACHE_SIZE: 104 | self.flush() 105 | 106 | def flush(self): 107 | """ 108 | Flush all unwritten data to disk. 109 | """ 110 | if self._cache_modified_count > 0: 111 | self.storage.write(self.cache) 112 | self._cache_modified_count = 0 113 | 114 | def close(self): 115 | self.flush() # Flush potentially unwritten data 116 | self.storage.close() 117 | -------------------------------------------------------------------------------- /flata/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions. 3 | """ 4 | 5 | from contextlib import contextmanager 6 | import warnings 7 | 8 | # Python 2/3 independant dict iteration 9 | iteritems = getattr(dict, 'iteritems', dict.items) 10 | itervalues = getattr(dict, 'itervalues', dict.values) 11 | 12 | 13 | class LRUCache(dict): 14 | """ 15 | A simple LRU cache. 16 | """ 17 | 18 | def __init__(self, *args, **kwargs): 19 | """ 20 | :param capacity: How many items to store before cleaning up old items 21 | or ``None`` for an unlimited cache size 22 | """ 23 | 24 | self.capacity = kwargs.pop('capacity', None) or float('nan') 25 | self.lru = [] 26 | 27 | super(LRUCache, self).__init__(*args, **kwargs) 28 | 29 | def refresh(self, key): 30 | """ 31 | Push a key to the tail of the LRU queue 32 | """ 33 | if key in self.lru: 34 | self.lru.remove(key) 35 | self.lru.append(key) 36 | 37 | def get(self, key, default=None): 38 | item = super(LRUCache, self).get(key, default) 39 | self.refresh(key) 40 | 41 | return item 42 | 43 | def __getitem__(self, key): 44 | item = super(LRUCache, self).__getitem__(key) 45 | self.refresh(key) 46 | 47 | return item 48 | 49 | def __setitem__(self, key, value): 50 | super(LRUCache, self).__setitem__(key, value) 51 | 52 | self.refresh(key) 53 | 54 | # Check, if the cache is full and we have to remove old items 55 | # If the queue is of unlimited size, self.capacity is NaN and 56 | # x > NaN is always False in Python and the cache won't be cleared. 57 | if len(self) > self.capacity: 58 | self.pop(self.lru.pop(0)) 59 | 60 | def __delitem__(self, key): 61 | super(LRUCache, self).__delitem__(key) 62 | self.lru.remove(key) 63 | 64 | def clear(self): 65 | super(LRUCache, self).clear() 66 | del self.lru[:] 67 | 68 | 69 | # Source: https://github.com/PythonCharmers/python-future/blob/466bfb2dfa36d865285dc31fe2b0c0a53ff0f181/future/utils/__init__.py#L102-L134 70 | def with_metaclass(meta, *bases): 71 | """ 72 | Function from jinja2/_compat.py. License: BSD. 73 | 74 | Use it like this:: 75 | 76 | class BaseForm(object): 77 | pass 78 | 79 | class FormType(type): 80 | pass 81 | 82 | class Form(with_metaclass(FormType, BaseForm)): 83 | pass 84 | 85 | This requires a bit of explanation: the basic idea is to make a 86 | dummy metaclass for one level of class instantiation that replaces 87 | itself with the actual metaclass. Because of internal type checks 88 | we also need to make sure that we downgrade the custom metaclass 89 | for one level to something closer to type (that's why __call__ and 90 | __init__ comes back from type etc.). 91 | 92 | This has the advantage over six.with_metaclass of not introducing 93 | dummy classes into the final MRO. 94 | """ 95 | 96 | class Metaclass(meta): 97 | __call__ = type.__call__ 98 | __init__ = type.__init__ 99 | 100 | def __new__(cls, name, this_bases, d): 101 | if this_bases is None: 102 | return type.__new__(cls, name, (), d) 103 | return meta(name, bases, d) 104 | 105 | return Metaclass('temporary_class', None, {}) 106 | 107 | 108 | @contextmanager 109 | def catch_warning(warning_cls): 110 | with warnings.catch_warnings(): 111 | warnings.filterwarnings('error', category=warning_cls) 112 | 113 | yield 114 | 115 | 116 | class FrozenDict(dict): 117 | def __hash__(self): 118 | return hash(tuple(sorted(self.items()))) 119 | 120 | def _immutable(self, *args, **kws): 121 | raise TypeError('object is immutable') 122 | 123 | __setitem__ = _immutable 124 | __delitem__ = _immutable 125 | clear = _immutable 126 | update = _immutable 127 | setdefault = _immutable 128 | pop = _immutable 129 | popitem = _immutable 130 | 131 | 132 | def freeze(obj): 133 | if isinstance(obj, dict): 134 | return FrozenDict((k, freeze(v)) for k, v in obj.items()) 135 | elif isinstance(obj, list): 136 | return tuple(freeze(el) for el in obj) 137 | elif isinstance(obj, set): 138 | return frozenset(obj) 139 | else: 140 | return obj 141 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flata 2 | ---- 3 | 4 | |Build Status| |Coverage| |Version| 5 | 6 | 7 | Flata is inspired by TinyDB_ and lowdb_. It is a lightweight document 8 | oriented database optimized for FlatApi and fun :) It's written in pure 9 | Python and has no external dependencies. The target are small apps or 10 | fake api that would be blown away by a SQL-DB or an external database server. 11 | 12 | Many thanks to : 13 | ================ 14 | 15 | Markus Siemens's TinyDB. All credit should go to Markus, upon his hard work 16 | I can create the Flata as what I want. I borrow some concepts from lowdb which 17 | will have better support for Restful API. 18 | 19 | Difference between TinyDB and Flata 20 | 21 | - **No default table** The _default table has been removed from Flata. User needs to create a table first before inserting any data. 22 | 23 | - **Built-in ID** Flata always attachs a built-in id field, to every record, but user can customize the built-in id field as they prefer. 24 | 25 | - **Only table object can execute CRUD** The instance of TinyDB can execute CRUD actions, but it is different story in Flata. In Flata only the instance of table is allowed to execute CRUD actions. This concept is borrowed from lowdb. 26 | 27 | - **Return object instead of ID** Flata will return new or updated objects with IDs after the data is inserted or updated. It is good for Restful API to present the latest data in the database. 28 | 29 | - **Format of database is not compatible** Database file created by TinyDB will not be compatible with Flata, because data structure stored as list in Flata instead of dict in TinyDB. 30 | 31 | 32 | Installation 33 | ************ 34 | 35 | - Via pypi 36 | 37 | .. code-block:: bash 38 | 39 | $ pip install flata 40 | 41 | - Via github 42 | 43 | .. code-block:: bash 44 | 45 | $ pip install -e git+https://github.com/harryho/flata@master#egg=flata 46 | 47 | 48 | Example code 49 | ************ 50 | 51 | - Create database file with empty table1 52 | 53 | .. code-block:: python 54 | 55 | >>> from flata import Flata, where 56 | >>> from flata.storages import JSONStorage 57 | >>> db = Flata('/path/to/db.json', storage=JSONStorage) 58 | >>> db.table('table1') # Method table will create or retrieve if it exists 59 | >>> db.get('table1') # Method get only retrieve the table it exists 60 | 61 | - Insert or update data into table1 62 | 63 | .. code-block:: python 64 | 65 | >>> db.table('table1').insert({'data':1 }) 66 | >>> db.get('table1').insert({'data':2 }) 67 | >>> tb = db.get('table1') 68 | >>> tb.all() 69 | >>> tb.update({'data': 100}, where('data') ==1 ) 70 | >>> tb.all() 71 | 72 | 73 | - Query data from table1 74 | 75 | .. code-block:: python 76 | 77 | >>> tb = db.get('table1') 78 | >>> tb.search(Query().data == 2) 79 | 80 | - Customize default unique id field `id` 81 | 82 | .. code-block:: python 83 | 84 | >>> tb2 = db.table('table2' , id_field = '_guid') 85 | >>> tb2.insert({'data':1 }) 86 | >>> tb2.all() 87 | 88 | 89 | Stable release 90 | ************** 91 | - |Flata 4.0.0| 92 | 93 | 94 | 95 | Old versions 96 | ************ 97 | - |Flata 3.3.1| 98 | - |Flata 3.2.0| 99 | 100 | 101 | 102 | Change log 103 | ********** 104 | 105 | - Flata 3.2.0 106 | 107 | Add ignore case feature for search and match methods 108 | 109 | - Flata 3.1.0 110 | 111 | Change the get method 112 | 113 | - Flata 3.0.0 114 | 115 | Change the built-in field from '_oid' to 'id'. 116 | 117 | 118 | 119 | .. |Build Status| image:: https://travis-ci.org/harryho/flata.svg?branch=master 120 | :target: https://travis-ci.org/harryho/flata 121 | .. |Coverage| image:: https://coveralls.io/repos/github/harryho/flata/badge.svg?branch=master 122 | :target: https://coveralls.io/github/harryho/flata?branch=master 123 | .. |Version| image:: https://badge.fury.io/py/flata.svg 124 | :target: https://badge.fury.io/py/flata 125 | .. _TinyDB: https://github.com/msiemens/tinydb 126 | .. _lowdb: https://github.com/typicode/lowdb 127 | 128 | .. |Flata 1.1.0| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=1.1.0 129 | .. |Flata 2.0.0| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=2.0.0 130 | .. |Flata 2.1.0| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=3.1.0 131 | .. |Flata 3.2.0| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=3.2.0 132 | .. |Flata 3.3.1| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=3.3.1 133 | .. |Flata 4.0.0| :target:: https://pypi.python.org/pypi?:action=display&name=flata&version=4.0.0 134 | -------------------------------------------------------------------------------- /tests/test_queries.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flata.queries import Query 3 | 4 | 5 | def test_no_path(): 6 | with pytest.raises(ValueError): 7 | Query() == 2 8 | 9 | 10 | def test_eq(): 11 | query = Query().value == 1 12 | assert query({'value': 1}) 13 | assert not query({'value': 2}) 14 | assert hash(query) 15 | 16 | query = Query().value == [0, 1] 17 | assert query({'value': [0, 1]}) 18 | assert not query({'value': [0, 1, 2]}) 19 | assert hash(query) 20 | 21 | 22 | def test_ne(): 23 | query = Query().value != 1 24 | assert query({'value': 2}) 25 | assert not query({'value': 1}) 26 | assert hash(query) 27 | 28 | query = Query().value != [0, 1] 29 | assert query({'value': [0, 1, 2]}) 30 | assert not query({'value': [0, 1]}) 31 | assert hash(query) 32 | 33 | 34 | def test_lt(): 35 | query = Query().value < 1 36 | assert query({'value': 0}) 37 | assert not query({'value': 1}) 38 | assert hash(query) 39 | 40 | 41 | def test_le(): 42 | query = Query().value <= 1 43 | assert query({'value': 0}) 44 | assert query({'value': 1}) 45 | assert not query({'value': 2}) 46 | assert hash(query) 47 | 48 | 49 | def test_gt(): 50 | query = Query().value > 1 51 | assert query({'value': 2}) 52 | assert not query({'value': 1}) 53 | assert hash(query) 54 | 55 | 56 | def test_ge(): 57 | query = Query().value >= 1 58 | assert query({'value': 2}) 59 | assert query({'value': 1}) 60 | assert not query({'value': 0}) 61 | assert hash(query) 62 | 63 | 64 | def test_or(): 65 | query = ( 66 | (Query().val1 == 1) | 67 | (Query().val2 == 2) 68 | ) 69 | assert query({'val1': 1}) 70 | assert query({'val2': 2}) 71 | assert query({'val1': 1, 'val2': 2}) 72 | assert not query({'val1': '', 'val2': ''}) 73 | assert hash(query) 74 | 75 | 76 | def test_and(): 77 | query = ( 78 | (Query().val1 == 1) & 79 | (Query().val2 == 2) 80 | ) 81 | assert query({'val1': 1, 'val2': 2}) 82 | assert not query({'val1': 1}) 83 | assert not query({'val2': 2}) 84 | assert not query({'val1': '', 'val2': ''}) 85 | assert hash(query) 86 | 87 | 88 | def test_not(): 89 | query = ~ (Query().val1 == 1) 90 | assert query({'val1': 5, 'val2': 2}) 91 | assert not query({'val1': 1, 'val2': 2}) 92 | assert hash(query) 93 | 94 | query = ( 95 | (~ (Query().val1 == 1)) & 96 | (Query().val2 == 2) 97 | ) 98 | assert query({'val1': '', 'val2': 2}) 99 | assert query({'val2': 2}) 100 | assert not query({'val1': 1, 'val2': 2}) 101 | assert not query({'val1': 1}) 102 | assert not query({'val1': '', 'val2': ''}) 103 | assert hash(query) 104 | 105 | 106 | def test_has_key(): 107 | query = Query().val3.exists() 108 | 109 | assert query({'val3': 1}) 110 | assert not query({'val1': 1, 'val2': 2}) 111 | assert hash(query) 112 | 113 | 114 | def test_regex(): 115 | query = Query().val.matches(r'\d{2}\.') 116 | 117 | assert query({'val': '42.'}) 118 | assert not query({'val': '44'}) 119 | assert not query({'val': 'ab.'}) 120 | assert not query({'': None}) 121 | assert hash(query) 122 | 123 | query = Query().val.search(r'\d+') 124 | 125 | assert query({'val': 'ab3'}) 126 | assert not query({'val': 'abc'}) 127 | assert not query({'val': ''}) 128 | assert not query({'': None}) 129 | assert hash(query) 130 | 131 | 132 | def test_custom(): 133 | def test(value): 134 | return value == 42 135 | 136 | query = Query().val.test(test) 137 | 138 | assert query({'val': 42}) 139 | assert not query({'val': 40}) 140 | assert not query({'val': '44'}) 141 | assert not query({'': None}) 142 | assert hash(query) 143 | 144 | def in_list(value, l): 145 | return value in l 146 | 147 | query = Query().val.test(in_list, tuple([25, 35])) 148 | assert not query({'val': 20}) 149 | assert query({'val': 25}) 150 | assert not query({'val': 30}) 151 | assert query({'val': 35}) 152 | assert not query({'val': 36}) 153 | assert hash(query) 154 | 155 | 156 | def test_custom_with_params(): 157 | def test(value, minimum, maximum): 158 | return minimum <= value <= maximum 159 | 160 | query = Query().val.test(test, 1, 10) 161 | 162 | assert query({'val': 5}) 163 | assert not query({'val': 0}) 164 | assert not query({'val': 11}) 165 | assert not query({'': None}) 166 | assert hash(query) 167 | 168 | 169 | def test_any(): 170 | query = Query().followers.any(Query().name == 'don') 171 | 172 | assert query({'followers': [{'name': 'don'}, {'name': 'john'}]}) 173 | assert not query({'followers': 1}) 174 | assert not query({}) 175 | assert hash(query) 176 | 177 | query = Query().followers.any(Query().num.matches('\\d+')) 178 | assert query({'followers': [{'num': '12'}, {'num': 'abc'}]}) 179 | assert not query({'followers': [{'num': 'abc'}]}) 180 | assert hash(query) 181 | 182 | query = Query().followers.any(['don', 'jon']) 183 | assert query({'followers': ['don', 'greg', 'bill']}) 184 | assert not query({'followers': ['greg', 'bill']}) 185 | assert not query({}) 186 | assert hash(query) 187 | 188 | query = Query().followers.any([{'name': 'don'}, {'name': 'john'}]) 189 | assert query({'followers': [{'name': 'don'}, {'name': 'greg'}]}) 190 | assert not query({'followers': [{'name': 'greg'}]}) 191 | assert hash(query) 192 | 193 | 194 | def test_all(): 195 | query = Query().followers.all(Query().name == 'don') 196 | assert query({'followers': [{'name': 'don'}]}) 197 | assert not query({'followers': [{'name': 'don'}, {'name': 'john'}]}) 198 | assert hash(query) 199 | 200 | query = Query().followers.all(Query().num.matches('\\d+')) 201 | assert query({'followers': [{'num': '123'}, {'num': '456'}]}) 202 | assert not query({'followers': [{'num': '123'}, {'num': 'abc'}]}) 203 | assert hash(query) 204 | 205 | query = Query().followers.all(['don', 'john']) 206 | assert query({'followers': ['don', 'john', 'greg']}) 207 | assert not query({'followers': ['don', 'greg']}) 208 | assert not query({}) 209 | assert hash(query) 210 | 211 | query = Query().followers.all([{'name': 'john'}, {'age': 17}]) 212 | assert query({'followers': [{'name': 'john'}, {'age': 17}]}) 213 | assert not query({'followers': [{'name': 'john'}, {'age': 18}]}) 214 | assert hash(query) 215 | 216 | 217 | def test_has(): 218 | query = Query().key1.key2.exists() 219 | str(query) # This used to cause a bug... 220 | 221 | assert query({'key1': {'key2': {'key3': 1}}}) 222 | assert query({'key1': {'key2': 1}}) 223 | assert not query({'key1': 3}) 224 | assert not query({'key1': {'key1': 1}}) 225 | assert not query({'key2': {'key1': 1}}) 226 | assert hash(query) 227 | 228 | query = Query().key1.key2 == 1 229 | 230 | assert query({'key1': {'key2': 1}}) 231 | assert not query({'key1': {'key2': 2}}) 232 | assert hash(query) 233 | 234 | # Nested has: key exists 235 | query = Query().key1.key2.key3.exists() 236 | assert query({'key1': {'key2': {'key3': 1}}}) 237 | # Not a dict 238 | assert not query({'key1': 1}) 239 | assert not query({'key1': {'key2': 1}}) 240 | # Wrong key 241 | assert not query({'key1': {'key2': {'key0': 1}}}) 242 | assert not query({'key1': {'key0': {'key3': 1}}}) 243 | assert not query({'key0': {'key2': {'key3': 1}}}) 244 | 245 | assert hash(query) 246 | 247 | # Nested has: check for value 248 | query = Query().key1.key2.key3 == 1 249 | assert query({'key1': {'key2': {'key3': 1}}}) 250 | assert not query({'key1': {'key2': {'key3': 0}}}) 251 | assert hash(query) 252 | 253 | # Test special methods: regex matches 254 | query = Query().key1.value.matches(r'\d+') 255 | assert query({'key1': {'value': '123'}}) 256 | assert not query({'key2': {'value': '123'}}) 257 | assert not query({'key2': {'value': 'abc'}}) 258 | assert hash(query) 259 | 260 | # Test special methods: regex contains 261 | query = Query().key1.value.search(r'\d+') 262 | assert query({'key1': {'value': 'a2c'}}) 263 | assert not query({'key2': {'value': 'a2c'}}) 264 | assert not query({'key2': {'value': 'abc'}}) 265 | assert hash(query) 266 | 267 | # Test special methods: nested has and regex matches 268 | query = Query().key1.x.y.matches(r'\d+') 269 | assert query({'key1': {'x': {'y': '123'}}}) 270 | assert not query({'key1': {'x': {'y': 'abc'}}}) 271 | assert hash(query) 272 | 273 | # Test special method: nested has and regex contains 274 | query = Query().key1.x.y.search(r'\d+') 275 | assert query({'key1': {'x': {'y': 'a2c'}}}) 276 | assert not query({'key1': {'x': {'y': 'abc'}}}) 277 | assert hash(query) 278 | 279 | # Test special methods: custom test 280 | query = Query().key1.int.test(lambda x: x == 3) 281 | assert query({'key1': {'int': 3}}) 282 | assert hash(query) 283 | 284 | 285 | def test_hash(): 286 | d = { 287 | Query().key1 == 2: True, 288 | Query().key1.key2.key3.exists(): True, 289 | Query().key1.exists() & Query().key2.exists(): True, 290 | Query().key1.exists() | Query().key2.exists(): True, 291 | } 292 | 293 | assert (Query().key1 == 2) in d 294 | assert (Query().key1.key2.key3.exists()) in d 295 | assert (Query()['key1.key2'].key3.exists()) not in d 296 | 297 | # Commutative property of & and | 298 | assert (Query().key1.exists() & Query().key2.exists()) in d 299 | assert (Query().key2.exists() & Query().key1.exists()) in d 300 | assert (Query().key1.exists() | Query().key2.exists()) in d 301 | assert (Query().key2.exists() | Query().key1.exists()) in d 302 | 303 | 304 | def test_orm_usage(): 305 | data = {'name': 'John', 'age': {'year': 2000}} 306 | 307 | User = Query() 308 | query1 = User.name == 'John' 309 | query2 = User.age.year == 2000 310 | assert query1(data) 311 | assert query2(data) 312 | -------------------------------------------------------------------------------- /flata/queries.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the querying interface. 3 | 4 | Starting with :class:`~flata.queries.Query` you can construct complex 5 | queries: 6 | 7 | >>> ((where('f1') == 5) & (where('f2') != 2)) | where('s').matches(r'^\w+$') 8 | (('f1' == 5) and ('f2' != 2)) or ('s' ~= ^\w+$ ) 9 | 10 | Queries are executed by using the ``__call__``: 11 | 12 | >>> q = where('val') == 5 13 | >>> q({'val': 5}) 14 | True 15 | >>> q({'val': 1}) 16 | False 17 | """ 18 | 19 | import re 20 | import sys 21 | 22 | from .utils import catch_warning, freeze 23 | 24 | __all__ = ('Query', 'where') 25 | 26 | 27 | def is_sequence(obj): 28 | return hasattr(obj, '__iter__') 29 | 30 | 31 | class QueryImpl(object): 32 | """ 33 | A query implementation. 34 | 35 | This query implementation wraps a test function which is run when the 36 | query is evaluated by calling the object. 37 | 38 | Queries can be combined with logical and/or and modified with logical not. 39 | """ 40 | def __init__(self, test, hashval): 41 | self.test = test 42 | self.hashval = hashval 43 | 44 | def __call__(self, value): 45 | return self.test(value) 46 | 47 | def __hash__(self): 48 | return hash(self.hashval) 49 | 50 | def __repr__(self): 51 | return 'QueryImpl{0}'.format(self.hashval) 52 | 53 | def __eq__(self, other): 54 | return self.hashval == other.hashval 55 | 56 | # --- Query modifiers ----------------------------------------------------- 57 | 58 | def __and__(self, other): 59 | # We use a frozenset for the hash as the AND operation is commutative 60 | # (a | b == b | a) 61 | return QueryImpl(lambda value: self(value) and other(value), 62 | ('and', frozenset([self.hashval, other.hashval]))) 63 | 64 | def __or__(self, other): 65 | # We use a frozenset for the hash as the OR operation is commutative 66 | # (a & b == b & a) 67 | return QueryImpl(lambda value: self(value) or other(value), 68 | ('or', frozenset([self.hashval, other.hashval]))) 69 | 70 | def __invert__(self): 71 | return QueryImpl(lambda value: not self(value), 72 | ('not', self.hashval)) 73 | 74 | 75 | class Query(object): 76 | """ 77 | Flata Queries. 78 | 79 | Allows to build queries for Flata databases. There are two main ways of 80 | using queries: 81 | 82 | 1) ORM-like usage: 83 | 84 | >>> User = Query() 85 | >>> db.search(User.name == 'John Doe') 86 | >>> db.search(User['logged-in'] == True) 87 | 88 | 2) Classical usage: 89 | 90 | >>> db.search(where('value') == True) 91 | 92 | Note that ``where(...)`` is a shorthand for ``Query(...)`` allowing for 93 | a more fluent syntax. 94 | 95 | Besides the methods documented here you can combine queries using the 96 | binary AND and OR operators: 97 | 98 | >>> db.search(where('field1').exists() & where('field2') == 5) # Binary AND 99 | >>> db.search(where('field1').exists() | where('field2') == 5) # Binary OR 100 | 101 | Queries are executed by calling the resulting object. They expect to get the 102 | element to test as the first argument and return ``True`` or ``False`` 103 | depending on whether the elements matches the query or not. 104 | """ 105 | 106 | def __init__(self): 107 | self._path = [] 108 | 109 | def __getattr__(self, item): 110 | query = Query() 111 | query._path = self._path + [item] 112 | 113 | return query 114 | 115 | __getitem__ = __getattr__ 116 | 117 | def _generate_test(self, test, hashval): 118 | """ 119 | Generate a query based on a test function. 120 | 121 | :param test: The test the query executes. 122 | :param hashval: The hash of the query. 123 | :return: A :class:`~flata.queries.QueryImpl` object 124 | """ 125 | if not self._path: 126 | raise ValueError('Query has no path') 127 | 128 | def impl(value): 129 | try: 130 | # Resolve the path 131 | for part in self._path: 132 | value = value[part] 133 | except (KeyError, TypeError): 134 | return False 135 | else: 136 | return test(value) 137 | 138 | return QueryImpl(impl, hashval) 139 | 140 | def __eq__(self, rhs): 141 | """ 142 | Test a dict value for equality. 143 | 144 | >>> Query().f1 == 42 145 | 146 | :param rhs: The value to compare against 147 | """ 148 | if sys.version_info <= (3, 0): # pragma: no cover 149 | # Special UTF-8 handling on Python 2 150 | def test(value): 151 | with catch_warning(UnicodeWarning): 152 | try: 153 | return value == rhs 154 | except UnicodeWarning: 155 | # Dealing with a case, where 'value' or 'rhs' 156 | # is unicode and the other is a byte string. 157 | if isinstance(value, str): 158 | return value.decode('utf-8') == rhs 159 | elif isinstance(rhs, str): 160 | return value == rhs.decode('utf-8') 161 | 162 | else: # pragma: no cover 163 | def test(value): 164 | return value == rhs 165 | 166 | return self._generate_test(lambda value: test(value), 167 | ('==', tuple(self._path), freeze(rhs))) 168 | 169 | def __ne__(self, rhs): 170 | """ 171 | Test a dict value for inequality. 172 | 173 | >>> Query().f1 != 42 174 | 175 | :param rhs: The value to compare against 176 | """ 177 | return self._generate_test(lambda value: value != rhs, 178 | ('!=', tuple(self._path), freeze(rhs))) 179 | 180 | def __lt__(self, rhs): 181 | """ 182 | Test a dict value for being lower than another value. 183 | 184 | >>> Query().f1 < 42 185 | 186 | :param rhs: The value to compare against 187 | """ 188 | return self._generate_test(lambda value: value < rhs, 189 | ('<', tuple(self._path), rhs)) 190 | 191 | def __le__(self, rhs): 192 | """ 193 | Test a dict value for being lower than or equal to another value. 194 | 195 | >>> where('f1') <= 42 196 | 197 | :param rhs: The value to compare against 198 | """ 199 | return self._generate_test(lambda value: value <= rhs, 200 | ('<=', tuple(self._path), rhs)) 201 | 202 | def __gt__(self, rhs): 203 | """ 204 | Test a dict value for being greater than another value. 205 | 206 | >>> Query().f1 > 42 207 | 208 | :param rhs: The value to compare against 209 | """ 210 | return self._generate_test(lambda value: value > rhs, 211 | ('>', tuple(self._path), rhs)) 212 | 213 | def __ge__(self, rhs): 214 | """ 215 | Test a dict value for being greater than or equal to another value. 216 | 217 | >>> Query().f1 >= 42 218 | 219 | :param rhs: The value to compare against 220 | """ 221 | return self._generate_test(lambda value: value >= rhs, 222 | ('>=', tuple(self._path), rhs)) 223 | 224 | def exists(self): 225 | """ 226 | Test for a dict where a provided key exists. 227 | 228 | >>> Query().f1.exists() >= 42 229 | 230 | :param rhs: The value to compare against 231 | """ 232 | return self._generate_test(lambda _: True, 233 | ('exists', tuple(self._path))) 234 | 235 | def matches(self, regex): 236 | """ 237 | Run a regex test against a dict value (whole string has to match). 238 | 239 | >>> Query().f1.matches(r'^\w+$') 240 | 241 | :param regex: The regular expression to use for matching 242 | """ 243 | return self._generate_test(lambda value: re.match(regex, value), 244 | ('matches', tuple(self._path), regex)) 245 | 246 | def matches_ignore_case(self, regex): 247 | """ 248 | Run a regex test against a dict value (whole string has to match). 249 | It is not case-sensitive. 250 | 251 | >>> Query().f1.matches(r'^\w+$') 252 | 253 | :param regex: The regular expression to use for matching 254 | """ 255 | regex = regex.lower() 256 | return self._generate_test(lambda value: re.match(regex, value.lower()), 257 | ('matches', tuple(self._path), regex)) 258 | 259 | def search(self, regex): 260 | """ 261 | Run a regex test against a dict value (only substring string has to 262 | match). 263 | 264 | >>> Query().f1.search(r'^\w+$') 265 | 266 | :param regex: The regular expression to use for matching 267 | """ 268 | return self._generate_test(lambda value: re.search(regex, value), 269 | ('search', tuple(self._path), regex)) 270 | 271 | def search_ignore_case(self, regex): 272 | """ 273 | Run a regex test against a dict value (only substring string has to 274 | match). It is not case-sensitive. 275 | 276 | >>> Query().f1.search(r'^\w+$') 277 | 278 | :param regex: The regular expression to use for matching 279 | """ 280 | regex = regex.lower() 281 | return self._generate_test(lambda value: re.search(regex, value.lower()), 282 | ('search', tuple(self._path), regex)) 283 | 284 | def test(self, func, *args): 285 | """ 286 | Run a user-defined test function against a dict value. 287 | 288 | >>> def test_func(val): 289 | ... return val == 42 290 | ... 291 | >>> Query().f1.test(test_func) 292 | 293 | :param func: The function to call, passing the dict as the first 294 | argument 295 | :param args: Additional arguments to pass to the test function 296 | """ 297 | return self._generate_test(lambda value: func(value, *args), 298 | ('test', tuple(self._path), func, args)) 299 | 300 | def any(self, cond): 301 | """ 302 | Checks if a condition is met by any element in a list, 303 | where a condition can also be a sequence (e.g. list). 304 | 305 | >>> Query().f1.any(Query().f2 == 1) 306 | 307 | Matches:: 308 | 309 | {'f1': [{'f2': 1}, {'f2': 0}]} 310 | 311 | >>> Query().f1.any([1, 2, 3]) 312 | # Match f1 that contains any element from [1, 2, 3] 313 | 314 | Matches:: 315 | 316 | {'f1': [1, 2]} 317 | {'f1': [3, 4, 5]} 318 | 319 | :param cond: Either a query that at least one element has to match or 320 | a list of which at least one element has to be contained 321 | in the tested element. 322 | - """ 323 | if callable(cond): 324 | def _cmp(value): 325 | return is_sequence(value) and any(cond(e) for e in value) 326 | 327 | else: 328 | def _cmp(value): 329 | return is_sequence(value) and any(e in cond for e in value) 330 | 331 | return self._generate_test(lambda value: _cmp(value), 332 | ('any', tuple(self._path), freeze(cond))) 333 | 334 | def all(self, cond): 335 | """ 336 | Checks if a condition is met by any element in a list, 337 | where a condition can also be a sequence (e.g. list). 338 | 339 | >>> Query().f1.all(Query().f2 == 1) 340 | 341 | Matches:: 342 | 343 | {'f1': [{'f2': 1}, {'f2': 1}]} 344 | 345 | >>> Query().f1.all([1, 2, 3]) 346 | # Match f1 that contains any element from [1, 2, 3] 347 | 348 | Matches:: 349 | 350 | {'f1': [1, 2, 3, 4, 5]} 351 | 352 | :param cond: Either a query that all elements have to match or a list 353 | which has to be contained in the tested element. 354 | """ 355 | if callable(cond): 356 | def _cmp(value): 357 | return is_sequence(value) and all(cond(e) for e in value) 358 | 359 | else: 360 | def _cmp(value): 361 | return is_sequence(value) and all(e in value for e in cond) 362 | 363 | return self._generate_test(lambda value: _cmp(value), 364 | ('all', tuple(self._path), freeze(cond))) 365 | 366 | 367 | def where(key): 368 | return Query()[key] 369 | -------------------------------------------------------------------------------- /flata/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the :class:`database ` and 3 | :class:`tables ` implementation. 4 | """ 5 | from . import JSONStorage, MemoryStorage 6 | from .utils import LRUCache, iteritems, itervalues 7 | 8 | 9 | class Element(dict): 10 | """ 11 | Represents an element stored in the database. 12 | 13 | This is a transparent proxy for database elements. It exists 14 | to provide a way to access an element's id via ``el.id``. 15 | """ 16 | def __init__(self, value=None, id=None, **kwargs): 17 | super(Element, self).__init__(**kwargs) 18 | 19 | if value is not None: 20 | self.update(value) 21 | self.id = id 22 | 23 | 24 | class StorageProxy(object): 25 | 26 | DEFAULT_ID_FIELD = 'id' 27 | 28 | def __init__(self, storage, table_name, **kwargs): 29 | self._storage = storage 30 | self._table_name = table_name 31 | self._id_field = kwargs.pop('id_field', StorageProxy.DEFAULT_ID_FIELD) 32 | 33 | def read(self): 34 | try: 35 | raw_data = (self._storage.read() or {})[self._table_name] 36 | except KeyError: 37 | self.write({}) 38 | return {} 39 | 40 | data = {} 41 | # for key, val in iteritems(raw_data): 42 | # id = int(key) 43 | # data[id] = Element(val, id) 44 | 45 | for item in raw_data: 46 | id = item[self._id_field] 47 | data[id] = Element(item, id) 48 | 49 | return data 50 | 51 | def write(self, values): 52 | data = self._storage.read() or {} 53 | data[self._table_name] = values 54 | self._storage.write(data) 55 | 56 | def purge_table(self): 57 | try: 58 | data = self._storage.read() or {} 59 | del data[self._table_name] 60 | self._storage.write(data) 61 | except KeyError: 62 | pass 63 | 64 | @property 65 | def table_name(self): 66 | return self._table_name 67 | 68 | @property 69 | def id_field(self): 70 | return self._id_field or StorageProxy.DEFAULT_ID_FIELD 71 | 72 | 73 | 74 | class Flata(object): 75 | """ 76 | The main class of Flata. 77 | 78 | Gives access to the database, provides methods to create/delete/search 79 | and getting tables. 80 | 81 | Flata is different from TinyDB, which can not access and manipulate 82 | data on the table from the instance of PseudB. 83 | 84 | There is no default in Flata as well. 85 | """ 86 | 87 | DEFAULT_STORAGE = JSONStorage 88 | 89 | def __init__(self, *args, **kwargs): 90 | """ 91 | Create a new instance of Flata. 92 | 93 | All arguments and keyword arguments will be passed to the underlying 94 | storage class (default: :class:`~flata.storages.JSONStorage`). 95 | 96 | :param storage: The class of the storage to use. Will be initialized 97 | with ``args`` and ``kwargs``. 98 | :param cache: The class of the CachingMiddleware to use. If it is not 99 | not null, it will be used as storage. 100 | """ 101 | 102 | storage = kwargs.pop('storage', Flata.DEFAULT_STORAGE) 103 | cache = kwargs.pop('cache', None) 104 | # table = kwargs.pop('default_table', Flata.DEFAULT_TABLE) 105 | 106 | # Prepare the storage 107 | self._opened = False 108 | 109 | #: :type: Storage 110 | self._storage = cache if cache else storage(*args, **kwargs) 111 | # if storage == MemoryStorage else 112 | 113 | self._opened = True 114 | 115 | # Prepare the default table 116 | 117 | self._tables = {} 118 | self._table = None # self.table(table) 119 | 120 | def table(self, name, **options): 121 | """ 122 | Get access to a specific table. 123 | 124 | Creates a new table, if it hasn't been created before, otherwise it 125 | returns the cached :class:`~flata.Table` object. 126 | 127 | :param name: The name of the table. It is a required input. 128 | :type name: str 129 | :param id: Customize the object id field. 130 | :param cache_size: How many query results to cache. 131 | 132 | """ 133 | 134 | if not name: 135 | raise ValueError('Table name can not be None or empty.') 136 | 137 | if name in self._tables: 138 | return self._tables[name] 139 | 140 | table = self.table_class(StorageProxy(self._storage, name, **options), **options) 141 | 142 | self._tables[name] = table 143 | self._table = table 144 | 145 | # table._read will create an empty table in the storage, if necessary 146 | table._read() 147 | 148 | return table 149 | 150 | def get(self, name): 151 | try: 152 | return self._tables[name] 153 | except KeyError: 154 | return None 155 | 156 | 157 | def tables(self): 158 | """ 159 | Get the names of all tables in the database. 160 | 161 | :returns: a set of table names 162 | :rtype: set[str] 163 | """ 164 | 165 | return set(self._storage.read()) 166 | 167 | def all(self): 168 | """ 169 | Get all elements stored in the table. 170 | 171 | :returns: a list with all elements. 172 | :rtype: list[Element] 173 | """ 174 | 175 | return self._storage.read() 176 | 177 | def purge_tables(self): 178 | """ 179 | Purge all tables from the database. **CANNOT BE REVERSED!** 180 | """ 181 | 182 | self._storage.write({}) 183 | self._tables.clear() 184 | 185 | def purge_table(self, name): 186 | """ 187 | Purge a specific table from the database. **CANNOT BE REVERSED!** 188 | 189 | :param name: The name of the table. 190 | :type name: str 191 | """ 192 | if name in self._tables: 193 | del self._tables[name] 194 | 195 | proxy = StorageProxy(self._storage, name) 196 | proxy.purge_table() 197 | 198 | def close(self): 199 | """ 200 | Close the database. 201 | """ 202 | self._opened = False 203 | self._storage.close() 204 | 205 | def __enter__(self): 206 | return self 207 | 208 | def __exit__(self, *args): 209 | if self._opened is True: 210 | self.close() 211 | 212 | # def __getattr__(self, name): 213 | # """ 214 | # Forward all unknown attribute calls to the underlying standard table. 215 | # """ 216 | # return getattr(self._table, name) 217 | 218 | # Methods that are executed on the default table 219 | # Because magic methods are not handlet by __getattr__ we need to forward 220 | # them manually here 221 | 222 | def __len__(self): 223 | """ 224 | Get the total number of elements in the default table. 225 | 226 | >>> db = Flata('db.json') 227 | >>> len(db) 228 | 0 229 | """ 230 | return len(self._table) 231 | 232 | def __iter__(self): 233 | """ 234 | Iter over all elements from default table. 235 | """ 236 | return self._table.__iter__() 237 | 238 | 239 | 240 | class Table(object): 241 | """ 242 | Represents a single Flata Table. 243 | """ 244 | 245 | def __init__(self, storage, cache_size=10, **kwargs): 246 | """ 247 | Get access to a table. 248 | 249 | :param storage: Access to the storage 250 | :type storage: StorageProxyus 251 | :param cache_size: Maximum size of query cache. 252 | """ 253 | 254 | self._storage = storage 255 | self._table_name = storage.table_name 256 | self._id_field = storage.id_field 257 | self._query_cache = LRUCache(capacity=cache_size) 258 | 259 | data = self._read() 260 | if data: 261 | self._last_id = max(i for i in data) 262 | else: 263 | self._last_id = 0 264 | 265 | def process_elements(self, func, cond=None, ids=None): 266 | """ 267 | Helper function for processing all elements specified by condition 268 | or IDs. 269 | 270 | A repeating pattern in Flata is to run some code on all elements 271 | that match a condition or are specified by their ID. This is 272 | implemented in this function. 273 | The function passed as ``func`` has to be a callable. It's first 274 | argument will be the data currently in the database. It's second 275 | argument is the element ID of the currently processed element. 276 | 277 | See: :meth:`~.update`, :meth:`.remove` 278 | 279 | :param func: the function to execute on every included element. 280 | first argument: all data 281 | second argument: the current id 282 | :param cond: elements to use, or 283 | :param ids: elements to use 284 | :returns: the element IDs that were affected during processed 285 | """ 286 | 287 | data = self._read() 288 | updated_data = [] 289 | 290 | if ids is not None: 291 | # Processed element specified by id 292 | for id in ids: 293 | func(data, id) 294 | if id in data: 295 | updated_data.append(data[id]) 296 | 297 | else: 298 | # Collect affected ids 299 | ids = [] 300 | 301 | # Processed elements specified by condition 302 | for id in list(data): 303 | if cond(data[id]): 304 | func(data, id) 305 | ids.append(id) 306 | if id in data: 307 | updated_data.append(data[id]) 308 | 309 | 310 | new_data = list(data.values()) 311 | 312 | self._write(new_data) 313 | 314 | return ids, updated_data 315 | 316 | def clear_cache(self): 317 | """ 318 | Clear the query cache. 319 | 320 | A simple helper that clears the internal query cache. 321 | """ 322 | self._query_cache.clear() 323 | 324 | def _get_next_id(self): 325 | """ 326 | Increment the ID used the last time and return it 327 | """ 328 | 329 | current_id = self._last_id + 1 330 | self._last_id = current_id 331 | 332 | return current_id 333 | 334 | def _read(self): 335 | """ 336 | Reading access to the DB. 337 | 338 | :returns: all values 339 | :rtype: dict 340 | """ 341 | 342 | return self._storage.read() 343 | 344 | def _write(self, values): 345 | """ 346 | Writing access to the DB. 347 | 348 | :param values: the new values to write 349 | :type values: dict 350 | """ 351 | 352 | self._query_cache.clear() 353 | self._storage.write(values) 354 | 355 | def __len__(self): 356 | """ 357 | Get the total number of elements in the table. 358 | """ 359 | return len(self._read()) 360 | 361 | def all(self): 362 | """ 363 | Get all elements stored in the table. 364 | 365 | :returns: a list with all elements. 366 | :rtype: list[Element] 367 | """ 368 | 369 | return list(itervalues(self._read())) 370 | 371 | def __iter__(self): 372 | """ 373 | Iter over all elements stored in the table. 374 | 375 | :returns: an iterator over all elements. 376 | :rtype: listiterator[Element] 377 | """ 378 | 379 | for value in itervalues(self._read()): 380 | yield value 381 | 382 | def insert(self, element): 383 | """ 384 | Insert a new element into the table. 385 | 386 | :param element: the element to insert 387 | :returns: the inserted element with ID 388 | """ 389 | 390 | if not isinstance(self, Table): 391 | raise ValueError('Only table instance can support insert action.') 392 | 393 | id = self._get_next_id() 394 | 395 | if not isinstance(element, dict): 396 | raise ValueError('Element is not a dictionary') 397 | 398 | data = self._read() 399 | 400 | items = list(data.values()) 401 | element[self._id_field] = id 402 | items.append(element) 403 | 404 | self._write(items) 405 | 406 | return element 407 | 408 | def insert_multiple(self, elements): 409 | """ 410 | Insert multiple elements into the table. 411 | 412 | :param elements: a list of elements to insert 413 | :returns: a list containing the inserted elements with IDs 414 | """ 415 | if not isinstance(self, Table): 416 | raise ValueError('Only table instance can support insert action.') 417 | 418 | ids = [] 419 | data = self._read() 420 | items = list(data.values()) 421 | 422 | for element in elements: 423 | id = self._get_next_id() 424 | ids.append(id) 425 | element[self._id_field] = id 426 | items.append(element) 427 | 428 | # data[id] = element 429 | 430 | self._write(items) 431 | 432 | return elements 433 | 434 | def remove(self, cond=None, ids=None): 435 | """ 436 | Remove all matching elements. 437 | 438 | :param cond: the condition to check against 439 | :type cond: query 440 | :param ids: a list of element IDs 441 | :type ids: list 442 | :returns: a list containing the removed element's ID 443 | """ 444 | 445 | return self.process_elements(lambda data, id: data.pop(id), 446 | cond, ids) 447 | 448 | def update(self, fields, cond=None, ids=None): 449 | """ 450 | Update all matching elements to have a given set of fields. 451 | 452 | :param fields: the fields that the matching elements will have 453 | or a method that will update the elements 454 | :type fields: dict | dict -> None 455 | :param cond: which elements to update 456 | :type cond: query 457 | :param ids: a list of element IDs 458 | :type ids: list 459 | :returns: a list containing the updated element's ID 460 | """ 461 | 462 | if callable(fields): 463 | return self.process_elements( 464 | lambda data, id: fields(data[id]), 465 | cond, ids 466 | ) 467 | else: 468 | return self.process_elements( 469 | lambda data, id: data[id].update(fields), 470 | cond, ids 471 | ) 472 | 473 | def purge(self): 474 | """ 475 | Purge the table by removing all elements. 476 | """ 477 | 478 | self._write({}) 479 | self._last_id = 0 480 | 481 | def search(self, cond): 482 | """ 483 | Search for all elements matching a 'where' cond. 484 | 485 | :param cond: the condition to check against 486 | :type cond: Query 487 | 488 | :returns: list of matching elements 489 | :rtype: list[Element] 490 | """ 491 | 492 | if cond in self._query_cache: 493 | return self._query_cache[cond][:] 494 | 495 | elements = [element for element in self.all() if cond(element)] 496 | self._query_cache[cond] = elements 497 | 498 | return elements[:] 499 | 500 | def get(self, cond=None, id=None): 501 | """ 502 | Get exactly one element specified by a query or and ID. 503 | 504 | Returns ``None`` if the element doesn't exist 505 | 506 | :param cond: the condition to check against 507 | :type cond: Query 508 | 509 | :param id: the element's ID 510 | 511 | :returns: the element or None 512 | :rtype: Element | None 513 | """ 514 | 515 | # Cannot use process_elements here because we want to return a 516 | # specific element 517 | 518 | if id is not None: 519 | # Element specified by ID 520 | return self._read().get(id, None) 521 | 522 | # Element specified by condition 523 | for element in self.all(): 524 | if cond(element): 525 | return element 526 | 527 | def count(self, cond): 528 | """ 529 | Count the elements matching a condition. 530 | 531 | :param cond: the condition use 532 | :type cond: Query 533 | """ 534 | 535 | return len(self.search(cond)) 536 | 537 | def contains(self, cond=None, ids=None): 538 | """ 539 | Check wether the database contains an element matching a condition or 540 | an ID. 541 | 542 | If ``ids`` is set, it checks if the db contains an element with one 543 | of the specified. 544 | 545 | :param cond: the condition use 546 | :type cond: Query 547 | :param ids: the element IDs to look for 548 | """ 549 | 550 | if ids is not None: 551 | # Elements specified by ID 552 | return any(self.get(id=id) for id in ids) 553 | 554 | # Element specified by condition 555 | return self.get(cond) is not None 556 | 557 | # Set the default table class 558 | Flata.table_class = Table 559 | -------------------------------------------------------------------------------- /tests/test_crud.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import sys 3 | 4 | import pytest 5 | 6 | from flata import Flata, where, Query 7 | from flata.storages import MemoryStorage 8 | from flata.middlewares import Middleware, CachingMiddleware 9 | 10 | def test_insert(db): 11 | db.purge_tables() 12 | tb = db.table('t') 13 | tb.insert({'int': 1, 'char': 'a'}) 14 | 15 | assert tb.count(where('int') == 1) == 1 16 | 17 | db.purge_tables() 18 | 19 | db.table('t').insert({'int': 1, 'char': 'a'}) 20 | db.table('t').insert({'int': 1, 'char': 'b'}) 21 | db.table('t').insert({'int': 1, 'char': 'c'}) 22 | 23 | assert db.table('t').count(where('int') == 1) == 3 24 | assert db.table('t').count(where('char') == 'a') == 1 25 | 26 | 27 | def test_insert_ids(db): 28 | db.purge_tables() 29 | assert db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 1, 'char': 'a', 'int': 1} 30 | assert db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 2, 'char': 'a', 'int': 1} 31 | 32 | 33 | def test_insert_multiple(db): 34 | db.purge_tables() 35 | 36 | # Insert multiple from list 37 | db.table('t').insert_multiple([{'int': 1, 'char': 'a'}, 38 | {'int': 1, 'char': 'b'}, 39 | {'int': 1, 'char': 'c'}]) 40 | 41 | assert db.table('t').count(where('int') == 1) == 3 42 | assert db.table('t').count(where('char') == 'a') == 1 43 | 44 | # Insert multiple from generator function 45 | def generator(): 46 | for j in range(10): 47 | yield {'int': j} 48 | 49 | db.purge_tables() 50 | 51 | db.table('t').insert_multiple(generator()) 52 | 53 | for i in range(10): 54 | assert db.table('t').count(where('int') == i) == 1 55 | assert db.table('t').count(where('int').exists()) == 10 56 | 57 | # Insert multiple from inline generator 58 | db.purge_tables() 59 | 60 | db.table('t').insert_multiple({'int': i} for i in range(10)) 61 | 62 | for i in range(10): 63 | assert db.table('t').count(where('int') == i) == 1 64 | 65 | 66 | def test_insert_multiple_with_ids(db): 67 | db.purge_tables() 68 | 69 | # Insert multiple from list 70 | assert db.table('t').insert_multiple( 71 | [{'int': 1, 'char': 'a'}, 72 | {'int': 1, 'char': 'b'}, 73 | {'int': 1, 'char': 'c'}]) == [{'id': 1, 'char': 'a', 'int': 1}, 74 | {'id': 2, 'char': 'b', 'int': 1}, 75 | {'id': 3, 'char': 'c', 'int': 1}] 76 | 77 | 78 | def test_remove(db): 79 | db.table('t').remove(where('char') == 'b') 80 | assert len(db.table('t').all()) == 2 81 | assert db.table('t').count(where('int') == 1) == 2 82 | 83 | def test_remove_by_id(db): 84 | db.table('t').remove(where('id') == 1) 85 | assert len(db.table('t').all()) == 2 86 | db.table('t').remove( ids = [2] ) 87 | assert len(db.table('t').all()) == 1 88 | 89 | 90 | def test_remove_multiple(db): 91 | db.table('t').remove(where('int') == 1) 92 | 93 | assert len(db.table('t').all()) == 0 94 | 95 | 96 | def test_remove_ids(db): 97 | db.table('t').remove(ids=[1, 2]) 98 | 99 | assert len(db.table('t').all()) == 1 100 | 101 | 102 | def test_remove_returns_ids(db): 103 | assert db.table('t').remove(where('char') == 'b')[0] == [2] 104 | 105 | 106 | def test_update(db): 107 | assert db.table('t').count(where('int') == 1) == 3 108 | 109 | db.table('t').update({'int': 2}, where('char') == 'a') 110 | 111 | assert db.table('t').count(where('int') == 2) == 1 112 | assert db.table('t').count(where('int') == 1) == 2 113 | 114 | 115 | def test_update_returns_ids(db): 116 | db.purge_tables() 117 | assert db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 1, 'char': 'a', 'int': 1} 118 | assert db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 2, 'char': 'a', 'int': 1} 119 | 120 | assert db.table('t').update( 121 | {'char': 'b'}, where('int') == 1) == ( 122 | [1, 2], [{'id': 1, 'char': 'b', 'int': 1}, 123 | {'id': 2, 'char': 'b', 'int': 1}]) 124 | 125 | 126 | def test_update_transform(db): 127 | def increment(field): 128 | def transform(el): 129 | el[field] += 1 130 | 131 | return transform 132 | 133 | def delete(field): 134 | def transform(el): 135 | del el[field] 136 | 137 | return transform 138 | 139 | assert db.table('t').count(where('int') == 1) == 3 140 | 141 | db.table('t').update(increment('int'), where('char') == 'a') 142 | db.table('t').update(delete('char'), where('char') == 'a') 143 | 144 | assert db.table('t').count(where('int') == 2) == 1 145 | assert db.table('t').count(where('char') == 'a') == 0 146 | assert db.table('t').count(where('int') == 1) == 2 147 | 148 | 149 | def test_update_ids(db): 150 | db.table('t').update({'int': 2}, ids=[1, 2]) 151 | 152 | assert db.table('t').count(where('int') == 2) == 2 153 | 154 | 155 | def test_queries_where(db): 156 | assert len(db.table('t').all() ) == 3 157 | assert not db.table('t')._query_cache 158 | assert len(db.table('t').search(where('int') == 1)) == 3 159 | 160 | assert len(db.table('t')._query_cache) == 1 161 | assert len(db.table('t').search(where('int') == 1)) == 3 # Query result from cache 162 | assert len(db.table('t').search(~(where('char')=='a'))) == 2 163 | assert len(db.table('t').search((where('int') == 1) & (where('char')=='a'))) == 1 164 | assert len(db.table('t').search((where('char')=='b') | (where('char')=='a'))) == 2 165 | 166 | def test_queries_query(db): 167 | assert len(db.table('t').search(Query().char == 'a')) == 1 168 | assert len(db.table('t').search(Query().char == 'b')) == 1 169 | assert len(db.table('t').search((Query().char == 'c') & (Query().int == 1 ))) == 1 170 | assert len(db.table('t').search((Query().char == 'c') | (Query().char == 'a' ))) == 2 171 | assert len(db.table('t').search((Query()['char'] == 'c') & (Query()['int'] == 1 ))) == 1 172 | assert len(db.table('t').search((Query()['char']== 'c') | (Query()['char'] == 'a' ))) == 2 173 | 174 | 175 | def test_queries_matches(db): 176 | db.purge_tables() 177 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 178 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 179 | 180 | assert len(db.table('t').search(Query()['chars'].matches('abcd'))) == 1 181 | assert len(db.table('t').search(Query()['chars'].matches('abc'))) == 1 182 | assert len(db.table('t').search(Query()['chars'].matches('ad'))) == 0 183 | # assert len(db.table('t').search(Query()['chars'].match(['cd']))) == 1 184 | 185 | def test_queries_matches_ignore_case(db): 186 | db.purge_tables() 187 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 188 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 189 | 190 | assert len(db.table('t').search(Query()['chars'].matches_ignore_case('aBcd'))) == 1 191 | assert len(db.table('t').search(Query()['chars'].matches_ignore_case('abcc'))) == 0 192 | assert len(db.table('t').search(Query()['chars'].matches_ignore_case('Ab'))) == 1 193 | 194 | def test_queries_search(db): 195 | db.purge_tables() 196 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 197 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 198 | 199 | assert len(db.table('t').search(Query()['chars'].search('cd'))) == 1 200 | assert len(db.table('t').search(Query()['chars'].search('d'))) == 1 201 | assert len(db.table('t').search(Query()['chars'].search('a'))) == 2 202 | assert len(db.table('t').search(Query()['chars'].search('abcd'))) == 1 203 | assert len(db.table('t').search(Query()['chars'].search('bcda'))) == 0 204 | 205 | def test_queries_search_ignore_case(db): 206 | db.purge_tables() 207 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 208 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 209 | 210 | assert len(db.table('t').search(Query()['chars'].search_ignore_case('Cd'))) == 1 211 | assert len(db.table('t').search(Query()['chars'].search_ignore_case('D'))) == 1 212 | assert len(db.table('t').search(Query()['chars'].search_ignore_case('a'))) == 2 213 | assert len(db.table('t').search(Query()['chars'].search_ignore_case('aBcd'))) == 1 214 | assert len(db.table('t').search(Query()['chars'].search_ignore_case('bcda'))) == 0 215 | 216 | 217 | @pytest.mark.skipif(sys.version_info < (3, 0), 218 | reason="requires python3") 219 | def test_queries_any(db): 220 | db.purge_tables() 221 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 222 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 223 | assert db.table('t').insert({'int': 3, 'chars': 'x', 'char2': 'abcd'}) == {'id': 3, 'chars': 'x', 'char2': 'abcd', 'int': 3} 224 | 225 | assert len(db.table('t').search(Query()['chars'].any(['d']))) == 1 226 | assert len(db.table('t').search(Query()['chars'].any(['c']))) == 2 227 | assert len(db.table('t').search(Query()['chars'].any(['acd']))) == 0 228 | assert len(db.table('t').search(Query()['chars'].any('acd'))) == 2 229 | 230 | def test_queries_any_in_list(db): 231 | db.purge_tables() 232 | assert db.table('t').insert({'int': 1, 'charList': ['abcd', 'a']}) == {'id': 1, 'charList': ['abcd', 'a'], 'int': 1} 233 | assert db.table('t').insert({'int': 2, 'charList': ['ac']}) == {'id': 2, 'charList': ['ac'], 'int': 2} 234 | 235 | assert len(db.table('t').search(Query()['charList'].any(['a']))) == 1 236 | assert len(db.table('t').search(Query()['charList'].any(['a', 'abcd']))) == 1 237 | assert len(db.table('t').search(Query()['charList'].any('ac'))) == 2 238 | assert len(db.table('t').search(Query()['charList'].any('abcd'))) == 1 239 | 240 | @pytest.mark.skipif(sys.version_info < (3, 0), 241 | reason="requires python3") 242 | def test_queries_all(db): 243 | db.purge_tables() 244 | assert db.table('t').insert({'int': 1, 'chars': 'abcd'}) == {'id': 1, 'chars': 'abcd', 'int': 1} 245 | assert db.table('t').insert({'int': 2, 'chars': 'ac'}) == {'id': 2, 'chars': 'ac', 'int': 2} 246 | assert db.table('t').insert({'int': 3, 'chars': 'x', 'char2': 'abcd'}) == {'id': 3, 'chars': 'x', 'char2': 'abcd', 'int': 3} 247 | 248 | assert len(db.table('t').search(Query()['chars'].all(['d']))) == 1 249 | assert len(db.table('t').search(Query()['chars'].all(['c']))) == 2 250 | assert len(db.table('t').search(Query()['chars'].all(['acd']))) == 0 251 | assert len(db.table('t').search(Query()['chars'].all('acd'))) == 1 252 | 253 | 254 | def test_queries_all_in_list(db): 255 | db.purge_tables() 256 | assert db.table('t').insert({'int': 1, 'charList': ['abcd', 'a']}) == {'id': 1, 'charList': ['abcd', 'a'], 'int': 1} 257 | assert db.table('t').insert({'int': 2, 'charList': ['ac']}) == {'id': 2, 'charList': ['ac'], 'int': 2} 258 | 259 | assert len(db.table('t').search(Query()['charList'].all(['abcd']))) == 1 260 | assert len(db.table('t').search(Query()['charList'].all(['a']))) == 1 261 | assert len(db.table('t').search(Query()['charList'].all(['ac']))) == 1 262 | assert len(db.table('t').search(Query()['charList'].all('abcd'))) == 0 263 | assert len(db.table('t').search(Query()['charList'].all('ac'))) == 0 264 | 265 | def test_get(db): 266 | item = db.table('t').get(where('char') == 'b') 267 | assert item['char'] == 'b' 268 | 269 | 270 | def test_get_ids(db): 271 | el = db.table('t').all()[0] 272 | assert db.table('t').get(id=el.id) == el 273 | assert db.table('t').get(id=float('NaN')) is None 274 | 275 | 276 | def test_count(db): 277 | assert db.table('t').count(where('int') == 1) == 3 278 | assert db.table('t').count(where('char') == 'd') == 0 279 | 280 | 281 | def test_contains(db): 282 | assert db.table('t').contains(where('int') == 1) 283 | assert not db.table('t').contains(where('int') == 0) 284 | 285 | 286 | def test_contains_ids(db): 287 | assert db.table('t').contains(ids=[1, 2]) 288 | assert not db.table('t').contains(ids=[88]) 289 | 290 | 291 | def test_get_idempotent(db): 292 | u = db.table('t').get(where('int') == 1) 293 | z = db.table('t').get(where('int') == 1) 294 | assert u == z 295 | 296 | 297 | def test_multiple_dbs(): 298 | db1 = Flata(storage=MemoryStorage) 299 | db2 = Flata(storage=MemoryStorage) 300 | 301 | db1.table('t').insert({'int': 1, 'char': 'a'}) 302 | db1.table('t').insert({'int': 1, 'char': 'b'}) 303 | db1.table('t').insert({'int': 1, 'value': 5.0}) 304 | 305 | db2.table('t').insert({'color': 'blue', 'animal': 'turtle'}) 306 | 307 | assert len(db1.table('t').all()) == 3 308 | assert len(db2.table('t').all()) == 1 309 | 310 | 311 | 312 | def test_unique_ids(tmpdir): 313 | """ 314 | :type tmpdir: py._path.local.LocalPath 315 | """ 316 | path = str(tmpdir.join('test.db.json')) 317 | 318 | # Verify ids are unique when reopening the DB and inserting 319 | with Flata(path) as _db: 320 | _db.table('t').insert({'x': 1}) 321 | 322 | with Flata(path) as _db: 323 | _db.table('t').insert({'x': 1}) 324 | 325 | with Flata(path) as _db: 326 | data = _db.table('t').all() 327 | 328 | assert data[0].id != data[1].id 329 | 330 | # Verify ids stay unique when inserting/removing 331 | with Flata(path) as _db: 332 | _db.purge_tables() 333 | _db.table('t').insert_multiple({'x': i} for i in range(5)) 334 | _db.table('t').remove(where('x') == 2) 335 | 336 | assert len(_db.table('t').all()) == 4 337 | 338 | ids = [e.id for e in _db.table('t').all()] 339 | assert len(ids) == len(set(ids)) 340 | 341 | 342 | def test_lastid_after_open(tmpdir): 343 | NUM = 100 344 | path = str(tmpdir.join('test.db.json')) 345 | 346 | with Flata(path) as _db: 347 | _db.table('t').insert_multiple({'i': i} for i in range(NUM)) 348 | 349 | with Flata(path) as _db: 350 | assert _db.table('t')._last_id == NUM 351 | 352 | 353 | @pytest.mark.skipif(sys.version_info >= (3, 0), 354 | reason="requires python2") 355 | def test_unicode_memory(db): 356 | unic_str = 'ß'.decode('utf-8') 357 | byte_str = 'ß' 358 | 359 | db.table('t').insert({'value': unic_str}) 360 | assert db.table('t').contains(where('value') == byte_str) 361 | assert db.table('t').contains(where('value') == unic_str) 362 | 363 | db.purge_tables() 364 | db.table('t').insert({'value': byte_str}) 365 | assert db.table('t').contains(where('value') == byte_str) 366 | assert db.table('t').contains(where('value') == unic_str) 367 | 368 | 369 | @pytest.mark.skipif(sys.version_info >= (3, 0), 370 | reason="requires python2") 371 | def test_unicode_json(tmpdir): 372 | unic_str1 = 'a'.decode('utf-8') 373 | byte_str1 = 'a' 374 | 375 | unic_str2 = 'ß'.decode('utf-8') 376 | byte_str2 = 'ß' 377 | 378 | path = str(tmpdir.join('test.db.json')) 379 | 380 | with Flata(path) as _db: 381 | _db.purge_tables() 382 | _db.table('t').insert({'value': byte_str1}) 383 | _db.table('t').insert({'value': byte_str2}) 384 | assert _db.table('t').contains(where('value') == byte_str1) 385 | assert _db.table('t').contains(where('value') == unic_str1) 386 | assert _db.table('t').contains(where('value') == byte_str2) 387 | assert _db.table('t').contains(where('value') == unic_str2) 388 | 389 | with Flata(path) as _db: 390 | _db.purge_tables() 391 | _db.table('t').insert({'value': unic_str1}) 392 | _db.table('t').insert({'value': unic_str2}) 393 | assert _db.table('t').contains(where('value') == byte_str1) 394 | assert _db.table('t').contains(where('value') == unic_str1) 395 | assert _db.table('t').contains(where('value') == byte_str2) 396 | assert _db.table('t').contains(where('value') == unic_str2) 397 | 398 | 399 | def testids_json(tmpdir): 400 | path = str(tmpdir.join('test.db.json')) 401 | 402 | with Flata(path) as _db: 403 | _db.purge_tables() 404 | assert _db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 1, 'char': 'a', 'int': 1} 405 | assert _db.table('t').insert({'int': 1, 'char': 'a'}) == {'id': 2, 'char': 'a', 'int': 1} 406 | 407 | _db.purge_tables() 408 | assert _db.table('t').insert_multiple( 409 | [{'int': 1, 'char': 'a'} 410 | ,{'int': 1, 'char': 'b'} 411 | ,{'int': 1, 'char': 'c'}]) == [{'id': 1, 'char': 'a', 'int': 1} 412 | ,{'id': 2, 'char': 'b', 'int': 1} 413 | ,{'id': 3, 'char': 'c', 'int': 1}] 414 | 415 | assert _db.table('t').contains(ids=[1, 2]) 416 | assert not _db.table('t').contains(ids=[88]) 417 | 418 | _db.table('t').update({'int': 2}, ids=[1, 2]) 419 | assert _db.table('t').count(where('int') == 2) == 2 420 | 421 | el = _db.table('t').all()[0] 422 | assert _db.table('t').get(id=el.id) == el 423 | assert _db.table('t').get(id=float('NaN')) is None 424 | 425 | _db.table('t').remove(ids=[1, 2]) 426 | assert len(_db.table('t').all()) == 1 427 | 428 | 429 | 430 | def test_insert_object(tmpdir): 431 | path = str(tmpdir.join('test.db.json')) 432 | 433 | with Flata(path) as _db: 434 | _db.purge_tables() 435 | data = [ {'int': 1, 'object' : {'object_id': 2}}] 436 | _db.table('t').insert_multiple(data) 437 | 438 | assert _db.table('t').all() == [{'id': 1, 'int': 1, 'object': {'object_id': 2}}] 439 | 440 | def test_insert_invalid_array_string(tmpdir): 441 | path = str(tmpdir.join('test.db.json')) 442 | 443 | with Flata(path) as _db: 444 | data = [{'int': 1}, {'int': 2}] 445 | _db.table('t').insert_multiple(data) 446 | 447 | with pytest.raises(ValueError): 448 | _db.table('t').insert([1, 2, 3]) # Fails 449 | 450 | with pytest.raises(ValueError): 451 | _db.table('t').insert('fails') # Fails 452 | 453 | assert len(_db.table('t').all()) == 2 454 | 455 | # _db.table('t').insert({'int': 3}) # Does not fail 456 | 457 | 458 | def test_insert_invalid_dict(tmpdir): 459 | path = str(tmpdir.join('test.db.json')) 460 | 461 | with Flata(path) as _db: 462 | _db.purge_tables() 463 | data = [{'int': 1}, {'int': 2}] 464 | _db.table('t').insert_multiple(data) 465 | 466 | with pytest.raises(TypeError): 467 | _db.table('t').insert({'int': set(['bark'])}) # Fails 468 | 469 | assert len(_db.table('t').all()) == 2 # Table only has 2 records 470 | 471 | # _db.table('t').insert({'int': 3}) # Does not fail 472 | 473 | 474 | def test_gc(tmpdir): 475 | path = str(tmpdir.join('test.db.json')) 476 | db = Flata(path) 477 | table = db.table('foo') 478 | table.insert({'something': 'else'}) 479 | table.insert({'int': 13}) 480 | assert len(table.search(where('int') == 13)) == 1 481 | assert table.all() == [{'id': 1,'something': 'else'}, {'id': 2,'int': 13}] 482 | db.close() 483 | 484 | 485 | def test_empty_write(tmpdir): 486 | path = str(tmpdir.join('test.db.json')) 487 | 488 | class ReadOnlyMiddleware(Middleware): 489 | def write(self, data): 490 | raise AssertionError('No write for unchanged db') 491 | 492 | Flata(path).close() 493 | Flata(path, storage=ReadOnlyMiddleware()).close() 494 | 495 | 496 | def test_not_defaultid (tmpdir): 497 | path = str(tmpdir.join('test.db.json')) 498 | db = Flata(path) 499 | table = db.table('foo', id_field='_not_default_id') 500 | table.insert({'something': 'else'}) 501 | assert table.all() == [{'_not_default_id': 1,'something': 'else'}] 502 | 503 | def test_update_with_not_default_id(tmpdir): 504 | path = str(tmpdir.join('test.db.json')) 505 | db = Flata(path) 506 | table = db.table('foo', id_field='_not_default_id') 507 | table.insert({'something': 'else'}) 508 | 509 | assert db.table('foo').count(where('something') == 'else') == 1 510 | 511 | db.table('foo').update({'something': 'updated'}, where('something') == 'else') 512 | 513 | assert db.table('foo').count(where('something') == 'updated') == 1 514 | # assert db.table('t').count(where('int') == 1) == 2 515 | 516 | def test_remove_with_not_default_id(tmpdir): 517 | path = str(tmpdir.join('test.db.json')) 518 | db = Flata(path) 519 | table = db.table('foo', id_field='_not_default_id') 520 | table.insert({'something': 'else'}) 521 | 522 | assert db.table('foo').count(where('something') == 'else') == 1 523 | 524 | db.table('foo').remove(where('something') == 'else') 525 | 526 | assert db.table('foo').all() == [] 527 | 528 | def test_query_memory_storage(): 529 | db = Flata(storage=MemoryStorage) 530 | db.table('t').insert_multiple([ 531 | {'name': 'foo', 'value': 42}, 532 | {'name': 'bar', 'value': -1337} 533 | ]) 534 | 535 | query = where('value') > 0 536 | 537 | results = db.table('t').search(query) 538 | assert len(results) == 1 539 | # Now, modify the result ist 540 | results.extend([1]) 541 | 542 | assert db.table('t').search(query) == [{'id': 1,'name': 'foo', 'value': 42}] 543 | 544 | 545 | 546 | def test_insert_with_external_cache(db): 547 | _cache = CachingMiddleware(MemoryStorage)() 548 | db = Flata(cache=_cache) 549 | tb = db.table('t') 550 | tb.insert({'int': 1, 'char': 'a'}) 551 | 552 | assert tb.count(where('int') == 1) == 1 553 | 554 | db2 = Flata(cache=_cache) 555 | tb = db2.table('t') 556 | 557 | db2.table('t').insert({'int': 1, 'char': 'a'}) 558 | db2.table('t').insert({'int': 1, 'char': 'b'}) 559 | db2.table('t').insert({'int': 1, 'char': 'c'}) 560 | 561 | assert db2.table('t').count(where('int') == 1) == 4 562 | assert db2.table('t').count(where('char') == 'a') == 2 563 | 564 | def test_remove_with_external_cache(db): 565 | _cache = CachingMiddleware(MemoryStorage)() 566 | db = Flata(cache=_cache) 567 | tb = db.table('t') 568 | tb.insert({'int': 1, 'char': 'a'}) 569 | 570 | assert tb.count(where('int') == 1) == 1 571 | 572 | db2 = Flata(cache=_cache) 573 | tb = db2.table('t') 574 | 575 | db2.table('t').remove(where('int') == 1) 576 | assert db2.table('t').count(where('int') == 1) == 0 577 | 578 | def test_update_with_external_cache(db): 579 | _cache = CachingMiddleware(MemoryStorage)() 580 | db = Flata(cache=_cache) 581 | tb = db.table('t') 582 | tb.insert({'int': 1, 'char': 'a'}) 583 | 584 | assert tb.count(where('int') == 1) == 1 585 | 586 | db2 = Flata(cache=_cache) 587 | tb = db2.table('t') 588 | db2.table('t').update({'int': 2}, where('char') == 'a') 589 | 590 | assert db2.table('t').count(where('int') == 2) == 1 591 | 592 | def test_flatdb_is_iterable(db): 593 | assert [r for r in db] == db.table('t').all() 594 | 595 | 596 | --------------------------------------------------------------------------------