diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..aa948b0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,17 @@ +[flake8] +ignore = E501,W503,FS003,F403,F405,E203 +include = src +max-line-length = 88 +max-doc-length = 88 +# mccabe cyclomatic complexity +max-complexity = 7 +# flake8-expression-complexity +max-expression-complexity = 5.5 +# flake8-class-attributes-order +use_class_attributes_order_strict_mode=True +# flake8-eradicate +# this +eradicate_whitelist_extend = "#openapi.yaml#" +per-file-ignores = + services/*/generated/*: F401,W505 + services/*/on_service_ready.py: F401,E800 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45b7e68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +settings.json + +# 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/ +share/python-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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cbf75ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "foo", + "type": "python", + "request": "launch", + "module": "foo", + "justMyCode": true + }, + { + "name": "bar", + "type": "python", + "request": "launch", + "module": "bar", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8146e59 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,674 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +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 = "coverage" +version = "7.2.7" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "eradicate" +version = "2.3.0" +description = "Removes commented-out code." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, + {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, +] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "flake8-comprehensions" +version = "3.13.0" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_comprehensions-3.13.0-py3-none-any.whl", hash = "sha256:cc0d6dbb336ff4e9cdf4eb605a3f719ea59261f2d6ba52034871a173c40e1f60"}, + {file = "flake8_comprehensions-3.13.0.tar.gz", hash = "sha256:83cf98e816c9e23360f36aaf47de59a5b21437fdff8a056c46e2ad49f81861bf"}, +] + +[package.dependencies] +flake8 = ">=3.0,<3.2.0 || >3.2.0" + +[[package]] +name = "flake8-eradicate" +version = "1.5.0" +description = "Flake8 plugin to find commented out code" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, + {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, +] + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">5" + +[[package]] +name = "flake8-functions" +version = "0.0.7" +description = "A flake8 extension that checks functions" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "flake8_functions-0.0.7-py3-none-any.whl", hash = "sha256:f2f75545c2b0df9eeba0ad316e2ac38c101676970b4441300fc07af3226a44f6"}, + {file = "flake8_functions-0.0.7.tar.gz", hash = "sha256:40584b05d57e5ab185545bcfa08aa0edca52b04646d0df266e2b1667d6437184"}, +] + +[package.dependencies] +mr-proper = "*" +setuptools = "*" + +[[package]] +name = "flake8-pep585" +version = "0.1.7" +description = "flake8 plugin to enforce new-style type hints (PEP 585)" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "flake8-pep585-0.1.7.tar.gz", hash = "sha256:363f9413aa12849ee9bfdc437c4e79cc4e0fb3af4abbb61cfed79860e349e0e0"}, + {file = "flake8_pep585-0.1.7-py3-none-any.whl", hash = "sha256:d5c7a5858382d6ca8c56554bd8bed090e12c378b98f6d7c6502abed9a40a658e"}, +] + +[[package]] +name = "flake8-pep604" +version = "0.1.0" +description = "flake8 plugin to enforce use of `|` over `typing.Union`" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "flake8_pep604-0.1.0-py2.py3-none-any.whl", hash = "sha256:87449930c00c50dfac4b2d3dcb847b49507a05fb8b888dc973dd338df350ca81"}, + {file = "flake8_pep604-0.1.0.tar.gz", hash = "sha256:38d8852ac6a7c33a0d863841154fdc055128713f2f78d13e9664ac277374c4f2"}, +] + +[package.dependencies] +flake8 = ">=3.8" + +[[package]] +name = "flake8-use-fstring" +version = "1.4" +description = "Flake8 plugin for string formatting style." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "flake8-use-fstring-1.4.tar.gz", hash = "sha256:6550bf722585eb97dffa8343b0f1c372101f5c4ab5b07ebf0edd1c79880cdd39"}, +] + +[package.dependencies] +flake8 = ">=3" + +[package.extras] +ci = ["coverage (>=4.0.0,<5.0.0)", "coveralls", "flake8-builtins", "flake8-commas", "flake8-fixme", "flake8-print", "flake8-quotes", "flake8-todo", "pytest (>=4)", "pytest-cov (>=2)"] +dev = ["coverage (>=4.0.0,<5.0.0)", "flake8-builtins", "flake8-commas", "flake8-fixme", "flake8-print", "flake8-quotes", "flake8-todo", "pytest (>=4)", "pytest-cov (>=2)"] +test = ["coverage (>=4.0.0,<5.0.0)", "flake8-builtins", "flake8-commas", "flake8-fixme", "flake8-print", "flake8-quotes", "flake8-todo", "pytest (>=4)", "pytest-cov (>=2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +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 = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "loguru" +version = "0.7.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"}, + {file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v0.990)", "pre-commit (==3.2.1)", "pytest (==6.1.2)", "pytest (==7.2.1)", "pytest-cov (==2.12.1)", "pytest-cov (==4.0.0)", "pytest-mypy-plugins (==1.10.1)", "pytest-mypy-plugins (==1.9.3)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.2.0)", "tox (==3.27.1)", "tox (==4.4.6)"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mr-proper" +version = "0.0.7" +description = "Static Python code analyzer, that tries to check if functions in code are pure or not and why." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "mr_proper-0.0.7-py3-none-any.whl", hash = "sha256:74a1b60240c46f10ba518707ef72811a01e5c270da0a78b5dd2dd923d99fdb14"}, + {file = "mr_proper-0.0.7.tar.gz", hash = "sha256:03b517b19e617537f711ce418b125e5f2efd82ec881539cdee83195c78c14a02"}, +] + +[package.dependencies] +click = ">=7.1.2" +setuptools = "*" +stdlib-list = ">=0.5.0" + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +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 = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "platformdirs" +version = "3.8.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "plumbum" +version = "1.8.2" +description = "Plumbum: shell combinators library" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "plumbum-1.8.2-py3-none-any.whl", hash = "sha256:3ad9e5f56c6ec98f6f7988f7ea8b52159662ea9e915868d369dbccbfca0e367e"}, + {file = "plumbum-1.8.2.tar.gz", hash = "sha256:9e6dc032f4af952665f32f3206567bc23b7858b1413611afe603a3f8ad9bfd75"}, +] + +[package.dependencies] +pywin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +dev = ["paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] +docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] +ssh = ["paramiko"] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "rpyc" +version = "5.3.1" +description = "Remote Python Call (RPyC) is a transparent and symmetric distributed computing library" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rpyc-5.3.1-py3-none-any.whl", hash = "sha256:6e8153792ac221a80f420d2e0b241a10c6e43b105d325998b18a4e7af329f9ec"}, + {file = "rpyc-5.3.1.tar.gz", hash = "sha256:f2233174879faf18ae266437d5a65511ce46c817cec4edc1344f036758cfbf52"}, +] + +[package.dependencies] +plumbum = "*" + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "stdlib-list" +version = "0.9.0" +description = "A list of Python Standard Libraries (2.7 through 3.9)." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "stdlib_list-0.9.0-py3-none-any.whl", hash = "sha256:f79957d59e41930d44afcd81e465f740b9a7a9828707a40e24ab1092b12bd423"}, + {file = "stdlib_list-0.9.0.tar.gz", hash = "sha256:98eb66135976c96b4ee3f4c0ef0552ebb5a9977ce3028433db79f4738b02af26"}, +] + +[package.extras] +dev = ["build", "stdlib-list[doc,lint,test]"] +doc = ["furo", "sphinx"] +lint = ["black", "mypy", "ruff"] +support = ["sphobjinv"] +test = ["coverage[toml]", "pytest", "pytest-cov"] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "bf97d78cee39148ec209f0c2056783ba41c0b6aa3937e1ef15f45cc6b10e5dd1" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fd24306 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,72 @@ +[tool.poetry] +name = "pyDataInterface" +version = "0.1.0" +description = "" +authors = ["Mose Mueller "] +readme = "README.md" +packages = [{include = "pyDataInterface", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.11" +rpyc = "^5.3.1" +loguru = "^0.7.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +mypy = "^1.4.1" +black = "^23.1.0" +isort = "^5.12.0" +flake8 = "^5.0.4" +flake8-use-fstring = "^1.4" +flake8-functions = "^0.0.7" +flake8-comprehensions = "^3.11.1" +flake8-pep585 = "^0.1.7" +flake8-pep604 = "^0.1.0" +flake8-eradicate = "^1.4.0" + +[tool.setuptools.package-data] +pyDataInterface = ['frontend/*', 'examples/*', 'version.json'] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pyright] +include = ["pyDataInterface", "tests"] +exclude = ["**/node_modules", + "**/__pycache__", + "docs", + "frontend", +] +venvPath = "." +venv = ".venv" +typeCheckingMode = "basic" +reportUnknownMemberType = true + +[tool.black] +line-length = 88 +exclude = ''' +/( + \.git + | \.mypy_cache + | \.tox + | venv + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" + +[tool.mypy] +show_error_codes = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_incomplete_defs = true +check_untyped_defs = true +ignore_missing_imports = false diff --git a/src/pyDataInterface/__init__.py b/src/pyDataInterface/__init__.py new file mode 100644 index 0000000..1ad0ad1 --- /dev/null +++ b/src/pyDataInterface/__init__.py @@ -0,0 +1,3 @@ +from .data_service import DataService + +__all__ = ["DataService"] diff --git a/src/pyDataInterface/client.py b/src/pyDataInterface/client.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pyDataInterface/data_service/__init__.py b/src/pyDataInterface/data_service/__init__.py new file mode 100644 index 0000000..77faca0 --- /dev/null +++ b/src/pyDataInterface/data_service/__init__.py @@ -0,0 +1,7 @@ +from .data_service import DataService +from .data_service_list import DataServiceList + +__all__ = [ + "DataService", + "DataServiceList", +] diff --git a/src/pyDataInterface/data_service/data_service.py b/src/pyDataInterface/data_service/data_service.py new file mode 100644 index 0000000..7833eaf --- /dev/null +++ b/src/pyDataInterface/data_service/data_service.py @@ -0,0 +1,337 @@ +import asyncio +import inspect +import threading +from collections.abc import Callable +from concurrent.futures import Future +from typing import Any, cast + +import rpyc +from loguru import logger + +from .data_service_list import DataServiceList + + +class DataService(rpyc.Service): + _full_access_path: set[str] + """ TODO: improve this docstring + A set of strings, each representing a unique path to access the attribute from an + exposed class instance. Each path starts with the name of the exposed class. It's + dynamically updated to accurately represent the current attribute structure. + This attribute is used to emit notifications to a web server whenever the attribute + changes, allowing for real-time tracking and updates of class instance + modifications. + + Example: + -------- + >>> class SubClass(DataService): + >>> pass + + >>> class ExposedClass(DataService): + >>> attr = SubClass() + + >>> service = ExposedClass() + >>> # ... expose class + + >>> print(service.attr._full_access_path) # {"ServiceClass.attr"} + + Have a look at tests/test_full_access_path.py to see more examples. + """ + _list_mapping: dict[int, DataServiceList] = {} + """ + A dictionary mapping the id of the original lists to the corresponding + DataServiceList instances. + This is used to ensure that all references to the same list within the DataService + object point to the same DataServiceList, so that any modifications to that list can + be tracked consistently. The keys of the dictionary are the ids of the original + lists, and the values are the DataServiceList instances that wrap these lists. + """ + + def __init__(self) -> None: + # dictionary to keep track of running tasks + self.__tasks: dict[str, Future[None]] = {} + self._autostart_tasks: dict[str, tuple[Any]] + if "_autostart_tasks" not in self.__dict__: + self._autostart_tasks = {} + + self._set_start_and_stop_for_async_methods() + + self._start_async_loop_in_thread() + self._start_autostart_tasks() + + self._update_full_access_path(self, f"{self.__class__.__name__}") + self._turn_lists_into_notify_lists() + self._do_something_with_properties() + self._initialised = True + + def _do_something_with_properties(self) -> None: + for attr_name in dir(self.__class__): + attr_value = getattr(self.__class__, attr_name) + if isinstance(attr_value, property): # If attribute is a property + logger.debug(attr_value.fget.__code__.co_names) + + def _turn_lists_into_notify_lists(self) -> None: + def create_callback(attr_name: str) -> Callable: + """TODO: explain what this is used for... + Create a callback with current attr_name captured in the default argument. + + Default arguments solve the late binding problem by capturing the value at + the time the lambda is defined, not when it is called, thus preventing + attr_name from being overwritten in another loop iteratianother + """ + + return lambda index, value, attr_name=attr_name: self._emit( + access_path=self._full_access_path, + name=f"{attr_name}[{index}]", + value=value, + ) + + # Convert all list attributes (both class and instance) to DataServiceList + for attr_name in set(dir(self)) - set(dir(object)): + attr_value = getattr(self, attr_name) + + if isinstance(attr_value, list): + # Create callback for current attr_name + callback = create_callback(attr_name) + + # Check if attr_value is already a DataServiceList or in the mapping + if isinstance(attr_value, DataServiceList): + attr_value.add_callback(callback) + continue + + if id(attr_value) in self._list_mapping: + notifying_list = self._list_mapping[id(attr_value)] + notifying_list.add_callback(callback) + else: + notifying_list = DataServiceList(attr_value, callback=[callback]) + self._list_mapping[id(attr_value)] = notifying_list + + setattr(self, attr_name, notifying_list) + + def _start_autostart_tasks(self) -> None: + if self._autostart_tasks is not None: + for service_name, args in self._autostart_tasks.items(): + start_method = getattr(self, f"start_{service_name}", None) + if start_method is not None and callable(start_method): + start_method(*args) + else: + logger.warning( + f"No start method found for service '{service_name}'" + ) + + def _start_async_loop_in_thread(self) -> None: + # create a new event loop and run it in a separate thread + self.__loop = asyncio.new_event_loop() + self.__thread = threading.Thread(target=self._start_loop) + self.__thread.start() + + def _set_start_and_stop_for_async_methods(self) -> None: + # inspect the methods of the class + for name, method in inspect.getmembers( + self, predicate=inspect.iscoroutinefunction + ): + + def start_task(*args: Any, **kwargs: Any) -> None: + async def task(*args: Any, **kwargs: Any) -> None: + try: + await getattr(self, name)(*args, **kwargs) + except asyncio.CancelledError: + print(f"Task {name} was cancelled") + + self.__tasks[name] = asyncio.run_coroutine_threadsafe( + task(*args, **kwargs), self.__loop + ) + + def stop_task() -> None: + # cancel the task + task = self.__tasks.get(name) + if task is not None: + self.__loop.call_soon_threadsafe(task.cancel) + + # create start and stop methods for each coroutine + setattr(self, f"start_{name}", start_task) + setattr(self, f"stop_{name}", stop_task) + + def _update_full_access_path(self, obj: "DataService", parent_path: str) -> None: + """ + Recursive helper function to update '_full_access_path' for the object and all + its nested attributes + """ + + parent_class_name = parent_path.split(".")[0] if parent_path else None + + # Remove all access paths that don't start with the parent class name. As the + # exposed class is instantiated last, this ensures that all access paths start + # with the root class + access_path: set[str] = { + p + for p in cast(list[str], getattr(obj, "_full_access_path", set())) + if not parent_class_name or p.startswith(parent_class_name) + } + # add the new access path + access_path.add(parent_path) + setattr(obj, "_full_access_path", access_path) + + # Recursively update access paths for all nested attributes of the object + for nested_attr_name in set(dir(obj)) - set(dir(object)): + nested_attr = getattr(obj, nested_attr_name) + if isinstance(nested_attr, list): + for i, list_item in enumerate(nested_attr): + if isinstance(list_item, DataService): + new_path = f"{parent_path}.{nested_attr_name}[{i}]" + self._update_full_access_path(list_item, new_path) + elif isinstance(nested_attr, DataService): + new_path = f"{parent_path}.{nested_attr_name}" + self._update_full_access_path(nested_attr, new_path) + + def _start_loop(self) -> None: + asyncio.set_event_loop(self.__loop) + try: + self.__loop.run_forever() + finally: + # cancel all running tasks + for task in self.__tasks.values(): + self.__loop.call_soon_threadsafe(task.cancel) + self.__loop.call_soon_threadsafe(self.__loop.stop) + self.__thread.join() + + def __setattr__(self, __name: str, __value: Any) -> None: + if self.__dict__.get("_initialised"): + access_path: set[str] = getattr(self, "_full_access_path", set()) + if access_path: + self._emit(access_path, __name, __value) + # TODO: add emits for properties -> can use co_names, which is a tuple + # containing the names used by the bytecode + super().__setattr__(__name, __value) + + def _emit(self, access_path: set[str], name: str, value: Any) -> None: + for path in access_path: + logger.debug(f"{path}.{name} changed to {value}!") + + def _rpyc_getattr(self, name: str) -> Any: + if name.startswith("_"): + # disallow special and private attributes + raise AttributeError("cannot access private/special names") + # allow all other attributes + return getattr(self, name) + + def _rpyc_setattr(self, name: str, value: Any): + if name.startswith("_"): + # disallow special and private attributes + raise AttributeError("cannot access private/special names") + + # check if the attribute has a setter method + attr = getattr(self, name, None) + if isinstance(attr, property) and attr.fset is None: + raise AttributeError(f"{name} attribute does not have a setter method") + + # allow all other attributes + setattr(self, name, value) + + def serialize(self, prefix: str = "") -> dict[str, dict[str, Any]]: + """ + Serializes the instance into a dictionary, preserving the structure of the + instance. + + For each attribute, method, and property, the method includes its name, type, + value, readonly status, and documentation if any in the resulting dictionary. + Attributes and methods starting with an underscore are ignored. + + For attributes, methods, and properties unique to the class (not inherited from + the base class), the method uses the format "." for keys in the + dictionary. If no prefix is provided, the key format is simply "". + + For nested DataService instances, the method serializes recursively and appends + the key of the nested instance to the prefix in the format ".". + + For attributes of type list, each item in the list is serialized individually. + If an item in the list is an instance of DataService, it is serialized + recursively with its key in the format "..", where + "item_id" is the id of the item itself. + + Args: + prefix (str, optional): The prefix for each key in the serialized + dictionary. This is mainly used when this method is called recursively to + maintain the structure of nested instances. + + Returns: + dict: The serialized instance. + """ + result: dict[str, dict[str, Any]] = {} + + # Get the dictionary of the base class + base_dict = set(super().__class__.__dict__) + # Get the dictionary of the derived class + derived_dict = set(self.__class__.__dict__) + # Get the difference between the two dictionaries + derived_only_dict = derived_dict - base_dict + + instance_dict = set(self.__dict__) + # Merge the class and instance dictionaries + merged_dict = derived_only_dict | instance_dict + + # Iterate over attributes, properties, class attributes, and methods + for key in merged_dict: + if key.startswith("_"): + continue # Skip attributes that start with underscore + + # Get the value of the current attribute or method + value = getattr(self, key) + + # Prepare the key by appending prefix and the key + key = f"{prefix}.{key}" if prefix else key + + if isinstance(value, DataService): + result[key] = { + "type": type(value).__name__, + "value": value.serialize(prefix=key), + "readonly": False, + "id": id(value), + "doc": inspect.getdoc(value), + } + elif isinstance(value, list): + result[key] = { + "type": "list", + "value": [ + { + "type": type(item).__name__, + "value": item.serialize(prefix=key) + if isinstance(item, DataService) + else item, + "readonly": False, + "id": id(item), + } + for item in value + ], + "readonly": False, + } + elif inspect.isfunction(value) or inspect.ismethod(value): + sig = inspect.signature(value) + parameters = { + k: v.annotation.__name__ + if v.annotation is not inspect._empty + else None + for k, v in sig.parameters.items() + } + result[key] = { + "type": "method", + "async": asyncio.iscoroutinefunction(value), + "parameters": parameters, + "readonly": False, + "doc": inspect.getdoc(value), + } + elif isinstance(getattr(self.__class__, key, None), property): + prop: property = getattr(self.__class__, key) + result[key] = { + "type": type(value).__name__, + "value": value, + "readonly": prop.fset is None, + "doc": inspect.getdoc(prop), + } + else: + result[key] = { + "type": type(value).__name__, + "value": value, + "readonly": False, + } + + return result diff --git a/src/pyDataInterface/data_service/data_service_list.py b/src/pyDataInterface/data_service/data_service_list.py new file mode 100644 index 0000000..2813768 --- /dev/null +++ b/src/pyDataInterface/data_service/data_service_list.py @@ -0,0 +1,58 @@ +from collections.abc import Callable +from typing import Any + + +class DataServiceList(list): + """ + DataServiceList is a list with additional functionality to trigger callbacks + whenever an item is set. This can be used to track changes in the list items. + + The class takes the same arguments as the list superclass during initialization, + with an additional optional 'callback' argument that is a list of functions. + These callbacks are stored and executed whenever an item in the DataServiceList + is set via the __setitem__ method. The callbacks receive the index of the changed + item and its new value as arguments. + + The original list that is passed during initialization is kept as a private + attribute to prevent it from being garbage collected. + + Additional callbacks can be added after initialization using the `add_callback` + method. + + Attributes: + _original_list (list): + Reference to the original list, to prevent it from being garbage collected. + callbacks (list): + List of callback functions to be executed on item set. + """ + + def __init__( + self, + *args: list[Any], + callback: list[Callable[[int, Any], None]] | None = None, + **kwargs: Any, + ) -> None: + self.callbacks: list[Callable[[int, Any], None]] = [] + if isinstance(callback, list): + self.callbacks = callback + + # prevent gc to delete the passed list by keeping a reference + self._original_list = args[0] + + super().__init__(*args, **kwargs) # type: ignore + + def __setitem__(self, key: int, value: Any) -> None: # type: ignore + super().__setitem__(key, value) # type: ignore + + for callback in self.callbacks: + callback(key, value) + + def add_callback(self, callback: Callable[[int, Any], None]) -> None: + """ + Add a new callback function to be executed on item set. + + Args: + callback (Callable[[int, Any], None]): Callback function that takes two + arguments - index of the changed item and its new value. + """ + self.callbacks.append(callback) diff --git a/src/pyDataInterface/server/__init__.py b/src/pyDataInterface/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pyDataInterface/server/server.py b/src/pyDataInterface/server/server.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_autostart_task.py b/tests/test_autostart_task.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_emit_on_change.py b/tests/test_emit_on_change.py new file mode 100644 index 0000000..965063c --- /dev/null +++ b/tests/test_emit_on_change.py @@ -0,0 +1,138 @@ +from typing import Any + +from pytest import CaptureFixture + +from pyDataInterface import DataService + + +def emit(self: Any, access_path: set[str], name: str, value: Any) -> None: + if isinstance(value, DataService): + value = value.serialize() + + for path in access_path: + print(f"{path}.{name} = {value}") + + +DataService._emit = emit # type: ignore + + +def test_class_attribute(capsys: CaptureFixture) -> None: + class ServiceClass(DataService): + attr = 0 + + service_instance = ServiceClass() + + service_instance.attr = 1 + captured = capsys.readouterr() + assert captured.out == "ServiceClass.attr = 1\n" + + +def test_instance_attribute(capsys: CaptureFixture) -> None: + class ServiceClass(DataService): + def __init__(self) -> None: + self.attr = "Hello World" + super().__init__() + + service_instance = ServiceClass() + + service_instance.attr = "Hello" + captured = capsys.readouterr() + assert captured.out == "ServiceClass.attr = Hello\n" + + +def test_class_list_attribute(capsys: CaptureFixture) -> None: + class ServiceClass(DataService): + attr = [0, 1] + + service_instance = ServiceClass() + + service_instance.attr[0] = 1337 + captured = capsys.readouterr() + assert captured.out == "ServiceClass.attr[0] = 1337\n" + + +def test_instance_list_attribute(capsys: CaptureFixture) -> None: + class SubClass(DataService): + name = "SubClass" + + class ServiceClass(DataService): + def __init__(self) -> None: + self.attr = [0, SubClass()] + super().__init__() + + service_instance = ServiceClass() + + _ = capsys.readouterr() + + service_instance.attr[0] = "Hello" + captured = capsys.readouterr() + assert captured.out == "ServiceClass.attr[0] = Hello\n" + + service_instance.attr[1] = SubClass() + captured = capsys.readouterr() + assert ( + captured.out.strip() + == "ServiceClass.attr[1] = {'name': {'type': 'str', 'value': 'SubClass'," + " 'readonly': False}}" + ) + + +def test_reused_instance_list_attribute(capsys: CaptureFixture) -> None: + some_list = [0, 1, 2] + + class ServiceClass(DataService): + def __init__(self) -> None: + self.attr = some_list + self.attr_2 = some_list + self.attr_3 = [0, 1, 2] + super().__init__() + + service_instance = ServiceClass() + + service_instance.attr[0] = "Hello" + captured = capsys.readouterr() + + assert service_instance.attr == service_instance.attr_2 + assert service_instance.attr != service_instance.attr_3 + expected_output = sorted( + [ + "ServiceClass.attr[0] = Hello", + "ServiceClass.attr_2[0] = Hello", + ] + ) + actual_output = sorted(captured.out.strip().split("\n")) + assert actual_output == expected_output + + +def test_nested_reused_instance_list_attribute(capsys: CaptureFixture) -> None: + some_list = [0, 1, 2] + + class SubClass(DataService): + attr_list = some_list + + def __init__(self) -> None: + self.attr_list_2 = some_list + super().__init__() + + class ServiceClass(DataService): + def __init__(self) -> None: + self.attr = some_list + self.subclass = SubClass() + super().__init__() + + service_instance = ServiceClass() + + _ = capsys.readouterr() + service_instance.attr[0] = "Hello" + captured = capsys.readouterr() + + assert service_instance.attr == service_instance.subclass.attr_list + expected_output = sorted( + [ + "ServiceClass.subclass.attr_list_2[0] = Hello", + "ServiceClass.subclass.attr_list[0] = Hello", + "ServiceClass.attr[0] = Hello", + ] + ) + actual_output = sorted(captured.out.strip().split("\n")) + assert actual_output == expected_output diff --git a/tests/test_full_access_path.py b/tests/test_full_access_path.py new file mode 100644 index 0000000..fc22247 --- /dev/null +++ b/tests/test_full_access_path.py @@ -0,0 +1,364 @@ +from pyDataInterface import DataService + + +def test_class_attributes() -> None: + class SubClass(DataService): + pass + + class ServiceClass(DataService): + attr_1 = SubClass() + + test_service = ServiceClass() + assert test_service.attr_1._full_access_path == {"ServiceClass.attr_1"} + + +def test_instance_attributes() -> None: + class SubClass(DataService): + pass + + class ServiceClass(DataService): + def __init__(self): + self.attr_1 = SubClass() + super().__init__() + + test_service = ServiceClass() + assert test_service.attr_1._full_access_path == {"ServiceClass.attr_1"} + + +def test_reused_instance_attributes() -> None: + class SubClass(DataService): + pass + + subclass_instance = SubClass() + + class ServiceClass(DataService): + def __init__(self): + self.attr_1 = subclass_instance + self.attr_2 = subclass_instance + super().__init__() + + test_service = ServiceClass() + assert test_service.attr_1._full_access_path == { + "ServiceClass.attr_1", + "ServiceClass.attr_2", + } + assert test_service.attr_2._full_access_path == { + "ServiceClass.attr_1", + "ServiceClass.attr_2", + } + + assert test_service.attr_1._full_access_path == { + "ServiceClass.attr_1", + "ServiceClass.attr_2", + } + + +def test_reused_attributes_mixed() -> None: + class SubClass(DataService): + pass + + subclass_instance = SubClass() + + class ServiceClass(DataService): + attr_1 = subclass_instance + + def __init__(self): + self.attr_2 = subclass_instance + super().__init__() + + test_service = ServiceClass() + assert test_service.attr_1._full_access_path == { + "ServiceClass.attr_1", + "ServiceClass.attr_2", + } + assert test_service.attr_2._full_access_path == { + "ServiceClass.attr_1", + "ServiceClass.attr_2", + } + + +def test_nested_class_attributes() -> None: + class SubSubSubClass(DataService): + pass + + class SubSubClass(DataService): + attr = SubSubSubClass() + + class SubClass(DataService): + attr = SubSubClass() + + class ServiceClass(DataService): + attr = SubClass() + + test_service = ServiceClass() + assert test_service.attr._full_access_path == { + "ServiceClass.attr", + } + assert test_service.attr.attr._full_access_path == { + "ServiceClass.attr.attr", + } + assert test_service.attr.attr.attr._full_access_path == { + "ServiceClass.attr.attr.attr", + } + + +def test_nested_instance_attributes() -> None: + class SubSubSubClass(DataService): + pass + + class SubSubClass(DataService): + def __init__(self): + self.attr = SubSubSubClass() + super().__init__() + + class SubClass(DataService): + def __init__(self): + self.attr = SubSubClass() + super().__init__() + + class ServiceClass(DataService): + def __init__(self): + self.attr = SubClass() + super().__init__() + + test_service = ServiceClass() + assert test_service.attr._full_access_path == { + "ServiceClass.attr", + } + assert test_service.attr.attr._full_access_path == { + "ServiceClass.attr.attr", + } + assert test_service.attr.attr.attr._full_access_path == { + "ServiceClass.attr.attr.attr", + } + + +def test_advanced_nested_instance_attributes() -> None: + class SubSubSubClass(DataService): + pass + + class SubSubClass(DataService): + def __init__(self): + self.attr = SubSubSubClass() + super().__init__() + + subsubclass_instance = SubSubClass() + + class SubClass(DataService): + def __init__(self): + self.attr = subsubclass_instance + super().__init__() + + class ServiceClass(DataService): + def __init__(self): + self.attr = SubClass() + self.subattr = subsubclass_instance + super().__init__() + + test_service = ServiceClass() + assert test_service.attr._full_access_path == { + "ServiceClass.attr", + } + assert test_service.attr.attr._full_access_path == { + "ServiceClass.attr.attr", + "ServiceClass.subattr", + } + assert test_service.attr.attr.attr._full_access_path == { + "ServiceClass.attr.attr.attr", + "ServiceClass.subattr.attr", # as the SubSubSubClass does not implement anything, both subattr.attr and attr.attr.attr refer to the same instance + } + + +def test_advanced_nested_class_attributes() -> None: + class SubSubSubClass(DataService): + pass + + class SubSubClass(DataService): + attr = SubSubSubClass() + + class SubClass(DataService): + attr = SubSubClass() + + class ServiceClass(DataService): + attr = SubClass() + subattr = SubSubClass() + + test_service = ServiceClass() + assert test_service.attr._full_access_path == { + "ServiceClass.attr", + } + assert test_service.subattr._full_access_path == { + "ServiceClass.subattr", + } + assert test_service.attr.attr._full_access_path == { + "ServiceClass.attr.attr", + } + assert test_service.attr.attr.attr._full_access_path == { + "ServiceClass.attr.attr.attr", + "ServiceClass.subattr.attr", # as the SubSubSubClass does not implement anything, both subattr.attr and attr.attr.attr refer to the same instance + } + + +def test_advanced_nested_attributes_mixed() -> None: + class SubSubClass(DataService): + pass + + class SubClass(DataService): + attr = SubSubClass() + + def __init__(self): + self.attr_1 = SubSubClass() + super().__init__() + + class ServiceClass(DataService): + subattr = SubClass() + + def __init__(self): + self.attr = SubClass() + super().__init__() + + test_service = ServiceClass() + assert test_service.attr._full_access_path == { + "ServiceClass.attr", + } + assert test_service.subattr._full_access_path == { + "ServiceClass.subattr", + } + + # Subclass.attr is the same for all instances + assert test_service.attr.attr == test_service.subattr.attr + assert test_service.attr.attr._full_access_path == { + "ServiceClass.attr.attr", + "ServiceClass.subattr.attr", + } + assert test_service.subattr.attr._full_access_path == { + "ServiceClass.subattr.attr", + "ServiceClass.attr.attr", + } + + # attr_1 is different for all instances of SubClass + assert test_service.attr.attr_1 != test_service.subattr.attr + assert test_service.attr.attr_1 != test_service.subattr.attr_1 + assert test_service.subattr.attr_1._full_access_path == { + "ServiceClass.subattr.attr_1", + } + assert test_service.attr.attr_1._full_access_path == { + "ServiceClass.attr.attr_1", + } + + +def test_class_list_attributes() -> None: + class SubClass(DataService): + pass + + subclass_instance = SubClass() + + class ServiceClass(DataService): + attr_list = [SubClass() for _ in range(2)] + attr_list_2 = [subclass_instance, subclass_instance] + attr = subclass_instance + + test_service = ServiceClass() + assert test_service.attr_list[0] != test_service.attr_list[1] + assert test_service.attr_list[0]._full_access_path == { + "ServiceClass.attr_list[0]", + } + assert test_service.attr_list[1]._full_access_path == { + "ServiceClass.attr_list[1]", + } + + assert test_service.attr_list_2[0] == test_service.attr + assert test_service.attr_list_2[0] == test_service.attr_list_2[1] + assert test_service.attr_list_2[0]._full_access_path == { + "ServiceClass.attr", + "ServiceClass.attr_list_2[0]", + "ServiceClass.attr_list_2[1]", + } + assert test_service.attr_list_2[1]._full_access_path == { + "ServiceClass.attr", + "ServiceClass.attr_list_2[0]", + "ServiceClass.attr_list_2[1]", + } + + +def test_nested_class_list_attributes() -> None: + class SubSubClass(DataService): + pass + + subsubclass_instance = SubSubClass() + + class SubClass(DataService): + attr_list = [subsubclass_instance] + + class ServiceClass(DataService): + attr = [SubClass()] + subattr = subsubclass_instance + + test_service = ServiceClass() + assert test_service.attr[0].attr_list[0] == test_service.subattr + assert test_service.attr[0].attr_list[0]._full_access_path == { + "ServiceClass.attr[0].attr_list[0]", + "ServiceClass.subattr", + } + + +def test_instance_list_attributes() -> None: + class SubClass(DataService): + pass + + subclass_instance = SubClass() + + class ServiceClass(DataService): + def __init__(self): + self.attr_list = [SubClass() for _ in range(2)] + self.attr_list_2 = [subclass_instance, subclass_instance] + self.attr = subclass_instance + super().__init__() + + test_service = ServiceClass() + assert test_service.attr_list[0] != test_service.attr_list[1] + assert test_service.attr_list[0]._full_access_path == { + "ServiceClass.attr_list[0]", + } + assert test_service.attr_list[1]._full_access_path == { + "ServiceClass.attr_list[1]", + } + + assert test_service.attr_list_2[0] == test_service.attr + assert test_service.attr_list_2[0] == test_service.attr_list_2[1] + assert test_service.attr_list_2[0]._full_access_path == { + "ServiceClass.attr", + "ServiceClass.attr_list_2[0]", + "ServiceClass.attr_list_2[1]", + } + assert test_service.attr_list_2[1]._full_access_path == { + "ServiceClass.attr", + "ServiceClass.attr_list_2[0]", + "ServiceClass.attr_list_2[1]", + } + + +def test_nested_instance_list_attributes() -> None: + class SubSubClass(DataService): + pass + + subsubclass_instance = SubSubClass() + + class SubClass(DataService): + def __init__(self): + self.attr_list = [subsubclass_instance] + super().__init__() + + class ServiceClass(DataService): + subattr = subsubclass_instance + + def __init__(self): + self.attr = [SubClass()] + super().__init__() + + test_service = ServiceClass() + assert test_service.attr[0].attr_list[0] == test_service.subattr + assert test_service.attr[0].attr_list[0]._full_access_path == { + "ServiceClass.attr[0].attr_list[0]", + "ServiceClass.subattr", + } diff --git a/tests/test_properties.py b/tests/test_properties.py new file mode 100644 index 0000000..da593fe --- /dev/null +++ b/tests/test_properties.py @@ -0,0 +1,34 @@ +from pytest import CaptureFixture + +from pyDataInterface import DataService + + +def test_properties(capsys: CaptureFixture) -> None: + class ServiceClass(DataService): + _power = True + + @property + def power(self) -> bool: + return self._power + + @power.setter + def power(self, value: bool) -> None: + self._power = value + + @property + def power_two(self) -> bool: + return self._power + + test_service = ServiceClass() + test_service.power = False + + captured = capsys.readouterr() + expected_output = sorted( + [ + "ServiceClass.power = False", + "ServiceClass.power_two = False", + "ServiceClass._power = False", + ] + ) + actual_output = sorted(captured.out.strip().split("\n")) + assert actual_output == expected_output