diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..60c564f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 100 \ No newline at end of file diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 2493c2f..61bc473 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -4,64 +4,78 @@ on: [push] jobs: lint: - runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest"] + python: ["3.9"] + + runs-on: ${{ matrix.os }} steps: - - name: Run black, flake8, mypy - uses: dls-controls/pipenv-run-action@v1 + - name: Checkout Source + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup python + uses: actions/setup-python@v4 with: - pipenv-run: lint + python-version: ${{ matrix.python }} + + - name: Install dependancies + run: | + python -m pip install --upgrade pip + pip install .[dev] -r requirements.txt + - name: Run black, flake8, mypy + run: | + pre-commit run --all-files --show-diff-on-failure --color=always -v - wheel: + sdist_wheel: strategy: fail-fast: false matrix: os: ["ubuntu-latest"] - python: ["3.8"] + python: ["3.9"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - name: Checkout Source + uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Create Sdist and Wheel + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Build Sdist # Set SOURCE_DATE_EPOCH from git commit for reproducible build # https://reproducible-builds.org/ # Set group writable and umask to do the same to match inside DLS run: | chmod -R g+w . umask 0002 - SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) pipx run build --sdist --wheel - - - name: Upload Wheel and Sdist as artifacts - uses: actions/upload-artifact@v2 + SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) pipx run --python $(which python${{ matrix.python }}) build --sdist --wheel + - name: Upload Sdist as artifact + uses: actions/upload-artifact@v3 with: name: dist path: dist/* - - name: Install minimum python version - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - - name: Install wheel in a venv and check cli works - # ${GITHUB_REPOSITORY##*/} is the repo name without org - # Replace this with the cli command if different to the repo name - run: pipx run --python $(which python${{ matrix.python }}) --spec dist/*.whl uvicorn --version - test: strategy: fail-fast: false matrix: os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.8", "3.9"] # maybe add 3.10 in future - pipenv: ["skip-lock"] + python: ["3.8", "3.9", "3.10"] + deploy: [false] include: # Add an extra Python3.8 runner to use the lockfile - os: "ubuntu-latest" python: "3.8" - pipenv: "deploy" + deploy: true runs-on: ${{ matrix.os }} env: @@ -69,22 +83,33 @@ jobs: PY_IGNORE_IMPORTMISMATCH: "1" steps: - - name: Setup repo and test - uses: dls-controls/pipenv-run-action@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup python + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - pipenv-install: --dev --${{ matrix.pipenv }} - allow-editable-installs: ${{ matrix.pipenv == 'deploy' }} - pipenv-run: tests + + - name: Install dependencies + if: ${{ !matrix.deploy }} + run: | + python -m pip install --upgrade pip + pip install .[dev] - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v2 - # with: - # name: ${{ matrix.python }}/${{ matrix.os }}/${{ matrix.pipenv }} - # files: cov.xml + - name: Install dependencies (deploy) + if: ${{ matrix.deploy }} + run: | + python -m pip install --upgrade pip + pip install .[dev] -r requirements.txt + + - name: Run tests with pytest + run: | + pytest release: - needs: [lint, wheel, test] + needs: [lint, sdist_wheel, test] runs-on: ubuntu-latest # upload to PyPI and make a release on every tag if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') @@ -103,3 +128,9 @@ jobs: generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: pipx run twine upload dist/* diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index c1b48b4..6ffecdc 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -10,14 +10,25 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout Source + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Setup repo and test - uses: dls-controls/pipenv-run-action@v1 + - name: Setup python + uses: actions/setup-python@v4 with: python-version: 3.8 - pipenv-install: --dev - pipenv-run: python src/diffcalc_API/openapi.py + + - name: Install dependencies + if: ${{ !matrix.deploy }} + run: | + python -m pip install --upgrade pip + pip install .[dev] -r requirements.txt + + - name: Make openapi.json + run: | + python src/diffcalc_API/openapi.py - name: Upload uses: actions/upload-artifact@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f98aa9..efc0a17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,19 +13,26 @@ repos: name: Run black stages: [commit] language: system - entry: pipenv run black --check --diff + entry: black --check --diff types: [python] - id: flake8 name: Run flake8 stages: [commit] language: system - entry: pipenv run flake8 + entry: flake8 types: [python] + - id: pydocstyle + name: Run pydocstyle + stages: [commit] + language: system + entry: pydocstyle + files: ^(src\/).*\.py + - id: mypy name: Run mypy stages: [commit] language: system - entry: pipenv run mypy src tests - pass_filenames: false + entry: mypy + files: ^(src\/|tests\/).*\.py \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ff78a11..dda6870 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "type": "shell", "label": "Tests with coverage", - "command": "pipenv run tests", + "command": "pytest", "options": { "cwd": "${workspaceRoot}" }, diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c6eb2bd..721132e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -16,8 +16,8 @@ To get the source source code and run the unit tests, run:: $ git clone git://github.com/dls-controls/diffcalc_API.git $ cd diffcalc_API - $ pipenv install --dev - $ pipenv run tests + $ pip install -e .[dev] + $ pytest While 100% code coverage does not make a library bug-free, it significantly reduces the number of easily caught bugs! Please make sure coverage remains the @@ -36,12 +36,12 @@ The code in this repository conforms to standards set by the following tools: These checks will be run by pre-commit_. You can either choose to run these tests on all files tracked by git:: - $ pipenv run lint + $ pre-commit run --all-files --show-diff-on-failure --color=always -v Or you can install a pre-commit hook that will run each time you do a ``git commit`` on just the files that have changed:: - $ pipenv run pre-commit install + $ pre-commit install .. _black: https://github.com/psf/black .. _flake8: https://flake8.pycqa.org/en/latest/ @@ -88,7 +88,7 @@ Docs follow the underlining convention:: You can build the docs from the project directory by running:: - $ pipenv run docs + $ sphinx-build -EWT --keep-going docs build/html $ firefox build/html/index.html Release Process diff --git a/Dockerfile b/Dockerfile index 03e3b00..ac99dc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,7 @@ WORKDIR ${ENV_DIR} COPY . ${ENV_DIR} #Install dependencies -RUN pip install pipenv; \ - pipenv install --system --python 3.8; \ - pip uninstall diffcalc-core -y; \ - pip install git+https://github.com/DiamondLightSource/diffcalc-core.git +RUN pip install . -c requirements.txt #Run the API diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 00db4e5..0000000 --- a/Pipfile +++ /dev/null @@ -1,17 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -diffcalc_API = {editable = true, extras = ["dev"], path = "."} - -[packages] -diffcalc_API = {editable = true, path = "."} - -[scripts] -lint = "pre-commit run --all-files --show-diff-on-failure --color=always -v" -tests = "pytest" -docs = "sphinx-build -EWT --keep-going docs build/html" -# Delete any files that git ignore hides from us -gitclean = "git clean -fdX" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index f1b2bc9..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1211 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "1a08e8674f6c87e6f3ecfe03bc9844ee8a4714ce907e3212d277d71cea8c5fc8" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "anyio": { - "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "diffcalc-api": { - "editable": true, - "path": "." - }, - "diffcalc-core": { - "hashes": [ - "sha256:39c7cc2699b756ba98cacab8a41a0a8c49d7b4cfffe45b5e3391a799ed91f3f9", - "sha256:8a0838a02d718b468a3e286a1a76a51c7cfb7f1d48b2739b5aa00867bdfb9fb6" - ], - "markers": "python_version >= '3.7'", - "version": "==0.4.0" - }, - "fastapi": { - "hashes": [ - "sha256:4e542f71b5b0f4f54a82449e8bd0244305de319186a31addab976bf672206420", - "sha256:5340f6016860baf94a52be5040c3a2e94c39ddc7ab4cc7548558182db742d9d6" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==0.80.0" - }, - "h11": { - "hashes": [ - "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", - "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" - ], - "markers": "python_version >= '3.6'", - "version": "==0.13.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "motor": { - "hashes": [ - "sha256:3e36d29406c151b61342e6a8fa5e90c00c4723b76e30f11276a4373ea2064b7d", - "sha256:b076de44970f518177f0eeeda8b183f52eafa557775bfe3294e93bda18867a71" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.0" - }, - "numpy": { - "hashes": [ - "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", - "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", - "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", - "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", - "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", - "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", - "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", - "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", - "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", - "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", - "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", - "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", - "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", - "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", - "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", - "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", - "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", - "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", - "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", - "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", - "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", - "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", - "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", - "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", - "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", - "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", - "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", - "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" - ], - "markers": "python_version >= '3.8'", - "version": "==1.23.2" - }, - "pydantic": { - "hashes": [ - "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44", - "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d", - "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84", - "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555", - "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7", - "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131", - "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8", - "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3", - "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56", - "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0", - "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4", - "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453", - "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044", - "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e", - "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15", - "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb", - "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001", - "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d", - "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3", - "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e", - "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f", - "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c", - "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b", - "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8", - "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567", - "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979", - "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326", - "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb", - "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f", - "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa", - "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747", - "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801", - "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55", - "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08", - "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==1.9.2" - }, - "pymongo": { - "hashes": [ - "sha256:01721da74558f2f64a9f162ee063df403ed656b7d84229268d8e4ae99cfba59c", - "sha256:07564178ecc203a84f63e72972691af6c0c82d2dc0c9da66ba711695276089ba", - "sha256:0f53253f4777cbccc426e669a2af875f26c95bd090d88593287b9a0a8ac7fa25", - "sha256:10f09c4f09757c2e2a707ad7304f5d69cb8fdf7cbfb644dbacfe5bbe8afe311b", - "sha256:124d0e880b66f9b0778613198e89984984fdd37a3030a9007e5f459a42dfa2d3", - "sha256:147a23cd96feb67606ac957744d8d25b013426cdc3c7164a4f99bd8253f649e3", - "sha256:153b8f8705970756226dfeeb7bb9637e0ad54a4d79b480b4c8244e34e16e1662", - "sha256:193cc97d44b1e6d2253ea94e30c6f94f994efb7166e2452af4df55825266e88b", - "sha256:1a957cdc2b26eeed4d8f1889a40c6023dd1bd94672dd0f5ce327314f2caaefd4", - "sha256:1c81414b706627f15e921e29ae2403aab52e33e36ed92ed989c602888d7c3b90", - "sha256:21238b19243a42f9a34a6d39e7580ceebc6da6d2f3cf729c1cff9023cb61a5f1", - "sha256:2bfe6b59f431f40fa545547616f4acf0c0c4b64518b1f951083e3bad06eb368b", - "sha256:314b556afd72eb21a6a10bd1f45ef252509f014f80207db59c97372103c88237", - "sha256:31c50da4a080166bc29403aa91f4c76e0889b4f24928d1b60508a37c1bf87f9a", - "sha256:3be53e9888e759c49ae35d747ff77a04ff82b894dd64601e0f3a5a159b406245", - "sha256:44b36ccb90aac5ea50be23c1a6e8f24fbfc78afabdef114af16c6e0a80981364", - "sha256:4cadaaa5c19ad23fc84559e90284f2eb003c36958ebb2c06f286b678f441285f", - "sha256:60c470a58c5b62b1b12a5f5458f8e2f2f67b94e198d03dc5352f854d9230c394", - "sha256:6673ab3fbf3135cc1a8c0f70d480db5b2378c3a70af8d602f73f76b8338bdf97", - "sha256:68e1e49a5675748233f7b05330f092582cd52f2850b4244939fd75ba640593ed", - "sha256:69d0180bca594e81cdb4a2af328bdb4046f59e10aaeef7619496fe64f2ec918c", - "sha256:6bd5888997ea3eae9830c6cc7964b61dcfbc50eb3a5a6ce56ad5f86d5579b11c", - "sha256:701d331060dae72bf3ebdb82924405d14136a69282ccb00c89fc69dee21340b4", - "sha256:70216ec4c248213ae95ea499b6314c385ce01a5946c448fb22f6c8395806e740", - "sha256:72f338f6aabd37d343bd9d1fdd3de921104d395766bcc5cdc4039e4c2dd97766", - "sha256:764fc15418d94bce5c2f8ebdbf66544f96f42efb1364b61e715e5b33281b388d", - "sha256:766acb5b1a19eae0f7467bcd3398748f110ea5309cdfc59faa5185dcc7fd4dca", - "sha256:76892bbce743eb9f90360b3626ea92f13d338010a1004b4488e79e555b339921", - "sha256:773467d25c293f8e981b092361dab5fd800e1ba318403b7959d35004c67faedc", - "sha256:80cbf0b043061451660099fff9001a7faacb2c9c983842b4819526e2f944dc6c", - "sha256:83168126ae2457d1a19b2af665cafa7ef78c2dcff192d7d7b5dad6b36c73ae24", - "sha256:83cc3c35aeeceb67143914db67f685206e1aa37ea837d872f4bc28d7f80917c9", - "sha256:8a86e8c2ac2ec87141e1c6cb00bdb18a4560f06e5f96769abcd1dda24dc0e764", - "sha256:8a9bc4dcfc2bda69ee88cdb7a89b03f2b8eca668519b704384a264dea2db4209", - "sha256:8c223aea52c359cc8fdee5bd3475532590755c269ec4d4fe581acd47a44e9952", - "sha256:8cbb868e88c4eee1c53364bb343d226a3c0e959e791e6828030cb78f46cfcbe3", - "sha256:902e2c9030cb042c49750bc70d72d830d42c64ea0df5ff8630c171e065c93dd7", - "sha256:a25c0eb2d610b20e276e684be61c337396813b636b69373c17314283cb1a3b14", - "sha256:a3efdf154844244e0dabe902cf1827fdced55fa5b144adec2a86e5ce50a99b97", - "sha256:a6bf01b9237f794fa3bdad5089474067d28be7e199b356a18d3f247a45775f26", - "sha256:a7eb5b06744b911b6668b427c8abc71b6d624e72d3dfffed00988fa1b4340f97", - "sha256:b0be613d926c5dbb0d3fc6b58e4f2be4979f80ae76fda6e47309f011b388fe0c", - "sha256:b211e161b6cc2790e0d640ad38e0429d06c944e5da23410f4dc61809dba25095", - "sha256:b537dd282de1b53d9ae7cf9f3df36420c8618390f2da92100391f3ba8f3c141a", - "sha256:c549bb519456ee230e92f415c5b4d962094caac0fdbcc4ed22b576f66169764e", - "sha256:c69ef5906dcd6ec565d4d887ba97ceb2a84f3b614307ee3b4780cb1ea40b1867", - "sha256:c8b4a782aac43948308087b962c9ecb030ba98886ce6dee3ad7aafe8c5e1ce80", - "sha256:cc7ebc37b03956a070260665079665eae69e5e96007694214f3a2107af96816a", - "sha256:ccfdc7722df445c49dc6b5d514c3544cad99b53189165f7546793933050ac7fb", - "sha256:d8bb745321716e7a11220a67c88212ecedde4021e1de4802e563baef9df921d2", - "sha256:d94f535df9f539615bc3dbbef185ded3b609373bb44ca1afffcabac70202678a", - "sha256:d98d2a8283c9928a9e5adf2f3c0181e095579e9732e1613aaa55d386e2bcb6c5", - "sha256:dc24737d24ce0de762bee9c2a884639819485f679bbac8ab5be9c161ef6f9b2c", - "sha256:e08fe1731f5429435b8dea1db9663f9ed1812915ff803fc9991c7c4841ed62ad", - "sha256:e09cdf5aad507c8faa30d97884cc42932ed3a9c2b7f22cc3ccc607bae03981b3", - "sha256:e152c26ffc30331e9d57591fc4c05453c209aa20ba299d1deb7173f7d1958c22", - "sha256:e1b8f5e2f9637492b0da4d51f78ecb17786e61d6c461ead8542c944750faf4f9", - "sha256:e39cacee70a98758f9b2da53ee175378f07c60113b1fa4fae40cbaee5583181e", - "sha256:e64442aba81ed4df1ca494b87bf818569a1280acaa73071c68014f7a884e83f1", - "sha256:e7dcb73f683c155885a3488646fcead3a895765fed16e93c9b80000bc69e96cb", - "sha256:ecdcb0d4e9b08b739035f57a09330efc6f464bd7f942b63897395d996ca6ebd5", - "sha256:ed90a9de4431cbfb2f3b2ef0c5fd356e61c85117b2be4db3eae28cb409f6e2d5", - "sha256:f1c23527f8e13f526fededbb96f2e7888f179fe27c51d41c2724f7059b75b2fa", - "sha256:f47d5f10922cf7f7dfcd1406bd0926cef6d866a75953c3745502dffd7ac197dd", - "sha256:fe0820d169635e41c14a5d21514282e0b93347878666ec9d5d3bf0eed0649948", - "sha256:ff66014687598823b6b23751884b4aa67eb934445406d95894dfc60cb7bfcc18" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" - }, - "scipy": { - "hashes": [ - "sha256:01c2015e132774feefe059d5354055fec6b751d7a7d70ad2cf5ce314e7426e2a", - "sha256:0424d1bbbfa51d5ddaa16d067fd593863c9f2fb7c6840c32f8a08a8832f8e7a4", - "sha256:10417935486b320d98536d732a58362e3d37e84add98c251e070c59a6bfe0863", - "sha256:12005d30894e4fe7b247f7233ba0801a341f887b62e2eb99034dd6f2a8a33ad6", - "sha256:16207622570af10f9e6a2cdc7da7a9660678852477adbcd056b6d1057a036fef", - "sha256:45f0d6c0d6e55582d3b8f5c58ad4ca4259a02affb190f89f06c8cc02e21bba81", - "sha256:5d1b9cf3771fd921f7213b4b886ab2606010343bb36259b544a816044576d69e", - "sha256:693b3fe2e7736ce0dbc72b4d933798eb6ca8ce51b8b934e3f547cc06f48b2afb", - "sha256:73b704c5eea9be811919cae4caacf3180dd9212d9aed08477c1d2ba14900a9de", - "sha256:79dd7876614fc2869bf5d311ef33962d2066ea888bc66c80fd4fa80f8772e5a9", - "sha256:7bad16b91918bf3288089a78a4157e04892ea6475fb7a1d9bcdf32c30c8a3dba", - "sha256:8d541db2d441ef87afb60c4a2addb00c3af281633602a4967e733ef4b7050504", - "sha256:8f2232c9d9119ec356240255a715a289b3a33be828c3e4abac11fd052ce15b1e", - "sha256:97a1f1e51ea30782d7baa8d0c52f72c3f9f05cb609cf1b990664231c5102bccd", - "sha256:adb6c438c6ef550e2bb83968e772b9690cb421f2c6073f9c2cb6af15ee538bc9", - "sha256:bb687d245b6963673c639f318eea7e875d1ba147a67925586abed3d6f39bb7d8", - "sha256:bd490f77f35800d5620f4d9af669e372d9a88db1f76ef219e1609cc4ecdd1a24", - "sha256:c0dfd7d2429452e7e94904c6a3af63cbaa3cf51b348bd9d35b42db7e9ad42791", - "sha256:d3a326673ac5afa9ef5613a61626b9ec15c8f7222b4ecd1ce0fd8fcba7b83c59", - "sha256:e2004d2a3c397b26ca78e67c9d320153a1a9b71ae713ad33f4a3a3ab3d79cc65", - "sha256:e2ac088ea4aa61115b96b47f5f3d94b3fa29554340b6629cd2bfe6b0521ee33b", - "sha256:f7c3c578ff556333f3890c2df6c056955d53537bb176698359088108af73a58f", - "sha256:fc58c3fcb8a724b703ffbc126afdca5a8353d4d5945d5c92db85617e165299e7" - ], - "markers": "python_version < '3.12' and python_version >= '3.8'", - "version": "==1.9.0" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "starlette": { - "hashes": [ - "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf", - "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7" - ], - "markers": "python_version >= '3.6'", - "version": "==0.19.1" - }, - "typing-extensions": { - "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" - ], - "markers": "python_version < '3.10'", - "version": "==4.3.0" - }, - "uvicorn": { - "hashes": [ - "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af", - "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b" - ], - "markers": "python_version >= '3.7'", - "version": "==0.18.3" - } - }, - "develop": { - "alabaster": { - "hashes": [ - "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", - "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" - ], - "version": "==0.7.12" - }, - "anyio": { - "hashes": [ - "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b", - "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==3.6.1" - }, - "attrs": { - "hashes": [ - "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6", - "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c" - ], - "markers": "python_version >= '3.5'", - "version": "==22.1.0" - }, - "babel": { - "hashes": [ - "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51", - "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb" - ], - "markers": "python_version >= '3.6'", - "version": "==2.10.3" - }, - "black": { - "hashes": [ - "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", - "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", - "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", - "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", - "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", - "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", - "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", - "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", - "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", - "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", - "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", - "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", - "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", - "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", - "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", - "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", - "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", - "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", - "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", - "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", - "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", - "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", - "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==22.3.0" - }, - "certifi": { - "hashes": [ - "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", - "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.6.15" - }, - "cfgv": { - "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2", - "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820", - "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827", - "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3", - "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d", - "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145", - "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875", - "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2", - "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74", - "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f", - "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c", - "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973", - "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1", - "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782", - "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0", - "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760", - "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a", - "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3", - "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7", - "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a", - "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f", - "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e", - "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86", - "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa", - "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa", - "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796", - "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a", - "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928", - "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0", - "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac", - "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c", - "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685", - "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d", - "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e", - "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f", - "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558", - "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58", - "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781", - "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a", - "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa", - "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc", - "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892", - "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d", - "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817", - "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1", - "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c", - "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908", - "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19", - "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60", - "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b" - ], - "markers": "python_version >= '3.7'", - "version": "==6.4.4" - }, - "diffcalc-api": { - "editable": true, - "path": "." - }, - "diffcalc-core": { - "hashes": [ - "sha256:39c7cc2699b756ba98cacab8a41a0a8c49d7b4cfffe45b5e3391a799ed91f3f9", - "sha256:8a0838a02d718b468a3e286a1a76a51c7cfb7f1d48b2739b5aa00867bdfb9fb6" - ], - "markers": "python_version >= '3.7'", - "version": "==0.4.0" - }, - "distlib": { - "hashes": [ - "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe", - "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c" - ], - "version": "==0.3.5" - }, - "docutils": { - "hashes": [ - "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", - "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.17.1" - }, - "fastapi": { - "hashes": [ - "sha256:4e542f71b5b0f4f54a82449e8bd0244305de319186a31addab976bf672206420", - "sha256:5340f6016860baf94a52be5040c3a2e94c39ddc7ab4cc7548558182db742d9d6" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==0.80.0" - }, - "filelock": { - "hashes": [ - "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc", - "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.0" - }, - "flake8": { - "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.9.2" - }, - "flake8-isort": { - "hashes": [ - "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0", - "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf" - ], - "version": "==4.2.0" - }, - "h11": { - "hashes": [ - "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", - "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" - ], - "markers": "python_version >= '3.6'", - "version": "==0.13.0" - }, - "identify": { - "hashes": [ - "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893", - "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.3" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "imagesize": { - "hashes": [ - "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", - "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.1" - }, - "importlib-metadata": { - "hashes": [ - "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", - "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" - ], - "markers": "python_version < '3.10'", - "version": "==4.12.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "isort": { - "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" - ], - "markers": "python_full_version >= '3.6.1' and python_version < '4'", - "version": "==5.10.1" - }, - "jinja2": { - "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.2" - }, - "markupsafe": { - "hashes": [ - "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", - "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", - "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", - "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", - "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", - "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", - "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", - "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", - "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", - "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", - "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", - "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", - "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", - "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", - "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", - "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", - "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", - "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", - "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", - "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", - "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", - "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", - "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", - "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", - "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", - "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", - "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", - "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", - "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", - "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", - "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", - "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", - "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", - "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", - "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", - "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", - "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", - "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", - "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", - "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.1" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "mock": { - "hashes": [ - "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", - "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.3" - }, - "motor": { - "hashes": [ - "sha256:3e36d29406c151b61342e6a8fa5e90c00c4723b76e30f11276a4373ea2064b7d", - "sha256:b076de44970f518177f0eeeda8b183f52eafa557775bfe3294e93bda18867a71" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.0" - }, - "mypy": { - "hashes": [ - "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655", - "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9", - "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3", - "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6", - "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0", - "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58", - "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103", - "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09", - "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417", - "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56", - "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2", - "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856", - "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0", - "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8", - "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27", - "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5", - "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71", - "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27", - "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe", - "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca", - "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf", - "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9", - "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c" - ], - "markers": "python_version >= '3.6'", - "version": "==0.971" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "nodeenv": { - "hashes": [ - "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", - "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.7.0" - }, - "numpy": { - "hashes": [ - "sha256:17e5226674f6ea79e14e3b91bfbc153fdf3ac13f5cc54ee7bc8fdbe820a32da0", - "sha256:2bd879d3ca4b6f39b7770829f73278b7c5e248c91d538aab1e506c628353e47f", - "sha256:4f41f5bf20d9a521f8cab3a34557cd77b6f205ab2116651f12959714494268b0", - "sha256:5593f67e66dea4e237f5af998d31a43e447786b2154ba1ad833676c788f37cde", - "sha256:5e28cd64624dc2354a349152599e55308eb6ca95a13ce6a7d5679ebff2962913", - "sha256:633679a472934b1c20a12ed0c9a6c9eb167fbb4cb89031939bfd03dd9dbc62b8", - "sha256:806970e69106556d1dd200e26647e9bee5e2b3f1814f9da104a943e8d548ca38", - "sha256:806cc25d5c43e240db709875e947076b2826f47c2c340a5a2f36da5bb10c58d6", - "sha256:8247f01c4721479e482cc2f9f7d973f3f47810cbc8c65e38fd1bbd3141cc9842", - "sha256:8ebf7e194b89bc66b78475bd3624d92980fca4e5bb86dda08d677d786fefc414", - "sha256:8ecb818231afe5f0f568c81f12ce50f2b828ff2b27487520d85eb44c71313b9e", - "sha256:8f9d84a24889ebb4c641a9b99e54adb8cab50972f0166a3abc14c3b93163f074", - "sha256:909c56c4d4341ec8315291a105169d8aae732cfb4c250fbc375a1efb7a844f8f", - "sha256:9b83d48e464f393d46e8dd8171687394d39bc5abfe2978896b77dc2604e8635d", - "sha256:ac987b35df8c2a2eab495ee206658117e9ce867acf3ccb376a19e83070e69418", - "sha256:b78d00e48261fbbd04aa0d7427cf78d18401ee0abd89c7559bbf422e5b1c7d01", - "sha256:b8b97a8a87cadcd3f94659b4ef6ec056261fa1e1c3317f4193ac231d4df70215", - "sha256:bd5b7ccae24e3d8501ee5563e82febc1771e73bd268eef82a1e8d2b4d556ae66", - "sha256:bdc02c0235b261925102b1bd586579b7158e9d0d07ecb61148a1799214a4afd5", - "sha256:be6b350dfbc7f708d9d853663772a9310783ea58f6035eec649fb9c4371b5389", - "sha256:c403c81bb8ffb1c993d0165a11493fd4bf1353d258f6997b3ee288b0a48fce77", - "sha256:cf8c6aed12a935abf2e290860af8e77b26a042eb7f2582ff83dc7ed5f963340c", - "sha256:d98addfd3c8728ee8b2c49126f3c44c703e2b005d4a95998e2167af176a9e722", - "sha256:dc76bca1ca98f4b122114435f83f1fcf3c0fe48e4e6f660e07996abf2f53903c", - "sha256:dec198619b7dbd6db58603cd256e092bcadef22a796f778bf87f8592b468441d", - "sha256:df28dda02c9328e122661f399f7655cdcbcf22ea42daa3650a26bce08a187450", - "sha256:e603ca1fb47b913942f3e660a15e55a9ebca906857edfea476ae5f0fe9b457d5", - "sha256:ecfdd68d334a6b97472ed032b5b37a30d8217c097acfff15e8452c710e775524" - ], - "markers": "python_version >= '3.8'", - "version": "==1.23.2" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep8-naming": { - "hashes": [ - "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23", - "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48" - ], - "markers": "python_version >= '3.7'", - "version": "==0.13.2" - }, - "pipenv": { - "hashes": [ - "sha256:edbff81766d4328115b28738df0e5240cb2299a34ff0c60d524ad2855df18c69", - "sha256:f05d87a4fd76fc02f8cf3b0b179f00889e82d6d7204a4ea9ebcf534965956ecc" - ], - "markers": "python_version >= '3.7'", - "version": "==2022.8.24" - }, - "platformdirs": { - "hashes": [ - "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788", - "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.2" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "pre-commit": { - "hashes": [ - "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7", - "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959" - ], - "markers": "python_version >= '3.7'", - "version": "==2.20.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" - }, - "pydantic": { - "hashes": [ - "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44", - "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d", - "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84", - "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555", - "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7", - "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131", - "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8", - "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3", - "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56", - "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0", - "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4", - "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453", - "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044", - "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e", - "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15", - "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb", - "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001", - "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d", - "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3", - "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e", - "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f", - "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c", - "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b", - "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8", - "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567", - "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979", - "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326", - "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb", - "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f", - "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa", - "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747", - "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801", - "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55", - "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08", - "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==1.9.2" - }, - "pyflakes": { - "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" - }, - "pygments": { - "hashes": [ - "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", - "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" - ], - "markers": "python_version >= '3.6'", - "version": "==2.13.0" - }, - "pymongo": { - "hashes": [ - "sha256:01721da74558f2f64a9f162ee063df403ed656b7d84229268d8e4ae99cfba59c", - "sha256:07564178ecc203a84f63e72972691af6c0c82d2dc0c9da66ba711695276089ba", - "sha256:0f53253f4777cbccc426e669a2af875f26c95bd090d88593287b9a0a8ac7fa25", - "sha256:10f09c4f09757c2e2a707ad7304f5d69cb8fdf7cbfb644dbacfe5bbe8afe311b", - "sha256:124d0e880b66f9b0778613198e89984984fdd37a3030a9007e5f459a42dfa2d3", - "sha256:147a23cd96feb67606ac957744d8d25b013426cdc3c7164a4f99bd8253f649e3", - "sha256:153b8f8705970756226dfeeb7bb9637e0ad54a4d79b480b4c8244e34e16e1662", - "sha256:193cc97d44b1e6d2253ea94e30c6f94f994efb7166e2452af4df55825266e88b", - "sha256:1a957cdc2b26eeed4d8f1889a40c6023dd1bd94672dd0f5ce327314f2caaefd4", - "sha256:1c81414b706627f15e921e29ae2403aab52e33e36ed92ed989c602888d7c3b90", - "sha256:21238b19243a42f9a34a6d39e7580ceebc6da6d2f3cf729c1cff9023cb61a5f1", - "sha256:2bfe6b59f431f40fa545547616f4acf0c0c4b64518b1f951083e3bad06eb368b", - "sha256:314b556afd72eb21a6a10bd1f45ef252509f014f80207db59c97372103c88237", - "sha256:31c50da4a080166bc29403aa91f4c76e0889b4f24928d1b60508a37c1bf87f9a", - "sha256:3be53e9888e759c49ae35d747ff77a04ff82b894dd64601e0f3a5a159b406245", - "sha256:44b36ccb90aac5ea50be23c1a6e8f24fbfc78afabdef114af16c6e0a80981364", - "sha256:4cadaaa5c19ad23fc84559e90284f2eb003c36958ebb2c06f286b678f441285f", - "sha256:60c470a58c5b62b1b12a5f5458f8e2f2f67b94e198d03dc5352f854d9230c394", - "sha256:6673ab3fbf3135cc1a8c0f70d480db5b2378c3a70af8d602f73f76b8338bdf97", - "sha256:68e1e49a5675748233f7b05330f092582cd52f2850b4244939fd75ba640593ed", - "sha256:69d0180bca594e81cdb4a2af328bdb4046f59e10aaeef7619496fe64f2ec918c", - "sha256:6bd5888997ea3eae9830c6cc7964b61dcfbc50eb3a5a6ce56ad5f86d5579b11c", - "sha256:701d331060dae72bf3ebdb82924405d14136a69282ccb00c89fc69dee21340b4", - "sha256:70216ec4c248213ae95ea499b6314c385ce01a5946c448fb22f6c8395806e740", - "sha256:72f338f6aabd37d343bd9d1fdd3de921104d395766bcc5cdc4039e4c2dd97766", - "sha256:764fc15418d94bce5c2f8ebdbf66544f96f42efb1364b61e715e5b33281b388d", - "sha256:766acb5b1a19eae0f7467bcd3398748f110ea5309cdfc59faa5185dcc7fd4dca", - "sha256:76892bbce743eb9f90360b3626ea92f13d338010a1004b4488e79e555b339921", - "sha256:773467d25c293f8e981b092361dab5fd800e1ba318403b7959d35004c67faedc", - "sha256:80cbf0b043061451660099fff9001a7faacb2c9c983842b4819526e2f944dc6c", - "sha256:83168126ae2457d1a19b2af665cafa7ef78c2dcff192d7d7b5dad6b36c73ae24", - "sha256:83cc3c35aeeceb67143914db67f685206e1aa37ea837d872f4bc28d7f80917c9", - "sha256:8a86e8c2ac2ec87141e1c6cb00bdb18a4560f06e5f96769abcd1dda24dc0e764", - "sha256:8a9bc4dcfc2bda69ee88cdb7a89b03f2b8eca668519b704384a264dea2db4209", - "sha256:8c223aea52c359cc8fdee5bd3475532590755c269ec4d4fe581acd47a44e9952", - "sha256:8cbb868e88c4eee1c53364bb343d226a3c0e959e791e6828030cb78f46cfcbe3", - "sha256:902e2c9030cb042c49750bc70d72d830d42c64ea0df5ff8630c171e065c93dd7", - "sha256:a25c0eb2d610b20e276e684be61c337396813b636b69373c17314283cb1a3b14", - "sha256:a3efdf154844244e0dabe902cf1827fdced55fa5b144adec2a86e5ce50a99b97", - "sha256:a6bf01b9237f794fa3bdad5089474067d28be7e199b356a18d3f247a45775f26", - "sha256:a7eb5b06744b911b6668b427c8abc71b6d624e72d3dfffed00988fa1b4340f97", - "sha256:b0be613d926c5dbb0d3fc6b58e4f2be4979f80ae76fda6e47309f011b388fe0c", - "sha256:b211e161b6cc2790e0d640ad38e0429d06c944e5da23410f4dc61809dba25095", - "sha256:b537dd282de1b53d9ae7cf9f3df36420c8618390f2da92100391f3ba8f3c141a", - "sha256:c549bb519456ee230e92f415c5b4d962094caac0fdbcc4ed22b576f66169764e", - "sha256:c69ef5906dcd6ec565d4d887ba97ceb2a84f3b614307ee3b4780cb1ea40b1867", - "sha256:c8b4a782aac43948308087b962c9ecb030ba98886ce6dee3ad7aafe8c5e1ce80", - "sha256:cc7ebc37b03956a070260665079665eae69e5e96007694214f3a2107af96816a", - "sha256:ccfdc7722df445c49dc6b5d514c3544cad99b53189165f7546793933050ac7fb", - "sha256:d8bb745321716e7a11220a67c88212ecedde4021e1de4802e563baef9df921d2", - "sha256:d94f535df9f539615bc3dbbef185ded3b609373bb44ca1afffcabac70202678a", - "sha256:d98d2a8283c9928a9e5adf2f3c0181e095579e9732e1613aaa55d386e2bcb6c5", - "sha256:dc24737d24ce0de762bee9c2a884639819485f679bbac8ab5be9c161ef6f9b2c", - "sha256:e08fe1731f5429435b8dea1db9663f9ed1812915ff803fc9991c7c4841ed62ad", - "sha256:e09cdf5aad507c8faa30d97884cc42932ed3a9c2b7f22cc3ccc607bae03981b3", - "sha256:e152c26ffc30331e9d57591fc4c05453c209aa20ba299d1deb7173f7d1958c22", - "sha256:e1b8f5e2f9637492b0da4d51f78ecb17786e61d6c461ead8542c944750faf4f9", - "sha256:e39cacee70a98758f9b2da53ee175378f07c60113b1fa4fae40cbaee5583181e", - "sha256:e64442aba81ed4df1ca494b87bf818569a1280acaa73071c68014f7a884e83f1", - "sha256:e7dcb73f683c155885a3488646fcead3a895765fed16e93c9b80000bc69e96cb", - "sha256:ecdcb0d4e9b08b739035f57a09330efc6f464bd7f942b63897395d996ca6ebd5", - "sha256:ed90a9de4431cbfb2f3b2ef0c5fd356e61c85117b2be4db3eae28cb409f6e2d5", - "sha256:f1c23527f8e13f526fededbb96f2e7888f179fe27c51d41c2724f7059b75b2fa", - "sha256:f47d5f10922cf7f7dfcd1406bd0926cef6d866a75953c3745502dffd7ac197dd", - "sha256:fe0820d169635e41c14a5d21514282e0b93347878666ec9d5d3bf0eed0649948", - "sha256:ff66014687598823b6b23751884b4aa67eb934445406d95894dfc60cb7bfcc18" - ], - "markers": "python_version >= '3.7'", - "version": "==4.2.0" - }, - "pyparsing": { - "hashes": [ - "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", - "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" - ], - "markers": "python_full_version >= '3.6.8'", - "version": "==3.0.9" - }, - "pytest": { - "hashes": [ - "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", - "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" - ], - "markers": "python_version >= '3.7'", - "version": "==7.1.2" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.0" - }, - "pytz": { - "hashes": [ - "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", - "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" - ], - "version": "==2022.2.1" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" - }, - "scipy": { - "hashes": [ - "sha256:01c2015e132774feefe059d5354055fec6b751d7a7d70ad2cf5ce314e7426e2a", - "sha256:0424d1bbbfa51d5ddaa16d067fd593863c9f2fb7c6840c32f8a08a8832f8e7a4", - "sha256:10417935486b320d98536d732a58362e3d37e84add98c251e070c59a6bfe0863", - "sha256:12005d30894e4fe7b247f7233ba0801a341f887b62e2eb99034dd6f2a8a33ad6", - "sha256:16207622570af10f9e6a2cdc7da7a9660678852477adbcd056b6d1057a036fef", - "sha256:45f0d6c0d6e55582d3b8f5c58ad4ca4259a02affb190f89f06c8cc02e21bba81", - "sha256:5d1b9cf3771fd921f7213b4b886ab2606010343bb36259b544a816044576d69e", - "sha256:693b3fe2e7736ce0dbc72b4d933798eb6ca8ce51b8b934e3f547cc06f48b2afb", - "sha256:73b704c5eea9be811919cae4caacf3180dd9212d9aed08477c1d2ba14900a9de", - "sha256:79dd7876614fc2869bf5d311ef33962d2066ea888bc66c80fd4fa80f8772e5a9", - "sha256:7bad16b91918bf3288089a78a4157e04892ea6475fb7a1d9bcdf32c30c8a3dba", - "sha256:8d541db2d441ef87afb60c4a2addb00c3af281633602a4967e733ef4b7050504", - "sha256:8f2232c9d9119ec356240255a715a289b3a33be828c3e4abac11fd052ce15b1e", - "sha256:97a1f1e51ea30782d7baa8d0c52f72c3f9f05cb609cf1b990664231c5102bccd", - "sha256:adb6c438c6ef550e2bb83968e772b9690cb421f2c6073f9c2cb6af15ee538bc9", - "sha256:bb687d245b6963673c639f318eea7e875d1ba147a67925586abed3d6f39bb7d8", - "sha256:bd490f77f35800d5620f4d9af669e372d9a88db1f76ef219e1609cc4ecdd1a24", - "sha256:c0dfd7d2429452e7e94904c6a3af63cbaa3cf51b348bd9d35b42db7e9ad42791", - "sha256:d3a326673ac5afa9ef5613a61626b9ec15c8f7222b4ecd1ce0fd8fcba7b83c59", - "sha256:e2004d2a3c397b26ca78e67c9d320153a1a9b71ae713ad33f4a3a3ab3d79cc65", - "sha256:e2ac088ea4aa61115b96b47f5f3d94b3fa29554340b6629cd2bfe6b0521ee33b", - "sha256:f7c3c578ff556333f3890c2df6c056955d53537bb176698359088108af73a58f", - "sha256:fc58c3fcb8a724b703ffbc126afdca5a8353d4d5945d5c92db85617e165299e7" - ], - "markers": "python_version < '3.12' and python_version >= '3.8'", - "version": "==1.9.0" - }, - "setuptools": { - "hashes": [ - "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", - "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" - ], - "markers": "python_version >= '3.7'", - "version": "==65.3.0" - }, - "sniffio": { - "hashes": [ - "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", - "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" - ], - "markers": "python_version >= '3.5'", - "version": "==1.2.0" - }, - "snowballstemmer": { - "hashes": [ - "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", - "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" - ], - "version": "==2.2.0" - }, - "sphinx": { - "hashes": [ - "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693", - "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89" - ], - "markers": "python_version >= '3.6'", - "version": "==5.1.1" - }, - "sphinx-rtd-theme": { - "hashes": [ - "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", - "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "sphinx-rtd-theme-github-versions": { - "hashes": [ - "sha256:23018e51a5d27ef4f69dd86314f73b19088f2cfd91c74a24db1517832233dc07", - "sha256:7f67ba75cac8ddd51326f0b2f314bf653e72b4c96a5adc6e779acc8c01da81ab" - ], - "markers": "python_version >= '3.7'", - "version": "==1.1" - }, - "sphinxcontrib-applehelp": { - "hashes": [ - "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", - "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-devhelp": { - "hashes": [ - "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", - "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" - }, - "sphinxcontrib-htmlhelp": { - "hashes": [ - "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07", - "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.0" - }, - "sphinxcontrib-jsmath": { - "hashes": [ - "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", - "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.1" - }, - "sphinxcontrib-qthelp": { - "hashes": [ - "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", - "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" - ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" - }, - "sphinxcontrib-serializinghtml": { - "hashes": [ - "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", - "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" - ], - "markers": "python_version >= '3.5'", - "version": "==1.1.5" - }, - "starlette": { - "hashes": [ - "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf", - "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7" - ], - "markers": "python_version >= '3.6'", - "version": "==0.19.1" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, - "typed-ast": { - "hashes": [ - "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2", - "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1", - "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6", - "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62", - "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac", - "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d", - "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc", - "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2", - "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97", - "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35", - "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6", - "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1", - "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4", - "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c", - "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e", - "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec", - "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f", - "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72", - "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47", - "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72", - "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe", - "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6", - "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3", - "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66" - ], - "markers": "python_version >= '3.6'", - "version": "==1.5.4" - }, - "typing-extensions": { - "hashes": [ - "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", - "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" - ], - "markers": "python_version < '3.10'", - "version": "==4.3.0" - }, - "urllib3": { - "hashes": [ - "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", - "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", - "version": "==1.26.12" - }, - "uvicorn": { - "hashes": [ - "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af", - "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b" - ], - "markers": "python_version >= '3.7'", - "version": "==0.18.3" - }, - "virtualenv": { - "hashes": [ - "sha256:4193b7bc8a6cd23e4eb251ac64f29b4398ab2c233531e66e40b19a6b7b0d30c1", - "sha256:d86ea0bb50e06252d79e6c241507cb904fcd66090c3271381372d6221a3970f9" - ], - "markers": "python_version >= '3.6'", - "version": "==20.16.3" - }, - "virtualenv-clone": { - "hashes": [ - "sha256:418ee935c36152f8f153c79824bb93eaf6f0f7984bae31d3f48f350b9183501a", - "sha256:44d5263bceed0bac3e1424d64f798095233b64def1c5689afa43dc3223caf5b0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.5.7" - }, - "zipp": { - "hashes": [ - "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2", - "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009" - ], - "markers": "python_version >= '3.7'", - "version": "==3.8.1" - } - } -} diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst index 31c8e89..417034b 100644 --- a/docs/tutorials/installation.rst +++ b/docs/tutorials/installation.rst @@ -4,7 +4,7 @@ Installation .. note:: For installation inside DLS, please see the internal documentation on - ``dls-python3`` and ``pipenv``. Although these instructions will work + ``dls-python3`` and ``pip``. Although these instructions will work inside DLS, they are intended for external use. If you want to contribute to the library itself, please follow @@ -43,6 +43,8 @@ from github:: python3 -m pip install git+git://github.com/dls-controls/diffcalc_API.git The library should now be installed and the commandline interface on your path. -You can check the version that has been installed by typing:: +You can check diffcalc_API has been correctly installed by typing:: - diffcalc_API --version + python -c "import diffcalc_API" + +If you observe any output, repeat the steps above and observe the outputs. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ccd70c7..d99ac1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,4 @@ # dls-python3, and setuptools must produce the same dist-info. Cap setuptools # to the last version that didn't add License-File to METADATA requires = ["setuptools<57", "wheel==0.33.1"] -build-backend = "setuptools.build_meta" +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5875220 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,74 @@ +alabaster==0.7.12 +anyio==3.6.1 +attrs==22.1.0 +babel==2.10.3 +black==22.3.0 +certifi==2022.6.15 +cfgv==3.3.1 +charset-normalizer==2.1.0 +click==8.1.3 +coverage==6.4.3 +git+https://github.com/DiamondLightSource/diffcalc-core.git#egg=diffcalc-core +distlib==0.3.5 +docutils==0.17.1 +fastapi==0.79.0 +filelock==3.8.0 +flake8==3.9.2 +flake8-isort==4.2.0 +h11==0.13.0 +identify==2.5.3 +idna==3.3 +imagesize==1.4.1 +importlib-metadata==4.12.0 +iniconfig==1.1.1 +isort==5.10.1 +jinja2==3.1.2 +markupsafe==2.1.1 +mccabe==0.6.1 +mock==4.0.3 +motor==3.0.0 +mypy==0.971 +mypy-extensions==0.4.3 +nodeenv==1.7.0 +numpy==1.23.1 +packaging==21.3 +pathspec==0.9.0 +pep8-naming==0.13.1 +platformdirs==2.5.2 +pluggy==1.0.0 +pre-commit==2.20.0 +py==1.11.0 +pycodestyle==2.7.0 +pydantic==1.9.1 +pyflakes==2.3.1 +pygments==2.12.0 +pyparsing==3.0.9 +pytest==7.1.2 +pytest-cov==3.0.0 +pytz==2022.1 +pyyaml==6.0 +pymongo==4.2.0 +requests==2.28.1 +scipy==1.9.0 +setuptools==63.4.2 +sniffio==1.2.0 +snowballstemmer==2.2.0 +sphinx==5.1.1 +sphinx-rtd-theme==1.0.0 +sphinx-rtd-theme-github-versions==1.1 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 +starlette==0.19.1 +toml==0.10.2 +tomli==2.0.1 +typed-ast==1.5.4 +typing-extensions==4.3.0 +urllib3==1.26.11 +uvicorn==0.18.2 +virtualenv==20.16.3 +virtualenv-clone==0.5.7 +zipp==3.8.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 91ea339..f1b46ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,6 @@ install_requires = uvicorn pymongo motor - setuptools==63.4.2 [options.extras_require] # For development tests/docs @@ -44,10 +43,11 @@ dev = flake8-isort sphinx-rtd-theme-github-versions pre-commit - pipenv typed-ast pep8-naming mock + pydocstyle + mongomock [options.packages.find] where = src @@ -81,7 +81,7 @@ addopts = # filterwarnings = error #Uncomment later. # Doctest python code in docs, python code in src docstrings, test functions in tests testpaths = - docs src tests + src tests [coverage:run] # This is covered in the versiongit test suite so exclude it here diff --git a/src/diffcalc_API/__init__.py b/src/diffcalc_API/__init__.py index 8bf36c9..c8ba73b 100644 --- a/src/diffcalc_API/__init__.py +++ b/src/diffcalc_API/__init__.py @@ -1,7 +1,6 @@ +"""API to expose diffcalc-core methods.""" + from . import config, database, openapi, server from ._version_git import __version__ -# __all__ defines the public API for the package. -# Each module also defines its own __all__. - __all__ = ["__version__", "server", "config", "database", "openapi"] diff --git a/src/diffcalc_API/config.py b/src/diffcalc_API/config.py index 551a464..703f972 100644 --- a/src/diffcalc_API/config.py +++ b/src/diffcalc_API/config.py @@ -1,3 +1,5 @@ +"""API configuration options.""" + import logging from pydantic import BaseSettings @@ -15,6 +17,12 @@ class Settings(BaseSettings): + """Class which gets environment variables. + + Environment variables set to the attributes of this class are automatically + set to the values of these attributes, and used in the application. + """ + mongo_url: str = "localhost:27017" api_version = version logging_level: str = "WARN" diff --git a/src/diffcalc_API/database.py b/src/diffcalc_API/database.py index a314481..bd95e7a 100644 --- a/src/diffcalc_API/database.py +++ b/src/diffcalc_API/database.py @@ -1,3 +1,5 @@ +"""Mongo database configuration options.""" + import motor.motor_asyncio from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase diff --git a/src/diffcalc_API/errors/__init__.py b/src/diffcalc_API/errors/__init__.py index 92906ae..c64b1d5 100644 --- a/src/diffcalc_API/errors/__init__.py +++ b/src/diffcalc_API/errors/__init__.py @@ -1,3 +1,10 @@ +""" +Defines all errors that can be raised when accessing almost all endpoints. + +Because some errors arise from persistence retrieval, those are defined in +structures within diffcalc_API.stores instead. +""" + from diffcalc_API.errors import constraints, hkl, ub __all__ = ["hkl", "ub", "constraints"] diff --git a/src/diffcalc_API/errors/constraints.py b/src/diffcalc_API/errors/constraints.py index 9ca1867..b5f6265 100644 --- a/src/diffcalc_API/errors/constraints.py +++ b/src/diffcalc_API/errors/constraints.py @@ -1,3 +1,4 @@ +"""Errors that can be raised when accessing constraints endpoints.""" import numpy as np from diffcalc_API.config import ALL_CONSTRAINTS @@ -9,6 +10,8 @@ class ErrorCodes(ErrorCodesBase): + """All error codes which constraints routes can raise.""" + INVALID_CONSTRAINT = 400 @@ -16,7 +19,10 @@ class ErrorCodes(ErrorCodesBase): class InvalidConstraintError(DiffcalcAPIException): + """Error that gets thrown when the provided constraint is invalid.""" + def __init__(self, constraint: str): + """Set detail and status code.""" self.detail = ( f"property {constraint} does not exist as a valid constraint." f" Valid constraints are: {ALL_CONSTRAINTS}" diff --git a/src/diffcalc_API/errors/definitions.py b/src/diffcalc_API/errors/definitions.py index 20f68f6..66c4f7b 100644 --- a/src/diffcalc_API/errors/definitions.py +++ b/src/diffcalc_API/errors/definitions.py @@ -1,3 +1,5 @@ +"""Errors used throughout the package and for documentation.""" + from enum import IntEnum from typing import Any, Dict, List, Union @@ -9,19 +11,27 @@ class DiffcalcAPIException(Exception): + """Error when there is an issue with the request.""" + def __init__(self, status_code: int, detail: str): + """Set status code and detail.""" self.status_code = status_code self.detail = detail class DiffcalcExceptionModel(BaseModel): + """Error when there is an issue with diffcalc-core's execution of the request.""" + status_code: int detail: str class ErrorCodesBase(IntEnum): + """Base class to store multiple error codes for documentation/testing purposes.""" + @classmethod def all_codes(cls) -> List[int]: + """Generate a list of all the code values for documentation purposes.""" return [val.value for val in cls] diff --git a/src/diffcalc_API/errors/hkl.py b/src/diffcalc_API/errors/hkl.py index 2b6d134..04f42f0 100644 --- a/src/diffcalc_API/errors/hkl.py +++ b/src/diffcalc_API/errors/hkl.py @@ -1,3 +1,4 @@ +"""Errors that can be raised when accessing /hkl/ endpoints.""" from typing import Optional import numpy as np @@ -10,6 +11,8 @@ class ErrorCodes(ErrorCodesBase): + """All error codes which hkl routes can raise.""" + INVALID_MILLER_INDICES = 400 INVALID_SCAN_BOUNDS = 400 INVALID_SOLUTION_BOUNDS = 400 @@ -19,7 +22,10 @@ class ErrorCodes(ErrorCodesBase): class InvalidMillerIndicesError(DiffcalcAPIException): + """Error that gets thrown when provided miller indices are invalid.""" + def __init__(self, detail: Optional[str] = None) -> None: + """Set detail and status code.""" self.detail = ( "At least one of the hkl indices must be non-zero" if not detail else detail ) @@ -27,7 +33,10 @@ def __init__(self, detail: Optional[str] = None) -> None: class InvalidScanBoundsError(DiffcalcAPIException): + """Error that gets thrown when provided scan bounds are invalid.""" + def __init__(self, start: float, stop: float, inc: float) -> None: + """Set detail and status code.""" self.detail = ( f"numpy range cannot be formed from start: {start}" f" to stop: {stop} in increments of: {inc}" @@ -36,6 +45,14 @@ def __init__(self, start: float, stop: float, inc: float) -> None: class InvalidSolutionBoundsError(DiffcalcAPIException): + """Error that gets thrown when provided solution bounds are invalid. + + The diffraction angle calculator often provides multiple diffractometer + angles as equivalent to a set of miller indices. Solution bounds can be provided + to constrain these, however they must pass some checks before use. + """ + def __init__(self, detail: str) -> None: + """Set detail and status code.""" self.detail = detail self.status_code = ErrorCodes.INVALID_SOLUTION_BOUNDS diff --git a/src/diffcalc_API/errors/ub.py b/src/diffcalc_API/errors/ub.py index 92f9d0e..5d17068 100644 --- a/src/diffcalc_API/errors/ub.py +++ b/src/diffcalc_API/errors/ub.py @@ -1,3 +1,5 @@ +"""Defines all errors that can be raised when accessing ub endpoints.""" + from typing import Optional, Union import numpy as np @@ -11,6 +13,8 @@ class ErrorCodes(ErrorCodesBase): + """All error codes which ub routes can raise.""" + INVALID_SET_LATTICE_PARAMS = 400 REFERENCE_RETRIEVAL_ERROR = 403 INVALID_PROPERTY = 400 @@ -22,7 +26,14 @@ class ErrorCodes(ErrorCodesBase): class NoTagOrIdxProvidedError(DiffcalcAPIException): + """Error that gets thrown when neither a tag or index are provided. + + Some ub routes require editing existing objects handled by a tag or index. If + neither of these are provided, the API doesn't know which object to modify. + """ + def __init__(self): + """Set detail and status code.""" self.detail = ( "One of the following must be provided as a query parameter:" + " tag (string), index (integer)" @@ -31,7 +42,14 @@ def __init__(self): class BothTagAndIdxProvidedError(DiffcalcAPIException): + """Error that gets thrown when both a tag and index are provided. + + Some ub routes require editing existing objects handled by a tag or index. If + both of these are provided, the API doesn't know which to choose to modify. + """ + def __init__(self): + """Set detail and status code.""" self.detail = ( "both the tag and index have been provided. These are identifiers" + " for a specific orientation or reflection, and so both cannot be" @@ -41,18 +59,35 @@ def __init__(self): class InvalidSetLatticeParamsError(DiffcalcAPIException): + """Error that gets thrown if the request body is empty. + + All parameters in the request body to the endpoint which sets the lattice are + completely optional by definition. However in practise at least some parameters + should be provided. + """ + def __init__(self): + """Set detail and status code.""" self.detail = ("please provide lattice parameters in request body",) self.status_code = ErrorCodes.INVALID_SET_LATTICE_PARAMS class ReferenceRetrievalError(DiffcalcAPIException): + """Error that gets thrown if a reflection or orientation cannot be retrieved. + + Commonly caused by an issue with the tag or index provided. + """ + def __init__(self, handle: Optional[Union[str, int]], reference_type: str) -> None: + """Set detail and status code.""" self.detail = f"cannot retrieve {reference_type} with tag or index {handle}" self.status_code = ErrorCodes.REFERENCE_RETRIEVAL_ERROR class InvalidPropertyError(DiffcalcAPIException): + """Error that gets thrown if attempting to modify a non-existing property.""" + def __init__(self): + """Set detail and status code.""" self.detail = f"invalid property. Choose one of: {VECTOR_PROPERTIES}" self.status_code = ErrorCodes.INVALID_PROPERTY diff --git a/src/diffcalc_API/examples/__init__.py b/src/diffcalc_API/examples/__init__.py index 603a2eb..de30da2 100644 --- a/src/diffcalc_API/examples/__init__.py +++ b/src/diffcalc_API/examples/__init__.py @@ -1,3 +1,5 @@ +"""Examples to use in endpoints for fastAPI docs, to make it easier to read.""" + from diffcalc_API.examples import ub __all__ = ["ub"] diff --git a/src/diffcalc_API/examples/ub.py b/src/diffcalc_API/examples/ub.py index f5c4d8a..1ce6407 100644 --- a/src/diffcalc_API/examples/ub.py +++ b/src/diffcalc_API/examples/ub.py @@ -1,3 +1,6 @@ +"""API examples used in diffcalc_API.routes.ub.""" + + from diffcalc_API.models.ub import ( AddOrientationParams, AddReflectionParams, diff --git a/src/diffcalc_API/models/__init__.py b/src/diffcalc_API/models/__init__.py index 1a46719..a56b8b8 100644 --- a/src/diffcalc_API/models/__init__.py +++ b/src/diffcalc_API/models/__init__.py @@ -1,3 +1,12 @@ +"""Defines all pydantic models used for endpoint requests and responses. + +Ub and Hkl endpoints have specific pydantic models for their request bodies, defined in +modules diffcalc_API.models.ub and diffcalc_API.models.hkl respectively. + +Module diffcalc_API.models.response defines general endpoint response models used by +all routes. +""" + from diffcalc_API.models import hkl, response, ub __all__ = ["ub", "hkl", "response"] diff --git a/src/diffcalc_API/models/hkl.py b/src/diffcalc_API/models/hkl.py index f6cc1c6..ef3a199 100644 --- a/src/diffcalc_API/models/hkl.py +++ b/src/diffcalc_API/models/hkl.py @@ -1,3 +1,5 @@ +"""Defines pydantic models relating to hkl endpoints.""" + from dataclasses import dataclass from typing import Iterator, List, Optional, Union @@ -6,6 +8,17 @@ @dataclass class SolutionConstraints: + """Class to store solution constraints. + + The diffraction angle calculator often provides multiple diffractometer + angles as equivalent to a set of miller indices. Solution bounds can be provided + to constrain these, however they must pass some checks before use. + + Solutions are constrained by diffractometer angles, or axes. A high bound and low + bound must be given for each provided. All three of these conditions must contain + the same number of elements to be valid. + """ + axes: Optional[List[str]] = None low_bound: Optional[List[float]] = None high_bound: Optional[List[float]] = None @@ -13,9 +26,14 @@ class SolutionConstraints: msg: str = "" def __post_init__(self): + """Immediately upon initialisation, evaluate validity of bounds.""" self.invalid_bounds() def invalid_bounds(self) -> None: + """Compute if the inputes are valid. + + If not, sets self.valid to False. + """ axes, low_bound, high_bound = self.axes, self.low_bound, self.high_bound msg = self.msg diff --git a/src/diffcalc_API/models/response.py b/src/diffcalc_API/models/response.py index cabc60c..9ba0da0 100644 --- a/src/diffcalc_API/models/response.py +++ b/src/diffcalc_API/models/response.py @@ -1,3 +1,4 @@ +"""Pydantic models relating to all endpoint responses.""" from typing import Dict, List from pydantic import BaseModel @@ -6,24 +7,50 @@ class InfoResponse(BaseModel): + """Used for all HTTP requests that simply change persisted state.""" + message: str class StringResponse(BaseModel): + """Used for some HTTP get requests. + + InfoResponse models should only be returned to inform the client that + a persisted state has changed. The StringResponse model however addresses + the case where a HTTP get request needs to return a string. The distinction + is minor but important. + """ + payload: str class ArrayResponse(BaseModel): + """Used for UB calculation retrieval.""" + payload: List[List[float]] class ScanResponse(BaseModel): + """Used for all scans in hkl endpoints.""" + payload: Dict[str, List[Dict[str, float]]] class DiffractorAnglesResponse(BaseModel): + """Diffractor Angles Response. + + Used for any endpoint with an attached service that returns a set of diffractor + angles. + """ + payload: List[Dict[str, float]] class MillerIndicesResponse(BaseModel): + """Miller Indices Response. + + Used for any endpoint with an attached service that returns a set of miller + indices. + """ + payload: HklModel diff --git a/src/diffcalc_API/models/ub.py b/src/diffcalc_API/models/ub.py index 7fbb842..f678ae7 100644 --- a/src/diffcalc_API/models/ub.py +++ b/src/diffcalc_API/models/ub.py @@ -1,21 +1,29 @@ +"""Pydantic models relating to ub routes.""" + from typing import Optional from pydantic import BaseModel class HklModel(BaseModel): + """Model containing miller indices.""" + h: float k: float l: float class XyzModel(BaseModel): + """Model containing real space positions.""" + x: float y: float z: float class PositionModel(BaseModel): + """Model containing diffractometer angles.""" + mu: float delta: float nu: float @@ -25,6 +33,8 @@ class PositionModel(BaseModel): class SetLatticeParams(BaseModel): + """Request body definition to set the lattice.""" + name: Optional[str] = None system: Optional[str] = None a: Optional[float] = None @@ -36,18 +46,24 @@ class SetLatticeParams(BaseModel): class AddReflectionParams(BaseModel): + """Request body definition to add a reflection of the UB calculation.""" + hkl: HklModel position: PositionModel energy: float class AddOrientationParams(BaseModel): + """Request body definition to add an orientation of the UB calculation.""" + hkl: HklModel xyz: XyzModel position: Optional[PositionModel] = None class EditReflectionParams(BaseModel): + """Request body definition to edit a reflection of the UB calculation.""" + hkl: Optional[HklModel] = None position: Optional[PositionModel] = None energy: Optional[float] = None @@ -55,11 +71,17 @@ class EditReflectionParams(BaseModel): class EditOrientationParams(BaseModel): + """Request body definition to edit an orientation of the UB calculation.""" + hkl: Optional[HklModel] = None xyz: Optional[XyzModel] = None position: Optional[PositionModel] = None set_tag: Optional[str] = None -def select_idx_or_tag_str(idx: Optional[int], tag: Optional[str]): +def select_idx_or_tag_str(idx: Optional[int], tag: Optional[str]) -> str: + """Select an index or tag, and generate a string from it. + + Return a string for diffcalc_API.models.response.InfoResponse endpoint responses. + """ return f"index {idx}" if idx is not None else f"tag {tag}" diff --git a/src/diffcalc_API/openapi.py b/src/diffcalc_API/openapi.py index c83b6e0..cc4f5a5 100644 --- a/src/diffcalc_API/openapi.py +++ b/src/diffcalc_API/openapi.py @@ -1,3 +1,5 @@ +"""Generate openapi.json.""" + import json from fastapi.openapi.utils import get_openapi diff --git a/src/diffcalc_API/routes/__init__.py b/src/diffcalc_API/routes/__init__.py index 9f64f11..da7ab89 100644 --- a/src/diffcalc_API/routes/__init__.py +++ b/src/diffcalc_API/routes/__init__.py @@ -1,3 +1,5 @@ +"""Defines all endpoints for the API.""" + from diffcalc_API.routes import constraints, hkl, ub __all__ = ["ub", "hkl", "constraints"] diff --git a/src/diffcalc_API/routes/constraints.py b/src/diffcalc_API/routes/constraints.py index 96e1c08..e97cb7b 100644 --- a/src/diffcalc_API/routes/constraints.py +++ b/src/diffcalc_API/routes/constraints.py @@ -1,3 +1,5 @@ +"""Endpoints relating to the management of constraints.""" + from typing import Dict, Optional from fastapi import APIRouter, Body, Depends, Query @@ -15,6 +17,16 @@ async def get_constraints( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Get the status of the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + StringResponse with the state of the contsraints object. + """ content = await service.get_constraints(name, store, collection) return StringResponse(payload=content) @@ -26,6 +38,15 @@ async def set_constraints( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Set the constraints in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + constraints: dictionary with the constraints to set + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ await service.set_constraints(name, constraints, store, collection) return InfoResponse( message=( @@ -42,6 +63,15 @@ async def remove_constraint( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Remove a constraint in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the constraint to remove + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ await service.remove_constraint(name, property, store, collection) return InfoResponse( @@ -60,6 +90,16 @@ async def set_constraint( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Set a constraint in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the constraint to set + value: the value of the constraint to set to + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ await service.set_constraint(name, property, value, store, collection) return InfoResponse( diff --git a/src/diffcalc_API/routes/hkl.py b/src/diffcalc_API/routes/hkl.py index 466ba73..da5cffa 100644 --- a/src/diffcalc_API/routes/hkl.py +++ b/src/diffcalc_API/routes/hkl.py @@ -1,3 +1,5 @@ +"""Endpoints relating to calculating positions using constraints and the UB matrix.""" + from typing import List, Optional from fastapi import APIRouter, Depends, Query @@ -27,6 +29,26 @@ async def calculate_ub( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Calculate the UB matrix. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + tag1: the tag of the first reference object. + idx1: the index of the first reference object. + tag2: the tag of the second reference object. + idx2: the index of the second reference object. + + For each reference object, only a tag or index needs to be given. If none are + provided, diffcalc-core tries to work it out from the available reference + objects. + + Returns: + ArrayResponse object containing a list of angles, combined together into one + dictionary. + + """ content = await service.calculate_ub( name, store, collection, tag1, idx1, tag2, idx2 ) @@ -44,6 +66,22 @@ async def lab_position_from_miller_indices( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Convert miller indices to a list of diffractometer positions. + + Args: + name: the name of the hkl object to access within the store + miller_indices: miller indices to be converted + wavelength: wavelength of light used in the experiment + axes: angles to constrain the solutions by + low_bounds: minimum values of constrained axes + high_bound: maximum values of constrained axes + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + DiffractorAnglesResponse containing a list of all possible diffractometer + positions. + """ solution_constraints = SolutionConstraints(axes, low_bound, high_bound) if not solution_constraints.valid: raise InvalidSolutionBoundsError(solution_constraints.msg) @@ -67,6 +105,18 @@ async def miller_indices_from_lab_position( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Convert a diffractometer position to a set of miller indices. + + Args: + name: the name of the hkl object to access within the store + pos: object containing diffractometer position to be converted. + wavelength: wavelength of light used in the experiment + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + MillerIndicesResponse containing the miller indices. + """ hkl = await service.miller_indices_from_lab_position( name, pos, wavelength, store, collection ) @@ -86,6 +136,24 @@ async def scan_hkl( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Retrieve possible diffractometer positions for a range of miller indices. + + Args: + name: the name of the hkl object to access within the store + start: miller indices to start at + stop: miller indices to stop at + inc: miller indices to increment by + wavelength: wavelength of light used in the experiment + axes: angles to constrain the solutions by + low_bounds: minimum values of constrained axes + high_bound: maximum values of constrained axes + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + ScanResponse containing a dictionary of each set of miller indices and their + possible diffractometer positions. + """ solution_constraints = SolutionConstraints(axes, low_bound, high_bound) if not solution_constraints.valid: raise InvalidSolutionBoundsError(solution_constraints.msg) @@ -116,6 +184,24 @@ async def scan_wavelength( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Retrieve possible diffractometer positions for a range of wavelengths. + + Args: + name: the name of the hkl object to access within the store + start: wavelength to start at + stop: wavelength to stop at + inc: wavelength to increment by + hkl: desired miller indices to use for the experiment + axes: angles to constrain the solutions by + low_bounds: minimum values of constrained axes + high_bound: maximum values of constrained axes + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + ScanResponse containing a dictionary of each wavelength and the corresponding + possible diffractometer positions. + """ solution_constraints = SolutionConstraints(axes, low_bound, high_bound) if not solution_constraints.valid: raise InvalidSolutionBoundsError(solution_constraints.msg) @@ -141,6 +227,26 @@ async def scan_constraint( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Retrieve possible diffractometer positions while scanning across a constraint. + + Args: + name: the name of the hkl object to access within the store + constraint: the name of the constraint to use. + start: constraint to start at + stop: constraint to stop at + inc: constraint to increment by + hkl: desired miller indices to use for the experiment + wavelength: wavelength of light used in the experiment + axes: angles to constrain the solutions by + low_bounds: minimum values of constrained axes + high_bound: maximum values of constrained axes + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + ScanResponse containing a dictionary of each constraint value and the + corresponding possible diffractometer positions. + """ solution_constraints = SolutionConstraints(axes, low_bound, high_bound) if not solution_constraints.valid: raise InvalidSolutionBoundsError(solution_constraints.msg) diff --git a/src/diffcalc_API/routes/ub.py b/src/diffcalc_API/routes/ub.py index f8f9118..76bc33e 100644 --- a/src/diffcalc_API/routes/ub.py +++ b/src/diffcalc_API/routes/ub.py @@ -1,3 +1,5 @@ +"""Endpoints relating to the management of setting up the UB calculation.""" + from typing import Optional from fastapi import APIRouter, Body, Depends, Query @@ -32,6 +34,16 @@ async def get_ub( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Get the status of the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + a string with the current state of the UB object + """ content = await service.get_ub(name, store, collection) return StringResponse(payload=content) @@ -44,6 +56,16 @@ async def add_reflection( collection: Optional[str] = Query(default=None, example="B07"), tag: Optional[str] = Query(default=None, example="refl1"), ): + """Add reflection to the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail about the reflection object to be added + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to attribute to the new reflection + + """ await service.add_reflection(name, params, store, collection, tag) return InfoResponse( message=( @@ -62,6 +84,18 @@ async def edit_reflection( tag: Optional[str] = Query(default=None, example="refl1"), idx: Optional[int] = Query(default=None), ): + """Modify reflection in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail describing what the reflection should be edited to + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the reflection by + idx: optional index to retrieve the reflection by + + Exactly one tag or index must be provided. + """ if (tag is None) and (idx is None): raise NoTagOrIdxProvidedError() @@ -85,6 +119,17 @@ async def delete_reflection( tag: Optional[str] = Query(default=None, example="refl1"), idx: Optional[int] = Query(default=None), ): + """Delete reflection in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the reflection by + idx: optional index to retrieve the reflection by + + Exactly one tag or index must be provided. + """ if (idx is None) and (tag is None): raise NoTagOrIdxProvidedError() if (idx is not None) and (tag is not None): @@ -107,6 +152,16 @@ async def add_orientation( collection: Optional[str] = Query(default=None, example="B07"), tag: Optional[str] = Query(default=None, example="plane"), ): + """Add orientation to the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail about the orientation object to be added + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to attribute to the new orientation + + """ await service.add_orientation(name, params, store, collection, tag) return InfoResponse( message=( @@ -125,6 +180,18 @@ async def edit_orientation( tag: Optional[str] = Query(default=None), idx: Optional[int] = Query(default=None, example=0), ): + """Modify orientation in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail describing what the orientation should be edited to + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the orientation by + idx: optional index to retrieve the orientation by + + Exactly one tag or index must be provided. + """ if (idx is None) and (tag is None): raise NoTagOrIdxProvidedError() if (idx is not None) and (tag is not None): @@ -147,6 +214,17 @@ async def delete_orientation( tag: Optional[str] = Query(default=None, example="plane"), idx: Optional[int] = Query(default=None), ): + """Delete orientation in the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the orientation by + idx: optional index to retrieve the orientation by + + Exactly one tag or index must be provided. + """ if (idx is None) and (tag is None): raise NoTagOrIdxProvidedError() if (idx is not None) and (tag is not None): @@ -168,6 +246,15 @@ async def set_lattice( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Set the Crystal parameters in the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + params: the parameters to use to set the lattice + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ non_empty_vars = [var for var, value in params if value is not None] if len(non_empty_vars) == 0: @@ -190,6 +277,17 @@ async def modify_property( store: HklCalcStore = Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Set a property of the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the property of the UB object to set + target_value: the miller indices to set them to + store: accessor to the hkl object + collection: collection within which the hkl object resides + + The property to be set must be a valid vector property. + """ if property not in VECTOR_PROPERTIES: raise InvalidPropertyError() diff --git a/src/diffcalc_API/server.py b/src/diffcalc_API/server.py index f6fa7c3..c6e2039 100644 --- a/src/diffcalc_API/server.py +++ b/src/diffcalc_API/server.py @@ -1,3 +1,4 @@ +"""Startup script for the API server.""" import logging import traceback from typing import Optional @@ -33,6 +34,7 @@ @app.exception_handler(DiffcalcException) async def diffcalc_exception_handler(request: Request, exc: DiffcalcException): + """Handle diffcalc-core exceptions, which mostly happen on bad requests.""" tb = traceback.format_exc() logger.warning(f"Diffcalc Exception caught by middleware: {tb}") @@ -44,8 +46,9 @@ async def diffcalc_exception_handler(request: Request, exc: DiffcalcException): @app.exception_handler(DiffcalcAPIException) async def http_exception_handler(request: Request, exc: DiffcalcAPIException): + """Handle exceptions raised due to bad request parameters/queries and bodies.""" tb = traceback.format_exc() - logger.error(f"Diffcalc API Exception caught by middleware: {tb}") + logger.warning(f"Diffcalc API Exception caught by middleware: {tb}") return responses.JSONResponse( status_code=exc.status_code, @@ -55,6 +58,10 @@ async def http_exception_handler(request: Request, exc: DiffcalcAPIException): @app.middleware("http") async def server_exceptions_middleware(request: Request, call_next): + """Handle all other exceptions. + + These are undocumented, and if raised should be investigated immediately. + """ try: return await call_next(request) except Exception as e: @@ -78,6 +85,7 @@ async def create_hkl_object( store=Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Create the hkl object which will be persisted.""" await store.create(name, collection) return InfoResponse(message=f"crystal {name} in collection {collection} created") @@ -88,6 +96,7 @@ async def delete_hkl_object( store=Depends(get_store), collection: Optional[str] = Query(default=None, example="B07"), ): + """Delete the hkl object from the store i.e. persistence layer.""" await store.delete(name, collection) return InfoResponse(message=f"crystal {name} in collection {collection} deleted") diff --git a/src/diffcalc_API/services/__init__.py b/src/diffcalc_API/services/__init__.py index 2350aac..5e5f3bb 100644 --- a/src/diffcalc_API/services/__init__.py +++ b/src/diffcalc_API/services/__init__.py @@ -1,3 +1,5 @@ +"""Defines business logic for all endpoints, separately from the API logic.""" + from diffcalc_API.services import constraints, hkl, ub __all__ = ["ub", "hkl", "constraints"] diff --git a/src/diffcalc_API/services/constraints.py b/src/diffcalc_API/services/constraints.py index 0c85e90..82e659c 100644 --- a/src/diffcalc_API/services/constraints.py +++ b/src/diffcalc_API/services/constraints.py @@ -1,3 +1,5 @@ +"""Defines business logic for handling requests from constraints endpoints.""" + from typing import Dict, Optional from diffcalc.hkl.constraints import Constraints @@ -10,6 +12,16 @@ async def get_constraints( name: str, store: HklCalcStore, collection: Optional[str] ) -> str: + """Get the status of the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + a string with the current state of the constraints + """ hklcalc = await store.load(name, collection) return str(hklcalc.constraints) @@ -20,6 +32,15 @@ async def set_constraints( store: HklCalcStore, collection: Optional[str], ) -> None: + """Set the constraints in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + constraints: dictionary with the constraints to set + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ hklcalc = await store.load(name, collection) boolean_constraints = set(constraints.keys()).intersection( @@ -39,6 +60,15 @@ async def remove_constraint( store: HklCalcStore, collection: Optional[str], ) -> None: + """Remove a constraint in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the constraint to remove + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ hklcalc = await store.load(name, collection) if property not in ALL_CONSTRAINTS: @@ -56,6 +86,16 @@ async def set_constraint( store: HklCalcStore, collection: Optional[str], ) -> None: + """Set a constraint in the constraints object in the given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the constraint to set + value: the value of the constraint to set to + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ hklcalc = await store.load(name, collection) if property not in ALL_CONSTRAINTS: diff --git a/src/diffcalc_API/services/hkl.py b/src/diffcalc_API/services/hkl.py index 682fe2a..46c2e9b 100644 --- a/src/diffcalc_API/services/hkl.py +++ b/src/diffcalc_API/services/hkl.py @@ -1,3 +1,5 @@ +"""Defines business logic for handling requests from hkl endpoints.""" + from itertools import product from typing import Dict, List, Optional, Tuple, Union @@ -18,6 +20,19 @@ async def lab_position_from_miller_indices( store: HklCalcStore, collection: Optional[str], ) -> List[Dict[str, float]]: + """Convert miller indices to a list of diffractometer positions. + + Args: + name: the name of the hkl object to access within the store + miller_indices: miller indices to be converted + wavelength: wavelength of light used in the experiment + solution_constraints: object containings angles to constrain solutions by + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + A list of all possible diffractometer positions + """ hklcalc = await store.load(name, collection) if all([idx == 0 for idx in miller_indices]): @@ -36,6 +51,18 @@ async def miller_indices_from_lab_position( store: HklCalcStore, collection: Optional[str], ) -> HklModel: + """Convert a diffractometer position to a set of miller indices. + + Args: + name: the name of the hkl object to access within the store + pos: object containing diffractometer position to be converted. + wavelength: wavelength of light used in the experiment + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + Object containing converted lab position + """ hklcalc = await store.load(name, collection) hkl = np.round(hklcalc.get_hkl(Position(**pos.dict()), wavelength), 16) return HklModel(h=hkl[0], k=hkl[1], l=hkl[2]) @@ -51,6 +78,22 @@ async def scan_hkl( store: HklCalcStore, collection: Optional[str], ) -> Dict[str, List[Dict[str, float]]]: + """Retrieve possible diffractometer positions for a range of miller indices. + + Args: + name: the name of the hkl object to access within the store + start: miller indices to start at + stop: miller indices to stop at + inc: miller indices to increment by + wavelength: wavelength of light used in the experiment + solution_constraints: object containings angles to constrain solutions by + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + Dictionary of each set of miller indices and their possible diffractometer + positions. + """ hklcalc = await store.load(name, collection) if (len(start) != 3) or (len(stop) != 3) or (len(inc) != 3): @@ -89,6 +132,22 @@ async def scan_wavelength( store: HklCalcStore, collection: Optional[str], ) -> Dict[str, List[Dict[str, float]]]: + """Retrieve possible diffractometer positions for a range of wavelengths. + + Args: + name: the name of the hkl object to access within the store + start: wavelength to start at + stop: wavelength to stop at + inc: wavelength to increment by + hkl: desired miller indices to use for the experiment + solution_constraints: object containings angles to constrain solutions by + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + Dictionary of each wavelength and the corresponding possible diffractometer + positions. + """ hklcalc = await store.load(name, collection) if len(np.arange(start, stop + inc, inc)) == 0: @@ -118,6 +177,24 @@ async def scan_constraint( store: HklCalcStore, collection: Optional[str], ) -> Dict[str, List[Dict[str, float]]]: + """Retrieve possible diffractometer positions while scanning across a constraint. + + Args: + name: the name of the hkl object to access within the store + constraint: the name of the constraint to use. + start: constraint to start at + stop: constraint to stop at + inc: constraint to increment by + hkl: desired miller indices to use for the experiment + wavelength: wavelength of light used in the experiment + solution_constraints: object containings angles to constrain solutions by + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + + Returns: + Dictionary of each constraint value and the corresponding possible + diffractometer positions. + """ hklcalc = await store.load(name, collection) if len(np.arange(start, stop + inc, inc)) == 0: @@ -135,6 +212,19 @@ async def scan_constraint( def generate_axis(start: float, stop: float, inc: float): + """Attempt to generate a numpy range between values. + + Args: + start: value to start at + stop: value to stop at + inc: value to increment by + + Returns: + a numpy range. + + Throws an error if the numpy range is null, most likely due to non-logical + range like 0->1 in increments of a negative number. + """ if len(np.arange(start, stop + inc, inc)) == 0: raise InvalidScanBoundsError(start, stop, inc) @@ -145,6 +235,16 @@ def combine_lab_position_results( positions: List[Tuple[Position, Dict[str, float]]], solution_constraints: SolutionConstraints, ) -> List[Dict[str, float]]: + """Combine physical and virtual angles. + + Args: + positions: list of each set of physical and virtual angles. + solution_constraints: object containings angles to constrain solutions by + + Returns: + a list of angles, combined together into one dictionary. + + """ axes = solution_constraints.axes low_bound = solution_constraints.low_bound high_bound = solution_constraints.high_bound @@ -176,6 +276,25 @@ async def calculate_ub( tag2: Optional[str], idx2: Optional[int], ) -> List[List[float]]: + """Calculate the UB matrix. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object. + collection: collection within which the hkl object resides. + tag1: the tag of the first reference object. + idx1: the index of the first reference object. + tag2: the tag of the second reference object. + idx2: the index of the second reference object. + + For each reference object, only a tag or index needs to be given. If none are + provided, diffcalc-core tries to work it out from the available reference + objects. + + Returns: + a list of angles, combined together into one dictionary. + + """ hklcalc = await store.load(name, collection) first_retrieve: Optional[Union[str, int]] = tag1 if tag1 else idx1 diff --git a/src/diffcalc_API/services/ub.py b/src/diffcalc_API/services/ub.py index 9e55d33..7a30285 100644 --- a/src/diffcalc_API/services/ub.py +++ b/src/diffcalc_API/services/ub.py @@ -1,3 +1,5 @@ +"""Business logic for handling requests from ub endpoints.""" + from typing import Optional, Union from diffcalc.hkl.geometry import Position @@ -15,6 +17,16 @@ async def get_ub(name: str, store: HklCalcStore, collection: Optional[str]) -> str: + """Get the status of the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + + Returns: + a string with the current state of the UB object + """ hklcalc = await store.load(name, collection) return str(hklcalc.ubcalc) @@ -27,6 +39,16 @@ async def add_reflection( collection: Optional[str], tag: Optional[str], ) -> None: + """Add reflection to the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail about the reflection object to be added + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to attribute to the new reflection + + """ hklcalc = await store.load(name, collection) hklcalc.ubcalc.add_reflection( @@ -47,6 +69,18 @@ async def edit_reflection( tag: Optional[str], idx: Optional[int], ) -> None: + """Modify reflection in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail describing what the reflection should be edited to + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the reflection by + idx: optional index to retrieve the reflection by + + Exactly one tag or index must be provided. + """ hklcalc = await store.load(name, collection) retrieve: Union[int, str] = ( @@ -85,6 +119,17 @@ async def delete_reflection( tag: Optional[str], idx: Optional[int], ) -> None: + """Delete reflection in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the reflection by + idx: optional index to retrieve the reflection by + + Exactly one tag or index must be provided. + """ hklcalc = await store.load(name, collection) retrieve: Union[str, int] = ( @@ -108,6 +153,16 @@ async def add_orientation( collection: Optional[str], tag: Optional[str], ) -> None: + """Add orientation to the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail about the orientation object to be added + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to attribute to the new orientation + + """ hklcalc = await store.load(name, collection) position = Position(**params.position.dict()) if params.position else None @@ -129,6 +184,18 @@ async def edit_orientation( tag: Optional[str], idx: Optional[int], ) -> None: + """Modify orientation in the UB object in the hkl object. + + Args: + name: the name of the hkl object to access within the store + params: detail describing what the orientation should be edited to + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the orientation by + idx: optional index to retrieve the orientation by + + Exactly one tag or index must be provided. + """ hklcalc = await store.load(name, collection) retrieve: Union[int, str] = ( @@ -167,6 +234,17 @@ async def delete_orientation( tag: Optional[str], idx: Optional[int], ) -> None: + """Delete orientation in the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + store: accessor to the hkl object + collection: collection within which the hkl object resides + tag: optional tag to retrieve the orientation by + idx: optional index to retrieve the orientation by + + Exactly one tag or index must be provided. + """ hklcalc = await store.load(name, collection) retrieve: Union[int, str] = ( @@ -186,6 +264,15 @@ async def delete_orientation( async def set_lattice( name: str, params: SetLatticeParams, store: HklCalcStore, collection: Optional[str] ) -> None: + """Set the Crystal parameters in the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + params: the parameters to use to set the lattice + store: accessor to the hkl object + collection: collection within which the hkl object resides + + """ hklcalc = await store.load(name, collection) input_params = params.dict() @@ -204,6 +291,17 @@ async def modify_property( store: HklCalcStore, collection: Optional[str], ) -> None: + """Set a property of the UB object in a given hkl object. + + Args: + name: the name of the hkl object to access within the store + property: the property of the UB object to set + target_value: the miller indices to set them to + store: accessor to the hkl object + collection: collection within which the hkl object resides + + The property to be set must be a valid vector property. + """ hklcalc = await store.load(name, collection) setattr(hklcalc.ubcalc, property, tuple(target_value.dict().values())) diff --git a/src/diffcalc_API/stores/__init__.py b/src/diffcalc_API/stores/__init__.py index b2ba863..8433adf 100644 --- a/src/diffcalc_API/stores/__init__.py +++ b/src/diffcalc_API/stores/__init__.py @@ -1,3 +1,15 @@ +"""Defines interactions with a persistence layer. + +Diffraction calculations can only be performed on HklCalculation objects, +which must be persisted. + +diffcalc_API.stores.pickling defines a class for persisting them on-file, +diffcalc_API.stores.mongo defines a class for persisting them on mongodb. + +This can be extended to any database or persistence model, so long as it follows +the protocol defined in diffcalc_API.stores.protocol. +""" + from . import pickling, protocol __all__ = ["pickling", "protocol"] diff --git a/src/diffcalc_API/stores/mongo.py b/src/diffcalc_API/stores/mongo.py index 4960429..264d933 100644 --- a/src/diffcalc_API/stores/mongo.py +++ b/src/diffcalc_API/stores/mongo.py @@ -1,3 +1,5 @@ +"""Defines interactions with mongo persistence layer.""" + from typing import Any, Dict, Optional import numpy as np @@ -16,12 +18,21 @@ class ErrorCodes(ErrorCodesBase): + """Persistence error codes. + + This class defines all error codes which can be raised in the retrieval or storage + of HklCalculation objects. + """ + OVERWRITE_ERROR = 405 DOCUMENT_NOT_FOUND_ERROR = 404 class OverwriteError(DiffcalcAPIException): + """Thrown if a HklCalculation object is created with a non-unique name.""" + def __init__(self, name: str) -> None: + """Set detail and status code of the error.""" self.detail = ( f"Document already exists for crystal {name}!" f"\nEither delete via DELETE request to this URL " @@ -31,20 +42,35 @@ def __init__(self, name: str) -> None: class DocumentNotFoundError(DiffcalcAPIException): + """Thrown if the store cannot retrieve a HklCalculation object.""" + def __init__(self, name: str, action: str) -> None: + """Set detail and status code of the error.""" self.detail = f"Document for crystal {name} not found! Cannot {action}." self.status_code = ErrorCodes.DOCUMENT_NOT_FOUND_ERROR class MongoHklCalcStore: + """Class to use mongo db as a persistence layer for the API.""" + def __init__( self, ) -> None: + """Set error codes that could be thrown during method excecution. + + Purely for documentation purposes. + """ self.responses = { code: ALL_RESPONSES[code] for code in np.unique(ErrorCodes.all_codes()) } async def create(self, name: str, collection: Optional[str]) -> None: + """Create a HklCalculation object. + + Args: + name: the unique name to attribute to the object + collection: the collection to store it inside. + """ coll: Collection = database[collection if collection else "default"] if await coll.find_one({"ubcalc.name": name}): @@ -57,6 +83,12 @@ async def create(self, name: str, collection: Optional[str]) -> None: await coll.insert_one(hkl.asdict) async def delete(self, name: str, collection: Optional[str]) -> None: + """Delete a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + """ coll: Collection = database[collection if collection else "default"] result: DeleteResult = await coll.delete_one({"ubcalc.name": name}) if result.deleted_count == 0: @@ -65,10 +97,25 @@ async def delete(self, name: str, collection: Optional[str]) -> None: async def save( self, name: str, hkl: HklCalculation, collection: Optional[str] ) -> None: + """Update a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + """ coll: Collection = database[collection if collection else "default"] await coll.find_one_and_update({"ubcalc.name": name}, {"$set": hkl.asdict}) async def load(self, name: str, collection: Optional[str]) -> HklCalculation: + """Load a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + + Returns: + The HklCalculation object. + """ coll: Collection = database[collection if collection else "default"] hkl_json: Optional[Dict[str, Any]] = await coll.find_one({"ubcalc.name": name}) if not hkl_json: diff --git a/src/diffcalc_API/stores/pickling.py b/src/diffcalc_API/stores/pickling.py index b653395..e91ea4c 100644 --- a/src/diffcalc_API/stores/pickling.py +++ b/src/diffcalc_API/stores/pickling.py @@ -1,3 +1,5 @@ +"""Defines interactions with a file system persistence layer.""" + import pickle from pathlib import Path from typing import Optional @@ -16,12 +18,17 @@ class ErrorCodes(ErrorCodesBase): + """Codes which can be raised in the retrieval/storage of HklCalculation objects.""" + OVERWRITE_ERROR = 405 FILE_NOT_FOUND_ERROR = 404 class OverwriteError(DiffcalcAPIException): + """Thrown if a HklCalculation object is created with a non-unique name.""" + def __init__(self, name): + """Set detail and status code.""" self.detail = ( f"File already exists for crystal {name}!" f"\nEither delete via DELETE request to this URL " @@ -31,7 +38,10 @@ def __init__(self, name): class FileNotFoundError(DiffcalcAPIException): + """Thrown if the store cannot retrieve a HklCalculation object.""" + def __init__(self, name): + """Set detail and status code.""" self.detail = ( f"File for crystal {name} not found." f"\nYou need to post to" @@ -42,14 +52,26 @@ def __init__(self, name): class PicklingHklCalcStore: + """Class to use the file system as a persistence layer for the API.""" + _root_directory: Path = Path(SAVE_PICKLES_FOLDER) def __init__(self) -> None: + """Set error codes that could be thrown during method excecution. + + Purely for documentation purposes. + """ self.responses = { code: ALL_RESPONSES[code] for code in np.unique(ErrorCodes.all_codes()) } async def create(self, name: str, collection: Optional[str]) -> None: + """Create a HklCalculation object. + + Args: + name: the unique name to attribute to the object + collection: the collection to store it inside. + """ pickled_file = ( Path(SAVE_PICKLES_FOLDER) / (collection if collection else "default") / name ) @@ -67,6 +89,12 @@ async def create(self, name: str, collection: Optional[str]) -> None: await self.save(name, hkl, collection) async def delete(self, name: str, collection: Optional[str]) -> None: + """Delete a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + """ pickled_file = ( Path(SAVE_PICKLES_FOLDER) / (collection if collection else "default") / name ) @@ -78,6 +106,12 @@ async def delete(self, name: str, collection: Optional[str]) -> None: async def save( self, name: str, calc: HklCalculation, collection: Optional[str] ) -> None: + """Update a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + """ file_path = ( self._root_directory / (collection if collection else "default") / name ) @@ -85,6 +119,15 @@ async def save( pickle.dump(obj=calc, file=stream) async def load(self, name: str, collection: Optional[str]) -> HklCalculation: + """Load a HklCalculation object. + + Args: + name: the name by which to retrieve the object + collection: the collection inside which it is stored. + + Returns: + The HklCalculation object. + """ file_path = ( self._root_directory / (collection if collection else "default") / name ) diff --git a/src/diffcalc_API/stores/protocol.py b/src/diffcalc_API/stores/protocol.py index 751c1b2..3cc9979 100644 --- a/src/diffcalc_API/stores/protocol.py +++ b/src/diffcalc_API/stores/protocol.py @@ -1,3 +1,9 @@ +"""Protocol for HklCalculation persistence. + +Defines how HklCalculation objects ought to be accessed in the API, and what methods +must be provided by persistence methods for API services to access them. +""" + from importlib import import_module from typing import Any, Dict, Optional, Protocol, Union @@ -5,24 +11,26 @@ class HklCalcStore(Protocol): - """ - Protocol, or interface, for interacting with the Hkl object. - """ + """Protocol for interacting with the HklCalculation object.""" responses: Dict[Union[int, str], Dict[str, Any]] async def create(self, name: str, collection: Optional[str]) -> None: + """Create a HklCalculation object.""" ... async def delete(self, name: str, collection: Optional[str]) -> None: + """Delete a HklCalculation object.""" ... async def save( self, name: str, calc: HklCalculation, collection: Optional[str] ) -> None: + """Save a HklCalculation object.""" ... async def load(self, name: str, collection: Optional[str]) -> HklCalculation: + """Load a HklCalculation object.""" ... @@ -30,12 +38,14 @@ async def load(self, name: str, collection: Optional[str]) -> HklCalculation: def get_store() -> HklCalcStore: + """Retrieve the class which handles HklCalculation objects.""" if STORE is None: raise ValueError() return STORE def setup_store(store_location: str, *args) -> None: + """Allow the server to select which store to use.""" global STORE path, clsname = store_location.rsplit(".", 1) STORE = getattr(import_module(path), clsname)(*args) diff --git a/tests/test_create_delete.py b/tests/test_create_delete.py new file mode 100644 index 0000000..e6a5635 --- /dev/null +++ b/tests/test_create_delete.py @@ -0,0 +1,38 @@ +import pytest +from diffcalc.hkl.calc import HklCalculation +from diffcalc.hkl.constraints import Constraints +from diffcalc.ub.calc import UBCalculation +from fastapi.testclient import TestClient + +from diffcalc_API.server import app +from diffcalc_API.stores.protocol import HklCalcStore, get_store +from tests.conftest import FakeHklCalcStore + +dummy_hkl = HklCalculation(UBCalculation(name="dummy"), Constraints()) + + +def dummy_get_store() -> HklCalcStore: + return FakeHklCalcStore(dummy_hkl) + + +@pytest.fixture(scope="session") +def client() -> TestClient: + app.dependency_overrides[get_store] = dummy_get_store + + return TestClient(app) + + +def test_create(client: TestClient): + response = client.post( + "/test?collection=B07", + ) + + assert response.status_code == 200 + + +def test_delete(client: TestClient): + response = client.delete( + "/test?collection=B07", + ) + + assert response.status_code == 200