diff --git a/.gitignore b/.gitignore index 91731c4..d01a0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,6 @@ fabric.properties # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Python cache artifacts +__pycache__/ +*.py[cod] diff --git a/poetry.lock b/poetry.lock index 47592a5..6f48d2a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,14 +26,14 @@ files = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"}, - {file = "anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0"}, + {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, + {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, ] [package.dependencies] @@ -62,26 +62,26 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "cachetools" -version = "6.2.2" +version = "7.0.1" description = "Extensible memoizing collections and decorators" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"}, - {file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"}, + {file = "cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf"}, + {file = "cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341"}, ] [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] @@ -278,26 +278,27 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.124.0" +version = "0.129.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastapi-0.124.0-py3-none-any.whl", hash = "sha256:91596bdc6dde303c318f06e8d2bc75eafb341fc793a0c9c92c0bc1db1ac52480"}, - {file = "fastapi-0.124.0.tar.gz", hash = "sha256:260cd178ad75e6d259991f2fd9b0fee924b224850079df576a3ba604ce58f4e6"}, + {file = "fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec"}, + {file = "fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af"}, ] [package.dependencies] annotated-doc = ">=0.0.2" -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.51.0" +pydantic = ">=2.7.0" +starlette = ">=0.40.0,<1.0.0" typing-extensions = ">=4.8.0" +typing-inspection = ">=0.4.2" [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.9.3)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=5.8.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "h11" @@ -387,14 +388,14 @@ files = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] @@ -644,14 +645,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "starlette" -version = "0.50.0" +version = "0.52.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca"}, - {file = "starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca"}, + {file = "starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74"}, + {file = "starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"}, ] [package.dependencies] @@ -663,55 +664,60 @@ full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] @@ -744,14 +750,14 @@ typing-extensions = ">=4.12.0" [[package]] name = "urllib3" -version = "2.6.1" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"}, - {file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] @@ -762,14 +768,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.40.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"}, - {file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"}, + {file = "uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"}, + {file = "uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea"}, ] [package.dependencies] @@ -795,4 +801,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "59021f7b1eb090bf5577598b027168b21b51fd99db085606b9800b069ace5a4e" +content-hash = "aaabe25075be7ede936902be718690f90e88fd4b93ad21c5a0168d0d17f1aa4a" diff --git a/pyproject.toml b/pyproject.toml index 0e04ae8..cfcf91a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,11 +9,11 @@ authors = [ keywords = ["SureHub", "Sure Petcare", "REST", "API"] requires-python = ">=3.10" dependencies = [ - "uvicorn~=0.38", - "dynaconf~=3.2", - "fastapi~=0.124", - "requests~=2.32", - "cachetools~=6.2", + "uvicorn>=0.38,<1.0", + "dynaconf>=3.2,<4.0", + "fastapi>=0.124,<1.0", + "requests>=2.32,<3.0", + "cachetools>=7.0,<8.0", ] [project.urls] @@ -22,10 +22,10 @@ documentation = "https://fabieu.github.io/surehub-api/" [dependency-groups] dev = [ - "pytest~=9.0", - "vermin~=1.8", - "autopep8~=2.3", - "httpx~=0.28", + "pytest>=9.0,<10.0", + "vermin>=1.8,<2.0", + "autopep8>=2.3,<3.0", + "httpx>=0.28,<1.0", ] [build-system] diff --git a/surehub_api/entities/enums.py b/surehub_api/entities/enums.py new file mode 100644 index 0000000..f574c70 --- /dev/null +++ b/surehub_api/entities/enums.py @@ -0,0 +1,453 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import Any + +from pydantic import GetJsonSchemaHandler +from pydantic_core import CoreSchema + + +class ConsumptionHabitModelState(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + + +class ConsumptionHabitOutcome(IntEnum): + OK = 0 + BELOW_LIMIT = 1 + ABOVE_LIMIT = 2 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Consumption Habit Outcome" + schema["description"] = ( + "Outcome of a consumption habit evaluation:\n" + "- `0` (OK): Within normal range\n" + "- `1` (BELOW_LIMIT): Below the expected limit\n" + "- `2` (ABOVE_LIMIT): Above the expected limit" + ) + return schema + + +class DeviceType(IntEnum): + UNKNOWN_DEVICE_0 = 0 + HUB = 1 + REPEATER = 2 + PET_DOOR_CONNECT = 3 + PET_FEEDER_CONNECT = 4 + PROGRAMMER = 5 + DUALSCAN_CAT_FLAP_CONNECT = 6 + MICROCHIP_FEEDER = 7 + FELAQUA_CONNECT = 8 # Poseidon + CAT_FLAP_CONNECT = 9 + DUALSCAN_PET_DOOR_CONNECT = 10 + DOG_BOWL_CONNECT = 32 # Cerberus + UNKNOWN_DEVICE_255 = 255 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Device Type" + schema["description"] = ( + "Type of Sure Petcare device:\n" + "- `0` (UNKNOWN_DEVICE_0): Unknown device\n" + "- `1` (HUB): Hub\n" + "- `2` (REPEATER): Repeater\n" + "- `3` (PET_DOOR_CONNECT): Pet Door Connect\n" + "- `4` (PET_FEEDER_CONNECT): Pet Feeder Connect\n" + "- `5` (PROGRAMMER): Programmer\n" + "- `6` (DUALSCAN_CAT_FLAP_CONNECT): DualScan Cat Flap Connect\n" + "- `7` (MICROCHIP_FEEDER): Microchip Feeder\n" + "- `8` (FELAQUA_CONNECT): Felaqua Connect (Poseidon)\n" + "- `9` (CAT_FLAP_CONNECT): Cat Flap Connect\n" + "- `10` (DUALSCAN_PET_DOOR_CONNECT): DualScan Pet Door Connect\n" + "- `32` (DOG_BOWL_CONNECT): Dog Bowl Connect (Cerberus)\n" + "- `255` (UNKNOWN_DEVICE_255): Unknown device" + ) + return schema + + +class DoorDirection(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + + +class DoorSide(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + + +class DoorStatus(IntEnum): + VALUE_4 = 4 + VALUE_6 = 6 + VALUE_8 = 8 + VALUE_10 = 10 + VALUE_11 = 11 + VALUE_12 = 12 + VALUE_13 = 13 + + +class HouseholdInviteStatus(IntEnum): + PENDING = 0 + ACCEPTED = 1 + EXPIRED = 2 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Household Invite Status" + schema["description"] = ( + "Status of a household invitation:\n" + "- `0` (PENDING): Invitation is pending\n" + "- `1` (ACCEPTED): Invitation has been accepted\n" + "- `2` (EXPIRED): Invitation has expired" + ) + return schema + + +class LockMode(IntEnum): + NONE = 0 + IN = 1 + OUT = 2 + BOTH = 3 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Lock Mode" + schema["description"] = ( + "Controls the direction of locking:\n" + "- `0` (NONE): No locking\n" + "- `1` (IN): Lock inbound only\n" + "- `2` (OUT): Lock outbound only\n" + "- `3` (BOTH): Lock both directions" + ) + return schema + + +class PetGender(IntEnum): + FEMALE = 0 + MALE = 1 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Pet Gender" + schema["description"] = ( + "Gender of the pet:\n" + "- `0` (FEMALE): Female\n" + "- `1` (MALE): Male" + ) + return schema + + +class PetPositionWhere(IntEnum): + UNKNOWN = 0 + INSIDE = 1 + OUTSIDE = 2 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Pet Position" + schema["description"] = ( + "Where the pet is located:\n" + "- `0` (UNKNOWN): Unknown position\n" + "- `1` (INSIDE): Inside the house\n" + "- `2` (OUTSIDE): Outside the house" + ) + return schema + + +class RequestChangeStateResponseStatus(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + VALUE_5 = 5 + + +class Spayed(IntEnum): + UNKNOWN = 0 + YES = 1 + NO = 2 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Spayed/Neutered Status" + schema["description"] = ( + "Whether the pet has been spayed or neutered:\n" + "- `0` (UNKNOWN): Unknown\n" + "- `1` (YES): Spayed/neutered\n" + "- `2` (NO): Not spayed/neutered" + ) + return schema + + +class SpecialProfiles(IntEnum): + SPECIAL_PROFILE_0 = 0 + SPECIAL_PROFILE_1 = 1 + SPECIAL_PROFILE_2 = 2 + SPECIAL_PROFILE_3 = 3 + SPECIAL_PROFILE_4 = 4 + SPECIAL_PROFILE_5 = 5 + SPECIAL_PROFILE_6 = 6 + + +class SubstanceTypes(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + + +class TimelineEventType(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_6 = 6 + VALUE_7 = 7 + VALUE_9 = 9 + VALUE_10 = 10 + VALUE_11 = 11 + VALUE_12 = 12 + VALUE_13 = 13 + VALUE_14 = 14 + VALUE_17 = 17 + VALUE_18 = 18 + VALUE_19 = 19 + VALUE_20 = 20 + VALUE_21 = 21 + VALUE_22 = 22 + VALUE_23 = 23 + VALUE_24 = 24 + VALUE_25 = 25 + VALUE_28 = 28 + VALUE_29 = 29 + VALUE_30 = 30 + VALUE_31 = 31 + VALUE_32 = 32 + VALUE_33 = 33 + VALUE_34 = 34 + VALUE_35 = 35 + VALUE_36 = 36 + VALUE_40 = 40 + VALUE_50 = 50 + VALUE_51 = 51 + VALUE_52 = 52 + VALUE_53 = 53 + VALUE_54 = 54 + VALUE_55 = 55 + VALUE_9999 = 9999 + VALUE_19999 = 19999 + VALUE_20000 = 20000 + VALUE_20001 = 20001 + VALUE_20002 = 20002 + VALUE_20003 = 20003 + VALUE_20004 = 20004 + VALUE_20005 = 20005 + VALUE_20006 = 20006 + VALUE_20007 = 20007 + VALUE_20008 = 20008 + VALUE_20009 = 20009 + VALUE_20010 = 20010 + VALUE_20011 = 20011 + VALUE_20012 = 20012 + VALUE_20399 = 20399 + VALUE_20400 = 20400 + VALUE_20401 = 20401 + VALUE_20402 = 20402 + VALUE_20403 = 20403 + VALUE_20404 = 20404 + VALUE_20405 = 20405 + VALUE_20406 = 20406 + VALUE_20407 = 20407 + VALUE_20408 = 20408 + VALUE_20409 = 20409 + VALUE_20410 = 20410 + VALUE_20411 = 20411 + VALUE_20999 = 20999 + VALUE_21000 = 21000 + VALUE_21001 = 21001 + VALUE_21002 = 21002 + VALUE_21003 = 21003 + VALUE_21004 = 21004 + VALUE_21005 = 21005 + VALUE_21006 = 21006 + VALUE_21007 = 21007 + VALUE_21008 = 21008 + VALUE_21009 = 21009 + VALUE_21010 = 21010 + VALUE_21011 = 21011 + VALUE_21012 = 21012 + VALUE_21013 = 21013 + VALUE_21014 = 21014 + VALUE_21015 = 21015 + VALUE_21016 = 21016 + VALUE_21017 = 21017 + VALUE_21018 = 21018 + VALUE_21019 = 21019 + VALUE_21020 = 21020 + VALUE_21999 = 21999 + VALUE_23000 = 23000 + VALUE_23001 = 23001 + VALUE_23002 = 23002 + VALUE_23003 = 23003 + VALUE_23004 = 23004 + VALUE_23005 = 23005 + VALUE_23006 = 23006 + VALUE_23999 = 23999 + VALUE_24999 = 24999 + VALUE_26999 = 26999 + VALUE_28999 = 28999 + VALUE_30000 = 30000 + VALUE_30001 = 30001 + VALUE_30002 = 30002 + + +class UserTimeFormat(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + + +class UserWeightUnit(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + + +class ChangeProfileAction(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + + +class DeviceTagProfiles(IntEnum): + DISABLED = 2 + ENABLED = 3 + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler(core_schema) + schema["title"] = "Device Tag Profile" + schema["description"] = ( + "Profile setting for a device tag:\n" + "- `2` (DISABLED): Tag profile disabled\n" + "- `3` (ENABLED): Tag profile enabled" + ) + return schema + + +class DualScanLockingMode(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + + +class FailSafeOptions(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + + +class FeederBowlType(IntEnum): + VALUE_1 = 1 + VALUE_4 = 4 + VALUE_5 = 5 + + +class FoodTypes(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + + +class LedMode(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + VALUE_128 = 128 + + +class PairingMode(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_128 = 128 + + +class PetDoorLockingMode(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + VALUE_5 = 5 + + +class PetDoorTagType(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_4 = 4 + VALUE_8 = 8 + VALUE_16 = 16 + VALUE_32 = 32 + VALUE_64 = 64 + VALUE_128 = 128 + + +class ReportHouseholdEvent(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + + +class ThalamusMovementTimedAccessAllowedSpecialProfiles(IntEnum): + VALUE_3 = 3 + VALUE_5 = 5 + VALUE_6 = 6 + + +class ThalamusTagType(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + VALUE_5 = 5 + VALUE_6 = 6 + VALUE_7 = 7 + VALUE_8 = 8 + + +class TrainingMode(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + + +class UpdateDeviceTagActions(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + + +class ZeroAction(IntEnum): + VALUE_0 = 0 + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 diff --git a/surehub_api/entities/official.py b/surehub_api/entities/official.py index eaa0d61..fc43e94 100644 --- a/surehub_api/entities/official.py +++ b/surehub_api/entities/official.py @@ -1,36 +1,93 @@ +from __future__ import annotations + from datetime import datetime, time, date -from enum import IntEnum from typing import Optional, List, Any -from pydantic import BaseModel, Field, GetJsonSchemaHandler -from pydantic_core import CoreSchema +from pydantic import BaseModel, Field + +from surehub_api.entities.enums import ( + ConsumptionHabitModelState, + ConsumptionHabitOutcome, + DeviceType, + DoorDirection, + DoorSide, + DoorStatus, + HouseholdInviteStatus, + LockMode, + PetGender, + PetPositionWhere, + RequestChangeStateResponseStatus, + Spayed, + SpecialProfiles, + TimelineEventType, + UserTimeFormat, + UserWeightUnit, +) + + +class AnimoPet(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + gender: Optional[PetGender] = None + date_of_birth: Optional[datetime] = None + weight: Optional[str] = None + breed_id: Optional[int] = None + household_id: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + +class AnimoPetPaginatedDataResult(BaseModel): + data: Optional[List[AnimoPet]] = None + meta: Optional[PaginatedMetaDataResult] = None -class DeviceType(IntEnum): - UNKNOWN_DEVICE_0 = 0 - HUB = 1 - REPEATER = 2 - PET_DOOR_CONNECT = 3 - PET_FEEDER_CONNECT = 4 - PROGRAMMER = 5 - DUALSCAN_CAT_FLAP_CONNECT = 6 - MICROCHIP_FEEDER = 7 - FELAQUA_CONNECT = 8 # Poseidon - CAT_FLAP_CONNECT = 9 - DUALSCAN_PET_DOOR_CONNECT = 10 - DOG_BOWL_CONNECT = 32 # Cerberus - UNKNOWN_DEVICE_255 = 255 +class AuthChangePassword(BaseModel): + user_id: Optional[int] = None + password: str + new_password: Optional[str] = None + + +class AuthLogin(BaseModel): + client_uid: Optional[str] = None + device_id: Optional[str] = None + email_address: str + password: str + + +class AuthLogout(BaseModel): + client_uid: Optional[str] = None + device_id: Optional[str] = None -# TODO: Add descriptive names to numeric special profiles -class SpecialProfile(IntEnum): - SPECIAL_PROFILE_0 = 0 - SPECIAL_PROFILE_1 = 1 - SPECIAL_PROFILE_2 = 2 - SPECIAL_PROFILE_3 = 3 - SPECIAL_PROFILE_4 = 4 - SPECIAL_PROFILE_5 = 5 - SPECIAL_PROFILE_6 = 6 + +class AuthRegister(BaseModel): + email_address: str + first_name: str + last_name: str + password: str + language_id: int + country_id: int + photo_id: Optional[int] = None + marketing_opt_in: bool + weight_units: Optional[UserWeightUnit] = None + time_format: Optional[UserTimeFormat] = None + device_id: str + + +class AuthResetPassword(BaseModel): + email_address: str + password: str + token: str + client_uid: Optional[str] = None + device_id: Optional[str] = None + + +class AuthResetPasswordRequest(BaseModel): + email_address: str + + +class AuthToken(BaseModel): + token: str class DeviceTag(BaseModel): @@ -43,310 +100,524 @@ class DeviceTag(BaseModel): updated_at: Optional[datetime] = None -class Tag(BaseModel): - id: int - tag: Optional[str] = None - supported_product_ids: Optional[List[DeviceType]] = None - incompatible_product_ids: Optional[List[DeviceType]] = None - version: int +class Breed(BaseModel): + id: Optional[int] = None + species_id: Optional[int] = None + name: Optional[str] = None + version: Optional[int] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None - - -class Curfew(BaseModel): - enabled: Optional[bool] = None - lock_time: Optional[time] = None - unlock_time: Optional[time] = None - -class LockMode(IntEnum): - NONE = 0 - IN = 1 - OUT = 2 - BOTH = 3 - @classmethod - def __get_pydantic_json_schema__(cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler) -> dict[str, Any]: - schema = handler(core_schema) - schema["title"] = "Lock Mode" - schema["description"] = ( - "Controls the direction of locking:\n" - "- `0` (NONE): No locking\n" - "- `1` (IN): Lock inbound only\n" - "- `2` (OUT): Lock outbound only\n" - "- `3` (BOTH): Lock both directions" - ) - return schema +class BreedDataResponse(BaseModel): + data: Optional[Breed] = None -class DeviceControl(BaseModel): - curfew: Curfew | List[Curfew] | None = None - fast_polling: Optional[bool] = None - locking: Optional[LockMode] = None - led_mode: Optional[int] = None - pairing_mode: Optional[int] = None +class BreedPaginatedDataResult(BaseModel): + data: Optional[List[Breed]] = None + meta: Optional[PaginatedMetaDataResult] = None -class DeviceStatus(BaseModel): - led_mode: Optional[int] = None - pairing_mode: Optional[int] = None - status: Optional[bool] = None +class BreedQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + species_id: Optional[int] = None + lang: Optional[str] = None -class Device(BaseModel): - id: int - parent_device_id: Optional[int] = None - product_id: int - household_id: Optional[int] = None - index: Optional[int] = None +class Condition(BaseModel): + id: Optional[int] = None name: Optional[str] = None - serial_number: Optional[str] = None - mac_address: Optional[str] = None - version: int + version: Optional[int] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None - pairing_at: Optional[datetime] = None - last_activity_at: Optional[datetime] = None - last_new_event_at: Optional[datetime] = None - control: Optional[DeviceControl] = None - tags: Optional[List[DeviceTag]] = None -class Photo(BaseModel): - id: int - title: Optional[str] = None - location: Optional[str] = None - hash: Optional[str] = None - uploading_user_id: Optional[int] = None - version: int - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None +class ConditionDataResponse(BaseModel): + data: Optional[Condition] = None -class PetPositionWhere(IntEnum): - INSIDE = 1 - OUTSIDE = 2 +class ConditionPaginatedDataResult(BaseModel): + data: Optional[List[Condition]] = None + meta: Optional[PaginatedMetaDataResult] = None -class CreatePetPosition(BaseModel): - where: PetPositionWhere - since: Optional[datetime] = None +class ConditionQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + lang: Optional[str] = None -class PetPosition(BaseModel): - id: int - pet_id: Optional[int] = None - tag_id: Optional[int] = None - device_id: Optional[int] = None - user_id: Optional[int] = None - where: Optional[PetPositionWhere] = None - since: Optional[datetime] = None +class ConsumptionAlert(BaseModel): + pet_id: int + tag_id: int + pet_weight: int + amount: int + time_noticed_utc: datetime + created_at: datetime -class PetConsumptionStatus(BaseModel): - id: int - tag_id: Optional[int] = None - device_id: Optional[int] = None - change: Optional[List[float]] = None - at: Optional[datetime] = None +class ConsumptionHabit(BaseModel): + outcome: ConsumptionHabitOutcome + calendar_day: date + amount: int + lower_limit: Optional[int] = None + upper_limit: Optional[int] = None + created_at: datetime -class PetStatus(BaseModel): +class ConsumptionHabitModelState(BaseModel): pet_id: Optional[int] = None - activity: Optional[PetPosition] = None - feeding: Optional[PetConsumptionStatus] = None - drinking: Optional[PetConsumptionStatus] = None + tag_id: Optional[int] = None + state: Optional[ConsumptionHabitModelState] = None -class PetCondition(BaseModel): - id: int - version: int +class Country(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + native_name: Optional[str] = None + code: Optional[str] = None + default_language_id: Optional[int] = None + default_timezone_id: Optional[int] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None -class PetGender(IntEnum): - FEMALE = 0 - MALE = 1 +class CountryDataResponse(BaseModel): + data: Optional[Country] = None -class Spayed(IntEnum): - UNKNOWN = 0 - YES = 1 - NO = 2 +class CountryPaginatedDataResult(BaseModel): + data: Optional[List[Country]] = None + meta: Optional[PaginatedMetaDataResult] = None -class Pet(BaseModel): - id: int - name: Optional[str] = None +class CountryQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + iso_code2: Optional[str] = None + lang: Optional[str] = None + + +class CreateHousehold(BaseModel): + name: str + timezone_id: int + + +class CreateHouseholdInvite(BaseModel): + code: Optional[str] = None + email_address: str + owner: bool + write: bool + + +class CreatePet(BaseModel): + name: str gender: Optional[PetGender] = None date_of_birth: Optional[datetime] = None - weight: Optional[str] = None + weight: Optional[float] = None comments: Optional[str] = None breed_id: Optional[int] = None - breed_id_2: Optional[int] = None + breed_id2: Optional[int] = None + spayed: Optional[Spayed] = None food_type_id: Optional[int] = None - household_id: Optional[int] = None photo_id: Optional[int] = None species_id: Optional[int] = None - spayed: Optional[Spayed] = None - tag_id: Optional[int] = None - version: int - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None - photo: Optional[Photo] = None - conditions: Optional[List[PetCondition]] = None - tag: Optional[Tag] = None - status: Optional[PetStatus] = None - position: Optional[PetPosition] = None - - -class PublicUser(BaseModel): - id: int - name: Optional[str] = None - photo_id: Optional[int] = None - photo: Optional[Photo] = None - - -class HouseholdInviteUser(BaseModel): - creator: Optional[PublicUser] = None - acceptor: Optional[PublicUser] = None + conditions: Optional[List[Condition]] = None + household_id: int -class HouseholdInviteStatus(IntEnum): - PENDING = 0 - ACCEPTED = 1 - EXPIRED = 2 +class CreatePetPosition(BaseModel): + where: Optional[PetPositionWhere] = None + since: Optional[datetime] = None -class HouseholdInvite(BaseModel): - id: int - code: Optional[str] = None - email_address: Optional[str] = None - owner: Optional[bool] = None - write: Optional[bool] = None - status: Optional[HouseholdInviteStatus] = None - user: Optional[HouseholdInviteUser] = None - version: int - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None - used_at: Optional[datetime] = None +class CreateUserSettings(BaseModel): + key: str + value: str -class HouseholdUser(BaseModel): - id: int - owner: Optional[bool] = None - write: Optional[bool] = None - user: Optional[PublicUser] = None - version: int - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None +class Curfew(BaseModel): + enabled: Optional[bool] = None + lock_time: Optional[time] = None + unlock_time: Optional[time] = None -class Timezone(BaseModel): - id: int - name: Optional[str] = None - timezone: Optional[str] = None - utc_offset: int - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None +class DeleteAccount(BaseModel): + password: str + households: Optional[List[int]] = None -class Household(BaseModel): +class Device(BaseModel): id: int + parent_device_id: Optional[int] = None + product_id: int + household_id: Optional[int] = None + index: Optional[int] = None name: Optional[str] = None - share_code: Optional[str] = None - created_user_id: Optional[int] = None - timezone_id: Optional[int] = None + serial_number: Optional[str] = None + mac_address: Optional[str] = None version: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None deleted_at: Optional[datetime] = None - invites: Optional[List[HouseholdInvite]] = None - users: Optional[List[HouseholdUser]] = None - timezone: Optional[Timezone] = None + pairing_at: Optional[datetime] = None + last_activity_at: Optional[datetime] = None + last_new_event_at: Optional[datetime] = None + control: Optional[DeviceControl] = None + status: Optional[DeviceStatus] = None + tags: Optional[List[DeviceTag]] = None -class MeStart(BaseModel): - devices: Optional[List[Device]] = None - households: Optional[List[Household]] = None - pets: Optional[List[Pet]] = None - photos: Optional[List[Photo]] = None - tags: Optional[List[Tag]] = None - user: Optional[HouseholdUser] = None +class DeviceControl(BaseModel): + curfew: Curfew | List[Curfew] | None = None + fast_polling: Optional[bool] = None + locking: Optional[LockMode] = None + led_mode: Optional[int] = None + pairing_mode: Optional[int] = None -class ConsumptionHabitOutcomeEnum(IntEnum): - OK = 0 - BELOW_LIMIT = 1 - ABOVE_LIMIT = 2 +class DeviceControlSchema(BaseModel): + data: Optional[Any] = None + pending: Optional[List[DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None -class ReportWeightFrame(BaseModel): - index: Optional[int] = None - weight: float - change: float - food_type_id: Optional[int] = None - target_weight: Optional[int] = None - multi: Optional[bool] = None +class DeviceControlPending(BaseModel): + state: Optional[Any] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None -class FeedingReportDataPoint(BaseModel): - from_: Optional[datetime] = Field(default=None, alias="from") - to: Optional[datetime] = None - duration: Optional[int] = None +class DeviceControlResult(BaseModel): + request_id: Optional[str] = None + response_id: Optional[str] = None + status: Optional[RequestChangeStateResponseStatus] = None + status_id: Optional[RequestChangeStateResponseStatus] = None + requested_at: Optional[datetime] = None + committed_at: Optional[datetime] = None - context: Optional[int] = None - bowl_count: Optional[int] = None - actual_weight: Optional[float] = None - weights: Optional[List[ReportWeightFrame]] = None +class DeviceControlStateChange(BaseModel): + request_id: Optional[str] = None + response_id: Optional[str] = None + status: Optional[RequestChangeStateResponseStatus] = None + status_id: Optional[RequestChangeStateResponseStatus] = None + requested_at: Optional[datetime] = None + committed_at: Optional[datetime] = None device_id: Optional[int] = None - tag_id: Optional[int] = None + state: Optional[Any] = None + requested_by: Optional[int] = None + child_state_changes: Optional[List[DeviceControlStateChange]] = None + parent_request_id: Optional[str] = None - user_id: Optional[int] = None - entry_user_id: Optional[int] = None - exit_user_id: Optional[int] = None - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None +class DeviceControlStateChangeDataResponse(BaseModel): + data: Optional[DeviceControlStateChange] = None -class FeedingReport(BaseModel): - datapoints: Optional[List[FeedingReportDataPoint]] = None +class DeviceControlStateChangeListDataResponse(BaseModel): + data: Optional[List[DeviceControlStateChange]] = None -class DrinkingReportDataPoint(BaseModel): - from_: Optional[datetime] = Field(default=None, alias="from") - to: Optional[datetime] = None - duration: Optional[int] = None +class DeviceDataResponse(BaseModel): + data: Optional[Device] = None - context: Optional[int] = None - bowl_count: Optional[int] = None - weights: Optional[List[ReportWeightFrame]] = None - actual_weight: Optional[float] = None - device_id: Optional[int] = None - tag_id: Optional[int] = None +class DeviceIEnumerableDataResponse(BaseModel): + data: Optional[List[Device]] = None - user_id: Optional[int] = None - entry_user_id: Optional[int] = None - exit_user_id: Optional[int] = None - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - deleted_at: Optional[datetime] = None +class DeviceNeedsUpdate(BaseModel): + needs_manual_update: Optional[bool] = None + + +class DeviceNeedsUpdateDataResponse(BaseModel): + data: Optional[DeviceNeedsUpdate] = None + + +class DevicePaginatedDataResult(BaseModel): + data: Optional[List[Device]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class DevicePairByCode(BaseModel): + pairing_code: str + + +class DeviceReadiness(BaseModel): + device_ready: Optional[bool] = None + profiles_available: Optional[int] = None + profiles_updated_at: Optional[datetime] = None + + +class DeviceReadinessDataResponse(BaseModel): + data: Optional[DeviceReadiness] = None + + +class DeviceStatus(BaseModel): + led_mode: Optional[int] = None + pairing_mode: Optional[int] = None + status: Optional[bool] = None + + +class DeviceTagData(BaseModel): + data: Optional[DeviceTag] = None + pending: Optional[List[DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceTagDataResponse(BaseModel): + data: Optional[DeviceTag] = None + + +class DeviceTagPaginatedDataResult(BaseModel): + data: Optional[List[DeviceTag]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +# TODO: Add descriptive names to numeric special profiles class DrinkingReport(BaseModel): datapoints: Optional[List[DrinkingReportDataPoint]] = None +class DrinkingReportDataPoint(BaseModel): + from_: Optional[datetime] = Field(default=None, alias="from") + to: Optional[datetime] = None + duration: Optional[int] = None + + context: Optional[int] = None + bowl_count: Optional[int] = None + weights: Optional[List[ReportWeightFrame]] = None + actual_weight: Optional[float] = None + + device_id: Optional[int] = None + tag_id: Optional[int] = None + + user_id: Optional[int] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + + +class Error(BaseModel): + success: Optional[bool] = None + error: Optional[dict] = None + + +class FeedingReport(BaseModel): + datapoints: Optional[List[FeedingReportDataPoint]] = None + + +class FeedingReportDataPoint(BaseModel): + from_: Optional[datetime] = Field(default=None, alias="from") + to: Optional[datetime] = None + duration: Optional[int] = None + + context: Optional[int] = None + bowl_count: Optional[int] = None + actual_weight: Optional[float] = None + weights: Optional[List[ReportWeightFrame]] = None + + device_id: Optional[int] = None + tag_id: Optional[int] = None + + user_id: Optional[int] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + + +class FoodType(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class FoodTypeDataResponse(BaseModel): + data: Optional[FoodType] = None + + +class FoodTypePaginatedDataResult(BaseModel): + data: Optional[List[FoodType]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class FoodTypeQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + lang: Optional[str] = None + + +class Household(BaseModel): + id: int + name: Optional[str] = None + share_code: Optional[str] = None + created_user_id: Optional[int] = None + timezone_id: Optional[int] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + invites: Optional[List[HouseholdInvite]] = None + users: Optional[List[HouseholdUser]] = None + timezone: Optional[Timezone] = None + + +class HouseholdDataResponse(BaseModel): + data: Optional[Household] = None + + +class HouseholdInvite(BaseModel): + id: int + code: Optional[str] = None + email_address: Optional[str] = None + owner: Optional[bool] = None + write: Optional[bool] = None + status: Optional[HouseholdInviteStatus] = None + user: Optional[HouseholdInviteUser] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + used_at: Optional[datetime] = None + + +class HouseholdInviteDataResponse(BaseModel): + data: Optional[HouseholdInvite] = None + + +class HouseholdInvitePaginatedDataResult(BaseModel): + data: Optional[List[HouseholdInvite]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class HouseholdInviteUser(BaseModel): + creator: Optional[PublicUser] = None + acceptor: Optional[PublicUser] = None + + +class HouseholdPaginatedDataResult(BaseModel): + data: Optional[List[Household]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class HouseholdUser(BaseModel): + id: int + owner: Optional[bool] = None + write: Optional[bool] = None + user: Optional[PublicUser] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class HouseholdUserDataResponse(BaseModel): + data: Optional[HouseholdUser] = None + + +class HouseholdUserPaginatedDataResult(BaseModel): + data: Optional[List[HouseholdUser]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Info(BaseModel): + language: Optional[str] = None + country: Optional[str] = None + + +class InfoDataResponse(BaseModel): + data: Optional[Info] = None + + +class Invite(BaseModel): + id: Optional[int] = None + code: Optional[str] = None + email_address: Optional[str] = None + owner: Optional[bool] = None + write: Optional[bool] = None + status: Optional[HouseholdInviteStatus] = None + user: Optional[HouseholdInviteUser] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + used_at: Optional[datetime] = None + + +class InviteDataResponse(BaseModel): + data: Optional[Invite] = None + + +class InvitePaginatedDataResult(BaseModel): + data: Optional[List[Invite]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Language(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + native_name: Optional[str] = None + code: Optional[str] = None + enabled: Optional[bool] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class LanguageDataResponse(BaseModel): + data: Optional[Language] = None + + +class LanguagePaginatedDataResult(BaseModel): + data: Optional[List[Language]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class MeStart(BaseModel): + devices: Optional[List[Device]] = None + households: Optional[List[Household]] = None + pets: Optional[List[Pet]] = None + photos: Optional[List[Photo]] = None + tags: Optional[List[Tag]] = None + user: Optional[User] = None + segments: Optional[List[str]] = None + + +class MeStartDataResponse(BaseModel): + data: Optional[MeStart] = None + + +class Movement(BaseModel): + id: Optional[int] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + direction: Optional[DoorDirection] = None + side: Optional[DoorSide] = None + type: Optional[DoorStatus] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class MovementReport(BaseModel): + datapoints: Optional[List[MovementReportDataPoint]] = None + + class MovementReportDataPoint(BaseModel): from_: Optional[datetime] = Field(default=None, alias="from") to: Optional[datetime] = None @@ -367,32 +638,630 @@ class MovementReportDataPoint(BaseModel): deleted_at: Optional[datetime] = None -class MovementReport(BaseModel): - datapoints: Optional[List[MovementReportDataPoint]] = None +class Notification(BaseModel): + id: Optional[int] = None + type: Optional[TimelineEventType] = None + text: Optional[str] = None + created_at: Optional[datetime] = None -class ConsumptionHabit(BaseModel): - outcome: ConsumptionHabitOutcomeEnum - calendar_day: date - amount: int - lower_limit: Optional[int] = None - upper_limit: Optional[int] = None - created_at: datetime +class NotificationPaginatedDataResult(BaseModel): + data: Optional[List[Notification]] = None + meta: Optional[PaginatedMetaDataResult] = None -class ConsumptionAlert(BaseModel): - pet_id: int - tag_id: int - pet_weight: int - amount: int - time_noticed_utc: datetime - created_at: datetime +class ObjectDataResponse(BaseModel): + data: Optional[Any] = None -class PetReport(BaseModel): - movement: MovementReport - feeding: FeedingReport - drinking: DrinkingReport +class PaginatedMetaDataResult(BaseModel): + page: Optional[int] = None + page_size: Optional[int] = None + count: Optional[int] = None + total_pages: Optional[int] = None - consumption_habit: Optional[List[ConsumptionHabit]] = None - consumption_alert: Optional[List[ConsumptionAlert]] = None + +class Pet(BaseModel): + id: int + name: Optional[str] = None + gender: Optional[PetGender] = None + date_of_birth: Optional[datetime] = None + weight: Optional[str] = None + comments: Optional[str] = None + breed_id: Optional[int] = None + breed_id2: Optional[int] = None + food_type_id: Optional[int] = None + household_id: Optional[int] = None + photo_id: Optional[int] = None + species_id: Optional[int] = None + spayed: Optional[Spayed] = None + tag_id: Optional[int] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + photo: Optional[Photo] = None + conditions: Optional[List[PetCondition]] = None + tag: Optional[Tag] = None + status: Optional[PetStatus] = None + position: Optional[PetPosition] = None + + +class PetCondition(BaseModel): + id: int + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class PetConditionDataResponse(BaseModel): + data: Optional[PetCondition] = None + + +class PetConditionPaginatedDataResult(BaseModel): + data: Optional[List[PetCondition]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class PetConsumption(BaseModel): + total_consumption: Optional[float] = None + date: Optional[datetime] = None + + +class PetConsumptionStatus(BaseModel): + id: int + tag_id: Optional[int] = None + device_id: Optional[int] = None + change: Optional[List[float]] = None + at: Optional[datetime] = None + + +class PetDashboard(BaseModel): + pet_id: Optional[int] = None + movement: Optional[PetMovement] = None + drinking: Optional[PetConsumption] = None + feeding: Optional[PetConsumption] = None + drinking_habit: Optional[ConsumptionHabit] = None + drinking_alert: Optional[ConsumptionAlert] = None + habit_model_state: Optional[ConsumptionHabitModelState] = None + + +class PetDashboardListDataResponse(BaseModel): + data: Optional[List[PetDashboard]] = None + + +class PetDashboardQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + pet_id: List[int] + from_: datetime = Field(alias='from') + days_history: Optional[int] = None + + +class PetDataResponse(BaseModel): + data: Optional[Pet] = None + + +class PetInsight(BaseModel): + pet_id: Optional[int] = None + drinking_habit: Optional[ConsumptionHabit] = None + drinking_alert: Optional[ConsumptionAlert] = None + habit_model_state: Optional[ConsumptionHabitModelState] = None + + +class PetInsightDataResponse(BaseModel): + data: Optional[PetInsight] = None + + +class PetInsightQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + + +class PetMovement(BaseModel): + date: Optional[datetime] = None + time_outside: Optional[str] = None + + +class PetPaginatedDataResult(BaseModel): + data: Optional[List[Pet]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class PetPosition(BaseModel): + id: int + pet_id: Optional[int] = None + tag_id: Optional[int] = None + device_id: Optional[int] = None + user_id: Optional[int] = None + where: Optional[PetPositionWhere] = None + since: Optional[datetime] = None + + +class PetPositionDataResponse(BaseModel): + data: Optional[PetPosition] = None + + +class PetPositionPaginatedDataResult(BaseModel): + data: Optional[List[PetPosition]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class PetReport(BaseModel): + movement: MovementReport + feeding: FeedingReport + drinking: DrinkingReport + + consumption_habit: Optional[List[ConsumptionHabit]] = None + consumption_alert: Optional[List[ConsumptionAlert]] = None + + +class PetStatus(BaseModel): + pet_id: Optional[int] = None + activity: Optional[PetPosition] = None + feeding: Optional[PetConsumptionStatus] = None + drinking: Optional[PetConsumptionStatus] = None + + +class PetStatusDataResponse(BaseModel): + data: Optional[PetStatus] = None + + +class PetStatusPaginatedDataResult(BaseModel): + data: Optional[List[PetStatus]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Photo(BaseModel): + id: int + title: Optional[str] = None + location: Optional[str] = None + hash: Optional[str] = None + uploading_user_id: Optional[int] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class PhotoDataResponse(BaseModel): + data: Optional[Photo] = None + + +class PhotoPaginatedDataResult(BaseModel): + data: Optional[List[Photo]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class ProblemDetails(BaseModel): + type: Optional[str] = None + title: Optional[str] = None + status: Optional[int] = None + detail: Optional[str] = None + instance: Optional[str] = None + + +class Product(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class ProductDataResponse(BaseModel): + data: Optional[Product] = None + + +class ProductPaginatedDataResult(BaseModel): + data: Optional[List[Product]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class ProductQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + lang: Optional[str] = None + + +class PublicUser(BaseModel): + id: int + name: Optional[str] = None + photo_id: Optional[int] = None + photo: Optional[Photo] = None + + +class PublicUserDataResponse(BaseModel): + data: Optional[PublicUser] = None + + +class ReportHousehold(BaseModel): + pet_id: Optional[int] = None + device_id: Optional[int] = None + movement: Optional[ReportHouseholdMovementDataPoint] = None + feeding: Optional[ReportHouseholdFeedingDataPoint] = None + drinking: Optional[ReportHouseholdDrinkingDataPoint] = None + consumption_habit: Optional[List[ConsumptionHabit]] = None + consumption_alert: Optional[List[ConsumptionAlert]] = None + + +class ReportHouseholdDataResponse(BaseModel): + data: Optional[ReportHousehold] = None + + +class ReportHouseholdDrinking(BaseModel): + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + context: Optional[int] = None + bowl_count: Optional[int] = None + device_id: Optional[int] = None + weights: Optional[List[ReportWeightFrame]] = None + actual_weight: Optional[float] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + + +class ReportHouseholdDrinkingDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdDrinking]] = None + + +class ReportHouseholdFeeding(BaseModel): + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + context: Optional[int] = None + bowl_count: Optional[int] = None + device_id: Optional[int] = None + weights: Optional[List[ReportWeightFrame]] = None + actual_weight: Optional[float] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + + +class ReportHouseholdFeedingDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdFeeding]] = None + + +class ReportHouseholdListDataResponse(BaseModel): + data: Optional[List[ReportHousehold]] = None + + +class ReportHouseholdMovement(BaseModel): + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + entry_device_id: Optional[int] = None + entry_user_id: Optional[int] = None + exit_device_id: Optional[int] = None + exit_user_id: Optional[int] = None + active: Optional[bool] = None + exit_movement_id: Optional[int] = None + entry_movement_id: Optional[int] = None + + +class ReportHouseholdMovementDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdMovement]] = None + + +class ReportHouseholdQuery(BaseModel): + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + + +class ReportWeightFrame(BaseModel): + index: Optional[int] = None + weight: float + change: float + food_type_id: Optional[int] = None + target_weight: Optional[int] = None + multi: Optional[bool] = None + + +class Species(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class SpeciesDataResponse(BaseModel): + data: Optional[Species] = None + + +class SpeciesPaginatedDataResult(BaseModel): + data: Optional[List[Species]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class SpeciesQuery(BaseModel): + page: Optional[int] = None + items_per_page: Optional[int] = None + page_size: Optional[int] = None + lang: Optional[str] = None + + +class Start(BaseModel): + breed: Optional[List[Breed]] = None + condition: Optional[List[Condition]] = None + country: Optional[List[Country]] = None + language: Optional[List[Language]] = None + product: Optional[List[Product]] = None + timezone: Optional[List[Timezone]] = None + + +class StartDataResponse(BaseModel): + data: Optional[Start] = None + + +class StartQuery(BaseModel): + lang: Optional[str] = None + + +class Tag(BaseModel): + id: int + tag: Optional[str] = None + supported_product_ids: Optional[List[DeviceType]] = None + incompatible_product_ids: Optional[List[DeviceType]] = None + version: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + + +class TagDataResponse(BaseModel): + data: Optional[Tag] = None + + +class TagDevice(BaseModel): + id: Optional[int] = None + index: Optional[int] = None + profile: Optional[int] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class TagDeviceDataResponse(BaseModel): + data: Optional[TagDevice] = None + + +class TagDevicePaginatedDataResult(BaseModel): + data: Optional[List[TagDevice]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class TagPaginatedDataResult(BaseModel): + data: Optional[List[Tag]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Timeline(BaseModel): + id: Optional[int] = None + type: Optional[int] = None + data: Optional[str] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + households: Optional[List[Household]] = None + devices: Optional[List[Device]] = None + movements: Optional[List[Movement]] = None + pets: Optional[List[Pet]] = None + tags: Optional[List[Tag]] = None + users: Optional[List[PublicUser]] = None + weights: Optional[List[Weight]] = None + + +class TimelinePaginatedDataResult(BaseModel): + data: Optional[List[Timeline]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Timezone(BaseModel): + id: int + name: Optional[str] = None + timezone: Optional[str] = None + utc_offset: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class TimezoneDataResponse(BaseModel): + data: Optional[Timezone] = None + + +class TimezonePaginatedDataResult(BaseModel): + data: Optional[List[Timezone]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class UpdateDevice(BaseModel): + name: str + + +class UpdateDeviceTag(BaseModel): + profile: Optional[SpecialProfiles] = None + + +class UpdateHousehold(BaseModel): + name: Optional[str] = None + timezone_id: Optional[int] = None + + +class UpdateHouseholdInvite(BaseModel): + owner: Optional[bool] = None + write: Optional[bool] = None + + +class UpdateHouseholdUser(BaseModel): + owner: Optional[bool] = None + write: Optional[bool] = None + + +class UpdateMe(BaseModel): + email_address: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + language_id: Optional[int] = None + country_id: Optional[int] = None + photo_id: Optional[int] = None + marketing_opt_in: Optional[bool] = None + weight_units: Optional[UserWeightUnit] = None + time_format: Optional[UserTimeFormat] = None + notifications: Optional[dict] = None + password: Optional[str] = None + + +class UpdatePet(BaseModel): + name: str + gender: Optional[PetGender] = None + date_of_birth: Optional[datetime] = None + weight: Optional[float] = None + comments: Optional[str] = None + breed_id: Optional[int] = None + breed_id2: Optional[int] = None + spayed: Optional[Spayed] = None + food_type_id: Optional[int] = None + photo_id: Optional[int] = None + species_id: Optional[int] = None + conditions: Optional[List[Condition]] = None + + +class UpdatePhoto(BaseModel): + title: Optional[str] = None + + +class UpdateUserSettings(BaseModel): + value: str + + +class User(BaseModel): + id: Optional[int] = None + email_address: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + country_id: Optional[int] = None + language_id: Optional[int] = None + photo_id: Optional[int] = None + marketing_opt_in: Optional[bool] = None + terms_accepted: Optional[datetime] = None + weight_units: Optional[int] = None + time_format: Optional[int] = None + notifications: Optional[dict] = None + photo: Optional[Photo] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + use_colour: Optional[str] = None + segments: Optional[List[str]] = None + + +class UserClient(BaseModel): + platform: Optional[UserClientPlatform] = None + token: Optional[str] = None + + +class UserClientDataResponse(BaseModel): + data: Optional[UserClient] = None + + +class UserClientPaginatedDataResult(BaseModel): + data: Optional[List[UserClient]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class UserClientPlatform(BaseModel): + app: Optional[UserClientPlatformApp] = None + device: Optional[UserClientPlatformDevice] = None + locale: Optional[UserClientPlatformLocale] = None + + +class UserClientPlatformApp(BaseModel): + bundle_identifier: Optional[str] = None + version: Optional[str] = None + + +class UserClientPlatformDevice(BaseModel): + name: Optional[str] = None + model: Optional[UserClientPlatformDeviceModel] = None + uuid: Optional[str] = None + os: Optional[UserClientPlatformDeviceOs] = None + + +class UserClientPlatformDeviceModel(BaseModel): + name: Optional[str] = None + manufacturer: Optional[str] = None + version: Optional[str] = None + + +class UserClientPlatformDeviceOs(BaseModel): + platform: Optional[str] = None + version: Optional[str] = None + + +class UserClientPlatformLocale(BaseModel): + language: Optional[str] = None + country: Optional[str] = None + + +class UserDataResponse(BaseModel): + data: Optional[User] = None + + +class UserSetting(BaseModel): + id: Optional[int] = None + user_id: Optional[int] = None + key: Optional[str] = None + value: Optional[str] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class UserSettingDataResponse(BaseModel): + data: Optional[UserSetting] = None + + +class UserSettingPaginatedDataResult(BaseModel): + data: Optional[List[UserSetting]] = None + meta: Optional[PaginatedMetaDataResult] = None + + +class Weight(BaseModel): + id: Optional[int] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + context: Optional[int] = None + duration: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + frames: Optional[List[WeightFrame]] = None + + +class WeightFrame(BaseModel): + id: Optional[int] = None + index: Optional[int] = None + current_weight: Optional[float] = None + change: Optional[float] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None diff --git a/surehub_api/entities/official_v2.py b/surehub_api/entities/official_v2.py index a6612c2..d6468e1 100644 --- a/surehub_api/entities/official_v2.py +++ b/surehub_api/entities/official_v2.py @@ -1,35 +1,511 @@ -from enum import IntEnum -from typing import Optional +from __future__ import annotations -from pydantic import BaseModel +from datetime import datetime, date +from typing import Any, Optional, List +from pydantic import BaseModel, Field -# TODO: Add descriptive names to device tag actions -class DeviceTagAction(IntEnum): - ACTION_0 = 0 - ACTION_1 = 1 - ACTION_2 = 2 +from surehub_api.entities.enums import ( + ChangeProfileAction, + ConsumptionHabitOutcome, + DeviceTagProfiles, + DoorDirection, + DoorSide, + DoorStatus, + DualScanLockingMode, + FailSafeOptions, + FeederBowlType, + FoodTypes, + LedMode, + PairingMode, + PetDoorLockingMode, + PetDoorTagType, + ReportHouseholdEvent, + RequestChangeStateResponseStatus, + SpecialProfiles, + SubstanceTypes, + ThalamusMovementTimedAccessAllowedSpecialProfiles, + ThalamusTagType, + TrainingMode, + UpdateDeviceTagActions, + ZeroAction, +) -class DeviceTagProfile(IntEnum): - DISABLED = 2 - ENABLED = 3 +class ConsumptionAlert(BaseModel): + pet_id: Optional[int] = None + tag_id: Optional[int] = None + pet_weight: Optional[int] = None + amount: Optional[int] = None + time_noticed_utc: Optional[datetime] = None + created_at: Optional[datetime] = None + + +class ConsumptionHabit(BaseModel): + outcome: Optional[ConsumptionHabitOutcome] = None + calendar_day: Optional[date] = None + amount: Optional[int] = None + lower_limit: Optional[int] = None + upper_limit: Optional[int] = None + created_at: Optional[datetime] = None + + +class DeviceControlCurfew(BaseModel): + enabled: Optional[bool] = None + lock_time: Optional[str] = None + unlock_time: Optional[str] = None + + +class DeviceControlDualScanPetDoorV2(BaseModel): + fast_polling: Optional[bool] = None + tag_profiles: Optional[List[DeviceControlThalamusTagProfile]] = None + timed_access: Optional[List[DeviceControlThalamusMovementTagTimedAccess]] = None + locking: Optional[DualScanLockingMode] = None + lockdown: Optional[bool] = None + timed_access_override: Optional[bool] = None + fail_safe: Optional[FailSafeOptions] = None + + +class DeviceControlDualScanPetDoorV2DeviceControl(BaseModel): + data: Optional[DeviceControlDualScanPetDoorV2] = None + pending: Optional[List[DeviceControlDualScanPetDoorV2DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlDualScanPetDoorV2DeviceControlPending(BaseModel): + state: Optional[DeviceControlDualScanPetDoorV2] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlDualScanV2(BaseModel): + fast_polling: Optional[bool] = None + tag_profiles: Optional[List[DeviceControlThalamusTagProfile]] = None + timed_access: Optional[List[DeviceControlThalamusMovementTagTimedAccess]] = None + locking: Optional[DualScanLockingMode] = None + lockdown: Optional[bool] = None + fail_safe: Optional[FailSafeOptions] = None + + +class DeviceControlDualScanV2DeviceControl(BaseModel): + data: Optional[DeviceControlDualScanV2] = None + pending: Optional[List[DeviceControlDualScanV2DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlDualScanV2DeviceControlPending(BaseModel): + state: Optional[DeviceControlDualScanV2] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlFeederBowl(BaseModel): + settings: Optional[List[DeviceControlFeederBowlSettings]] = None + type: Optional[FeederBowlType] = None + + +class DeviceControlFeederBowlSettings(BaseModel): + food_type: Optional[FoodTypes] = None + target: Optional[float] = None + + +class DeviceControlFeederLid(BaseModel): + close_delay: Optional[int] = None + + +class DeviceControlFeederTagTimedFeeding(BaseModel): + tag_id: Optional[int] = None + fasting: Optional[List[DeviceControlFeederTimedFeeding]] = None + + +class DeviceControlFeederTimedFeeding(BaseModel): + enabled: Optional[bool] = None + start_time: Optional[str] = None + end_time: Optional[str] = None + + +class DeviceControlFeederV2(BaseModel): + fast_polling: Optional[bool] = None + tag_profiles: Optional[List[DeviceControlThalamusTagProfile]] = None + bowls: Optional[DeviceControlFeederBowl] = None + lid: Optional[DeviceControlFeederLid] = None + tare: Optional[ZeroAction] = None + training_mode: Optional[TrainingMode] = None + timed_feeding: Optional[List[DeviceControlFeederTagTimedFeeding]] = None + + +class DeviceControlFeederV2DeviceControl(BaseModel): + data: Optional[DeviceControlFeederV2] = None + pending: Optional[List[DeviceControlFeederV2DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlFeederV2DeviceControlPending(BaseModel): + state: Optional[DeviceControlFeederV2] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlHub(BaseModel): + led_mode: Optional[LedMode] = None + pairing_mode: Optional[PairingMode] = None + flash_leds: Optional[bool] = None + + +class DeviceControlHubDeviceControl(BaseModel): + data: Optional[DeviceControlHub] = None + pending: Optional[List[DeviceControlHubDeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlHubDeviceControlPending(BaseModel): + state: Optional[DeviceControlHub] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlNoIdDogBowl(BaseModel): + fast_polling: Optional[bool] = None + tag_profiles: Optional[List[DeviceControlThalamusTagProfile]] = None + food_type: Optional[FoodTypes] = None + substance_type: Optional[SubstanceTypes] = None + + +class DeviceControlNoIdDogBowlDeviceControl(BaseModel): + data: Optional[DeviceControlNoIdDogBowl] = None + pending: Optional[List[DeviceControlNoIdDogBowlDeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlNoIdDogBowlDeviceControlPending(BaseModel): + state: Optional[DeviceControlNoIdDogBowl] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlPending(BaseModel): + state: Optional[Any] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlPetDoor(BaseModel): + fast_polling: Optional[bool] = None + curfew: Optional[DeviceControlCurfew] = None + locking: Optional[PetDoorLockingMode] = None + tag_profiles: Optional[List[DeviceControlPetDoorTagProfile]] = None + + +class DeviceControlPetDoorDeviceControl(BaseModel): + data: Optional[DeviceControlPetDoor] = None + pending: Optional[List[DeviceControlPetDoorDeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlPetDoorDeviceControlPending(BaseModel): + state: Optional[DeviceControlPetDoor] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + + +class DeviceControlPetDoorMicrochip(BaseModel): + microchip_number: Optional[str] = None + type: Optional[PetDoorTagType] = None + + +class DeviceControlPetDoorTagProfile(BaseModel): + tag_id: Optional[int] = None + index: Optional[int] = None + microchip: Optional[DeviceControlPetDoorMicrochip] = None + + +class DeviceControlPoseidon(BaseModel): + fast_polling: Optional[bool] = None + tag_profiles: Optional[List[DeviceControlThalamusTagProfile]] = None + learn_mode: Optional[bool] = None + + +class DeviceControlPoseidonDeviceControl(BaseModel): + data: Optional[DeviceControlPoseidon] = None + pending: Optional[List[DeviceControlPoseidonDeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceControlPoseidonDeviceControlPending(BaseModel): + state: Optional[DeviceControlPoseidon] = None + request_id: Optional[str] = None + requested_at: Optional[datetime] = None + requested_by: Optional[str] = None + +class DeviceControlResult(BaseModel): + request_id: Optional[str] = None + response_id: Optional[str] = None + status: Optional[RequestChangeStateResponseStatus] = None + status_id: Optional[RequestChangeStateResponseStatus] = None + requested_at: Optional[datetime] = None + committed_at: Optional[datetime] = None -class ThalamusMovementTimedAccessAllowedSpecialProfile(IntEnum): - SPECIAL_PROFILE_3 = 3 - SPECIAL_PROFILE_5 = 5 - SPECIAL_PROFILE_6 = 6 + +class DeviceControlThalamusMicrochip(BaseModel): + microchip_number: Optional[str] = None + type: Optional[ThalamusTagType] = None + + +class DeviceControlThalamusMovementTagTimedAccess(BaseModel): + tag_id: Optional[int] = None + timed_access: Optional[List[DeviceControlThalamusMovementTimedAccess]] = None class DeviceControlThalamusMovementTimedAccess(BaseModel): - profile: ThalamusMovementTimedAccessAllowedSpecialProfile + profile: Optional[ThalamusMovementTimedAccessAllowedSpecialProfiles] lock_time: Optional[str] = None unlock_time: Optional[str] = None +class DeviceControlThalamusTagProfile(BaseModel): + tag_id: Optional[int] = None + index: Optional[int] = None + profile: Optional[SpecialProfiles] = None + action: Optional[ChangeProfileAction] = None + request_action: Optional[UpdateDeviceTagActions] = None + microchip: Optional[DeviceControlThalamusMicrochip] = None + + +class DeviceTag(BaseModel): + id: Optional[int] = None + device_id: Optional[int] = None + index: Optional[int] = None + profile: Optional[int] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class DeviceTagData(BaseModel): + data: Optional[DeviceTag] = None + pending: Optional[List[DeviceControlPending]] = None + results: Optional[List[DeviceControlResult]] = None + + +class DeviceV2(BaseModel): + id: Optional[int] = None + + +class Error(BaseModel): + success: Optional[bool] = None + error: Optional[dict] = None + + +class HouseholdV2(BaseModel): + id: Optional[int] = None + + +class Movement(BaseModel): + id: Optional[int] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + direction: Optional[DoorDirection] = None + side: Optional[DoorSide] = None + type: Optional[DoorStatus] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class PaginatedMetaDataResult(BaseModel): + page: Optional[int] = None + page_size: Optional[int] = None + count: Optional[int] = None + total_pages: Optional[int] = None + + +class PetV2(BaseModel): + id: Optional[int] = None + + +class Photo(BaseModel): + id: Optional[int] = None + title: Optional[str] = None + location: Optional[str] = None + hash: Optional[str] = None + uploading_user_id: Optional[int] = None + version: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class ProblemDetails(BaseModel): + type: Optional[str] = None + title: Optional[str] = None + status: Optional[int] = None + detail: Optional[str] = None + instance: Optional[str] = None + + +class PublicUser(BaseModel): + id: Optional[int] = None + name: Optional[str] = None + photo_id: Optional[int] = None + photo: Optional[Photo] = None + + +class ReportHousehold(BaseModel): + pet_id: Optional[int] = None + device_id: Optional[int] = None + movement: Optional[ReportHouseholdMovementDataPoint] = None + feeding: Optional[ReportHouseholdFeedingDataPoint] = None + drinking: Optional[ReportHouseholdDrinkingDataPoint] = None + consumption_habit: Optional[List[ConsumptionHabit]] = None + consumption_alert: Optional[List[ConsumptionAlert]] = None + + +class ReportHouseholdDataResponse(BaseModel): + data: Optional[ReportHousehold] = None + + +class ReportHouseholdDrinking(BaseModel): + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + context: Optional[int] = None + bowl_count: Optional[int] = None + device_id: Optional[int] = None + weights: Optional[List[ReportWeightFrame]] = None + actual_weight: Optional[float] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + + +class ReportHouseholdDrinkingDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdDrinking]] = None + + +class ReportHouseholdFeeding(BaseModel): + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + context: Optional[int] = None + bowl_count: Optional[int] = None + device_id: Optional[int] = None + weights: Optional[List[ReportWeightFrame]] = None + actual_weight: Optional[float] = None + entry_user_id: Optional[int] = None + exit_user_id: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + + +class ReportHouseholdFeedingDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdFeeding]] = None + + +class ReportHouseholdMovement(BaseModel): + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + deleted_at: Optional[datetime] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + user_id: Optional[int] = None + from_: Optional[datetime] = Field(default=None, alias='from') + to: Optional[datetime] = None + duration: Optional[int] = None + entry_device_id: Optional[int] = None + entry_user_id: Optional[int] = None + exit_device_id: Optional[int] = None + exit_user_id: Optional[int] = None + active: Optional[bool] = None + exit_movement_id: Optional[int] = None + entry_movement_id: Optional[int] = None + + +class ReportHouseholdMovementDataPoint(BaseModel): + datapoints: Optional[List[ReportHouseholdMovement]] = None + + +class ReportHouseholdV2Query(BaseModel): + from_: datetime = Field(alias='from') + to: datetime + event_type: Optional[ReportHouseholdEvent] = None + + +class ReportWeightFrame(BaseModel): + index: Optional[int] = None + weight: Optional[float] = None + change: Optional[float] = None + food_type_id: Optional[int] = None + target_weight: Optional[int] = None + multi: Optional[bool] = None + + +class TagV2(BaseModel): + id: Optional[int] = None + + +class TimelineV2(BaseModel): + id: Optional[int] = None + type: Optional[int] = None + data: Optional[str] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + household: Optional[List[HouseholdV2]] = None + devices: Optional[List[DeviceV2]] = None + movements: Optional[List[Movement]] = None + pets: Optional[List[PetV2]] = None + tags: Optional[List[TagV2]] = None + users: Optional[List[PublicUser]] = None + weights: Optional[List[Weight]] = None + + +class TimelineV2PaginatedDataResult(BaseModel): + data: Optional[List[TimelineV2]] = None + meta: Optional[PaginatedMetaDataResult] = None + + class UpdateDeviceTag(BaseModel): tag_id: Optional[int] = None - request_action: DeviceTagAction - profile: DeviceTagProfile + request_action: UpdateDeviceTagActions + profile: DeviceTagProfiles timed_access: Optional[DeviceControlThalamusMovementTimedAccess] = None + + +class UpdateDeviceTagV2(BaseModel): + tag_id: Optional[int] = None + request_action: Optional[UpdateDeviceTagActions] = None + profile: Optional[DeviceTagProfiles] = None + timed_access: Optional[List[DeviceControlThalamusMovementTimedAccess]] = None + + +class Weight(BaseModel): + id: Optional[int] = None + device_id: Optional[int] = None + tag_id: Optional[int] = None + context: Optional[int] = None + duration: Optional[int] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + frames: Optional[List[WeightFrame]] = None + + +class WeightFrame(BaseModel): + id: Optional[int] = None + index: Optional[int] = None + current_weight: Optional[float] = None + change: Optional[float] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None diff --git a/surehub_api/services/auth.py b/surehub_api/services/auth.py index 78e4560..982b1cd 100644 --- a/surehub_api/services/auth.py +++ b/surehub_api/services/auth.py @@ -1,11 +1,11 @@ import requests from cachetools import TTLCache -from fastapi import HTTPException from surehub_api.config import settings +from surehub_api.entities import official +from surehub_api.utils import response_handler DEFAULT_HEADERS = { - "Host": "app-api.production.surehub.io", "Accept": "application/json, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "en-US,en-GB;q=0.9", @@ -27,17 +27,20 @@ def _get_token() -> str: token = cache.get("token") if not token: - payload = { - "email_address": settings.email, - "password": settings.password, - "device_id": "web", - } - response = requests.post(f"{settings.endpoint}/api/auth/login", json=payload, headers=DEFAULT_HEADERS) - - if response.ok: - token = response.json()["data"]["token"] - cache["token"] = token - else: - raise HTTPException(status_code=response.status_code, detail=response.text.replace("\"", "'")) + auth_login = official.AuthLogin( + device_id="web", + email_address=settings.email, + password=settings.password, + ) + + response = requests.post( + f"{settings.endpoint}/api/auth/login", + json=auth_login.model_dump(mode='json'), + headers=DEFAULT_HEADERS + ) + auth_token = response_handler.parse(response, model=official.AuthToken) + + token = auth_token.token + cache["token"] = token return token diff --git a/surehub_api/services/dashboard.py b/surehub_api/services/dashboard.py index f5f036c..d046ad1 100644 --- a/surehub_api/services/dashboard.py +++ b/surehub_api/services/dashboard.py @@ -3,11 +3,11 @@ from surehub_api.config import settings from surehub_api.entities import official from surehub_api.services import auth -from surehub_api.utils import http_utils +from surehub_api.utils import response_handler def get_dashboard() -> official.MeStart: uri = f"{settings.endpoint}/api/me/start" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.MeStart) diff --git a/surehub_api/services/devices.py b/surehub_api/services/devices.py index e7aa141..bc24d59 100644 --- a/surehub_api/services/devices.py +++ b/surehub_api/services/devices.py @@ -1,11 +1,11 @@ -from typing import List, Any +from typing import List import requests from surehub_api.config import settings from surehub_api.entities import official, custom from surehub_api.services import auth -from surehub_api.utils import http_utils +from surehub_api.utils import response_handler DEVICE_TYPES_SUPPORTING_INDOOR_ONLY_MODE = [ official.DeviceType.DUALSCAN_PET_DOOR_CONNECT, @@ -25,21 +25,21 @@ def get_devices( params["HouseholdId"] = household_ids response = requests.get(uri, headers=auth.auth_headers(), params=params) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Device]) def get_device_by_id(device_id: int) -> official.Device: uri = f"{settings.endpoint}/api/device/{device_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Device) -def get_device_state_by_id(device_id) -> Any: +def get_device_state_by_id(device_id) -> official.DeviceControl: uri = f"{settings.endpoint}/api/device/{device_id}/control" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.DeviceControl) def set_lock_mode(device_id: int, lock_mode: custom.LockMode) -> official.DeviceControl: @@ -50,14 +50,14 @@ def set_lock_mode(device_id: int, lock_mode: custom.LockMode) -> official.Device } response = requests.put(uri, headers=auth.auth_headers(), json=data) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.DeviceControl) def get_tags_of_device(device_id: int) -> List[official.DeviceTag]: - uri = f"{settings.ENDPOINT}/api/device/{device_id}/tag" + uri = f"{settings.endpoint}/api/device/{device_id}/tag" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Tag]) def update_device_state(device_id: int, device_state: official.DeviceControl) -> official.DeviceControl: uri = f"{settings.endpoint}/api/device/{device_id}/control" @@ -67,25 +67,25 @@ def update_device_state(device_id: int, device_state: official.DeviceControl) -> def get_tag_of_device(device_id: int, tag_id: int) -> official.DeviceTag: - uri = f"{settings.ENDPOINT}/api/device/{device_id}/tag/{tag_id}" + uri = f"{settings.endpoint}/api/device/{device_id}/tag/{tag_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Tag) def assign_tag_to_device(device_id: int, tag_id: int) -> official.DeviceTag: - uri = f"{settings.ENDPOINT}/api/device/{device_id}/tag/{tag_id}" + uri = f"{settings.endpoint}/api/device/{device_id}/tag/{tag_id}" data = { - "profile": official.SpecialProfile.SPECIAL_PROFILE_0 # It is currently not known what this is for + "profile": official.SpecialProfiles.SPECIAL_PROFILE_0 # It is currently not known what this is for } response = requests.put(uri, headers=auth.auth_headers(), json=data) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Tag) def remove_tag_from_device(device_id: int, tag_id: int) -> official.DeviceTag: - uri = f"{settings.ENDPOINT}/api/device/{device_id}/tag/{tag_id}" + uri = f"{settings.endpoint}/api/device/{device_id}/tag/{tag_id}" response = requests.delete(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Tag) diff --git a/surehub_api/services/households.py b/surehub_api/services/households.py index 3ba6401..274b723 100644 --- a/surehub_api/services/households.py +++ b/surehub_api/services/households.py @@ -5,60 +5,60 @@ from surehub_api.config import settings from surehub_api.entities import official from surehub_api.services import auth -from surehub_api.utils import http_utils +from surehub_api.utils import response_handler -def get_households() -> list: +def get_households() -> List[official.Household]: uri = f"{settings.endpoint}/api/household" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Household]) def get_household_by_id(household_id: int) -> official.Household: uri = f"{settings.endpoint}/api/household/{household_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Household) def get_users_of_household(household_id: int) -> List[official.HouseholdUser]: uri = f"{settings.endpoint}/api/household/{household_id}/user" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.HouseholdUser]) def get_user_of_household(household_id: int, user_id: int) -> official.HouseholdUser: uri = f"{settings.endpoint}/api/household/{household_id}/user/{user_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.HouseholdUser) def get_pets_of_household(household_id: int) -> List[official.Pet]: uri = f"{settings.endpoint}/api/household/{household_id}/pet" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Pet]) def get_pet_of_household(household_id: int, pet_id: int) -> official.Pet: uri = f"{settings.endpoint}/api/household/{household_id}/pet/{pet_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Pet) -def get_devices_of_household(household_id: int) -> list: +def get_devices_of_household(household_id: int) -> List[official.Device]: uri = f"{settings.endpoint}/api/household/{household_id}/device" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Device]) def get_device_of_household(household_id: int, device_id: int) -> official.Device: uri = f"{settings.endpoint}/api/household/{household_id}/device/{device_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Device) diff --git a/surehub_api/services/pets.py b/surehub_api/services/pets.py index 3d99dd1..e783d91 100644 --- a/surehub_api/services/pets.py +++ b/surehub_api/services/pets.py @@ -8,25 +8,25 @@ from surehub_api.config import settings from surehub_api.entities import official, dto, official_v2 from surehub_api.services import auth, devices -from surehub_api.utils import http_utils +from surehub_api.utils import response_handler def get_pets() -> List[official.Pet]: uri = f"{settings.endpoint}/api/pet" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=List[official.Pet]) def get_pet(pet_id: int) -> official.Pet: uri = f"{settings.endpoint}/api/pet/{pet_id}" response = requests.get(uri, headers=auth.auth_headers()) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.Pet) def get_pet_state(pet_id: int) -> dto.PetStateResponse: - pet = official.Pet.model_validate(get_pet(pet_id)) + pet = get_pet(pet_id) return dto.PetStateResponse( position=pet.position, @@ -56,11 +56,11 @@ def _update_pet_position(pet_id: int, position: official.PetPositionWhere) -> No ) response = requests.post(uri, headers=auth.auth_headers(), json=payload.model_dump(mode='json')) - http_utils.raise_for_status(response) + response_handler.raise_for_status(response) def _update_indoor_only_mode(pet_id: int, indoor_only: bool, household_ids: List[int] | None = None) -> None: - pet = official.Pet.model_validate(get_pet(pet_id)) + pet = get_pet(pet_id) if not pet.tag_id: raise HTTPException( @@ -68,8 +68,7 @@ def _update_indoor_only_mode(pet_id: int, indoor_only: bool, household_ids: List detail=f"Failed to update indoor mode, because pet with id {pet_id} has no associated tag" ) - supported_devices = [official.Device.model_validate(device) - for device in devices.get_devices(household_ids=household_ids) + supported_devices = [device for device in devices.get_devices(household_ids=household_ids) if device.get("product_id") in devices.DEVICE_TYPES_SUPPORTING_INDOOR_ONLY_MODE] if not supported_devices: @@ -78,8 +77,8 @@ def _update_indoor_only_mode(pet_id: int, indoor_only: bool, household_ids: List detail=f"Failed to update indoor mode for pet id {pet_id}, because no devices supporting indoor-only mode were found", ) - request_action = official_v2.DeviceTagAction.ACTION_0 - profile = official_v2.DeviceTagProfile.ENABLED if indoor_only else official_v2.DeviceTagProfile.DISABLED + request_action = official_v2.UpdateDeviceTagActions.VALUE_0 + profile = official_v2.DeviceTagProfiles.ENABLED if indoor_only else official_v2.DeviceTagProfiles.DISABLED for device in supported_devices: uri = f"{settings.endpoint}/api/v2/device/{device.id}/tag/async" @@ -91,12 +90,12 @@ def _update_indoor_only_mode(pet_id: int, indoor_only: bool, household_ids: List ) response = requests.put(uri, headers=auth.auth_headers(), json=[payload.model_dump(mode='json')]) - http_utils.raise_for_status(response) + response_handler.raise_for_status(response) def get_pet_position(pet_id: int) -> official.PetPosition: pet = get_pet(pet_id) - pet_position = pet.get('position') + pet_position = pet.position if not pet_position: raise HTTPException(status_code=500, detail=f"Invalid position '{pet_position}' for pet_id {pet_id}") @@ -108,10 +107,10 @@ def get_pet_positions() -> List[official.PetPosition]: pet_positions = [] for pet in get_pets(): - pet_position = pet.get('position') + pet_position = pet.position if not pet_position: - raise HTTPException(status_code=500, detail=f"Invalid position '{pet_position}' for pet_id {pet.get('id')}") + raise HTTPException(status_code=500, detail=f"Invalid position '{pet_position}' for pet_id {pet.id}") pet_positions.append(pet_position) @@ -127,4 +126,4 @@ def set_pet_position(pet_id: int, pet_position: official.CreatePetPosition) -> o pet_position_dict['since'] = datetime.now(timezone.utc).isoformat() response = requests.post(uri, headers=auth.auth_headers(), json=pet_position_dict) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.PetPosition) diff --git a/surehub_api/services/reports.py b/surehub_api/services/reports.py index 646fd39..53fc9c3 100644 --- a/surehub_api/services/reports.py +++ b/surehub_api/services/reports.py @@ -5,7 +5,7 @@ from surehub_api.config import settings from surehub_api.entities import official from surehub_api.services import auth -from surehub_api.utils import http_utils +from surehub_api.utils import response_handler def get_pet_report(household_id: int, pet_id: int, from_datetime: datetime, @@ -18,4 +18,4 @@ def get_pet_report(household_id: int, pet_id: int, from_datetime: datetime, } response = requests.get(uri, headers=auth.auth_headers(), params=params) - return http_utils.extract_response_data(response) + return response_handler.parse(response, model=official.PetReport) diff --git a/surehub_api/services/timeline.py b/surehub_api/services/timeline.py index 37d3400..2096e90 100644 --- a/surehub_api/services/timeline.py +++ b/surehub_api/services/timeline.py @@ -1,4 +1,3 @@ -import json import math import requests @@ -6,6 +5,7 @@ from surehub_api.config import settings from surehub_api.services import auth +from surehub_api.utils import response_handler def get_timeline_of_household(household_id: int) -> list: @@ -15,23 +15,18 @@ def get_timeline_of_household(household_id: int) -> list: fetch_size = 100 response = requests.get(uri, headers=auth.auth_headers()) - if response.ok: - data = json.loads(response.text) - count = data['meta']['count'] - page_size = data['meta']['page_size'] - - request_count = math.ceil(count / page_size) - - for i in range(1, request_count + 1): - payload = {'page_size': fetch_size, 'page': i} - response2 = requests.get(uri, headers=auth.auth_headers(), params=payload) - - if response2.ok: - page = json.loads(response2.text) - result += page['data'] - else: - raise HTTPException(status_code=response.status_code, detail=response2.text.replace("\"", "'")) - - return result - else: - raise HTTPException(status_code=response.status_code, detail=response.text.replace("\"", "'")) + meta = response_handler.parse(response, key='meta') + count = meta.get('count') + page_size = meta.get('page_size') + + if count is None or page_size in (None, 0): + raise HTTPException(status_code=500, detail="Invalid response format: missing timeline meta fields") + + request_count = math.ceil(count / page_size) + + for i in range(1, request_count + 1): + payload = {'page_size': fetch_size, 'page': i} + response2 = requests.get(uri, headers=auth.auth_headers(), params=payload) + result += response_handler.parse(response2) + + return result diff --git a/surehub_api/utils/http_utils.py b/surehub_api/utils/response_handler.py similarity index 55% rename from surehub_api/utils/http_utils.py rename to surehub_api/utils/response_handler.py index b770ac6..17cf3aa 100644 --- a/surehub_api/utils/http_utils.py +++ b/surehub_api/utils/response_handler.py @@ -1,8 +1,12 @@ +from logging import getLogger from typing import Any from fastapi import HTTPException +from pydantic import TypeAdapter, ValidationError from requests import Response +logger = getLogger(__name__) + def raise_for_status(response: Response) -> None: """Raise FastAPI HTTPException if response indicates an error. @@ -10,11 +14,13 @@ def raise_for_status(response: Response) -> None: :param response: requests.Response object :raise HTTPException: If status code indicates client or server error """ + _log_request(response) + if not response.ok: raise HTTPException(status_code=response.status_code, detail=_extract_error_detail(response)) -def extract_response_data(response: Response, key: str = "data") -> Any: +def parse(response: Response, key: str = "data", model: Any | None = None) -> Any: """ Validates an HTTP response and raises HTTPException on errors. @@ -23,7 +29,8 @@ def extract_response_data(response: Response, key: str = "data") -> Any: - Extracts error details from response body when available :param response: requests.Response object - :param key: root level key to extract payload + :param key: root level key to extract payload (default: "data") + :param model: Pydantic model class to validate the extracted payload against (optional) :raise HTTPException: If status code indicates client or server error """ @@ -37,7 +44,15 @@ def extract_response_data(response: Response, key: str = "data") -> Any: if key not in payload: raise HTTPException(status_code=500, detail=f"Invalid response format: missing '{key}'") - return payload[key] + data = payload[key] + + if model is None: + return data + + try: + return TypeAdapter(model).validate_python(data) + except ValidationError as ex: + raise HTTPException(status_code=500, detail=f"Invalid response format: {ex.errors()}") def _extract_error_detail(response) -> str | dict: @@ -45,3 +60,14 @@ def _extract_error_detail(response) -> str | dict: return response.json() except ValueError: return response.text.replace('"', "'") + + +def _log_request(response: Response) -> None: + method = response.request.method + url = response.request.url + status_code = response.status_code + + if 200 <= status_code < 300: + logger.info("External API request %s %s returned %s", method, url, status_code) + elif 400 <= status_code < 600: + logger.error("External API request %s %s returned %s", method, url, status_code)