diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65971607..09096a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: github_access_token: ${{ secrets.GITHUB_TOKEN }} - name: Clone Repository uses: actions/checkout@v4 + - name: Type check + run: nix develop --command mypy ./realtime - name: Start Supabase local development setup run: nix develop --command supabase start --workdir infra -x studio,mailpit,edge-runtime,logflare,vector,supavisor,imgproxy,storage-api - name: Run python tests through nix diff --git a/Makefile b/Makefile index eff0a51b..bdc6e4eb 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,14 @@ install_poetry: curl -sSL https://install.python-poetry.org | python - poetry install -tests: install tests_only tests_pre_commit +tests: install run_mypy tests_only tests_pre_commit tests_pre_commit: poetry run pre-commit run --all-files +run_mypy: + poetry run mypy ./realtime + run_infra: npx supabase start --workdir infra -x studio,mailpit,edge-runtime,logflare,vector,supavisor,imgproxy,storage-api diff --git a/poetry.lock b/poetry.lock index f0d600dc..a3129aef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -137,6 +137,18 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "async-timeout" version = "5.0.1" @@ -987,6 +999,140 @@ files = [ {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, ] +[[package]] +name = "pydantic" +version = "2.11.7" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.19.2" @@ -1321,6 +1467,21 @@ files = [ {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "ujson" version = "5.10.0" @@ -1651,4 +1812,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "fd7d0314902a95f7ec46e16d1709c7aad642f86d6806fff7d09b6762aaa19800" +content-hash = "828e1d71cd1cf2de0944a4ef62578b1b74e07ecd2c2f0688b2ee0443886dfa4a" diff --git a/pyproject.toml b/pyproject.toml index f1279a5a..6a6eadde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ requires-python = ">=3.9" dependencies = [ "websockets >=11,<16", "typing-extensions >=4.14.0", + "pydantic (>=2.11.7,<3.0.0)", ] [tool.poetry.group.dev.dependencies] @@ -77,3 +78,13 @@ keep-runtime-typing = true [tool.pytest.ini_options] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" + +[tool.mypy] +python_version = "3.9" +check_untyped_defs = true +allow_redefinition = true + +warn_return_any = true +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true diff --git a/realtime/_async/channel.py b/realtime/_async/channel.py index 33305dbe..92e0219a 100644 --- a/realtime/_async/channel.py +++ b/realtime/_async/channel.py @@ -3,7 +3,7 @@ import asyncio import json import logging -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional from realtime.types import ( Binding, @@ -16,6 +16,7 @@ RealtimeSubscribeStates, ) +from ..message import Message from ..transformers import http_endpoint_url from .presence import ( AsyncRealtimePresence, @@ -52,23 +53,27 @@ def __init__( :param params: Optional parameters for connection. """ self.socket = socket - self.params = params or {} - if self.params.get("config") is None: - self.params["config"] = { - "broadcast": {"ack": False, "self": False}, - "presence": {"key": ""}, - "private": False, + self.params: RealtimeChannelOptions = ( + params + if params + else { + "config": { + "broadcast": {"ack": False, "self": False}, + "presence": {"key": ""}, + "private": False, + } } + ) self.topic = topic self._joined_once = False - self.bindings: Dict[str, List[Binding]] = {} + self.bindings: dict[str, list[Binding]] = {} self.presence = AsyncRealtimePresence(self) self.state = ChannelStates.CLOSED - self._push_buffer: List[AsyncPush] = [] + self._push_buffer: list[AsyncPush] = [] self.timeout = self.socket.timeout - self.join_push = AsyncPush(self, ChannelEvents.join, self.params) + self.join_push: AsyncPush = AsyncPush(self, ChannelEvents.join, self.params) self.rejoin_timer = AsyncTimer( self._rejoin_until_connected, lambda tries: 2**tries ) @@ -111,8 +116,9 @@ def on_error(payload, *args): self._on("close", on_close) self._on("error", on_error) - def on_reply(payload, ref): - self._trigger(self._reply_event_name(ref), payload) + def on_reply(payload: Dict[str, Any], ref: Optional[str]): + if ref: + self._trigger(self._reply_event_name(ref), payload) self._on(ChannelEvents.reply, on_reply) @@ -169,22 +175,24 @@ async def subscribe( presence = config.get("presence", {}) private = config.get("private", False) - access_token_payload = {} - config = { - "broadcast": broadcast, - "presence": presence, - "private": private, - "postgres_changes": list( - map(lambda x: x.filter, self.bindings.get("postgres_changes", [])) - ), + config_payload: Dict[str, Any] = { + "config": { + "broadcast": broadcast, + "presence": presence, + "private": private, + "postgres_changes": list( + map( + lambda x: x.filter, + self.bindings.get("postgres_changes", []), + ) + ), + } } if self.socket.access_token: - access_token_payload["access_token"] = self.socket.access_token + config_payload["access_token"] = self.socket.access_token - self.join_push.update_payload( - {**{"config": config}, **access_token_payload} - ) + self.join_push.update_payload(config_payload) self._joined_once = True def on_join_push_ok(payload: Dict[str, Any]): @@ -253,7 +261,7 @@ def on_join_push_timeout(*args): return self - async def unsubscribe(self): + async def unsubscribe(self) -> None: """ Unsubscribe from the channel and leave the topic. Sets channel state to LEAVING and cleans up timers and pushes. @@ -263,9 +271,9 @@ async def unsubscribe(self): self.rejoin_timer.reset() self.join_push.destroy() - def _close(*args): + def _close(*args) -> None: logger.info(f"channel {self.topic} leave") - self._trigger(ChannelEvents.close, "leave") + self._trigger(ChannelEvents.close, {}) leave_push = AsyncPush(self, ChannelEvents.leave, {}) leave_push.receive("ok", _close).receive("timeout", _close) @@ -310,21 +318,24 @@ async def join(self) -> AsyncRealtimeChannel: :return: Channel """ try: - await self.socket.send( - { - "topic": self.topic, - "event": "phx_join", - "payload": {"config": self.params}, - "ref": None, - } + message = Message( + topic=self.topic, + event=ChannelEvents.join, + payload={"config": self.params}, + ref=None, ) + await self.socket.send(message) + return self except Exception as e: print(e) return self # Event handling methods def _on( - self, type: str, callback: Callback, filter: Optional[Dict[str, Any]] = None + self, + type: str, + callback: Callback[[Dict[str, Any], Optional[str]], None], + filter: Optional[Dict[str, Any]] = None, ) -> AsyncRealtimeChannel: """ Set up a listener for a specific event. @@ -411,7 +422,7 @@ def on_postgres_changes( ) def on_system( - self, callback: Callable[[Dict[str, Any], None]] + self, callback: Callable[[Dict[str, Any]], None] ) -> AsyncRealtimeChannel: """ Set up a listener for system events. @@ -508,7 +519,7 @@ def _can_push(self): async def send_presence(self, event: str, data: Any) -> None: await self.push(ChannelEvents.presence, {"event": event, "payload": data}) - def _trigger(self, type: str, payload: Optional[Any], ref: Optional[str] = None): + def _trigger(self, type: str, payload: Dict[str, Any], ref: Optional[str] = None): type_lowercase = type.lower() events = [ ChannelEvents.close, @@ -562,7 +573,7 @@ def _trigger(self, type: str, payload: Optional[Any], ref: Optional[str] = None) elif binding.type == type_lowercase: binding.callback(payload, ref) - def _reply_event_name(self, ref: str): + def _reply_event_name(self, ref: str) -> str: return f"chan_reply_{ref}" async def _rejoin_until_connected(self): diff --git a/realtime/_async/client.py b/realtime/_async/client.py index e862ea48..0287c2cf 100644 --- a/realtime/_async/client.py +++ b/realtime/_async/client.py @@ -3,12 +3,12 @@ import logging import re from functools import wraps -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Union from urllib.parse import urlencode, urlparse, urlunparse import websockets from websockets import connect -from websockets.client import ClientProtocol +from websockets.asyncio.client import ClientConnection from ..exceptions import NotConnectedError from ..message import Message @@ -62,7 +62,7 @@ def __init__( :param timeout: Connection timeout in seconds. Defaults to DEFAULT_TIMEOUT. """ if not is_ws_url(url): - ValueError("url must be a valid WebSocket URL or HTTP URL string") + raise ValueError("url must be a valid WebSocket URL or HTTP URL string") self.url = f"{re.sub(r'https://', 'wss://', re.sub(r'http://', 'ws://', url, flags=re.IGNORECASE), flags=re.IGNORECASE)}/websocket" if token: self.url += f"?apikey={token}" @@ -72,7 +72,7 @@ def __init__( self.access_token = token self.send_buffer: List[Callable] = [] self.hb_interval = hb_interval - self._ws_connection: Optional[ClientProtocol] = None + self._ws_connection: Optional[ClientConnection] = None self.ref = 0 self.auto_reconnect = auto_reconnect self.channels: Dict[str, AsyncRealtimeChannel] = {} @@ -97,13 +97,15 @@ async def _listen(self) -> None: try: async for msg in self._ws_connection: - logger.info(f"receive: {msg}") + logger.info(f"receive: {msg!r}") - msg = Message(**json.loads(msg)) - channel = self.channels.get(msg.topic) + message = Message.model_validate_json(msg) + channel = self.channels.get(message.topic) if channel: - channel._trigger(msg.event, msg.payload, msg.ref) + channel._trigger( + message.event, dict(**message.payload), message.ref + ) except websockets.exceptions.ConnectionClosedError as e: await self._on_connect_error(e) @@ -236,7 +238,7 @@ async def _heartbeat(self) -> None: while self.is_connected: try: - data = dict( + data = Message( topic=PHOENIX_CHANNEL, event=ChannelEvents.heartbeat, payload={}, @@ -294,14 +296,6 @@ async def remove_all_channels(self) -> None: await self.close() - def summary(self) -> None: - """ - Prints a list of topics and event the socket is listening to - :return: None - """ - for topic, channel in self.channels.items(): - print(f"Topic: {topic} | Events: {[e for e, _ in channel.listeners]}]") - async def set_auth(self, token: Optional[str]) -> None: """ Set the authentication token for the connection and update all joined channels. @@ -325,7 +319,7 @@ def _make_ref(self) -> str: self.ref += 1 return f"{self.ref}" - async def send(self, message: Dict[str, Any]) -> None: + async def send(self, message: Union[Message, Dict[str, Any]]) -> None: """ Send a message through the WebSocket connection. @@ -340,16 +334,22 @@ async def send(self, message: Dict[str, Any]) -> None: Returns: None """ - - message = json.dumps(message) - logger.info(f"send: {message}") + if isinstance(message, Message): + msg = message + else: + logger.warning( + "Warning: calling AsyncRealtimeClient.send with a dictionary is deprecated. Please call it with a Message object instead. This will be a hard error in the future." + ) + msg = Message(**message) + message_str = msg.model_dump_json() + logger.info(f"send: {message_str}") async def send_message(): if not self._ws_connection: raise NotConnectedError("_send") try: - await self._ws_connection.send(message) + await self._ws_connection.send(message_str) except websockets.exceptions.ConnectionClosedError as e: await self._on_connect_error(e) except websockets.exceptions.ConnectionClosedOK: diff --git a/realtime/_async/presence.py b/realtime/_async/presence.py index 417ec4cd..0f43c5d6 100644 --- a/realtime/_async/presence.py +++ b/realtime/_async/presence.py @@ -6,6 +6,7 @@ from typing import Any, Callable, Dict, List, Optional, Union from ..types import ( + Presence, PresenceDiff, PresenceEvents, PresenceOnJoinCallback, @@ -25,13 +26,11 @@ def __init__(self, channel, opts: Optional[PresenceOpts] = None): self.state: RealtimePresenceState = {} self.pending_diffs: List[RawPresenceDiff] = [] self.join_ref: Optional[str] = None - self.caller = { - "onJoin": lambda *args: None, - "onLeave": lambda *args: None, - "onSync": lambda: None, - "onAuthSuccess": lambda: None, - "onAuthFailure": lambda: None, - } + self.on_join_callback: Optional[PresenceOnJoinCallback] = None + self.on_leave_callback: Optional[PresenceOnLeaveCallback] = None + self.on_sync_callback: Optional[Callable[[], None]] = None + self.on_auth_success_callback: Optional[Callable[[], None]] = None + self.on_auth_failure_callback: Optional[Callable[[], None]] = None # Initialize with default events if not provided events = ( opts.events @@ -44,56 +43,50 @@ def __init__(self, channel, opts: Optional[PresenceOpts] = None): self.channel._on("phx_auth", callback=self._on_auth_event) def on_join(self, callback: PresenceOnJoinCallback): - self.caller["onJoin"] = callback + self.on_join_callback = callback def on_leave(self, callback: PresenceOnLeaveCallback): - self.caller["onLeave"] = callback + self.on_leave_callback = callback def on_sync(self, callback: Callable[[], None]): - self.caller["onSync"] = callback + self.on_sync_callback = callback def on_auth_success(self, callback: Callable[[], None]): - self.caller["onAuthSuccess"] = callback + self.on_auth_success_callback = callback def on_auth_failure(self, callback: Callable[[], None]): - self.caller["onAuthFailure"] = callback + self.on_auth_failure_callback = callback def _on_state_event(self, payload: RawPresenceState, *args): - onJoin = self.caller["onJoin"] - onLeave = self.caller["onLeave"] - onSync = self.caller["onSync"] - self.join_ref = self.channel.join_ref - self.state = self._sync_state(self.state, payload, onJoin, onLeave) + self.state = self._sync_state(self.state, payload) for diff in self.pending_diffs: - self.state = self._sync_diff(self.state, diff, onJoin, onLeave) + self.state = self._sync_diff(self.state, diff) self.pending_diffs = [] - onSync() - - def _on_diff_event(self, payload: Dict[str, Any], *args): - onJoin = self.caller["onJoin"] - onLeave = self.caller["onLeave"] - onSync = self.caller["onSync"] + if self.on_sync_callback: + self.on_sync_callback() + def _on_diff_event(self, payload: RawPresenceDiff, *args): if self.in_pending_sync_state(): self.pending_diffs.append(payload) else: - self.state = self._sync_diff(self.state, payload, onJoin, onLeave) - onSync() + self.state = self._sync_diff(self.state, payload) + if self.on_sync_callback: + self.on_sync_callback() def _on_auth_event(self, payload: Dict[str, Any], *args): if payload.get("status") == "ok": - self.caller["onAuthSuccess"]() + if self.on_auth_success_callback: + self.on_auth_success_callback() else: - self.caller["onAuthFailure"]() + if self.on_auth_failure_callback: + self.on_auth_failure_callback() def _sync_state( self, current_state: RealtimePresenceState, new_state: Union[RawPresenceState, RealtimePresenceState], - onJoin: PresenceOnJoinCallback, - onLeave: PresenceOnLeaveCallback, ) -> RealtimePresenceState: state = {key: list(value) for key, value in current_state.items()} transformed_state = AsyncRealtimePresence._transform_state(new_state) @@ -128,16 +121,12 @@ def _sync_state( else: joins[key] = value - return self._sync_diff( - state, {"joins": joins, "leaves": leaves}, onJoin, onLeave - ) + return self._sync_diff(state, {"joins": joins, "leaves": leaves}) def _sync_diff( self, state: RealtimePresenceState, diff: Union[RawPresenceDiff, PresenceDiff], - onJoin: PresenceOnJoinCallback, - onLeave: PresenceOnLeaveCallback, ) -> RealtimePresenceState: joins = AsyncRealtimePresence._transform_state(diff.get("joins", {})) leaves = AsyncRealtimePresence._transform_state(diff.get("leaves", {})) @@ -148,7 +137,7 @@ def _sync_diff( if len(current_presences) > 0: joined_presence_refs = { - presence.get("presence_ref") for presence in state.get(key) + presence.get("presence_ref") for presence in new_presences } cur_presences = list( presence @@ -157,7 +146,8 @@ def _sync_diff( ) state[key] = cur_presences + state[key] - onJoin(key, current_presences, new_presences) + if self.on_join_callback: + self.on_join_callback(key, current_presences, new_presences) for key, left_presences in leaves.items(): current_presences = state.get(key, []) @@ -175,7 +165,8 @@ def _sync_diff( ] state[key] = current_presences - onLeave(key, current_presences, left_presences) + if self.on_leave_callback: + self.on_leave_callback(key, current_presences, left_presences) if len(current_presences) == 0: del state[key] @@ -229,9 +220,11 @@ def _transform_state( new_state[key] = [] for presence in presences["metas"]: - presence["presence_ref"] = presence.pop("phx_ref", None) - presence.pop("phx_ref_prev", None) - new_state[key].append(presence) + if "phx_ref_prev" in presence: + del presence["phx_ref_prev"] + new_presence: Presence = {"presence_ref": presence.pop("phx_ref")} + new_presence.update(presence) + new_state[key].append(new_presence) else: new_state[key] = presences diff --git a/realtime/_async/push.py b/realtime/_async/push.py index 8e7a68f0..60064bd6 100644 --- a/realtime/_async/push.py +++ b/realtime/_async/push.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import asyncio import logging -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional +from ..message import Message from ..types import DEFAULT_TIMEOUT, Callback, _Hook if TYPE_CHECKING: @@ -13,9 +16,9 @@ class AsyncPush: def __init__( self, - channel: "AsyncRealtimeChannel", + channel: AsyncRealtimeChannel, event: str, - payload: Optional[Dict[str, Any]] = None, + payload: Optional[Mapping[str, Any]] = None, timeout: int = DEFAULT_TIMEOUT, ): self.channel = channel @@ -44,25 +47,23 @@ async def send(self): self.start_timeout() self.sent = True - try: - await self.channel.socket.send( - { - "topic": self.channel.topic, - "event": self.event, - "payload": self.payload, - "ref": self.ref, - "join_ref": self.channel.join_push.ref, - } - ) - except Exception as e: - logger.error(f"send push failed: {e}") + message = Message( + topic=self.channel.topic, + event=self.event, + ref=self.ref, + payload=self.payload, + join_ref=self.channel.join_push.ref, + ) + await self.channel.socket.send(message) def update_payload(self, payload: Dict[str, Any]): self.payload = {**self.payload, **payload} - def receive(self, status: str, callback: Callback) -> "AsyncPush": - if self._has_received(status): - callback(self.received_resp.get("response", {})) + def receive( + self, status: str, callback: Callback[[Dict[str, Any]], None] + ) -> AsyncPush: + if self.received_resp and self.received_resp.get("status") == status: + callback(self.received_resp) self.rec_hooks.append(_Hook(status, callback)) return self @@ -72,13 +73,14 @@ def start_timeout(self): return self.ref = self.channel.socket._make_ref() - self.ref_event = self.channel._reply_event_name(self.ref) + current_event = self.channel._reply_event_name(self.ref) + self.ref_event = current_event - def on_reply(payload, *args): + def on_reply(payload: Dict[str, Any], _ref: Optional[str]): self._cancel_ref_event() self._cancel_timeout() self.received_resp = payload - self._match_receive(**self.received_resp) + self._match_receive(**payload) self.channel._on(self.ref_event, on_reply) @@ -88,7 +90,7 @@ async def timeout(self): self.timeout_task = asyncio.create_task(timeout(self)) - def trigger(self, status: str, response: Any): + def trigger(self, status: str, response: dict[str, Any]): if self.ref_event: payload = { "status": status, @@ -113,10 +115,12 @@ def _cancel_timeout(self): self.timeout_task.cancel() self.timeout_task = None - def _match_receive(self, status: str, response: Any): + def _match_receive(self, status: str, response: dict[str, Any]): for hook in self.rec_hooks: if hook.status == status: hook.callback(response) - def _has_received(self, status: str): - return self.received_resp and self.received_resp.get("status") == status + def _has_received(self, status: str) -> bool: + if self.received_resp and self.received_resp.get("status") == status: + return True + return False diff --git a/realtime/exceptions.py b/realtime/exceptions.py index 8ecb0d40..39992048 100644 --- a/realtime/exceptions.py +++ b/realtime/exceptions.py @@ -1,3 +1,6 @@ +from typing import Optional + + class NotConnectedError(Exception): """ Raised when operations requiring a connection are executed when socket is not connected @@ -15,7 +18,7 @@ class AuthorizationError(Exception): Raised when there is an authorization failure for private channels """ - def __init__(self, message: str = None): + def __init__(self, message: Optional[str] = None): self.message: str = message or "Authorization failed for private channel" def __str__(self): diff --git a/realtime/message.py b/realtime/message.py index f4ed22fd..b5125a9f 100644 --- a/realtime/message.py +++ b/realtime/message.py @@ -1,26 +1,15 @@ -from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any, Mapping, Optional +from pydantic import BaseModel -@dataclass -class Message: + +class Message(BaseModel): """ Dataclass abstraction for message """ event: str - payload: Dict[str, Any] - ref: Any + payload: Mapping[str, Any] topic: str + ref: Optional[str] = None join_ref: Optional[str] = None - - def __hash__(self): - return hash( - ( - self.event, - tuple(list(self.payload.values())), - self.ref, - self.topic, - self.join_ref, - ) - ) diff --git a/realtime/types.py b/realtime/types.py index e40159ae..5c14129a 100644 --- a/realtime/types.py +++ b/realtime/types.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from enum import Enum from typing import Any, Callable, Dict, List, Literal, Optional, TypedDict, TypeVar -from typing_extensions import ParamSpec +from typing_extensions import ParamSpec, TypeAlias # Constants DEFAULT_TIMEOUT = 10 @@ -12,7 +14,7 @@ # Type variables and custom types T_ParamSpec = ParamSpec("T_ParamSpec") T_Retval = TypeVar("T_Retval") -Callback = Callable[T_ParamSpec, T_Retval] +Callback: TypeAlias = Callable[T_ParamSpec, T_Retval] # Enums @@ -24,7 +26,7 @@ class ChannelEvents(str, Enum): close = "phx_close" error = "phx_error" - join = "phx_join" + join = "phx_join" # type: ignore reply = "phx_reply" leave = "phx_leave" heartbeat = "heartbeat" @@ -64,7 +66,7 @@ def __init__( self, type: str, filter: Dict[str, Any], - callback: Callback, + callback: Callback[[Dict[str, Any], Optional[str]], None], id: Optional[str] = None, ): self.type = type @@ -74,12 +76,12 @@ def __init__( class _Hook: - def __init__(self, status: str, callback: Callback): + def __init__(self, status: str, callback: Callback[[Dict[str, Any]], None]): self.status = status self.callback = callback -class Presence(Dict[str, Any]): +class Presence(TypedDict, total=False): presence_ref: str @@ -116,7 +118,7 @@ class RealtimeChannelOptions(TypedDict): class PresenceMeta(TypedDict, total=False): phx_ref: str - phx_ref_prev: str + phx_ref_prev: Optional[str] class RawPresenceStateEntry(TypedDict): diff --git a/tests/test_connection.py b/tests/test_connection.py index b5c6d4f1..dd9c3221 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,5 +1,6 @@ import asyncio import datetime +import logging import os import aiohttp @@ -7,6 +8,7 @@ from dotenv import load_dotenv from realtime import AsyncRealtimeChannel, AsyncRealtimeClient, RealtimeSubscribeStates +from realtime.message import Message from realtime.types import DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_TIMEOUT load_dotenv() @@ -420,11 +422,11 @@ async def test_send_message_reconnection(socket: AsyncRealtimeClient): await socket._ws_connection.close() # Try to send a message - this should trigger reconnection - message = { - "topic": "test-channel", - "event": "test-event", - "payload": {"test": "data"}, - } + message = Message( + topic="test-channel", + event="test-event", + payload={"test": "data"}, + ) await socket.send(message) # Wait for reconnection to complete