diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..4c646bc --- /dev/null +++ b/.coveragerc @@ -0,0 +1,34 @@ +# .coveragerc to control coverage.py +[run] +branch = True +source= + application/ + domain/ + infrastructure/ +omit=*/test_* + +[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/.editorconfig b/.editorconfig deleted file mode 100644 index c1322dc..0000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = false \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml deleted file mode 100644 index cc813a1..0000000 --- a/.github/workflows/pytest.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Run tests - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:latest - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: password - POSTGRES_DB: test_db - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install Poetry - run: | - pip install poetry - poetry self update - - name: Install dependencies - run: poetry install - - name: Run all tests - run: | - poetry run pytest src - env: - # The hostname used to communicate with the PostgreSQL service container - DATABASE_URL: postgresql://postgres:password@localhost:5432/test_db diff --git a/.gitignore b/.gitignore index 76d0350..29c4057 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,107 @@ -__pycache__ .vscode -src-gen/ + +# 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 +cov.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 -htmlcov/ -logs.json -tmp/ -.idea/ +.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/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 22e1b87..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,25 +0,0 @@ -default_language_version: - python: python3.11 - -repos: - # Only for removing unused imports > Other staff done by Black - - repo: https://github.com/myint/autoflake - rev: "v1.4" # Version to check - hooks: - - id: autoflake - args: - - --in-place - - --remove-all-unused-imports - - --ignore-init-module-imports - - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) - args: ["--profile", "black"] - - - repo: https://github.com/ambv/black - rev: 22.3.0 - hooks: - - id: black diff --git a/.python-version b/.python-version deleted file mode 100644 index 2c07333..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c30f629 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +dist: "xenial" +language: python +python: + - "3.7" +install: + - pip install pipenv + - pipenv sync -d +script: 'pytest --cov' +after_success: 'codecov' \ No newline at end of file diff --git a/LICENSE b/LICENSE index abacc49..3a8d16d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Ermlab sp. z o. o. +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 @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..f661485 --- /dev/null +++ b/Pipfile @@ -0,0 +1,26 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +falcon = "*" +gunicorn = "*" +pylint = "*" +pytest = "*" +pytest-watch = "*" +schematics = "*" +dependency-injector = "*" + +[dev-packages] +pylint = "*" +ptvsd = "*" +pre-commit = "*" +autopep8 = "*" +pytest-cov = "*" +pytest = "*" +codecov = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..4464173 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,615 @@ +{ + "_meta": { + "hash": { + "sha256": "9c134e368b2649a609338c762093bf0bf0648829198bb8e915e6b2b6c01402a5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "argh": { + "hashes": [ + "sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3", + "sha256:e9535b8c84dc9571a48999094fda7f33e63c3f1b74f3e5f3ac0105a58405bb65" + ], + "version": "==0.26.2" + }, + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "colorama": { + "hashes": [ + "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", + "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" + ], + "version": "==0.4.1" + }, + "dependency-injector": { + "hashes": [ + "sha256:f478a26e9bf3111ce98bbfb8502af274643947f87a7e12a6481a35eaa693062b" + ], + "index": "pypi", + "version": "==3.14.2" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "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" + }, + "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:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "pytest": { + "hashes": [ + "sha256:1d131cc532be0023ef8ae265e2a779938d0619bb6c2510f52987ffcba7fa1ee4", + "sha256:ca4761407f1acc85ffd1609f464ca20bb71a767803505bd4127d0e45c5a50e23" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "pytest-watch": { + "hashes": [ + "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9" + ], + "index": "pypi", + "version": "==4.2.0" + }, + "python-mimeparse": { + "hashes": [ + "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", + "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282" + ], + "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" + }, + "schematics": { + "hashes": [ + "sha256:8fcc6182606fd0b24410a1dbb066d9bbddbe8da9c9509f47b743495706239283", + "sha256:a40b20635c0e43d18d3aff76220f6cd95ea4decb3f37765e49529b17d81b0439" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "watchdog": { + "hashes": [ + "sha256:965f658d0732de3188211932aeb0bb457587f04f63ab4c1e33eab878e9de961d" + ], + "version": "==0.9.0" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + }, + "develop": { + "aspy.yaml": { + "hashes": [ + "sha256:04d26279513618f1024e1aba46471db870b3b33aef204c2d09bcf93bea9ba13f", + "sha256:0a77e23fafe7b242068ffc0252cee130d3e509040908fc678d9d1060e7494baa" + ], + "version": "==1.1.1" + }, + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "atomicwrites": { + "hashes": [ + "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", + "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" + ], + "version": "==1.2.1" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "cached-property": { + "hashes": [ + "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", + "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" + ], + "version": "==1.5.1" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "cfgv": { + "hashes": [ + "sha256:73f48a752bd7aab103c4b882d6596c6360b7aa63b34073dd2c35c7b4b8f93010", + "sha256:d1791caa9ff5c0c7bce80e7ecc1921752a2eb7c2463a08ed9b6c96b85a2f75aa" + ], + "version": "==1.1.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "codecov": { + "hashes": [ + "sha256:8ed8b7c6791010d359baed66f84f061bba5bd41174bf324c31311e8737602788", + "sha256:ae00d68e18d8a20e9c3288ba3875ae03db3a8e892115bf9b83ef20507732bed4" + ], + "index": "pypi", + "version": "==2.0.15" + }, + "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" + }, + "identify": { + "hashes": [ + "sha256:5e956558a9a1e3b3891d7c6609fc9709657a11878af288ace484d1a46a93922b", + "sha256:623086059219cc7b86c77a3891f3700cb175d4ce02b8fb8802b047301d71e783" + ], + "version": "==1.1.7" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "importlib-metadata": { + "hashes": [ + "sha256:28fba9f65e5415a691dd254cdb602bcc4d6f738e68407ad251651db358b63bcf", + "sha256:4a545e6125dc72b4ad98201ea3f40f92e8126e3a19667352b3a134d22b8bc74f" + ], + "version": "==0.7" + }, + "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" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "pluggy": { + "hashes": [ + "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", + "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" + ], + "version": "==0.8.0" + }, + "pre-commit": { + "hashes": [ + "sha256:7542bd8ae1c58745175ea0a9295964ee82a10f7e18c4344f5e4c02bd85d02561", + "sha256:87f687da6a2651d5067cfec95b854b004e95b70143cbf2369604bb3acbce25ec" + ], + "index": "pypi", + "version": "==1.12.0" + }, + "ptvsd": { + "hashes": [ + "sha256:533b3ca9a3973700d5fe6cb152cf6c69bac2839389460164c84ab1956ec992a0", + "sha256:8e6feb4d577b1a939af4b08821fd6afa6e71652d1e2ce41579d8b959b1e21d94", + "sha256:cfcde6a3de3cfa720e4f637af13deeae744f6dc6665b9bda92380885caf16ae6" + ], + "index": "pypi", + "version": "==4.2.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "version": "==1.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "version": "==2.4.0" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "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" + }, + "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" + }, + "requests": { + "hashes": [ + "sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54", + "sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263" + ], + "version": "==2.20.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "virtualenv": { + "hashes": [ + "sha256:686176c23a538ecc56d27ed9d5217abd34644823d6391cbeb232f42bf722baad", + "sha256:f899fafcd92e1150f40c8215328be38ff24b519cd95357fa6e78e006c7638208" + ], + "version": "==16.1.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} diff --git a/README.md b/README.md index 6c0e409..5a52f14 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,155 @@ -# Python Domain Driven Design (DDD) example project - -[![Build Status](https://github.com/pgorecki/python-ddd/actions/workflows/pytest.yml/badge.svg?branch=main)](https://github.com/pgorecki/python-ddd/actions/workflows/pytest.yml) -[![codecov](https://codecov.io/gh/pgorecki/python-ddd/branch/master/graph/badge.svg)](https://codecov.io/gh/pgorecki/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) -Disclaimer: this is a work in progress project, stay tuned for updates (*). +AUCTION APPLICATION -(*) *This project is is accompanied by my blog https://dddinpython.com/ where I introduce some of the concepts and patterns implemented this repository.* +The goal is to implement an automatic bidding system, described here: https://www.ebay.co.uk/pages/help/buy/bidding-overview.html -## The goal of this project -AUCTION APPLICATION +TODO for near future: + +- [x] simple authorization (user id in request header) + +- [ ] first business use-case (up to three open items at the same time) + +- [ ] application-level exceptions for invalid commands + +- [x] 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 + +- [X] command validation + https://stackoverflow.com/questions/32239353/command-validation-in-ddd-with-cqrs + +- [ ] handling async commands (mediator pattern, asyncio) + +- [ ] Application-level event bus, publisher/subscriber pattern + +- [ ] framework agnostic integration tests?? + + +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, 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 -The goal is to implement an automatic bidding system using DDD tactical patterns, -described here: https://www.ebay.co.uk/pages/help/buy/bidding-overview.html +* As a bidder, when placing a bid, my bid must be higher than the actual price -## Domain +* Auction Store will automatically calculate the current price of an item based on the bids that were made -Online Auctions domain was selected for the purpose of this project, which is loosely based on Ebay bidding system. +* When auction ends, auction store will notify the seller by email. The email will contain the name of the winner and the sell price. -The main reason for selecting this domain is general familiarity and limited complexity - many people understand how internet bidding work and the whole concept is not difficult to understand (or at least it's much simpler that healthcare or banking domains). On the other hand it's not simply a CRUD system - there are some business rules that must be implemented in the system. +* When auction ends, all losing participants will recieve an email with the information that they lost an auction. -### Domain description +* When auction ends, the winning participant will reciewve an email with information the user has won and the price for an item. -This is a summary of the information we got from a domain expert about how the auction system should work. -#### Selling -When you want to sell an item, the first step in getting your item in front of buyers is creating a `Listing`. For now we only focus on selling through auctions (https://www.ebay.com/help/selling/listings/auction-format?id=4110), but in the future we may consider selling with "Buy It Now" (https://www.ebay.com/help/selling/listings/selling-buy-now?id=4109) or in some other ways. When presenting an `Listing` for sale (which we call `publishing` in the Catalog), seller must provide a `Listing` duration and initial price. Also it is also possible to schedule a `Listing` publication (https://www.ebay.com/help/selling/listings/selecting-listing-duration?id=4652). -You can cancel your listing when bidding takes place, but only under certain circumstances: if time left in listing < 12 hrs you can cancel your listing only if no bids were places (but we might change it in the future). +``` +pipenv install +pipenv shell +``` -If you are a new seller (you never sold an item before), you can list only one `Item` in the `Catalog` at a time. +To run tests +``` +pytest +``` -#### Buying +To run tests in watch mode +``` +ptw +``` -When `Listing` is selled through auction `Bidding` takes place. As a `Buyer`, you can place a bid, which must be greated than the current price + 1 USD and which sets the the highest price you are willing to pay for an item. System will let you know if someone outbids you and you can decide if you want to increase your maximum limit. Sometimes you can can be automatically outbidded (if some other buyer sets his maximum limit higher that yours) - see https://www.ebay.com/help/buying/bidding/automatic-bidding?id=4014. +To run the app as Falcon server +``` +FRAMEWORK=falcon gunicorn --reload main +``` -After a fixed time since the bidding was started a bidding ends and the `Winner` is announced. +To run the app as Flask server +``` +FRAMEWORK=flask gunicorn --reload main +``` -#### Payments +To run application Shell: +``` +python shell.py +``` -At this point, payments are out of scope for this project. +Within the shell you can execute and queries and commands, i.e.: +``` +(InteractiveConsole) +>>> c = AddItemCommand({'title': 'Fluffy bunny'}) +>>> command_bus.execute(c) +(ok) {} +``` -#### Users +Project structure: -Each user can be a `Seller` or a `Buyer` (or both). User priveledges can be elevated to a `Staff member` or `Administrator`. +``` +context-1 + domain + entities + value_objects + aggregates + services + repositories + factories? + interfaces + events + application + services + infrastructure + ?? + tests?? +context-2 -## Event storming +context-3 -Event storming technique was used to discover the business domain and the most important business processes. +di_setup +main - main entrypoint to the app +``` -### Listing draft management +Domain artifacts -![](docs/images/draft_management.png) +* entities - mutable, identifiable, unaware of persistance -### Publishing Listing to Catalog +* value objects - immutable, self-contained -![](docs/images/publishing_to_catalog.png) +* 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 -### Bidding process +* events - significant state transition, something which domain experts care about -![](docs/images/bidding_process.png) +* factories - for entity construction, ubiquotous language verbs, hide construction details (Python function) +* repositories - store aggregates, abstraction over persistence mehanism -## Context Map +* context maps - mappings between concepts between bounded contexts -`Lising`, `Bidding` and `Payment` bounded contexts were identified as a result of event storming. Relationship between these bounded contexts is presented in the following context map. +Installing Python 3.7 from source on Ubuntu 18.04: https://gist.github.com/jerblack/798718c1910ccdd4ede92481229043be -![](docs/images/auctions_ContextMap.png) +References: -Since `Payment` context will be provided by a 3rd party payments provider (via REST API), the downstream context (`Bidding`) must conform to whatever the upstream provides. +* Command design pattern: https://www.youtube.com/watch?v=9qA5kw8dcSU +* https://skillsmatter.com/skillscasts/5025-domain-driven-design-with-python(python-ddd) -## How to run this project -`poetry shell` -`poetry install` +## About Ermlab Software -`poe compose_up` - run in a separate shell to start the database +__Ermlab__ - Polish python and machine learning company -`poe start` +:owl: [Website](https://ermlab.com/en/?utm_source=github&utm_medium=readme&utm_campaign=python-ddd) | :octocat: [Repository](https://github.com/ermlab) -`poe test` +. diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index 4551056..0000000 --- a/alembic.ini +++ /dev/null @@ -1,100 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = migrations - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to migrations/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" -# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. Valid values are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # default: use os.pathsep - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = %(DATABASE_URL) - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/src/api/models/__init__.py b/application/__init__.py similarity index 100% rename from src/api/models/__init__.py rename to application/__init__.py diff --git a/application/command_bus.py b/application/command_bus.py new file mode 100644 index 0000000..3af0669 --- /dev/null +++ b/application/command_bus.py @@ -0,0 +1,22 @@ +from application.commands import Command, CommandResult + +class CommandBus(object): + """ + 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_factory): + self._command_handler_factory = command_handler_factory + + 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: + # 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 new file mode 100644 index 0000000..dacbcfc --- /dev/null +++ b/application/command_handlers.py @@ -0,0 +1,16 @@ +from application.commands import AddItemCommand, CommandResult, ResultStatus + + +class AddItemCommandHandler(object): + def __init__(self, items_repository): # TODO: interface + 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, + starting_price=command.starting_price, + end_date=command.end_date + ) + return CommandResult(ResultStatus.OK) diff --git a/application/commands.py b/application/commands.py new file mode 100644 index 0000000..f74768c --- /dev/null +++ b/application/commands.py @@ -0,0 +1,52 @@ +from enum import Enum + +from schematics.exceptions import DataError +from schematics.models import Model +from schematics.types import StringType, DateTimeType, DecimalType + + +class ResultStatus(str, Enum): + OK = 'ok' + PENDING = 'pending' + 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() + except DataError: + 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() + end_date = DateTimeType() diff --git a/src/api/infrastructure/database.py b/application/exceptions.py similarity index 100% rename from src/api/infrastructure/database.py rename to application/exceptions.py 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/queries.py b/application/queries.py new file mode 100644 index 0000000..a26684e --- /dev/null +++ b/application/queries.py @@ -0,0 +1,38 @@ +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): # TODO: Type hint + self.data = data + self.status = status + + def __repr__(self): + return '<{}>({}) {}'.format(type(self).__name__, self.status, self.data) + +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..6d4dc56 --- /dev/null +++ b/application/response.py @@ -0,0 +1,33 @@ +import inspect +import json +from datetime import datetime + + +# TODO: refactor, move JSONEncoder to composition_root +class CustomJSONEncoder(json.JSONEncoder): + # REF: https://stackoverflow.com/a/35483750 + def default(self, obj): + if isinstance(obj, datetime): + 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 json_response(obj): + return json.dumps(obj, cls=CustomJSONEncoder) diff --git a/application/services.py b/application/services.py new file mode 100644 index 0000000..6a4b423 --- /dev/null +++ b/application/services.py @@ -0,0 +1,3 @@ +class IdentityHashingService: + def hash(self, value): + return value diff --git a/application/settings.py b/application/settings.py new file mode 100644 index 0000000..bb00074 --- /dev/null +++ b/application/settings.py @@ -0,0 +1 @@ +APPLICATION_NAME = 'foo:app' \ 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..e4bc078 --- /dev/null +++ b/application/test_command_bus.py @@ -0,0 +1,41 @@ +import dependency_injector.containers as containers +import dependency_injector.providers as providers + +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 MockAuctionItemsRepository: + def add(*args, **kwargs): + pass + + +class OverriddenCommandBusContainer(CommandBusContainer): + items_repository = providers.Singleton(MockAuctionItemsRepository) + + 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) + ) + + +def test_command_bus_will_dispatch_command(): + # Arrange + bus = OverriddenCommandBusContainer.command_bus_factory() + command = AddItemCommand() + + # Act + result = bus.execute(command) + + # Assert + assert result.status == ResultStatus.OK diff --git a/application/test_commands.py b/application/test_commands.py new file mode 100644 index 0000000..37cc590 --- /dev/null +++ b/application/test_commands.py @@ -0,0 +1,26 @@ +from schematics.exceptions import DataError + +from application.commands import AddItemCommand + + +def test_valid_add_item_command(): + 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({'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'}) + 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 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/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/application/test_response.py b/application/test_response.py new file mode 100644 index 0000000..1937bac --- /dev/null +++ b/application/test_response.py @@ -0,0 +1,78 @@ +from datetime import datetime + +from application.response import CustomJSONEncoder, json_response + + +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_for_date(): + current_date = datetime.now() + data = { + 'date': current_date, + } + expected = ( + '{' + f"\"date\": \"{current_date.isoformat() + 'Z'}\"" + '}' + ) + + 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_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 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 diff --git a/composition_root.py b/composition_root.py new file mode 100644 index 0000000..eb616d6 --- /dev/null +++ b/composition_root.py @@ -0,0 +1,74 @@ +import dependency_injector.containers as containers +import dependency_injector.providers as providers + +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 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(): + 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 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) + + 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 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, + query_bus=QueryBusContainer.query_bus_factory, + authentication_service=BaseContainer.authentication_service_factory, + ) + info_controller_factory = providers.Factory(InfoController, + authentication_service=BaseContainer.authentication_service_factory + ) diff --git a/diary.md b/diary.md deleted file mode 100644 index 9b13cc9..0000000 --- a/diary.md +++ /dev/null @@ -1,69 +0,0 @@ -## Seedwork - - -## Architecture of Catalog module (2021-05-25) - -What kind of architecture should we choose for the *Catalog* module? Should we use CQRS or maybe something simpler and easier to implement? Let's see what others says about it? - -> CQRS stands for Command Query Responsibility Segregation. It's a pattern that I first heard described by Greg Young. At its heart is the notion that you can use a different model to update information than the model you use to read information. For some situations, this separation can be valuable, but beware that for most systems CQRS adds risky complexity. [1] - -Since *Catalog* module is mostly about managing list items, it will likely contain a lot of CRUD-like functionality. We don't need separate read and writes models in this case, it seems like an overkill to me. So having full CQRS is not worth it. However, sticking to separation of commands and queries still looks tempting. Let's check out the alternatives: - -1. "The typical entry point for this in DDD is an Application Service. Application services orchestrate calls to repositories and domain objects" [2][3]. - - -Maybe we could implementing using a generic CRUD handler? - - -## Handling commands - -The typical way of reading the system state via queries and changing the system state is via comands. The high-level route controller code could look like: - -``` -def get_route_controller(request, module): - module.execute_query(MyQuery( - foo=request.GET.foo, - bar=request.GET.bar, - )) - return Response(HTTP_200_OK) - - -def post_route_controller(request, module): - result = module.execute(MyCommand( - foo=request.POST.foo, - bar=request.POST.bar, - )) - return Response(HTTP_200_OK) -``` - -In this case `execute_command` is responsible for passing a command to an appriopriate command handler. - - -Keep in mind this is a happy path and bad things can happen along a way. In particular: -- creating command can fail: i.e. command can have incorrect params, or some params can be missing (command param validation) [400] -- command execution can fail due to numerous reasons: - - an object that we want act upon may not exist [404] - - a user may not have permissions to perform certain command [403] - - business rule may fail [400] - - application level policy may fail, i.e. too many requests issued by the same user [429] - - .... - - - -For example, query or command can be invalid, an , user may - - -References: - -- [1] https://martinfowler.com/bliki/CQRS.html -- [2] https://softwareengineering.stackexchange.com/questions/302187/crud-operations-in-ddd -- [3] https://lostechies.com/jimmybogard/2008/08/21/services-in-domain-driven-design/ - - - - - - -## Other references - -- https://github.com/VaughnVernon/IDDD_Samples/tree/master \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 660f412..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: "3.8" -services: - db: - image: postgres:12.3 - environment: - POSTGRES_PASSWORD: password - volumes: - - ./tmp/db/data:/var/lib/postgresql/data - ports: - - "5432:5432" - networks: - - internal-network - - external-network - test_db: - image: postgres:12.3 - environment: - POSTGRES_PASSWORD: password - volumes: - - ./tmp/test_db/data:/var/lib/postgresql/data - ports: - - "5433:5432" - networks: - - internal-network - - external-network - -networks: - internal-network: - internal: true - external-network: - diff --git a/docs/architecture_decision_log/001_initial.md b/docs/architecture_decision_log/001_initial.md deleted file mode 100644 index 996bc27..0000000 --- a/docs/architecture_decision_log/001_initial.md +++ /dev/null @@ -1,21 +0,0 @@ -# 1. Record architecture decisions - -Date: 2021-05-25 - -## Status - -Accepted - -## Context - -As the project is an example of a more advanced monolith architecture, it is necessary to save all architectural decisions in one place. - -## Decision - -For all architectural decisions Architecture Decision Log (ADL) is created. All decisions will be recorded as Architecture Decision Records (ADR). - -Each ADR will be recorded using [Michael Nygard template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions), which contains following sections: Status, Context, Decision and Consequences. - -## Consequences - -All architectural decisions should be recorded in log. Old decisions should be recorded as well with an approximate decision date. New decisions should be recorded on a regular basis. \ No newline at end of file diff --git a/docs/architecture_decision_log/002_use_modular_monolith.md b/docs/architecture_decision_log/002_use_modular_monolith.md deleted file mode 100644 index 30069f8..0000000 --- a/docs/architecture_decision_log/002_use_modular_monolith.md +++ /dev/null @@ -1,22 +0,0 @@ -# 2. Use Modular Monolith System Architecture - -Date: 2021-05-25 - -## Status - -Accepted - -## Context - -An example of Modular Monolith architecture and tactical DDD implementation in Python is missing on the internet. - -## Decision - -We decided to create nontrivial application using Modular Monolith architecture and Domain-Driven Design tactical patterns. - -## Consequences - -- All modules must run in one single process as single application (Monolith) -- All modules should have maximum autonomy (Modular) -- DDD Bounded Contexts will be used to divide monolith into modules -- DDD tactical patterns will be used to implement most of modules \ No newline at end of file diff --git a/docs/architecture_decision_log/003_use_python.md b/docs/architecture_decision_log/003_use_python.md deleted file mode 100644 index 0ede8bf..0000000 --- a/docs/architecture_decision_log/003_use_python.md +++ /dev/null @@ -1,19 +0,0 @@ -# 3. Use .NET Core and C# language - -Date: 2021-05-25 - -## Status - -Accepted - -## Context - -As it is monolith, only one language must be selected for implementation. - -## Decision - -We decided to use Python 3.9 as it is the newest stable Python release at the moment of writing this document. The choice of web framework is deferred at this point. - -## Consequences - -- Whole application will be implemented in Python \ No newline at end of file diff --git a/docs/architecture_decision_log/004_divide_system_into_2_modules copy.md b/docs/architecture_decision_log/004_divide_system_into_2_modules copy.md deleted file mode 100644 index 3c442a4..0000000 --- a/docs/architecture_decision_log/004_divide_system_into_2_modules copy.md +++ /dev/null @@ -1,35 +0,0 @@ -# 4. Divide the system into 4 modules - -Date: 2021-05-25 - -## Status - -Accepted - -## Context - -The "Auction" domain contains 3 main subdomains: Bidding (core domain), Catalog (supporting subdomain), and User Access (generic domain). - -Since we decided to use Modular Monolith, all subdomains should be implemented as modules within a single system. - -## Possible solutions -1. Create one "Auction" module and divide it into sub-modules. This solution is simpler to implement at the beginning. We do not have to set module boundaries and think how to communicate between them. On the other hand, this causes a lack of autonomy and can lead to Big Ball Of Mud anti-pattern. -2. Create 3 modules based on Bounded Contexts which in this scenario maps 1:1 to domains. This solution is more difficult at the beginning. We need to set modules boundaries, communication strategy between modules and have more advanced infrastructure code. It is a more complex solution. On the other hand, it supports autonomy, maintainability, readability. We can develop our Domain Models in all of the Bounded Contexts independently. - -## Decision - -Solution 2. - -We created 3 modules: Bidding, Catalog, User Access. The key factor here is module autonomy and maintainability. We want to develop each module independently. This is more cleaner solution. It involves more work at the beginning but we are ready to pay this price. - -## Consequences -- We can implement each module/Bounded Context independently. -- We need to set clear boundaries between modules and communication strategy between modules (and implement them) -- We need to define the API of each module -- The API/GUI layer needs to know about all of the modules -- We need to create shared libraries/classes to limit boilerplate code which will be the same in all modules -- Complexity of the whole solution will increase -- Complexity of each module will decrease -- We will have clear separation of concerns -- In addition to the application, we must divide the data -- We can delegate development of particular module to defined team, work should be done without any conflicts on codebase \ No newline at end of file diff --git a/docs/architecture_decision_log/005_separate_commands_and_queries.md b/docs/architecture_decision_log/005_separate_commands_and_queries.md deleted file mode 100644 index 1633ff4..0000000 --- a/docs/architecture_decision_log/005_separate_commands_and_queries.md +++ /dev/null @@ -1,33 +0,0 @@ -# 4. Separate commands and queries - -Date: 2021-05-25 - -## Status - -Accepted - -## Context - -We want to keep controllers as thin as possible. Therefore each controller should have access to a module, which exposes an interface allowing to read system state using `queries` and change system state using `commands`. However, keep in mind that this does not imply having separate read and write models (as in pure CQRS) - it's up to a module architecture if same or different models are needed. - -``` -def get_route_controller(request, module): - module.execute_query(MyQuery( - foo=request.GET.foo, - bar=request.GET.bar, - )) - return Response(HTTP_200_OK) - - -def post_route_controller(request, module): - result = module.execute(MyCommand( - foo=request.POST.foo, - bar=request.POST.bar, - )) - return Response(HTTP_200_OK) -``` - - -## Consequences -- controllers are thin -- each module must implement `execute_query` and `execute_command` methods \ No newline at end of file diff --git a/docs/architecture_decision_log/006_error_handling_during_operation_execution.md b/docs/architecture_decision_log/006_error_handling_during_operation_execution.md deleted file mode 100644 index 1267ace..0000000 --- a/docs/architecture_decision_log/006_error_handling_during_operation_execution.md +++ /dev/null @@ -1,37 +0,0 @@ -# 4. Error handling during operation execution - -Date: 2021-05-25 - -## Status - -Pending - -## Context - -When executing an operation (command or query), many bad things can happen. This can be related to access permission, data validation, business rule validation, infrastructure errors, etc. We need to collect and handle such errors in an unified way for further processing (API responses, UI notifications for a user). The solution should also support a way to convert errors into meaningful messages for an end user (including language translations). - -## Possible solutions -1. Raise an exception during operation processing. This solutioun relies on throwing an exception during command execution and handling it somewhere upper in the call stack. - -Pros: -- can throw exception from anywhere - -Cons: -- non-linear program flow can become a mess really quickly because it’s hard to trace all existing connections between throw and catch statements. - -2. Explicitly return values indicating success or failure of an operation instead of throwing exceptions using a `Result` class. Expected erros should be reported to a `Result` object. Unexpected errors should be handled using exceptions. - -Pros: -- can return multiple errors within one operation - -Cons: -- ... - -## Decision - -TBA - - -## Consequences - -.... \ No newline at end of file diff --git a/docs/auctions.cml b/docs/auctions.cml deleted file mode 100644 index d9ae6a1..0000000 --- a/docs/auctions.cml +++ /dev/null @@ -1,36 +0,0 @@ -ContextMap OnlineAuctionsContextMap { - type = SYSTEM_LANDSCAPE - state = TO_BE - contains ListingContext - contains BiddingContext - contains PaymentContext - - BiddingContext [SK]<->[SK] ListingContext - - BiddingContext [D, CF] <- PaymentContext -} - -BoundedContext ListingContext implements ListingDomain - -BoundedContext BiddingContext implements BiddingDomain - -BoundedContext PaymentContext implements PaymentDomain - -Domain OnlineAuctionsDomain { - Subdomain ListingDomain { - type = SUPPORTING_DOMAIN - domainVisionStatement = "Subdomain managing all listings of items for sale" - } - - Subdomain BiddingDomain { - type = CORE_DOMAIN - domainVisionStatement = "Subdomain managing ongoing bidding" - } - - Subdomain PaymentDomain { - type = GENERIC_SUBDOMAIN - domainVisionStatement = "Subdomain managing payments" - } - -} - diff --git a/docs/example.cml b/docs/example.cml deleted file mode 100644 index fc14701..0000000 --- a/docs/example.cml +++ /dev/null @@ -1,70 +0,0 @@ -/* Example Context Map written with 'ContextMapper DSL' */ -ContextMap InsuranceContextMap { - type = SYSTEM_LANDSCAPE - state = TO_BE - /* Add bounded contexts to this context map: */ - contains CustomerManagementContext - contains CustomerSelfServiceContext - contains PrintingContext - contains PolicyManagementContext - contains RiskManagementContext - contains DebtCollection - - /* Define the context relationships: */ - CustomerSelfServiceContext <- CustomerManagementContext - - CustomerManagementContext <- PrintingContext - - PrintingContext -> PolicyManagementContext - - RiskManagementContext <-> PolicyManagementContext - - PolicyManagementContext <- CustomerManagementContext - - DebtCollection <- PrintingContext - - PolicyManagementContext <-> DebtCollection -} - -/* Bounded Context Definitions */ -BoundedContext CustomerManagementContext implements CustomerManagementDomain - -BoundedContext CustomerSelfServiceContext implements CustomerManagementDomain - -BoundedContext PrintingContext implements PrintingDomain - -BoundedContext PolicyManagementContext implements PolicyManagementDomain - -BoundedContext RiskManagementContext implements RiskManagementDomain - -BoundedContext DebtCollection implements DebtsDomain - -/* Domain & Subdomain Definitions */ -Domain InsuranceDomain { - Subdomain CustomerManagementDomain { - type = CORE_DOMAIN - domainVisionStatement = "Subdomain managing everything customer-related." - } - - Subdomain PolicyManagementDomain { - type = CORE_DOMAIN - domainVisionStatement = "Subdomain managing contracts and policies." - } - - Subdomain PrintingDomain { - type = SUPPORTING_DOMAIN - domainVisionStatement = "Service (external system) to solve printing for all kinds of documents (debts, policies, etc.)" - } - - Subdomain RiskManagementDomain { - type = GENERIC_SUBDOMAIN - domainVisionStatement = "Subdomain supporting everything which relates to risk management." - } - - Subdomain DebtsDomain { - type = GENERIC_SUBDOMAIN - domainVisionStatement = "Subomain including everything related to the incoming money (debts, dunning, etc.)" - } - -} - diff --git a/docs/images/auctions_ContextMap.png b/docs/images/auctions_ContextMap.png deleted file mode 100644 index 8701325..0000000 Binary files a/docs/images/auctions_ContextMap.png and /dev/null differ diff --git a/docs/images/bidding_process.png b/docs/images/bidding_process.png deleted file mode 100644 index e10d484..0000000 Binary files a/docs/images/bidding_process.png and /dev/null differ diff --git a/docs/images/draft_management.png b/docs/images/draft_management.png deleted file mode 100644 index 65cdf16..0000000 Binary files a/docs/images/draft_management.png and /dev/null differ diff --git a/docs/images/publishing_to_catalog.png b/docs/images/publishing_to_catalog.png deleted file mode 100644 index 02f42f2..0000000 Binary files a/docs/images/publishing_to_catalog.png and /dev/null differ diff --git a/src/config/__init__.py b/domain/__init__.py similarity index 100% rename from src/config/__init__.py rename to domain/__init__.py diff --git a/domain/entities.py b/domain/entities.py new file mode 100644 index 0000000..fc1f360 --- /dev/null +++ b/domain/entities.py @@ -0,0 +1,10 @@ +from domain.value_objects import Currency + + +class AuctionItem: + def __init__(self, id: str, title: str, description: str, current_price: Currency, start_date, end_date): + self.id = id + self.title = title + self.description = description + self.current_price = current_price + self.end_date = end_date diff --git a/domain/test_entities.py b/domain/test_entities.py new file mode 100644 index 0000000..16e34c4 --- /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', + 'current_price': Currency(10.00), + 'start_date': datetime.now(), + 'end_date': datetime.now(), + } + auction_item = AuctionItem(id=data['id'], title=data['title'], description=data['description'], + current_price=data['current_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['current_price'] + assert auction_item.end_date == data['end_date'] + + +def test_auction_item_without_parameters_raises_exception(): + with pytest.raises(TypeError): + AuctionItem() diff --git a/domain/test_value_objects.py b/domain/test_value_objects.py new file mode 100644 index 0000000..e5bdffb --- /dev/null +++ b/domain/test_value_objects.py @@ -0,0 +1,25 @@ +from domain.value_objects import Currency + +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 new file mode 100644 index 0000000..e7fa3d1 --- /dev/null +++ b/domain/value_objects.py @@ -0,0 +1,25 @@ +class Currency: + def __init__(self, amount: float): + self._amount = round(amount,2) + + @property + def amount(self): + 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) + + def __str__(self): + return '{:.2f}'.format(self.amount) \ No newline at end of file diff --git a/src/modules/bidding/__init__.py b/infrastructure/__init__.py similarity index 100% rename from src/modules/bidding/__init__.py rename to infrastructure/__init__.py diff --git a/src/modules/bidding/domain/__init__.py b/infrastructure/framework/falcon/__init__.py similarity index 100% rename from src/modules/bidding/domain/__init__.py rename to infrastructure/framework/falcon/__init__.py diff --git a/infrastructure/framework/falcon/app.py b/infrastructure/framework/falcon/app.py new file mode 100644 index 0000000..b67a321 --- /dev/null +++ b/infrastructure/framework/falcon/app.py @@ -0,0 +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 new file mode 100644 index 0000000..fbcb757 --- /dev/null +++ b/infrastructure/framework/falcon/controllers.py @@ -0,0 +1,60 @@ +import falcon + +from application.commands import AddItemCommand +from application.queries import GetItemsQuery +from application.response import json_response +from application.settings import APPLICATION_NAME +from infrastructure.framework.falcon.authentication import authenticate +from infrastructure.framework.falcon.base import RouteController + + +class InfoController(RouteController): + def on_get(self, req, res): + doc = { + 'framework': 'Falcon {}'.format(falcon.__version__), + 'application': APPLICATION_NAME, + } + res.body = json_response(doc) + res.status = falcon.HTTP_200 + + +class ItemsController(RouteController): + def on_get(self, req, res): + 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 = json_response(result) + res.status = falcon.HTTP_200 + + @authenticate + def on_post(self, req, res): + command = AddItemCommand({ + **req.media, + 'seller_id': req.context['user_id'] + }, strict=False) + command_name = type(command).__name__ + + if not command.is_valid(): + 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 = json_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/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/src/modules/bidding/infrastructure/__init__.py b/infrastructure/repositories/__init__.py similarity index 100% rename from src/modules/bidding/infrastructure/__init__.py rename to infrastructure/repositories/__init__.py diff --git a/infrastructure/repositories/auction_items_repository.py b/infrastructure/repositories/auction_items_repository.py new file mode 100644 index 0000000..a6f45e1 --- /dev/null +++ b/infrastructure/repositories/auction_items_repository.py @@ -0,0 +1,20 @@ +import uuid +from datetime import datetime + +from domain.entities import AuctionItem +from domain.value_objects import Currency + + +class AuctionItemsRepository: + 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, current_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)) 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/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 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 new file mode 100644 index 0000000..8d6b260 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +import os + +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)) + +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 diff --git a/migrations/README b/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100644 index 4d69434..0000000 --- a/migrations/env.py +++ /dev/null @@ -1,83 +0,0 @@ -aaaa() - - -from logging.config import fileConfig - -from sqlalchemy import engine_from_config, pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -from src.modules.catalog.infrastructure.persistence import ListingMetadata - -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = [ListingMetadata] - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. -section = config.config_ini_section -config.set_section_option( - section, "DATABASE_URL", "postgresql://postgres:password@localhost:5432/postgres" -) - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 5db8e0e..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1746 +0,0 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. - -[[package]] -name = "alembic" -version = "1.13.1" -description = "A database migration tool for SQLAlchemy." -optional = false -python-versions = ">=3.8" -files = [ - {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, - {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, -] - -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" - -[package.extras] -tz = ["backports.zoneinfo"] - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "asgiref" -version = "3.7.2" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.7" -files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "bcrypt" -version = "4.1.2" -description = "Modern password hashing for your software and your servers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, - {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, - {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, - {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, - {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, - {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, - {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, - {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, - {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, - {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, - {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, - {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, - {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, - {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, - {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, -] - -[package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] - -[[package]] -name = "black" -version = "21.12b0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, - {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, -] - -[package.dependencies] -click = ">=7.1.2" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" -platformdirs = ">=2" -tomli = ">=0.2.6,<2.0.0" -typing-extensions = {version = ">=3.10.0.0,<3.10.0.1 || >3.10.0.1", markers = "python_version >= \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2024.2.2" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.0.4" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.6" -files = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "colorlog" -version = "5.0.1" -description = "Add colours to the output of Python's logging module." -optional = false -python-versions = "*" -files = [ - {file = "colorlog-5.0.1-py2.py3-none-any.whl", hash = "sha256:4e6be13d9169254e2ded6526a6a4a1abb8ac564f2fa65b310a98e4ca5bea2c04"}, - {file = "colorlog-5.0.1.tar.gz", hash = "sha256:f17c013a06962b02f4449ee07cfdbe6b287df29efc2c9a1515b4a376f4e588ea"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[[package]] -name = "coverage" -version = "7.4.4" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, -] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "dependency-injector" -version = "4.41.0" -description = "Dependency injection framework for Python" -optional = false -python-versions = "*" -files = [ - {file = "dependency-injector-4.41.0.tar.gz", hash = "sha256:939dfc657104bc3e66b67afd3fb2ebb0850c9a1e73d0d26066f2bbdd8735ff9c"}, - {file = "dependency_injector-4.41.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2381a251b04244125148298212550750e6e1403e9b2850cc62e0e829d050ad3"}, - {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75280dfa23f7c88e1bf56c3920d58a43516816de6f6ab2a6650bb8a0f27d5c2c"}, - {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63bfba21f8bff654a80e9b9d06dd6c43a442990b73bf89cd471314c11c541ec2"}, - {file = "dependency_injector-4.41.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3535d06416251715b45f8412482b58ec1c6196a4a3baa207f947f0b03a7c4b44"}, - {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d09c08c944a25dabfb454238c1a889acd85102b93ae497de523bf9ab7947b28a"}, - {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:586a0821720b15932addbefb00f7370fbcd5831d6ebbd6494d774b44ff96d23a"}, - {file = "dependency_injector-4.41.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7fa4970f12a3fc95d8796938b11c41276ad1ff4c447b0e589212eab3fc527a90"}, - {file = "dependency_injector-4.41.0-cp310-cp310-win32.whl", hash = "sha256:d557e40673de984f78dab13ebd68d27fbb2f16d7c4e3b663ea2fa2f9fae6765b"}, - {file = "dependency_injector-4.41.0-cp310-cp310-win_amd64.whl", hash = "sha256:3744c327d18408e74781bd6d8b7738745ee80ef89f2c8daecf9ebd098cb84972"}, - {file = "dependency_injector-4.41.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:89c67edffe7007cf33cee79ecbca38f48efcc2add5c280717af434db6c789377"}, - {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786f7aac592e191c9caafc47732161d807bad65c62f260cd84cd73c7e2d67d6d"}, - {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b61a15bc46a3aa7b29bd8a7384b650aa3a7ef943491e93c49a0540a0b3dda4"}, - {file = "dependency_injector-4.41.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4f113e5d4c3070973ad76e5bda7317e500abae6083d78689f0b6e37cf403abf"}, - {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fa3ed8f0700e47a0e7363f949b4525ffa8277aa1c5b10ca5b41fce4dea61bb9"}, - {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e15ea0f2b14c1127e8b0d1597fef13f98845679f63bf670ba12dbfc12a16ef"}, - {file = "dependency_injector-4.41.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3055b3fc47a0d6e5f27defb4166c0d37543a4967c279549b154afaf506ce6efc"}, - {file = "dependency_injector-4.41.0-cp311-cp311-win32.whl", hash = "sha256:37d5954026e3831663518d78bdf4be9c2dbfea691edcb73c813aa3093aa4363a"}, - {file = "dependency_injector-4.41.0-cp311-cp311-win_amd64.whl", hash = "sha256:f89a507e389b7e4d4892dd9a6f5f4da25849e24f73275478634ac594d621ab3f"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ac79f3c05747f9724bd56c06985e78331fc6c85eb50f3e3f1a35e0c60f9977e9"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75e7a733b372db3144a34020c4233f6b94db2c6342d6d16bc5245b1b941ee2bd"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40936d9384363331910abd59dd244158ec3572abf9d37322f15095315ac99893"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a31d9d60be4b585585081109480cfb2ef564d3b851cb32a139bf8408411a93a"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:953bfac819d32dc72b963767589e0ed372e5e9e78b03fb6b89419d0500d34bbe"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8f0090ff14038f17a026ca408a3a0b0e7affb6aa7498b2b59d670f40ac970fbe"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6b29abac56ce347d2eb58a560723e1663ee2125cf5cc38866ed92b84319927ec"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-win32.whl", hash = "sha256:059fbb48333148143e8667a5323d162628dfe27c386bd0ed3deeecfc390338bf"}, - {file = "dependency_injector-4.41.0-cp36-cp36m-win_amd64.whl", hash = "sha256:16de2797dcfcc2263b8672bf0751166f7c7b369ca2ff9246ceb67b65f8e1d802"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c71d30b6708438050675f338edb9a25bea6c258478dbe5ec8405286756a2d347"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d283aee588a72072439e6721cb64aa6cba5bc18c576ef0ab28285a6ec7a9d655"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc852da612c7e347f2fcf921df2eca2718697a49f648a28a63db3ab504fd9510"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02620454ee8101f77a317f3229935ce687480883d72a40858ff4b0c87c935cce"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7a92680bea1c260e5c0d2d6cd60b0c913cba76a456a147db5ac047ecfcfcc758"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:168334cba3f1cbf55299ef38f0f2e31879115cc767b780c859f7814a52d80abb"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:48b6886a87b4ceb9b9f78550f77b2a5c7d2ce33bc83efd886556ad468cc9c85a"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-win32.whl", hash = "sha256:87be84084a1b922c4ba15e2e5aa900ee24b78a5467997cb7aec0a1d6cdb4a00b"}, - {file = "dependency_injector-4.41.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8b8cf1c6c56f5c18bdbd9f5e93b52ca29cb4d99606d4056e91f0c761eef496dc"}, - {file = "dependency_injector-4.41.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a8686fa330c83251c75c8238697686f7a0e0f6d40658538089165dc72df9bcff"}, - {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d670a844268dcd758195e58e9a5b39fc74bb8648aba99a13135a4a10ec9cfac"}, - {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3b9d41e0eff4c8e16fea1e33de66ff0030fe51137ca530f3c52ce110447914"}, - {file = "dependency_injector-4.41.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a724e0a737baadb4378f5dc1b079867cc3a88552fcca719b3dba84716828b2"}, - {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3588bd887b051d16b8bcabaae1127eb14059a0719a8fe34c8a75ba59321b352c"}, - {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:409441122f40e1b4b8582845fdd76deb9dc5c9d6eb74a057b85736ef9e9c671f"}, - {file = "dependency_injector-4.41.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7dcba8665cafec825b7095d5dd80afb5cf14404450eca3fe8b66e1edbf4dbc10"}, - {file = "dependency_injector-4.41.0-cp38-cp38-win32.whl", hash = "sha256:8b51efeaebacaf79ef68edfc65e9687699ccffb3538c4a3ab30d0d77e2db7189"}, - {file = "dependency_injector-4.41.0-cp38-cp38-win_amd64.whl", hash = "sha256:1662e2ef60ac6e681b9e11b5d8b7c17a0f733688916cf695f9540f8f50a61b1e"}, - {file = "dependency_injector-4.41.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51217cb384b468d7cc355544cec20774859f00812f9a1a71ed7fa701c957b2a7"}, - {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3890a12423ae3a9eade035093beba487f8d092ee6c6cb8706f4e7080a56e819"}, - {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99ed73b1521bf249e2823a08a730c9f9413a58f4b4290da022e0ad4fb333ba3d"}, - {file = "dependency_injector-4.41.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300838e9d4f3fbf539892a5a4072851728e23b37a1f467afcf393edd994d88f0"}, - {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:56d37b9d2f50a18f059d9abdbea7669a7518bd42b81603c21a27910a2b3f1657"}, - {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4a44ca3ce5867513a70b31855b218be3d251f5068ce1c480cc3a4ad24ffd3280"}, - {file = "dependency_injector-4.41.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67b369592c57549ccdcad0d5fef1ddb9d39af7fed8083d76e789ab0111fc6389"}, - {file = "dependency_injector-4.41.0-cp39-cp39-win32.whl", hash = "sha256:740a8e8106a04d3f44b52b25b80570fdac96a8a3934423de7c9202c5623e7936"}, - {file = "dependency_injector-4.41.0-cp39-cp39-win_amd64.whl", hash = "sha256:22b11dbf696e184f0b3d5ac4e5418aeac3c379ba4ea758c04a83869b7e5d1cbf"}, - {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b365a8548e9a49049fa6acb24d3cd939f619eeb8e300ca3e156e44402dcc07ec"}, - {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5168dc59808317dc4cdd235aa5d7d556d33e5600156acaf224cead236b48a3e8"}, - {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3229d83e99e255451605d5276604386e06ad948e3d60f31ddd796781c77f76f"}, - {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1baee908f21190bdc46a65ce4c417a5175e9397ca62354928694fce218f84487"}, - {file = "dependency_injector-4.41.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b37f36ecb0c1227f697e1d4a029644e3eda8dd0f0716aa63ad04d96dbb15bbbb"}, - {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b0c9c966ff66c77364a2d43d08de9968aff7e3903938fe912ba49796b2133344"}, - {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12e91ac0333e7e589421943ff6c6bf9cf0d9ac9703301cec37ccff3723406332"}, - {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2440b32474d4e747209528ca3ae48f42563b2fbe3d74dbfe949c11dfbfef7c4"}, - {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54032d62610cf2f4421c9d92cef52957215aaa0bca403cda580c58eb3f726eda"}, - {file = "dependency_injector-4.41.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:76b94c8310929e54136f3cb3de3adc86d1a657b3984299f40bf1cd2ba0bae548"}, - {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6ee9810841c6e0599356cb884d16453bfca6ab739d0e4f0248724ed8f9ee0d79"}, - {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b98945edae88e777091bf0848f869fb94bd76dfa4066d7c870a5caa933391d0"}, - {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2dee5d4abdd21f1a30a51d46645c095be9dcc404c7c6e9f81d0a01415a49e64"}, - {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d03f5fa0fa98a18bd0dfce846db80e2798607f0b861f1f99c97f441f7669d7a2"}, - {file = "dependency_injector-4.41.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f2842e15bae664a9f69932e922b02afa055c91efec959cb1896f6c499bf68180"}, -] - -[package.dependencies] -six = ">=1.7.0,<=1.16.0" - -[package.extras] -aiohttp = ["aiohttp"] -flask = ["flask"] -pydantic = ["pydantic"] -yaml = ["pyyaml"] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.110.0" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, - {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.36.3,<0.37.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "filelock" -version = "3.13.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "freezegun" -version = "1.4.0" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.7" -files = [ - {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, - {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, -] - -[package.dependencies] -python-dateutil = ">=2.7" - -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "0.16.3" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.7" -files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, -] - -[package.dependencies] -anyio = ">=3.0,<5.0" -certifi = "*" -h11 = ">=0.13,<0.15" -sniffio = "==1.*" - -[package.extras] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "httpx" -version = "0.23.3" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.7" -files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, -] - -[package.dependencies] -certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "identify" -version = "2.5.35" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "lato" -version = "0.12.0" -description = "Lato is a Python microframework designed for building modular monoliths and loosely coupled applications." -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "lato-0.12.0-py3-none-any.whl", hash = "sha256:0ef3512ff0d3f7912623595ecc21e8f23dfce8e0ff385f17d847b8bbfed27253"}, - {file = "lato-0.12.0.tar.gz", hash = "sha256:c3d36cdae0d5ca2db8868423f780c42bff22ce30bd6f3783ee5576ef223cec7a"}, -] - -[package.dependencies] -mergedeep = ">=1.3.4,<2.0.0" -pydantic = ">=2.4.2,<3.0.0" -pytest-asyncio = ">=0.23.5.post1,<0.24.0" - -[[package]] -name = "mako" -version = "1.3.2" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, - {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, -] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -description = "A deep merge function for 🐍." -optional = false -python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mypy" -version = "1.9.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "24.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, -] - -[[package]] -name = "pastel" -version = "0.2.1" -description = "Bring colors to your terminal." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, - {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - -[[package]] -name = "pluggy" -version = "1.4.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "poethepoet" -version = "0.10.0" -description = "A task runner that works well with poetry." -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "poethepoet-0.10.0-py3-none-any.whl", hash = "sha256:6fb3021603d4421c6fcc40072bbcf150a6c52ef70ff4d3be089b8b04e015ef5a"}, - {file = "poethepoet-0.10.0.tar.gz", hash = "sha256:70b97cb194b978dc464c70793e85e6f746cddf82b84a38bfb135946ad71ae19c"}, -] - -[package.dependencies] -pastel = ">=0.2.0,<0.3.0" -tomlkit = ">=0.6.0,<1.0.0" - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "psycopg2-binary" -version = "2.9.9" -description = "psycopg2 - Python-PostgreSQL Database Adapter" -optional = false -python-versions = ">=3.7" -files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, -] - -[[package]] -name = "pydantic" -version = "2.6.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.16.3" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.2.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, - {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, -] - -[package.dependencies] -pydantic = ">=2.3.0" -python-dotenv = ">=0.21.0" - -[package.extras] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pytest" -version = "8.1.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.4,<2.0" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.23.8" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, -] - -[package.dependencies] -pytest = ">=7.0.0,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "2.12.1" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, -] - -[package.dependencies] -coverage = ">=5.2.1" -pytest = ">=4.6" -toml = "*" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" -optional = false -python-versions = ">=3.6" -files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, -] - -[[package]] -name = "python-multipart" -version = "0.0.5" -description = "A streaming multipart parser for Python" -optional = false -python-versions = "*" -files = [ - {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, -] - -[package.dependencies] -six = ">=1.4.0" - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "setuptools" -version = "69.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "sqlalchemy" -version = "1.4.52" -description = "Database Abstraction Library" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "SQLAlchemy-1.4.52-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f68016f9a5713684c1507cc37133c28035f29925c75c0df2f9d0f7571e23720a"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24bb0f81fbbb13d737b7f76d1821ec0b117ce8cbb8ee5e8641ad2de41aa916d3"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e93983cc0d2edae253b3f2141b0a3fb07e41c76cd79c2ad743fc27eb79c3f6db"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84e10772cfc333eb08d0b7ef808cd76e4a9a30a725fb62a0495877a57ee41d81"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:427988398d2902de042093d17f2b9619a5ebc605bf6372f7d70e29bde6736842"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-win32.whl", hash = "sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94"}, - {file = "SQLAlchemy-1.4.52-cp310-cp310-win_amd64.whl", hash = "sha256:80e7f697bccc56ac6eac9e2df5c98b47de57e7006d2e46e1a3c17c546254f6ef"}, - {file = "SQLAlchemy-1.4.52-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f251af4c75a675ea42766880ff430ac33291c8d0057acca79710f9e5a77383d"}, - {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8f9e4c4718f111d7b530c4e6fb4d28f9f110eb82e7961412955b3875b66de0"}, - {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb1672b57f58c0318ad2cff80b384e816735ffc7e848d8aa51e0b0fc2f4b7bb"}, - {file = "SQLAlchemy-1.4.52-cp311-cp311-win32.whl", hash = "sha256:6e41cb5cda641f3754568d2ed8962f772a7f2b59403b95c60c89f3e0bd25f15e"}, - {file = "SQLAlchemy-1.4.52-cp311-cp311-win_amd64.whl", hash = "sha256:5bed4f8c3b69779de9d99eb03fd9ab67a850d74ab0243d1be9d4080e77b6af12"}, - {file = "SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c"}, - {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554"}, - {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de9acf369aaadb71a725b7e83a5ef40ca3de1cf4cdc93fa847df6b12d3cd924b"}, - {file = "SQLAlchemy-1.4.52-cp312-cp312-win32.whl", hash = "sha256:763bd97c4ebc74136ecf3526b34808c58945023a59927b416acebcd68d1fc126"}, - {file = "SQLAlchemy-1.4.52-cp312-cp312-win_amd64.whl", hash = "sha256:f12aaf94f4d9679ca475975578739e12cc5b461172e04d66f7a3c39dd14ffc64"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:853fcfd1f54224ea7aabcf34b227d2b64a08cbac116ecf376907968b29b8e763"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f98dbb8fcc6d1c03ae8ec735d3c62110949a3b8bc6e215053aa27096857afb45"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b5de6af8852500d01398f5047d62ca3431d1e29a331d0b56c3e14cb03f8094c"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3491c85df263a5c2157c594f54a1a9c72265b75d3777e61ee13c556d9e43ffc9"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-win32.whl", hash = "sha256:427c282dd0deba1f07bcbf499cbcc9fe9a626743f5d4989bfdfd3ed3513003dd"}, - {file = "SQLAlchemy-1.4.52-cp36-cp36m-win_amd64.whl", hash = "sha256:ca5ce82b11731492204cff8845c5e8ca1a4bd1ade85e3b8fcf86e7601bfc6a39"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:29d4247313abb2015f8979137fe65f4eaceead5247d39603cc4b4a610936cd2b"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a752bff4796bf22803d052d4841ebc3c55c26fb65551f2c96e90ac7c62be763a"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ea11727feb2861deaa293c7971a4df57ef1c90e42cb53f0da40c3468388000"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d913f8953e098ca931ad7f58797f91deed26b435ec3756478b75c608aa80d139"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a251146b921725547ea1735b060a11e1be705017b568c9f8067ca61e6ef85f20"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-win32.whl", hash = "sha256:1f8e1c6a6b7f8e9407ad9afc0ea41c1f65225ce505b79bc0342159de9c890782"}, - {file = "SQLAlchemy-1.4.52-cp37-cp37m-win_amd64.whl", hash = "sha256:346ed50cb2c30f5d7a03d888e25744154ceac6f0e6e1ab3bc7b5b77138d37710"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4dae6001457d4497736e3bc422165f107ecdd70b0d651fab7f731276e8b9e12d"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d2e08d79f5bf250afb4a61426b41026e448da446b55e4770c2afdc1e200fce"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbce5dd7c7735e01d24f5a60177f3e589078f83c8a29e124a6521b76d825b85"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bdb7b4d889631a3b2a81a3347c4c3f031812eb4adeaa3ee4e6b0d028ad1852b5"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c294ae4e6bbd060dd79e2bd5bba8b6274d08ffd65b58d106394cb6abbf35cf45"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-win32.whl", hash = "sha256:bcdfb4b47fe04967669874fb1ce782a006756fdbebe7263f6a000e1db969120e"}, - {file = "SQLAlchemy-1.4.52-cp38-cp38-win_amd64.whl", hash = "sha256:7d0dbc56cb6af5088f3658982d3d8c1d6a82691f31f7b0da682c7b98fa914e91"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a551d5f3dc63f096ed41775ceec72fdf91462bb95abdc179010dc95a93957800"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab773f9ad848118df7a9bbabca53e3f1002387cdbb6ee81693db808b82aaab0"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2de46f5d5396d5331127cfa71f837cca945f9a2b04f7cb5a01949cf676db7d1"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7027be7930a90d18a386b25ee8af30514c61f3852c7268899f23fdfbd3107181"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99224d621affbb3c1a4f72b631f8393045f4ce647dd3262f12fe3576918f8bf3"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-win32.whl", hash = "sha256:c124912fd4e1bb9d1e7dc193ed482a9f812769cb1e69363ab68e01801e859821"}, - {file = "SQLAlchemy-1.4.52-cp39-cp39-win_amd64.whl", hash = "sha256:2c286fab42e49db23c46ab02479f328b8bdb837d3e281cae546cc4085c83b680"}, - {file = "SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] -mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] -sqlcipher = ["sqlcipher3_binary"] - -[[package]] -name = "sqlalchemy-json" -version = "0.4.0" -description = "JSON type with nested change tracking for SQLAlchemy" -optional = false -python-versions = "*" -files = [ - {file = "sqlalchemy-json-0.4.0.tar.gz", hash = "sha256:d8e72cac50724a17cc137c98bec5cb5990e9f1e8fc3eb30dd225fb47c087ea27"}, - {file = "sqlalchemy_json-0.4.0-py2.py3-none-any.whl", hash = "sha256:0f52f24301aa3b5ea240b622facc489eff2e7bfddde931ba988bfabc306b1778"}, -] - -[package.dependencies] -six = "*" -sqlalchemy = ">=0.7" - -[[package]] -name = "sqlalchemy-utils" -version = "0.38.3" -description = "Various utility functions for SQLAlchemy." -optional = false -python-versions = "~=3.6" -files = [ - {file = "SQLAlchemy-Utils-0.38.3.tar.gz", hash = "sha256:9f9afba607a40455cf703adfa9846584bf26168a0c5a60a70063b70d65051f4d"}, - {file = "SQLAlchemy_Utils-0.38.3-py3-none-any.whl", hash = "sha256:5c13b5d08adfaa85f3d4e8ec09a75136216fad41346980d02974a70a77988bf9"}, -] - -[package.dependencies] -SQLAlchemy = ">=1.3" - -[package.extras] -arrow = ["arrow (>=0.3.4)"] -babel = ["Babel (>=1.3)"] -color = ["colour (>=0.0.4)"] -encrypted = ["cryptography (>=0.6)"] -intervals = ["intervals (>=0.7.1)"] -password = ["passlib (>=1.6,<2.0)"] -pendulum = ["pendulum (>=2.0.5)"] -phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -timezone = ["python-dateutil"] -url = ["furl (>=0.4.1)"] - -[[package]] -name = "starlette" -version = "0.36.3" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, - {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "starlette-context" -version = "0.3.6" -description = "Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id." -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "starlette_context-0.3.6-py3-none-any.whl", hash = "sha256:b14ce373fbb6895a2182a7104b9f63ba20c8db83444005fb9a844dd77ad9895c"}, - {file = "starlette_context-0.3.6.tar.gz", hash = "sha256:d361a36ba2d4acca3ab680f917b25e281533d725374752d47607a859041958cb"}, -] - -[package.dependencies] -starlette = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "1.2.3" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.4" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, -] - -[[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, - {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, - {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, - {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, - {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, - {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, - {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, - {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, - {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, - {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, - {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, -] - -[[package]] -name = "typing-extensions" -version = "4.10.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, -] - -[[package]] -name = "urllib3" -version = "2.2.1" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.14.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = "*" -files = [ - {file = "uvicorn-0.14.0-py3-none-any.whl", hash = "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae"}, - {file = "uvicorn-0.14.0.tar.gz", hash = "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292"}, -] - -[package.dependencies] -asgiref = ">=3.3.4" -click = ">=7" -h11 = ">=0.8" - -[package.extras] -standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (==0.2.*)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=9.1)"] - -[[package]] -name = "virtualenv" -version = "20.25.1" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "vulture" -version = "2.11" -description = "Find dead code" -optional = false -python-versions = ">=3.8" -files = [ - {file = "vulture-2.11-py2.py3-none-any.whl", hash = "sha256:12d745f7710ffbf6aeb8279ba9068a24d4e52e8ed333b8b044035c9d6b823aba"}, - {file = "vulture-2.11.tar.gz", hash = "sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"}, -] - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[metadata] -lock-version = "2.0" -python-versions = "^3.10.0" -content-hash = "44490e84888709bc4e640d8a067c1f262a4e51cc2c768934804860103c11692b" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 0b65b39..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,60 +0,0 @@ -[tool.poetry] -name = "python-ddd" -version = "0.1.0" -description = "" -authors = ["Przemysław Górecki "] - -[tool.poetry.dependencies] -python = "^3.10.0" -black = "^21.5b1" -uvicorn = "^0.14.0" -starlette-context = "^0.3.3" -SQLAlchemy = "^1.4.22" -dependency-injector = "^4.35.2" -colorlog = "^5.0.1" -python-json-logger = "^2.0.2" -alembic = "^1.7.3" -sqlalchemy-json = "^0.4.0" -typed-ast = "^1.4.3" -python-multipart = "^0.0.5" -psycopg2-binary = "^2.9.2" -freezegun = "^1.1.0" -SQLAlchemy-Utils = "^0.38.3" -pre-commit = "^2.20.0" -click = "8.0.4" -httpx = "^0.23.1" -requests = "^2.28.1" -bcrypt = "^4.0.1" -mypy = "^1.4.1" -fastapi = "^0.110.0" -pydantic-settings = "^2.2.1" -lato = "^0.12.0" - -[tool.poetry.dev-dependencies] -poethepoet = "^0.10.0" -pytest-cov = "^2.12.1" -vulture = "^2.7" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - - -[tool.poe.tasks] -test = { shell = "DATABASE_URL=postgresql://postgres:password@localhost:5433/postgres pytest src" } -test_domain = "pytest -k domain" -test_infrastructure = "pytest -k infrastructure" -test_application = "pytest -k application" -test_unit = "pytest -m unit" -test_integration = "pytest -m 'not unit'" -test_coverage = "pytest --cov=src --cov-report=html" -start = { shell = "uvicorn src.api.main:app --reload" } -start_cli = { shell = "cd src && python -m cli" } -compose_up = "docker-compose -f docker-compose.dev.yml up" - -[tool.black] -exclude = 'tmp' - -[tool.mypy] -mypy_path = "src" - diff --git a/pytest.ini b/pytest.ini index b4745ab..b0e5a94 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,3 @@ [pytest] -norecursedirs = tmp - -markers = - unit: marks test as unit test i.e. not using any external services (deselect with '-m "not unit"') - integration: marks tests as integration i.e. using a database (deselect with '-m "not integration"') - serial \ No newline at end of file +filterwarnings = + ignore::DeprecationWarning \ No newline at end of file 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 diff --git a/src/alembic.ini b/src/alembic.ini deleted file mode 100644 index 1589d7f..0000000 --- a/src/alembic.ini +++ /dev/null @@ -1,100 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. Valid values are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # default: use os.pathsep - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -sqlalchemy.url = postgresql://postgres:password@localhost/postgres - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/src/alembic/README b/src/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/src/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/src/alembic/env.py b/src/alembic/env.py deleted file mode 100644 index b723bd0..0000000 --- a/src/alembic/env.py +++ /dev/null @@ -1,76 +0,0 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config, pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -from modules.catalog.infrastructure.listing_repository import CatalogListingModel - -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = [CatalogListingModel.metadata] - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/src/alembic/script.py.mako b/src/alembic/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/src/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/src/alembic/versions/d6c2334f4816_initial_listing_model.py b/src/alembic/versions/d6c2334f4816_initial_listing_model.py deleted file mode 100644 index 42de88a..0000000 --- a/src/alembic/versions/d6c2334f4816_initial_listing_model.py +++ /dev/null @@ -1,34 +0,0 @@ -"""initial listing model - -Revision ID: d6c2334f4816 -Revises: -Create Date: 2021-09-27 17:33:02.166128 - -""" -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "d6c2334f4816" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "catalog_listing", - sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), - sa.Column("data", postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("catalog_listing") - # ### end Alembic commands ### diff --git a/src/api/README.md b/src/api/README.md deleted file mode 100644 index 0850a1c..0000000 --- a/src/api/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# API application - -HTTP(S) REST API endpoint - implemented using FastAPI. \ No newline at end of file diff --git a/src/api/__init__.py b/src/api/__init__.py deleted file mode 100644 index 38ed7e6..0000000 --- a/src/api/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys -from pathlib import Path - -# add base project path to PYTHONPATH -BASE_DIR = Path(__file__).resolve().parent.parent -sys.path.append(str(BASE_DIR)) diff --git a/src/api/__main__.py b/src/api/__main__.py deleted file mode 100644 index 091f58a..0000000 --- a/src/api/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -import uvicorn - -from api.main import app - -uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/api/dependencies.py b/src/api/dependencies.py deleted file mode 100644 index f94a82d..0000000 --- a/src/api/dependencies.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Annotated - -from fastapi import Depends, Request -from fastapi.security import OAuth2PasswordBearer -from lato import Application, TransactionContext - -from modules.iam.application.services import IamService -from modules.iam.domain.entities import User - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - - -async def get_application(request: Request) -> Application: - return request.state.lato_application - - -async def get_transaction_context( - app: Annotated[Application, Depends(get_application)], -) -> TransactionContext: - """Creates a new transaction context for each request""" - async with app.transaction_context() as ctx: - yield ctx - - -async def get_authenticated_user( - access_token: Annotated[str, Depends(oauth2_scheme)], - ctx: Annotated[TransactionContext, Depends(get_transaction_context)], -) -> User: - current_user = ctx[IamService].find_user_by_access_token(access_token) - return current_user diff --git a/src/api/main.py b/src/api/main.py deleted file mode 100644 index 55492c6..0000000 --- a/src/api/main.py +++ /dev/null @@ -1,107 +0,0 @@ -import time - -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from pydantic import ValidationError - -from api.dependencies import oauth2_scheme # noqa -from api.routers import bidding, catalog, diagnostics, iam -from config.api_config import ApiConfig -from config.container import ApplicationContainer -from seedwork.domain.exceptions import DomainException, EntityNotFoundException -from seedwork.infrastructure.database import Base -from seedwork.infrastructure.logging import LoggerFactory, logger - -# configure logger prior to first usage -LoggerFactory.configure(logger_name="api") - -# dependency injection container -config = ApiConfig() -container = ApplicationContainer(config=config) -db_engine = container.db_engine() -logger.info(f"using db engine {db_engine}, creating tables") -Base.metadata.create_all(db_engine) -logger.info("setup complete") - -app = FastAPI(debug=config.DEBUG) - -app.include_router(catalog.router) -app.include_router(bidding.router) -app.include_router(iam.router) -app.include_router(diagnostics.router) -app.container = container # type: ignore - - -@app.exception_handler(ValidationError) -async def pydantic_validation_exception_handler(request: Request, exc: ValidationError): - return JSONResponse( - status_code=422, - content={ - "detail": exc.errors(), - }, - ) - - -# startup - -try: - import uuid - - from modules.iam.application.services import IamService - - with container.application().transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=uuid.UUID(int=1), - email="user1@example.com", - password="password", - access_token="token", - ) -except ValueError as e: - ... - - -@app.exception_handler(DomainException) -async def domain_exception_handler(request: Request, exc: DomainException): - if container.config.DEBUG: - raise exc - - return JSONResponse( - status_code=500, - content={"message": f"Oops! {exc} did something. There goes a rainbow..."}, - ) - - -@app.exception_handler(EntityNotFoundException) -async def entity_not_found_exception_handler( - request: Request, exc: EntityNotFoundException -): - return JSONResponse( - status_code=404, - content={ - "message": f"Entity {exc.kwargs} not found in {exc.repository.__class__.__name__}" - }, - ) - - -@app.middleware("http") -async def add_lato_application(request: Request, call_next): - request.state.lato_application = container.application() - return await call_next(request) - - -@app.middleware("http") -async def add_process_time(request: Request, call_next): - start_time = time.time() - try: - response = await call_next(request) - process_time = time.time() - start_time - response.headers["X-Process-Time"] = str(process_time) - return response - finally: - pass - - -@app.get("/") -async def root(): - return {"info": "Online auctions API. See /docs for documentation"} diff --git a/src/api/models/bidding.py b/src/api/models/bidding.py deleted file mode 100644 index adcc0fa..0000000 --- a/src/api/models/bidding.py +++ /dev/null @@ -1,33 +0,0 @@ -from datetime import datetime - -from pydantic import BaseModel - -from seedwork.domain.value_objects import GenericUUID - - -class BidReadModel(BaseModel): - amount: float - currency: str - bidder_id: GenericUUID - bidder_username: str - - class Config: - arbitrary_types_allowed = True - - -class BiddingResponse(BaseModel): - listing_id: GenericUUID - auction_status: str = "active" # active, ended - auction_end_date: datetime - bids: list[BidReadModel] - - class Config: - arbitrary_types_allowed = True - - -class PlaceBidRequest(BaseModel): - bidder_id: GenericUUID - amount: float - - class Config: - arbitrary_types_allowed = True diff --git a/src/api/models/catalog.py b/src/api/models/catalog.py deleted file mode 100644 index 6bc365a..0000000 --- a/src/api/models/catalog.py +++ /dev/null @@ -1,35 +0,0 @@ -from uuid import UUID, uuid4 - -from pydantic import BaseModel - - -class CurrentUser(BaseModel): - id: UUID - username: str - - @classmethod - def fake_user(cls): - return CurrentUser(id=uuid4(), username="fake_user") - - -class ListingWriteModel(BaseModel): - title: str - description: str - ask_price_amount: float - ask_price_currency: str = "USD" - - -class ListingPublishModel(BaseModel): - id: UUID - - -class ListingReadModel(BaseModel): - id: UUID - title: str = "" - description: str - ask_price_amount: float - ask_price_currency: str - - -class ListingIndexModel(BaseModel): - data: list[ListingReadModel] diff --git a/src/api/routers/bidding.py b/src/api/routers/bidding.py deleted file mode 100644 index 68dfbdf..0000000 --- a/src/api/routers/bidding.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Depends -from lato import Application - -from api.dependencies import get_application -from api.models.bidding import BiddingResponse, PlaceBidRequest -from config.container import inject -from modules.bidding.application.command import PlaceBidCommand, RetractBidCommand -from modules.bidding.application.query.get_bidding_details import GetBiddingDetails - -router = APIRouter() - -""" -Inspired by https://developer.ebay.com/api-docs/buy/offer/types/api:Bidding -""" - - -@router.get("/bidding/{listing_id}", tags=["bidding"], response_model=BiddingResponse) -@inject -async def get_bidding_details_of_listing( - listing_id, app: Annotated[Application, Depends(get_application)] -): - """ - Shows listing details - """ - query = GetBiddingDetails(listing_id=listing_id) - result = await app.execute_async(query) - return BiddingResponse( - listing_id=result.id, - auction_end_date=result.ends_at, - bids=result.bids, - ) - - -@router.post( - "/bidding/{listing_id}/place_bid", tags=["bidding"], response_model=BiddingResponse -) -@inject -async def place_bid( - listing_id, - request_body: PlaceBidRequest, - app: Annotated[Application, Depends(get_application)], -): - """ - Places a bid on a listing - """ - # TODO: get bidder from current user - - command = PlaceBidCommand( - listing_id=listing_id, - bidder_id=request_body.bidder_id, - amount=request_body.amount, - ) - await app.execute_async(command) - # execute_async, or execute? - - query = GetBiddingDetails(listing_id=listing_id) - result = await app.execute_async(query) - return BiddingResponse( - listing_id=result.id, - auction_end_date=result.ends_at, - bids=result.bids, - ) - - -@router.post( - "/bidding/{listing_id}/retract_bid", - tags=["bidding"], - response_model=BiddingResponse, -) -@inject -async def retract_bid( - listing_id, app: Annotated[Application, Depends(get_application)] -): - """ - Retracts a bid from a listing - """ - command = RetractBidCommand( - listing_id=listing_id, - bidder_id="", - ) - app.execute(command) - - query = GetBiddingDetails(listing_id=listing_id) - query_result = app.execute_query(query) - payload = query_result.payload - return BiddingResponse( - listing_id=str(payload.id), - auction_end_date=payload.ends_at, - bids=payload.bids, - ) diff --git a/src/api/routers/catalog.py b/src/api/routers/catalog.py deleted file mode 100644 index eae8ff2..0000000 --- a/src/api/routers/catalog.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Depends - -from api.dependencies import Application, User, get_application, get_authenticated_user -from api.models.catalog import ListingIndexModel, ListingReadModel, ListingWriteModel -from config.container import inject -from modules.catalog.application.command import ( - CreateListingDraftCommand, - DeleteListingDraftCommand, - PublishListingDraftCommand, -) -from modules.catalog.application.query.get_all_listings import GetAllListings -from modules.catalog.application.query.get_listing_details import GetListingDetails -from seedwork.domain.value_objects import GenericUUID, Money - -""" -Inspired by https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/createOffer -""" - -router = APIRouter() - - -@router.get("/catalog", tags=["catalog"], response_model=ListingIndexModel) -async def get_all_listings(app: Annotated[Application, Depends(get_application)]): - """ - Shows all published listings in the catalog - """ - query = GetAllListings() - result = await app.execute_async(query) - return dict(data=result) - - -@router.get("/catalog/{listing_id}", tags=["catalog"], response_model=ListingReadModel) -@inject -async def get_listing_details( - listing_id, app: Annotated[Application, Depends(get_application)] -): - """ - Shows listing details - """ - query = GetListingDetails(listing_id=listing_id) - query_result = await app.execute_async(query) - return dict(data=query_result.payload) - - -@router.post( - "/catalog", tags=["catalog"], status_code=201, response_model=ListingReadModel -) -@inject -async def create_listing( - request_body: ListingWriteModel, - app: Annotated[Application, Depends(get_application)], - current_user: Annotated[User, Depends(get_authenticated_user)], -): - """ - Creates a new listing - """ - command = CreateListingDraftCommand( - listing_id=GenericUUID.next_id(), - title=request_body.title, - description=request_body.description, - ask_price=Money(request_body.ask_price_amount, request_body.ask_price_currency), - seller_id=current_user.id, - ) - app.execute(command) - - query = GetListingDetails(listing_id=command.listing_id) - query_result = app.execute_query(query) - return dict(query_result.payload) - - -@router.delete( - "/catalog/{listing_id}", tags=["catalog"], status_code=204, response_model=None -) -@inject -async def delete_listing( - listing_id, - app: Annotated[Application, Depends(get_application)], - current_user: Annotated[User, Depends(get_authenticated_user)], -): - """ - Deletes a listing - """ - command = DeleteListingDraftCommand( - listing_id=listing_id, - seller_id=current_user.id, - ) - await app.execute_async(command) - - -@router.post( - "/catalog/{listing_id}/publish", - tags=["catalog"], - status_code=200, - response_model=ListingReadModel, -) -@inject -async def publish_listing( - listing_id: GenericUUID, - app: Annotated[Application, Depends(get_application)], - current_user: Annotated[User, Depends(get_authenticated_user)], -): - """ - Publishes a listing - """ - command = PublishListingDraftCommand( - listing_id=listing_id, - seller_id=current_user.id, - ) - await app.execute_async(command) - - query = GetListingDetails(listing_id=listing_id) - response = await app.execute_async(query) - return response diff --git a/src/api/routers/diagnostics.py b/src/api/routers/diagnostics.py deleted file mode 100644 index 5c7ddfb..0000000 --- a/src/api/routers/diagnostics.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Depends - -from api.dependencies import ( - Application, - User, - get_authenticated_user, - get_application, -) - -from .iam import UserResponse - -router = APIRouter() - - -@router.get("/debug", tags=["diagnostics"]) -async def debug( - app: Annotated[Application, Depends(get_application)], - current_user: Annotated[User, Depends(get_authenticated_user)], -): - return dict( - app_id=id(app), - name=app.name, - version=app["app_version"], - user=UserResponse( - id=str(current_user.id), - username=current_user.username, - access_token=current_user.access_token, - ), - ) diff --git a/src/api/routers/iam.py b/src/api/routers/iam.py deleted file mode 100644 index f3a4fea..0000000 --- a/src/api/routers/iam.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from pydantic import BaseModel - -from api.dependencies import TransactionContext, get_transaction_context -from config.container import inject -from modules.iam.application.exceptions import InvalidCredentialsException -from modules.iam.application.services import IamService - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -router = APIRouter() - - -class UserResponse(BaseModel): - id: str - username: str - access_token: str - - -class LoginResponse(BaseModel): - access_token: str - token_type: str - - -# @router.get("/token", tags=["iam"]) -# async def get_token(ctx: Annotated[TransactionContext, Depends(get_transaction_context_for_public_route)]): -# return ctx.current_user.access_token - - -@router.post("/token", tags=["iam"]) -@inject -async def login( - ctx: Annotated[TransactionContext, Depends(get_transaction_context)], - form_data: OAuth2PasswordRequestForm = Depends(), -) -> LoginResponse: - try: - iam_service = ctx[IamService] - user = iam_service.authenticate_with_name_and_password( - form_data.username, form_data.password - ) - except InvalidCredentialsException: - # TODO: automatically map application exceptions to HTTP exceptions - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Incorrect username or password", - ) - - return LoginResponse(access_token=user.access_token, token_type="bearer") - - -@router.get("/users/me", tags=["iam"]) -async def get_users_me( - ctx: Annotated[TransactionContext, Depends(get_transaction_context)], -) -> UserResponse: - user = ctx.current_user - return UserResponse( - id=str(user.id), username=user.username, access_token=user.access_token - ) diff --git a/src/api/tests/test_bidding.py b/src/api/tests/test_bidding.py deleted file mode 100644 index a4c949a..0000000 --- a/src/api/tests/test_bidding.py +++ /dev/null @@ -1,63 +0,0 @@ -import pytest - -from modules.catalog.application.command import ( - CreateListingDraftCommand, - PublishListingDraftCommand, -) -from seedwork.domain.value_objects import GenericUUID, Money -from seedwork.infrastructure.logging import logger - - -async def setup_app_for_bidding_tests(app, listing_id, seller_id, bidder_id): - logger.info("Adding users") - with app.transaction_context() as ctx: - iam_service = ctx["iam_service"] - - iam_service.create_user( - user_id=seller_id, - email="seller@example.com", - password="password", - access_token="token1", - ) - ctx["logger"].debug(f"Added seller: {seller_id}") - - iam_service.create_user( - user_id=bidder_id, - email="bidder@example.com", - password="password", - access_token="token2", - ) - ctx["logger"].debug(f"Added bidder: {bidder_id}") - - await app.execute_async( - CreateListingDraftCommand( - listing_id=listing_id, - title="Foo", - description="Bar", - ask_price=Money(10), - seller_id=seller_id, - ) - ) - logger.info(f"Created listing draft: {listing_id}") - - await app.execute_async( - PublishListingDraftCommand(listing_id=listing_id, seller_id=seller_id) - ) - logger.info(f"Published listing draft {listing_id} by seller {seller_id}") - - logger.info("test setup complete") - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_place_bid(app, api_client): - listing_id = GenericUUID(int=1) - seller_id = GenericUUID(int=2) - bidder_id = GenericUUID(int=3) - await setup_app_for_bidding_tests(app, listing_id, seller_id, bidder_id) - - url = f"/bidding/{listing_id}/place_bid" - - response = api_client.post(url, json={"bidder_id": str(bidder_id), "amount": 11}) - json = response.json() - assert response.status_code == 200 diff --git a/src/api/tests/test_catalog.py b/src/api/tests/test_catalog.py deleted file mode 100644 index 8ccaa52..0000000 --- a/src/api/tests/test_catalog.py +++ /dev/null @@ -1,162 +0,0 @@ -import pytest - -from modules.catalog.application.command import ( - CreateListingDraftCommand, - PublishListingDraftCommand, -) -from seedwork.domain.value_objects import GenericUUID, Money - - -@pytest.mark.integration -def test_empty_catalog_list(api_client): - response = api_client.get("/catalog") - assert response.status_code == 200 - assert response.json() == {"data": []} - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_catalog_list_with_one_item(app, api_client): - # arrange - await app.execute_async( - CreateListingDraftCommand( - listing_id=GenericUUID(int=1), - title="Foo", - description="Bar", - ask_price=Money(10), - seller_id=GenericUUID(int=2), - ) - ) - - # act - response = api_client.get("/catalog") - - # assert - assert response.status_code == 200 - response_data = response.json()["data"] - assert len(response_data) == 1 - assert response.json() == { - "data": [ - { - "id": str(GenericUUID(int=1)), - "title": "Foo", - "description": "Bar", - "ask_price_amount": 10.0, - "ask_price_currency": "USD", - } - ] - } - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_catalog_list_with_two_items(app, api_client): - # arrange - await app.execute_async( - CreateListingDraftCommand( - listing_id=GenericUUID(int=1), - title="Foo #1", - description="Bar", - ask_price=Money(10), - seller_id=GenericUUID(int=2), - ) - ) - await app.execute_async( - CreateListingDraftCommand( - listing_id=GenericUUID(int=2), - title="Foo #2", - description="Bar", - ask_price=Money(10), - seller_id=GenericUUID(int=2), - ) - ) - - # act - response = api_client.get("/catalog") - - # assert - assert response.status_code == 200 - response_data = response.json()["data"] - assert len(response_data) == 2 - - -def test_catalog_create_draft_fails_due_to_incomplete_data( - api, authenticated_api_client -): - response = authenticated_api_client.post("/catalog") - assert response.status_code == 422 - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_catalog_delete_draft(app, authenticated_api_client): - current_user = authenticated_api_client.current_user - await app.execute_async( - CreateListingDraftCommand( - listing_id=GenericUUID(int=1), - title="Listing to be deleted", - description="...", - ask_price=Money(10), - seller_id=current_user.id, - ) - ) - - response = authenticated_api_client.delete(f"/catalog/{str(GenericUUID(int=1))}") - - assert response.status_code == 204 - - -@pytest.mark.integration -def test_catalog_delete_non_existing_draft_returns_404(authenticated_api_client): - listing_id = GenericUUID(int=1) - response = authenticated_api_client.delete(f"/catalog/{listing_id}") - assert response.status_code == 404 - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_catalog_publish_listing_draft(app, authenticated_api_client): - # arrange - current_user = authenticated_api_client.current_user - listing_id = GenericUUID(int=1) - await app.execute_async( - CreateListingDraftCommand( - listing_id=listing_id, - title="Listing to be published", - description="...", - ask_price=Money(10), - seller_id=current_user.id, - ) - ) - - # act - response = authenticated_api_client.post(f"/catalog/{listing_id}/publish") - - # assert that the listing was published - assert response.status_code == 200 - - -@pytest.mark.asyncio -async def test_published_listing_appears_in_biddings(app, authenticated_api_client): - # arrange - listing_id = GenericUUID(int=1) - current_user = authenticated_api_client.current_user - await app.execute_async( - CreateListingDraftCommand( - listing_id=listing_id, - title="Listing to be published", - description="...", - ask_price=Money(10), - seller_id=current_user.id, - ) - ) - await app.execute_async( - PublishListingDraftCommand( - listing_id=listing_id, - seller_id=current_user.id, - ) - ) - - url = f"/bidding/{listing_id}" - response = authenticated_api_client.get(url) - assert response.status_code == 200 diff --git a/src/api/tests/test_common.py b/src/api/tests/test_common.py deleted file mode 100644 index 5bec52d..0000000 --- a/src/api/tests/test_common.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest - - -@pytest.mark.integration -def test_homepage_returns_200(api_client): - response = api_client.get("/") - assert response.status_code == 200 - - -@pytest.mark.integration -def test_docs_page_returns_200(api_client): - response = api_client.get("/docs") - assert response.status_code == 200 - - -@pytest.mark.integration -def test_openapi_schema_returns_200(api_client): - response = api_client.get("/openapi.json") - assert response.status_code == 200 diff --git a/src/api/tests/test_diagnostics.py b/src/api/tests/test_diagnostics.py deleted file mode 100644 index 2e26105..0000000 --- a/src/api/tests/test_diagnostics.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - - -@pytest.mark.integration -def test_debug_endpoint(authenticated_api_client): - response = authenticated_api_client.get("/debug") - assert response.status_code == 200 diff --git a/src/api/tests/test_login.py b/src/api/tests/test_login.py deleted file mode 100644 index 21417ee..0000000 --- a/src/api/tests/test_login.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest - -from modules.iam.application.services import IamService -from seedwork.domain.value_objects import GenericUUID - - -@pytest.mark.integration -def test_login_with_api_token(app, api_client): - # arrange - with app.transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=GenericUUID(int=1), - email="admin@example.com", - password="admin", - access_token="token", - is_superuser=True, - ) - - # act - response = api_client.post( - "/token", data={"username": "admin@example.com", "password": "admin"} - ) - - # assert - assert response.status_code == 200 - assert response.json()["access_token"] == "token" - - -@pytest.mark.integration -def test_login_with_invalid_username_returns_400(app, api_client): - # arrange - with app.transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=GenericUUID(int=1), - email="admin@example.com", - password="admin", - access_token="token", - is_superuser=True, - ) - - # act - response = api_client.post( - "/token", data={"username": "john@example.com", "password": "password"} - ) - - # assert - assert response.status_code == 400 - - -@pytest.mark.integration -def test_login_with_invalid_password_returns_400(app, api_client): - # arrange - with app.transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=GenericUUID(int=1), - email="admin@example.com", - password="admin", - access_token="token", - is_superuser=True, - ) - - # act - response = api_client.post( - "/token", - data={"username": "admin@example.com", "password": "incorrect_password"}, - ) - - # assert - assert response.status_code == 400 diff --git a/src/cli/README.md b/src/cli/README.md deleted file mode 100644 index 1abda9a..0000000 --- a/src/cli/README.md +++ /dev/null @@ -1,22 +0,0 @@ -This is a sample command line script to print all listings - -1. Start the database - -``` -docker-compose -f docker-compose.dev.yml up -``` - -2. Apply all migrations - -``` -cd src -alembic upgrade head -``` - -3. Run the script (from src directory): - -``` -python -m cli -``` - - diff --git a/src/cli/__init__.py b/src/cli/__init__.py deleted file mode 100644 index 38ed7e6..0000000 --- a/src/cli/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import sys -from pathlib import Path - -# add base project path to PYTHONPATH -BASE_DIR = Path(__file__).resolve().parent.parent -sys.path.append(str(BASE_DIR)) diff --git a/src/cli/__main__.py b/src/cli/__main__.py deleted file mode 100644 index 02d9b96..0000000 --- a/src/cli/__main__.py +++ /dev/null @@ -1,62 +0,0 @@ -import uuid - -from config.container import TopLevelContainer -from modules.catalog.application.command import CreateListingDraftCommand -from modules.catalog.application.query import GetAllListings -from modules.catalog.domain.repositories import ListingRepository -from modules.catalog.infrastructure.listing_repository import Base -from seedwork.domain.value_objects import Money -from seedwork.infrastructure.logging import LoggerFactory, logger - -# a sample command line script to print all listings -# run with "cd src && python -m cli" - -# configure logger prior to first usage -LoggerFactory.configure(logger_name="cli") - -container = TopLevelContainer() -container.config.from_dict( - dict( - # DATABASE_URL="sqlite+pysqlite:///:memory:", - DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres", - DATABASE_ECHO=False, - DEBUG=True, - ) -) - -# let's create the database schema -engine = container.db_engine() -Base.metadata.create_all(engine) - -# let's create a new application instance -app = container.application() - - -# let's query the listings, this method implicitly creates a transaction context and then executes a query -# see `get_all_listings` query handler in `src/modules/catalog/application/query/get_all_listings.py` -query_result = app.execute_query(GetAllListings()) - -# now let's print the listings -listings = query_result.payload -print("Listings:") -for listing in listings: - print(f"{listing['id']} - {listing['title']}") - -# now we are explicitly creating a transaction context, this time we want to execute a command -with app.transaction_context() as ctx: - # see `create_listing_draft` command handler in `src/modules/catalog/application/command/create_listing_draft.py` - ctx.execute( - CreateListingDraftCommand( - listing_id=uuid.uuid4(), - title="First listing", - description="...", - ask_price=Money(100), - seller_id=uuid.UUID(int=1), - ) - ) - -# use transaction context to access any dependency (i.e a repository, a service, etc.) -with app.transaction_context() as ctx: - listing_repository = ctx.get_service(ListingRepository) - listing_count = listing_repository.count() - logger.info(f"There are {listing_count} listings in the database") diff --git a/src/cli/dependencies.py b/src/cli/dependencies.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/config/api_config.py b/src/config/api_config.py deleted file mode 100644 index eb22405..0000000 --- a/src/config/api_config.py +++ /dev/null @@ -1,22 +0,0 @@ -from pydantic import Field -from pydantic_settings import BaseSettings - -# env_filename = os.getenv("ENV_FILENAME", ".env") - - -class ApiConfig(BaseSettings): - """ - All API Settings are here - """ - - APP_NAME: str = "Online Auctions API" - DEBUG: bool = Field(default=True) - DATABASE_ECHO: bool = Field(default=False) - DATABASE_URL: str = Field( - default="postgresql://postgres:password@localhost:5432/postgres", - ) - LOGGER_NAME: str = "api" - - -# SECRET_KEY = config("SECRET_KEY", cast=Secret, default="secret") -# ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=CommaSeparatedStrings, default="*") diff --git a/src/config/container.py b/src/config/container.py deleted file mode 100644 index eb1a6b0..0000000 --- a/src/config/container.py +++ /dev/null @@ -1,244 +0,0 @@ -import asyncio -import copy -import inspect -import json -import uuid -from typing import Optional -from uuid import UUID - -from dependency_injector import containers, providers -from dependency_injector.containers import Container -from dependency_injector.providers import Dependency, Factory, Provider, Singleton -from dependency_injector.wiring import Provide, inject # noqa -from lato import Application, DependencyProvider, TransactionContext -from pydantic_settings import BaseSettings -from sqlalchemy import create_engine -from sqlalchemy.orm import Session - -from modules.bidding.application import bidding_module -from modules.bidding.infrastructure.listing_repository import ( - PostgresJsonListingRepository as BiddingPostgresJsonListingRepository, -) -from modules.catalog.application import catalog_module -from modules.catalog.infrastructure.listing_repository import ( - PostgresJsonListingRepository as CatalogPostgresJsonListingRepository, -) -from modules.iam.application.services import IamService -from modules.iam.infrastructure.repository import PostgresJsonUserRepository -from seedwork.application.inbox_outbox import InMemoryOutbox -from seedwork.infrastructure.logging import Logger, logger - - -def _default(val): - import uuid - - if isinstance(val, uuid.UUID): - return str(val) - raise TypeError() - - -def dumps(d): - return json.dumps(d, default=_default) - - -def create_db_engine(config): - engine = create_engine( - config.DATABASE_URL, echo=config.DATABASE_ECHO, json_serializer=dumps - ) - from seedwork.infrastructure.database import Base - - # TODO: it seems like a hack, but it works... - Base.metadata.bind = engine - return engine - - -def create_application(db_engine) -> Application: - """Creates new instance of the application""" - application = Application( - "BiddingApp", - app_version=0.1, - db_engine=db_engine, - ) - application.include_submodule(catalog_module) - application.include_submodule(bidding_module) - - @application.on_create_transaction_context - def on_create_transaction_context(**kwargs): - engine = application.get_dependency("db_engine") - session = Session(engine) - correlation_id = uuid.uuid4() - logger.correlation_id.set(uuid.uuid4()) # type: ignore - - # create IoC container for the transaction - dependency_provider = ContainerProvider( - TransactionContainer( - db_session=session, correlation_id=correlation_id, logger=logger - ) - ) - - return TransactionContext(dependency_provider) - - @application.on_enter_transaction_context - def on_enter_transaction_context(ctx: TransactionContext): - ctx.set_dependencies(publish=ctx.publish) - logger.debug("Entering transaction") - - @application.on_exit_transaction_context - def on_exit_transaction_context( - ctx: TransactionContext, exception: Optional[Exception] = None - ): - session = ctx["db_session"] - if exception: - session.rollback() - logger.warning(f"rollback due to {exception}") - - # from pydantic import ValidationError - # if type(exception) not in [ValidationError]: - # raise exception - else: - session.commit() - logger.debug(f"committed") - session.close() - logger.debug(f"transaction ended") - logger.correlation_id.set(uuid.UUID(int=0)) # type: ignore - - @application.transaction_middleware - async def logging_middleware(ctx: TransactionContext, call_next): - description = ( - f"{ctx.current_action[1]} -> {repr(ctx.current_action[0])}" - if ctx.current_action - else "" - ) - logger.debug(f"Executing {description}...") - result = call_next() - if asyncio.iscoroutine(result): - result = await result - logger.debug(f"Finished executing {description}") - return result - - @application.transaction_middleware - async def event_collector_middleware(ctx: TransactionContext, call_next): - handler_kwargs = call_next.keywords - - result = call_next() - if asyncio.iscoroutine(result): - result = await result - - logger.debug(f"Collecting event from {ctx['message'].__class__}") - - domain_events = [] - repositories = filter( - lambda x: hasattr(x, "collect_events"), handler_kwargs.values() - ) - for repo in repositories: - domain_events.extend(repo.collect_events()) - for event in domain_events: - logger.debug(f"Publishing {event}") - await ctx.publish_async(event) - - return result - - return application - - -class ApplicationContainer(containers.DeclarativeContainer): - """Dependency Injection container for the application (application-level dependencies) - see https://github.com/ets-labs/python-dependency-injector for more details - """ - - __self__ = providers.Self() - config = providers.Dependency(instance_of=BaseSettings) - db_engine = providers.Singleton(create_db_engine, config) - application = providers.Singleton(create_application, db_engine) - - -class TransactionContainer(containers.DeclarativeContainer): - """Dependency Injection container for the transaction context (transaction-level dependencies) - Most of the dependencies are singletons, as each transaction receives new transaction container. - """ - - correlation_id = providers.Dependency(instance_of=UUID) - db_session = providers.Dependency(instance_of=Session) - logger = providers.Dependency(instance_of=Logger) - - outbox = providers.Singleton(InMemoryOutbox) - - catalog_listing_repository = providers.Singleton( - CatalogPostgresJsonListingRepository, - db_session=db_session, - ) - - bidding_listing_repository = providers.Singleton( - BiddingPostgresJsonListingRepository, - db_session=db_session, - ) - - user_repository = providers.Singleton( - PostgresJsonUserRepository, - db_session=db_session, - ) - - iam_service = providers.Singleton(IamService, user_repository=user_repository) - - -def resolve_provider_by_type(container: Container, cls: type) -> Optional[Provider]: - def inspect_provider(provider: Provider) -> bool: - if isinstance(provider, (Factory, Singleton)): - return issubclass(provider.cls, cls) - elif isinstance(provider, Dependency): - return issubclass(provider.instance_of, cls) - - return False - - matching_providers = inspect.getmembers( - container, - inspect_provider, - ) - if matching_providers: - if len(matching_providers) > 1: - raise ValueError( - f"Cannot uniquely resolve {cls}. Found {len(providers)} matching resources." - ) - return matching_providers[0][1] - return None - - -class ContainerProvider(DependencyProvider): - """A dependency provider that uses a dependency injector container under the hood""" - - def __init__(self, container: Container): - self.container = container - self.counter = 0 - - def has_dependency(self, identifier: str | type) -> bool: - if isinstance(identifier, type) and resolve_provider_by_type( - self.container, identifier - ): - return True - if type(identifier) is str: - return identifier in self.container.providers - return False - - def register_dependency(self, identifier, dependency_instance): - pr = providers.Object(dependency_instance) - try: - setattr(self.container, identifier, pr) - except TypeError: - setattr(self.container, f"{str(identifier)}-{self.counter}", pr) - self.counter += 1 - - def get_dependency(self, identifier): - try: - if isinstance(identifier, type): - provider = resolve_provider_by_type(self.container, identifier) - else: - provider = getattr(self.container, identifier) - instance = provider() - except Exception as e: - raise e - return instance - - def copy(self, *args, **kwargs): - dp = ContainerProvider(copy.copy(self.container)) - dp.update(*args, **kwargs) - return dp diff --git a/src/conftest.py b/src/conftest.py deleted file mode 100644 index 9283bb4..0000000 --- a/src/conftest.py +++ /dev/null @@ -1,63 +0,0 @@ -import uuid - -import pytest -from fastapi.testclient import TestClient -from sqlalchemy import create_engine -from sqlalchemy.orm import Session - -from api.main import app as fastapi_instance -from config.api_config import ApiConfig -from modules.iam.application.services import IamService -from seedwork.infrastructure.database import Base - - -@pytest.fixture -def engine(): - config = ApiConfig() - eng = create_engine(config.DATABASE_URL, echo=config.DATABASE_ECHO) - - with eng.begin() as connection: - Base.metadata.drop_all(connection) - Base.metadata.create_all(connection) - return eng - - -@pytest.fixture -def db_session(engine): - with Session(engine) as session: - yield session - - -@pytest.fixture -def api(): - return fastapi_instance - - -@pytest.fixture -def api_client(api, app): - client = TestClient(api) - return client - - -@pytest.fixture -def authenticated_api_client(api, app): - access_token = uuid.uuid4() - with app.transaction_context() as ctx: - iam: IamService = ctx[IamService] - current_user = iam.create_user( - uuid.UUID(int=1), - email="user1@example.com", - password="password", - access_token=str(access_token), - is_superuser=False, - ) - headers = {"Authorization": f"bearer {access_token}"} - client = TestClient(api, headers=headers) - client.current_user = current_user - return client - - -@pytest.fixture -def app(api, db_session): - app = api.container.application() - return app diff --git a/src/modules/README.md b/src/modules/README.md deleted file mode 100644 index 9d6462b..0000000 --- a/src/modules/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory contains all modules related to bounded contexts. \ No newline at end of file diff --git a/src/modules/__init__.py b/src/modules/__init__.py deleted file mode 100644 index 5abc475..0000000 --- a/src/modules/__init__.py +++ /dev/null @@ -1 +0,0 @@ -foo = "bar" diff --git a/src/modules/bidding/README.md b/src/modules/bidding/README.md deleted file mode 100644 index f6ee6c1..0000000 --- a/src/modules/bidding/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Bidding bounded context - -Bidding is competitively offering a price that the bidder or the person offering a bid is willing to pay for a commodity this commodity can be anything, cars, bikes, properties, etc. The price offered is called a bid, the person offering the price is called the bidder and the entire phenomenon is known as bidding. -https://www.educba.com/bidding-vs-auction/ - -## Bidding process - -As a Buyer, you can place a bid, which must be greater than the current price + 1 USD and which sets the highest price you are willing to pay for an item. -System will let you know (by email) if someone outbids you, and you can decide if you want to increase your maximum limit. -Sometimes you can be automatically outbid (if some other buyer sets his maximum limit higher that yours). - -For example: -1. Alice wants to sell X. She sets the ask price for this item as 10 USD. -2. Bob wants to place a bid on X. The minimum amount is 10 USD (the ask price), and he places the bid of 15 USD. As the only bidder, he is the winner and the current price for X is 10 USD. -3. Alice and Bob are notified by email. -4. Charlie places his bid, but now the minimum price he can bid is 11 USD, so he decides to bid 12 USD. As this is not enough to outbid Bob, he is not the winner, and the price is increased to 12 USD. -5. Charlie places another bid, this time with the amount of 20 USD. As this is more than Bob's maximum limit, Charlie is the winner and the price is increased to 16 USD. -6. Alice is notified of new winner and price for X. Bob is notified by email that he is outbid. -7. Charlie decides to increase his maximum limit to 25 USD by bidding again with the increased amount. -8. Now if Bob wants to win the bidding, he must place a bid of at least 26 USD. - - -## Ubiquitous language - -**Listing** - a product for sale - -**Bidding** - a competitive process of offering a price for listing of a commodity for sale - -**Seller** - the person selling a listing - -**Bidder** - the person offering the price is called the bidder - -**Bid** - a maximum price offered by a bidder to purchase a listing - - -## User stories: - -- [x] As a bidder, I want to place a bid. - -- [x] As a bidder, I want to retract a bid. -- -- [ ] As a seller, I want to cancel my listing immediately. \ No newline at end of file diff --git a/src/modules/bidding/application/__init__.py b/src/modules/bidding/application/__init__.py deleted file mode 100644 index 216f805..0000000 --- a/src/modules/bidding/application/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from lato import ApplicationModule -import importlib - - -bidding_module = ApplicationModule("bidding") -importlib.import_module("modules.bidding.application.command") -importlib.import_module("modules.bidding.application.query") -importlib.import_module("modules.bidding.application.event") diff --git a/src/modules/bidding/application/command/__init__.py b/src/modules/bidding/application/command/__init__.py deleted file mode 100644 index e49b5d1..0000000 --- a/src/modules/bidding/application/command/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .place_bid import PlaceBidCommand -from .retract_bid import RetractBidCommand diff --git a/src/modules/bidding/application/command/place_bid.py b/src/modules/bidding/application/command/place_bid.py deleted file mode 100644 index bafac91..0000000 --- a/src/modules/bidding/application/command/place_bid.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass - -from modules.bidding.application import bidding_module -from modules.bidding.domain.repositories import ListingRepository -from modules.bidding.domain.value_objects import Bid, Bidder, Money -from seedwork.application.commands import Command -from seedwork.domain.value_objects import GenericUUID - - -class PlaceBidCommand(Command): - listing_id: GenericUUID - bidder_id: GenericUUID - amount: int # todo: Decimal - currency: str = "USD" - - -@bidding_module.handler(PlaceBidCommand) -def place_bid( - command: PlaceBidCommand, listing_repository: ListingRepository -): - bidder = Bidder(id=command.bidder_id) - bid = Bid(bidder=bidder, max_price=Money(command.amount)) - - listing = listing_repository.get_by_id(command.listing_id) - listing.place_bid(bid) diff --git a/src/modules/bidding/application/command/retract_bid.py b/src/modules/bidding/application/command/retract_bid.py deleted file mode 100644 index 34d67db..0000000 --- a/src/modules/bidding/application/command/retract_bid.py +++ /dev/null @@ -1,21 +0,0 @@ -from modules.bidding.application import bidding_module -from modules.bidding.domain.entities import Listing -from modules.bidding.domain.repositories import ListingRepository -from modules.bidding.domain.value_objects import Bidder -from seedwork.application.commands import Command -from seedwork.domain.value_objects import GenericUUID - - -class RetractBidCommand(Command): - listing_id: GenericUUID - bidder_id: GenericUUID - - -@bidding_module.handler(RetractBidCommand) -def retract_bid( - command: RetractBidCommand, listing_repository: ListingRepository -): - bidder = Bidder(id=command.bidder_id) - - listing: Listing = listing_repository.get_by_id(id=command.listing_id) - listing.retract_bid_of(bidder) diff --git a/src/modules/bidding/application/event/__init__.py b/src/modules/bidding/application/event/__init__.py deleted file mode 100644 index ff1954c..0000000 --- a/src/modules/bidding/application/event/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .notify_outbid_winner import notify_outbid_winner -from .when_listing_is_published_start_auction import ( - when_listing_is_published_start_auction, -) diff --git a/src/modules/bidding/application/event/notify_outbid_winner.py b/src/modules/bidding/application/event/notify_outbid_winner.py deleted file mode 100644 index 347a7fd..0000000 --- a/src/modules/bidding/application/event/notify_outbid_winner.py +++ /dev/null @@ -1,8 +0,0 @@ -from modules.bidding.application import bidding_module -from modules.bidding.domain.events import BidWasPlaced -from seedwork.infrastructure.logging import logger - - -@bidding_module.handler(BidWasPlaced) -def notify_outbid_winner(event: BidWasPlaced): - logger.info(f"Message from a handler: Listing {event.listing_id} was published") diff --git a/src/modules/bidding/application/event/when_listing_is_published_start_auction.py b/src/modules/bidding/application/event/when_listing_is_published_start_auction.py deleted file mode 100644 index ddd4f91..0000000 --- a/src/modules/bidding/application/event/when_listing_is_published_start_auction.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime, timedelta - -from modules.bidding.application import bidding_module -from modules.bidding.domain.entities import Listing -from modules.bidding.domain.repositories import ListingRepository -from modules.bidding.domain.value_objects import Seller -from modules.catalog.domain.events import ListingPublishedEvent - - -@bidding_module.handler(ListingPublishedEvent) -def when_listing_is_published_start_auction( - event: ListingPublishedEvent, listing_repository: ListingRepository -): - listing = Listing( - id=event.listing_id, - seller=Seller(id=event.seller_id), - ask_price=event.ask_price, - starts_at=datetime.now(), - ends_at=datetime.now() + timedelta(days=7), - ) - listing_repository.add(listing) diff --git a/src/modules/bidding/application/query/__init__.py b/src/modules/bidding/application/query/__init__.py deleted file mode 100644 index 6c69791..0000000 --- a/src/modules/bidding/application/query/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .get_bidding_details import GetBiddingDetails -from .get_pastdue_listings import GetPastdueListings diff --git a/src/modules/bidding/application/query/get_bidding_details.py b/src/modules/bidding/application/query/get_bidding_details.py deleted file mode 100644 index 512de18..0000000 --- a/src/modules/bidding/application/query/get_bidding_details.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass - -from sqlalchemy.orm import Session - -from modules.bidding.application import bidding_module -from modules.bidding.application.query.model_mappers import ( - ListingDAO, - map_listing_model_to_dao, -) -from modules.bidding.infrastructure.listing_repository import ListingModel -from seedwork.application.queries import Query -from seedwork.application.query_handlers import QueryResult -from seedwork.domain.value_objects import GenericUUID - - -class GetBiddingDetails(Query): - listing_id: GenericUUID - - -@bidding_module.handler(GetBiddingDetails) -def get_bidding_details( - query: GetBiddingDetails, - session: Session, -) -> ListingDAO: - listing_model = ( - session.query(ListingModel).filter_by(id=str(query.listing_id)).one() - ) - dao = map_listing_model_to_dao(listing_model) - return dao diff --git a/src/modules/bidding/application/query/get_pastdue_listings.py b/src/modules/bidding/application/query/get_pastdue_listings.py deleted file mode 100644 index a9f936e..0000000 --- a/src/modules/bidding/application/query/get_pastdue_listings.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass, field -from datetime import datetime - -from modules.bidding.application import bidding_module -from modules.bidding.domain.repositories import ListingRepository -from seedwork.application.queries import Query -from seedwork.application.query_handlers import QueryResult - - -class GetPastdueListings(Query): - now: datetime = field(default_factory=datetime.utcnow) - - -@bidding_module.handler(GetPastdueListings) -def get_past_due_listings( - query: GetPastdueListings, listing_repository: ListingRepository -): - # TODO: not yet implemented - return [] diff --git a/src/modules/bidding/application/query/model_mappers.py b/src/modules/bidding/application/query/model_mappers.py deleted file mode 100644 index 752bcaf..0000000 --- a/src/modules/bidding/application/query/model_mappers.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime - -from pydantic import BaseModel - -from modules.bidding.infrastructure.listing_repository import ListingModel -from seedwork.domain.value_objects import GenericUUID - - -class ListingDAO(BaseModel): - id: GenericUUID - ends_at: datetime - bids: list - - -def map_listing_model_to_dao(instance: ListingModel): - """maps ListingModel to a data access object (a dictionary)""" - data = instance.data - return ListingDAO( - id=instance.id, - ends_at=data["ends_at"], - bids=[], - ) diff --git a/src/modules/bidding/domain/entities.py b/src/modules/bidding/domain/entities.py deleted file mode 100644 index 8ade934..0000000 --- a/src/modules/bidding/domain/entities.py +++ /dev/null @@ -1,172 +0,0 @@ -from dataclasses import dataclass, field -from datetime import datetime, timedelta -from typing import Optional - -from modules.bidding.domain.events import ( - BidWasPlaced, - BidWasRetracted, - HighestBidderWasOutbid, - ListingWasCancelled, -) -from modules.bidding.domain.rules import ( - BidCanBeRetracted, - ListingCanBeCancelled, - PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice, -) -from modules.bidding.domain.value_objects import Bid, Bidder, Seller -from seedwork.domain.entities import AggregateRoot -from seedwork.domain.events import DomainEvent -from seedwork.domain.exceptions import DomainException -from seedwork.domain.value_objects import GenericUUID, Money - - -class BidderIsNotBiddingListing(DomainException): - ... - - -class BidCannotBeRetracted(DomainException): - ... - - -class ListingCannotBeCancelled(DomainException): - ... - - -@dataclass(kw_only=True) -class Listing(AggregateRoot[GenericUUID]): - seller: Seller - ask_price: Money - starts_at: datetime - ends_at: datetime - bids: list[Bid] = field(default_factory=list) - - # public queries - @property - def current_price(self) -> Money: - """The current price is the price buyers are competing against""" - if len(self.bids) < 2: - return self.ask_price - - sorted_prices = sorted([bid.max_price for bid in self.bids], reverse=True) - return sorted_prices[1] - - @property - def next_minimum_price(self) -> Money: - return self.current_price + Money(1, currency=self.ask_price.currency) - - # public commands - def place_bid(self, bid: Bid): - """Public method""" - self.check_rule( - PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice( - current_price=bid.max_price, next_minimum_price=self.next_minimum_price - ) - ) - - previous_winner_id = self.highest_bid.bidder.id if self.highest_bid else None - current_winner_id = bid.bidder.id - - if self.has_bid_placed_by(bidder=bid.bidder): - self._update_bid(bid) - else: - self._add_bid(bid) - - self.register_event( - BidWasPlaced( - listing_id=self.id, - bidder_id=bid.bidder.id, - ) - ) - - # if there was previous winner... - if previous_winner_id is not None and previous_winner_id != current_winner_id: - self.register_event( - HighestBidderWasOutbid( - listing_id=self.id, - outbid_bidder_id=previous_winner_id, - ) - ) - - def retract_bid_of(self, bidder: Bidder): - """Public method""" - bid = self.get_bid_of(bidder) - self.check_rule( - BidCanBeRetracted(listing_ends_at=self.ends_at, bid_placed_at=bid.placed_at) - ) - - self._remove_bid_of(bidder=bidder) - self.register_event( - BidWasRetracted( - listing_id=self.id, - retracting_bidder_id=bidder.id, - winning_bidder_id=self.highest_bid.bidder.id - if self.highest_bid - else None, - ) - ) - - def cancel(self): - """ - Seller can cancel a listing (end a listing early). Listing must be eligible to cancelled, - depending on time left and if bids have been placed. - """ - self.check_rule( - ListingCanBeCancelled( - time_left_in_listing=self.time_left_in_listing, - no_bids_were_placed=len(self.bids) == 0, - ) - ) - self.ends_at = datetime.utcnow() - self.register_event(ListingWasCancelled(listing_id=self.id)) - - def end(self) -> DomainEvent: - """ - Ends listing. - """ - raise NotImplementedError() - - # public queries - def get_bid_of(self, bidder: Bidder) -> Bid: - try: - bid = next(filter(lambda bid: bid.bidder == bidder, self.bids)) - except StopIteration as e: - raise BidderIsNotBiddingListing() from e - return bid - - def has_bid_placed_by(self, bidder: Bidder) -> bool: - """Checks if listing has a bid placed by a bidder""" - try: - self.get_bid_of(bidder=bidder) - except BidderIsNotBiddingListing: - return False - return True - - @property - def highest_bid(self) -> Optional[Bid]: - try: - highest_bid = max(self.bids, key=lambda bid: bid.max_price) - except ValueError: - # nobody is bidding - return None - return highest_bid - - @property - def time_left_in_listing(self): - now = datetime.utcnow() - zero_seconds = timedelta() - return max(self.ends_at - now, zero_seconds) - - # private commands and queries - def _add_bid(self, bid: Bid): - assert not self.has_bid_placed_by( - bidder=bid.bidder - ), "Only one bid of a bidder is allowed" - self.bids.append(bid) - - def _update_bid(self, bid: Bid): - self.bids = [ - bid if bid.bidder == existing.bidder else existing for existing in self.bids - ] - - def _remove_bid_of(self, bidder: Bidder): - self.bids = [bid for bid in self.bids if bid.bidder != bidder] diff --git a/src/modules/bidding/domain/events.py b/src/modules/bidding/domain/events.py deleted file mode 100644 index fdb930e..0000000 --- a/src/modules/bidding/domain/events.py +++ /dev/null @@ -1,20 +0,0 @@ -from seedwork.domain.events import DomainEvent -from seedwork.domain.value_objects import GenericUUID - - -class BidWasPlaced(DomainEvent): - listing_id: GenericUUID - bidder_id: GenericUUID - - -class HighestBidderWasOutbid(DomainEvent): - listing_id: GenericUUID - outbid_bidder_id: GenericUUID - - -class BidWasRetracted(DomainEvent): - ... - - -class ListingWasCancelled(DomainEvent): - ... diff --git a/src/modules/bidding/domain/exceptions.py b/src/modules/bidding/domain/exceptions.py deleted file mode 100644 index 571f823..0000000 --- a/src/modules/bidding/domain/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ -from seedwork.domain.exceptions import DomainException - - -class BidCannotBePlacedException(DomainException): - ... diff --git a/src/modules/bidding/domain/repositories.py b/src/modules/bidding/domain/repositories.py deleted file mode 100644 index 1894cdc..0000000 --- a/src/modules/bidding/domain/repositories.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABC - -from modules.bidding.domain.entities import GenericUUID, Listing -from seedwork.domain.repositories import GenericRepository - - -class ListingRepository(GenericRepository[GenericUUID, Listing], ABC): - """An interface for Listing repository""" diff --git a/src/modules/bidding/domain/rules.py b/src/modules/bidding/domain/rules.py deleted file mode 100644 index 47ddaa4..0000000 --- a/src/modules/bidding/domain/rules.py +++ /dev/null @@ -1,54 +0,0 @@ -from datetime import datetime, timedelta - -from pydantic import Field - -from seedwork.domain.rules import BusinessRule -from seedwork.domain.value_objects import Money - - -class PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice(BusinessRule): - __message = "Placed bid must be greater or equal than {next_minimum_price}" - - current_price: Money - next_minimum_price: Money - - def is_broken(self) -> bool: - return self.current_price < self.next_minimum_price - - def get_message(self) -> str: - return self.__message.format(next_minimum_price=self.next_minimum_price) - - -class BidCanBeRetracted(BusinessRule): - __message = "Bid cannot be retracted" - - listing_ends_at: datetime - bid_placed_at: datetime - now: datetime = Field(default_factory=datetime.utcnow) - - def is_broken(self) -> bool: - time_left_in_listing = self.now - self.listing_ends_at - time_since_placed = self.now - self.bid_placed_at - less_than_12_hours_before_bidding_ends = time_left_in_listing < timedelta( - hours=12 - ) - less_than_1_hour_since_bid_was_placed = time_since_placed < timedelta(hours=1) - - return ( - less_than_12_hours_before_bidding_ends - and less_than_1_hour_since_bid_was_placed - ) - - -class ListingCanBeCancelled(BusinessRule): - __message = "Listing cannot be cancelled" - - time_left_in_listing: timedelta - no_bids_were_placed: int - - def is_broken(self) -> bool: - can_be_cancelled = self.time_left_in_listing > timedelta(hours=12) or ( - self.time_left_in_listing <= timedelta(hours=12) - and self.no_bids_were_placed - ) - return not can_be_cancelled diff --git a/src/modules/bidding/domain/services.py b/src/modules/bidding/domain/services.py deleted file mode 100644 index 6dc392e..0000000 --- a/src/modules/bidding/domain/services.py +++ /dev/null @@ -1,26 +0,0 @@ -# TODO: Work in progress... - - -# class EndListingPolicy: -# def end_listing(self, listing): -# specification = EndListingSpecification() -# if specification.is_satisfied_by(listing): -# command = EndListingCommand(...) -# dispatch(command) -# -# -# class EndingListingService(ApplicationService): -# def end_pastdue_listings(self): -# listing_ids = get_listing_ids_for_overdue_listings() -# -# # v1 -# for listing_id in listing_ids: -# listing = listing_repository.get_by_id(listing_id) -# events = listing.end() -# events_publisher.publish(events) -# -# # v2 -# policy = EndListingPolicy() -# for listing_id in listing_ids: -# listing = listing_repository.get_by_id(listing_id) -# policy.end_listing(listing) diff --git a/src/modules/bidding/domain/value_objects.py b/src/modules/bidding/domain/value_objects.py deleted file mode 100644 index 7b80d9a..0000000 --- a/src/modules/bidding/domain/value_objects.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime - -from dataclasses import dataclass, field - -from seedwork.domain.value_objects import GenericUUID, Money, ValueObject - - -@dataclass(frozen=True) -class Bidder(ValueObject): - id: GenericUUID - - -@dataclass(frozen=True) -class Seller(ValueObject): - id: GenericUUID - - -@dataclass(frozen=True) -class Bid(ValueObject): - max_price: Money # a maximum price that a bidder is willing to pay - bidder: Bidder - placed_at: datetime = field(default_factory=datetime.utcnow) diff --git a/src/modules/bidding/infrastructure/listing_repository.py b/src/modules/bidding/infrastructure/listing_repository.py deleted file mode 100644 index ca49890..0000000 --- a/src/modules/bidding/infrastructure/listing_repository.py +++ /dev/null @@ -1,103 +0,0 @@ -import datetime -import uuid - -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.sql.schema import Column -from sqlalchemy_json import mutable_json_type -from sqlalchemy_utils import UUIDType - -from modules.bidding.domain.entities import Bid, Bidder, Listing, Money, Seller -from modules.bidding.domain.repositories import ListingRepository -from seedwork.domain.value_objects import GenericUUID -from seedwork.infrastructure.database import Base -from seedwork.infrastructure.repository import SqlAlchemyGenericRepository - -""" -References: -"Introduction to SQLAlchemy 2020 (Tutorial)" by: Mike Bayer -https://youtu.be/sO7FFPNvX2s?t=7214 -""" - - -class ListingModel(Base): - """Data model for listing domain object in the bidding context""" - - __tablename__ = "bidding_listing" - id = Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) - data = Column(mutable_json_type(dbtype=JSONB, nested=True)) - - -def serialize_money(money: Money) -> dict: - return { - "amount": money.amount, - "currency": money.currency, - } - - -def serialize_id(value: GenericUUID) -> str: - return str(value) - - -def deserialize_id(value: str) -> GenericUUID: - if isinstance(value, uuid.UUID): - return GenericUUID(value.hex) - return GenericUUID(value) - - -def deserialize_money(data: dict) -> Money: - return Money(data["amount"], currency=data["currency"]) - - -def serialize_datetime(value: datetime.datetime) -> str: - return value.isoformat() - - -def deserialize_datetime(value: str) -> datetime.datetime: - return datetime.datetime.fromisoformat(value) - - -def serialize_bid(bid: Bid) -> dict: - return { - "bidder_id": serialize_id(bid.bidder.id), - "max_price": serialize_money(bid.max_price), - "placed_at": serialize_datetime(bid.placed_at), - } - - -def deserialize_bid(data: dict) -> Bid: - return Bid( - bidder=Bidder(id=deserialize_id(data["bidder_id"])), - max_price=deserialize_money(data["max_price"]), - placed_at=deserialize_datetime(data["placed_at"]), - ) - - -class ListingDataMapper: - def model_to_entity(self, instance: ListingModel) -> Listing: - d = instance.data - return Listing( - id=deserialize_id(instance.id), - seller=Seller(id=deserialize_id(d["seller_id"])), - ask_price=deserialize_money(d["ask_price"]), - starts_at=deserialize_datetime(d["starts_at"]), - ends_at=deserialize_datetime(d["ends_at"]), - ) - - def entity_to_model(self, entity: Listing) -> ListingModel: - return ListingModel( - id=entity.id, - data={ - "starts_at": serialize_datetime(entity.starts_at), - "ends_at": serialize_datetime(entity.ends_at), - "ask_price": serialize_money(entity.ask_price), - "seller_id": serialize_id(entity.seller.id), - "bids": [serialize_bid(b) for b in entity.bids], - }, - ) - - -class PostgresJsonListingRepository(SqlAlchemyGenericRepository, ListingRepository): - """Listing repository implementation""" - - mapper_class = ListingDataMapper - model_class = ListingModel diff --git a/src/modules/bidding/tests/__init__.py b/src/modules/bidding/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/bidding/tests/application/test_create_listing_when_draft_is_published.py b/src/modules/bidding/tests/application/test_create_listing_when_draft_is_published.py deleted file mode 100644 index 4c77891..0000000 --- a/src/modules/bidding/tests/application/test_create_listing_when_draft_is_published.py +++ /dev/null @@ -1,24 +0,0 @@ -import pytest - -from modules.bidding.domain.repositories import ( - ListingRepository as BiddingListingRepository, -) -from modules.catalog.domain.events import ListingPublishedEvent -from seedwork.domain.value_objects import GenericUUID, Money - - -@pytest.mark.integration -@pytest.mark.asyncio -async def test_create_listing_on_draft_published_event(app, engine): - listing_id = GenericUUID(int=1) - await app.publish_async( - ListingPublishedEvent( - listing_id=listing_id, - seller_id=GenericUUID.next_id(), - ask_price=Money(10), - ) - ) - - with app.transaction_context() as ctx: - listing_repository = ctx[BiddingListingRepository] - assert listing_repository.count() == 1 diff --git a/src/modules/bidding/tests/domain/__init__.py b/src/modules/bidding/tests/domain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/bidding/tests/domain/test_bidding.py b/src/modules/bidding/tests/domain/test_bidding.py deleted file mode 100644 index 27371a3..0000000 --- a/src/modules/bidding/tests/domain/test_bidding.py +++ /dev/null @@ -1,241 +0,0 @@ -from datetime import datetime, timedelta - -import pytest - -from modules.bidding.domain.entities import Listing -from modules.bidding.domain.value_objects import Bid, Bidder, Money, Seller -from seedwork.domain.exceptions import BusinessRuleValidationException -from seedwork.domain.value_objects import GenericUUID - - -@pytest.mark.unit -def test_listing_initial_price(): - seller = Seller(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - assert listing.highest_bid is None - - -@pytest.mark.unit -def test_place_one_bid(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - bid = Bid(max_price=Money(20), bidder=bidder, placed_at=now) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - listing.place_bid(bid) - assert listing.highest_bid == Bid(max_price=Money(20), bidder=bidder, placed_at=now) - assert listing.current_price == Money(10) - - -@pytest.mark.unit -def test_place_two_bids_second_buyer_outbids(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID(int=1)) - bidder1 = Bidder(id=GenericUUID(int=2)) - bidder2 = Bidder(id=GenericUUID(int=3)) - listing = Listing( - id=GenericUUID(int=4), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - assert listing.current_price == Money(10) - assert listing.next_minimum_price == Money(11) - - # bidder1 places a bid - listing.place_bid(Bid(bidder=bidder1, max_price=Money(15), placed_at=now)) - assert listing.current_price == Money(10) - assert listing.next_minimum_price == Money(11) - - # bidder2 successfully outbids bidder1 - listing.place_bid(Bid(bidder=bidder2, max_price=Money(30), placed_at=now)) - assert listing.current_price == Money(15) - assert listing.next_minimum_price == Money(16) - assert listing.highest_bid == Bid(Money(30), bidder=bidder2, placed_at=now) - - -@pytest.mark.unit -def test_place_two_bids_second_buyer_fails_to_outbid(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID(int=1)) - bidder1 = Bidder(id=GenericUUID(int=2)) - bidder2 = Bidder(id=GenericUUID(int=3)) - listing = Listing( - id=GenericUUID(int=4), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - - # bidder1 places a bid - listing.place_bid(Bid(bidder=bidder1, max_price=Money(30), placed_at=now)) - assert listing.current_price == Money(10) - assert listing.next_minimum_price == Money(11) - - # bidder2 tries to outbid bidder1... - listing.place_bid(Bid(bidder=bidder2, max_price=Money(20), placed_at=now)) - - # ...but he fails. bidder1 is still a winner, but current price changes - assert listing.highest_bid == Bid(Money(30), bidder=bidder1, placed_at=now) - assert listing.current_price == Money(20) - - -@pytest.mark.unit -def test_place_two_bids_second_buyer_fails_to_outbid_with_same_amount(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID(int=1)) - bidder1 = Bidder(id=GenericUUID(int=2)) - bidder2 = Bidder(id=GenericUUID(int=3)) - listing = Listing( - id=GenericUUID(int=4), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - listing.place_bid(Bid(bidder=bidder1, max_price=Money(30), placed_at=now)) - listing.place_bid(Bid(bidder=bidder2, max_price=Money(30), placed_at=now)) - assert listing.highest_bid == Bid(Money(30), bidder=bidder1, placed_at=now) - assert listing.current_price == Money(30) - - -@pytest.mark.unit -def test_place_two_bids_by_same_bidder(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - listing.place_bid(Bid(max_price=Money(20), bidder=bidder, placed_at=now)) - listing.place_bid(Bid(max_price=Money(30), bidder=bidder, placed_at=now)) - - assert len(listing.bids) == 1 - assert listing.highest_bid == Bid(max_price=Money(30), bidder=bidder, placed_at=now) - assert listing.current_price == Money(10) - - -@pytest.mark.unit -def test_cannot_place_bid_if_listing_ended(): - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - bid = Bid( - max_price=Money(10), - bidder=bidder, - placed_at=datetime.utcnow() + timedelta(seconds=1), - ) - with pytest.raises( - BusinessRuleValidationException, - match="PriceOfPlacedBidMustBeGreaterOrEqualThanNextMinimumPrice", - ): - listing.place_bid(bid) - - -@pytest.mark.unit -def test_retract_bid(): - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=datetime.utcnow(), - ends_at=datetime.utcnow(), - ) - bid = Bid( - max_price=Money(100), - bidder=bidder, - placed_at=datetime.utcnow() - timedelta(seconds=1), - ) - listing.place_bid(bid) - with pytest.raises(BusinessRuleValidationException, match="BidCanBeRetracted"): - listing.retract_bid_of(bidder=bidder) - - -@pytest.mark.unit -def test_cancel_listing(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=now, - ends_at=now + timedelta(days=10), - ) - - listing.cancel() - - assert listing.time_left_in_listing == timedelta() - - -@pytest.mark.unit -def test_can_cancel_listing_with_bids(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=now, - ends_at=now + timedelta(days=10), - ) - bid = Bid( - max_price=Money(100), - bidder=bidder, - placed_at=now, - ) - listing.place_bid(bid) - - listing.cancel() - - assert listing.time_left_in_listing == timedelta() - - -@pytest.mark.unit -def test_cannot_cancel_listing_with_bids(): - now = datetime.utcnow() - seller = Seller(id=GenericUUID.next_id()) - bidder = Bidder(id=GenericUUID.next_id()) - listing = Listing( - id=Listing.next_id(), - seller=seller, - ask_price=Money(10), - starts_at=now, - ends_at=now + timedelta(hours=1), - ) - bid = Bid( - max_price=Money(100), - bidder=bidder, - placed_at=now, - ) - listing.place_bid(bid) - - with pytest.raises(BusinessRuleValidationException, match="ListingCanBeCancelled"): - listing.cancel() diff --git a/src/modules/bidding/tests/infrastructure/__init__.py b/src/modules/bidding/tests/infrastructure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/bidding/tests/infrastructure/test_listing_repository.py b/src/modules/bidding/tests/infrastructure/test_listing_repository.py deleted file mode 100644 index 2743edb..0000000 --- a/src/modules/bidding/tests/infrastructure/test_listing_repository.py +++ /dev/null @@ -1,112 +0,0 @@ -import datetime -import uuid - -import pytest - -from modules.bidding.domain.entities import Bid, Bidder, Listing, Money, Seller -from modules.bidding.infrastructure.listing_repository import ( - ListingDataMapper, - ListingModel, - PostgresJsonListingRepository, -) -from seedwork.domain.value_objects import GenericUUID - - -@pytest.mark.integration -def test_listing_repo_is_empty(db_session): - repo = PostgresJsonListingRepository(db_session=db_session) - assert repo.count() == 0 - - -@pytest.mark.unit -def test_listing_data_mapper_maps_entity_to_model(): - listing = Listing( - id=GenericUUID(int=1), - seller=Seller(id=GenericUUID(int=2)), - ask_price=Money(100, "PLN"), - starts_at=datetime.datetime(2020, 12, 1), - ends_at=datetime.datetime(2020, 12, 31), - bids=[ - Bid( - max_price=Money(200, "PLN"), - bidder=Bidder(id=GenericUUID(int=3)), - placed_at=datetime.datetime(2020, 12, 30), - ) - ], - ) - mapper = ListingDataMapper() - - actual = mapper.entity_to_model(listing) - - expected = ListingModel( - id=GenericUUID(int=1), - data={ - "seller_id": "00000000-0000-0000-0000-000000000002", - "ask_price": { - "amount": 100, - "currency": "PLN", - }, - "starts_at": "2020-12-01T00:00:00", - "ends_at": "2020-12-31T00:00:00", - "bids": [ - { - "max_price": { - "amount": 200, - "currency": "PLN", - }, - "bidder_id": "00000000-0000-0000-0000-000000000003", - "placed_at": "2020-12-30T00:00:00", - } - ], - }, - ) - assert actual.id == expected.id - assert actual.data == expected.data - - -@pytest.mark.unit -def test_listing_data_mapper_maps_model_to_entity(): - instance = ListingModel( - id=GenericUUID(int=1), - data={ - "seller_id": "00000000-0000-0000-0000-000000000002", - "ask_price": { - "amount": 100, - "currency": "PLN", - }, - "starts_at": "2020-12-01T00:00:00", - "ends_at": "2020-12-31T00:00:00", - }, - ) - mapper = ListingDataMapper() - - actual = mapper.model_to_entity(instance) - - expected = Listing( - id=GenericUUID(int=1), - seller=Seller(id=GenericUUID("00000000000000000000000000000002")), - ask_price=Money(100, "PLN"), - starts_at=datetime.datetime(2020, 12, 1), - ends_at=datetime.datetime(2020, 12, 31), - ) - assert actual == expected - - -@pytest.mark.integration -def test_listing_persistence(db_session): - original = Listing( - id=Listing.next_id(), - seller=Seller(id=uuid.uuid4()), - ask_price=Money(100, "PLN"), - starts_at=datetime.datetime(2020, 12, 1), - ends_at=datetime.datetime(2020, 12, 31), - ) - repository = PostgresJsonListingRepository(db_session=db_session) - - repository.add(original) - repository.persist_all() - - repository = PostgresJsonListingRepository(db_session=db_session) - persisted = repository.get_by_id(original.id) - - assert original == persisted diff --git a/src/modules/catalog/README.md b/src/modules/catalog/README.md deleted file mode 100644 index c04df04..0000000 --- a/src/modules/catalog/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Listing catalog module - -This part is a seller portal. It allows sellers to create and manage their listings. - -# Business rules - -# User stories: - -- [x] As a seller, I want to create a listing draft for a product I want to sell. - -- [x] As a seller, I want to update a listing draft. - -- [x] As a seller, I want to delete a listing draft. - -- [x] As a seller, I want to publish a listing draft immediately. - -- [ ] As a seller, I want to schedule a listing draft for publishing. - - -- [ ] As a seller, I want to view all my listings (published, unpublished, ended). - -- [ ] As a seller, I want to view details of a listing. - -- [ ] As a system, I want to notify a seller when a listing is published. - -- [ ] As a system, I want to notify a seller when a listing is ended. \ No newline at end of file diff --git a/src/modules/catalog/__init__.py b/src/modules/catalog/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/application/__init__.py b/src/modules/catalog/application/__init__.py deleted file mode 100644 index 8312877..0000000 --- a/src/modules/catalog/application/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from lato import ApplicationModule -import importlib - -catalog_module = ApplicationModule("catalog") -importlib.import_module("modules.catalog.application.command") -importlib.import_module("modules.catalog.application.query") diff --git a/src/modules/catalog/application/command/__init__.py b/src/modules/catalog/application/command/__init__.py deleted file mode 100644 index 5f55e1e..0000000 --- a/src/modules/catalog/application/command/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .create_listing_draft import CreateListingDraftCommand -from .delete_listing_draft import DeleteListingDraftCommand -from .publish_listing_draft import PublishListingDraftCommand -from .update_listing_draft import UpdateListingDraftCommand diff --git a/src/modules/catalog/application/command/create_listing_draft.py b/src/modules/catalog/application/command/create_listing_draft.py deleted file mode 100644 index 2c9ef11..0000000 --- a/src/modules/catalog/application/command/create_listing_draft.py +++ /dev/null @@ -1,32 +0,0 @@ -from lato import Command - -from modules.catalog.application import catalog_module -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.events import ListingDraftCreatedEvent -from modules.catalog.domain.repositories import ListingRepository -from seedwork.domain.value_objects import GenericUUID, Money - - -class CreateListingDraftCommand(Command): - """A command for creating new listing in draft state""" - - listing_id: GenericUUID - title: str - description: str - ask_price: Money - seller_id: GenericUUID - - -@catalog_module.handler(CreateListingDraftCommand) -async def create_listing_draft( - command: CreateListingDraftCommand, repository: ListingRepository, publish -): - listing = Listing( - id=command.listing_id, - title=command.title, - description=command.description, - ask_price=command.ask_price, - seller_id=command.seller_id, - ) - repository.add(listing) - publish(ListingDraftCreatedEvent(listing_id=listing.id)) diff --git a/src/modules/catalog/application/command/delete_listing_draft.py b/src/modules/catalog/application/command/delete_listing_draft.py deleted file mode 100644 index a3cf5a8..0000000 --- a/src/modules/catalog/application/command/delete_listing_draft.py +++ /dev/null @@ -1,36 +0,0 @@ -from dataclasses import dataclass - -from modules.catalog.application import catalog_module -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.events import ListingDraftDeletedEvent -from modules.catalog.domain.repositories import ListingRepository -from modules.catalog.domain.rules import ( - OnlyListingOwnerCanDeleteListing, - PublishedListingMustNotBeDeleted, -) -from seedwork.application.command_handlers import CommandResult -from lato import Command, TransactionContext -from seedwork.domain.mixins import check_rule -from seedwork.domain.value_objects import GenericUUID - - -class DeleteListingDraftCommand(Command): - """A command for deleting a listing""" - - listing_id: GenericUUID - seller_id: GenericUUID - - -@catalog_module.handler(DeleteListingDraftCommand) -def delete_listing_draft( - command: DeleteListingDraftCommand, repository: ListingRepository, publish -): - listing: Listing = repository.get_by_id(command.listing_id) - check_rule( - OnlyListingOwnerCanDeleteListing( - listing_seller_id=listing.seller_id, current_seller_id=command.seller_id - ) - ) - check_rule(PublishedListingMustNotBeDeleted(status=listing.status)) - repository.remove(listing) - publish(ListingDraftDeletedEvent(listing_id=listing.id)) diff --git a/src/modules/catalog/application/command/publish_listing_draft.py b/src/modules/catalog/application/command/publish_listing_draft.py deleted file mode 100644 index ae51b4c..0000000 --- a/src/modules/catalog/application/command/publish_listing_draft.py +++ /dev/null @@ -1,29 +0,0 @@ -from modules.catalog.application import catalog_module -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.repositories import ListingRepository -from modules.catalog.domain.rules import OnlyListingOwnerCanPublishListing -from modules.catalog.domain.value_objects import ListingId, SellerId -from seedwork.application.commands import Command -from seedwork.domain.mixins import check_rule - - -class PublishListingDraftCommand(Command): - """A command for publishing a draft of a listing""" - - listing_id: ListingId # a listing to be published - seller_id: SellerId # a seller, who is publishing a listing - - -@catalog_module.handler(PublishListingDraftCommand) -async def publish_listing_draft( - command: PublishListingDraftCommand, - listing_repository: ListingRepository, -): - listing: Listing = listing_repository.get_by_id(command.listing_id) - - check_rule( - OnlyListingOwnerCanPublishListing( - listing_seller_id=listing.seller_id, current_seller_id=command.seller_id - ) - ) - listing.publish() diff --git a/src/modules/catalog/application/command/update_listing_draft.py b/src/modules/catalog/application/command/update_listing_draft.py deleted file mode 100644 index b41ea76..0000000 --- a/src/modules/catalog/application/command/update_listing_draft.py +++ /dev/null @@ -1,27 +0,0 @@ -from modules.catalog.application import catalog_module -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.repositories import ListingRepository -from lato import Command -from seedwork.domain.value_objects import GenericUUID, Money - - -class UpdateListingDraftCommand(Command): - """A command for updating a listing""" - - listing_id: GenericUUID - title: str - description: str - ask_price: Money - modify_user_id: GenericUUID - - -@catalog_module.handler(UpdateListingDraftCommand) -def update_listing_draft( - command: UpdateListingDraftCommand, repository: ListingRepository -): - listing: Listing = repository.get_by_id(command.listing_id) - listing.change_main_attributes( - title=command.title, - description=command.description, - ask_price=command.ask_price, - ) diff --git a/src/modules/catalog/application/event/__init__.py b/src/modules/catalog/application/event/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/application/event/do_nothing_when_listing_published.py b/src/modules/catalog/application/event/do_nothing_when_listing_published.py deleted file mode 100644 index 7fb1163..0000000 --- a/src/modules/catalog/application/event/do_nothing_when_listing_published.py +++ /dev/null @@ -1,8 +0,0 @@ -from modules.catalog.domain.events import ListingPublishedEvent -from seedwork.infrastructure.logging import logger -from modules.catalog.application import catalog_module - - -@catalog_module.handler(ListingPublishedEvent) -def do_nothing_when_listing_published(event: ListingPublishedEvent): - logger.info(f"Message from a handler: Listing {event.listing_id} was published") diff --git a/src/modules/catalog/application/query/__init__.py b/src/modules/catalog/application/query/__init__.py deleted file mode 100644 index fff9f81..0000000 --- a/src/modules/catalog/application/query/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .get_all_listings import GetAllListings -from .get_listing_details import GetListingDetails -from .get_listings_of_seller import GetListingsOfSeller diff --git a/src/modules/catalog/application/query/get_all_listings.py b/src/modules/catalog/application/query/get_all_listings.py deleted file mode 100644 index 64880d1..0000000 --- a/src/modules/catalog/application/query/get_all_listings.py +++ /dev/null @@ -1,20 +0,0 @@ -from sqlalchemy.orm import Session - -from modules.catalog.application import catalog_module -from modules.catalog.application.query.model_mappers import map_listing_model_to_dao -from modules.catalog.infrastructure.listing_repository import ListingModel -from seedwork.application.queries import Query - - -class GetAllListings(Query): - """This query does not need any parameters""" - - -@catalog_module.handler(GetAllListings) -async def get_all_listings( - query: GetAllListings, - session: Session, -) -> list[ListingModel]: - queryset = session.query(ListingModel) - listings = [map_listing_model_to_dao(row) for row in queryset.all()] - return listings diff --git a/src/modules/catalog/application/query/get_listing_details.py b/src/modules/catalog/application/query/get_listing_details.py deleted file mode 100644 index 7fa52d9..0000000 --- a/src/modules/catalog/application/query/get_listing_details.py +++ /dev/null @@ -1,21 +0,0 @@ -from dataclasses import dataclass - -from sqlalchemy.orm import Session - -from modules.catalog.application import catalog_module -from modules.catalog.application.query.model_mappers import map_listing_model_to_dao -from modules.catalog.infrastructure.listing_repository import ListingModel -from seedwork.application.queries import Query -from seedwork.application.query_handlers import QueryResult -from seedwork.domain.value_objects import GenericUUID - - -class GetListingDetails(Query): - listing_id: GenericUUID - - -@catalog_module.handler(GetListingDetails) -def get_listing_details(query: GetListingDetails, session: Session) -> QueryResult: - row = session.query(ListingModel).filter_by(id=query.listing_id).one() - details = map_listing_model_to_dao(row) - return details diff --git a/src/modules/catalog/application/query/get_listings_of_seller.py b/src/modules/catalog/application/query/get_listings_of_seller.py deleted file mode 100644 index 7bb97c2..0000000 --- a/src/modules/catalog/application/query/get_listings_of_seller.py +++ /dev/null @@ -1,21 +0,0 @@ -from sqlalchemy.orm import Session - -from modules.catalog.application import catalog_module -from modules.catalog.infrastructure.listing_repository import ListingModel -from seedwork.application.queries import Query -from seedwork.application.query_handlers import QueryResult -from seedwork.domain.value_objects import GenericUUID - - -class GetListingsOfSeller(Query): - seller_id: GenericUUID - - -@catalog_module.handler(GetListingsOfSeller) -def get_listings_of_seller(query: GetListingsOfSeller, session: Session) -> list[dict]: - # FIXME: use seller_id to filter out listings - queryset = session.query(ListingModel) # .filter( - # listing_repository.model.data['seller'].astext.cast(UUID) == query.seller_id - # ) - listings = [dict(id=row.id, **row.data) for row in queryset.all()] - return listings diff --git a/src/modules/catalog/application/query/model_mappers.py b/src/modules/catalog/application/query/model_mappers.py deleted file mode 100644 index 19ab808..0000000 --- a/src/modules/catalog/application/query/model_mappers.py +++ /dev/null @@ -1,14 +0,0 @@ -from modules.catalog.infrastructure.listing_repository import ListingModel - - -def map_listing_model_to_dao(instance: ListingModel): - """maps ListingModel to a data access object (a dictionary)""" - data = instance.data - return dict( - id=instance.id, - title=data["title"], - description=data["description"], - ask_price_amount=data["ask_price"]["amount"], - ask_price_currency=data["ask_price"]["currency"], - seller_id=data["seller_id"], - ) diff --git a/src/modules/catalog/domain/__init__.py b/src/modules/catalog/domain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/domain/entities.py b/src/modules/catalog/domain/entities.py deleted file mode 100644 index 767b696..0000000 --- a/src/modules/catalog/domain/entities.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass - -from modules.catalog.domain.events import ( - DomainEvent, - ListingDraftUpdatedEvent, - ListingPublishedEvent, -) -from modules.catalog.domain.rules import ( - ListingAskPriceMustBeGreaterThanZero, - ListingMustBeDraft, -) -from seedwork.domain.entities import AggregateRoot -from seedwork.domain.value_objects import GenericUUID, Money - -from .value_objects import ListingStatus - - -@dataclass(kw_only=True) -class Listing(AggregateRoot): - title: str - description: str - ask_price: Money - seller_id: GenericUUID - status = ListingStatus.DRAFT - - def change_main_attributes(self, title: str, description: str, ask_price: Money): - self.title = title - self.description = description - self.ask_price = ask_price - self.register_event(ListingDraftUpdatedEvent(listing_id=self.id)) - - def publish(self): - """Instantly publish listing for sale""" - self.check_rule(ListingMustBeDraft(status=self.status)) - self.check_rule(ListingAskPriceMustBeGreaterThanZero(ask_price=self.ask_price)) - self.status = ListingStatus.PUBLISHED - self.register_event( - ListingPublishedEvent( - listing_id=self.id, ask_price=self.ask_price, seller_id=self.seller_id - ) - ) - - -@dataclass -class Seller(AggregateRoot): - id: GenericUUID - is_new: bool = True - currently_published_listings_count: int = 0 - - def publish_listing(self, listing) -> list[DomainEvent]: - self.check_rule( - ListingAskPriceMustBeGreaterThanZero(ask_price=listing.ask_price) - ) - # self.check_rule(ListingMustBeInDraftState(listing.status)) - # self.check_rule(SellerMustBeEligibleForAddingNextListing(self)) - return listing.publish() diff --git a/src/modules/catalog/domain/events.py b/src/modules/catalog/domain/events.py deleted file mode 100644 index 11348fb..0000000 --- a/src/modules/catalog/domain/events.py +++ /dev/null @@ -1,20 +0,0 @@ -from seedwork.domain.events import DomainEvent -from seedwork.domain.value_objects import GenericUUID, Money - - -class ListingDraftCreatedEvent(DomainEvent): - listing_id: GenericUUID - - -class ListingDraftUpdatedEvent(DomainEvent): - listing_id: GenericUUID - - -class ListingDraftDeletedEvent(DomainEvent): - listing_id: GenericUUID - - -class ListingPublishedEvent(DomainEvent): - listing_id: GenericUUID - seller_id: GenericUUID - ask_price: Money diff --git a/src/modules/catalog/domain/repositories.py b/src/modules/catalog/domain/repositories.py deleted file mode 100644 index b699db1..0000000 --- a/src/modules/catalog/domain/repositories.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABC - -from modules.catalog.domain.entities import GenericUUID, Listing -from seedwork.domain.repositories import GenericRepository - - -class ListingRepository(GenericRepository[GenericUUID, Listing], ABC): - """An interface for Listing repository""" diff --git a/src/modules/catalog/domain/rules.py b/src/modules/catalog/domain/rules.py deleted file mode 100644 index 33bc9d6..0000000 --- a/src/modules/catalog/domain/rules.py +++ /dev/null @@ -1,72 +0,0 @@ -from seedwork.domain.rules import BusinessRule -from seedwork.domain.value_objects import Money - -from .value_objects import ListingId, ListingStatus, SellerId - -# import modules.catalog.domain.entities as entities - - -class ListingMustBeInDraftState(BusinessRule): - __message = "Listing status must be draft" - - listing_status: ListingStatus - - def is_broken(self) -> bool: - return self.listing_status != ListingStatus.DRAFT - - -class ListingAskPriceMustBeGreaterThanZero(BusinessRule): - __message = "Listing price must be greater that zero" - - ask_price: Money - - def is_broken(self) -> bool: - return self.ask_price.amount <= 0 - - -class ListingMustBeDraft(BusinessRule): - __message = "Listing must be in draft state" - - status: str - - def is_broken(self) -> bool: - return self.status != ListingStatus.DRAFT - - -class SellerMustBeEligibleForAddingNextListing(BusinessRule): - __message = "Seller is not eligible for adding new listing" - - is_new: bool - currently_published_listings_count: int - - def is_broken(self) -> bool: - return self.is_new and self.currently_published_listings_count > 0 - - -class PublishedListingMustNotBeDeleted(BusinessRule): - __message = "A published listing can not be deleted" - - status: str - - def is_broken(self) -> bool: - return self.status == ListingStatus.PUBLISHED - - -class OnlyListingOwnerCanPublishListing(BusinessRule): - __message = "Only listing owner can publish a listing" - - listing_seller_id: ListingId - current_seller_id: SellerId - - def is_broken(self) -> bool: - return self.listing_seller_id != self.current_seller_id - - -class OnlyListingOwnerCanDeleteListing(BusinessRule): - __message = "Only listing owner can delete a listing" - - listing_seller_id: ListingId - current_seller_id: SellerId - - def is_broken(self) -> bool: - return self.listing_seller_id != self.current_seller_id diff --git a/src/modules/catalog/domain/services.py b/src/modules/catalog/domain/services.py deleted file mode 100644 index f5e26f9..0000000 --- a/src/modules/catalog/domain/services.py +++ /dev/null @@ -1,15 +0,0 @@ -# from seedwork.domain.services import DomainService -# from seedwork.domain.value_objects import UUID -# from .entities import Listing, Seller -# from .repositories import ListingRepository -# from .rules import ( -# ListingMustBeInDraftState, -# SellerMustBeEligibleForAddingNextListing, -# ) - - -# class CatalogService: -# def publish_listing(self, listing: Listing, seller: Seller): -# self.check_rule(ListingMustBeInDraftState(listing.status)) -# self.check_rule(SellerMustBeEligibleForAddingNextListing(seller)) -# listing.publish() diff --git a/src/modules/catalog/domain/value_objects.py b/src/modules/catalog/domain/value_objects.py deleted file mode 100644 index e6502f4..0000000 --- a/src/modules/catalog/domain/value_objects.py +++ /dev/null @@ -1,11 +0,0 @@ -from seedwork.domain.value_objects import GenericUUID, ValueObject -from enum import Enum - -# some aliases to fight primitive obsession -ListingId = GenericUUID -SellerId = GenericUUID - - -class ListingStatus(str, Enum): - DRAFT = "draft" - PUBLISHED = "published" diff --git a/src/modules/catalog/infrastructure/__init__.py b/src/modules/catalog/infrastructure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/infrastructure/listing_repository.py b/src/modules/catalog/infrastructure/listing_repository.py deleted file mode 100644 index 9a62bf3..0000000 --- a/src/modules/catalog/infrastructure/listing_repository.py +++ /dev/null @@ -1,61 +0,0 @@ -import uuid - -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.sql.schema import Column -from sqlalchemy_json import mutable_json_type -from sqlalchemy_utils import UUIDType - -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.repositories import ListingRepository -from seedwork.domain.value_objects import GenericUUID, Money -from seedwork.infrastructure.data_mapper import DataMapper -from seedwork.infrastructure.database import Base -from seedwork.infrastructure.repository import SqlAlchemyGenericRepository - -""" -References: -"Introduction to SQLAlchemy 2020 (Tutorial)" by: Mike Bayer -https://youtu.be/sO7FFPNvX2s?t=7214 -""" - - -class ListingModel(Base): - """Data model for listing domain object""" - - __tablename__ = "catalog_listing" - id = Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) - data = Column(mutable_json_type(dbtype=JSONB, nested=True)) - - -class ListingDataMapper(DataMapper[Listing, ListingModel]): - def model_to_entity(self, instance: ListingModel) -> Listing: - d = instance.data - return Listing( - id=instance.id, - title=d["title"], - description=d["description"], - ask_price=Money(**d["ask_price"]), - seller_id=GenericUUID(d["seller_id"]), - ) - - def entity_to_model(self, entity: Listing) -> ListingModel: - return ListingModel( - id=entity.id, - data={ - "title": entity.title, - "description": entity.description, - "ask_price": { - "amount": entity.ask_price.amount, - "currency": entity.ask_price.currency, - }, - "seller_id": str(entity.seller_id), - "status": entity.status, - }, - ) - - -class PostgresJsonListingRepository(ListingRepository, SqlAlchemyGenericRepository): - """Listing repository implementation""" - - mapper_class = ListingDataMapper - model_class = ListingModel diff --git a/src/modules/catalog/tests/__init__.py b/src/modules/catalog/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/tests/application/test_create_listing_draft.py b/src/modules/catalog/tests/application/test_create_listing_draft.py deleted file mode 100644 index a451430..0000000 --- a/src/modules/catalog/tests/application/test_create_listing_draft.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest - -from modules.catalog.application.command.create_listing_draft import ( - CreateListingDraftCommand, - create_listing_draft, -) -from modules.catalog.domain.entities import Seller -from seedwork.domain.value_objects import GenericUUID, Money -from seedwork.infrastructure.repository import InMemoryRepository -from seedwork.tests.application.test_utils import FakeEventPublisher - - -@pytest.mark.unit -@pytest.mark.asyncio -async def test_create_listing_draft(): - # arrange - listing_id = GenericUUID(int=1) - command = CreateListingDraftCommand( - listing_id=listing_id, - title="foo", - description="bar", - ask_price=Money(1), - seller_id=Seller.next_id(), - ) - publish = FakeEventPublisher() - repository = InMemoryRepository() - - # act - await create_listing_draft(command, repository, publish) - - # assert - assert repository.get_by_id(listing_id).title == "foo" - assert publish.contains("ListingDraftCreatedEvent") diff --git a/src/modules/catalog/tests/application/test_delete_listing_draft.py b/src/modules/catalog/tests/application/test_delete_listing_draft.py deleted file mode 100644 index b5bf9c3..0000000 --- a/src/modules/catalog/tests/application/test_delete_listing_draft.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest - -from modules.catalog.application.command.delete_listing_draft import ( - DeleteListingDraftCommand, - delete_listing_draft, -) -from modules.catalog.domain.entities import Listing -from modules.catalog.domain.events import ListingDraftDeletedEvent -from modules.catalog.infrastructure.listing_repository import ( - PostgresJsonListingRepository, -) -from seedwork.domain.value_objects import GenericUUID, Money -from seedwork.infrastructure.repository import InMemoryRepository -from seedwork.tests.application.test_utils import FakeEventPublisher - -@pytest.mark.unit -def test_delete_listing_draft(): - # arrange - seller_id = GenericUUID.next_id() - repository = InMemoryRepository() - listing_id = Listing.next_id() - listing = Listing( - id=listing_id, - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=seller_id, - ) - repository.add(listing) - publish = FakeEventPublisher() - - command = DeleteListingDraftCommand( - listing_id=listing.id, - seller_id=seller_id, - ) - - # act - delete_listing_draft(command, repository, publish) - - # assert - assert publish.contains(ListingDraftDeletedEvent) - - -@pytest.mark.integration -def test_delete_listing_draft_removes_from_database(db_session): - seller_id = GenericUUID.next_id() - repository = PostgresJsonListingRepository(db_session=db_session) - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=seller_id, - ) - repository.add(listing) - publish = FakeEventPublisher() - - command = DeleteListingDraftCommand( - listing_id=listing.id, - seller_id=seller_id, - ) - - # act - delete_listing_draft(command, repository, publish) - - # assert - assert repository.count() == 0 diff --git a/src/modules/catalog/tests/application/test_publish_listing.py b/src/modules/catalog/tests/application/test_publish_listing.py deleted file mode 100644 index c85314d..0000000 --- a/src/modules/catalog/tests/application/test_publish_listing.py +++ /dev/null @@ -1,77 +0,0 @@ -import pytest - -from modules.catalog.application.command.publish_listing_draft import ( - PublishListingDraftCommand, - publish_listing_draft, -) -from modules.catalog.domain.entities import Listing, Seller -from modules.catalog.domain.value_objects import ListingStatus -from seedwork.domain.exceptions import BusinessRuleValidationException -from seedwork.domain.value_objects import Money -from seedwork.infrastructure.repository import InMemoryRepository - - -@pytest.mark.unit -@pytest.mark.asyncio -async def test_publish_listing(): - # arrange - seller_repository = InMemoryRepository() - seller = Seller(id=Seller.next_id()) - seller_repository.add(seller) - - listing_repository = InMemoryRepository() - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=seller.id, - ) - listing_repository.add(listing) - - command = PublishListingDraftCommand( - listing_id=listing.id, - seller_id=seller.id, - ) - - # act - await publish_listing_draft( - command, - listing_repository=listing_repository, - ) - - # assert - assert listing.status == ListingStatus.PUBLISHED - - -@pytest.mark.unit -@pytest.mark.asyncio -async def test_publish_listing_and_break_business_rule(): - # arrange - seller_repository = InMemoryRepository() - seller = Seller(id=Seller.next_id()) - seller_repository.add(seller) - - listing_repository = InMemoryRepository() - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(0), # this will break the rule - seller_id=seller.id, - ) - listing_repository.add(listing) - - command = PublishListingDraftCommand( - listing_id=listing.id, - seller_id=seller.id, - ) - - # act - - # assert - with pytest.raises(BusinessRuleValidationException): - await publish_listing_draft( - command, - listing_repository=listing_repository, - ) diff --git a/src/modules/catalog/tests/application/test_update_listing_draft.py b/src/modules/catalog/tests/application/test_update_listing_draft.py deleted file mode 100644 index b40c3ba..0000000 --- a/src/modules/catalog/tests/application/test_update_listing_draft.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest - -from modules.catalog.application.command.update_listing_draft import ( - UpdateListingDraftCommand, - update_listing_draft, -) -from modules.catalog.domain.entities import Listing -from seedwork.domain.value_objects import GenericUUID, Money -from seedwork.infrastructure.repository import InMemoryRepository - - -@pytest.mark.unit -def test_update_listing_draft(): - # arrange - repository = InMemoryRepository() - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=GenericUUID.next_id(), - ) - repository.add(listing) - - command = UpdateListingDraftCommand( - listing_id=listing.id, - title="Tiny golden dragon", - description=listing.description, - ask_price=listing.ask_price, - modify_user_id=listing.seller_id, - ) - - # act - update_listing_draft(command, repository) - - # assert - assert listing.title == "Tiny golden dragon" - - -@pytest.mark.skip( - "UpdateListingDraftCommand with optional fields is not yet implemented" -) -@pytest.mark.unit -def test_partially_update_listing_draft(): - # arrange - repository = InMemoryRepository() - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=GenericUUID.next_id(), - ) - repository.add(listing) - - # act - command = UpdateListingDraftCommand( - # only 2 fields should be updated, but all are required in a command - listing_id=listing.id, - title="Tiny golden dragon", - ) - update_listing_draft(command, repository) - - # assert - assert repository.get_by_id(listing.id).title == "Tiny golden dragon" diff --git a/src/modules/catalog/tests/domain/test_entities.py b/src/modules/catalog/tests/domain/test_entities.py deleted file mode 100644 index 9ff52c8..0000000 --- a/src/modules/catalog/tests/domain/test_entities.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from modules.catalog.domain.entities import Listing, Seller -from modules.catalog.domain.value_objects import ListingStatus -from seedwork.domain.exceptions import BusinessRuleValidationException -from seedwork.domain.value_objects import Money - - -@pytest.mark.unit -def test_seller_publishes_listing_happy_path(): - seller = Seller(id=Seller.next_id()) - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(1), - seller_id=seller.id, - ) - - seller.publish_listing(listing) - - assert listing.status == ListingStatus.PUBLISHED - - -@pytest.mark.unit -def test_seller_fails_to_publish_listing_with_zero_price(): - seller = Seller(id=Seller.next_id()) - listing = Listing( - id=Listing.next_id(), - title="Tiny dragon", - description="Tiny dragon for sale", - ask_price=Money(0), - seller_id=seller.id, - ) - - with pytest.raises(BusinessRuleValidationException): - seller.publish_listing(listing) diff --git a/src/modules/catalog/tests/domain/test_rules.py b/src/modules/catalog/tests/domain/test_rules.py deleted file mode 100644 index 9ad5e8f..0000000 --- a/src/modules/catalog/tests/domain/test_rules.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from modules.catalog.domain.rules import ListingAskPriceMustBeGreaterThanZero -from seedwork.domain.value_objects import Money - - -@pytest.mark.unit -def test_AuctionItemPriceMustBeGreaterThanZero_rule(): - rule = ListingAskPriceMustBeGreaterThanZero(ask_price=Money(1)) - assert not rule.is_broken() - - -@pytest.mark.unit -def test_AuctionItemPriceMustBeGreaterThanZero_rule_is_broken(): - rule = ListingAskPriceMustBeGreaterThanZero(ask_price=Money(0)) - assert rule.is_broken() diff --git a/src/modules/catalog/tests/infrastructure/__init__.py b/src/modules/catalog/tests/infrastructure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/catalog/tests/infrastructure/test_listing_repository.py b/src/modules/catalog/tests/infrastructure/test_listing_repository.py deleted file mode 100644 index 315ea49..0000000 --- a/src/modules/catalog/tests/infrastructure/test_listing_repository.py +++ /dev/null @@ -1,142 +0,0 @@ -import pytest -from sqlalchemy.orm import Session - -from modules.catalog.domain.entities import Listing, Money -from modules.catalog.infrastructure.listing_repository import ( - ListingDataMapper, - ListingModel, - PostgresJsonListingRepository, -) -from seedwork.domain.value_objects import GenericUUID - - -@pytest.mark.integration -def test_listing_repo_is_empty(db_session): - repo = PostgresJsonListingRepository(db_session=db_session) - assert repo.count() == 0 - - -@pytest.mark.integration -def test_sqlalchemy_repo_is_adding(engine): - with Session(engine) as session: - repo = PostgresJsonListingRepository(db_session=session) - listing = Listing( - id=GenericUUID(int=1), - title="Foo", - description="...", - ask_price=Money(10), - seller_id=GenericUUID(int=1), - ) - repo.add(listing) - session.commit() - - with Session(engine) as session: - repo = PostgresJsonListingRepository(db_session=session) - listing = repo.get_by_id(GenericUUID(int=1)) - assert listing is not None - - -@pytest.mark.integration -def test_sqlalchemy_repo_is_persisting_chages(engine): - with Session(engine) as session: - repo = PostgresJsonListingRepository(db_session=session) - listing = Listing( - id=GenericUUID(int=1), - title="Foo", - description="...", - ask_price=Money(10), - seller_id=GenericUUID(int=1), - ) - repo.add(listing) - session.commit() - - with Session(engine) as session: - repo = PostgresJsonListingRepository(db_session=session) - listing = repo.get_by_id(GenericUUID(int=1)) - listing.title = "Bar" - repo.persist_all() - session.commit() - - with Session(engine) as session: - repo = PostgresJsonListingRepository(db_session=session) - listing = repo.get_by_id(GenericUUID(int=1)) - assert listing.title == "Bar" - - -@pytest.mark.unit -def test_listing_data_mapper_maps_entity_to_model(): - listing = Listing( - id=GenericUUID("00000000000000000000000000000001"), - title="Foo", - description="...", - ask_price=Money(10), - seller_id=GenericUUID("00000000000000000000000000000002"), - ) - mapper = ListingDataMapper() - - actual = mapper.entity_to_model(listing) - - expected = ListingModel( - id=GenericUUID("00000000000000000000000000000001"), - data={ - "title": "Foo", - "description": "...", - "ask_price": { - "amount": 10, - "currency": "USD", - }, - "seller_id": "00000000-0000-0000-0000-000000000002", - "status": "draft", - }, - ) - assert actual.id == expected.id - assert actual.data == expected.data - - -@pytest.mark.unit -def test_listing_data_mapper_maps_model_to_entity(): - instance = ListingModel( - id=GenericUUID("00000000000000000000000000000001"), - data={ - "title": "Foo", - "description": "...", - "ask_price": { - "amount": 10, - "currency": "USD", - }, - "seller_id": "00000000-0000-0000-0000-000000000002", - "status": "draft", - }, - ) - mapper = ListingDataMapper() - - actual = mapper.model_to_entity(instance) - - expected = Listing( - id=GenericUUID("00000000000000000000000000000001"), - title="Foo", - description="...", - ask_price=Money(10), - seller_id=GenericUUID("00000000000000000000000000000002"), - ) - assert actual == expected - - -@pytest.mark.integration -def test_listing_persistence(db_session): - original = Listing( - id=Listing.next_id(), - ask_price=Money(1), - title="red dragon", - description="", - seller_id=GenericUUID.next_id(), - ) - repository = PostgresJsonListingRepository(db_session=db_session) - - repository.add(original) - repository.persist_all() - - repository = PostgresJsonListingRepository(db_session=db_session) - persisted = repository.get_by_id(original.id) - - assert original == persisted diff --git a/src/modules/iam/README.md b/src/modules/iam/README.md deleted file mode 100644 index 40b3ceb..0000000 --- a/src/modules/iam/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Identity and Access Management module - - - -> Identity and access management (IAM) is a framework of business processes, policies and technologies that facilitates the management of electronic or digital identities. With an IAM framework in place, information technology (IT) managers can control user access to critical information within their organizations. - - -References: - -- https://craftingjava.com/blog/user-management-domain-model-rest-api/ \ No newline at end of file diff --git a/src/modules/iam/__init__.py b/src/modules/iam/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/iam/application/__init__.py b/src/modules/iam/application/__init__.py deleted file mode 100644 index 2468929..0000000 --- a/src/modules/iam/application/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from seedwork.application import ApplicationModule - -iam_module = ApplicationModule("iam") diff --git a/src/modules/iam/application/exceptions.py b/src/modules/iam/application/exceptions.py deleted file mode 100644 index 4c049f9..0000000 --- a/src/modules/iam/application/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -from seedwork.application import ApplicationException - - -class InvalidCredentialsException(ApplicationException): - def __init__(self, message="Invalid password"): - super().__init__(message) - - -class InvalidAccessTokenException(ApplicationException): - def __init__(self, message="Invalid access token"): - super().__init__(message) diff --git a/src/modules/iam/application/repository.py b/src/modules/iam/application/repository.py deleted file mode 100644 index 363b534..0000000 --- a/src/modules/iam/application/repository.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import abstractmethod - -from modules.iam.application.services import User -from seedwork.domain.repositories import GenericRepository -from seedwork.domain.value_objects import Email, GenericUUID - - -class UserRepository(GenericRepository[GenericUUID, User]): - @abstractmethod - def get_by_email(self, email: Email) -> User | None: - ... - - @abstractmethod - def get_by_access_token(self, access_token: str) -> User | None: - ... diff --git a/src/modules/iam/application/services.py b/src/modules/iam/application/services.py deleted file mode 100644 index 2f8ae8e..0000000 --- a/src/modules/iam/application/services.py +++ /dev/null @@ -1,49 +0,0 @@ -import bcrypt - -from modules.iam.application.exceptions import InvalidCredentialsException -from modules.iam.domain.entities import User -from seedwork.domain.value_objects import Email - - -class IamService: - def __init__(self, user_repository): - self.user_repository = user_repository - - def create_user( - self, user_id, email, password, access_token, is_superuser=False - ) -> User: - user = self.user_repository.get_by_email(email) - if user: - raise ValueError(f"User with email {email} already exists") - - user = self.user_repository.get_by_access_token(access_token) - if user: - raise ValueError(f"User with access_token {access_token} already exists") - - password_hash = bcrypt.hashpw(password.encode("UTF-8"), bcrypt.gensalt()) - user = User( - id=user_id, - email=Email(email), - password_hash=password_hash.decode("UTF-8"), - access_token=access_token, - is_superuser=is_superuser, - ) - self.user_repository.add(user) - return user - - def authenticate_with_name_and_password(self, name, password) -> User: - user = self.user_repository.get_by_email(name) - if not user: - raise InvalidCredentialsException() - - password_match = bcrypt.checkpw( - password.encode("UTF-8"), user.password_hash.encode("UTF-8") - ) - if not password_match: - raise InvalidCredentialsException() - - return user - - def find_user_by_access_token(self, access_token: str) -> User: - user = self.user_repository.get_by_access_token(access_token) - return user diff --git a/src/modules/iam/domain/entities.py b/src/modules/iam/domain/entities.py deleted file mode 100644 index e7b71c3..0000000 --- a/src/modules/iam/domain/entities.py +++ /dev/null @@ -1,37 +0,0 @@ -from dataclasses import dataclass - -from seedwork.domain.entities import AggregateRoot -from seedwork.domain.value_objects import Email, GenericUUID - -UserId = GenericUUID - - -@dataclass -class User(AggregateRoot): - id: GenericUUID - email: Email - password_hash: bytes - access_token: str - is_superuser: bool = False - - @property - def username(self): - return self.email - - @username.setter - def username(self, value): - self.email = value - - -class AnonymousUser(User): - def __init__(self): - super().__init__( - id=GenericUUID("00000000-0000-0000-0000-000000000000"), - email=None, - password_hash=b"", - access_token="", - ) - - @property - def username(self): - return "" diff --git a/src/modules/iam/infrastructure/repository.py b/src/modules/iam/infrastructure/repository.py deleted file mode 100644 index 73f1f1f..0000000 --- a/src/modules/iam/infrastructure/repository.py +++ /dev/null @@ -1,67 +0,0 @@ -import uuid - -from sqlalchemy import Boolean, String -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.exc import NoResultFound -from sqlalchemy.sql.schema import Column - -from modules.iam.application.repository import UserRepository -from modules.iam.application.services import User -from seedwork.domain.value_objects import Email -from seedwork.infrastructure.database import Base -from seedwork.infrastructure.json_data_mapper import JSONDataMapper -from seedwork.infrastructure.repository import SqlAlchemyGenericRepository - - -class UserModel(Base): - __tablename__ = "user" - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - email = Column(String(255), unique=True, nullable=False) - password = Column(String(255)) - access_token = Column(String(255), unique=True, nullable=False) - is_superuser = Column(Boolean(), nullable=False) - - -class UserDataMapper(JSONDataMapper): - def model_to_entity(self, instance: UserModel) -> User: - return User( - id=instance.id, - email=instance.email, - password_hash=instance.password, - access_token=instance.access_token, - is_superuser=instance.is_superuser, - ) - - def entity_to_model(self, entity: User) -> UserModel: - return UserModel( - id=entity.id, - email=entity.email, - password=entity.password_hash, - access_token=entity.access_token, - is_superuser=entity.is_superuser, - ) - - -class PostgresJsonUserRepository(SqlAlchemyGenericRepository, UserRepository): - """Listing repository implementation""" - - mapper_class = UserDataMapper - model_class = UserModel - - def get_by_access_token(self, access_token: str) -> User | None: - try: - instance = ( - self._session.query(UserModel) - .filter_by(access_token=access_token) - .one() - ) - return self._get_entity(instance) - except NoResultFound: - return None - - def get_by_email(self, email: Email) -> User | None: - try: - instance = self._session.query(UserModel).filter_by(email=email).one() - return self._get_entity(instance) - except NoResultFound: - return None diff --git a/src/modules/iam/infrastructure/user_repository.py b/src/modules/iam/infrastructure/user_repository.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/iam/tests/test_iam_service.py b/src/modules/iam/tests/test_iam_service.py deleted file mode 100644 index 7656d1f..0000000 --- a/src/modules/iam/tests/test_iam_service.py +++ /dev/null @@ -1,52 +0,0 @@ -import pytest - -from modules.iam.application.services import IamService -from seedwork.domain.value_objects import GenericUUID - - -@pytest.mark.integration -def test_create_user_with_duplicated_email_raises_exception(app): - # arrange - with app.transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=GenericUUID(int=1), - email="user1@example.com", - password="password", - access_token="token", - ) - - # assert - with pytest.raises(ValueError): - # act - with app.transaction_context() as ctx: - iam_service = ctx[IamService] - iam_service.create_user( - user_id=GenericUUID(int=2), - email="user2@example.com", - password="password", - access_token="token", - ) - - -@pytest.mark.integration -def test_create_user_with_duplicated_access_token_raises_exception(app): - # arrange - with app.transaction_context() as ctx: - ctx[IamService].create_user( - user_id=GenericUUID(int=1), - email="user1@example.com", - password="password", - access_token="token", - ) - - # assert - with pytest.raises(ValueError): - # act - with app.transaction_context() as ctx: - ctx[IamService].create_user( - user_id=GenericUUID(int=2), - email="user2@example.com", - password="password", - access_token="token", - ) diff --git a/src/mypy.ini b/src/mypy.ini deleted file mode 100644 index 05cf7b1..0000000 --- a/src/mypy.ini +++ /dev/null @@ -1,3 +0,0 @@ -[mypy] -ignore_missing_imports = True -plugins = pydantic.mypy \ No newline at end of file diff --git a/src/seedwork/README.md b/src/seedwork/README.md deleted file mode 100644 index 39df327..0000000 --- a/src/seedwork/README.md +++ /dev/null @@ -1,6 +0,0 @@ -A **framework** is supposed to be a part-baked application that you extend in controlled ways to provide what you need. A **seedwork** is some minimal functionality that you modify however you like to get what you need. Of course this means that there's no way for you to get common updates to the seedwork, once you grow it you own it. - - -References: - -- https://martinfowler.com/bliki/Seedwork.html \ No newline at end of file diff --git a/src/seedwork/__init__.py b/src/seedwork/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seedwork/application/__init__.py b/src/seedwork/application/__init__.py deleted file mode 100644 index 559957d..0000000 --- a/src/seedwork/application/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -import importlib -import inspect -from collections import defaultdict -from functools import partial -from typing import Any, Type, TypeVar - -from seedwork.application.command_handlers import CommandResult -from seedwork.application.commands import Command -from seedwork.application.events import EventResult, EventResultSet, IntegrationEvent -from seedwork.application.exceptions import ApplicationException -from seedwork.application.inbox_outbox import InMemoryInbox -from seedwork.application.queries import Query -from seedwork.application.query_handlers import QueryResult -from seedwork.domain.events import DomainEvent -from seedwork.domain.repositories import GenericRepository -from seedwork.utils.data_structures import OrderedSet - - -def get_function_arguments(func): - handler_signature = inspect.signature(func) - kwargs_iterator = iter(handler_signature.parameters.items()) - _, first_param = next(kwargs_iterator) - first_parameter = first_param.annotation - remaining_parameters = {} - for name, param in kwargs_iterator: - remaining_parameters[name] = param.annotation - - return first_parameter, remaining_parameters - - -T = TypeVar("T", CommandResult, EventResult) - - -def collect_domain_events(result: T, handler_kwargs) -> T: - domain_events = [] - repositories = filter( - lambda x: isinstance(x, GenericRepository), handler_kwargs.values() - ) - for repo in repositories: - domain_events.extend(repo.collect_events()) - result.events.extend(domain_events) - return result - - -class DependencyProvider: - """Basic dependency provider that uses a dictionary to store and inject dependencies""" - - def __init__(self, **kwargs): - self.dependencies = kwargs - - def register_dependency(self, identifier, dependency_instance): - self.dependencies[identifier] = dependency_instance - - def get_dependency(self, identifier): - return self.dependencies[identifier] - - def _get_arguments(self, func): - return get_function_arguments(func) - - def _resolve_arguments(self, handler_parameters) -> dict: - """Match handler_parameters with dependencies""" - kwargs = {} - for param_name, param_type in handler_parameters.items(): - try: - if param_type is inspect._empty: - raise ValueError("No type annotation") - kwargs[param_name] = self.get_dependency(param_type) - continue - except (ValueError, KeyError): - pass - - try: - kwargs[param_name] = self.get_dependency(param_name) - continue - except (ValueError, KeyError): - pass - - return kwargs - - def get_handler_kwargs(self, func, **overrides): - _, handler_parameters = self._get_arguments(func) - kwargs = self._resolve_arguments(handler_parameters) - kwargs.update(**overrides) - return kwargs - - def __getitem__(self, key): - return self.get_dependency(key) - - def __setitem__(self, key, value): - self.register_dependency(key, value) - - -class TransactionContext: - """A context spanning a single transaction for execution of commands and queries - - Typically, the following thing happen in a transaction context: - - a command handler is called, which results in aggregate changes that fire domain events - - a domain event is raised, after - - a domain event handler is called - - a command is executed - - - """ - - def __init__(self, app, **overrides): - self.app = app - self.overrides = overrides - self.dependency_provider = app.dependency_provider - self.task = None - self.next_commands = [] - self.integration_events = [] - - def __enter__(self): - """Should be used to start a transaction""" - self.app._on_enter_transaction_context(self) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Should be used to commit/end a transaction""" - self.app._on_exit_transaction_context(self, exc_type, exc_val, exc_tb) - - def _wrap_with_middlewares( - self, handler_func, command=None, query=None, event=None - ): - p = handler_func - for middleware in self.app._transaction_middlewares: - p = partial(middleware, self, p, command, query, event) - return p - - def execute_query(self, query) -> QueryResult: - assert ( - self.task is None - ), "Cannot execute query while another task is being executed" - self.task = query - - handler_func = self.app.get_query_handler(query) - handler_kwargs = self.dependency_provider.get_handler_kwargs( - handler_func, **self.overrides - ) - p = partial(handler_func, query, **handler_kwargs) - wrapped_handler = self._wrap_with_middlewares(p, query=query) - result = wrapped_handler() - assert isinstance( - result, QueryResult - ), f"Got {result} instead of QueryResult from {handler_func}" - return result - - def execute_command(self, command) -> CommandResult: - assert ( - self.task is None - ), "Cannot execute command while another task is being executed" - self.task = command - - handler_func = self.app.get_command_handler(command) - handler_kwargs = self.dependency_provider.get_handler_kwargs( - handler_func, **self.overrides - ) - p = partial(handler_func, command, **handler_kwargs) - wrapped_handler = self._wrap_with_middlewares(p, command=command) - - # execute wrapped command handler - command_result = wrapped_handler() or CommandResult.success() - assert isinstance( - command_result, CommandResult - ), f"Got {command_result} instead of CommandResult from {handler_func}" - command_result = collect_domain_events(command_result, handler_kwargs) - - self.next_commands = [] - self.integration_events = [] - event_queue = command_result.events.copy() - while len(event_queue) > 0: - event = event_queue.pop(0) - if isinstance(event, IntegrationEvent): - self.collect_integration_event(event) - - elif isinstance(event, DomainEvent): - event_results = self.handle_domain_event(event) - self.next_commands.extend(event_results.commands) - event_queue.extend(event_results.events) - - return CommandResult.success(payload=command_result.payload) - - def handle_domain_event(self, event) -> EventResultSet: - event_results = [] - for handler_func in self.app.get_event_handlers(event): - handler_kwargs = self.dependency_provider.get_handler_kwargs( - handler_func, **self.overrides - ) - p = partial(handler_func, event, **handler_kwargs) - wrapped_handler = self._wrap_with_middlewares(p, event=event) - event_result = wrapped_handler() or EventResult.success() - assert isinstance( - event_result, EventResult - ), f"Got {event_result} instead of EventResult from {handler_func}" - event_result = collect_domain_events(event_result, handler_kwargs) - event_results.append(event_result) - return EventResultSet(event_results) - - def collect_integration_event(self, event): - self.integration_events.append(event) - - def get_service(self, service_cls) -> Any: - """Get a dependency from the dependency provider""" - return self.dependency_provider.get_dependency(service_cls) - - def __getitem__(self, item) -> Any: - return self.get_service(item) - - @property - def current_user(self): - return self.dependency_provider.get_dependency("current_user") - - -class ApplicationModule: - def __init__(self, name, version=1.0): - self.name = name - self.version = version - self.command_handlers = {} - self.query_handlers = {} - self.event_handlers = defaultdict(OrderedSet) - - def query_handler(self, handler_func): - """Query handler decorator""" - query_cls, _ = get_function_arguments(handler_func) - self.query_handlers[query_cls] = handler_func - return handler_func - - def command_handler(self, handler_func): - """Command handler decorator""" - command_cls, _ = get_function_arguments(handler_func) - self.command_handlers[command_cls] = handler_func - return handler_func - - def domain_event_handler(self, handler_func): - """Event handler decorator""" - event_cls, _ = get_function_arguments(handler_func) - self.event_handlers[event_cls].add(handler_func) - return handler_func - - def import_from(self, module_name): - importlib.import_module(module_name) - - def __repr__(self): - return f"<{self.name} v{self.version} {object.__repr__(self)}>" - - -class Application(ApplicationModule): - def __init__(self, name=__name__, version=1.0, dependency_provider=None, **kwargs): - super().__init__(name, version) - self.dependency_provider = dependency_provider or DependencyProvider(**kwargs) - self._transaction_middlewares = [] - self._on_enter_transaction_context = lambda ctx: None - self._on_exit_transaction_context = lambda ctx, exc_type, exc_val, exc_tb: None - self._modules = set([self]) - - def include_module(self, a_module): - assert isinstance( - a_module, ApplicationModule - ), "Can only include ApplicationModule instances" - self._modules.add(a_module) - - def on_enter_transaction_context(self, func): - self._on_enter_transaction_context = func - return func - - def on_exit_transaction_context(self, func): - self._on_exit_transaction_context = func - return func - - def transaction_middleware(self, middleware_func): - """Middleware for processing transaction boundaries (i.e. running a command or query)""" - self._transaction_middlewares.insert(0, middleware_func) - return middleware_func - - def get_query_handler(self, query): - query_cls = type(query) - for app_module in self._modules: - handler_func = app_module.query_handlers.get(query_cls) - if handler_func: - return handler_func - raise Exception(f"No query handler found for command {query_cls}") - - def get_command_handler(self, command): - command_cls = type(command) - for app_module in self._modules: - handler_func = app_module.command_handlers.get(command_cls) - if handler_func: - return handler_func - raise Exception(f"No command handler found for command {command_cls}") - - def get_event_handlers(self, event): - event_cls = type(event) - event_handlers = [] - for app_module in self._modules: - event_handlers.extend(app_module.event_handlers.get(event_cls, [])) - return event_handlers - - def transaction_context(self, **dependencies): - return TransactionContext(self, **dependencies) - - def execute_command(self, command, **dependencies): - with self.transaction_context(**dependencies) as ctx: - return ctx.execute(command) - - def execute_query(self, query, **dependencies): - with self.transaction_context(**dependencies) as ctx: - return ctx.execute_query(query) diff --git a/src/seedwork/application/command_handlers.py b/src/seedwork/application/command_handlers.py deleted file mode 100644 index 54a28fd..0000000 --- a/src/seedwork/application/command_handlers.py +++ /dev/null @@ -1,42 +0,0 @@ -import sys -from dataclasses import dataclass, field -from typing import Any, Optional - -from seedwork.domain.type_hints import DomainEvent -from seedwork.domain.value_objects import GenericUUID - - -@dataclass -class CommandResult: - entity_id: Optional[GenericUUID] = None - payload: Any = None - events: list[DomainEvent] = field(default_factory=list) - errors: list[Any] = field(default_factory=list) - - def has_errors(self): - return len(self.errors) > 0 - - def add_error(self, message, exception, exception_info): - self.errors.append((message, exception, exception_info)) - - def is_success(self) -> bool: - return not self.has_errors() - - @classmethod - def failure(cls, message="Failure", exception=None) -> "CommandResult": - """Creates a failed result""" - exception_info = sys.exc_info() - result = cls() - result.add_error(message, exception, exception_info) - return result - - @classmethod - def success( - cls, entity_id=None, payload=None, event=None, events=None - ) -> "CommandResult": - """Creates a successful result""" - if events is None: - events = [] - if event: - events.append(event) - return cls(entity_id=entity_id, payload=payload, events=events) diff --git a/src/seedwork/application/commands.py b/src/seedwork/application/commands.py deleted file mode 100644 index 5d7fb46..0000000 --- a/src/seedwork/application/commands.py +++ /dev/null @@ -1,7 +0,0 @@ -from lato import Command as LatoCommand -from pydantic import ConfigDict - - -class Command(LatoCommand): - """Abstract base class for all commands""" - model_config = ConfigDict(arbitrary_types_allowed=True) \ No newline at end of file diff --git a/src/seedwork/application/event_dispatcher.py b/src/seedwork/application/event_dispatcher.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seedwork/application/events.py b/src/seedwork/application/events.py deleted file mode 100644 index e8084c9..0000000 --- a/src/seedwork/application/events.py +++ /dev/null @@ -1,84 +0,0 @@ -import sys -from dataclasses import dataclass, field -from typing import Any - -from pydantic import BaseModel - -from seedwork.domain.type_hints import DomainEvent -from seedwork.domain.value_objects import GenericUUID - - -class EventId(GenericUUID): - """Unique identifier of an event""" - - -class IntegrationEvent(BaseModel): - """ - Integration events are used to communicate between modules/system via inbox-outbox pattern. - They are created in a domain event handler and then saved in an outbox for further delivery. - As a result, integration events are handled asynchronously. - """ - - -@dataclass -class EventResult: - """ - Result of event execution (success or failure) by an event handler. - """ - - event_id: EventId = field(default_factory=EventId.next_id) - payload: Any = None - command: Any = ( - None # command to be executed as a result of this event (experimental) - ) - events: list[DomainEvent] = field(default_factory=list) - errors: list[Any] = field(default_factory=list) - - def has_errors(self) -> bool: - """Returns True if an event execution failed""" - return len(self.errors) > 0 - - def is_success(self) -> bool: - """Returns True if an event was successfully executed""" - return not self.has_errors() - - def __hash__(self): - return id(self) - - @classmethod - def failure(cls, message="Failure", exception=None) -> "EventResult": - """Creates a failed result""" - exception_info = sys.exc_info() - errors = [(message, exception, exception_info)] - result = cls(errors=errors) - return result - - @classmethod - def success( - cls, event_id=None, payload=None, command=None, event=None, events=None - ) -> "EventResult": - """Creates a successful result""" - if events is None: - events = [] - if event: - events.append(event) - return cls(event_id=event_id, payload=payload, command=command, events=events) - - -class EventResultSet(set): - """For now just aa fancy name for a set""" - - def is_success(self): - return all([r.is_success() for r in self]) - - @property - def events(self): - all_events = [] - for event in self: - all_events.extend(event.events) - return all_events - - @property - def commands(self): - all_commands = [event.command for event in self if event.command] - return all_commands diff --git a/src/seedwork/application/exceptions.py b/src/seedwork/application/exceptions.py deleted file mode 100644 index 63f16d7..0000000 --- a/src/seedwork/application/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -class ApplicationException(Exception): - pass - - -class UnitOfWorkNotSetException(ApplicationException): - pass diff --git a/src/seedwork/application/inbox_outbox.py b/src/seedwork/application/inbox_outbox.py deleted file mode 100644 index 9d4ceba..0000000 --- a/src/seedwork/application/inbox_outbox.py +++ /dev/null @@ -1,32 +0,0 @@ -import contextlib - - -class InMemoryInbox: - def __init__(self): - self.events = [] - - def is_empty(self): - return len(self.events) == 0 - - @contextlib.contextmanager - def get_next_event(self): - yield self.events.pop(0) - - def enqueue(self, event): - self.events.append(event) - - -class ProcessInboxUntilEmptyStrategy: - def __init__(self, inbox: InMemoryInbox): - self.inbox = inbox - - def should_process_next_event(self): - return not self.inbox.is_empty() - - -class InMemoryOutbox: - def __init__(self): - self.events = [] - - def save(self, event): - self.events.append(event) diff --git a/src/seedwork/application/queries.py b/src/seedwork/application/queries.py deleted file mode 100644 index e5295fe..0000000 --- a/src/seedwork/application/queries.py +++ /dev/null @@ -1,5 +0,0 @@ -from lato import Command - - -class Query(Command): - """Base class for all queries""" diff --git a/src/seedwork/application/query_handlers.py b/src/seedwork/application/query_handlers.py deleted file mode 100644 index cfd6ecc..0000000 --- a/src/seedwork/application/query_handlers.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -from dataclasses import dataclass, field -from typing import Any, Generic, Optional, TypeVar - -T = TypeVar("T") - - -@dataclass -class QueryResult(Generic[T]): - payload: Optional[T] = None - errors: list[Any] = field(default_factory=list) - - def has_errors(self): - return len(self.errors) > 0 - - def is_success(self) -> bool: - return not self.has_errors() - - @classmethod - def failure(cls, message="Failure", exception=None) -> "QueryResult": - """Creates a failed result""" - exception_info = sys.exc_info() - errors = [(message, exception, exception_info)] - result = cls(errors=errors) - return result - - @classmethod - def success(cls, payload=None) -> "QueryResult": - """Creates a successful result""" - return cls(payload=payload) diff --git a/src/seedwork/application/utils.py b/src/seedwork/application/utils.py deleted file mode 100644 index 51525ff..0000000 --- a/src/seedwork/application/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -from seedwork.application.commands import CommandResult -from seedwork.application.events import EventResult - - -def as_event_result(command_result: CommandResult) -> EventResult: - """Translates command result to event result""" - return EventResult( - payload=command_result.payload, - events=command_result.events, - errors=command_result.errors, - ) diff --git a/src/seedwork/domain/__init__.py b/src/seedwork/domain/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seedwork/domain/aggregates.py b/src/seedwork/domain/aggregates.py deleted file mode 100644 index 7eeb302..0000000 --- a/src/seedwork/domain/aggregates.py +++ /dev/null @@ -1,5 +0,0 @@ -from .entities import Entity - - -class Aggregate(Entity): - ... diff --git a/src/seedwork/domain/entities.py b/src/seedwork/domain/entities.py deleted file mode 100644 index 27db645..0000000 --- a/src/seedwork/domain/entities.py +++ /dev/null @@ -1,32 +0,0 @@ -from dataclasses import dataclass, field -from typing import Generic, TypeVar - -from seedwork.domain.events import DomainEvent -from seedwork.domain.mixins import BusinessRuleValidationMixin -from seedwork.domain.value_objects import GenericUUID - -EntityId = TypeVar("EntityId", bound=GenericUUID) - - -@dataclass -class Entity(Generic[EntityId]): - id: EntityId = field(hash=True) - - @classmethod - def next_id(cls) -> EntityId: - return GenericUUID.next_id() - - -@dataclass(kw_only=True) -class AggregateRoot(BusinessRuleValidationMixin, Entity[EntityId]): - """Consists of 1+ entities. Spans transaction boundaries.""" - - events: list = field(default_factory=list) - - def register_event(self, event: DomainEvent): - self.events.append(event) - - def collect_events(self): - events = self.events - self.events = [] - return events diff --git a/src/seedwork/domain/events.py b/src/seedwork/domain/events.py deleted file mode 100644 index 3864e69..0000000 --- a/src/seedwork/domain/events.py +++ /dev/null @@ -1,21 +0,0 @@ -from lato import Event - - -class DomainEvent(Event): - """ - Domain events are used to communicate between aggregates within a single transaction boundary via in-memory queue. - Domain events are synchronous in nature. - """ - - class Config: - arbitrary_types_allowed = True - - def __next__(self): - yield self - - -class CompositeDomainEvent(DomainEvent): - events: list[DomainEvent] - - def __next__(self): - yield from self.events diff --git a/src/seedwork/domain/exceptions.py b/src/seedwork/domain/exceptions.py deleted file mode 100644 index 37a1dc2..0000000 --- a/src/seedwork/domain/exceptions.py +++ /dev/null @@ -1,19 +0,0 @@ -class DomainException(Exception): - pass - - -class BusinessRuleValidationException(DomainException): - def __init__(self, rule): - self.rule = rule - - def __str__(self): - return str(self.rule) - - -class EntityNotFoundException(Exception): - def __init__(self, repository, **kwargs): - - message = f"Entity with {kwargs} not found" - super().__init__(message) - self.repository = repository - self.kwargs = kwargs diff --git a/src/seedwork/domain/mixins.py b/src/seedwork/domain/mixins.py deleted file mode 100644 index 161292d..0000000 --- a/src/seedwork/domain/mixins.py +++ /dev/null @@ -1,12 +0,0 @@ -from .exceptions import BusinessRuleValidationException -from .rules import BusinessRule - - -def check_rule(rule: BusinessRule): - if rule.is_broken(): - raise BusinessRuleValidationException(rule) - - -class BusinessRuleValidationMixin: - def check_rule(self, rule: BusinessRule): - check_rule(rule) diff --git a/src/seedwork/domain/repositories.py b/src/seedwork/domain/repositories.py deleted file mode 100644 index a75ae57..0000000 --- a/src/seedwork/domain/repositories.py +++ /dev/null @@ -1,43 +0,0 @@ -import abc -from typing import Generic, TypeVar - -from seedwork.domain.entities import Entity as DomainEntity -from seedwork.domain.value_objects import GenericUUID - -Entity = TypeVar("Entity", bound=DomainEntity) -EntityId = TypeVar("EntityId", bound=GenericUUID) - - -class GenericRepository(Generic[EntityId, Entity], metaclass=abc.ABCMeta): - """An interface for a generic repository""" - - @abc.abstractmethod - def add(self, entity: Entity): - raise NotImplementedError() - - @abc.abstractmethod - def remove(self, entity: Entity): - raise NotImplementedError() - - @abc.abstractmethod - def get_by_id(self, id: EntityId) -> Entity: - raise NotImplementedError() - - @abc.abstractmethod - def persist(self, entity: Entity): - raise NotImplementedError() - - @abc.abstractmethod - def persist_all(self): - raise NotImplementedError() - - @abc.abstractmethod - def collect_events(self): - raise NotImplementedError() - - def __getitem__(self, index) -> Entity: - return self.get_by_id(index) - - @staticmethod - def next_id() -> EntityId: - return GenericUUID.next_id() diff --git a/src/seedwork/domain/rules.py b/src/seedwork/domain/rules.py deleted file mode 100644 index 43f6879..0000000 --- a/src/seedwork/domain/rules.py +++ /dev/null @@ -1,20 +0,0 @@ -from pydantic import BaseModel - - -class BusinessRule(BaseModel): - """This is a base class for implementing domain rules""" - - class Config: - arbitrary_types_allowed = True - - # This is an error message that broken rule reports back - __message: str = "Business rule is broken" - - def get_message(self) -> str: - return self.__message - - def is_broken(self) -> bool: - pass - - def __str__(self): - return f"{self.__class__.__name__} {super().__str__()}" diff --git a/src/seedwork/domain/services.py b/src/seedwork/domain/services.py deleted file mode 100644 index bc4e31c..0000000 --- a/src/seedwork/domain/services.py +++ /dev/null @@ -1,7 +0,0 @@ -from .mixins import BusinessRuleValidationMixin - - -class DomainService(BusinessRuleValidationMixin): - """ - Domain services carry domain knowledge that doesn’t naturally fit entities and value objects. - """ diff --git a/src/seedwork/domain/type_hints.py b/src/seedwork/domain/type_hints.py deleted file mode 100644 index 12fc20d..0000000 --- a/src/seedwork/domain/type_hints.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import List -from seedwork.domain.events import DomainEvent - -DomainEvents = List[DomainEvent] diff --git a/src/seedwork/domain/value_objects.py b/src/seedwork/domain/value_objects.py deleted file mode 100644 index 4383105..0000000 --- a/src/seedwork/domain/value_objects.py +++ /dev/null @@ -1,56 +0,0 @@ -import uuid -from dataclasses import dataclass -from typing import Any - -from pydantic import GetCoreSchemaHandler - - -class GenericUUID(uuid.UUID): - @classmethod - def next_id(cls): - return cls(int=uuid.uuid4().int) - - @classmethod - def __get_pydantic_core_schema__( - cls, source_type: Any, handler: GetCoreSchemaHandler - ): - return handler.generate_schema(uuid.UUID) - - -class ValueObject: - """ - Base class for value objects - """ - - -# @functools.total_ordering # type: ignore -@dataclass(frozen=True) -class Money(ValueObject): - amount: int = 0 - currency: str = "USD" - - def _check_currency(self, other): - if self.currency != other.currency: - raise ValueError("Cannot compare money of different currencies") - - def __eq__(self, other): - self._check_currency(other) - return self.amount == other.amount - - def __lt__(self, other): - self._check_currency(other) - return self.amount < other.amount - - def __add__(self, other): - self._check_currency(other) - return Money(self.amount + other.amount, currency=self.currency) - - def __repr__(self) -> str: - return f"{self.amount}{self.currency}" - - -class Email(str): - def __new__(cls, email): - if "@" not in email: - raise ValueError("Invalid email address") - return super().__new__(cls, email) diff --git a/src/seedwork/infrastructure/__init__.py b/src/seedwork/infrastructure/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seedwork/infrastructure/data_mapper.py b/src/seedwork/infrastructure/data_mapper.py deleted file mode 100644 index 0ac999b..0000000 --- a/src/seedwork/infrastructure/data_mapper.py +++ /dev/null @@ -1,40 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, Generic, TypeVar - -from seedwork.domain.entities import Entity, GenericUUID - -MapperEntity = TypeVar("MapperEntity", bound=Entity) -MapperModel = TypeVar("MapperModel", bound=Any) - - -class DataMapper(Generic[MapperEntity, MapperModel], ABC): - entity_class: type[MapperEntity] - model_class: type[MapperModel] - - @abstractmethod - def model_to_entity(self, instance: MapperModel) -> MapperEntity: - raise NotImplementedError() - - @abstractmethod - def entity_to_model(self, entity: MapperEntity) -> MapperModel: - raise NotImplementedError() - - -class JSONDataMapper(DataMapper): - def model_to_entity(self, instance: MapperModel) -> MapperEntity: - entity_id = GenericUUID(instance.get("id")) - entity_dict = { - "id": entity_id, - **instance["data"], - } - return self.entity_class(**entity_dict) - - def entity_to_model(self, entity: MapperEntity) -> MapperModel: - data = dict(**entity.__dict__) - entity_id = str(data.pop("id")) - return self.model_class( - **{ - "id": entity_id, - "data": data, - } - ) diff --git a/src/seedwork/infrastructure/database.py b/src/seedwork/infrastructure/database.py deleted file mode 100644 index f7b8d4f..0000000 --- a/src/seedwork/infrastructure/database.py +++ /dev/null @@ -1,22 +0,0 @@ -import json -import logging -import uuid - -from sqlalchemy.orm import declarative_base -from sqlalchemy_utils import force_auto_coercion - -logger = logging.getLogger(__name__) - - -force_auto_coercion() -Base = declarative_base() - - -def _default(val): - if isinstance(val, uuid.UUID): - return str(val) - raise TypeError() - - -def dumps(d): - return json.dumps(d, default=_default) diff --git a/src/seedwork/infrastructure/exceptions.py b/src/seedwork/infrastructure/exceptions.py deleted file mode 100644 index 81a0d3e..0000000 --- a/src/seedwork/infrastructure/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class InfrastructureException(Exception): - pass diff --git a/src/seedwork/infrastructure/json_data_mapper.py b/src/seedwork/infrastructure/json_data_mapper.py deleted file mode 100644 index 9cd210b..0000000 --- a/src/seedwork/infrastructure/json_data_mapper.py +++ /dev/null @@ -1,25 +0,0 @@ -from seedwork.domain.entities import Entity, GenericUUID - - -class JSONDataMapper: - """Used to serialize/deserialize entities from/to JSON data format""" - - def data_to_entity(self, data: dict, entity_class: type[Entity]) -> Entity: - """Creates business entity from dictionary with a `data` attribute""" - entity_id = GenericUUID(data.pop("id")) - entity_dict = { - "id": entity_id, - **data, - } - return entity_class(**entity_dict) - - def entity_to_data(self, entity: Entity, model_class): - """Stores entity attributes in dictionary with a `data` attribute""" - data = dict(**entity.__dict__) - entity_id = str(data.pop("id")) - return model_class( - **{ - "id": entity_id, - "data": data, - } - ) diff --git a/src/seedwork/infrastructure/logging.py b/src/seedwork/infrastructure/logging.py deleted file mode 100644 index 98d3c02..0000000 --- a/src/seedwork/infrastructure/logging.py +++ /dev/null @@ -1,153 +0,0 @@ -import logging -import uuid -from contextvars import ContextVar -from datetime import datetime -from logging import Logger -from logging.config import dictConfig - -from pythonjsonlogger import jsonlogger - -from seedwork.utils.functional import SimpleLazyObject - -correlation_id: ContextVar[uuid.UUID] = ContextVar( - "correlation_id", default=uuid.UUID("00000000-0000-0000-0000-000000000000") -) - - -class RequestContextFilter(logging.Filter): - """ "Provides correlation id parameter for the logger""" - - def __init__(self, name: str, correlation_id) -> None: - super().__init__(name=name) - self.correlation_id = correlation_id - - def filter(self, record): - record.correlation_id = self.correlation_id.get() - return True - - -class ElkJsonFormatter(jsonlogger.JsonFormatter): - """ - ELK stack-compatibile formatter - """ - - def add_fields(self, log_record, record, message_dict): - super(ElkJsonFormatter, self).add_fields(log_record, record, message_dict) - log_record["@timestamp"] = datetime.now().isoformat() - log_record["level"] = record.levelname - log_record["logger"] = record.name - - -class LoggerFactory: - _configured = False - - @classmethod - def configure( - cls, - logger_name="app", - log_filename="./logs.json", - correlation_id=correlation_id, - ): - cls.logger_name = logger_name - cls.log_filename = log_filename - cls.correlation_id = correlation_id - cls._configured = True - - @classmethod - def create_logger(cls): - """ - Returns a logger instance, based on a configuration options - """ - if not cls._configured: - cls.configure() - logging_config = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "default": { - # exact format is not important, this is the minimum information - "format": "%(asctime)s %(name)-12s %(levelname)-8s %(correlation_id)s %(message)s", - }, - "colored": { - "()": "colorlog.ColoredFormatter", - "format": "%(log_color)s%(asctime)s %(name)-12s %(levelname)-8s %(correlation_id)s %(message)s", - "log_colors": { - "DEBUG": "white", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red,bold", - }, - }, - "colored_db": { - "()": "colorlog.ColoredFormatter", - "format": "%(log_color)s%(asctime)s %(name)-12s %(levelname)-8s %(correlation_id)s %(message)s", - "log_colors": { - "DEBUG": "purple", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red,bold", - }, - }, - "json_formatter": { - "()": "seedwork.infrastructure.logging.ElkJsonFormatter", - }, - }, - "handlers": { - # console logs to stderr - "console": { - "class": "logging.StreamHandler", - "formatter": "default", - }, - "colored_console": { - "class": "colorlog.StreamHandler", - "formatter": "colored", - }, - "colored_console_db": { - "class": "colorlog.StreamHandler", - "formatter": "colored_db", - }, - "file_handler": { - "class": "logging.handlers.RotatingFileHandler", - "filename": cls.log_filename, - "formatter": "json_formatter", - } - if cls.log_filename - else None, - # Add Handler for Sentry for `warning` and above - # 'sentry': { - # 'level': 'WARNING', - # 'class': 'raven.contrib.django.raven_compat.handlers.SentryHandler', - # }, - }, - "loggers": { - cls.logger_name: { - "level": "DEBUG", - "handlers": ["colored_console", "file_handler"], # , 'sentry'], - }, - # Prevent noisy modules from logging to Sentry - "noisy_module": { - "level": "ERROR", - "handlers": ["console"], - "propagate": False, - }, - }, - } - - dictConfig(logging_config) - logger = logging.getLogger(name=cls.logger_name) - logger.correlation_id = cls.correlation_id - logger.addFilter( - RequestContextFilter( - name=cls.logger_name, correlation_id=cls.correlation_id - ) - ) - return logger - - -""" -We are making logger globally available, but to make it configurable logger lazy-evaluated. -Use `LoggerFactory.configure()` to configure the logger prior to its usage -""" -logger: Logger = SimpleLazyObject(LoggerFactory.create_logger) # type: ignore diff --git a/src/seedwork/infrastructure/repository.py b/src/seedwork/infrastructure/repository.py deleted file mode 100644 index d7cbd53..0000000 --- a/src/seedwork/infrastructure/repository.py +++ /dev/null @@ -1,166 +0,0 @@ -from typing import Any - -from sqlalchemy.orm import Session - -from seedwork.domain.entities import Entity -from seedwork.domain.events import DomainEvent -from seedwork.domain.exceptions import EntityNotFoundException -from seedwork.domain.repositories import GenericRepository -from seedwork.domain.value_objects import GenericUUID -from seedwork.infrastructure.data_mapper import DataMapper -from seedwork.infrastructure.database import Base - - -class InMemoryRepository(GenericRepository[GenericUUID, Entity]): - def __init__(self) -> None: - self.objects: dict[Any, Any] = {} - - def get_by_id(self, entity_id: GenericUUID) -> Entity: - try: - return self.objects[entity_id] - except KeyError: - raise EntityNotFoundException(repository=self, entity_id=entity_id) - - def remove_by_id(self, entity_id: GenericUUID): - try: - del self.objects[entity_id] - except KeyError: - raise EntityNotFoundException(repository=self, entity_id=entity_id) - - def add(self, entity: Entity): - assert issubclass(entity.__class__, Entity) - self.objects[entity.id] = entity - - def remove(self, entity: Entity): - del self.objects[entity.id] - - def count(self): - return len(self.objects) - - def persist(self, entity: Entity): - ... - - def persist_all(self): - ... - - def collect_events(self) -> list[DomainEvent]: - events = [] - for entity in self.objects.values(): - events.extend(entity.collect_events()) - return events - - -# a sentinel value for keeping track of entities removed from the repository -class Removed: - def __repr__(self): - return "" - - def __str__(self): - return "" - - -REMOVED = Removed() - - -class SqlAlchemyGenericRepository(GenericRepository[GenericUUID, Entity]): - mapper_class: type[DataMapper[Entity, Base]] - model_class: type[Entity] - - def __init__(self, db_session: Session, identity_map=None): - self._session = db_session - self._identity_map = identity_map or dict() - - def add(self, entity: Entity): - self._identity_map[entity.id] = entity - instance = self.map_entity_to_model(entity) - self._session.add(instance) - - def remove(self, entity: Entity): - self._check_not_removed(entity.id) - self._identity_map[entity.id] = REMOVED - instance = self._session.query(self.get_model_class()).get(entity.id) - self._session.delete(instance) - - def remove_by_id(self, entity_id: GenericUUID): - self._check_not_removed(entity_id) - self._identity_map[entity_id] = REMOVED - instance = self._session.query(self.get_model_class()).get(entity_id) - if instance is None: - raise EntityNotFoundException(repository=self, entity_id=entity_id) - self._session.delete(instance) - - def get_by_id(self, entity_id: GenericUUID): - instance = self._session.query(self.get_model_class()).get(entity_id) - if instance is None: - raise EntityNotFoundException(repository=self, entity_id=entity_id) - return self._get_entity(instance) - - def persist(self, entity: Entity): - """ - Persists all the changes made to the entity. - Basically, entity is mapped to a model instance using a data_mapper, and then added to sqlalchemy session. - """ - self._check_not_removed(entity.id) - assert ( - entity.id in self._identity_map - ), "Cannon persist entity which is unknown to the repo. Did you forget to call repo.add() for this entity?" - instance = self.map_entity_to_model(entity) - merged = self._session.merge(instance) - self._session.add(merged) - - def persist_all(self): - """Persists all changes made to entities known to the repository (present in the identity map).""" - for entity in self._identity_map.values(): - if entity is not REMOVED: - self.persist(entity) - - def collect_events(self): - """Collects all events from entities known to the repository (present in the identity map).""" - events = [] - for entity in self._identity_map.values(): - if entity is not REMOVED: - events.extend(entity.collect_events()) - return events - - @property - def data_mapper(self): - return self.mapper_class() - - def count(self) -> int: - return self._session.query(self.model_class).count() - - def map_entity_to_model(self, entity: Entity): - assert self.mapper_class, ( - f"No data_mapper attribute in {self.__class__.__name__}. " - "Make sure to include `mapper_class = MyDataMapper` in the Repository class." - ) - - return self.data_mapper.entity_to_model(entity) - - def map_model_to_entity(self, instance) -> Entity: - assert self.data_mapper - return self.data_mapper.model_to_entity(instance) - - def get_model_class(self): - assert self.model_class is not None, ( - f"No model_class attribute in in {self.__class__.__name__}. " - "Make sure to include `model_class = MyModel` in the class." - ) - return self.model_class - - def _get_entity(self, instance): - if instance is None: - return None - entity = self.map_model_to_entity(instance) - self._check_not_removed(entity.id) - - if entity.id in self._identity_map: - return self._identity_map[entity.id] - - self._identity_map[entity.id] = entity - return entity - - def _check_not_removed(self, entity_id): - assert ( - self._identity_map.get(entity_id, None) is not REMOVED - ), f"Entity {entity_id} already removed" diff --git a/src/seedwork/tests/application/test_application.py b/src/seedwork/tests/application/test_application.py deleted file mode 100644 index 69e42a6..0000000 --- a/src/seedwork/tests/application/test_application.py +++ /dev/null @@ -1,148 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.application import Application -from seedwork.application.command_handlers import CommandResult -from seedwork.application.commands import Command -from seedwork.domain.events import DomainEvent - - -@dataclass -class SendPing(Command): - pass - - -class PingSent(DomainEvent): - pass - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_application_config(): - app = Application("TestApp", 0.1) - - assert app.name == "TestApp" - assert app.version == 0.1 - - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_application_handles_command(): - app = Application() - - @app.command_handler - def handle_ping(command: SendPing): - ... - - assert app.get_command_handler(SendPing()) is handle_ping - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_application_handles_domain_event(): - app = Application() - - @app.domain_event_handler - def handle_ping_sent(event: PingSent): - ... - - assert app.get_event_handlers(PingSent()) == [handle_ping_sent] - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_app_parameters_injection(): - app = Application(correlation_id=1) - - @app.command_handler - def handle_ping(command: SendPing, correlation_id): - return CommandResult.success(payload=correlation_id) - - result = app.execute(SendPing()) - assert result.payload == 1 - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_transaction_context_parameter_injection(): - app = Application() - - @app.command_handler - def handle_ping(command: SendPing, correlation_id): - return CommandResult.success(payload=correlation_id) - - with app.transaction_context(correlation_id=1) as ctx: - result = ctx.execute(SendPing()) - assert result.payload == 1 - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_transaction_context_parameter_override(): - app = Application(correlation_id=1) - - @app.command_handler - def handle_ping(command: SendPing, correlation_id): - return CommandResult.success(payload=correlation_id) - - with app.transaction_context(correlation_id=2) as ctx: - result = ctx.execute(SendPing()) - assert result.payload == 2 - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_transaction_context_enter_exit(): - app = Application(correlation_id=1) - - @app.on_enter_transaction_context - def on_enter_transaction_context(ctx): - ctx.entered = True - - @app.on_exit_transaction_context - def on_exit_transaction_context(ctx, exc_type, exc_val, exc_tb): - ctx.exited = True - - @app.command_handler - def handle_ping(command: SendPing, correlation_id): - return CommandResult.success(payload=correlation_id) - - with app.transaction_context() as ctx: - ... - - assert ctx.entered - assert ctx.exited - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_transaction_context_middleware(): - app = Application(trace=[]) - - @app.transaction_middleware - def middleware1(ctx, next, command=None, query=None, event=None): - ctx.dependency_provider["trace"].append("middleware1") - return next() - - @app.transaction_middleware - def middleware1(ctx, next, command=None, query=None, event=None): - ctx.dependency_provider["trace"].append("middleware2") - return next() - - @app.command_handler - def handle_ping(command: SendPing): - return CommandResult.success() - - with app.transaction_context() as ctx: - ctx.execute(SendPing()) - - assert app.dependency_provider["trace"] == ["middleware1", "middleware2"] - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_missing_dependency(): - app = Application() - - @app.command_handler - def handle_ping(command: SendPing, missing_dependency): - return CommandResult.success(payload=missing_dependency) - - with pytest.raises(TypeError): - app.execute(SendPing()) diff --git a/src/seedwork/tests/application/test_application_and_one_module_branching.py b/src/seedwork/tests/application/test_application_and_one_module_branching.py deleted file mode 100644 index eb0a318..0000000 --- a/src/seedwork/tests/application/test_application_and_one_module_branching.py +++ /dev/null @@ -1,101 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.application import Application -from seedwork.application.command_handlers import CommandResult -from seedwork.application.commands import Command -from seedwork.application.events import EventResult -from seedwork.domain.events import DomainEvent - - -@dataclass -class CompleteOrderCommand(Command): - order_id: str - - -class OrderCompletedEvent(DomainEvent): - order_id: str - - -class PaymentProcessedEvent(DomainEvent): - order_id: str - - -class OrderShippedEvent(DomainEvent): - order_id: str - - -def create_app(): - app = Application() - - @app.command_handler - def complete_order(command: CompleteOrderCommand, history): - history.append(f"completing {command.order_id}") - return CommandResult.success( - payload=None, event=OrderCompletedEvent(order_id=command.order_id) - ) - - @app.domain_event_handler - def when_order_is_completed_process_payment_policy( - event: OrderCompletedEvent, history - ): - history.append( - f"starting when_order_is_completed_process_payment_policy for {event.order_id}" - ) - ... - return EventResult.success(event=PaymentProcessedEvent(order_id=event.order_id)) - - @app.domain_event_handler - def when_order_is_completed_ship_order_policy(event: OrderCompletedEvent, history): - history.append( - f"starting when_order_is_completed_ship_order_policy for {event.order_id}" - ) - ... - return EventResult.success(event=OrderShippedEvent(order_id=event.order_id)) - - @app.domain_event_handler - def when_payment_is_processed_open_champagne_policy( - event: PaymentProcessedEvent, history - ): - history.append( - f"starting when_payment_is_processed_open_champagne_policy for {event.order_id}" - ) - return EventResult.success() - - @app.domain_event_handler - def when_order_is_shipped_sit_and_relax_policy(event: OrderShippedEvent, history): - history.append( - f"starting when_order_is_shipped_sit_and_relax_policy for {event.order_id}" - ) - return EventResult.success() - - return app - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.integration -def test_mono_module_command_branching_flow(): - """This tests the branching code flow: - complete_order - ↓ - OrderCompletedEvent - ↓ ↓ - when_order_is_completed_process_payment_policy when_order_is_completed_ship_order_policy - ↓ ↓ - PaymentProcessedEvent OrderShippedEvent - ↓ ↓ - when_payment_is_processed_ship_order_policy when_order_is_shipped_sit_and_relax_policy - """ - app = create_app() - history = [] - with app.transaction_context(history=history) as ctx: - ctx.execute(CompleteOrderCommand(order_id="order1")) - - assert history == [ - "completing order1", - "starting when_order_is_completed_process_payment_policy for order1", - "starting when_order_is_completed_ship_order_policy for order1", - "starting when_payment_is_processed_open_champagne_policy for order1", - "starting when_order_is_shipped_sit_and_relax_policy for order1", - ] diff --git a/src/seedwork/tests/application/test_application_and_one_module_linear.py b/src/seedwork/tests/application/test_application_and_one_module_linear.py deleted file mode 100644 index e0e2bcc..0000000 --- a/src/seedwork/tests/application/test_application_and_one_module_linear.py +++ /dev/null @@ -1,92 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.application import Application -from seedwork.application.command_handlers import CommandResult -from seedwork.application.commands import Command -from seedwork.application.events import EventResult -from seedwork.domain.events import DomainEvent - - -@dataclass -class CompleteOrderCommand(Command): - order_id: str - - -class OrderCompletedEvent(DomainEvent): - order_id: str - - -class PaymentProcessedEvent(DomainEvent): - order_id: str - - -class OrderShippedEvent(DomainEvent): - order_id: str - - -app = Application() - - -@app.command_handler -def complete_order(command: CompleteOrderCommand, history): - history.append(f"completing {command.order_id}") - return CommandResult.success( - payload=None, event=OrderCompletedEvent(order_id=command.order_id) - ) - - -@app.domain_event_handler -def when_order_is_completed_process_payment_policy(event: OrderCompletedEvent, history): - history.append(f"processing payment for {event.order_id}") - return EventResult.success( - payload=None, event=PaymentProcessedEvent(order_id=event.order_id) - ) - - -@app.domain_event_handler -def when_payment_is_processed_ship_order_policy( - event: PaymentProcessedEvent, - history, -): - history.append(f"shipping order for {event.order_id}") - return EventResult.success(event=OrderShippedEvent(order_id=event.order_id)) - - -@app.domain_event_handler -def when_order_is_shipped_sit_and_relax_policy(event: OrderShippedEvent, history): - history.append(f"done with {event.order_id}") - return EventResult.success() - - -@app.on_enter_transaction_context -def on_enter_transaction_context(ctx): - """Prepare dependencies, begin transaction""" - ctx.dependency_provider["outbox"] = [] - - -@app.on_exit_transaction_context -def on_exit_transaction_context(ctx, exc_type, exc_val, exc_tb): - """Save events in outbox, End transaction""" - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.integration -def test_mono_module_command_linear_flow(): - global app - """This tests the linear code flow: - CompleteOrderCommand → OrderCompletedEvent → when_order_is_completed_process_payment_policy → - → ProcessPaymentCommand → PaymentProcessedEvent → when_payment_is_processed_ship_order_policy → - → ShipOrderCommand → OrderShippedEvent → when_order_is_shipped_sit_and_relax_policy - """ - history = [] - with app.transaction_context(history=history) as ctx: - ctx.execute(CompleteOrderCommand(order_id="order1")) - - assert history == [ - "completing order1", - "processing payment for order1", - "shipping order for order1", - "done with order1", - ] diff --git a/src/seedwork/tests/application/test_application_with_outbox.py b/src/seedwork/tests/application/test_application_with_outbox.py deleted file mode 100644 index 571a9a5..0000000 --- a/src/seedwork/tests/application/test_application_with_outbox.py +++ /dev/null @@ -1,63 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.application import Application -from seedwork.application.command_handlers import CommandResult -from seedwork.application.commands import Command -from seedwork.application.events import EventResult, IntegrationEvent -from seedwork.domain.events import DomainEvent - - -@pytest.mark.skip(reason="seedwork Application deprecated by lato") -@pytest.mark.unit -def test_command_execution_returns_integration_events(): - """ - In this test, we want to verify that the application stores integration events in the outbox. - """ - - @dataclass - class CompleteOrder(Command): - order_id: int - buyer_email: str - - class OrderCompleted(DomainEvent): - order_id: int - - class NotifyBuyerOfOrderCompletion(IntegrationEvent): - order_id: int - buyer_email: str - - class PrepareOrderForShipping(IntegrationEvent): - order_id: int - - outbox = [] - app = Application(outbox=outbox) - - @app.command_handler - def complete_order(command: CompleteOrder): - domain_event = OrderCompleted( - order_id=command.order_id, buyer_email=command.buyer_email - ) - integration_event = NotifyBuyerOfOrderCompletion( - order_id=command.order_id, buyer_email=command.buyer_email - ) - return CommandResult.success(events=[domain_event, integration_event]) - - @app.domain_event_handler - def on_order_completed(event: OrderCompleted): - integration_event = PrepareOrderForShipping(order_id=event.order_id) - return EventResult.success(event=integration_event) - - @app.on_exit_transaction_context - def on_exit_transaction_context(ctx, exc_type, exc_val, exc_tb): - outbox = ctx.dependency_provider["outbox"] - if exc_type is None: - outbox.extend(ctx.integration_events) - - with app.transaction_context() as ctx: - ctx.execute( - CompleteOrder(order_id=1, buyer_email="john.doe@example.com") - ) - - assert len(outbox) == 2 diff --git a/src/seedwork/tests/application/test_utils.py b/src/seedwork/tests/application/test_utils.py deleted file mode 100644 index 08c1ae5..0000000 --- a/src/seedwork/tests/application/test_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -from seedwork.application.events import DomainEvent -from typing import Type - -class FakeEventPublisher: - def __init__(self): - self.events = [] - - def __call__(self, event): - self.events.append(event) - - def contains(self, event: str | Type[DomainEvent]) -> bool: - return any([ev.__class__.__name__ == event or isinstance(ev, event) for ev in self.events]) \ No newline at end of file diff --git a/src/seedwork/tests/domain/test_entity.py b/src/seedwork/tests/domain/test_entity.py deleted file mode 100644 index 17daecb..0000000 --- a/src/seedwork/tests/domain/test_entity.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.domain.entities import AggregateRoot, Entity - - -@dataclass -class PersonEntity(Entity): - name: str - - -@dataclass -class PersonAggregate(AggregateRoot): - name: str - - -@pytest.mark.unit -def test_entity(): - bob = PersonEntity(id=PersonEntity.next_id(), name="Bob") - assert bob.id is not None - - -@pytest.mark.unit -def test_aggregate(): - bob = PersonAggregate(id=PersonEntity.next_id(), name="Bob") - assert bob.id is not None diff --git a/src/seedwork/tests/domain/test_value_objects.py b/src/seedwork/tests/domain/test_value_objects.py deleted file mode 100644 index 68ce045..0000000 --- a/src/seedwork/tests/domain/test_value_objects.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -from pydantic import BaseModel - -from seedwork.domain.value_objects import GenericUUID, Money - - -class CustomPydanticModel(BaseModel): - uuid: GenericUUID - - -@pytest.mark.unit -def test_money_equality(): - assert Money(10, "USD") == Money(10, "USD") - - -@pytest.mark.unit -def test_money_ordering(): - assert Money(10, "USD") < Money(100, "USD") diff --git a/src/seedwork/tests/infrastructure/test_data_mapper.py b/src/seedwork/tests/infrastructure/test_data_mapper.py deleted file mode 100644 index a8d86ee..0000000 --- a/src/seedwork/tests/infrastructure/test_data_mapper.py +++ /dev/null @@ -1,48 +0,0 @@ -from dataclasses import dataclass -from uuid import UUID - -import pytest - -from seedwork.domain.entities import Entity -from seedwork.infrastructure.data_mapper import JSONDataMapper - - -@dataclass -class PersonEntity(Entity): - """Person entity""" - - name: str - - -class PersonJSONDataMapper(JSONDataMapper): - entity_class = PersonEntity - model_class = dict - - -@pytest.mark.unit -def test_data_mapper_maps_entity_to_json(): - mapper = PersonJSONDataMapper() - entity_instance = PersonEntity( - id=UUID("12345678123456781234567812345678"), name="Bob" - ) - - actual = mapper.entity_to_model(entity_instance) - - expected = {"id": "12345678-1234-5678-1234-567812345678", "data": {"name": "Bob"}} - - assert actual == expected - - -@pytest.mark.unit -def test_data_mapper_maps_json_to_entity(): - mapper = PersonJSONDataMapper() - model_instance = { - "id": "12345678-1234-5678-1234-567812345678", - "data": {"name": "Bob"}, - } - - actual = mapper.model_to_entity(model_instance) - - expected = PersonEntity(id=UUID("12345678123456781234567812345678"), name="Bob") - - assert actual == expected diff --git a/src/seedwork/tests/infrastructure/test_repository.py b/src/seedwork/tests/infrastructure/test_repository.py deleted file mode 100644 index afac11b..0000000 --- a/src/seedwork/tests/infrastructure/test_repository.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass - -import pytest - -from seedwork.domain.entities import Entity -from seedwork.domain.exceptions import EntityNotFoundException -from seedwork.infrastructure.repository import InMemoryRepository - - -@dataclass -class Person(Entity): - first_name: str - last_name: str - - -@pytest.mark.unit -def test_InMemoryRepository_persist_one(): - # arrange - person = Person(id=Person.next_id(), first_name="John", last_name="Doe") - repository = InMemoryRepository() - - # act - repository.add(person) - - # assert - assert repository.get_by_id(person.id) == person - - -@pytest.mark.unit -def test_InMemoryRepository_persist_two(): - # arrange - person1 = Person(id=Person.next_id(), first_name="John", last_name="Doe") - person2 = Person(id=Person.next_id(), first_name="Mary", last_name="Doe") - repository = InMemoryRepository() - - # act - repository.add(person1) - repository.add(person2) - - # assert - assert repository.get_by_id(person1.id) == person1 - assert repository.get_by_id(person2.id) == person2 - - -@pytest.mark.unit -def test_InMemoryRepository_get_by_id_raises_exception(): - repository = InMemoryRepository() - with pytest.raises(EntityNotFoundException): - repository.get_by_id(Person.next_id()) - - -@pytest.mark.unit -def test_InMemoryRepository_remove_by_id_raises_exception(): - repository = InMemoryRepository() - with pytest.raises(EntityNotFoundException): - repository.remove_by_id(Person.next_id()) diff --git a/src/seedwork/tests/infrastructure/test_sqlalchemy_repository.py b/src/seedwork/tests/infrastructure/test_sqlalchemy_repository.py deleted file mode 100644 index f10df79..0000000 --- a/src/seedwork/tests/infrastructure/test_sqlalchemy_repository.py +++ /dev/null @@ -1,170 +0,0 @@ -import uuid -from dataclasses import dataclass - -import pytest -from sqlalchemy import Column, String -from sqlalchemy.orm import Session -from sqlalchemy_utils import UUIDType - -from seedwork.domain.entities import Entity -from seedwork.domain.exceptions import EntityNotFoundException -from seedwork.infrastructure.data_mapper import DataMapper -from seedwork.infrastructure.database import Base -from seedwork.infrastructure.repository import SqlAlchemyGenericRepository - - -@dataclass -class Person(Entity): - """Domain object""" - - first_name: str - last_name: str - - -class PersonModel(Base): - """Data model for a domain object""" - - __tablename__ = "person" - id = Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4) - first_name = Column(String) - last_name = Column(String) - - -class PersonDataMapper(DataMapper): - def model_to_entity(self, instance: PersonModel) -> Person: - return Person( - id=instance.id, - first_name=instance.first_name, - last_name=instance.last_name, - ) - - def entity_to_model(self, entity: Person) -> PersonModel: - return PersonModel( - id=entity.id, - first_name=entity.first_name, - last_name=entity.last_name, - ) - - -class PersonSqlAlchemyRepository(SqlAlchemyGenericRepository): - mapper_class = PersonDataMapper - model_class = PersonModel - - -@pytest.mark.integration -def test_sqlalchemy_repository_persist(db_session): - # arrange - person = Person(id=Person.next_id(), first_name="John", last_name="Doe") - repository = PersonSqlAlchemyRepository(db_session=db_session) - - # act - repository.add(person) - - # assert - assert repository.count() == 1 - - -@pytest.mark.integration -def test_sqlalchemy_repository_get_by_id(engine): - # arrange - person_id = Person.next_id() - - with Session(engine) as db_session: - person1 = Person(id=person_id, first_name="John", last_name="Doe") - repository1 = PersonSqlAlchemyRepository(db_session=db_session) - repository1.add(person1) - db_session.commit() - - # act - in separate session - with Session(engine) as db_session: - repository2 = PersonSqlAlchemyRepository(db_session=db_session) - person2 = repository2.get_by_id(person_id) - - # assert - assert person1 == person2 - - -@pytest.mark.integration -def test_sqlalchemy_repository_update(engine): - # arrange - person_id = Person.next_id() - - with Session(engine) as db_session: - person = Person(id=person_id, first_name="John", last_name="Doe") - repository = PersonSqlAlchemyRepository(db_session=db_session) - repository.add(person) - db_session.commit() - - # act - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - person = repository.get_by_id(person_id) - person.first_name = "Johnny" - repository.persist_all() - db_session.commit() - - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - person = repository.get_by_id(person_id) - assert person.first_name == "Johnny" - - -@pytest.mark.integration -def test_sqlalchemy_repository_remove_by_id(engine): - # arrange - person_id = Person.next_id() - - with Session(engine) as db_session: - person = Person(id=person_id, first_name="John", last_name="Doe") - repository = PersonSqlAlchemyRepository(db_session=db_session) - repository.add(person) - db_session.commit() - - # act - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - repository.remove_by_id(person_id) - db_session.commit() - - # assert - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - assert repository.count() == 0 - - -@pytest.mark.integration -def test_sqlalchemy_repository_remove(engine): - # arrange - person_id = Person.next_id() - - with Session(engine) as db_session: - person = Person(id=person_id, first_name="John", last_name="Doe") - repository = PersonSqlAlchemyRepository(db_session=db_session) - repository.add(person) - db_session.commit() - - # act - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - person = repository.get_by_id(person_id) - repository.remove(person) - db_session.commit() - - # assert - with Session(engine) as db_session: - repository = PersonSqlAlchemyRepository(db_session=db_session) - assert repository.count() == 0 - - -@pytest.mark.integration -def test_sqlalchemy_repository_get_by_id_raises_exception(db_session): - repository = PersonSqlAlchemyRepository(db_session=db_session) - with pytest.raises(EntityNotFoundException): - repository.get_by_id(Person.next_id()) - - -@pytest.mark.integration -def test_sqlalchemy_repository_remove_by_id_raises_exception(db_session): - repository = PersonSqlAlchemyRepository(db_session=db_session) - with pytest.raises(EntityNotFoundException): - repository.remove_by_id(Person.next_id()) diff --git a/src/seedwork/utils/__init__.py b/src/seedwork/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/seedwork/utils/data_structures.py b/src/seedwork/utils/data_structures.py deleted file mode 100644 index 5684c73..0000000 --- a/src/seedwork/utils/data_structures.py +++ /dev/null @@ -1,16 +0,0 @@ -from collections import OrderedDict - - -class OrderedSet(OrderedDict): - def __init__(self, iterable=None): - super().__init__() - if iterable: - for item in iterable: - self.add(item) - - def add(self, item): - self[item] = None - - def update(self, iterable): - for item in iterable: - self.add(item) diff --git a/src/seedwork/utils/functional.py b/src/seedwork/utils/functional.py deleted file mode 100644 index a1e7cf8..0000000 --- a/src/seedwork/utils/functional.py +++ /dev/null @@ -1,441 +0,0 @@ -""" -This is an exact copy of https://raw.githubusercontent.com/django/django/main/django/utils/functional.py -""" - -import copy -import itertools -import operator -from functools import total_ordering, wraps - - -class cached_property: - """ - Decorator that converts a method with a single self argument into a - property cached on the instance. - - A cached property can be made out of an existing method: - (e.g. ``url = cached_property(get_absolute_url)``). - The optional ``name`` argument is obsolete as of Python 3.6 and will be - deprecated in Django 4.0 (#30127). - """ - - name = None - - @staticmethod - def func(instance): - raise TypeError( - "Cannot use cached_property instance without calling " - "__set_name__() on it." - ) - - def __init__(self, func, name=None): - self.real_func = func - self.__doc__ = getattr(func, "__doc__") - - def __set_name__(self, owner, name): - if self.name is None: - self.name = name - self.func = self.real_func - elif name != self.name: - raise TypeError( - "Cannot assign the same cached_property to two different names " - "(%r and %r)." % (self.name, name) - ) - - def __get__(self, instance, cls=None): - """ - Call the function and put the return value in instance.__dict__ so that - subsequent attribute access on the instance returns the cached value - instead of calling cached_property.__get__(). - """ - if instance is None: - return self - res = instance.__dict__[self.name] = self.func(instance) - return res - - -class classproperty: - """ - Decorator that converts a method with a single cls argument into a property - that can be accessed directly from the class. - """ - - def __init__(self, method=None): - self.fget = method - - def __get__(self, instance, cls=None): - return self.fget(cls) - - def getter(self, method): - self.fget = method - return self - - -class Promise: - """ - Base class for the proxy class created in the closure of the lazy function. - It's used to recognize promises in code. - """ - - -def lazy(func, *resultclasses): - """ - Turn any callable into a lazy evaluated callable. result classes or types - is required -- at least one is needed so that the automatic forcing of - the lazy evaluation code is triggered. Results are not memoized; the - function is evaluated on every access. - """ - - @total_ordering - class __proxy__(Promise): - """ - Encapsulate a function call and act as a proxy for methods that are - called on the result of that function. The function is not evaluated - until one of the methods on the result is called. - """ - - __prepared = False - - def __init__(self, args, kw): - self.__args = args - self.__kw = kw - if not self.__prepared: - self.__prepare_class__() - self.__class__.__prepared = True - - def __reduce__(self): - return ( - _lazy_proxy_unpickle, - (func, self.__args, self.__kw) + resultclasses, - ) - - def __repr__(self): - return repr(self.__cast()) - - @classmethod - def __prepare_class__(cls): - for resultclass in resultclasses: - for type_ in resultclass.mro(): - for method_name in type_.__dict__: - # All __promise__ return the same wrapper method, they - # look up the correct implementation when called. - if hasattr(cls, method_name): - continue - meth = cls.__promise__(method_name) - setattr(cls, method_name, meth) - cls._delegate_bytes = bytes in resultclasses - cls._delegate_text = str in resultclasses - if cls._delegate_bytes and cls._delegate_text: - raise ValueError( - "Cannot call lazy() with both bytes and text return types." - ) - if cls._delegate_text: - cls.__str__ = cls.__text_cast - elif cls._delegate_bytes: - cls.__bytes__ = cls.__bytes_cast - - @classmethod - def __promise__(cls, method_name): - # Builds a wrapper around some magic method - def __wrapper__(self, *args, **kw): - # Automatically triggers the evaluation of a lazy value and - # applies the given magic method of the result type. - res = func(*self.__args, **self.__kw) - return getattr(res, method_name)(*args, **kw) - - return __wrapper__ - - def __text_cast(self): - return func(*self.__args, **self.__kw) - - def __bytes_cast(self): - return bytes(func(*self.__args, **self.__kw)) - - def __bytes_cast_encoded(self): - return func(*self.__args, **self.__kw).encode() - - def __cast(self): - if self._delegate_bytes: - return self.__bytes_cast() - elif self._delegate_text: - return self.__text_cast() - else: - return func(*self.__args, **self.__kw) - - def __str__(self): - # object defines __str__(), so __prepare_class__() won't overload - # a __str__() method from the proxied class. - return str(self.__cast()) - - def __eq__(self, other): - if isinstance(other, Promise): - other = other.__cast() - return self.__cast() == other - - def __lt__(self, other): - if isinstance(other, Promise): - other = other.__cast() - return self.__cast() < other - - def __hash__(self): - return hash(self.__cast()) - - def __mod__(self, rhs): - if self._delegate_text: - return str(self) % rhs - return self.__cast() % rhs - - def __add__(self, other): - return self.__cast() + other - - def __radd__(self, other): - return other + self.__cast() - - def __deepcopy__(self, memo): - # Instances of this class are effectively immutable. It's just a - # collection of functions. So we don't need to do anything - # complicated for copying. - memo[id(self)] = self - return self - - @wraps(func) - def __wrapper__(*args, **kw): - # Creates the proxy object, instead of the actual value. - return __proxy__(args, kw) - - return __wrapper__ - - -def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses): - return lazy(func, *resultclasses)(*args, **kwargs) - - -def lazystr(text): - """ - Shortcut for the common case of a lazy callable that returns str. - """ - return lazy(str, str)(text) - - -def keep_lazy(*resultclasses): - """ - A decorator that allows a function to be called with one or more lazy - arguments. If none of the args are lazy, the function is evaluated - immediately, otherwise a __proxy__ is returned that will evaluate the - function when needed. - """ - if not resultclasses: - raise TypeError("You must pass at least one argument to keep_lazy().") - - def decorator(func): - lazy_func = lazy(func, *resultclasses) - - @wraps(func) - def wrapper(*args, **kwargs): - if any( - isinstance(arg, Promise) - for arg in itertools.chain(args, kwargs.values()) - ): - return lazy_func(*args, **kwargs) - return func(*args, **kwargs) - - return wrapper - - return decorator - - -def keep_lazy_text(func): - """ - A decorator for functions that accept lazy arguments and return text. - """ - return keep_lazy(str)(func) - - -empty = object() - - -def new_method_proxy(func): - def inner(self, *args): - if self._wrapped is empty: - self._setup() - return func(self._wrapped, *args) - - return inner - - -class LazyObject: - """ - A wrapper for another class that can be used to delay instantiation of the - wrapped class. - - By subclassing, you have the opportunity to intercept and alter the - instantiation. If you don't need to do that, use SimpleLazyObject. - """ - - # Avoid infinite recursion when tracing __init__ (#19456). - _wrapped = None - - def __init__(self): - # Note: if a subclass overrides __init__(), it will likely need to - # override __copy__() and __deepcopy__() as well. - self._wrapped = empty - - __getattr__ = new_method_proxy(getattr) - - def __setattr__(self, name, value): - if name == "_wrapped": - # Assign to __dict__ to avoid infinite __setattr__ loops. - self.__dict__["_wrapped"] = value - else: - if self._wrapped is empty: - self._setup() - setattr(self._wrapped, name, value) - - def __delattr__(self, name): - if name == "_wrapped": - raise TypeError("can't delete _wrapped.") - if self._wrapped is empty: - self._setup() - delattr(self._wrapped, name) - - def _setup(self): - """ - Must be implemented by subclasses to initialize the wrapped object. - """ - raise NotImplementedError( - "subclasses of LazyObject must provide a _setup() method" - ) - - # Because we have messed with __class__ below, we confuse pickle as to what - # class we are pickling. We're going to have to initialize the wrapped - # object to successfully pickle it, so we might as well just pickle the - # wrapped object since they're supposed to act the same way. - # - # Unfortunately, if we try to simply act like the wrapped object, the ruse - # will break down when pickle gets our id(). Thus we end up with pickle - # thinking, in effect, that we are a distinct object from the wrapped - # object, but with the same __dict__. This can cause problems (see #25389). - # - # So instead, we define our own __reduce__ method and custom unpickler. We - # pickle the wrapped object as the unpickler's argument, so that pickle - # will pickle it normally, and then the unpickler simply returns its - # argument. - def __reduce__(self): - if self._wrapped is empty: - self._setup() - return (unpickle_lazyobject, (self._wrapped,)) - - def __copy__(self): - if self._wrapped is empty: - # If uninitialized, copy the wrapper. Use type(self), not - # self.__class__, because the latter is proxied. - return type(self)() - else: - # If initialized, return a copy of the wrapped object. - return copy.copy(self._wrapped) - - def __deepcopy__(self, memo): - if self._wrapped is empty: - # We have to use type(self), not self.__class__, because the - # latter is proxied. - result = type(self)() - memo[id(self)] = result - return result - return copy.deepcopy(self._wrapped, memo) - - __bytes__ = new_method_proxy(bytes) - __str__ = new_method_proxy(str) - __bool__ = new_method_proxy(bool) - - # Introspection support - __dir__ = new_method_proxy(dir) - - # Need to pretend to be the wrapped class, for the sake of objects that - # care about this (especially in equality tests) - __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) # type: ignore - __eq__ = new_method_proxy(operator.eq) - __lt__ = new_method_proxy(operator.lt) - __gt__ = new_method_proxy(operator.gt) - __ne__ = new_method_proxy(operator.ne) - __hash__ = new_method_proxy(hash) - - # List/Tuple/Dictionary methods support - __getitem__ = new_method_proxy(operator.getitem) - __setitem__ = new_method_proxy(operator.setitem) - __delitem__ = new_method_proxy(operator.delitem) - __iter__ = new_method_proxy(iter) - __len__ = new_method_proxy(len) - __contains__ = new_method_proxy(operator.contains) - - -def unpickle_lazyobject(wrapped): - """ - Used to unpickle lazy objects. Just return its argument, which will be the - wrapped object. - """ - return wrapped - - -class SimpleLazyObject(LazyObject): - """ - A lazy object initialized from any function. - - Designed for compound objects of unknown type. For builtins or objects of - known type, use django.utils.functional.lazy. - """ - - def __init__(self, func): - """ - Pass in a callable that returns the object to be wrapped. - - If copies are made of the resulting SimpleLazyObject, which can happen - in various circumstances within Django, then you must ensure that the - callable can be safely run more than once and will return the same - value. - """ - self.__dict__["_setupfunc"] = func - super().__init__() - - def _setup(self): - self._wrapped = self._setupfunc() - - # Return a meaningful representation of the lazy object for debugging - # without evaluating the wrapped object. - def __repr__(self): - if self._wrapped is empty: - repr_attr = self._setupfunc - else: - repr_attr = self._wrapped - return "<%s: %r>" % (type(self).__name__, repr_attr) - - def __copy__(self): - if self._wrapped is empty: - # If uninitialized, copy the wrapper. Use SimpleLazyObject, not - # self.__class__, because the latter is proxied. - return SimpleLazyObject(self._setupfunc) - else: - # If initialized, return a copy of the wrapped object. - return copy.copy(self._wrapped) - - def __deepcopy__(self, memo): - if self._wrapped is empty: - # We have to use SimpleLazyObject, not self.__class__, because the - # latter is proxied. - result = SimpleLazyObject(self._setupfunc) - memo[id(self)] = result - return result - return copy.deepcopy(self._wrapped, memo) - - -def partition(predicate, values): - """ - Split the values into two sets, based on the return value of the function - (True/False). e.g.: - - >>> partition(lambda x: x > 3, range(5)) - [0, 1, 2, 3], [4] - """ - results = ([], []) - for item in values: - results[predicate(item)].append(item) - return results