From 90749a5222ebfdfab44ef1b571e3ccb2cdf2a51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Zadro=C5=BCny?= Date: Sun, 12 Mar 2017 17:16:42 +0100 Subject: [PATCH] initial commit --- .gitignore | 144 ++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 15 +++++ README.rst | 33 ++++++++++ bin/run_tests.sh | 13 ++++ setup.py | 24 ++++++++ tests/__init__.py | 0 tests/test_Value.py | 36 +++++++++++ vo/Value.py | 114 +++++++++++++++++++++++++++++++++++ vo/__init__.py | 0 9 files changed, 379 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.rst create mode 100755 bin/run_tests.sh create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_Value.py create mode 100644 vo/Value.py create mode 100644 vo/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f4d997 --- /dev/null +++ b/.gitignore @@ -0,0 +1,144 @@ +# Created by .ignore support plugin (hsz.mobi) +### Python template +# 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/ +wheels/ +*.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/ +coverage/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +## Custom +/.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..edb3ba7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2017, Paweł Zadrożny @pawelzny + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7170e5b --- /dev/null +++ b/README.rst @@ -0,0 +1,33 @@ +============ +Value Object +============ + +:Info: DDD Value Objects implementation. +:Author: Paweł Zadrożny + +Installation: +============= + +.. code:: bash + pip install vo + + +**Package**: https://pypi.python.org/pypi/vo + + +Example +======= + +.. code:: python + from vo.Value import Value + + value1 = Value(test=True, some_text="I am some text string") + value2 = Value(some_text="I am some text string", test=True) + + assert(value1 == value2) # True + + value1.set("extra_attr", "whatever") + + assert(value1 == value2) # False + assert("whatever" == value1.get("extra_attr")) # True + assert("default value" == value2.get("extra_attr", "default value")) # True diff --git a/bin/run_tests.sh b/bin/run_tests.sh new file mode 100755 index 0000000..89491f7 --- /dev/null +++ b/bin/run_tests.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Get absolute path to this script directory +bin_dir=$(dirname $(readlink -f "$0")) +package_dir=${bin_dir}/../ +coverage_dir=${package_dir}/tests/coverage + +# Run tests with nose test runner +nosetests --where=${package_dir} \ + --with-coverage --cover-erase --cover-branches \ + --cover-html --cover-html-dir=${coverage_dir} \ + --cover-xml --cover-xml-file=${package_dir}/coverage.xml \ + --stop diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..433461e --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages + + +with open('README.rst') as f: + readme = f.read() + +with open('LICENSE') as f: + license = f.read() + +setup(name='vo', + version='0.1.0', + description='DDD Value Objects implementation', + long_description=readme, + keywords='value data object DDD', + author='Pawel Zadrozny @pawelzny', + author_email='pawel.zny@gmail.com', + url='https://github.com/pawelzny/vo', + license=license, + test_suite='nose.collector', + tests_require=['nose', 'coverage'], + packages=find_packages(exclude=('tests', 'docs', 'bin')), + zip_safe=False) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_Value.py b/tests/test_Value.py new file mode 100644 index 0000000..2e91c91 --- /dev/null +++ b/tests/test_Value.py @@ -0,0 +1,36 @@ +import unittest + +from vo.Value import Value + + +class ValueTest(unittest.TestCase): + def test_assign_kwargs(self): + value = Value(test=True, text="Some text") + + self.assertTrue(value.test) + self.assertEqual('Some text', value.text) + + def test_equal(self): + v1 = Value(text="first text", other_attr=1243) + v2 = Value(other_attr=1243, text="first text") + + self.assertEqual(v1, v2) + self.assertEqual(hash(v1), hash(v2)) + + v1.new_attr = True + v2.new_attr = False + + self.assertNotEqual(v1, v2) + self.assertNotEqual(hash(v1), hash(v2)) + + def test_setter(self): + v1 = Value() + v1.set(some_attr="I am some attribute") + self.assertEqual('I am some attribute', v1.get('some_attr')) + + v1.set(some_attr="Now i am different") + self.assertEqual("Now i am different", v1.some_attr) + + def test_getter(self): + v1 = Value() + self.assertEqual("Example string", v1.get('undefined_attr', "Example string")) diff --git a/vo/Value.py b/vo/Value.py new file mode 100644 index 0000000..9e105db --- /dev/null +++ b/vo/Value.py @@ -0,0 +1,114 @@ +import hashlib + + +class Value: + """ + Basic implementation of DDD Value Object. These objects + are meant to holds data only without any business logic. + When data has no identifier so it is not an Entity, + there comes Value Objects. + + Value Objects are consider same if they holds the same + data even if allocated in different parts of memory. + + :Example: + + import vo + + value1 = vo.Value(name="test value", test=True) + value2 = vo.Value(test=True, name="test value") + assert(value1 == value2) # True + + value1.set(extra="extra data") + assert(value1 == value2) # False + """ + + @staticmethod + def to_bytes(string): + """ + Converts string to byte string + :param + string (str): String or number to convert + :return + (str): Byte string + """ + + return bytes(repr(string), 'utf-8') + + def __init__(self, **kwargs): + """ + Dynamically add all kwargs to self dictionary. + :param + kwargs: Key-value pairs + """ + + self.set(**kwargs) + + def __eq__(self, other): + """ + Predicate if checksum of Value Objects are the same. + :param + other: ValueObject + :return + (bool): Boolean + """ + + return self.checksum() == other.checksum() + + def __ne__(self, other): + """ + Predicate if checksum of Value Objects are different. + :param + other: ValueObject + :return + (bool): Boolean + """ + + return self.checksum() != other.checksum() + + def __hash__(self): + """ + Returns hash of checksum + :return + (int): Integer representing object Hash + """ + + return hash(self.checksum()) + + def set(self, **kwargs): + """ + Set additional attributes from kwargs + :param + kwargs: + :return + (self): Returns self to allow method chaining. + """ + + self.__dict__.update(kwargs) + + return self + + def get(self, name, default=None): + """ + Gets attribute value or default if not exists. + :param + name (str): Attribute name + default (any): Default value + :return + (any): Existing attribute value or default + """ + + return getattr(self, name, default) + + def checksum(self): + """ + Computes and returns sha224 string from self dict items. + :return + (str): SHA224 string representing checksum of object values. + """ + + ck_sum = Value.to_bytes('checksum:') + for key, value in sorted(self.__dict__.items()): + ck_sum += Value.to_bytes(str(key) + str(value)) + + return hashlib.sha224(ck_sum).hexdigest() diff --git a/vo/__init__.py b/vo/__init__.py new file mode 100644 index 0000000..e69de29