From bc3d9eca8cdc624b07a518773745e8219ff78b93 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Tue, 7 Jan 2025 01:01:09 +0000 Subject: [PATCH 1/7] Add context manager and some broken tests --- data-pipeline/data_pipeline/etl_workflow.py | 1 - .../data_pipeline/pipeline_factory.py | 26 +- data-pipeline/data_pipeline/query.py | 29 +++ data-pipeline/poetry.lock | 236 +++++++++++++++++- data-pipeline/pyproject.toml | 1 + data-pipeline/tests/conftest.py | 12 + .../tests/unit/test_pipeline_factory.py | 26 ++ data-pipeline/tests/unit/test_query.py | 22 ++ 8 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 data-pipeline/data_pipeline/query.py create mode 100644 data-pipeline/tests/unit/test_pipeline_factory.py create mode 100644 data-pipeline/tests/unit/test_query.py diff --git a/data-pipeline/data_pipeline/etl_workflow.py b/data-pipeline/data_pipeline/etl_workflow.py index 17cf326..8779728 100644 --- a/data-pipeline/data_pipeline/etl_workflow.py +++ b/data-pipeline/data_pipeline/etl_workflow.py @@ -8,7 +8,6 @@ import pandas as pd -# Configure the logger for this module logger = logging.getLogger(__name__) diff --git a/data-pipeline/data_pipeline/pipeline_factory.py b/data-pipeline/data_pipeline/pipeline_factory.py index a319933..dcd9f44 100644 --- a/data-pipeline/data_pipeline/pipeline_factory.py +++ b/data-pipeline/data_pipeline/pipeline_factory.py @@ -1,12 +1,16 @@ """ -Factory to create and configure the ETL pipeline with injected dependencies. +Factory for creating the pipeline and related tools """ -from collections.abc import Callable +from collections.abc import Callable, Generator +from contextlib import contextmanager from pathlib import Path import pandas as pd from data_pipeline.etl_workflow import run_etl +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.webdriver import WebDriver def create_file_to_file_etl_pipeline( @@ -34,3 +38,21 @@ def etl_fn(input_file: Path, output_folder: Path) -> str: ) return etl_fn + + +@contextmanager +def use_web_driver(target_url) -> Generator[WebDriver, None, None]: + """ + Context manager for a Chrome WebDriver with options set for headless operation. + + Automatically closes the WebDriver when exiting the context. + """ + options = Options() + options.headless = True + + driver = webdriver.Chrome(options=options) + try: + driver.get(target_url) + yield driver + finally: + driver.quit() diff --git a/data-pipeline/data_pipeline/query.py b/data-pipeline/data_pipeline/query.py new file mode 100644 index 0000000..ba7ec32 --- /dev/null +++ b/data-pipeline/data_pipeline/query.py @@ -0,0 +1,29 @@ +""" +Query MIIC for school immunization data +""" + +import logging +from dataclasses import dataclass + +from selenium.webdriver.chrome.webdriver import WebDriver + +logger = logging.getLogger(__name__) + + +@dataclass +class AISRResponse: + """ + Dataclass to hold the response from interactions with AISR. + """ + + is_successful: bool + message: str + + +def login(web_driver: WebDriver, username: str, password: str) -> AISRResponse: + """ + Use Selenium to log into MIIC. + """ + logger.info("Logging into MIIC") + + return AISRResponse(is_successful=False, message="Failed") diff --git a/data-pipeline/poetry.lock b/data-pipeline/poetry.lock index 4f108e6..d07e376 100644 --- a/data-pipeline/poetry.lock +++ b/data-pipeline/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -43,6 +43,25 @@ files = [ {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "black" version = "24.10.0" @@ -98,6 +117,85 @@ files = [ {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -555,6 +653,20 @@ files = [ {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "packaging" version = "24.2" @@ -708,6 +820,17 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.10.3" @@ -867,6 +990,18 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "8.3.4" @@ -951,6 +1086,25 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "selenium" +version = "4.27.1" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "selenium-4.27.1-py3-none-any.whl", hash = "sha256:b89b1f62b5cfe8025868556fe82360d6b649d464f75d2655cb966c8f8447ea18"}, + {file = "selenium-4.27.1.tar.gz", hash = "sha256:5296c425a75ff1b44d0d5199042b36a6d1ef76c04fb775b97b40be739a9caae2"}, +] + +[package.dependencies] +certifi = ">=2021.10.8" +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +typing_extensions = ">=4.9,<5.0" +urllib3 = {version = ">=1.26,<3", extras = ["socks"]} +websocket-client = ">=1.8,<2.0" + [[package]] name = "six" version = "1.17.0" @@ -973,6 +1127,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "starlette" version = "0.41.3" @@ -1001,6 +1166,40 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "trio" +version = "0.28.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.9" +files = [ + {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"}, + {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.11.1" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.7" +files = [ + {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, + {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, +] + +[package.dependencies] +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "types-pytz" version = "2024.2.0.20241003" @@ -1059,6 +1258,9 @@ files = [ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] @@ -1083,7 +1285,37 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "websocket-client" +version = "1.8.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, + {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "09ee255998dd853529dd172841b69f157a3c7f107892cc037220ed3ef36ac39f" +content-hash = "75f449706366bde2b6aeff76a9ce7f974542cbd8079fa8f90c08e7fb9399924e" diff --git a/data-pipeline/pyproject.toml b/data-pipeline/pyproject.toml index 059fd18..f04559c 100644 --- a/data-pipeline/pyproject.toml +++ b/data-pipeline/pyproject.toml @@ -17,6 +17,7 @@ minnesota-immunization-data-pipeline = "data_pipeline.__main__:run" [tool.poetry.dependencies] python = "^3.11" pandas = "^2.2.3" +selenium = "^4.27.1" [tool.poetry.group.dev.dependencies] diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index c13e1ef..3630c51 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -8,6 +8,7 @@ import pytest import uvicorn +from data_pipeline.pipeline_factory import use_web_driver from fastapi import FastAPI from fastapi.responses import HTMLResponse @@ -73,3 +74,14 @@ def run_server(): process.terminate() process.join() + + +@pytest.fixture +def selenium_driver(): + """ + Sets up the driver for the tests. + """ + target_url = "http://127.0.0.1:8000" + + with use_web_driver(target_url) as driver: + yield driver diff --git a/data-pipeline/tests/unit/test_pipeline_factory.py b/data-pipeline/tests/unit/test_pipeline_factory.py new file mode 100644 index 0000000..a5d4166 --- /dev/null +++ b/data-pipeline/tests/unit/test_pipeline_factory.py @@ -0,0 +1,26 @@ +""" +Tests for the construction of the pipeline and tools +""" + +# pylint: disable=missing-function-docstring + +from unittest.mock import MagicMock, patch + +from data_pipeline.pipeline_factory import use_web_driver + + +def test_web_driver_closes_out_of_context(): + """ + Test that the WebDriver's quit method is called when exiting the context. + """ + target_url = "https://example.com" + + with patch("selenium.webdriver.Chrome") as mock_web_driver: + mock_driver = MagicMock() + mock_web_driver.return_value = mock_driver + + with use_web_driver(target_url): + mock_web_driver.assert_called_once() + mock_driver.get.assert_called_once_with(target_url) + + mock_driver.quit.assert_called_once() diff --git a/data-pipeline/tests/unit/test_query.py b/data-pipeline/tests/unit/test_query.py new file mode 100644 index 0000000..ed86759 --- /dev/null +++ b/data-pipeline/tests/unit/test_query.py @@ -0,0 +1,22 @@ +""" +Tests for MIIC quering +""" + +# pylint: disable=missing-function-docstring + +from data_pipeline.pipeline_factory import use_web_driver +from data_pipeline.query import login + +TEST_USERNAME = "test_user" +TEST_PASSWORD = "test_password" +TEST_ROW_ID = "test_row_id" + + +def test_login_return_row_element(fastapi_server): + with use_web_driver(fastapi_server) as driver: + result = login( + web_driver=driver, username=TEST_USERNAME, password=TEST_PASSWORD + ) + + assert result.message == "Logged in", "Message should be 'Logged in'" + assert result.is_successful, "Log in should be successful" From 53b37b1019556ff8e7d28db5113fd3876104364e Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 00:52:04 +0000 Subject: [PATCH 2/7] Pass tests and include commented out code --- data-pipeline/data_pipeline/aisr.py | 69 ++++ .../data_pipeline/pipeline_factory.py | 21 -- data-pipeline/data_pipeline/query.py | 29 -- data-pipeline/poetry.lock | 351 +++++++----------- data-pipeline/pyproject.toml | 3 +- data-pipeline/tests/conftest.py | 60 ++- data-pipeline/tests/unit/test_aisr.py | 84 +++++ .../tests/unit/test_pipeline_factory.py | 26 -- data-pipeline/tests/unit/test_query.py | 22 -- 9 files changed, 330 insertions(+), 335 deletions(-) create mode 100644 data-pipeline/data_pipeline/aisr.py delete mode 100644 data-pipeline/data_pipeline/query.py create mode 100644 data-pipeline/tests/unit/test_aisr.py delete mode 100644 data-pipeline/tests/unit/test_pipeline_factory.py delete mode 100644 data-pipeline/tests/unit/test_query.py diff --git a/data-pipeline/data_pipeline/aisr.py b/data-pipeline/data_pipeline/aisr.py new file mode 100644 index 0000000..441ac5f --- /dev/null +++ b/data-pipeline/data_pipeline/aisr.py @@ -0,0 +1,69 @@ +""" +Query MIIC for school immunization data +""" + +import logging +import uuid +from dataclasses import dataclass +from typing import Tuple +from urllib.parse import parse_qs, quote, urlparse + +import requests +from bs4 import BeautifulSoup + +logger = logging.getLogger(__name__) + + +def _get_session_and_tab(s: requests.Session, auth_realm_url: str) -> Tuple[str, str]: + """ + The session and tab are needed to authenticate with AISR. + """ + state = uuid.uuid4() + nonce = uuid.uuid4() + + # pylint: disable-next=line-too-long + url = f"{auth_realm_url}/protocol/openid-connect/auth?client_id=aisr-app&redirect_uri=https%3A%2F%2Faisr.web.health.state.mn.us%2Fhome&state={state}&response_mode=fragment&response_type=code&scope=openid&nonce={nonce}" + + response = s.request("GET", url, headers={}, data={}) + soup = BeautifulSoup(response.content, "html.parser") + form_element = soup.find("form", id="kc-form-login") + action_url = form_element.get("action") if form_element else None + parsed_url = urlparse(action_url) + query_dict = parse_qs(parsed_url.query) + + return query_dict["session_code"][0], query_dict["tab_id"][0] + + +@dataclass +class AISRResponse: + """ + Dataclass to hold the response from interactions with AISR. + """ + + is_successful: bool + message: str + + +def login( + session: requests.Session, auth_realm_url: str, username: str, password: str +) -> AISRResponse: + """ + Login with AISR. + """ + logger.info("Logging into MIIC") + session_code, tab_id = _get_session_and_tab(session, auth_realm_url) + + # pylint: disable-next=line-too-long + url = f"{auth_realm_url}/login-actions/authenticate?session_code={session_code}&execution={uuid.uuid4()}&client_id=aisr-app&tab_id={tab_id}" + + payload = f"password={quote(password)}&username={username}" + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + response = session.request("POST", url, headers=headers, data=payload) + + if response.status_code == 200: + logger.info("Logged in successfully") + return AISRResponse(is_successful=True, message="Logged in successfully") + + logger.error("Login failed") + return AISRResponse(is_successful=False, message="Failed to log in") diff --git a/data-pipeline/data_pipeline/pipeline_factory.py b/data-pipeline/data_pipeline/pipeline_factory.py index dcd9f44..2c20dc9 100644 --- a/data-pipeline/data_pipeline/pipeline_factory.py +++ b/data-pipeline/data_pipeline/pipeline_factory.py @@ -8,9 +8,6 @@ import pandas as pd from data_pipeline.etl_workflow import run_etl -from selenium import webdriver -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.chrome.webdriver import WebDriver def create_file_to_file_etl_pipeline( @@ -38,21 +35,3 @@ def etl_fn(input_file: Path, output_folder: Path) -> str: ) return etl_fn - - -@contextmanager -def use_web_driver(target_url) -> Generator[WebDriver, None, None]: - """ - Context manager for a Chrome WebDriver with options set for headless operation. - - Automatically closes the WebDriver when exiting the context. - """ - options = Options() - options.headless = True - - driver = webdriver.Chrome(options=options) - try: - driver.get(target_url) - yield driver - finally: - driver.quit() diff --git a/data-pipeline/data_pipeline/query.py b/data-pipeline/data_pipeline/query.py deleted file mode 100644 index ba7ec32..0000000 --- a/data-pipeline/data_pipeline/query.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Query MIIC for school immunization data -""" - -import logging -from dataclasses import dataclass - -from selenium.webdriver.chrome.webdriver import WebDriver - -logger = logging.getLogger(__name__) - - -@dataclass -class AISRResponse: - """ - Dataclass to hold the response from interactions with AISR. - """ - - is_successful: bool - message: str - - -def login(web_driver: WebDriver, username: str, password: str) -> AISRResponse: - """ - Use Selenium to log into MIIC. - """ - logger.info("Logging into MIIC") - - return AISRResponse(is_successful=False, message="Failed") diff --git a/data-pipeline/poetry.lock b/data-pipeline/poetry.lock index d07e376..670e4fd 100644 --- a/data-pipeline/poetry.lock +++ b/data-pipeline/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,8 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +19,8 @@ version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, @@ -38,29 +42,35 @@ version = "3.3.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] [[package]] -name = "attrs" -version = "24.3.0" -description = "Classes Without Boilerplate" +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6.0" +groups = ["main"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] +[package.dependencies] +soupsieve = ">1.2" + [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] [[package]] name = "black" @@ -68,6 +78,8 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -112,96 +124,21 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "charset-normalizer" version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, @@ -316,6 +253,8 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev", "test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -330,10 +269,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "test"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "(sys_platform == \"win32\" or platform_system == \"Windows\") and (python_version >= \"3.12\" or python_version == \"3.11\")", test = "(platform_system == \"Windows\" or sys_platform == \"win32\") and (python_version >= \"3.12\" or python_version == \"3.11\")"} [[package]] name = "coverage" @@ -341,6 +282,8 @@ version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, @@ -415,6 +358,8 @@ version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, @@ -430,6 +375,8 @@ version = "33.1.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "Faker-33.1.0-py3-none-any.whl", hash = "sha256:d30c5f0e2796b8970de68978365247657486eb0311c5abe88d0b895b68dff05d"}, {file = "faker-33.1.0.tar.gz", hash = "sha256:1c925fc0e86a51fc46648b504078c88d0cd48da1da2595c4e712841cab43a1e4"}, @@ -445,6 +392,8 @@ version = "0.115.6" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, @@ -465,6 +414,8 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -476,6 +427,8 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -490,6 +443,8 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -501,6 +456,8 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -515,6 +472,8 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -526,6 +485,8 @@ version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, @@ -584,6 +545,8 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -595,6 +558,8 @@ version = "2.2.0" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" +groups = ["main", "dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, @@ -653,26 +618,14 @@ files = [ {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, ] -[[package]] -name = "outcome" -version = "1.3.0.post0" -description = "Capture the outcome of Python function calls." -optional = false -python-versions = ">=3.7" -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -684,6 +637,8 @@ version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -769,6 +724,8 @@ version = "2.2.3.241126" description = "Type annotations for pandas" optional = false python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pandas_stubs-2.2.3.241126-py3-none-any.whl", hash = "sha256:74aa79c167af374fe97068acc90776c0ebec5266a6e5c69fe11e9c2cf51f2267"}, {file = "pandas_stubs-2.2.3.241126.tar.gz", hash = "sha256:cf819383c6d9ae7d4dabf34cd47e1e45525bb2f312e6ad2939c2c204cb708acd"}, @@ -784,6 +741,8 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -795,6 +754,8 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -811,6 +772,8 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -820,23 +783,14 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - [[package]] name = "pydantic" version = "2.10.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, @@ -857,6 +811,8 @@ version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, @@ -969,6 +925,8 @@ version = "3.3.3" description = "python code static checker" optional = false python-versions = ">=3.9.0" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, @@ -990,24 +948,14 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - [[package]] name = "pytest" version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1028,6 +976,8 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -1046,6 +996,8 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1054,12 +1006,27 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-multipart" +version = "0.0.20" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + [[package]] name = "pytz" version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1071,6 +1038,8 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1086,31 +1055,14 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "selenium" -version = "4.27.1" -description = "Official Python bindings for Selenium WebDriver" -optional = false -python-versions = ">=3.8" -files = [ - {file = "selenium-4.27.1-py3-none-any.whl", hash = "sha256:b89b1f62b5cfe8025868556fe82360d6b649d464f75d2655cb966c8f8447ea18"}, - {file = "selenium-4.27.1.tar.gz", hash = "sha256:5296c425a75ff1b44d0d5199042b36a6d1ef76c04fb775b97b40be739a9caae2"}, -] - -[package.dependencies] -certifi = ">=2021.10.8" -trio = ">=0.17,<1.0" -trio-websocket = ">=0.9,<1.0" -typing_extensions = ">=4.9,<5.0" -urllib3 = {version = ">=1.26,<3", extras = ["socks"]} -websocket-client = ">=1.8,<2.0" - [[package]] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1122,20 +1074,24 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = "*" +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] @@ -1144,6 +1100,8 @@ version = "0.41.3" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, @@ -1161,51 +1119,21 @@ version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] -[[package]] -name = "trio" -version = "0.28.0" -description = "A friendly Python library for async concurrency and I/O" -optional = false -python-versions = ">=3.9" -files = [ - {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"}, - {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"}, -] - -[package.dependencies] -attrs = ">=23.2.0" -cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} -idna = "*" -outcome = "*" -sniffio = ">=1.3.0" -sortedcontainers = "*" - -[[package]] -name = "trio-websocket" -version = "0.11.1" -description = "WebSocket library for Trio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, - {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, -] - -[package.dependencies] -trio = ">=0.11" -wsproto = ">=0.14" - [[package]] name = "types-pytz" version = "2024.2.0.20241003" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"}, {file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"}, @@ -1217,6 +1145,8 @@ version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, @@ -1231,6 +1161,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["dev", "test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1242,6 +1174,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -1253,14 +1187,13 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] -[package.dependencies] -pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} - [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] @@ -1273,6 +1206,8 @@ version = "0.34.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" +groups = ["test"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -1285,37 +1220,7 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "wsproto" -version = "1.2.0" -description = "WebSockets state-machine based protocol implementation" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] - -[package.dependencies] -h11 = ">=0.9.0,<1" - [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "75f449706366bde2b6aeff76a9ce7f974542cbd8079fa8f90c08e7fb9399924e" +content-hash = "c6d0ad3d655884e7e32d7cc606059ba51f80a946d019fbbe080f68d529148640" diff --git a/data-pipeline/pyproject.toml b/data-pipeline/pyproject.toml index f04559c..e299069 100644 --- a/data-pipeline/pyproject.toml +++ b/data-pipeline/pyproject.toml @@ -17,7 +17,7 @@ minnesota-immunization-data-pipeline = "data_pipeline.__main__:run" [tool.poetry.dependencies] python = "^3.11" pandas = "^2.2.3" -selenium = "^4.27.1" +beautifulsoup4 = "^4.12.3" [tool.poetry.group.dev.dependencies] @@ -35,6 +35,7 @@ types-requests = "^2.32.0.20241016" fastapi = "^0.115.6" uvicorn = "^0.34.0" pytest-cov = "^6.0.0" +python-multipart = "^0.0.20" [build-system] requires = ["poetry-core"] diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index 3630c51..ce59604 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -5,12 +5,13 @@ import time from multiprocessing import Process from pathlib import Path +from urllib.parse import urlencode import pytest import uvicorn from data_pipeline.pipeline_factory import use_web_driver -from fastapi import FastAPI -from fastapi.responses import HTMLResponse +from fastapi import FastAPI, Form +from fastapi.responses import HTMLResponse, JSONResponse @pytest.fixture(name="folders") @@ -61,6 +62,50 @@ async def root(): """ + @app.get("/protocol/openid-connect/auth", response_class=HTMLResponse) + async def oidc_auth(): + """ + Simulates an authentication endpoint. Returns an HTML page with a form + that includes the required `session_code` and `tab_id`. + """ + form_action_url = f"/protocol/openid-connect/login?{urlencode({ + 'session_code': 'mock-session-code', + 'tab_id': 'mock-tab-id' + })}" + + return f""" + + + + + Login + + +
+ + + +
+ + + """ + + @app.post("/login-actions/authenticate") + async def authenticate(username: str = Form(...), password: str = Form(...)): + """ + Simulates the login authentication endpoint. Validates username and password and returns + a response indicating success or failure. + """ + if username == "test_user" and password == "test_password": + return JSONResponse( + content={"message": "Login successful", "is_successful": True}, + status_code=200, + ) + return JSONResponse( + content={"message": "Invalid credentials", "is_successful": False}, + status_code=401, + ) + def run_server(): uvicorn.run(app, host="127.0.0.1", port=8000) @@ -74,14 +119,3 @@ def run_server(): process.terminate() process.join() - - -@pytest.fixture -def selenium_driver(): - """ - Sets up the driver for the tests. - """ - target_url = "http://127.0.0.1:8000" - - with use_web_driver(target_url) as driver: - yield driver diff --git a/data-pipeline/tests/unit/test_aisr.py b/data-pipeline/tests/unit/test_aisr.py new file mode 100644 index 0000000..f7da01b --- /dev/null +++ b/data-pipeline/tests/unit/test_aisr.py @@ -0,0 +1,84 @@ +""" +Tests for interacting with AISR +""" + +# pylint: disable=missing-function-docstring + +import uuid +from getpass import getpass +from typing import Tuple +from urllib.parse import parse_qs, quote, urlparse + +import requests +from bs4 import BeautifulSoup +from data_pipeline.aisr import login + +TEST_USERNAME = "test_user" +TEST_PASSWORD = "test_password" +TEST_ROW_ID = "test_row_id" + + +# def get_session_and_tab(s: requests.Session) -> Tuple[str, str]: +# state = uuid.uuid4() +# nonce = uuid.uuid4() + +# url = f"https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/protocol/openid-connect/auth?client_id=aisr-app&redirect_uri=https%3A%2F%2Faisr.web.health.state.mn.us%2Fhome&state={state}&response_mode=fragment&response_type=code&scope=openid&nonce={nonce}" + +# payload = {} +# headers = {} +# response = s.request("GET", url, headers=headers, data=payload) +# soup = BeautifulSoup(response.content, "html.parser") +# form_element = soup.find("form", id="kc-form-login") +# action_url = form_element.get("action") if form_element else None +# parsed_url = urlparse(action_url) +# query_dict = parse_qs(parsed_url.query) + +# return query_dict["session_code"][0], query_dict["tab_id"][0] + + +# def authenticate(s: requests.Session): +# url = f"https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/login-actions/authenticate?session_code={session_code}&execution=084dee30-925f-4a8f-829d-7a372e38d0de&client_id=aisr-app&tab_id={tab_id}" + +# password = getpass() +# payload = f"password={quote(password)}&username=dave.sandum%40isd197.org" +# headers = {"Content-Type": "application/x-www-form-urlencoded"} + +# response = s.request("POST", url, headers=headers, data=payload) +# print(response.text) + + +def logout(s: requests.Session): + url = "https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/protocol/openid-connect/logout?client_id=aisr-app" + s.request("GET", url, headers={}, data={}) + + +# with requests.Session() as session: +# session_code, tab_id = get_session_and_tab(session) +# authenticate(session) +# print(session.cookies) +# OPTIONS_URL = "https://aisr-api.web.health.state.mn.us/school/list/public" + +# res = session.request("GET", OPTIONS_URL, headers={}, data={}) +# print(res.text) + + +def test_login_successful(fastapi_server): + with requests.Session() as local_session: + result = login( + session=local_session, + auth_realm_url=fastapi_server, + username=TEST_USERNAME, + password=TEST_PASSWORD, + ) + assert result.is_successful, "Log in should be successful" + + +def test_login_failure(fastapi_server): + with requests.Session() as local_session: + result = login( + session=local_session, + auth_realm_url=fastapi_server, + username=TEST_USERNAME, + password="wrong_password", + ) + assert not result.is_successful, "Log in should fail with incorrect password" diff --git a/data-pipeline/tests/unit/test_pipeline_factory.py b/data-pipeline/tests/unit/test_pipeline_factory.py deleted file mode 100644 index a5d4166..0000000 --- a/data-pipeline/tests/unit/test_pipeline_factory.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Tests for the construction of the pipeline and tools -""" - -# pylint: disable=missing-function-docstring - -from unittest.mock import MagicMock, patch - -from data_pipeline.pipeline_factory import use_web_driver - - -def test_web_driver_closes_out_of_context(): - """ - Test that the WebDriver's quit method is called when exiting the context. - """ - target_url = "https://example.com" - - with patch("selenium.webdriver.Chrome") as mock_web_driver: - mock_driver = MagicMock() - mock_web_driver.return_value = mock_driver - - with use_web_driver(target_url): - mock_web_driver.assert_called_once() - mock_driver.get.assert_called_once_with(target_url) - - mock_driver.quit.assert_called_once() diff --git a/data-pipeline/tests/unit/test_query.py b/data-pipeline/tests/unit/test_query.py deleted file mode 100644 index ed86759..0000000 --- a/data-pipeline/tests/unit/test_query.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Tests for MIIC quering -""" - -# pylint: disable=missing-function-docstring - -from data_pipeline.pipeline_factory import use_web_driver -from data_pipeline.query import login - -TEST_USERNAME = "test_user" -TEST_PASSWORD = "test_password" -TEST_ROW_ID = "test_row_id" - - -def test_login_return_row_element(fastapi_server): - with use_web_driver(fastapi_server) as driver: - result = login( - web_driver=driver, username=TEST_USERNAME, password=TEST_PASSWORD - ) - - assert result.message == "Logged in", "Message should be 'Logged in'" - assert result.is_successful, "Log in should be successful" From b2537b179ca9b1dc3155b00d0c369ab93cd0b070 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 01:09:15 +0000 Subject: [PATCH 3/7] Cookies are successfully cleared after logout --- data-pipeline/data_pipeline/aisr.py | 8 ++++ data-pipeline/tests/conftest.py | 56 +++++++++++++++++---------- data-pipeline/tests/unit/test_aisr.py | 40 ++++++++++++------- 3 files changed, 71 insertions(+), 33 deletions(-) diff --git a/data-pipeline/data_pipeline/aisr.py b/data-pipeline/data_pipeline/aisr.py index 441ac5f..a89bd07 100644 --- a/data-pipeline/data_pipeline/aisr.py +++ b/data-pipeline/data_pipeline/aisr.py @@ -67,3 +67,11 @@ def login( logger.error("Login failed") return AISRResponse(is_successful=False, message="Failed to log in") + + +def logout(session: requests.Session, auth_realm_url: str): + """ + Log out of AISR. + """ + url = f"{auth_realm_url}/protocol/openid-connect/logout?client_id=aisr-app" + session.request("GET", url, headers={}, data={}) diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index ce59604..4fbd7c4 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -9,7 +9,6 @@ import pytest import uvicorn -from data_pipeline.pipeline_factory import use_web_driver from fastapi import FastAPI, Form from fastapi.responses import HTMLResponse, JSONResponse @@ -47,22 +46,10 @@ def fastapi_server(): """ app = FastAPI() - @app.get("/", response_class=HTMLResponse) - async def root(): - return """ - - - - - Test Page - - -

Hello Minnesota!

- - - """ - - @app.get("/protocol/openid-connect/auth", response_class=HTMLResponse) + @app.get( + "/auth/realms/idepc-aisr-realm/protocol/openid-connect/auth", + response_class=HTMLResponse, + ) async def oidc_auth(): """ Simulates an authentication endpoint. Returns an HTML page with a form @@ -90,22 +77,51 @@ async def oidc_auth(): """ - @app.post("/login-actions/authenticate") + @app.post("/auth/realms/idepc-aisr-realm/login-actions/authenticate") async def authenticate(username: str = Form(...), password: str = Form(...)): """ Simulates the login authentication endpoint. Validates username and password and returns - a response indicating success or failure. + a response with a cookie indicating success or failure. """ if username == "test_user" and password == "test_password": - return JSONResponse( + response = JSONResponse( content={"message": "Login successful", "is_successful": True}, status_code=200, ) + response.set_cookie( + key="KEYCLOAK_IDENTITY", + value="mocked-identity-token", + httponly=True, + secure=True, + ) + return response return JSONResponse( content={"message": "Invalid credentials", "is_successful": False}, status_code=401, ) + @app.get("/auth/realms/idepc-aisr-realm/protocol/openid-connect/logout") + async def logout(client_id: str): + """ + Simulates the logout endpoint. Removes the KEYCLOAK_IDENTITY cookie. + """ + if client_id == "aisr-app": + response = JSONResponse( + content={"message": "Logout successful"}, + status_code=200, + ) + response.delete_cookie( + key="KEYCLOAK_IDENTITY", + httponly=True, # Ensure this matches the original cookie settings + secure=True, # Ensure this matches the original cookie settings + ) + return response + + return JSONResponse( + content={"message": "Invalid client_id", "is_successful": False}, + status_code=400, + ) + def run_server(): uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/data-pipeline/tests/unit/test_aisr.py b/data-pipeline/tests/unit/test_aisr.py index f7da01b..cbb6869 100644 --- a/data-pipeline/tests/unit/test_aisr.py +++ b/data-pipeline/tests/unit/test_aisr.py @@ -4,14 +4,15 @@ # pylint: disable=missing-function-docstring -import uuid -from getpass import getpass -from typing import Tuple -from urllib.parse import parse_qs, quote, urlparse +# import uuid +# from getpass import getpass +# from typing import Tuple +# from urllib.parse import parse_qs, quote, urlparse import requests -from bs4 import BeautifulSoup -from data_pipeline.aisr import login + +# from bs4 import BeautifulSoup +from data_pipeline.aisr import login, logout TEST_USERNAME = "test_user" TEST_PASSWORD = "test_password" @@ -47,11 +48,6 @@ # print(response.text) -def logout(s: requests.Session): - url = "https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/protocol/openid-connect/logout?client_id=aisr-app" - s.request("GET", url, headers={}, data={}) - - # with requests.Session() as session: # session_code, tab_id = get_session_and_tab(session) # authenticate(session) @@ -63,10 +59,12 @@ def logout(s: requests.Session): def test_login_successful(fastapi_server): + test_realm_url = f"{fastapi_server}/auth/realms/idepc-aisr-realm" + with requests.Session() as local_session: result = login( session=local_session, - auth_realm_url=fastapi_server, + auth_realm_url=test_realm_url, username=TEST_USERNAME, password=TEST_PASSWORD, ) @@ -74,11 +72,27 @@ def test_login_successful(fastapi_server): def test_login_failure(fastapi_server): + test_realm_url = f"{fastapi_server}/auth/realms/idepc-aisr-realm" + with requests.Session() as local_session: result = login( session=local_session, - auth_realm_url=fastapi_server, + auth_realm_url=test_realm_url, username=TEST_USERNAME, password="wrong_password", ) assert not result.is_successful, "Log in should fail with incorrect password" + + +def test_logout_successful(fastapi_server): + test_realm_url = f"{fastapi_server}/auth/realms/idepc-aisr-realm" + + with requests.Session() as local_session: + login( + session=local_session, + auth_realm_url=test_realm_url, + username=TEST_USERNAME, + password=TEST_PASSWORD, + ) + logout(local_session, test_realm_url) + assert not local_session.cookies, "Session cookies should be cleared after logout" From 6c59f0aa799c86cf50a2a8ae1e917d78b1eab120 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 05:26:33 +0000 Subject: [PATCH 4/7] Update log in and tests --- data-pipeline/data_pipeline/aisr.py | 47 ++++++++++++++----- data-pipeline/poetry.lock | 31 +++++++++++- data-pipeline/pyproject.toml | 1 + data-pipeline/tests/conftest.py | 2 +- .../tests/integration/test_test_server.py | 18 ------- data-pipeline/tests/unit/test_aisr.py | 46 ------------------ 6 files changed, 68 insertions(+), 77 deletions(-) delete mode 100644 data-pipeline/tests/integration/test_test_server.py diff --git a/data-pipeline/data_pipeline/aisr.py b/data-pipeline/data_pipeline/aisr.py index a89bd07..0bea0ea 100644 --- a/data-pipeline/data_pipeline/aisr.py +++ b/data-pipeline/data_pipeline/aisr.py @@ -2,6 +2,7 @@ Query MIIC for school immunization data """ +import getpass import logging import uuid from dataclasses import dataclass @@ -9,12 +10,14 @@ from urllib.parse import parse_qs, quote, urlparse import requests -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, Tag logger = logging.getLogger(__name__) -def _get_session_and_tab(s: requests.Session, auth_realm_url: str) -> Tuple[str, str]: +def _get_session_and_tab( + session: requests.Session, auth_realm_url: str +) -> Tuple[str, str]: """ The session and tab are needed to authenticate with AISR. """ @@ -24,14 +27,19 @@ def _get_session_and_tab(s: requests.Session, auth_realm_url: str) -> Tuple[str, # pylint: disable-next=line-too-long url = f"{auth_realm_url}/protocol/openid-connect/auth?client_id=aisr-app&redirect_uri=https%3A%2F%2Faisr.web.health.state.mn.us%2Fhome&state={state}&response_mode=fragment&response_type=code&scope=openid&nonce={nonce}" - response = s.request("GET", url, headers={}, data={}) + response = session.request("GET", url, headers={}, data={}) soup = BeautifulSoup(response.content, "html.parser") form_element = soup.find("form", id="kc-form-login") - action_url = form_element.get("action") if form_element else None - parsed_url = urlparse(action_url) - query_dict = parse_qs(parsed_url.query) - return query_dict["session_code"][0], query_dict["tab_id"][0] + if isinstance(form_element, Tag): + action_url = form_element.get("action") + if isinstance(action_url, str): + parsed_url = urlparse(action_url) + query_dict = parse_qs(parsed_url.query) + print(query_dict["session_code"][0], query_dict["tab_id"][0]) + return query_dict["session_code"][0], query_dict["tab_id"][0] + raise ValueError("The action URL is not a valid string.") + raise ValueError("Login form not found or is not a valid HTML form element.") @dataclass @@ -54,19 +62,22 @@ def login( session_code, tab_id = _get_session_and_tab(session, auth_realm_url) # pylint: disable-next=line-too-long - url = f"{auth_realm_url}/login-actions/authenticate?session_code={session_code}&execution={uuid.uuid4()}&client_id=aisr-app&tab_id={tab_id}" + url = f"{auth_realm_url}/login-actions/authenticate?session_code={session_code}&execution=084dee30-925f-4a8f-829d-7a372e38d0de&client_id=aisr-app&tab_id={tab_id}" payload = f"password={quote(password)}&username={username}" headers = {"Content-Type": "application/x-www-form-urlencoded"} response = session.request("POST", url, headers=headers, data=payload) - if response.status_code == 200: + if response.status_code == 200 and "KEYCLOAK_IDENTITY" in response.cookies: logger.info("Logged in successfully") return AISRResponse(is_successful=True, message="Logged in successfully") - logger.error("Login failed") - return AISRResponse(is_successful=False, message="Failed to log in") + logger.error("Login failed or KEYCLOAK_IDENTITY cookie is missing") + return AISRResponse( + is_successful=False, + message="Login failed or KEYCLOAK_IDENTITY cookie is missing", + ) def logout(session: requests.Session, auth_realm_url: str): @@ -75,3 +86,17 @@ def logout(session: requests.Session, auth_realm_url: str): """ url = f"{auth_realm_url}/protocol/openid-connect/logout?client_id=aisr-app" session.request("GET", url, headers={}, data={}) + + +# with requests.Session() as s: +# login( +# s, +# "https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm", +# "dave.sandum@isd197.org", +# getpass.getpass(), +# ) +# print(s.cookies) +# OPTIONS_URL = "https://aisr-api.web.health.state.mn.us/school/list/public" + +# res = s.request("GET", OPTIONS_URL, headers={}, data={}) +# print(res.text) diff --git a/data-pipeline/poetry.lock b/data-pipeline/poetry.lock index 670e4fd..49e84e0 100644 --- a/data-pipeline/poetry.lock +++ b/data-pipeline/poetry.lock @@ -1126,6 +1126,35 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[[package]] +name = "types-beautifulsoup4" +version = "4.12.0.20241020" +description = "Typing stubs for beautifulsoup4" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" +files = [ + {file = "types-beautifulsoup4-4.12.0.20241020.tar.gz", hash = "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059"}, + {file = "types_beautifulsoup4-4.12.0.20241020-py3-none-any.whl", hash = "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30"}, +] + +[package.dependencies] +types-html5lib = "*" + +[[package]] +name = "types-html5lib" +version = "1.1.11.20241018" +description = "Typing stubs for html5lib" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.12\" or python_version == \"3.11\"" +files = [ + {file = "types-html5lib-1.1.11.20241018.tar.gz", hash = "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa"}, + {file = "types_html5lib-1.1.11.20241018-py3-none-any.whl", hash = "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403"}, +] + [[package]] name = "types-pytz" version = "2024.2.0.20241003" @@ -1223,4 +1252,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "c6d0ad3d655884e7e32d7cc606059ba51f80a946d019fbbe080f68d529148640" +content-hash = "ddf4abd75cff2f7592df34a393f6697ccc153d350ebefbab16abb6280ef4d581" diff --git a/data-pipeline/pyproject.toml b/data-pipeline/pyproject.toml index e299069..f8d903e 100644 --- a/data-pipeline/pyproject.toml +++ b/data-pipeline/pyproject.toml @@ -26,6 +26,7 @@ mypy = "^1.13.0" pylint = "^3.3.1" black = "^24.10.0" pandas-stubs = "^2.2.3.241009" +types-beautifulsoup4 = "^4.12.0.20241020" [tool.poetry.group.test.dependencies] pytest = "^8.3.3" diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index 4fbd7c4..0dc165f 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -97,7 +97,7 @@ async def authenticate(username: str = Form(...), password: str = Form(...)): return response return JSONResponse( content={"message": "Invalid credentials", "is_successful": False}, - status_code=401, + status_code=200, ) @app.get("/auth/realms/idepc-aisr-realm/protocol/openid-connect/logout") diff --git a/data-pipeline/tests/integration/test_test_server.py b/data-pipeline/tests/integration/test_test_server.py deleted file mode 100644 index 8c25997..0000000 --- a/data-pipeline/tests/integration/test_test_server.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Simple test to see if I can get -A server set ip -""" - -import requests - - -def test_fastapi_server_responds(fastapi_server): - """ - Test that the FastAPI server responds correctly with the expected HTML. - """ - response = requests.get(fastapi_server, timeout=10) - - assert response.status_code == 200 - - expected_content = "

Hello Minnesota!

" - assert expected_content in response.text diff --git a/data-pipeline/tests/unit/test_aisr.py b/data-pipeline/tests/unit/test_aisr.py index cbb6869..73f5d62 100644 --- a/data-pipeline/tests/unit/test_aisr.py +++ b/data-pipeline/tests/unit/test_aisr.py @@ -4,14 +4,7 @@ # pylint: disable=missing-function-docstring -# import uuid -# from getpass import getpass -# from typing import Tuple -# from urllib.parse import parse_qs, quote, urlparse - import requests - -# from bs4 import BeautifulSoup from data_pipeline.aisr import login, logout TEST_USERNAME = "test_user" @@ -19,45 +12,6 @@ TEST_ROW_ID = "test_row_id" -# def get_session_and_tab(s: requests.Session) -> Tuple[str, str]: -# state = uuid.uuid4() -# nonce = uuid.uuid4() - -# url = f"https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/protocol/openid-connect/auth?client_id=aisr-app&redirect_uri=https%3A%2F%2Faisr.web.health.state.mn.us%2Fhome&state={state}&response_mode=fragment&response_type=code&scope=openid&nonce={nonce}" - -# payload = {} -# headers = {} -# response = s.request("GET", url, headers=headers, data=payload) -# soup = BeautifulSoup(response.content, "html.parser") -# form_element = soup.find("form", id="kc-form-login") -# action_url = form_element.get("action") if form_element else None -# parsed_url = urlparse(action_url) -# query_dict = parse_qs(parsed_url.query) - -# return query_dict["session_code"][0], query_dict["tab_id"][0] - - -# def authenticate(s: requests.Session): -# url = f"https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm/login-actions/authenticate?session_code={session_code}&execution=084dee30-925f-4a8f-829d-7a372e38d0de&client_id=aisr-app&tab_id={tab_id}" - -# password = getpass() -# payload = f"password={quote(password)}&username=dave.sandum%40isd197.org" -# headers = {"Content-Type": "application/x-www-form-urlencoded"} - -# response = s.request("POST", url, headers=headers, data=payload) -# print(response.text) - - -# with requests.Session() as session: -# session_code, tab_id = get_session_and_tab(session) -# authenticate(session) -# print(session.cookies) -# OPTIONS_URL = "https://aisr-api.web.health.state.mn.us/school/list/public" - -# res = session.request("GET", OPTIONS_URL, headers={}, data={}) -# print(res.text) - - def test_login_successful(fastapi_server): test_realm_url = f"{fastapi_server}/auth/realms/idepc-aisr-realm" From 1796872feaa824072073412bb209a63d88cdaf45 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 05:31:22 +0000 Subject: [PATCH 5/7] Clean up log in code --- data-pipeline/data_pipeline/aisr.py | 15 --------------- data-pipeline/data_pipeline/pipeline_factory.py | 3 +-- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/data-pipeline/data_pipeline/aisr.py b/data-pipeline/data_pipeline/aisr.py index 0bea0ea..998fe8c 100644 --- a/data-pipeline/data_pipeline/aisr.py +++ b/data-pipeline/data_pipeline/aisr.py @@ -2,7 +2,6 @@ Query MIIC for school immunization data """ -import getpass import logging import uuid from dataclasses import dataclass @@ -86,17 +85,3 @@ def logout(session: requests.Session, auth_realm_url: str): """ url = f"{auth_realm_url}/protocol/openid-connect/logout?client_id=aisr-app" session.request("GET", url, headers={}, data={}) - - -# with requests.Session() as s: -# login( -# s, -# "https://authenticator4.web.health.state.mn.us/auth/realms/idepc-aisr-realm", -# "dave.sandum@isd197.org", -# getpass.getpass(), -# ) -# print(s.cookies) -# OPTIONS_URL = "https://aisr-api.web.health.state.mn.us/school/list/public" - -# res = s.request("GET", OPTIONS_URL, headers={}, data={}) -# print(res.text) diff --git a/data-pipeline/data_pipeline/pipeline_factory.py b/data-pipeline/data_pipeline/pipeline_factory.py index 2c20dc9..1b381f2 100644 --- a/data-pipeline/data_pipeline/pipeline_factory.py +++ b/data-pipeline/data_pipeline/pipeline_factory.py @@ -2,8 +2,7 @@ Factory for creating the pipeline and related tools """ -from collections.abc import Callable, Generator -from contextlib import contextmanager +from collections.abc import Callable from pathlib import Path import pandas as pd From 56c2dcf5eeb187783143fc93350815ea6ae85db9 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 05:42:54 +0000 Subject: [PATCH 6/7] Code cleanup --- data-pipeline/data_pipeline/aisr.py | 14 +++++++++----- data-pipeline/tests/conftest.py | 5 ++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/data-pipeline/data_pipeline/aisr.py b/data-pipeline/data_pipeline/aisr.py index 998fe8c..a89a577 100644 --- a/data-pipeline/data_pipeline/aisr.py +++ b/data-pipeline/data_pipeline/aisr.py @@ -1,5 +1,5 @@ """ -Query MIIC for school immunization data +Module for interactions with AISR """ import logging @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) -def _get_session_and_tab( +def _get_session_code_and_tab_id( session: requests.Session, auth_realm_url: str ) -> Tuple[str, str]: """ @@ -30,12 +30,12 @@ def _get_session_and_tab( soup = BeautifulSoup(response.content, "html.parser") form_element = soup.find("form", id="kc-form-login") + # the session code and tab id are found in the action URL of the form if isinstance(form_element, Tag): action_url = form_element.get("action") if isinstance(action_url, str): parsed_url = urlparse(action_url) query_dict = parse_qs(parsed_url.query) - print(query_dict["session_code"][0], query_dict["tab_id"][0]) return query_dict["session_code"][0], query_dict["tab_id"][0] raise ValueError("The action URL is not a valid string.") raise ValueError("Login form not found or is not a valid HTML form element.") @@ -58,7 +58,7 @@ def login( Login with AISR. """ logger.info("Logging into MIIC") - session_code, tab_id = _get_session_and_tab(session, auth_realm_url) + session_code, tab_id = _get_session_code_and_tab_id(session, auth_realm_url) # pylint: disable-next=line-too-long url = f"{auth_realm_url}/login-actions/authenticate?session_code={session_code}&execution=084dee30-925f-4a8f-829d-7a372e38d0de&client_id=aisr-app&tab_id={tab_id}" @@ -79,9 +79,13 @@ def login( ) -def logout(session: requests.Session, auth_realm_url: str): +def logout(session: requests.Session, auth_realm_url: str) -> AISRResponse: """ Log out of AISR. """ url = f"{auth_realm_url}/protocol/openid-connect/logout?client_id=aisr-app" session.request("GET", url, headers={}, data={}) + return AISRResponse( + is_successful=True, + message="Logged out successfully", + ) diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index 0dc165f..9ea22ac 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -42,7 +42,6 @@ def input_output_logs_folders(): def fastapi_server(): """ Spins up a FastAPI server for integration tests. - Serves a boilerplate HTML page with a

tag: 'Hello Minnesota!' """ app = FastAPI() @@ -112,8 +111,8 @@ async def logout(client_id: str): ) response.delete_cookie( key="KEYCLOAK_IDENTITY", - httponly=True, # Ensure this matches the original cookie settings - secure=True, # Ensure this matches the original cookie settings + httponly=True, + secure=True, ) return response From bc3500eaf3d4dfacb1a8b917a1078a7057884489 Mon Sep 17 00:00:00 2001 From: Dillon O'Leary Date: Thu, 23 Jan 2025 05:47:40 +0000 Subject: [PATCH 7/7] Fix string literal --- data-pipeline/tests/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data-pipeline/tests/conftest.py b/data-pipeline/tests/conftest.py index 9ea22ac..8489ddd 100644 --- a/data-pipeline/tests/conftest.py +++ b/data-pipeline/tests/conftest.py @@ -54,10 +54,10 @@ async def oidc_auth(): Simulates an authentication endpoint. Returns an HTML page with a form that includes the required `session_code` and `tab_id`. """ - form_action_url = f"/protocol/openid-connect/login?{urlencode({ - 'session_code': 'mock-session-code', - 'tab_id': 'mock-tab-id' - })}" + encoded_session_and_tab = urlencode( + {"session_code": "mock-session-code", "tab_id": "mock-tab-id"} + ) + form_action_url = f"/protocol/openid-connect/login?{encoded_session_and_tab}" return f"""