From 960412c9626bac7ac663c29a58cee368f221b79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 16 Nov 2018 12:05:45 +0100 Subject: [PATCH 01/44] Initial commit --- .gitignore | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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 test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# 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 + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a8d16d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Przemysław Górecki + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. From abe3321599338385bfcc94b032791f9420c6e674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 16 Nov 2018 12:11:40 +0100 Subject: [PATCH 02/44] initial infrastructure: flask and falcon --- Pipfile | 16 ++ Pipfile.lock | 268 +++++++++++++++++++++++++ README.md | 15 ++ application/application.py | 1 + domain/domain.py | 1 + infrastructure/framework/falcon/app.py | 16 ++ infrastructure/framework/flask/app.py | 12 ++ main.py | 17 ++ 8 files changed, 346 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 application/application.py create mode 100644 domain/domain.py create mode 100644 infrastructure/framework/falcon/app.py create mode 100644 infrastructure/framework/flask/app.py create mode 100644 main.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..1dc213c --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +falcon = "*" +gunicorn = "*" +pylint = "*" + +[dev-packages] +pylint = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..369e4be --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,268 @@ +{ + "_meta": { + "hash": { + "sha256": "6a59a2cf7ba158af8d9bba5bb4acdaac64b84b45773f7236027cb82dd7bd885d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "astroid": { + "hashes": [ + "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", + "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + ], + "version": "==2.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "falcon": { + "hashes": [ + "sha256:0a66b33458fab9c1e400a9be1a68056abda178eb02a8cb4b8f795e9df20b053b", + "sha256:3981f609c0358a9fcdb25b0e7fab3d9e23019356fb429c635ce4133135ae1bc4" + ], + "index": "pypi", + "version": "==1.4.1" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "gunicorn": { + "hashes": [ + "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", + "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + ], + "index": "pypi", + "version": "==19.9.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", + "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "python-mimeparse": { + "hashes": [ + "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", + "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282" + ], + "version": "==1.6.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", + "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + ], + "version": "==2.0.4" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", + "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5e507c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +pipenv install +pipenv shell + +code . + + +To run the app as Falcon server +``` +FRAMEWORK=falcon gunicorn --reload main +``` + +To run the app as Flask server +``` +FRAMEWORK=flask gunicorn --reload main +``` \ No newline at end of file diff --git a/application/application.py b/application/application.py new file mode 100644 index 0000000..bb00074 --- /dev/null +++ b/application/application.py @@ -0,0 +1 @@ +APPLICATION_NAME = 'foo:app' \ No newline at end of file diff --git a/domain/domain.py b/domain/domain.py new file mode 100644 index 0000000..7cb5c87 --- /dev/null +++ b/domain/domain.py @@ -0,0 +1 @@ +DOMAIN_NAME = 'foo:domanin' \ No newline at end of file diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py new file mode 100644 index 0000000..21ff1c6 --- /dev/null +++ b/infrastructure/framework/falcon/app.py @@ -0,0 +1,16 @@ +import falcon +import json +from application.application import APPLICATION_NAME + +class Info(object): + + def on_get(self, req, resp): + doc = { + 'framework': 'Falcon {}'.format(falcon.__version__), + 'application': APPLICATION_NAME + } + resp.body = json.dumps(doc, ensure_ascii=False) + resp.status = falcon.HTTP_200 + +app = falcon.API() +app.add_route('/info', Info()) diff --git a/infrastructure/framework/flask/app.py b/infrastructure/framework/flask/app.py new file mode 100644 index 0000000..615eb7c --- /dev/null +++ b/infrastructure/framework/flask/app.py @@ -0,0 +1,12 @@ +import flask +from flask import Flask, jsonify +app = Flask(__name__) + +from application.application import APPLICATION_NAME + +@app.route('/info') +def hello_world(): + return jsonify({ + 'framework': 'Flask {}'.format(flask.__version__), + 'application': APPLICATION_NAME, + }) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2af0ffb --- /dev/null +++ b/main.py @@ -0,0 +1,17 @@ +import os + +# def create_app(config): +# print('creating app, config:', config) +# from infrastructure.framework.flask.app import app +# return app + +framework = os.environ['FRAMEWORK'] +print('Running {} app'.format(framework)) + +application = None # required by gunicorn? + +if framework == 'falcon': + from infrastructure.framework.falcon.app import app as application +elif framework == 'flask': + from infrastructure.framework.flask.app import app as application + From 7557891fd5f6aa03155bd297c74c3664671b538b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 16 Nov 2018 14:13:31 +0100 Subject: [PATCH 03/44] add first domain enitty and value object --- Pipfile | 2 + Pipfile.lock | 101 ++++++++++++++++++++++++- README.md | 91 +++++++++++++++++++++- domain/__init__.py | 0 domain/domain.py | 1 - domain/entities.py | 7 ++ domain/test_value_objects.py | 5 ++ domain/value_objects.py | 7 ++ infrastructure/framework/falcon/app.py | 4 +- main.py | 9 +-- 10 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 domain/__init__.py delete mode 100644 domain/domain.py create mode 100644 domain/entities.py create mode 100644 domain/test_value_objects.py create mode 100644 domain/value_objects.py diff --git a/Pipfile b/Pipfile index 1dc213c..2819006 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,8 @@ flask = "*" falcon = "*" gunicorn = "*" pylint = "*" +pytest = "*" +pytest-watch = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 369e4be..e1e7c2f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6a59a2cf7ba158af8d9bba5bb4acdaac64b84b45773f7236027cb82dd7bd885d" + "sha256": "cdf95bfc98c1fcbff2bbd94fa3b74b339e5d156e36f3ba2bcc485a431c8c451c" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "argh": { + "hashes": [ + "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", + "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" + ], + "version": "==0.26.2" + }, "astroid": { "hashes": [ "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", @@ -23,6 +30,20 @@ ], "version": "==2.0.4" }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -30,6 +51,19 @@ ], "version": "==7.0" }, + "colorama": { + "hashes": [ + "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3", + "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c" + ], + "version": "==0.4.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, "falcon": { "hashes": [ "sha256:0a66b33458fab9c1e400a9be1a68056abda178eb02a8cb4b8f795e9df20b053b", @@ -150,6 +184,34 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pathtools": { + "hashes": [ + "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0" + ], + "version": "==0.1.2" + }, + "pluggy": { + "hashes": [ + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + ], + "version": "==0.8.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, "pylint": { "hashes": [ "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", @@ -158,6 +220,21 @@ "index": "pypi", "version": "==2.1.1" }, + "pytest": { + "hashes": [ + "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb", + "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7" + ], + "index": "pypi", + "version": "==4.0.0" + }, + "pytest-watch": { + "hashes": [ + "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" + ], + "index": "pypi", + "version": "==4.2.0" + }, "python-mimeparse": { "hashes": [ "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", @@ -165,6 +242,22 @@ ], "version": "==1.6.0" }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, "six": { "hashes": [ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", @@ -172,6 +265,12 @@ ], "version": "==1.11.0" }, + "watchdog": { + "hashes": [ + "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" + ], + "version": "==0.9.0" + }, "werkzeug": { "hashes": [ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", diff --git a/README.md b/README.md index c5e507c..539aeb0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,44 @@ +AUCTION APPLICATION + +The goal is to implement an automatic bidding system, described here: https://www.ebay.co.uk/pages/help/buy/bidding-overview.html + +User stories: + +* As a seller I can list a new item for sale. The item has the following fields: text, description, starting price + +* As a seller, I'm allowed to list up to 3 items at the same time + +* As a user I can view all the items for sale. For each item I will see: text, description, current price, minimum bidding price, a winner, all participants, action end date + +* As a bidder, when placing a bid, I enter the maximum amount I am willing to pay for the item. The seller and other bidders don't know my maximum bid + +* As a bidder, when placing a bid, my bid must be higher than the actual price + +* Auction Store will automatically calculate the current price of an item based on the bids that were made + +* When auction ends, auction store will notify the seller by email. The email will contain the name of the winner and the sell price. + +* When auction ends, all losing participants will recieve an email with the information that they lost an auction. + +* When auction ends, the winning participant will reciewve an email with information the user has won and the price for an item. + + + + +``` pipenv install pipenv shell +``` -code . +To run tests +``` +pytest +``` +To run tests in watch mode +``` +ptw +``` To run the app as Falcon server ``` @@ -12,4 +48,55 @@ FRAMEWORK=falcon gunicorn --reload main To run the app as Flask server ``` FRAMEWORK=flask gunicorn --reload main -``` \ No newline at end of file +``` + + +Project structure: + +``` +context-1 + domain + entities + value_objects + aggregates + services + repositories + factories? + interfaces + events + application + services + infrastructure + ?? + tests?? + +context-2 + +context-3 + +di_setup +main - main entrypoint to the app +``` + +Domain artifacts + +* entities - mutable, identifiable, unaware of persistance + +* value objects - immutable, self-contained + +* aggregates - any transaction should modify only one aggegate at a time, 70-80% usually contain olny one entity, consistency boundary, hosts methods which will modify the aggregate + +* events - significant state transition, something which domain experts care about + +* factories - for entity construction, ubiquotous language verbs, hide construction details (Python function) + +* repositories - store aggregates, abstraction over persistence mehanism + +* context maps - mappings between concepts between bounded contexts + +TODO: + * event bus? for domain + +References: + +* https://skillsmatter.com/skillscasts/5025-domain-driven-design-with-python(python-ddd) \ No newline at end of file diff --git a/domain/__init__.py b/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/domain/domain.py b/domain/domain.py deleted file mode 100644 index 7cb5c87..0000000 --- a/domain/domain.py +++ /dev/null @@ -1 +0,0 @@ -DOMAIN_NAME = 'foo:domanin' \ No newline at end of file diff --git a/domain/entities.py b/domain/entities.py new file mode 100644 index 0000000..27cea23 --- /dev/null +++ b/domain/entities.py @@ -0,0 +1,7 @@ +from domain.value_objects import Currency + +class AuctionItem: + def __init__(self, name : str, description : str, starting_price : Currency, start_date, end_date): + self.name = name + self.description = description + self.current_price = starting_price \ No newline at end of file diff --git a/domain/test_value_objects.py b/domain/test_value_objects.py new file mode 100644 index 0000000..e2a0f68 --- /dev/null +++ b/domain/test_value_objects.py @@ -0,0 +1,5 @@ +from domain.value_objects import Currency + +def test_currency_will_round_the_value(): + c = Currency(1.1234) + assert c.amount == 1.12 diff --git a/domain/value_objects.py b/domain/value_objects.py new file mode 100644 index 0000000..6fa52df --- /dev/null +++ b/domain/value_objects.py @@ -0,0 +1,7 @@ +class Currency: + def __init__(self, amount: float): + self._amount = round(amount,2) + + @property + def amount(self): + return self._amount \ No newline at end of file diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py index 21ff1c6..e55b1d5 100644 --- a/infrastructure/framework/falcon/app.py +++ b/infrastructure/framework/falcon/app.py @@ -3,7 +3,6 @@ from application.application import APPLICATION_NAME class Info(object): - def on_get(self, req, resp): doc = { 'framework': 'Falcon {}'.format(falcon.__version__), @@ -14,3 +13,6 @@ def on_get(self, req, resp): app = falcon.API() app.add_route('/info', Info()) + + +from domain.entities import AuctionItem diff --git a/main.py b/main.py index 2af0ffb..dd77ba8 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,9 @@ import os -# def create_app(config): -# print('creating app, config:', config) -# from infrastructure.framework.flask.app import app -# return app - -framework = os.environ['FRAMEWORK'] +framework = os.environ.get('FRAMEWORK', 'falcon') print('Running {} app'.format(framework)) -application = None # required by gunicorn? +application = None # required by gunicorn if framework == 'falcon': from infrastructure.framework.falcon.app import app as application From 60f8114043204343570f0dbde245b4d5a10aba2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 16 Nov 2018 14:25:01 +0100 Subject: [PATCH 04/44] currency operators --- domain/test_value_objects.py | 20 ++++++++++++++++++++ domain/value_objects.py | 17 ++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/domain/test_value_objects.py b/domain/test_value_objects.py index e2a0f68..e5bdffb 100644 --- a/domain/test_value_objects.py +++ b/domain/test_value_objects.py @@ -3,3 +3,23 @@ def test_currency_will_round_the_value(): c = Currency(1.1234) assert c.amount == 1.12 + +def test_can_add_2_currencies(): + amount1 = Currency(10) + amount2 = Currency(1) + sum = amount1 + amount2 + assert sum.amount == 11 + +def test_can_subtact_2_currencies(): + amount1 = Currency(10) + amount2 = Currency(1) + diff = amount1 - amount2 + assert diff.amount == 9 + +def test_can_compare_currencies(): + assert Currency(1) == Currency(1) + assert Currency(10) > Currency(1) + assert Currency(1) < Currency(10) + +def test_currenct_repr(): + assert str(Currency(1)) == '1.00' diff --git a/domain/value_objects.py b/domain/value_objects.py index 6fa52df..269c082 100644 --- a/domain/value_objects.py +++ b/domain/value_objects.py @@ -4,4 +4,19 @@ def __init__(self, amount: float): @property def amount(self): - return self._amount \ No newline at end of file + return self._amount + + def __add__(self, other): + return Currency(self.amount + other.amount) + + def __sub__(self, other): + return Currency(self.amount - other.amount) + + def __eq__(self, other): + return self.amount == other.amount + + def __lt__(self, other): + return self.amount < other.amount + + def __repr__(self): + return '{:.2f}'.format(self.amount) \ No newline at end of file From 033f10086c02f16dfe213f104a2af25315ecf8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 16 Nov 2018 15:59:56 +0100 Subject: [PATCH 05/44] command bus and first command --- Pipfile | 1 + Pipfile.lock | 10 +++++++- application/__init__.py | 0 application/command_bus.py | 5 ++++ application/command_handlers.py | 2 ++ application/commands.py | 27 +++++++++++++++++++++ application/exceptions.py | 0 application/guard.py | 2 ++ application/{application.py => settings.py} | 0 application/test_commands.py | 13 ++++++++++ infrastructure/framework/falcon/app.py | 25 ++++++++++++++++--- pytest.ini | 3 +++ 12 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 application/__init__.py create mode 100644 application/command_bus.py create mode 100644 application/command_handlers.py create mode 100644 application/commands.py create mode 100644 application/exceptions.py create mode 100644 application/guard.py rename application/{application.py => settings.py} (100%) create mode 100644 application/test_commands.py create mode 100644 pytest.ini diff --git a/Pipfile b/Pipfile index 2819006..abe6e39 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ gunicorn = "*" pylint = "*" pytest = "*" pytest-watch = "*" +schematics = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e1e7c2f..2f219ed 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "cdf95bfc98c1fcbff2bbd94fa3b74b339e5d156e36f3ba2bcc485a431c8c451c" + "sha256": "6442b200a074dcb79893838bdbf4979de19ba5699ea9c1cb863b837ea038b48e" }, "pipfile-spec": 6, "requires": { @@ -258,6 +258,14 @@ ], "version": "==3.13" }, + "schematics": { + "hashes": [ + "sha256:8fcc6182606fd0b24410a1dbb066d9bbddbe8da9c9509f47b743495706239283", + "sha256:a40b20635c0e43d18d3aff76220f6cd95ea4decb3f37765e49529b17d81b0439" + ], + "index": "pypi", + "version": "==2.1.0" + }, "six": { "hashes": [ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", diff --git a/application/__init__.py b/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/application/command_bus.py b/application/command_bus.py new file mode 100644 index 0000000..5c4f288 --- /dev/null +++ b/application/command_bus.py @@ -0,0 +1,5 @@ +from application.commands import Command + +class CommandBus(object): + def execute(self, command : Command): + pass \ No newline at end of file diff --git a/application/command_handlers.py b/application/command_handlers.py new file mode 100644 index 0000000..8c808a6 --- /dev/null +++ b/application/command_handlers.py @@ -0,0 +1,2 @@ +def add_item_handler(command): + pass \ No newline at end of file diff --git a/application/commands.py b/application/commands.py new file mode 100644 index 0000000..3a8f47e --- /dev/null +++ b/application/commands.py @@ -0,0 +1,27 @@ +from schematics.models import Model +from schematics.types import StringType +from schematics.exceptions import ValidationError, DataError + +class Command(Model): + def is_valid(self): + try: + self.validate() + except DataError: + return False + return True + + # def get_validation_errors(self): + # try: + # self.validate() + # except DataError as e: + # print('zzz', e.errors) + # return {} + # return {} + + def __repr__(self): + return '<{}>({})'.format(type(self).__name__, self.__dict__['_data']) + +class AddItemCommand(Command): + title = StringType(required=True) + description = StringType() + diff --git a/application/exceptions.py b/application/exceptions.py new file mode 100644 index 0000000..e69de29 diff --git a/application/guard.py b/application/guard.py new file mode 100644 index 0000000..b684806 --- /dev/null +++ b/application/guard.py @@ -0,0 +1,2 @@ +# def goard_not_empty(value): +# pass diff --git a/application/application.py b/application/settings.py similarity index 100% rename from application/application.py rename to application/settings.py diff --git a/application/test_commands.py b/application/test_commands.py new file mode 100644 index 0000000..faef81b --- /dev/null +++ b/application/test_commands.py @@ -0,0 +1,13 @@ +from application.commands import AddItemCommand + +def test_valid_add_item_command(): + command = AddItemCommand({ 'title': 'Fluffy dragon' }) + assert command.is_valid() == True + +def test_add_item_command_title_is_required(): + command = AddItemCommand({ 'description': 'Fluffy dragon' }) + assert command.is_valid() == False + +# def test_add_item_command_will_return_errors(): +# command = AddItemCommand({ 'description': 'Fluffy dragon' }) +# assert command.get_validation_errors() == False diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py index e55b1d5..5011502 100644 --- a/infrastructure/framework/falcon/app.py +++ b/infrastructure/framework/falcon/app.py @@ -1,18 +1,35 @@ import falcon import json -from application.application import APPLICATION_NAME +from application.commands import AddItemCommand +from application.settings import APPLICATION_NAME + +# TODO: command_bus should be injected by DI +from application.command_bus import CommandBus +command_bus = CommandBus() class Info(object): - def on_get(self, req, resp): + def on_get(self, req, res): doc = { 'framework': 'Falcon {}'.format(falcon.__version__), 'application': APPLICATION_NAME } - resp.body = json.dumps(doc, ensure_ascii=False) - resp.status = falcon.HTTP_200 + res.body = json.dumps(doc, ensure_ascii=False) + res.status = falcon.HTTP_200 + +class ItemsController(object): + def on_get(self, req, res): + command = AddItemCommand(req.params, strict=False) + command.validate() + result = command_bus.execute(command) + res.body = json.dumps(result, ensure_ascii=False) + res.status = falcon.HTTP_200 + + def on_post(self, req, res): + pass app = falcon.API() app.add_route('/info', Info()) +app.add_route('/items', ItemsController()) from domain.entities import AuctionItem diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b0e5a94 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore::DeprecationWarning \ No newline at end of file From a987b25bc626220b46427e1b4f5a5552406f8a6d Mon Sep 17 00:00:00 2001 From: pgorecki Date: Sat, 17 Nov 2018 14:34:52 +0100 Subject: [PATCH 06/44] WIP: command bus, no dependency injection yet --- README.md | 20 ++++++++++++++ application/command_bus.py | 31 ++++++++++++++++++--- application/commands.py | 33 +++++++++++++++++------ application/test_command_bus.py | 48 +++++++++++++++++++++++++++++++++ application/test_commands.py | 25 ++++++++++++----- 5 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 application/test_command_bus.py diff --git a/README.md b/README.md index 539aeb0..1cbc642 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,24 @@ AUCTION APPLICATION The goal is to implement an automatic bidding system, described here: https://www.ebay.co.uk/pages/help/buy/bidding-overview.html + +TODO for near future: + +* commands, command bus and handlers + +* executing commands with immediate feedback + http://blog.sapiensworks.com/post/2015/07/20/CQRS-Immediate-Feedback-Web-App + +* handling commands errors: application layer, business layer + +* start using dependency injection + +* command validation + https://stackoverflow.com/questions/32239353/command-validation-in-ddd-with-cqrs + +* handling async commands + + User stories: * As a seller I can list a new item for sale. The item has the following fields: text, description, starting price @@ -99,4 +117,6 @@ TODO: References: +* Command design pattern: https://www.youtube.com/watch?v=9qA5kw8dcSU + * https://skillsmatter.com/skillscasts/5025-domain-driven-design-with-python(python-ddd) \ No newline at end of file diff --git a/application/command_bus.py b/application/command_bus.py index 5c4f288..21b19c2 100644 --- a/application/command_bus.py +++ b/application/command_bus.py @@ -1,5 +1,30 @@ -from application.commands import Command +from application.commands import Command, CommandResult + + +def command_handler_locator(command: Command): + import importlib + module_name = type(command).__module__ + command_class_name = type(command).__name__ + handler_class_name = '{}Handler'.format(command_class_name) + importlib.invalidate_caches() + handler_module = importlib.import_module(module_name) + handler_class = getattr(handler_module, handler_class_name) + return handler_class + class CommandBus(object): - def execute(self, command : Command): - pass \ No newline at end of file + """ + Command bus is a central place for executing commands. + It offers some benefits over executing commands directly from the controller: + - in-memory bus can be replaced with persistent one, so that multiple applications can share same bus + - it can be used by different clients: web controller, console application, etc. + - we can provide rate limiting and protection against DOS attacks + - we can reject duplicated commands + """ + def __init__(self, command_handler_locator = None): + # TODO: inected + self.command_handler_locator = command_handler_locator + + def execute(self, command : Command) -> CommandResult: + handler = command_handler_locator(command)() + return handler.handle(command) \ No newline at end of file diff --git a/application/commands.py b/application/commands.py index 3a8f47e..5d5edd7 100644 --- a/application/commands.py +++ b/application/commands.py @@ -1,8 +1,24 @@ +from enum import Enum from schematics.models import Model from schematics.types import StringType from schematics.exceptions import ValidationError, DataError +class ResultStatus(Enum): + OK = 'ok' + ERROR = 'error' + +class CommandResult(object): + def __init__(self, status: ResultStatus, **kwargs): + self._kwargs = kwargs + self.status = status + + def __repr__(self): + return '<{}>({}) {}'.format(type(self).__name__, self.status, self._kwargs) + class Command(Model): + """ + Command is an immutable data structure holding object + """ def is_valid(self): try: self.validate() @@ -10,18 +26,19 @@ def is_valid(self): return False return True - # def get_validation_errors(self): - # try: - # self.validate() - # except DataError as e: - # print('zzz', e.errors) - # return {} - # return {} - def __repr__(self): return '<{}>({})'.format(type(self).__name__, self.__dict__['_data']) + + class AddItemCommand(Command): title = StringType(required=True) description = StringType() +class AddItemCommandHandler(): + def __init__(self): + # TODO: inject dependencies here + pass + + def handle(self, command): + pass \ No newline at end of file diff --git a/application/test_command_bus.py b/application/test_command_bus.py new file mode 100644 index 0000000..8c45e47 --- /dev/null +++ b/application/test_command_bus.py @@ -0,0 +1,48 @@ +from application.commands import Command, CommandResult, ResultStatus +from application.command_bus import CommandBus + + +class Light(object): + def __init__(self, is_turned_on = False): + self.is_turned_on = is_turned_on + + def turn_on(self): + self.is_turned_on = True + + def turn_off(self): + self.is_turned_on = False + + +class LightOnCommand(Command): + pass + + +class LightOffCommand(Command): + pass + + +class LightOnCommandHandler(object): + def __init__(self, light): + self.light = light + + def handle(self, command: LightOnCommand): + self.light.turn_on() + return CommandResult(ResultStatus.OK) + + +class LightOffCommandHandler(object): + def __init__(self, light): + self.light = light + + def handle(self, command: LightOnCommand): + self.light.turn_off() + return CommandResult(ResultStatus.OK) + + +def test_commnad_bus_will_dispatch_command(): + bus = CommandBus() + light = Light(is_turned_on=False) + command = LightOnCommand() + result = bus.execute(command) + assert result.status == ResultStatus.OK + assert light.is_turned_on == True \ No newline at end of file diff --git a/application/test_commands.py b/application/test_commands.py index faef81b..848b27b 100644 --- a/application/test_commands.py +++ b/application/test_commands.py @@ -1,12 +1,23 @@ -from application.commands import AddItemCommand -def test_valid_add_item_command(): - command = AddItemCommand({ 'title': 'Fluffy dragon' }) - assert command.is_valid() == True -def test_add_item_command_title_is_required(): - command = AddItemCommand({ 'description': 'Fluffy dragon' }) - assert command.is_valid() == False +# def test_will_handle_ping_command(): +# bus = CommandBus() +# light = Light() +# command = LightOnCommand(light) +# result = bus.execute(command) +# assert result.status == CommandResultStatus.OK + + + +# from application.commands import AddItemCommand + +# def test_valid_add_item_command(): +# command = AddItemCommand({ 'title': 'Fluffy dragon' }) +# assert command.is_valid() == True + +# def test_add_item_command_title_is_required(): +# command = AddItemCommand({ 'description': 'Fluffy dragon' }) +# assert command.is_valid() == False # def test_add_item_command_will_return_errors(): # command = AddItemCommand({ 'description': 'Fluffy dragon' }) From 87b897214083c0d25d8418a09794990fe93d5de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Sun, 25 Nov 2018 23:21:58 +0100 Subject: [PATCH 07/44] WIP dependency injection --- Pipfile | 1 + Pipfile.lock | 53 ++++--------------- application/command_bus.py | 29 +++++----- application/command_handlers.py | 6 ++- application/test_command_bus.py | 26 ++++++--- composition_root.py | 35 ++++++++++++ infrastructure/__init__.py | 0 infrastructure/framework/falcon/__init__.py | 0 infrastructure/framework/falcon/app.py | 36 +++---------- .../framework/falcon/controllers.py | 29 ++++++++++ 10 files changed, 121 insertions(+), 94 deletions(-) create mode 100644 composition_root.py create mode 100644 infrastructure/__init__.py create mode 100644 infrastructure/framework/falcon/__init__.py create mode 100644 infrastructure/framework/falcon/controllers.py diff --git a/Pipfile b/Pipfile index abe6e39..cd95617 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ pylint = "*" pytest = "*" pytest-watch = "*" schematics = "*" +dependency-injector = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 2f219ed..477e8fd 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6442b200a074dcb79893838bdbf4979de19ba5699ea9c1cb863b837ea038b48e" + "sha256": "59be8dcea11bb541131a25e15f4a292f6dca1edf3aa3a5733e2a9722befda276" }, "pipfile-spec": 6, "requires": { @@ -18,8 +18,7 @@ "default": { "argh": { "hashes": [ - "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", - "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" + "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3" ], "version": "==0.26.2" }, @@ -58,6 +57,13 @@ ], "version": "==0.4.0" }, + "dependency-injector": { + "hashes": [ + "sha256:f478a26e9bf3111ce98bbfb8502af274643947f87a7e12a6481a35eaa693062b" + ], + "index": "pypi", + "version": "==3.14.2" + }, "docopt": { "hashes": [ "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" @@ -146,34 +152,7 @@ }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672" ], "version": "==1.1.0" }, @@ -244,17 +223,7 @@ }, "pyyaml": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f" ], "version": "==3.13" }, diff --git a/application/command_bus.py b/application/command_bus.py index 21b19c2..df58cbc 100644 --- a/application/command_bus.py +++ b/application/command_bus.py @@ -1,15 +1,21 @@ from application.commands import Command, CommandResult -def command_handler_locator(command: Command): - import importlib - module_name = type(command).__module__ - command_class_name = type(command).__name__ - handler_class_name = '{}Handler'.format(command_class_name) - importlib.invalidate_caches() - handler_module = importlib.import_module(module_name) - handler_class = getattr(handler_module, handler_class_name) - return handler_class +def default_command_handler_locator(command, **kwargs): + print('finding handler for command', command, kwargs) + raise NotImplementedError('handler lookup') + + # import importlib + # def _default_command_handler_locator(command: Command): + # module_name = type(command).__module__ + # command_class_name = type(command).__name__ + # handler_class_name = '{}Handler'.format(command_class_name) + # print('locating handler for', command_class_name) + # importlib.invalidate_caches() + # handler_module = importlib.import_module(module_name) + # handler_class = getattr(handler_module, handler_class_name) + # return handler_class + # return _default_command_handler_locator class CommandBus(object): @@ -21,10 +27,9 @@ class CommandBus(object): - we can provide rate limiting and protection against DOS attacks - we can reject duplicated commands """ - def __init__(self, command_handler_locator = None): - # TODO: inected + def __init__(self, command_handler_locator, foo=None): self.command_handler_locator = command_handler_locator def execute(self, command : Command) -> CommandResult: - handler = command_handler_locator(command)() + handler = self.command_handler_locator(command)() return handler.handle(command) \ No newline at end of file diff --git a/application/command_handlers.py b/application/command_handlers.py index 8c808a6..6c48f06 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -1,2 +1,4 @@ -def add_item_handler(command): - pass \ No newline at end of file +def add_item_handler(): + def handle(command): + print('handling command', command) + return handle \ No newline at end of file diff --git a/application/test_command_bus.py b/application/test_command_bus.py index 8c45e47..ddc78ea 100644 --- a/application/test_command_bus.py +++ b/application/test_command_bus.py @@ -1,6 +1,6 @@ from application.commands import Command, CommandResult, ResultStatus from application.command_bus import CommandBus - +from composition_root import CommandBusContainer class Light(object): def __init__(self, is_turned_on = False): @@ -39,10 +39,20 @@ def handle(self, command: LightOnCommand): return CommandResult(ResultStatus.OK) -def test_commnad_bus_will_dispatch_command(): - bus = CommandBus() - light = Light(is_turned_on=False) - command = LightOnCommand() - result = bus.execute(command) - assert result.status == ResultStatus.OK - assert light.is_turned_on == True \ No newline at end of file +# def test_foo(): +# print('aa') +# foo = CommandBusContainer.command_handler_locator +# print('bb') +# print('ispecting foo', foo) +# result = foo(123) +# print('cc') +# assert result == True + + +# def test_commnad_bus_will_dispatch_command(): +# bus = CommandBusContainer.command_bus() +# light = Light(is_turned_on=False) +# command = LightOnCommand() +# result = bus.execute(command) +# assert result.status == ResultStatus.OK +# assert light.is_turned_on == True \ No newline at end of file diff --git a/composition_root.py b/composition_root.py new file mode 100644 index 0000000..6a3c541 --- /dev/null +++ b/composition_root.py @@ -0,0 +1,35 @@ +import dependency_injector.containers as containers +import dependency_injector.providers as providers + +from application.command_bus import CommandBus, default_command_handler_locator +from infrastructure.framework.falcon.controllers import InfoController, ItemsController + +class ObjectiveCommandHandler(): + def __init__(self, logger): + self.logger = logger + + def handle(self, command): + print('objective handler is handling', command, self.logger) + +def functional_handler(logger): + def handle(command): + print('functional handler is handling', command, logger) + return handle + +class CommandBusContainer(containers.DeclarativeContainer): + command_handler_locator=providers.Factory(default_command_handler_locator, + objectiveCommandHandler=providers.Factory(ObjectiveCommandHandler, logger=None), + functionalHandler=providers.Factory(functional_handler, logger=None), + # addItemCommandHandler=providers.Factory(add_item_handler), + ) + commandBus = providers.Factory( + CommandBus, + command_handler_locator=command_handler_locator + ) + +class FalconContainer(containers.DeclarativeContainer): + itemsController = providers.Factory(ItemsController, + command_bus=CommandBusContainer.commandBus + ) + infoController = providers.Factory(InfoController) + \ No newline at end of file diff --git a/infrastructure/__init__.py b/infrastructure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/framework/falcon/__init__.py b/infrastructure/framework/falcon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py index 5011502..1a12cd5 100644 --- a/infrastructure/framework/falcon/app.py +++ b/infrastructure/framework/falcon/app.py @@ -1,35 +1,11 @@ import falcon -import json -from application.commands import AddItemCommand -from application.settings import APPLICATION_NAME - -# TODO: command_bus should be injected by DI -from application.command_bus import CommandBus -command_bus = CommandBus() - -class Info(object): - def on_get(self, req, res): - doc = { - 'framework': 'Falcon {}'.format(falcon.__version__), - 'application': APPLICATION_NAME - } - res.body = json.dumps(doc, ensure_ascii=False) - res.status = falcon.HTTP_200 - -class ItemsController(object): - def on_get(self, req, res): - command = AddItemCommand(req.params, strict=False) - command.validate() - result = command_bus.execute(command) - res.body = json.dumps(result, ensure_ascii=False) - res.status = falcon.HTTP_200 - - def on_post(self, req, res): - pass +from composition_root import FalconContainer +from infrastructure.framework.falcon.controllers import InfoController app = falcon.API() -app.add_route('/info', Info()) -app.add_route('/items', ItemsController()) +app.add_route('/', FalconContainer.infoController()) +app.add_route('/items', FalconContainer.itemsController()) -from domain.entities import AuctionItem +foo = FalconContainer.foo +print(foo()) diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py new file mode 100644 index 0000000..3947a94 --- /dev/null +++ b/infrastructure/framework/falcon/controllers.py @@ -0,0 +1,29 @@ +import falcon +import json +from application.commands import AddItemCommand +from application.settings import APPLICATION_NAME + + +class InfoController(object): + def on_get(self, req, res): + doc = { + 'framework': 'Falcon {}'.format(falcon.__version__), + 'application': APPLICATION_NAME + } + res.body = json.dumps(doc, ensure_ascii=False) + res.status = falcon.HTTP_200 + + +class ItemsController(object): + def __init__(self, command_bus): + self.command_bus = command_bus + + def on_get(self, req, res): + command = AddItemCommand(req.params, strict=False) + command.validate() + result = self.command_bus.execute(command) + res.body = json.dumps(result, ensure_ascii=False) + res.status = falcon.HTTP_200 + + def on_post(self, req, res): + pass From a0bdc463120391c42fe5e8ec2ae4b1f973142841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Mon, 26 Nov 2018 08:20:10 +0100 Subject: [PATCH 08/44] WIP command handler injection --- .gitignore | 2 ++ application/command_bus.py | 12 ++++++++---- application/test_command_bus.py | 31 ++++++++++++++++--------------- composition_root.py | 19 ++++++++++--------- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index 894a44c..14ef862 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/application/command_bus.py b/application/command_bus.py index df58cbc..4097b48 100644 --- a/application/command_bus.py +++ b/application/command_bus.py @@ -27,9 +27,13 @@ class CommandBus(object): - we can provide rate limiting and protection against DOS attacks - we can reject duplicated commands """ - def __init__(self, command_handler_locator, foo=None): - self.command_handler_locator = command_handler_locator + def __init__(self, command_handler_factory): + self._command_handler_factory = command_handler_factory - def execute(self, command : Command) -> CommandResult: - handler = self.command_handler_locator(command)() + def get_handler_for_command(self, command: Command): + command_class_name = type(command).__name__ + return self._command_handler_factory(command_class_name) + + def execute(self, command: Command) -> CommandResult: + handler = self.get_handler_for_command(command) return handler.handle(command) \ No newline at end of file diff --git a/application/test_command_bus.py b/application/test_command_bus.py index ddc78ea..3ee53e6 100644 --- a/application/test_command_bus.py +++ b/application/test_command_bus.py @@ -1,3 +1,6 @@ +import dependency_injector.containers as containers +import dependency_injector.providers as providers + from application.commands import Command, CommandResult, ResultStatus from application.command_bus import CommandBus from composition_root import CommandBusContainer @@ -39,20 +42,18 @@ def handle(self, command: LightOnCommand): return CommandResult(ResultStatus.OK) -# def test_foo(): -# print('aa') -# foo = CommandBusContainer.command_handler_locator -# print('bb') -# print('ispecting foo', foo) -# result = foo(123) -# print('cc') -# assert result == True +class OverriddenCommandBusContainer(CommandBusContainer): + light_factory = providers.Singleton(Light) + command_handler_factory = providers.FactoryAggregate( + LightOnCommand=providers.Factory(LightOnCommandHandler, light=light_factory), + LightOffCommand=providers.Factory(LightOffCommandHandler, light=light_factory) + ) -# def test_commnad_bus_will_dispatch_command(): -# bus = CommandBusContainer.command_bus() -# light = Light(is_turned_on=False) -# command = LightOnCommand() -# result = bus.execute(command) -# assert result.status == ResultStatus.OK -# assert light.is_turned_on == True \ No newline at end of file +def test_commnad_bus_will_dispatch_command(): + bus = OverriddenCommandBusContainer.command_bus_factory() + command = LightOnCommand() + result = bus.execute(command) + # assert result.status == ResultStatus.OK + # assert light.is_turned_on == True + pass \ No newline at end of file diff --git a/composition_root.py b/composition_root.py index 6a3c541..0e243f4 100644 --- a/composition_root.py +++ b/composition_root.py @@ -16,20 +16,21 @@ def handle(command): print('functional handler is handling', command, logger) return handle + +from application.command_handlers import add_item_handler class CommandBusContainer(containers.DeclarativeContainer): - command_handler_locator=providers.Factory(default_command_handler_locator, - objectiveCommandHandler=providers.Factory(ObjectiveCommandHandler, logger=None), - functionalHandler=providers.Factory(functional_handler, logger=None), - # addItemCommandHandler=providers.Factory(add_item_handler), + command_handler_factory = providers.FactoryAggregate( + AddItemCommand=providers.Factory(add_item_handler) ) - commandBus = providers.Factory( + + command_bus_factory = providers.Factory( CommandBus, - command_handler_locator=command_handler_locator + command_handler_factory=providers.DelegatedFactory(command_handler_factory) ) class FalconContainer(containers.DeclarativeContainer): - itemsController = providers.Factory(ItemsController, - command_bus=CommandBusContainer.commandBus + items_controller_factory = providers.Factory(ItemsController, + command_bus=CommandBusContainer.command_bus_factory ) - infoController = providers.Factory(InfoController) + info_controller_factory = providers.Factory(InfoController) \ No newline at end of file From c7e1a6ea9984c1001bbca918e1e18339765dfddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Mon, 26 Nov 2018 11:11:16 +0100 Subject: [PATCH 09/44] code refactoring --- README.md | 14 +++++++++++--- application/command_bus.py | 1 + application/command_handlers.py | 14 ++++++++++---- application/commands.py | 11 +---------- composition_root.py | 7 +++++-- infrastructure/framework/falcon/app.py | 8 ++------ infrastructure/framework/falcon/controllers.py | 1 + 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1cbc642..b0b6847 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,14 @@ TODO for near future: * commands, command bus and handlers +* command validation + +* application-level exceptions for invalid commands + +* mediator pattern + +* TESTS!!!! + * executing commands with immediate feedback http://blog.sapiensworks.com/post/2015/07/20/CQRS-Immediate-Feedback-Web-App @@ -19,6 +27,8 @@ TODO for near future: * handling async commands +* framework agnostic integration tests?? + User stories: @@ -26,7 +36,7 @@ User stories: * As a seller, I'm allowed to list up to 3 items at the same time -* As a user I can view all the items for sale. For each item I will see: text, description, current price, minimum bidding price, a winner, all participants, action end date +* As a user I can view all the items for sale. For each item I will see: text, description, current price, minimum bidding price, a winner, all participants, auction end date * As a bidder, when placing a bid, I enter the maximum amount I am willing to pay for the item. The seller and other bidders don't know my maximum bid @@ -112,8 +122,6 @@ Domain artifacts * context maps - mappings between concepts between bounded contexts -TODO: - * event bus? for domain References: diff --git a/application/command_bus.py b/application/command_bus.py index 4097b48..fe7b74a 100644 --- a/application/command_bus.py +++ b/application/command_bus.py @@ -35,5 +35,6 @@ def get_handler_for_command(self, command: Command): return self._command_handler_factory(command_class_name) def execute(self, command: Command) -> CommandResult: + # mediator pattern?? handler = self.get_handler_for_command(command) return handler.handle(command) \ No newline at end of file diff --git a/application/command_handlers.py b/application/command_handlers.py index 6c48f06..1fb814d 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -1,4 +1,10 @@ -def add_item_handler(): - def handle(command): - print('handling command', command) - return handle \ No newline at end of file +from application.commands import AddItemCommand, CommandResult, ResultStatus + +class AddItemCommandHandler(object): + def __init__(self, items_repository): + self._items_repository = items_repository + + def handle(self, command: AddItemCommand): + # TODO: add logic + return CommandResult(ResultStatus.OK) + diff --git a/application/commands.py b/application/commands.py index 5d5edd7..29703f8 100644 --- a/application/commands.py +++ b/application/commands.py @@ -30,15 +30,6 @@ def __repr__(self): return '<{}>({})'.format(type(self).__name__, self.__dict__['_data']) - class AddItemCommand(Command): title = StringType(required=True) - description = StringType() - -class AddItemCommandHandler(): - def __init__(self): - # TODO: inject dependencies here - pass - - def handle(self, command): - pass \ No newline at end of file + description = StringType() \ No newline at end of file diff --git a/composition_root.py b/composition_root.py index 0e243f4..7cddd37 100644 --- a/composition_root.py +++ b/composition_root.py @@ -17,10 +17,13 @@ def handle(command): return handle -from application.command_handlers import add_item_handler +from application.command_handlers import AddItemCommandHandler class CommandBusContainer(containers.DeclarativeContainer): + items_repository = None command_handler_factory = providers.FactoryAggregate( - AddItemCommand=providers.Factory(add_item_handler) + AddItemCommand=providers.Factory(AddItemCommandHandler, + items_repository=items_repository + ) ) command_bus_factory = providers.Factory( diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py index 1a12cd5..80fcb8b 100644 --- a/infrastructure/framework/falcon/app.py +++ b/infrastructure/framework/falcon/app.py @@ -3,9 +3,5 @@ from infrastructure.framework.falcon.controllers import InfoController app = falcon.API() -app.add_route('/', FalconContainer.infoController()) -app.add_route('/items', FalconContainer.itemsController()) - - -foo = FalconContainer.foo -print(foo()) +app.add_route('/', FalconContainer.info_controller_factory()) +app.add_route('/items', FalconContainer.items_controller_factory()) \ No newline at end of file diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 3947a94..4a45d08 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -21,6 +21,7 @@ def __init__(self, command_bus): def on_get(self, req, res): command = AddItemCommand(req.params, strict=False) command.validate() + # TODO: exception handling? validation? result = self.command_bus.execute(command) res.body = json.dumps(result, ensure_ascii=False) res.status = falcon.HTTP_200 From 771fe366b3790ee1a92729535918f2bf1fcb77f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 26 Nov 2018 13:25:06 +0100 Subject: [PATCH 10/44] Debug config in vscode --- Pipfile | 1 + Pipfile.lock | 91 +++++++++++++++++++++++++++++++++++++++------------- README.md | 3 +- main.py | 3 +- 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/Pipfile b/Pipfile index cd95617..d173afd 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ dependency-injector = "*" [dev-packages] pylint = "*" +ptvsd = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 477e8fd..3c7d151 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "59be8dcea11bb541131a25e15f4a292f6dca1edf3aa3a5733e2a9722befda276" + "sha256": "eda80b32195ea13be8ecbb98283e238dd9e935891e43965001d467d97a1c323a" }, "pipfile-spec": 6, "requires": { @@ -18,16 +18,17 @@ "default": { "argh": { "hashes": [ - "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3" + "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", + "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" ], "version": "==0.26.2" }, "astroid": { "hashes": [ - "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", - "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" ], - "version": "==2.0.4" + "version": "==2.1.0" }, "atomicwrites": { "hashes": [ @@ -52,10 +53,10 @@ }, "colorama": { "hashes": [ - "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3", - "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c" + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" ], - "version": "==0.4.0" + "version": "==0.4.1" }, "dependency-injector": { "hashes": [ @@ -152,7 +153,34 @@ }, "markupsafe": { "hashes": [ - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672" + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" ], "version": "==1.1.0" }, @@ -193,19 +221,19 @@ }, "pylint": { "hashes": [ - "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", - "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + "sha256:51f5a52bd31cb2db5b83ff37e3e902460eaa5591dea2739ba5d10d13ec5c5350", + "sha256:fe49f9ada5c8999344ac3a37541e329eaff11d014460065c4128fc94cf5cf140" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.2.0" }, "pytest": { "hashes": [ - "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb", - "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7" + "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", + "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.0.1" }, "pytest-watch": { "hashes": [ @@ -223,7 +251,17 @@ }, "pyyaml": { "hashes": [ - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f" + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" ], "version": "==3.13" }, @@ -265,10 +303,10 @@ "develop": { "astroid": { "hashes": [ - "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", - "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" ], - "version": "==2.0.4" + "version": "==2.1.0" }, "isort": { "hashes": [ @@ -319,13 +357,22 @@ ], "version": "==0.6.1" }, + "ptvsd": { + "hashes": [ + "sha256:533b3ca9a3973700d5fe6cb152cf6c69bac2839389460164c84ab1956ec992a0", + "sha256:8e6feb4d577b1a939af4b08821fd6afa6e71652d1e2ce41579d8b959b1e21d94", + "sha256:cfcde6a3de3cfa720e4f637af13deeae744f6dc6665b9bda92380885caf16ae6" + ], + "index": "pypi", + "version": "==4.2.0" + }, "pylint": { "hashes": [ - "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", - "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + "sha256:51f5a52bd31cb2db5b83ff37e3e902460eaa5591dea2739ba5d10d13ec5c5350", + "sha256:fe49f9ada5c8999344ac3a37541e329eaff11d014460065c4128fc94cf5cf140" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.2.0" }, "six": { "hashes": [ diff --git a/README.md b/README.md index b0b6847..848c9f5 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,8 @@ Domain artifacts * context maps - mappings between concepts between bounded contexts - +Installing Python 3.7 from source on Ubuntu 18.04: https://gist.github.com/jerblack/798718c1910ccdd4ede92481229043be + References: * Command design pattern: https://www.youtube.com/watch?v=9qA5kw8dcSU diff --git a/main.py b/main.py index dd77ba8..9175280 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ import os +import ptvsd +ptvsd.enable_attach(address=('0.0.0.0', 3000)) framework = os.environ.get('FRAMEWORK', 'falcon') print('Running {} app'.format(framework)) @@ -9,4 +11,3 @@ from infrastructure.framework.falcon.app import app as application elif framework == 'flask': from infrastructure.framework.flask.app import app as application - From 22c857696e828b57339422ee99c2c38f7d1d5c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 26 Nov 2018 13:43:58 +0100 Subject: [PATCH 11/44] JSON serialization of ResultStatus and CommandResult objects --- application/commands.py | 6 +++++- infrastructure/framework/falcon/controllers.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/application/commands.py b/application/commands.py index 29703f8..e8948e8 100644 --- a/application/commands.py +++ b/application/commands.py @@ -1,9 +1,10 @@ +import json from enum import Enum from schematics.models import Model from schematics.types import StringType from schematics.exceptions import ValidationError, DataError -class ResultStatus(Enum): +class ResultStatus(str, Enum): OK = 'ok' ERROR = 'error' @@ -15,6 +16,9 @@ def __init__(self, status: ResultStatus, **kwargs): def __repr__(self): return '<{}>({}) {}'.format(type(self).__name__, self.status, self._kwargs) + def toJSON(self): + return json.dumps(self, default=lambda o:o.__dict__, sort_keys=True, indent=4) + class Command(Model): """ Command is an immutable data structure holding object diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 4a45d08..83c1aad 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -23,7 +23,7 @@ def on_get(self, req, res): command.validate() # TODO: exception handling? validation? result = self.command_bus.execute(command) - res.body = json.dumps(result, ensure_ascii=False) + res.body = result.toJSON() res.status = falcon.HTTP_200 def on_post(self, req, res): From 71e5163a2f0611f61ea0896b9d62b1749bd0d033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Tue, 27 Nov 2018 08:08:09 +0100 Subject: [PATCH 12/44] Added autopep8, check command validity --- Pipfile | 2 + Pipfile.lock | 161 +++++++++++++++++- application/commands.py | 8 +- .../framework/falcon/controllers.py | 14 +- 4 files changed, 176 insertions(+), 9 deletions(-) diff --git a/Pipfile b/Pipfile index d173afd..73d87aa 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,8 @@ dependency-injector = "*" [dev-packages] pylint = "*" ptvsd = "*" +pre-commit = "*" +autopep8 = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 3c7d151..f78b15a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eda80b32195ea13be8ecbb98283e238dd9e935891e43965001d467d97a1c323a" + "sha256": "d680b6591b68810e468ec54bcf5445ee73ded4b98effe7b2708578a74d96eff0" }, "pipfile-spec": 6, "requires": { @@ -280,6 +280,35 @@ ], "version": "==1.11.0" }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.1.0" + }, "watchdog": { "hashes": [ "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" @@ -301,6 +330,13 @@ } }, "develop": { + "aspy.yaml": { + "hashes": [ + "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", + "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" + ], + "version": "==1.1.1" + }, "astroid": { "hashes": [ "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", @@ -308,6 +344,49 @@ ], "version": "==2.1.0" }, + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "cached-property": { + "hashes": [ + "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", + "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + ], + "version": "==1.5.1" + }, + "cfgv": { + "hashes": [ + "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", + "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" + ], + "version": "==1.1.0" + }, + "identify": { + "hashes": [ + "sha256:5e956558a9a1e3b3891d7c6609fc9709657a11878af288ace484d1a46a93922b", + "sha256:623086059219cc7b86c77a3891f3700cb175d4ce02b8fb8802b047301d71e783" + ], + "version": "==1.1.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:36b02c84f9001adf65209fefdf951be8e9014a95eab9938c0779ad5670359b1c", + "sha256:60b6481a72908c93ccb707abeb926fb5a15319b9e6f0b76639a718837ee12de0" + ], + "version": "==0.6" + }, + "importlib-resources": { + "hashes": [ + "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", + "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" + ], + "markers": "python_version < '3.7'", + "version": "==1.0.2" + }, "isort": { "hashes": [ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", @@ -357,6 +436,20 @@ ], "version": "==0.6.1" }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "pre-commit": { + "hashes": [ + "sha256:7542bd8ae1c58745175ea0a9295964ee82a10f7e18c4344f5e4c02bd85d02561", + "sha256:87f687da6a2651d5067cfec95b854b004e95b70143cbf2369604bb3acbce25ec" + ], + "index": "pypi", + "version": "==1.12.0" + }, "ptvsd": { "hashes": [ "sha256:533b3ca9a3973700d5fe6cb152cf6c69bac2839389460164c84ab1956ec992a0", @@ -366,6 +459,13 @@ "index": "pypi", "version": "==4.2.0" }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "version": "==2.4.0" + }, "pylint": { "hashes": [ "sha256:51f5a52bd31cb2db5b83ff37e3e902460eaa5591dea2739ba5d10d13ec5c5350", @@ -374,6 +474,22 @@ "index": "pypi", "version": "==2.2.0" }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "version": "==3.13" + }, "six": { "hashes": [ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", @@ -381,6 +497,49 @@ ], "version": "==1.11.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.1.0" + }, + "virtualenv": { + "hashes": [ + "sha256:686176c23a538ecc56d27ed9d5217abd34644823d6391cbeb232f42bf722baad", + "sha256:f899fafcd92e1150f40c8215328be38ff24b519cd95357fa6e78e006c7638208" + ], + "version": "==16.1.0" + }, "wrapt": { "hashes": [ "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" diff --git a/application/commands.py b/application/commands.py index e8948e8..6f8995b 100644 --- a/application/commands.py +++ b/application/commands.py @@ -4,10 +4,12 @@ from schematics.types import StringType from schematics.exceptions import ValidationError, DataError + class ResultStatus(str, Enum): OK = 'ok' ERROR = 'error' + class CommandResult(object): def __init__(self, status: ResultStatus, **kwargs): self._kwargs = kwargs @@ -17,12 +19,14 @@ def __repr__(self): return '<{}>({}) {}'.format(type(self).__name__, self.status, self._kwargs) def toJSON(self): - return json.dumps(self, default=lambda o:o.__dict__, sort_keys=True, indent=4) + return json.dumps(self, default=lambda o: {k: v for k, v in o.__dict__.items() if not k.startswith('_')}, sort_keys=True) + class Command(Model): """ Command is an immutable data structure holding object """ + def is_valid(self): try: self.validate() @@ -36,4 +40,4 @@ def __repr__(self): class AddItemCommand(Command): title = StringType(required=True) - description = StringType() \ No newline at end of file + description = StringType() diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 83c1aad..6e6f206 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -1,14 +1,14 @@ import falcon import json from application.commands import AddItemCommand -from application.settings import APPLICATION_NAME - +from application.settings import APPLICATION_NAME + class InfoController(object): def on_get(self, req, res): doc = { 'framework': 'Falcon {}'.format(falcon.__version__), - 'application': APPLICATION_NAME + 'application': APPLICATION_NAME, } res.body = json.dumps(doc, ensure_ascii=False) res.status = falcon.HTTP_200 @@ -20,11 +20,13 @@ def __init__(self, command_bus): def on_get(self, req, res): command = AddItemCommand(req.params, strict=False) - command.validate() - # TODO: exception handling? validation? + if not command.is_valid(): + res.status = falcon.HTTP_400 + # TODO: Add error details + return result = self.command_bus.execute(command) res.body = result.toJSON() - res.status = falcon.HTTP_200 + res.status = falcon.HTTP_202 def on_post(self, req, res): pass From 392c33baa1794df8f8af713eb5019ac763525796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Tue, 27 Nov 2018 13:36:32 +0100 Subject: [PATCH 13/44] WIP: items controller, stub repo, experimenting with json serialization and dataclasses --- Pipfile.lock | 78 ++---------------- application/command_handlers.py | 15 ++-- application/commands.py | 2 +- application/services.py | 10 +++ application/test_command_bus.py | 82 +++++++++---------- application/test_commands.py | 14 ++-- composition_root.py | 58 ++++++++----- domain/entities.py | 8 +- domain/value_objects.py | 3 + .../framework/falcon/controllers.py | 24 ++++-- .../repositories/auction_items_repository.py | 17 ++++ 11 files changed, 151 insertions(+), 160 deletions(-) create mode 100644 application/services.py create mode 100644 infrastructure/repositories/auction_items_repository.py diff --git a/Pipfile.lock b/Pipfile.lock index f78b15a..99ed30b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -221,11 +221,11 @@ }, "pylint": { "hashes": [ - "sha256:51f5a52bd31cb2db5b83ff37e3e902460eaa5591dea2739ba5d10d13ec5c5350", - "sha256:fe49f9ada5c8999344ac3a37541e329eaff11d014460065c4128fc94cf5cf140" + "sha256:8e645abc9572749f0256f05db86af81ea2e3d583086cc2b73d241a64ecf571b7", + "sha256:f70e1b78240ba7fea809ecc00fbfbc51615ab531ef3f76f0548072c732358453" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.2.1" }, "pytest": { "hashes": [ @@ -280,35 +280,6 @@ ], "version": "==1.11.0" }, - "typed-ast": { - "hashes": [ - "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", - "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", - "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", - "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", - "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", - "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", - "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", - "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", - "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", - "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", - "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", - "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", - "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", - "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", - "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", - "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", - "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", - "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", - "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", - "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", - "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", - "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", - "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" - ], - "markers": "python_version < '3.7' and implementation_name == 'cpython'", - "version": "==1.1.0" - }, "watchdog": { "hashes": [ "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" @@ -379,14 +350,6 @@ ], "version": "==0.6" }, - "importlib-resources": { - "hashes": [ - "sha256:6e2783b2538bd5a14678284a3962b0660c715e5a0f10243fd5e00a4b5974f50b", - "sha256:d3279fd0f6f847cced9f7acc19bd3e5df54d34f93a2e7bb5f238f81545787078" - ], - "markers": "python_version < '3.7'", - "version": "==1.0.2" - }, "isort": { "hashes": [ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", @@ -468,11 +431,11 @@ }, "pylint": { "hashes": [ - "sha256:51f5a52bd31cb2db5b83ff37e3e902460eaa5591dea2739ba5d10d13ec5c5350", - "sha256:fe49f9ada5c8999344ac3a37541e329eaff11d014460065c4128fc94cf5cf140" + "sha256:8e645abc9572749f0256f05db86af81ea2e3d583086cc2b73d241a64ecf571b7", + "sha256:f70e1b78240ba7fea809ecc00fbfbc51615ab531ef3f76f0548072c732358453" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.2.1" }, "pyyaml": { "hashes": [ @@ -504,35 +467,6 @@ ], "version": "==0.10.0" }, - "typed-ast": { - "hashes": [ - "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", - "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", - "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", - "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", - "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", - "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", - "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", - "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", - "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", - "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", - "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", - "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", - "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", - "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", - "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", - "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", - "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", - "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", - "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", - "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", - "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", - "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", - "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" - ], - "markers": "python_version < '3.7' and implementation_name == 'cpython'", - "version": "==1.1.0" - }, "virtualenv": { "hashes": [ "sha256:686176c23a538ecc56d27ed9d5217abd34644823d6391cbeb232f42bf722baad", diff --git a/application/command_handlers.py b/application/command_handlers.py index 1fb814d..a0719cd 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -1,10 +1,13 @@ from application.commands import AddItemCommand, CommandResult, ResultStatus -class AddItemCommandHandler(object): - def __init__(self, items_repository): - self._items_repository = items_repository - def handle(self, command: AddItemCommand): - # TODO: add logic - return CommandResult(ResultStatus.OK) +class AddItemCommandHandler(object): + def __init__(self, items_repository): + self._items_repository = items_repository + def handle(self, command: AddItemCommand): + self._items_repository.add( + title=command.title, + description=command.description + ) + return CommandResult(ResultStatus.OK) diff --git a/application/commands.py b/application/commands.py index 6f8995b..577411b 100644 --- a/application/commands.py +++ b/application/commands.py @@ -18,7 +18,7 @@ def __init__(self, status: ResultStatus, **kwargs): def __repr__(self): return '<{}>({}) {}'.format(type(self).__name__, self.status, self._kwargs) - def toJSON(self): + def to_json(self): return json.dumps(self, default=lambda o: {k: v for k, v in o.__dict__.items() if not k.startswith('_')}, sort_keys=True) diff --git a/application/services.py b/application/services.py new file mode 100644 index 0000000..6b0ddc1 --- /dev/null +++ b/application/services.py @@ -0,0 +1,10 @@ +import json + + +class AuctionItemsService: + def __init__(self, items_repository): + self._items_repository = items_repository + + def get_all(self): + result = self._items_repository.get_all() + return json.dumps(list(map(lambda r: r.as_dict(), result))) diff --git a/application/test_command_bus.py b/application/test_command_bus.py index 3ee53e6..97e853b 100644 --- a/application/test_command_bus.py +++ b/application/test_command_bus.py @@ -1,59 +1,59 @@ -import dependency_injector.containers as containers -import dependency_injector.providers as providers +# import dependency_injector.containers as containers +# import dependency_injector.providers as providers -from application.commands import Command, CommandResult, ResultStatus -from application.command_bus import CommandBus -from composition_root import CommandBusContainer +# from application.commands import Command, CommandResult, ResultStatus +# from application.command_bus import CommandBus +# from composition_root import CommandBusContainer -class Light(object): - def __init__(self, is_turned_on = False): - self.is_turned_on = is_turned_on +# class Light(object): +# def __init__(self, is_turned_on = False): +# self.is_turned_on = is_turned_on - def turn_on(self): - self.is_turned_on = True +# def turn_on(self): +# self.is_turned_on = True - def turn_off(self): - self.is_turned_on = False +# def turn_off(self): +# self.is_turned_on = False -class LightOnCommand(Command): - pass +# class LightOnCommand(Command): +# pass -class LightOffCommand(Command): - pass +# class LightOffCommand(Command): +# pass -class LightOnCommandHandler(object): - def __init__(self, light): - self.light = light +# class LightOnCommandHandler(object): +# def __init__(self, light): +# self.light = light - def handle(self, command: LightOnCommand): - self.light.turn_on() - return CommandResult(ResultStatus.OK) +# def handle(self, command: LightOnCommand): +# self.light.turn_on() +# return CommandResult(ResultStatus.OK) -class LightOffCommandHandler(object): - def __init__(self, light): - self.light = light +# class LightOffCommandHandler(object): +# def __init__(self, light): +# self.light = light - def handle(self, command: LightOnCommand): - self.light.turn_off() - return CommandResult(ResultStatus.OK) +# def handle(self, command: LightOnCommand): +# self.light.turn_off() +# return CommandResult(ResultStatus.OK) -class OverriddenCommandBusContainer(CommandBusContainer): - light_factory = providers.Singleton(Light) - command_handler_factory = providers.FactoryAggregate( - LightOnCommand=providers.Factory(LightOnCommandHandler, light=light_factory), - LightOffCommand=providers.Factory(LightOffCommandHandler, light=light_factory) - ) +# class OverriddenCommandBusContainer(CommandBusContainer): +# light_factory = providers.Singleton(Light) +# command_handler_factory = providers.FactoryAggregate( +# LightOnCommand=providers.Factory(LightOnCommandHandler, light=light_factory), +# LightOffCommand=providers.Factory(LightOffCommandHandler, light=light_factory) +# ) -def test_commnad_bus_will_dispatch_command(): - bus = OverriddenCommandBusContainer.command_bus_factory() - command = LightOnCommand() - result = bus.execute(command) - # assert result.status == ResultStatus.OK - # assert light.is_turned_on == True - pass \ No newline at end of file +# def test_command_bus_will_dispatch_command(): +# bus = OverriddenCommandBusContainer.command_bus_factory() +# command = LightOnCommand() +# result = bus.execute(command) +# # assert result.status == ResultStatus.OK +# # assert light.is_turned_on == True +# pass \ No newline at end of file diff --git a/application/test_commands.py b/application/test_commands.py index 848b27b..30ec63c 100644 --- a/application/test_commands.py +++ b/application/test_commands.py @@ -9,15 +9,15 @@ -# from application.commands import AddItemCommand +from application.commands import AddItemCommand -# def test_valid_add_item_command(): -# command = AddItemCommand({ 'title': 'Fluffy dragon' }) -# assert command.is_valid() == True +def test_valid_add_item_command(): + command = AddItemCommand({ 'title': 'Fluffy dragon' }) + assert command.is_valid() == True -# def test_add_item_command_title_is_required(): -# command = AddItemCommand({ 'description': 'Fluffy dragon' }) -# assert command.is_valid() == False +def test_add_item_command_title_is_required(): + command = AddItemCommand({ 'description': 'Fluffy dragon' }) + assert command.is_valid() == False # def test_add_item_command_will_return_errors(): # command = AddItemCommand({ 'description': 'Fluffy dragon' }) diff --git a/composition_root.py b/composition_root.py index 7cddd37..1111c4e 100644 --- a/composition_root.py +++ b/composition_root.py @@ -2,38 +2,52 @@ import dependency_injector.providers as providers from application.command_bus import CommandBus, default_command_handler_locator +from application.command_handlers import AddItemCommandHandler +from application.services import AuctionItemsService from infrastructure.framework.falcon.controllers import InfoController, ItemsController +from infrastructure.repositories.auction_items_repository import AuctionItemsRepository + class ObjectiveCommandHandler(): - def __init__(self, logger): - self.logger = logger + def __init__(self, logger): + self.logger = logger + + def handle(self, command): + print('objective handler is handling', command, self.logger) - def handle(self, command): - print('objective handler is handling', command, self.logger) def functional_handler(logger): - def handle(command): - print('functional handler is handling', command, logger) - return handle + def handle(command): + print('functional handler is handling', command, logger) + return handle -from application.command_handlers import AddItemCommandHandler class CommandBusContainer(containers.DeclarativeContainer): - items_repository = None - command_handler_factory = providers.FactoryAggregate( - AddItemCommand=providers.Factory(AddItemCommandHandler, - items_repository=items_repository + items_repository = providers.Singleton(AuctionItemsRepository) + + command_handler_factory = providers.FactoryAggregate( + AddItemCommand=providers.Factory(AddItemCommandHandler, + items_repository=items_repository + ) + ) + + command_bus_factory = providers.Factory( + CommandBus, + command_handler_factory=providers.DelegatedFactory( + command_handler_factory) + ) + + +class ServicesContainer(containers.DeclarativeContainer): + items_service = providers.Factory( + AuctionItemsService, + items_repository=CommandBusContainer.items_repository ) - ) - command_bus_factory = providers.Factory( - CommandBus, - command_handler_factory=providers.DelegatedFactory(command_handler_factory) - ) class FalconContainer(containers.DeclarativeContainer): - items_controller_factory = providers.Factory(ItemsController, - command_bus=CommandBusContainer.command_bus_factory - ) - info_controller_factory = providers.Factory(InfoController) - \ No newline at end of file + items_controller_factory = providers.Factory(ItemsController, + command_bus=CommandBusContainer.command_bus_factory, + items_service=ServicesContainer.items_service, + ) + info_controller_factory = providers.Factory(InfoController) diff --git a/domain/entities.py b/domain/entities.py index 27cea23..af67ba2 100644 --- a/domain/entities.py +++ b/domain/entities.py @@ -1,7 +1,9 @@ from domain.value_objects import Currency + class AuctionItem: - def __init__(self, name : str, description : str, starting_price : Currency, start_date, end_date): - self.name = name + def __init__(self, id: str, title: str, description: str, starting_price: Currency, start_date, end_date): + self.title = title self.description = description - self.current_price = starting_price \ No newline at end of file + self.current_price = starting_price + self.end_date = end_date \ No newline at end of file diff --git a/domain/value_objects.py b/domain/value_objects.py index 269c082..e7fa3d1 100644 --- a/domain/value_objects.py +++ b/domain/value_objects.py @@ -19,4 +19,7 @@ def __lt__(self, other): return self.amount < other.amount def __repr__(self): + return '{:.2f}'.format(self.amount) + + def __str__(self): return '{:.2f}'.format(self.amount) \ No newline at end of file diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 6e6f206..9e289a3 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -15,18 +15,26 @@ def on_get(self, req, res): class ItemsController(object): - def __init__(self, command_bus): - self.command_bus = command_bus + def __init__(self, command_bus, items_service): + self._command_bus = command_bus + self._items_service = items_service def on_get(self, req, res): - command = AddItemCommand(req.params, strict=False) + result = self._items_service.get_all() + res.body = result + res.status = falcon.HTTP_200 + + def on_post(self, req, res): + command = AddItemCommand(req.media, strict=False) if not command.is_valid(): res.status = falcon.HTTP_400 # TODO: Add error details return - result = self.command_bus.execute(command) - res.body = result.toJSON() - res.status = falcon.HTTP_202 + # try: + result = self._command_bus.execute(command) + res.body = result.to_json() + res.status = falcon.HTTP_200 + # except: + # # TODO: Handle app exception + # pass - def on_post(self, req, res): - pass diff --git a/infrastructure/repositories/auction_items_repository.py b/infrastructure/repositories/auction_items_repository.py new file mode 100644 index 0000000..5cc73f7 --- /dev/null +++ b/infrastructure/repositories/auction_items_repository.py @@ -0,0 +1,17 @@ +import uuid +from datetime import datetime +from typing import List + +from domain.entities import AuctionItem +from domain.value_objects import Currency + + +class AuctionItemsRepository: + items = [] + def add(self, title, description, end_date=datetime.now()): + id = uuid.uuid4().hex + current_datetime = datetime.now() + self.items.append((id, AuctionItem(id=id, title=title, description=description, starting_price=Currency(10), start_date=current_datetime, end_date=end_date))) + + def get_all(self): + return list(map(lambda item: item[1], self.items)) \ No newline at end of file From 8569b82c789b024b76b45cbb226f43dd7d5f84e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Thu, 29 Nov 2018 13:50:52 +0100 Subject: [PATCH 14/44] WIP: query bus fundamentals --- application/commands.py | 3 -- application/queries.py | 43 +++++++++++++++++++ application/query_bus.py | 23 ++++++++++ application/query_handlers.py | 9 ++++ application/response.py | 19 ++++++++ application/services.py | 10 ----- composition_root.py | 21 ++++++--- .../framework/falcon/controllers.py | 21 ++++++--- 8 files changed, 122 insertions(+), 27 deletions(-) create mode 100644 application/queries.py create mode 100644 application/query_bus.py create mode 100644 application/query_handlers.py create mode 100644 application/response.py delete mode 100644 application/services.py diff --git a/application/commands.py b/application/commands.py index 577411b..ad13c00 100644 --- a/application/commands.py +++ b/application/commands.py @@ -18,9 +18,6 @@ def __init__(self, status: ResultStatus, **kwargs): def __repr__(self): return '<{}>({}) {}'.format(type(self).__name__, self.status, self._kwargs) - def to_json(self): - return json.dumps(self, default=lambda o: {k: v for k, v in o.__dict__.items() if not k.startswith('_')}, sort_keys=True) - class Command(Model): """ diff --git a/application/queries.py b/application/queries.py new file mode 100644 index 0000000..6bebc09 --- /dev/null +++ b/application/queries.py @@ -0,0 +1,43 @@ +import json +from enum import Enum + +from schematics.exceptions import DataError, ValidationError +from schematics.models import Model + + +class QueryResultStatus(str, Enum): + OK = 'ok' + ERROR = 'error' + + +class QueryResult(object): + def __init__(self, status: QueryResultStatus, data): + self.data = data + self.status = status + + def __repr__(self): + return '<{}>({}) {}'.format(type(self).__name__, self.status, self.data) + + def to_json(self): + # TODO: refactor + return json.dumps(self, default=lambda o: {k: v for k, v in o.__dict__.items() if not k.startswith('_')}, sort_keys=True) + + +class Query(Model): + """ + Query is an immutable data structure holding object + """ + + def is_valid(self): + try: + self.validate() + except DataError: + return False + return True + + def __repr__(self): + return '<{}>({})'.format(type(self).__name__, self.__dict__['_data']) + + +class GetItemsQuery(Query): + pass diff --git a/application/query_bus.py b/application/query_bus.py new file mode 100644 index 0000000..4f028ae --- /dev/null +++ b/application/query_bus.py @@ -0,0 +1,23 @@ +from application.queries import Query, QueryResult + + +class QueryBus(object): + """ + Query bus is a central place for querying the data. + It offers some benefits over direct querying the database and/or repositories: + - in-memory bus can be replaced with persistent one, so multiple apps can share one bus + - it can be used by multiple clients: web controller, console app, etc. + - we can provide rate limiting and DoS protection + - we can cache query results + """ + + def __init__(self, query_handler_factory): + self._query_handler_factory = query_handler_factory + + def get_handler_for_query(self, query: Query): + query_class_name = type(query).__name__ + return self._query_handler_factory(query_class_name) + + def execute(self, query: Query) -> QueryResult: + handler = self.get_handler_for_query(query) + return handler.handle(query) diff --git a/application/query_handlers.py b/application/query_handlers.py new file mode 100644 index 0000000..5ffb9d1 --- /dev/null +++ b/application/query_handlers.py @@ -0,0 +1,9 @@ +from application.queries import GetItemsQuery, QueryResult, QueryResultStatus + +class GetItemsQueryHandler(object): + def __init__(self, items_repository): + self._items_repository = items_repository + + def handle(self, query: GetItemsQuery): + data = self._items_repository.get_all() + return QueryResult(status=QueryResultStatus.OK, data=data) \ No newline at end of file diff --git a/application/response.py b/application/response.py new file mode 100644 index 0000000..27b28ee --- /dev/null +++ b/application/response.py @@ -0,0 +1,19 @@ +import json +from datetime import datetime +import inspect + +def is_public(v): + return isinstance(v, property) + +class CustomJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + try: + return { k: v for k,v in obj.__class__.__dict__.items() if not k.startswith('_')} + except: + return super().default(obj) + + +def response(obj): + return json.dumps(obj, cls=CustomJSONEncoder) diff --git a/application/services.py b/application/services.py deleted file mode 100644 index 6b0ddc1..0000000 --- a/application/services.py +++ /dev/null @@ -1,10 +0,0 @@ -import json - - -class AuctionItemsService: - def __init__(self, items_repository): - self._items_repository = items_repository - - def get_all(self): - result = self._items_repository.get_all() - return json.dumps(list(map(lambda r: r.as_dict(), result))) diff --git a/composition_root.py b/composition_root.py index 1111c4e..c888428 100644 --- a/composition_root.py +++ b/composition_root.py @@ -3,8 +3,10 @@ from application.command_bus import CommandBus, default_command_handler_locator from application.command_handlers import AddItemCommandHandler -from application.services import AuctionItemsService -from infrastructure.framework.falcon.controllers import InfoController, ItemsController +from application.query_bus import QueryBus +from application.query_handlers import GetItemsQueryHandler +from infrastructure.framework.falcon.controllers import (InfoController, + ItemsController) from infrastructure.repositories.auction_items_repository import AuctionItemsRepository @@ -38,16 +40,21 @@ class CommandBusContainer(containers.DeclarativeContainer): ) -class ServicesContainer(containers.DeclarativeContainer): - items_service = providers.Factory( - AuctionItemsService, - items_repository=CommandBusContainer.items_repository +class QueryBusContainer(containers.DeclarativeContainer): + items_repository = providers.Singleton(AuctionItemsRepository) + + query_handler_factory = providers.FactoryAggregate( + GetItemsQuery=providers.Factory( + GetItemsQueryHandler, items_repository=items_repository) ) + query_bus_factory = providers.Factory( + QueryBus, query_handler_factory=providers.DelegatedFactory(query_handler_factory)) + class FalconContainer(containers.DeclarativeContainer): items_controller_factory = providers.Factory(ItemsController, command_bus=CommandBusContainer.command_bus_factory, - items_service=ServicesContainer.items_service, + query_bus=QueryBusContainer.query_bus_factory, ) info_controller_factory = providers.Factory(InfoController) diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 9e289a3..9826cd4 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -2,6 +2,8 @@ import json from application.commands import AddItemCommand from application.settings import APPLICATION_NAME +from application.queries import GetItemsQuery +from application.response import response class InfoController(object): @@ -10,18 +12,24 @@ def on_get(self, req, res): 'framework': 'Falcon {}'.format(falcon.__version__), 'application': APPLICATION_NAME, } - res.body = json.dumps(doc, ensure_ascii=False) + res.body = response(doc) res.status = falcon.HTTP_200 class ItemsController(object): - def __init__(self, command_bus, items_service): + def __init__(self, command_bus, query_bus): self._command_bus = command_bus - self._items_service = items_service + self._query_bus = query_bus def on_get(self, req, res): - result = self._items_service.get_all() - res.body = result + query = GetItemsQuery() + if not query.is_valid(): + res.status = falcon.HTTP_400 + # TODO: Add error details + return + + result = self._query_bus.execute(query) + res.body = response(result) res.status = falcon.HTTP_200 def on_post(self, req, res): @@ -32,9 +40,8 @@ def on_post(self, req, res): return # try: result = self._command_bus.execute(command) - res.body = result.to_json() + res.body = response(result) res.status = falcon.HTTP_200 # except: # # TODO: Handle app exception # pass - From 450b37f11c9d901ec0042c6e74071648584d5725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Thu, 29 Nov 2018 14:00:44 +0100 Subject: [PATCH 15/44] QueryBus + custom JSON Encoder --- application/queries.py | 5 ----- application/response.py | 28 ++++++++++++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/application/queries.py b/application/queries.py index 6bebc09..344c986 100644 --- a/application/queries.py +++ b/application/queries.py @@ -18,11 +18,6 @@ def __init__(self, status: QueryResultStatus, data): def __repr__(self): return '<{}>({}) {}'.format(type(self).__name__, self.status, self.data) - def to_json(self): - # TODO: refactor - return json.dumps(self, default=lambda o: {k: v for k, v in o.__dict__.items() if not k.startswith('_')}, sort_keys=True) - - class Query(Model): """ Query is an immutable data structure holding object diff --git a/application/response.py b/application/response.py index 27b28ee..337925d 100644 --- a/application/response.py +++ b/application/response.py @@ -2,17 +2,29 @@ from datetime import datetime import inspect -def is_public(v): - return isinstance(v, property) - class CustomJSONEncoder(json.JSONEncoder): + # REF: https://stackoverflow.com/a/35483750 def default(self, obj): if isinstance(obj, datetime): - return obj.isoformat() - try: - return { k: v for k,v in obj.__class__.__dict__.items() if not k.startswith('_')} - except: - return super().default(obj) + return obj.isoformat()+'Z' + if hasattr(obj, "to_json"): + return self.default(obj.to_json()) + elif hasattr(obj, "__dict__"): + d = dict( + (key, value) + for key, value in inspect.getmembers(obj) + if not key.startswith("_") + and not inspect.isabstract(value) + and not inspect.isbuiltin(value) + and not inspect.isfunction(value) + and not inspect.isgenerator(value) + and not inspect.isgeneratorfunction(value) + and not inspect.ismethod(value) + and not inspect.ismethoddescriptor(value) + and not inspect.isroutine(value) + ) + return self.default(d) + return obj def response(obj): From 5df30c7a16922695ee0b93b48c1a3b54a6df94f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Thu, 29 Nov 2018 14:59:37 +0100 Subject: [PATCH 16/44] Missing fields in command --- application/command_handlers.py | 7 ++++++- application/commands.py | 4 +++- application/response.py | 3 ++- domain/entities.py | 3 ++- infrastructure/repositories/auction_items_repository.py | 4 ++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/application/command_handlers.py b/application/command_handlers.py index a0719cd..ef34eed 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -6,8 +6,13 @@ def __init__(self, items_repository): self._items_repository = items_repository def handle(self, command: AddItemCommand): + + # How to handle defaults for arguments not present in query/command? + self._items_repository.add( title=command.title, - description=command.description + description=command.description, + starting_price=command.starting_price, + end_date=command.end_date ) return CommandResult(ResultStatus.OK) diff --git a/application/commands.py b/application/commands.py index ad13c00..359a94e 100644 --- a/application/commands.py +++ b/application/commands.py @@ -1,7 +1,7 @@ import json from enum import Enum from schematics.models import Model -from schematics.types import StringType +from schematics.types import StringType, DateTimeType, DecimalType from schematics.exceptions import ValidationError, DataError @@ -38,3 +38,5 @@ def __repr__(self): class AddItemCommand(Command): title = StringType(required=True) description = StringType() + starting_price = DecimalType() + end_date = DateTimeType() diff --git a/application/response.py b/application/response.py index 337925d..23c07dc 100644 --- a/application/response.py +++ b/application/response.py @@ -1,6 +1,7 @@ +import inspect import json from datetime import datetime -import inspect + class CustomJSONEncoder(json.JSONEncoder): # REF: https://stackoverflow.com/a/35483750 diff --git a/domain/entities.py b/domain/entities.py index af67ba2..0147d11 100644 --- a/domain/entities.py +++ b/domain/entities.py @@ -3,7 +3,8 @@ class AuctionItem: def __init__(self, id: str, title: str, description: str, starting_price: Currency, start_date, end_date): + self.id = id self.title = title self.description = description - self.current_price = starting_price + self.starting_price = starting_price self.end_date = end_date \ No newline at end of file diff --git a/infrastructure/repositories/auction_items_repository.py b/infrastructure/repositories/auction_items_repository.py index 5cc73f7..c0a4396 100644 --- a/infrastructure/repositories/auction_items_repository.py +++ b/infrastructure/repositories/auction_items_repository.py @@ -8,10 +8,10 @@ class AuctionItemsRepository: items = [] - def add(self, title, description, end_date=datetime.now()): + def add(self, title, description, starting_price, end_date): id = uuid.uuid4().hex current_datetime = datetime.now() - self.items.append((id, AuctionItem(id=id, title=title, description=description, starting_price=Currency(10), start_date=current_datetime, end_date=end_date))) + self.items.append((id, AuctionItem(id=id, title=title, description=description, starting_price=starting_price, start_date=current_datetime, end_date=end_date))) def get_all(self): return list(map(lambda item: item[1], self.items)) \ No newline at end of file From ab1b8e4a1fd9a28b8372db0c950df628f8dd1b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Fri, 30 Nov 2018 11:54:37 +0100 Subject: [PATCH 17/44] updated readme, todos --- README.md | 14 ++++++-------- application/command_handlers.py | 2 +- application/commands.py | 1 + application/queries.py | 2 +- application/response.py | 3 ++- domain/entities.py | 1 + infrastructure/framework/falcon/controllers.py | 9 +++++---- .../repositories/auction_items_repository.py | 2 +- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 848c9f5..cbc998b 100644 --- a/README.md +++ b/README.md @@ -5,27 +5,25 @@ The goal is to implement an automatic bidding system, described here: https://ww TODO for near future: -* commands, command bus and handlers +* simple authorization (user id in request header) -* command validation +* first business use-case (up to three open items at the same time) * application-level exceptions for invalid commands -* mediator pattern - -* TESTS!!!! +* TESTS!!!! + code metrics + CI/CD * executing commands with immediate feedback http://blog.sapiensworks.com/post/2015/07/20/CQRS-Immediate-Feedback-Web-App * handling commands errors: application layer, business layer -* start using dependency injection - * command validation https://stackoverflow.com/questions/32239353/command-validation-in-ddd-with-cqrs -* handling async commands +* handling async commands (mediator pattern, asyncio) + +* Application-level event bus, publisher/subscriber pattern * framework agnostic integration tests?? diff --git a/application/command_handlers.py b/application/command_handlers.py index a0719cd..6d4d04f 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -2,7 +2,7 @@ class AddItemCommandHandler(object): - def __init__(self, items_repository): + def __init__(self, items_repository): # TODO: interface self._items_repository = items_repository def handle(self, command: AddItemCommand): diff --git a/application/commands.py b/application/commands.py index ad13c00..f7680cd 100644 --- a/application/commands.py +++ b/application/commands.py @@ -7,6 +7,7 @@ class ResultStatus(str, Enum): OK = 'ok' + PENDING = 'pending' ERROR = 'error' diff --git a/application/queries.py b/application/queries.py index 344c986..a26684e 100644 --- a/application/queries.py +++ b/application/queries.py @@ -11,7 +11,7 @@ class QueryResultStatus(str, Enum): class QueryResult(object): - def __init__(self, status: QueryResultStatus, data): + def __init__(self, status: QueryResultStatus, data): # TODO: Type hint self.data = data self.status = status diff --git a/application/response.py b/application/response.py index 337925d..f8ba8e1 100644 --- a/application/response.py +++ b/application/response.py @@ -2,6 +2,7 @@ from datetime import datetime import inspect +# TODO: refactor, move JSONEncoder to composition_root class CustomJSONEncoder(json.JSONEncoder): # REF: https://stackoverflow.com/a/35483750 def default(self, obj): @@ -27,5 +28,5 @@ def default(self, obj): return obj -def response(obj): +def json_response(obj): return json.dumps(obj, cls=CustomJSONEncoder) diff --git a/domain/entities.py b/domain/entities.py index af67ba2..ab85a75 100644 --- a/domain/entities.py +++ b/domain/entities.py @@ -3,6 +3,7 @@ class AuctionItem: def __init__(self, id: str, title: str, description: str, starting_price: Currency, start_date, end_date): + self.id = id self.title = title self.description = description self.current_price = starting_price diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 9826cd4..28ecee0 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -3,7 +3,7 @@ from application.commands import AddItemCommand from application.settings import APPLICATION_NAME from application.queries import GetItemsQuery -from application.response import response +from application.response import json_response class InfoController(object): @@ -12,7 +12,7 @@ def on_get(self, req, res): 'framework': 'Falcon {}'.format(falcon.__version__), 'application': APPLICATION_NAME, } - res.body = response(doc) + res.body = json_response(doc) res.status = falcon.HTTP_200 @@ -29,7 +29,7 @@ def on_get(self, req, res): return result = self._query_bus.execute(query) - res.body = response(result) + res.body = json_response(result) res.status = falcon.HTTP_200 def on_post(self, req, res): @@ -40,8 +40,9 @@ def on_post(self, req, res): return # try: result = self._command_bus.execute(command) - res.body = response(result) + res.body = json_response(result) res.status = falcon.HTTP_200 + # TODO: change to ActionResult (metadata - status, data, etc.) # except: # # TODO: Handle app exception # pass diff --git a/infrastructure/repositories/auction_items_repository.py b/infrastructure/repositories/auction_items_repository.py index 5cc73f7..32fc399 100644 --- a/infrastructure/repositories/auction_items_repository.py +++ b/infrastructure/repositories/auction_items_repository.py @@ -8,7 +8,7 @@ class AuctionItemsRepository: items = [] - def add(self, title, description, end_date=datetime.now()): + def add(self, title, description, end_date=datetime.now()): # TODO: datetime as dependency in composition_root id = uuid.uuid4().hex current_datetime = datetime.now() self.items.append((id, AuctionItem(id=id, title=title, description=description, starting_price=Currency(10), start_date=current_datetime, end_date=end_date))) From 800f1e273b95765d4243cdc605554ae49c255bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 12:42:56 +0100 Subject: [PATCH 18/44] Added .travis.yml for python --- .travis.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b107e9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: python \ No newline at end of file From 535fbc5ada73843a3f3e446b1427f0a69001e35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 13:03:31 +0100 Subject: [PATCH 19/44] Changes in .travis.yml --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b107e9d..66daa15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,7 @@ -language: python \ No newline at end of file +language: python +python: + - "3.7" +install: + - pipenv install +script: + - pytest \ No newline at end of file From b379c52975ce65340511d3930c91c74be4d4832f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 13:08:34 +0100 Subject: [PATCH 20/44] Changes in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 66daa15..9329ce9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.7" + - "3.7-dev" install: - pipenv install script: From 4218aae2a7e26e2c1835456023e38db989a50cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 13:15:34 +0100 Subject: [PATCH 21/44] Changes in .travis.yml --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9329ce9..e74ea2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: python python: - "3.7-dev" install: - - pipenv install -script: - - pytest \ No newline at end of file + - pip install pipenv + - pipenv sync -d +script: 'python -m pytest --cov=pyblizzard tests --cov-report=xml' +after_success: + - 'python-codacy-coverage -r coverage.xml' \ No newline at end of file From 2074950e63885bb6a40534744d0537742ca1244e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 13:23:44 +0100 Subject: [PATCH 22/44] Added requirements for coverage --- Pipfile | 2 + Pipfile.lock | 131 +++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/Pipfile b/Pipfile index abe6e39..fe3c423 100644 --- a/Pipfile +++ b/Pipfile @@ -11,9 +11,11 @@ pylint = "*" pytest = "*" pytest-watch = "*" schematics = "*" +codacy-coverage = "*" [dev-packages] pylint = "*" +pyblizzard = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 2f219ed..c2fe7e4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6442b200a074dcb79893838bdbf4979de19ba5699ea9c1cb863b837ea038b48e" + "sha256": "99ffcd9d7a2d95a11482972b96b08736252789ccb1963fc9eb0210c6351ad24d" }, "pipfile-spec": 6, "requires": { @@ -25,10 +25,10 @@ }, "astroid": { "hashes": [ - "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", - "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" ], - "version": "==2.0.4" + "version": "==2.1.0" }, "atomicwrites": { "hashes": [ @@ -44,6 +44,20 @@ ], "version": "==18.2.0" }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -51,12 +65,20 @@ ], "version": "==7.0" }, + "codacy-coverage": { + "hashes": [ + "sha256:b94651934745c638a980ad8d67494077e60f71e19e29aad1c275b66e0a070cbc", + "sha256:d8a1ce56b0dd156d6b1de14fa6217d32ec86097902f08a17ff2f95ba27264474" + ], + "index": "pypi", + "version": "==1.3.11" + }, "colorama": { "hashes": [ - "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3", - "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c" + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" ], - "version": "==0.4.0" + "version": "==0.4.1" }, "docopt": { "hashes": [ @@ -88,6 +110,13 @@ "index": "pypi", "version": "==19.9.0" }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, "isort": { "hashes": [ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", @@ -214,19 +243,19 @@ }, "pylint": { "hashes": [ - "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", - "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.2.2" }, "pytest": { "hashes": [ - "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb", - "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7" + "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", + "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.0.1" }, "pytest-watch": { "hashes": [ @@ -258,6 +287,13 @@ ], "version": "==3.13" }, + "requests": { + "hashes": [ + "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", + "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" + ], + "version": "==2.20.1" + }, "schematics": { "hashes": [ "sha256:8fcc6182606fd0b24410a1dbb066d9bbddbe8da9c9509f47b743495706239283", @@ -273,6 +309,13 @@ ], "version": "==1.11.0" }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, "watchdog": { "hashes": [ "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" @@ -296,10 +339,31 @@ "develop": { "astroid": { "hashes": [ - "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", - "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" ], - "version": "==2.0.4" + "version": "==2.1.0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" }, "isort": { "hashes": [ @@ -309,6 +373,14 @@ ], "version": "==4.3.4" }, + "jsonpickle": { + "hashes": [ + "sha256:8b6212f1155f43ce67fa945efae6d010ed059f3ca5ed377aa070e5903d45b722", + "sha256:d43ede55b3d9b5524a8e11566ea0b11c9c8109116ef6a509a1b619d2041e7397", + "sha256:ed4adf0d14564c56023862eabfac211cf01211a20c5271896c8ab6f80c68086c" + ], + "version": "==1.0" + }, "lazy-object-proxy": { "hashes": [ "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", @@ -350,13 +422,27 @@ ], "version": "==0.6.1" }, + "pyblizzard": { + "hashes": [ + "sha256:460a4701a88bcaf50ab13932970d1fee8aece054cb404f4b255a25c512f4f16d" + ], + "index": "pypi", + "version": "==1.1" + }, "pylint": { "hashes": [ - "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", - "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.2.2" + }, + "requests": { + "hashes": [ + "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", + "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" + ], + "version": "==2.20.1" }, "six": { "hashes": [ @@ -365,6 +451,13 @@ ], "version": "==1.11.0" }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, "wrapt": { "hashes": [ "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" From aacd916c9fa487d5251e8da2e40d3ec35fb502d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Mon, 3 Dec 2018 13:40:06 +0100 Subject: [PATCH 23/44] Coverage --- .coveragerc | 31 ++++++++++++++++++ .travis.yml | 4 +-- Pipfile | 3 +- Pipfile.lock | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..deb9088 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,31 @@ +# .coveragerc to control coverage.py +[run] +branch = True +omit = tests/*, manage.py +source=. + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + # Don't cover type checking imports + if TYPE_CHECKING: + +ignore_errors = True +skip_covered = True +show_missing = True +precision = 1 diff --git a/.travis.yml b/.travis.yml index e74ea2c..c035f39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,4 @@ python: install: - pip install pipenv - pipenv sync -d -script: 'python -m pytest --cov=pyblizzard tests --cov-report=xml' -after_success: - - 'python-codacy-coverage -r coverage.xml' \ No newline at end of file +script: 'python -m pytest --cov' \ No newline at end of file diff --git a/Pipfile b/Pipfile index fe3c423..5b685db 100644 --- a/Pipfile +++ b/Pipfile @@ -11,11 +11,10 @@ pylint = "*" pytest = "*" pytest-watch = "*" schematics = "*" -codacy-coverage = "*" [dev-packages] pylint = "*" -pyblizzard = "*" +pytest-cov = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index c2fe7e4..6694e2b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "99ffcd9d7a2d95a11482972b96b08736252789ccb1963fc9eb0210c6351ad24d" + "sha256": "31d67f909255ec0ee18ed2189dbf63ca6778e4ab79b836acc7e54f91b40a7d4e" }, "pipfile-spec": 6, "requires": { @@ -344,6 +344,20 @@ ], "version": "==2.1.0" }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, "certifi": { "hashes": [ "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", @@ -358,6 +372,42 @@ ], "version": "==3.0.4" }, + "coverage": { + "hashes": [ + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "version": "==4.5.2" + }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -422,6 +472,28 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pluggy": { + "hashes": [ + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + ], + "version": "==0.8.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, "pyblizzard": { "hashes": [ "sha256:460a4701a88bcaf50ab13932970d1fee8aece054cb404f4b255a25c512f4f16d" @@ -437,6 +509,22 @@ "index": "pypi", "version": "==2.2.2" }, + "pytest": { + "hashes": [ + "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", + "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "pytest-cov": { + "hashes": [ + "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", + "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" + ], + "index": "pypi", + "version": "==2.6.0" + }, "requests": { "hashes": [ "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", From bc1d343f357ab30bb896becea46bc8e857d50711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 06:40:33 +0100 Subject: [PATCH 24/44] Add coverage info --- Pipfile | 1 + Pipfile.lock | 174 +++++++++++---------------------------------------- README.md | 4 ++ 3 files changed, 42 insertions(+), 137 deletions(-) diff --git a/Pipfile b/Pipfile index 5b685db..d7677eb 100644 --- a/Pipfile +++ b/Pipfile @@ -14,6 +14,7 @@ schematics = "*" [dev-packages] pylint = "*" +python-coveralls = "*" pytest-cov = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 6694e2b..1d2ec04 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "31d67f909255ec0ee18ed2189dbf63ca6778e4ab79b836acc7e54f91b40a7d4e" + "sha256": "4a8c6f9a8f3c6230807c77b283497ddb84331fcadb9653b0b6dfc6b63da5deac" }, "pipfile-spec": 6, "requires": { @@ -44,20 +44,6 @@ ], "version": "==18.2.0" }, - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, "click": { "hashes": [ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", @@ -65,14 +51,6 @@ ], "version": "==7.0" }, - "codacy-coverage": { - "hashes": [ - "sha256:b94651934745c638a980ad8d67494077e60f71e19e29aad1c275b66e0a070cbc", - "sha256:d8a1ce56b0dd156d6b1de14fa6217d32ec86097902f08a17ff2f95ba27264474" - ], - "index": "pypi", - "version": "==1.3.11" - }, "colorama": { "hashes": [ "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", @@ -110,13 +88,6 @@ "index": "pypi", "version": "==19.9.0" }, - "idna": { - "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" - ], - "version": "==2.7" - }, "isort": { "hashes": [ "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", @@ -287,13 +258,6 @@ ], "version": "==3.13" }, - "requests": { - "hashes": [ - "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", - "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" - ], - "version": "==2.20.1" - }, "schematics": { "hashes": [ "sha256:8fcc6182606fd0b24410a1dbb066d9bbddbe8da9c9509f47b743495706239283", @@ -309,13 +273,6 @@ ], "version": "==1.11.0" }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" - }, "watchdog": { "hashes": [ "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" @@ -344,20 +301,6 @@ ], "version": "==2.1.0" }, - "atomicwrites": { - "hashes": [ - "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", - "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" - ], - "version": "==1.2.1" - }, - "attrs": { - "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" - ], - "version": "==18.2.0" - }, "certifi": { "hashes": [ "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", @@ -374,39 +317,25 @@ }, "coverage": { "hashes": [ - "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", - "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", - "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", - "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", - "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", - "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", - "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", - "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", - "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", - "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", - "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", - "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", - "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", - "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", - "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", - "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", - "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", - "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", - "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", - "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", - "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", - "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", - "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", - "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", - "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", - "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", - "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", - "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", - "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", - "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", - "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" - ], - "version": "==4.5.2" + "sha256:00d464797a236f654337181af72b4baea3d35d056ca480e45e9163bb5df496b8", + "sha256:0a90afa6f5ea08889da9066dca3ce2ef85d47587e3f66ca06a4fa8d3a0053acc", + "sha256:50727512afe77e044c7d7f2fd4cd0fe62b06527f965b335a810d956748e0514d", + "sha256:6c2fd127cd4e2decb0ab41fe3ac2948b87ad2ea0470e24b4be5f7e7fdfef8df3", + "sha256:6ed521ed3800d8f8911642b9b3c3891780a929db5e572c88c4713c1032530f82", + "sha256:76a73a48a308fb87a4417d630b0345d36166f489ef17ea5aa8e4596fb50a2296", + "sha256:85b1275b6d7a61ccc8024a4e9a4c9e896394776edce1a5d075ec116f91925462", + "sha256:8e60e720cad3ee6b0a32f475ae4040552c5623870a9ca0d3d4263faa89a8d96b", + "sha256:93c50475f189cd226e9688b9897a0cd3c4c5d9c90b1733fa8f6445cfc0182c51", + "sha256:94c1e66610807a7917d967ed6415b9d5fde7487ab2a07bb5e054567865ef6ef0", + "sha256:964f86394cb4d0fd2bb40ffcddca321acf4323b48d1aa5a93db8b743c8a00f79", + "sha256:99043494b28d6460035dd9410269cdb437ee460edc7f96f07ab45c57ba95e651", + "sha256:af2f59ce312523c384a7826821cae0b95f320fee1751387abba4f00eed737166", + "sha256:beb96d32ce8cfa47ec6433d95a33e4afaa97c19ac1b4a47ea40a424fedfee7c2", + "sha256:c00bac0f6b35b82ace069a6a0d88e8fd4cd18d964fc5e47329cd02b212397fbe", + "sha256:d079e36baceea9707fd50b268305654151011274494a33c608c075808920eda8", + "sha256:e813cba9ff0e3d37ad31dc127fac85d23f9a26d0461ef8042ac4539b2045e781" + ], + "version": "==4.0.3" }, "idna": { "hashes": [ @@ -423,14 +352,6 @@ ], "version": "==4.3.4" }, - "jsonpickle": { - "hashes": [ - "sha256:8b6212f1155f43ce67fa945efae6d010ed059f3ca5ed377aa070e5903d45b722", - "sha256:d43ede55b3d9b5524a8e11566ea0b11c9c8109116ef6a509a1b619d2041e7397", - "sha256:ed4adf0d14564c56023862eabfac211cf01211a20c5271896c8ab6f80c68086c" - ], - "version": "==1.0" - }, "lazy-object-proxy": { "hashes": [ "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", @@ -472,35 +393,6 @@ ], "version": "==0.6.1" }, - "more-itertools": { - "hashes": [ - "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", - "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", - "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" - ], - "version": "==4.3.0" - }, - "pluggy": { - "hashes": [ - "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", - "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" - ], - "version": "==0.8.0" - }, - "py": { - "hashes": [ - "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", - "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" - ], - "version": "==1.7.0" - }, - "pyblizzard": { - "hashes": [ - "sha256:460a4701a88bcaf50ab13932970d1fee8aece054cb404f4b255a25c512f4f16d" - ], - "index": "pypi", - "version": "==1.1" - }, "pylint": { "hashes": [ "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", @@ -509,21 +401,29 @@ "index": "pypi", "version": "==2.2.2" }, - "pytest": { + "python-coveralls": { "hashes": [ - "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", - "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" + "sha256:1748272081e0fc21e2c20c12e5bd18cb13272db1b130758df0d473da0cb31087", + "sha256:736dda01f64beda240e1500d5f264b969495b05fcb325c7c0eb7ebbfd1210b70" ], "index": "pypi", - "version": "==4.0.1" + "version": "==2.9.1" }, - "pytest-cov": { + "pyyaml": { "hashes": [ - "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", - "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" ], - "index": "pypi", - "version": "==2.6.0" + "version": "==3.13" }, "requests": { "hashes": [ diff --git a/README.md b/README.md index 539aeb0..b8662b2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Build Status](https://travis-ci.org/Ermlab/python-ddd.svg?branch=master)](https://travis-ci.org/Ermlab/python-ddd) +[![Coverage Status](https://coveralls.io/repos/github/Ermlab/python-ddd/badge.svg?branch=ci)](https://coveralls.io/github/Ermlab/python-ddd?branch=ci) + + AUCTION APPLICATION The goal is to implement an automatic bidding system, described here: https://www.ebay.co.uk/pages/help/buy/bidding-overview.html From 1b94af16e5e66a6a088b71667f12c8d1dba5ca8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 06:53:19 +0100 Subject: [PATCH 25/44] Change coverage badge provider --- .travis.yml | 2 +- Pipfile | 3 +- Pipfile.lock | 124 ++++++++++++++++++++++++++++++++++++--------------- README.md | 6 +-- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/.travis.yml b/.travis.yml index c035f39..d1b2a2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ python: install: - pip install pipenv - pipenv sync -d -script: 'python -m pytest --cov' \ No newline at end of file +script: 'python -m pytest --cov=./' \ No newline at end of file diff --git a/Pipfile b/Pipfile index d7677eb..3d43f07 100644 --- a/Pipfile +++ b/Pipfile @@ -14,8 +14,9 @@ schematics = "*" [dev-packages] pylint = "*" -python-coveralls = "*" pytest-cov = "*" +pytest = "*" +codecov = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 1d2ec04..29af07b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4a8c6f9a8f3c6230807c77b283497ddb84331fcadb9653b0b6dfc6b63da5deac" + "sha256": "c90aa80ddb3e465bff0693e63ab0b90309aede4915b376429bc604b6de1655e3" }, "pipfile-spec": 6, "requires": { @@ -301,6 +301,20 @@ ], "version": "==2.1.0" }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, "certifi": { "hashes": [ "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", @@ -315,27 +329,49 @@ ], "version": "==3.0.4" }, + "codecov": { + "hashes": [ + "sha256:8ed8b7c6791010d359baed66f84f061bba5bd41174bf324c31311e8737602788", + "sha256:ae00d68e18d8a20e9c3288ba3875ae03db3a8e892115bf9b83ef20507732bed4" + ], + "index": "pypi", + "version": "==2.0.15" + }, "coverage": { "hashes": [ - "sha256:00d464797a236f654337181af72b4baea3d35d056ca480e45e9163bb5df496b8", - "sha256:0a90afa6f5ea08889da9066dca3ce2ef85d47587e3f66ca06a4fa8d3a0053acc", - "sha256:50727512afe77e044c7d7f2fd4cd0fe62b06527f965b335a810d956748e0514d", - "sha256:6c2fd127cd4e2decb0ab41fe3ac2948b87ad2ea0470e24b4be5f7e7fdfef8df3", - "sha256:6ed521ed3800d8f8911642b9b3c3891780a929db5e572c88c4713c1032530f82", - "sha256:76a73a48a308fb87a4417d630b0345d36166f489ef17ea5aa8e4596fb50a2296", - "sha256:85b1275b6d7a61ccc8024a4e9a4c9e896394776edce1a5d075ec116f91925462", - "sha256:8e60e720cad3ee6b0a32f475ae4040552c5623870a9ca0d3d4263faa89a8d96b", - "sha256:93c50475f189cd226e9688b9897a0cd3c4c5d9c90b1733fa8f6445cfc0182c51", - "sha256:94c1e66610807a7917d967ed6415b9d5fde7487ab2a07bb5e054567865ef6ef0", - "sha256:964f86394cb4d0fd2bb40ffcddca321acf4323b48d1aa5a93db8b743c8a00f79", - "sha256:99043494b28d6460035dd9410269cdb437ee460edc7f96f07ab45c57ba95e651", - "sha256:af2f59ce312523c384a7826821cae0b95f320fee1751387abba4f00eed737166", - "sha256:beb96d32ce8cfa47ec6433d95a33e4afaa97c19ac1b4a47ea40a424fedfee7c2", - "sha256:c00bac0f6b35b82ace069a6a0d88e8fd4cd18d964fc5e47329cd02b212397fbe", - "sha256:d079e36baceea9707fd50b268305654151011274494a33c608c075808920eda8", - "sha256:e813cba9ff0e3d37ad31dc127fac85d23f9a26d0461ef8042ac4539b2045e781" - ], - "version": "==4.0.3" + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "version": "==4.5.2" }, "idna": { "hashes": [ @@ -393,6 +429,28 @@ ], "version": "==0.6.1" }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pluggy": { + "hashes": [ + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + ], + "version": "==0.8.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, "pylint": { "hashes": [ "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", @@ -401,29 +459,21 @@ "index": "pypi", "version": "==2.2.2" }, - "python-coveralls": { + "pytest": { "hashes": [ - "sha256:1748272081e0fc21e2c20c12e5bd18cb13272db1b130758df0d473da0cb31087", - "sha256:736dda01f64beda240e1500d5f264b969495b05fcb325c7c0eb7ebbfd1210b70" + "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", + "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" ], "index": "pypi", - "version": "==2.9.1" + "version": "==4.0.1" }, - "pyyaml": { + "pytest-cov": { "hashes": [ - "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", - "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", - "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", - "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", - "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", - "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", - "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", - "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", - "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", - "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", - "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", + "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" ], - "version": "==3.13" + "index": "pypi", + "version": "==2.6.0" }, "requests": { "hashes": [ diff --git a/README.md b/README.md index b8662b2..b0820e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Build Status](https://travis-ci.org/Ermlab/python-ddd.svg?branch=master)](https://travis-ci.org/Ermlab/python-ddd) -[![Coverage Status](https://coveralls.io/repos/github/Ermlab/python-ddd/badge.svg?branch=ci)](https://coveralls.io/github/Ermlab/python-ddd?branch=ci) - +[![Build Status](https://travis-ci.org/Ermlab/python-ddd.svg?branch=ci)](https://travis-ci.org/Ermlab/python-ddd) +[![codecov](https://codecov.io/gh/Ermlab/python-ddd/branch/ci/graph/badge.svg)](https://codecov.io/gh/Ermlab/python-ddd) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) AUCTION APPLICATION From 58cee78a24ca6ac4c538778a09605bce18dcf677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 06:57:44 +0100 Subject: [PATCH 26/44] Codecov after success --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d1b2a2e..b3d2c82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,5 @@ python: install: - pip install pipenv - pipenv sync -d -script: 'python -m pytest --cov=./' \ No newline at end of file +script: 'python -m pytest --cov=./' +after_success: 'codecov' \ No newline at end of file From 7d03b2cb5e93ee2673d3e25ff4cfacb4c466b753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 07:30:59 +0100 Subject: [PATCH 27/44] refactor coverage.rc change pytest script for travis --- .coveragerc | 8 +++++--- .travis.yml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index deb9088..4bf6cbb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,14 +1,16 @@ # .coveragerc to control coverage.py [run] branch = True -omit = tests/*, manage.py -source=. +source= + application/ + domain/ + infrastructure/ [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma - pragma: no cover + #pragma: no cover # Don't complain about missing debug-only code: def __repr__ diff --git a/.travis.yml b/.travis.yml index b3d2c82..da4ac69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,5 @@ python: install: - pip install pipenv - pipenv sync -d -script: 'python -m pytest --cov=./' +script: 'pytest --cov=./' after_success: 'codecov' \ No newline at end of file From 41a6fe1166d11353deb2c58cca794e935c4e339a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 07:34:16 +0100 Subject: [PATCH 28/44] Changes in travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index da4ac69..d1af959 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,5 @@ python: install: - pip install pipenv - pipenv sync -d -script: 'pytest --cov=./' +script: 'pytest --cov' after_success: 'codecov' \ No newline at end of file From d70e0405b99ed78c9ab762a182378c1e48f11573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Tue, 4 Dec 2018 07:37:37 +0100 Subject: [PATCH 29/44] Changed badges to master in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b0820e7..d1245ba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Build Status](https://travis-ci.org/Ermlab/python-ddd.svg?branch=ci)](https://travis-ci.org/Ermlab/python-ddd) -[![codecov](https://codecov.io/gh/Ermlab/python-ddd/branch/ci/graph/badge.svg)](https://codecov.io/gh/Ermlab/python-ddd) +[![Build Status](https://travis-ci.org/Ermlab/python-ddd.svg?branch=master)](https://travis-ci.org/Ermlab/python-ddd) +[![codecov](https://codecov.io/gh/Ermlab/python-ddd/branch/master/graph/badge.svg)](https://codecov.io/gh/Ermlab/python-ddd) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) AUCTION APPLICATION From 02d0b57a5a61365dfef1c3e57c8f61d1ac35eda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Tue, 4 Dec 2018 13:01:26 +0100 Subject: [PATCH 30/44] First tests for command bus and query bus --- .coveragerc | 1 + .gitignore | 1 + application/command_bus.py | 18 -------- application/test_command_bus.py | 78 +++++++++++++-------------------- application/test_query_bus.py | 36 +++++++++++++++ composition_root.py | 2 +- 6 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 application/test_query_bus.py diff --git a/.coveragerc b/.coveragerc index 4bf6cbb..4c646bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ source= application/ domain/ infrastructure/ +omit=*/test_* [report] # Regexes for lines to exclude from consideration diff --git a/.gitignore b/.gitignore index 14ef862..29c4057 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +cov.xml *.cover .hypothesis/ .pytest_cache/ diff --git a/application/command_bus.py b/application/command_bus.py index fe7b74a..3af0669 100644 --- a/application/command_bus.py +++ b/application/command_bus.py @@ -1,23 +1,5 @@ from application.commands import Command, CommandResult - -def default_command_handler_locator(command, **kwargs): - print('finding handler for command', command, kwargs) - raise NotImplementedError('handler lookup') - - # import importlib - # def _default_command_handler_locator(command: Command): - # module_name = type(command).__module__ - # command_class_name = type(command).__name__ - # handler_class_name = '{}Handler'.format(command_class_name) - # print('locating handler for', command_class_name) - # importlib.invalidate_caches() - # handler_module = importlib.import_module(module_name) - # handler_class = getattr(handler_module, handler_class_name) - # return handler_class - # return _default_command_handler_locator - - class CommandBus(object): """ Command bus is a central place for executing commands. diff --git a/application/test_command_bus.py b/application/test_command_bus.py index 97e853b..e4bc078 100644 --- a/application/test_command_bus.py +++ b/application/test_command_bus.py @@ -1,59 +1,41 @@ -# import dependency_injector.containers as containers -# import dependency_injector.providers as providers +import dependency_injector.containers as containers +import dependency_injector.providers as providers -# from application.commands import Command, CommandResult, ResultStatus -# from application.command_bus import CommandBus -# from composition_root import CommandBusContainer +from application.command_bus import CommandBus +from application.command_handlers import AddItemCommandHandler +from application.commands import (AddItemCommand, Command, CommandResult, + ResultStatus) +from composition_root import CommandBusContainer -# class Light(object): -# def __init__(self, is_turned_on = False): -# self.is_turned_on = is_turned_on -# def turn_on(self): -# self.is_turned_on = True +class MockAuctionItemsRepository: + def add(*args, **kwargs): + pass -# def turn_off(self): -# self.is_turned_on = False +class OverriddenCommandBusContainer(CommandBusContainer): + items_repository = providers.Singleton(MockAuctionItemsRepository) -# class LightOnCommand(Command): -# pass + command_handler_factory = providers.FactoryAggregate( + AddItemCommand=providers.Factory(AddItemCommandHandler, + items_repository=items_repository + ) + ) + command_bus_factory = providers.Factory( + CommandBus, + command_handler_factory=providers.DelegatedFactory( + command_handler_factory) + ) -# class LightOffCommand(Command): -# pass +def test_command_bus_will_dispatch_command(): + # Arrange + bus = OverriddenCommandBusContainer.command_bus_factory() + command = AddItemCommand() -# class LightOnCommandHandler(object): -# def __init__(self, light): -# self.light = light + # Act + result = bus.execute(command) -# def handle(self, command: LightOnCommand): -# self.light.turn_on() -# return CommandResult(ResultStatus.OK) - - -# class LightOffCommandHandler(object): -# def __init__(self, light): -# self.light = light - -# def handle(self, command: LightOnCommand): -# self.light.turn_off() -# return CommandResult(ResultStatus.OK) - - -# class OverriddenCommandBusContainer(CommandBusContainer): -# light_factory = providers.Singleton(Light) -# command_handler_factory = providers.FactoryAggregate( -# LightOnCommand=providers.Factory(LightOnCommandHandler, light=light_factory), -# LightOffCommand=providers.Factory(LightOffCommandHandler, light=light_factory) -# ) - - -# def test_command_bus_will_dispatch_command(): -# bus = OverriddenCommandBusContainer.command_bus_factory() -# command = LightOnCommand() -# result = bus.execute(command) -# # assert result.status == ResultStatus.OK -# # assert light.is_turned_on == True -# pass \ No newline at end of file + # Assert + assert result.status == ResultStatus.OK diff --git a/application/test_query_bus.py b/application/test_query_bus.py new file mode 100644 index 0000000..4c50e47 --- /dev/null +++ b/application/test_query_bus.py @@ -0,0 +1,36 @@ +import dependency_injector.containers as containers +import dependency_injector.providers as providers + +from application.queries import GetItemsQuery, QueryResultStatus +from application.query_bus import QueryBus +from application.query_handlers import GetItemsQueryHandler +from composition_root import QueryBusContainer + + +class MockAuctionItemsRepository: + def get_all(*args, **kwargs): + pass + + +class OverriddenQueryBusContainer(QueryBusContainer): + items_repository = providers.Singleton(MockAuctionItemsRepository) + + query_handler_factory = providers.FactoryAggregate( + GetItemsQuery=providers.Factory( + GetItemsQueryHandler, items_repository=items_repository) + ) + + query_bus_factory = providers.Factory( + QueryBus, query_handler_factory=providers.DelegatedFactory(query_handler_factory)) + + +def test_query_bus_will_dispatch_query(): + # Arrange + bus = OverriddenQueryBusContainer.query_bus_factory() + query = GetItemsQuery() + + # Act + result = bus.execute(query) + + # Assert + assert result.status == QueryResultStatus.OK diff --git a/composition_root.py b/composition_root.py index c888428..ad07fa0 100644 --- a/composition_root.py +++ b/composition_root.py @@ -1,7 +1,7 @@ import dependency_injector.containers as containers import dependency_injector.providers as providers -from application.command_bus import CommandBus, default_command_handler_locator +from application.command_bus import CommandBus from application.command_handlers import AddItemCommandHandler from application.query_bus import QueryBus from application.query_handlers import GetItemsQueryHandler From a579e821f1ceea92a6303417af3f6b178efb4449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Tue, 4 Dec 2018 14:00:39 +0100 Subject: [PATCH 31/44] Test existing entities and query validation --- application/test_queries.py | 5 +++++ domain/test_entities.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 application/test_queries.py create mode 100644 domain/test_entities.py diff --git a/application/test_queries.py b/application/test_queries.py new file mode 100644 index 0000000..23334cc --- /dev/null +++ b/application/test_queries.py @@ -0,0 +1,5 @@ +from application.queries import GetItemsQuery + +def test_valid_get_items_query(): + query = GetItemsQuery() + assert query.is_valid() == True \ No newline at end of file diff --git a/domain/test_entities.py b/domain/test_entities.py new file mode 100644 index 0000000..c4a3b31 --- /dev/null +++ b/domain/test_entities.py @@ -0,0 +1,31 @@ +from datetime import datetime + +import pytest + +from domain.entities import AuctionItem +from domain.value_objects import Currency + + +def test_auction_item_can_be_created(): + data = { + 'id': 'some_uuid', + 'title': 'Title', + 'description': 'Description', + 'starting_price': Currency(10.00), + 'start_date': datetime.now(), + 'end_date': datetime.now(), + } + auction_item = AuctionItem(id=data['id'], title=data['title'], description=data['description'], + starting_price=data['starting_price'], start_date=data['start_date'], end_date=data['end_date']) + assert isinstance(auction_item, AuctionItem) + assert auction_item.id == data['id'] + assert auction_item.title == data['title'] + assert auction_item.description == data['description'] + assert auction_item.current_price == data['starting_price'] + assert auction_item.end_date == data['end_date'] + + + +def test_auction_item_without_parameters_raises_exception(): + with pytest.raises(TypeError): + auction_item = AuctionItem() \ No newline at end of file From be81ca50e2a56d7491fdc6f86ddb57d4913bc58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Wed, 5 Dec 2018 07:51:11 +0100 Subject: [PATCH 32/44] Tests for json response --- application/test_response.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 application/test_response.py diff --git a/application/test_response.py b/application/test_response.py new file mode 100644 index 0000000..5497dfb --- /dev/null +++ b/application/test_response.py @@ -0,0 +1,51 @@ +from application.response import CustomJSONEncoder, json_response +from datetime import datetime +import json + + +class MockObjectWithToJsonMethod: + def __init__(self, description: str): + self.desc: str = description + + def to_json(self): + return { + 'desc': self.desc + } + + +class MockObjectWithDictMethod: + def __init__(self, description: str): + self.desc = description + + +class MockComplexObjectWithDictMethod: + def __init__(self, title: str, description: str, value: float): + self.title = title + self.desc = description + self.value = value + + +def test_custom_json_encoder(): + current_date = datetime.now() + data = { + 'date': current_date, + 'mock_object_to_json': MockObjectWithToJsonMethod('some text for to_json'), + 'mock_object_dict': MockObjectWithDictMethod('some text for __dict__'), + 'complex_json': MockComplexObjectWithDictMethod('title', 'some text for __dict__', 12.34), + } + expected = ( + '{' + f"\"date\": \"{current_date.isoformat()+'Z'}\"" + ', ' + f"\"mock_object_to_json\": \x7b\"desc\": \"some text for to_json\"\x7d" + ', ' + f"\"mock_object_dict\": \x7b\"desc\": \"some text for __dict__\"\x7d" + ', ' + f"\"complex_json\": \x7b\"desc\": \"some text for __dict__\", \"title\": \"title\", \"value\": 12.34\x7d" + '}' + ) + + actual = CustomJSONEncoder().encode(data) + json_response_value = json_response(data) + assert actual == expected + assert json_response_value == expected From fde029135ae39c8d7a4fe50d2baf43f77a40f3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Wed, 5 Dec 2018 09:13:08 +0100 Subject: [PATCH 33/44] Tests for auction repository --- infrastructure/repositories/__init__.py | 0 .../repositories/auction_items_repository.py | 5 ++++- .../test_auction_item_repository.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 infrastructure/repositories/__init__.py create mode 100644 infrastructure/repositories/test_auction_item_repository.py diff --git a/infrastructure/repositories/__init__.py b/infrastructure/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/repositories/auction_items_repository.py b/infrastructure/repositories/auction_items_repository.py index 32fc399..f30d9ce 100644 --- a/infrastructure/repositories/auction_items_repository.py +++ b/infrastructure/repositories/auction_items_repository.py @@ -7,11 +7,14 @@ class AuctionItemsRepository: - items = [] + def __init__(self, *args, **kwargs): + self.items = [] + def add(self, title, description, end_date=datetime.now()): # TODO: datetime as dependency in composition_root id = uuid.uuid4().hex current_datetime = datetime.now() self.items.append((id, AuctionItem(id=id, title=title, description=description, starting_price=Currency(10), start_date=current_datetime, end_date=end_date))) + return self.items[-1][0] def get_all(self): return list(map(lambda item: item[1], self.items)) \ No newline at end of file diff --git a/infrastructure/repositories/test_auction_item_repository.py b/infrastructure/repositories/test_auction_item_repository.py new file mode 100644 index 0000000..4146184 --- /dev/null +++ b/infrastructure/repositories/test_auction_item_repository.py @@ -0,0 +1,22 @@ +from infrastructure.repositories.auction_items_repository import AuctionItemsRepository +from datetime import datetime + + +def test_add(): + air = AuctionItemsRepository() + res1 = air.add(title='title', description='desc', end_date=datetime.now()) + res2 = air.add(title='title', description='desc', end_date=datetime.now()) + + assert isinstance(res1, str) + assert isinstance(res2, str) + assert res1 != res2 + +def test_get_all(): + air = AuctionItemsRepository() + assert len(air.get_all()) == 0 + + air.add(title='title', description='desc', end_date=datetime.now()) + assert len(air.get_all()) == 1 + + air.add(title='title', description='desc', end_date=datetime.now()) + assert len(air.get_all()) == 2 \ No newline at end of file From 86c1f32531e9877b1805ab8526689d7f4558efb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Fri, 7 Dec 2018 09:32:12 +0100 Subject: [PATCH 34/44] basic authentication --- Pipfile | 1 + Pipfile.lock | 29 ++++++--- application/command_handlers.py | 4 +- application/commands.py | 9 +++ application/services.py | 3 + composition_root.py | 16 ++++- infrastructure/framework/falcon/app.py | 18 ++++++ .../framework/falcon/authentication.py | 64 +++++++++++++++++++ infrastructure/framework/falcon/base.py | 9 +++ .../framework/falcon/controllers.py | 44 ++++++++----- infrastructure/repositories/exceptions.py | 2 + .../repositories/users_repository.py | 20 ++++++ main.py | 2 + 13 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 application/services.py create mode 100644 infrastructure/framework/falcon/authentication.py create mode 100644 infrastructure/framework/falcon/base.py create mode 100644 infrastructure/repositories/exceptions.py create mode 100644 infrastructure/repositories/users_repository.py diff --git a/Pipfile b/Pipfile index 73d87aa..5ebfb29 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,7 @@ pytest = "*" pytest-watch = "*" schematics = "*" dependency-injector = "*" +ptvsd = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 99ed30b..21b5338 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d680b6591b68810e468ec54bcf5445ee73ded4b98effe7b2708578a74d96eff0" + "sha256": "fdb430d0908dd76aed372fa9167a07951851f25806887d8a971e9a72c2283bf7" }, "pipfile-spec": 6, "requires": { @@ -212,6 +212,15 @@ ], "version": "==0.8.0" }, + "ptvsd": { + "hashes": [ + "sha256:533b3ca9a3973700d5fe6cb152cf6c69bac2839389460164c84ab1956ec992a0", + "sha256:8e6feb4d577b1a939af4b08821fd6afa6e71652d1e2ce41579d8b959b1e21d94", + "sha256:cfcde6a3de3cfa720e4f637af13deeae744f6dc6665b9bda92380885caf16ae6" + ], + "index": "pypi", + "version": "==4.2.0" + }, "py": { "hashes": [ "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", @@ -221,11 +230,11 @@ }, "pylint": { "hashes": [ - "sha256:8e645abc9572749f0256f05db86af81ea2e3d583086cc2b73d241a64ecf571b7", - "sha256:f70e1b78240ba7fea809ecc00fbfbc51615ab531ef3f76f0548072c732358453" + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" ], "index": "pypi", - "version": "==2.2.1" + "version": "==2.2.2" }, "pytest": { "hashes": [ @@ -345,10 +354,10 @@ }, "importlib-metadata": { "hashes": [ - "sha256:36b02c84f9001adf65209fefdf951be8e9014a95eab9938c0779ad5670359b1c", - "sha256:60b6481a72908c93ccb707abeb926fb5a15319b9e6f0b76639a718837ee12de0" + "sha256:28fba9f65e5415a691dd254cdb602bcc4d6f738e68407ad251651db358b63bcf", + "sha256:4a545e6125dc72b4ad98201ea3f40f92e8126e3a19667352b3a134d22b8bc74f" ], - "version": "==0.6" + "version": "==0.7" }, "isort": { "hashes": [ @@ -431,11 +440,11 @@ }, "pylint": { "hashes": [ - "sha256:8e645abc9572749f0256f05db86af81ea2e3d583086cc2b73d241a64ecf571b7", - "sha256:f70e1b78240ba7fea809ecc00fbfbc51615ab531ef3f76f0548072c732358453" + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" ], "index": "pypi", - "version": "==2.2.1" + "version": "==2.2.2" }, "pyyaml": { "hashes": [ diff --git a/application/command_handlers.py b/application/command_handlers.py index ef34eed..82c8268 100644 --- a/application/command_handlers.py +++ b/application/command_handlers.py @@ -7,12 +7,12 @@ def __init__(self, items_repository): def handle(self, command: AddItemCommand): - # How to handle defaults for arguments not present in query/command? - + # How to handle defaults for arguments not present in query/command? self._items_repository.add( title=command.title, description=command.description, starting_price=command.starting_price, end_date=command.end_date ) + print(len(self._items_repository.items)) return CommandResult(ResultStatus.OK) diff --git a/application/commands.py b/application/commands.py index 359a94e..55fe3cb 100644 --- a/application/commands.py +++ b/application/commands.py @@ -31,11 +31,20 @@ def is_valid(self): return False return True + def validation_errors(self): + try: + self.validate() + return None + except Exception as e: + return e + + def __repr__(self): return '<{}>({})'.format(type(self).__name__, self.__dict__['_data']) class AddItemCommand(Command): + seller_id = StringType(required=True) title = StringType(required=True) description = StringType() starting_price = DecimalType() diff --git a/application/services.py b/application/services.py new file mode 100644 index 0000000..177e82c --- /dev/null +++ b/application/services.py @@ -0,0 +1,3 @@ +class IdentityHashingService: + def hash(self, value): + return value \ No newline at end of file diff --git a/composition_root.py b/composition_root.py index c888428..16f3b13 100644 --- a/composition_root.py +++ b/composition_root.py @@ -5,9 +5,12 @@ from application.command_handlers import AddItemCommandHandler from application.query_bus import QueryBus from application.query_handlers import GetItemsQueryHandler +from application.services import IdentityHashingService from infrastructure.framework.falcon.controllers import (InfoController, ItemsController) from infrastructure.repositories.auction_items_repository import AuctionItemsRepository +from infrastructure.repositories.users_repository import InMemoryUsersRepository +from infrastructure.framework.falcon.authentication import BasicAuthenticationService class ObjectiveCommandHandler(): @@ -24,6 +27,14 @@ def handle(command): return handle + +class BaseContainer(containers.DeclarativeContainer): + hashing_service_factory = providers.Singleton(IdentityHashingService) + authentication_service_factory = providers.Factory(BasicAuthenticationService, + users_repository=providers.Factory(InMemoryUsersRepository, hashing_service=hashing_service_factory) + ) + + class CommandBusContainer(containers.DeclarativeContainer): items_repository = providers.Singleton(AuctionItemsRepository) @@ -56,5 +67,8 @@ class FalconContainer(containers.DeclarativeContainer): items_controller_factory = providers.Factory(ItemsController, command_bus=CommandBusContainer.command_bus_factory, query_bus=QueryBusContainer.query_bus_factory, + authentication_service=BaseContainer.authentication_service_factory, ) - info_controller_factory = providers.Factory(InfoController) + info_controller_factory = providers.Factory(InfoController, + authentication_service=BaseContainer.authentication_service_factory + ) diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py index 80fcb8b..b67a321 100644 --- a/infrastructure/framework/falcon/app.py +++ b/infrastructure/framework/falcon/app.py @@ -1,7 +1,25 @@ import falcon +import yaml from composition_root import FalconContainer from infrastructure.framework.falcon.controllers import InfoController + +def error_serializer(req, resp, exception): + representation = None + preferred = req.client_prefers(('application/x-yaml', + 'application/json')) + if preferred is not None: + if preferred == 'application/json': + representation = exception.to_json() + else: + representation = yaml.dump(exception.to_dict(), + encoding=None) + resp.body = representation + resp.content_type = preferred + resp.append_header('Vary', 'Accept') + + app = falcon.API() +app.set_error_serializer(error_serializer) app.add_route('/', FalconContainer.info_controller_factory()) app.add_route('/items', FalconContainer.items_controller_factory()) \ No newline at end of file diff --git a/infrastructure/framework/falcon/authentication.py b/infrastructure/framework/falcon/authentication.py new file mode 100644 index 0000000..9c1e06d --- /dev/null +++ b/infrastructure/framework/falcon/authentication.py @@ -0,0 +1,64 @@ +import falcon +import base64 +from infrastructure.framework.falcon.base import RouteController + +class UnauthenticatedException(Exception): + """ Use this exception when authentication fails """ + pass + +class ForbiddenException(Exception): + """ Use this exception when permission check fails """ + pass + +def authenticate(method): + def wrapper(self, req, res): + assert isinstance(self, RouteController), '@authenticate must be used with RouteController or derived classes' + assert self._authentication_service is not None,\ + 'You are using @authenticate for route {} but AuthenticationService not injected into {}'\ + .format(req.relative_uri, type(self).__name__) + + user = self._authentication_service.authenticate(req) + req.context['user_id'] = user.id + return method(self, req, res) + return wrapper + + +class BasicAuthenticationService: + def __init__(self, users_repository): + self._users_repository = users_repository + + def authenticate(self, req): + if req.auth is None: + raise falcon.HTTPError( + status=falcon.HTTP_401, + title='Authentication failed', + description='Authorization header is missing' + ) + + auth_type, credentials = req.auth.split(' ') + + if auth_type.lower() != 'basic': + raise falcon.HTTPError( + status=falcon.HTTP_401, + title='Authentication failed', + description="Expected 'Authorization: Basic ' header" + ) + + try: + decoded_credentials = base64.b64decode(credentials) + login, password = decoded_credentials.decode().split(':') + except Exception as e: + raise falcon.HTTPError( + status=falcon.HTTP_401, + title='Authentication failed', + description='Invalid credentials ({})'.format(e) + ) + user = self._users_repository.get_user_by_login_and_password(login, password) + + if user is None: + raise falcon.HTTPError( + status=falcon.HTTP_401, + title='Authentication failed', + description='Invalid credentials' + ) + return user \ No newline at end of file diff --git a/infrastructure/framework/falcon/base.py b/infrastructure/framework/falcon/base.py new file mode 100644 index 0000000..3fe8894 --- /dev/null +++ b/infrastructure/framework/falcon/base.py @@ -0,0 +1,9 @@ +""" +Base classes +""" + +class RouteController: + def __init__(self, command_bus = None, query_bus = None, authentication_service = None): + self._command_bus = command_bus + self._query_bus = query_bus + self._authentication_service = authentication_service diff --git a/infrastructure/framework/falcon/controllers.py b/infrastructure/framework/falcon/controllers.py index 9826cd4..051e96d 100644 --- a/infrastructure/framework/falcon/controllers.py +++ b/infrastructure/framework/falcon/controllers.py @@ -4,9 +4,10 @@ from application.settings import APPLICATION_NAME from application.queries import GetItemsQuery from application.response import response +from infrastructure.framework.falcon.base import RouteController +from infrastructure.framework.falcon.authentication import authenticate - -class InfoController(object): +class InfoController(RouteController): def on_get(self, req, res): doc = { 'framework': 'Falcon {}'.format(falcon.__version__), @@ -16,11 +17,7 @@ def on_get(self, req, res): res.status = falcon.HTTP_200 -class ItemsController(object): - def __init__(self, command_bus, query_bus): - self._command_bus = command_bus - self._query_bus = query_bus - +class ItemsController(RouteController): def on_get(self, req, res): query = GetItemsQuery() if not query.is_valid(): @@ -32,16 +29,31 @@ def on_get(self, req, res): res.body = response(result) res.status = falcon.HTTP_200 + @authenticate def on_post(self, req, res): - command = AddItemCommand(req.media, strict=False) + command = AddItemCommand({ + **req.media, + 'seller_id': req.context['user_id'] + }, strict=False) + command_name = type(command).__name__ + if not command.is_valid(): - res.status = falcon.HTTP_400 - # TODO: Add error details - return - # try: - result = self._command_bus.execute(command) - res.body = response(result) - res.status = falcon.HTTP_200 - # except: + raise falcon.HTTPError( + status=falcon.HTTP_400, + title='Invalid command', + description="{} validation failed due to {}".format(command_name, command.validation_errors()) + ) + + try: + result = self._command_bus.execute(command) + res.status = falcon.HTTP_200 + res.body = response(result) + except Exception as e: + raise falcon.HTTPError( + status=falcon.HTTP_400, + title='Failed to execute {}'.format(command_name), + description=str(e) + ) + # # TODO: Handle app exception # pass diff --git a/infrastructure/repositories/exceptions.py b/infrastructure/repositories/exceptions.py new file mode 100644 index 0000000..11e1ffd --- /dev/null +++ b/infrastructure/repositories/exceptions.py @@ -0,0 +1,2 @@ +class NotFoundException(Exception): + pass \ No newline at end of file diff --git a/infrastructure/repositories/users_repository.py b/infrastructure/repositories/users_repository.py new file mode 100644 index 0000000..fbe3d70 --- /dev/null +++ b/infrastructure/repositories/users_repository.py @@ -0,0 +1,20 @@ +import hashlib +from collections import namedtuple + +User = namedtuple('User', ['id', 'login', 'password']) + +class InMemoryUsersRepository: + def __init__(self, hashing_service): + self._hashing_service = hashing_service + self.all_users = [ + # TODO: use + User(id=1, login='Alice', password=hashing_service.hash('password')), + User(id=2, login='Bob', password=hashing_service.hash('password')), + ] + + def get_user_by_login_and_password(self, login, password): + hashed_password = self._hashing_service.hash(password) + return next( + (u for u in self.all_users if u.login == login and u.password == hashed_password), + None + ) \ No newline at end of file diff --git a/main.py b/main.py index 9175280..33f7b3b 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ import os + +# TODO: import conditionally, when ENVIRONMENT is in DEBUG mode import ptvsd ptvsd.enable_attach(address=('0.0.0.0', 3000)) From 4fa7f1e9cfb421e734b4e0733a33d406ee12ba5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 10:02:03 +0100 Subject: [PATCH 35/44] Missing tests --- application/test_commands.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/application/test_commands.py b/application/test_commands.py index 30ec63c..4dfa69a 100644 --- a/application/test_commands.py +++ b/application/test_commands.py @@ -1,5 +1,3 @@ - - # def test_will_handle_ping_command(): # bus = CommandBus() # light = Light() @@ -8,16 +6,17 @@ # assert result.status == CommandResultStatus.OK - from application.commands import AddItemCommand + def test_valid_add_item_command(): - command = AddItemCommand({ 'title': 'Fluffy dragon' }) - assert command.is_valid() == True + command = AddItemCommand({'seller_id': 'some_id_here', 'title': 'Fluffy dragon'}) + assert command.is_valid() is True + def test_add_item_command_title_is_required(): - command = AddItemCommand({ 'description': 'Fluffy dragon' }) - assert command.is_valid() == False + command = AddItemCommand({'seller_id': 'some_id_here', 'description': 'Fluffy dragon'}) + assert command.is_valid() is False # def test_add_item_command_will_return_errors(): # command = AddItemCommand({ 'description': 'Fluffy dragon' }) From ecdbc97080b3c8b51ca106b5c7916c38c56f3b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 10:26:00 +0100 Subject: [PATCH 36/44] More tests for commands --- application/test_commands.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/application/test_commands.py b/application/test_commands.py index 4dfa69a..37cc590 100644 --- a/application/test_commands.py +++ b/application/test_commands.py @@ -1,10 +1,4 @@ -# def test_will_handle_ping_command(): -# bus = CommandBus() -# light = Light() -# command = LightOnCommand(light) -# result = bus.execute(command) -# assert result.status == CommandResultStatus.OK - +from schematics.exceptions import DataError from application.commands import AddItemCommand @@ -18,6 +12,15 @@ def test_add_item_command_title_is_required(): command = AddItemCommand({'seller_id': 'some_id_here', 'description': 'Fluffy dragon'}) assert command.is_valid() is False -# def test_add_item_command_will_return_errors(): -# command = AddItemCommand({ 'description': 'Fluffy dragon' }) -# assert command.get_validation_errors() == False + +def test_add_item_command_will_return_errors(): + command = AddItemCommand({'description': 'Fluffy dragon'}) + actual: DataError = command.validation_errors() + assert actual.errors['seller_id'] is not None + assert actual.errors['title'] is not None + + +def test_add_item_command_will_not_return_errors_when_command_is_valid(): + command = AddItemCommand({'seller_id': 'some_id_here', 'title': 'Fluffy dragon', 'description': 'Fluffy dragon'}) + actual: DataError = command.validation_errors() + assert actual is None From 526d40e1939b4352482f328ffc0f34df77888227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 10:32:52 +0100 Subject: [PATCH 37/44] Tests for services (hash) --- application/services.py | 2 +- application/test_services.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 application/test_services.py diff --git a/application/services.py b/application/services.py index 177e82c..6a4b423 100644 --- a/application/services.py +++ b/application/services.py @@ -1,3 +1,3 @@ class IdentityHashingService: def hash(self, value): - return value \ No newline at end of file + return value diff --git a/application/test_services.py b/application/test_services.py new file mode 100644 index 0000000..00c011c --- /dev/null +++ b/application/test_services.py @@ -0,0 +1,6 @@ +from application.services import IdentityHashingService + + +def test_hash(): + value = 'some_value' + assert IdentityHashingService().hash(value=value) == value From b46756a52344aca646c468b2ed56fae7f9d7742e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 11:13:27 +0100 Subject: [PATCH 38/44] Removed ptvsd from Pipfile packages section --- Pipfile | 1 - Pipfile.lock | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Pipfile b/Pipfile index 25bdee8..f661485 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,6 @@ pytest = "*" pytest-watch = "*" schematics = "*" dependency-injector = "*" -ptvsd = "*" [dev-packages] pylint = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 979ef0a..4464173 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f047c04e3c052bba035f3fe8c969e97cfb513c82677a54ddb7088e4a87025f33" + "sha256": "9c134e368b2649a609338c762093bf0bf0648829198bb8e915e6b2b6c01402a5" }, "pipfile-spec": 6, "requires": { @@ -212,15 +212,6 @@ ], "version": "==0.8.0" }, - "ptvsd": { - "hashes": [ - "sha256:533b3ca9a3973700d5fe6cb152cf6c69bac2839389460164c84ab1956ec992a0", - "sha256:8e6feb4d577b1a939af4b08821fd6afa6e71652d1e2ce41579d8b959b1e21d94", - "sha256:cfcde6a3de3cfa720e4f637af13deeae744f6dc6665b9bda92380885caf16ae6" - ], - "index": "pypi", - "version": "==4.2.0" - }, "py": { "hashes": [ "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", From d34f2162f86c0bbd061c53a4e6d0874bcca41da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 11:41:17 +0100 Subject: [PATCH 39/44] Tests refactor --- application/test_response.py | 53 +++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/application/test_response.py b/application/test_response.py index 5497dfb..1937bac 100644 --- a/application/test_response.py +++ b/application/test_response.py @@ -1,6 +1,6 @@ -from application.response import CustomJSONEncoder, json_response from datetime import datetime -import json + +from application.response import CustomJSONEncoder, json_response class MockObjectWithToJsonMethod: @@ -25,23 +25,14 @@ def __init__(self, title: str, description: str, value: float): self.value = value -def test_custom_json_encoder(): +def test_custom_json_encoder_for_date(): current_date = datetime.now() data = { 'date': current_date, - 'mock_object_to_json': MockObjectWithToJsonMethod('some text for to_json'), - 'mock_object_dict': MockObjectWithDictMethod('some text for __dict__'), - 'complex_json': MockComplexObjectWithDictMethod('title', 'some text for __dict__', 12.34), } expected = ( '{' - f"\"date\": \"{current_date.isoformat()+'Z'}\"" - ', ' - f"\"mock_object_to_json\": \x7b\"desc\": \"some text for to_json\"\x7d" - ', ' - f"\"mock_object_dict\": \x7b\"desc\": \"some text for __dict__\"\x7d" - ', ' - f"\"complex_json\": \x7b\"desc\": \"some text for __dict__\", \"title\": \"title\", \"value\": 12.34\x7d" + f"\"date\": \"{current_date.isoformat() + 'Z'}\"" '}' ) @@ -49,3 +40,39 @@ def test_custom_json_encoder(): json_response_value = json_response(data) assert actual == expected assert json_response_value == expected + + +def test_custom_json_encoder_for_object_with_to_json_method(): + data = { + 'mock_object_to_json': MockObjectWithToJsonMethod('some text for to_json') + } + expected = '{"mock_object_to_json": {"desc": "some text for to_json"}}' + + actual = CustomJSONEncoder().encode(data) + json_response_value = json_response(data) + assert actual == expected + assert json_response_value == expected + + +def test_custom_json_encoder_for_object_with_dict(): + data = { + 'mock_object_dict': MockObjectWithDictMethod('some text for __dict__'), + } + expected = '{"mock_object_dict": {"desc": "some text for __dict__"}}' + + actual = CustomJSONEncoder().encode(data) + json_response_value = json_response(data) + assert actual == expected + assert json_response_value == expected + + +def test_custom_json_encoder_for_complex_object_with_dict(): + data = { + 'complex_json': MockComplexObjectWithDictMethod('title', 'some text for __dict__', 12.34), + } + expected = '{"complex_json": {"desc": "some text for __dict__", "title": "title", "value": 12.34}}' + + actual = CustomJSONEncoder().encode(data) + json_response_value = json_response(data) + assert actual == expected + assert json_response_value == expected From b200bba7642fbcfbb01472958f9a141a6037cec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wolak?= Date: Fri, 7 Dec 2018 11:55:14 +0100 Subject: [PATCH 40/44] Update travis linux distro and python to the stable version --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d1af959..c30f629 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ +dist: "xenial" language: python python: - - "3.7-dev" + - "3.7" install: - pip install pipenv - pipenv sync -d From 2566037158bbc10686d3bdb8a059e85040c8a7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 12:54:46 +0100 Subject: [PATCH 41/44] README update --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 78ec98f..9c13627 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,27 @@ The goal is to implement an automatic bidding system, described here: https://ww TODO for near future: -* simple authorization (user id in request header) +*[X] simple authorization (user id in request header) -* first business use-case (up to three open items at the same time) +*[ ] first business use-case (up to three open items at the same time) -* application-level exceptions for invalid commands +*[ ] application-level exceptions for invalid commands -* TESTS!!!! + code metrics + CI/CD +*[X] TESTS!!!! +code metrics + CI/CD -* executing commands with immediate feedback +*[ ] executing commands with immediate feedback http://blog.sapiensworks.com/post/2015/07/20/CQRS-Immediate-Feedback-Web-App -* handling commands errors: application layer, business layer +*[ ] handling commands errors: application layer, business layer -* command validation +*[X] command validation https://stackoverflow.com/questions/32239353/command-validation-in-ddd-with-cqrs -* handling async commands (mediator pattern, asyncio) +*[ ] handling async commands (mediator pattern, asyncio) -* Application-level event bus, publisher/subscriber pattern +*[ ] Application-level event bus, publisher/subscriber pattern -* framework agnostic integration tests?? +*[ ] framework agnostic integration tests?? User stories: From 4fbe97e0a64a7e236d3b49dffae1fcd2443b56ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Darmofa=C5=82?= Date: Fri, 7 Dec 2018 12:55:55 +0100 Subject: [PATCH 42/44] README update --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9c13627..8866804 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,27 @@ The goal is to implement an automatic bidding system, described here: https://ww TODO for near future: -*[X] simple authorization (user id in request header) +- [x] simple authorization (user id in request header) -*[ ] first business use-case (up to three open items at the same time) +- [ ] first business use-case (up to three open items at the same time) -*[ ] application-level exceptions for invalid commands +- [ ] application-level exceptions for invalid commands -*[X] TESTS!!!! +code metrics + CI/CD +- [x] TESTS!!!! +code metrics + CI/CD -*[ ] executing commands with immediate feedback +- [ ] executing commands with immediate feedback http://blog.sapiensworks.com/post/2015/07/20/CQRS-Immediate-Feedback-Web-App -*[ ] handling commands errors: application layer, business layer +- [ ] handling commands errors: application layer, business layer -*[X] command validation +- [X] command validation https://stackoverflow.com/questions/32239353/command-validation-in-ddd-with-cqrs -*[ ] handling async commands (mediator pattern, asyncio) +- [ ] handling async commands (mediator pattern, asyncio) -*[ ] Application-level event bus, publisher/subscriber pattern +- [ ] Application-level event bus, publisher/subscriber pattern -*[ ] framework agnostic integration tests?? +- [ ] framework agnostic integration tests?? User stories: From 11f0b0f1fd483c785533b4744abb4508e5aaec83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20G=C3=B3recki?= Date: Sat, 15 Dec 2018 16:14:11 +0100 Subject: [PATCH 43/44] basic application shell --- README.md | 12 ++++++++++++ main.py | 9 ++++++--- shell.py | 13 +++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 shell.py diff --git a/README.md b/README.md index 8866804..0caebd5 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,18 @@ To run the app as Flask server FRAMEWORK=flask gunicorn --reload main ``` +To run application Shell: +``` +python shell.py +``` + +Within the shell you can execute and queries and commands, i.e.: +``` +(InteractiveConsole) +>>> c = AddItemCommand({'title': 'Fluffy bunny'}) +>>> command_bus.execute(c) +(ok) {} +``` Project structure: diff --git a/main.py b/main.py index 33f7b3b..8d6b260 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,11 @@ import os -# TODO: import conditionally, when ENVIRONMENT is in DEBUG mode -import ptvsd -ptvsd.enable_attach(address=('0.0.0.0', 3000)) +try: + # try importing Python debugger package for use with Visual Studio Code + import ptvsd + ptvsd.enable_attach(address=('0.0.0.0', 3000)) +except: + print('ptvsd disabled') framework = os.environ.get('FRAMEWORK', 'falcon') print('Running {} app'.format(framework)) diff --git a/shell.py b/shell.py new file mode 100644 index 0000000..bfeaacf --- /dev/null +++ b/shell.py @@ -0,0 +1,13 @@ +import readline # optional, will allow Up/Down/History in the console +import code +import application +import domain +from composition_root import CommandBusContainer +from application.commands import * + +variables = globals().copy() +variables.update({ + 'command_bus': CommandBusContainer.command_bus_factory(), +}) +shell = code.InteractiveConsole(variables) +shell.interact() \ No newline at end of file From af67f35b9f830437b53f0cf3a5baa34896752c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Sopy=C5=82a?= Date: Wed, 26 May 2021 09:21:23 +0200 Subject: [PATCH 44/44] Add about --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0caebd5..5a52f14 100644 --- a/README.md +++ b/README.md @@ -142,4 +142,14 @@ References: * Command design pattern: https://www.youtube.com/watch?v=9qA5kw8dcSU -* https://skillsmatter.com/skillscasts/5025-domain-driven-design-with-python(python-ddd) \ No newline at end of file +* https://skillsmatter.com/skillscasts/5025-domain-driven-design-with-python(python-ddd) + + + +## About Ermlab Software + +__Ermlab__ - Polish python and machine learning company + +:owl: [Website](https://ermlab.com/en/?utm_source=github&utm_medium=readme&utm_campaign=python-ddd) | :octocat: [Repository](https://github.com/ermlab) + +.