diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b990971 --- /dev/null +++ b/.gitignore @@ -0,0 +1,118 @@ +README.rst + +.vagrant + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit tests / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env +envfile + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Vim +Session.vim + +# Pycharm +.idea + +# sqlite db for testing +local.db +/tests/fast-test.sh +/tests/fastest-test.sh +/tests/sbds-install.sh +/tests/test.sh +/tests/sync.sh +/tests/failed_blocks/ +tests/failed_blocks/ +/envdir/ +/sbds.egg-info/ +/envd/ +/tests/envdir-to-envfile.sh +/deploy/ +/test.py + +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1bd59dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM phusion/baseimage:0.9.19 + +# Standard stuff +ENV LANG en_US.UTF-8 +ENV LC_ALL en_US.UTF-8 + +# Stuff for building steem-python +ARG BUILD_ROOT=/build + +# Now we install the essentials +RUN \ + apt-get update && \ + apt-get install -y python3-pip + +# This updates the distro-provided pip +RUN pip3 install --upgrade pip + +RUN mkdir ${BUILD_ROOT} + +COPY Makefile ${BUILD_ROOT}/ +COPY Pipfile ${BUILD_ROOT}/ +COPY Pipfile.lock ${BUILD_ROOT}/ + +WORKDIR ${BUILD_ROOT} + +RUN pip3 install --upgrade pip && \ + pip3 install --upgrade pipenv && \ + pipenv install --three --dev && \ + pipenv install . + +COPY . ${BUILD_ROOT} + +# run tests +RUN pipenv run py.test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..097f2b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +default: test + +test: + pipenv run pytest diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e5dd0cf --- /dev/null +++ b/Pipfile @@ -0,0 +1,28 @@ +[[source]] + +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + + +[packages] + +attrdict = "*" +appdirs = "*" + + +[dev-packages] + +"pep8" = "*" +pytest = "*" +"pytest-pep8" = "*" +pytest-pylint = "*" +yapf = "*" +"autopep8" = "*" +"flake8" = "*" +"pytest-flake8" = "*" + + +[requires] + +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..489e148 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,243 @@ +{ + "_meta": { + "hash": { + "sha256": "0cff184f2ce33ae5643e32fe505945779e8c9ae2e91b46c1c38f5938bf42050e" + }, + "host-environment-markers": { + "implementation_name": "cpython", + "implementation_version": "3.6.4", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "17.3.0", + "platform_system": "Darwin", + "platform_version": "Darwin Kernel Version 17.3.0: Thu Nov 9 18:09:22 PST 2017; root:xnu-4570.31.3~1/RELEASE_X86_64", + "python_full_version": "3.6.4", + "python_version": "3.6", + "sys_platform": "darwin" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e", + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92" + ], + "version": "==1.4.3" + }, + "attrdict": { + "hashes": [ + "sha256:86aeb6d3809e0344409f8148d7cac9eabce5f0b577c160b5e90d10df3f8d2ad3" + ], + "version": "==2.0.0" + }, + "six": { + "hashes": [ + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" + ], + "version": "==1.11.0" + } + }, + "develop": { + "apipkg": { + "hashes": [ + "sha256:65d2aa68b28e7d31233bb2ba8eb31cda40e4671f8ac2d6b241e358c9652a74b9", + "sha256:2e38399dbe842891fe85392601aab8f40a8f4cc5a9053c326de35a1cc0297ac6" + ], + "version": "==1.4" + }, + "astroid": { + "hashes": [ + "sha256:db5cfc9af6e0b60cd07c19478fb54021fc20d2d189882fbcbc94fc69a8aecc58", + "sha256:f0a0e386dbca9f93ea9f3ea6f32b37a24720502b7baa9cb17c3976a680d43a06" + ], + "version": "==1.6.1" + }, + "attrs": { + "hashes": [ + "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450", + "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9" + ], + "version": "==17.4.0" + }, + "autopep8": { + "hashes": [ + "sha256:c7be71ab0cb2f50c9c22c82f0c9acaafc6f57492c3fbfee9790c415005c2b9a5" + ], + "version": "==1.3.4" + }, + "execnet": { + "hashes": [ + "sha256:fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83", + "sha256:a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a" + ], + "version": "==1.5.0" + }, + "flake8": { + "hashes": [ + "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37", + "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0" + ], + "version": "==3.5.0" + }, + "isort": { + "hashes": [ + "sha256:cd5d3fc2c16006b567a17193edf4ed9830d9454cbeb5a42ac80b36ea00c23db4", + "sha256:79f46172d3a4e2e53e7016e663cc7a8b538bec525c36675fcfd2767df30b3983" + ], + "version": "==4.2.15" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pep8": { + "hashes": [ + "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee", + "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374" + ], + "version": "==1.7.1" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", + "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" + ], + "version": "==1.5.2" + }, + "pycodestyle": { + "hashes": [ + "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9", + "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766" + ], + "version": "==2.3.1" + }, + "pyflakes": { + "hashes": [ + "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", + "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + ], + "version": "==1.6.0" + }, + "pylint": { + "hashes": [ + "sha256:156839bedaa798febee72893beef00c650c2e7abafb5586fc7a6a56be7f80412", + "sha256:4fe3b99da7e789545327b75548cee6b511e4faa98afe268130fea1af4b5ec022" + ], + "version": "==1.8.2" + }, + "pytest": { + "hashes": [ + "sha256:b84878865558194630c6147f44bdaef27222a9f153bbd4a08908b16bf285e0b1", + "sha256:53548280ede7818f4dc2ad96608b9f08ae2cc2ca3874f2ceb6f97e3583f25bc4" + ], + "version": "==3.3.2" + }, + "pytest-cache": { + "hashes": [ + "sha256:be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9" + ], + "version": "==1.0" + }, + "pytest-flake8": { + "hashes": [ + "sha256:e67686645860009cf1f9a5016e110234b4a2c4584711d62ba753ed651ab9ab28", + "sha256:e716072d07a557defdd5c4141984569731e292961370a5663c1697283aa16200" + ], + "version": "==0.9.1" + }, + "pytest-pep8": { + "hashes": [ + "sha256:032ef7e5fa3ac30f4458c73e05bb67b0f036a8a5cb418a534b3170f89f120318" + ], + "version": "==1.0.6" + }, + "pytest-pylint": { + "hashes": [ + "sha256:9b8ca25823b2f39e89d8170453f5282e57b973395060e838ced5f8c09271ca65", + "sha256:2efaf761472637df9a8f4a3f4fac37f8ce433d70957c5f5767c4be322a42a3d2", + "sha256:9f38725b22967a56724115c9df0a93dda37fea71dd5495fb1354b82e3d938d0d", + "sha256:85da6403c69eb715b9703df640818f337603f2cac947f932b033588851aaaf16", + "sha256:b85763dc36757bfb736b07fecb4f67a0892dcb00868e01f150c7424f608bd62e", + "sha256:ec63f7c4c05331654ab54fda8e68b8a11512009d506a8e35ee9b6d40a359356d", + "sha256:2bb26948f0355d14b274742153a6b4daa51e6d60481143bfd7f025699a27210d" + ], + "version": "==0.7.1" + }, + "six": { + "hashes": [ + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb", + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9" + ], + "version": "==1.11.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + }, + "yapf": { + "hashes": [ + "sha256:a0bbc8ed274609f9c7575a5d69056fa393e26a778b3e070a72f4998b8e90c3cd", + "sha256:bd19f246be7193ad2acdc04702b92315f1ae28d49c82f6671afdeefe9d32f468" + ], + "version": "==0.20.1" + } + } +} diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..98f2e05 --- /dev/null +++ b/README.markdown @@ -0,0 +1,40 @@ +# jsondict + +This is a python package called jsondict that wraps a dict to +provide dumb json file backed persistence. + +It also returns a default of `None` for missing keys instead of KeyError, +because an exception for an undefined key is annoying, so it's sort of like +a defaultdict. + +It's not general purpose, and it's not for large amounts of data. It's +just sort of like a standard dictionary that you don't need to worry about +reading/saving to disk. It's naive and inefficient but perfect for +application configs and whatnot. Don't be silly and put more than a few +kilobytes in this, the whole file is written out every update and the whole +file is re-read and re-parsed every read. + +# Installation + +``` +pip3 install --upgrade pipenv +git clone https://github.com/sneak/jsondict.git +cd jsondict +pipenv install --three . +``` + +# Other Info + +Right now it's only tested on python3. Python2 support is desired. + +# Author + +Jeffrey Paul + +# See Also + +* [attrdict](https://github.com/bcj/AttrDict) + +# License + +MIT diff --git a/README.md b/README.md deleted file mode 100644 index a0672b2..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# jsondict -dumb json persistence for a python dictionary diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..63ca215 --- /dev/null +++ b/circle.yml @@ -0,0 +1,11 @@ +machine: + services: + - docker + +dependencies: + override: + - echo "Ignore CircleCI detected dependencies" + +test: + override: + - docker build -t sneak/persistentattrdict . diff --git a/jsondict/__init__.py b/jsondict/__init__.py new file mode 100644 index 0000000..8de3975 --- /dev/null +++ b/jsondict/__init__.py @@ -0,0 +1,63 @@ +from attrdict import AttrDict +import json +import os.path +import os +import tempfile + + +class JsonDict(object): + def __init__(self,persistence=None): + # TODO: either filename or file-like object + # for now, just filename + self._persistence = persistence + self._wrapped = dict() + + def __len__(self): + self._reread() + return len(self._wrapped) + + def keys(self): + self._reread() + return self._wrapped.keys() + + def values(self): + self._reread() + return self._wrapped.values() + + def __setitem__(self, key, item): + self._wrapped[key] = item + self._commit() + return item + + def __delitem__(self, key): + del self._wrapped[key] + self._commit() + + def __getitem__(self, key): + self._reread() + if key in self._wrapped: + return self._wrapped[key] + else: + return None + + def _commit(self): + serialized = json.dumps(self._wrapped) + atomic_write(self._persistence, serialized) + + def _reread(self): + with open(self._persistence, 'rb') as fd: + self._wrapped = json.load(fd) + +#def PersistentAppAttrDict(appname): + #configfilename = appdirs.user_data_dir(appname,appname) + "appconfig.json" + #open + +def atomic_write(fn,content): + filedir = os.path.dirname(os.path.abspath(fn)) + if not os.path.exists(filedir): + os.makedirs(filedir) + (tempfd, tempfn) = tempfile.mkstemp(dir=filedir) + tempfh = os.fdopen(tempfd, 'w') + tempfh.write(content) + tempfh.close() + os.rename(tempfn,fn) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..26b0aa0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[metadata] +description-file=README.markdown +[aliases] +test=pytest + +[tool:pytest] +norecursedirs=dist docs build .tox deploy +addopts = --pep8 --flake8 +testpaths = tests + +[yapf] +indent_width = 4 +column_limit = 77 +based_on_style = pep8 +spaces_before_comment = 2 +split_before_logical_operator = true +dedent_closing_brackets = true +i18n_comment = NOQA diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c92b4f2 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +from pipenv.project import Project +from pipenv.utils import convert_deps_to_pip +from setuptools import setup, find_packages +import sys + +pfile = Project(chdir=False).parsed_pipfile +requirements = convert_deps_to_pip(pfile['packages'], r=False) +test_requirements = convert_deps_to_pip(pfile['dev-packages'], r=False) + +setup( + name='jsondict', + version='0.0.1', + description='dumb json persistence for a dict with defaults', + long_description=open('README.markdown','r').read(), + keywords=['persistence', 'json', 'datastructure'], + license='MIT', + url='https://github.com/sneak/jsondict', + maintainer='Jeffrey Paul', + maintainer_email='sneak@sneak.berlin', + packages=find_packages(), + setup_requires=[ + 'pytest-runner', + 'pipenv', + ], + tests_require=test_requirements, + install_requires=requirements, + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Development Status :: 4 - Beta' + ]) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..be0b585 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +from logging import getLogger + +getLogger('flake8').propagate = False diff --git a/tests/test_import.py b/tests/test_import.py new file mode 100644 index 0000000..122a027 --- /dev/null +++ b/tests/test_import.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +from jsondict import JsonDict # noqa + + +# pylint: disable=unused-import,unused-variable +def test_import(): + _ = JsonDict() # noqa diff --git a/tests/test_pdd.py b/tests/test_pdd.py new file mode 100644 index 0000000..51ca355 --- /dev/null +++ b/tests/test_pdd.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from jsondict import JsonDict, atomic_write +import json +import unittest + + +class Testcases(unittest.TestCase): + + def test_atomic_write(self): + ts = "this is my test string" + tfn = '/tmp/pad_test.txt' + atomic_write(tfn, ts) + with open(tfn, 'r') as fh: + self.assertEqual(fh.read(), ts) + + def test_persistence(self): + tfn = '/tmp/pad_test.json' + mypdd = JsonDict(persistence=tfn) + mypdd['testattr'] = 'loldongs' + + with open(tfn, 'r') as fh: + x = json.load(fh) + + self.assertEqual(x['testattr'], 'loldongs') + + del mypdd['testattr'] + + self.assertEqual(mypdd['testattr'], None) + + def test_read(self): + tfn = '/tmp/pad_test.json' + mypdd = JsonDict(persistence=tfn) + mypdd['testattr'] = 'loldongs' + + with open(tfn, 'w') as fh: + json.dump({'testattr': 'notloldongs'}, fh) + + self.assertNotEqual(mypdd['testattr'], 'loldongs') + + self.assertEqual(mypdd['nonexist'], None)