diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9ee3ef..5dedad3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,25 +19,16 @@ jobs: - name: Clone Repository uses: actions/checkout@v4 - - name: Set up Poetry - uses: abatilo/actions-poetry@v4 - with: - poetry-version: latest - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'poetry' - - - name: Install Supabase CLI - uses: supabase/setup-cli@v1 - with: - version: latest + - name: Set up Poetry + run: pipx install poetry==1.8.5 --python python${{ matrix.python-version }} - name: Start Supabase local development setup - run: supabase start --workdir infra -x studio,inbucket,edge-runtime,logflare,vector,supavisor,imgproxy,storage-api + run: make run_infra - name: Run Tests run: make run_tests @@ -88,16 +79,13 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing contents: write # needed for github actions bot to write to repo steps: - - name: Set up Poetry - uses: abatilo/actions-poetry@v4 - with: - poetry-version: 1.8.4 - - name: Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: 3.11 - cache: 'poetry' + + - name: Set up Poetry + run: pipx install poetry==1.8.5 --python python3.11 - name: Clone Repository uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index fc0a24f..df412bf 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ dmypy.json .idea/* # End of https://www.toptal.com/developers/gitignore/api/python + + +.vscode/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 399de05..e20c711 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: '^.*\.(md|MD|html)$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -10,7 +10,7 @@ repos: args: ["--fix=lf"] - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.0 hooks: - id: isort args: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9965a34..a549f59 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.0" + ".": "2.4.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index a43503d..33a2b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ +## [2.4.0](https://github.com/supabase/realtime-py/compare/v2.3.0...v2.4.0) (2025-02-19) + + +### Features + +* remove the need of calling listen method ([#274](https://github.com/supabase/realtime-py/issues/274)) ([0a96f70](https://github.com/supabase/realtime-py/commit/0a96f709db5f2c6dc04b246bc637368b0df11674)) + + +### Bug Fixes + +* Set default heartbeat interval to 25s ([#276](https://github.com/supabase/realtime-py/issues/276)) ([09c6269](https://github.com/supabase/realtime-py/commit/09c6269f7bab8654865a942244951cedccd78bbb)) + ## [2.3.0](https://github.com/supabase/realtime-py/compare/v2.2.0...v2.3.0) (2025-01-29) diff --git a/Makefile b/Makefile index a3fcbe8..9e31cd3 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ run_tests: tests local_tests: run_infra sleep tests tests_only: - poetry run pytest --cov=./ --cov-report=xml --cov-report=html -vv + poetry run pytest --cov=realtime --cov-report=xml --cov-report=html -vv sleep: sleep 2 diff --git a/README.md b/README.md index c715d4d..b4f8501 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,6 @@ def _on_subscribe(status: RealtimeSubscribeStates, err: Optional[Exception]): print('Realtime channel was unexpectedly closed.') await channel.subscribe(_on_subscribe) - -# Listen for all incoming events, often the last thing you want to do. -await client.listen() ``` ### Notes: diff --git a/example/README.md b/example/README.md deleted file mode 100644 index e4c3243..0000000 --- a/example/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Example project - -Simple example project to test out realtime python code. - -Run it with: -`poetry run python app.py` \ No newline at end of file diff --git a/example/app.py b/example/app.py deleted file mode 100644 index 7279a09..0000000 --- a/example/app.py +++ /dev/null @@ -1,140 +0,0 @@ -import asyncio -import datetime -import logging -import os - -from realtime import AsyncRealtimeChannel, AsyncRealtimeClient - -logging.basicConfig( - format="%(asctime)s:%(levelname)s - %(message)s", level=logging.INFO -) - - -def presence_callback(payload): - print("presence: ", payload) - - -def postgres_changes_callback(payload, *args): - print("*: ", payload) - - -def postgres_changes_insert_callback(payload, *args): - print("INSERT: ", payload) - - -def postgres_changes_delete_callback(payload, *args): - print("DELETE: ", payload) - - -def postgres_changes_update_callback(payload, *args): - print("UPDATE: ", payload) - - -async def realtime(payload): - print("async realtime ", payload) - - -async def test_broadcast_events(socket: AsyncRealtimeClient): - await socket.connect() - - channel = socket.channel( - "test-broadcast", params={"config": {"broadcast": {"self": True}}} - ) - received_events = [] - - def broadcast_callback(payload, *args): - print("broadcast: ", payload) - received_events.append(payload) - - await channel.on_broadcast("test-event", callback=broadcast_callback).subscribe() - - await asyncio.sleep(1) - - # Send 3 broadcast events - for i in range(3): - await channel.send_broadcast("test-event", {"message": f"Event {i+1}"}) - - # Wait a short time to ensure all events are processed - await asyncio.sleep(1) - - assert len(received_events) == 3 - assert received_events[0]["payload"]["message"] == "Event 1" - assert received_events[1]["payload"]["message"] == "Event 2" - assert received_events[2]["payload"]["message"] == "Event 3" - - -async def test_postgres_changes(socket: AsyncRealtimeClient): - await socket.connect() - - # Add your access token here - # await socket.set_auth("ACCESS_TOKEN") - - channel = socket.channel("test-postgres-changes") - - await channel.on_postgres_changes( - "*", table="todos", callback=postgres_changes_callback - ).on_postgres_changes( - "INSERT", - table="todos", - filter="id=eq.10", - callback=postgres_changes_insert_callback, - ).on_postgres_changes( - "DELETE", table="todos", callback=postgres_changes_delete_callback - ).on_postgres_changes( - "UPDATE", table="todos", callback=postgres_changes_update_callback - ).subscribe() - - await socket.listen() - - -async def test_presence(socket: AsyncRealtimeClient): - await socket.connect() - - asyncio.create_task(socket.listen()) - - channel: AsyncRealtimeChannel = socket.channel("room") - - def on_sync(): - print("on_sync", channel.presence.state) - - def on_join(key, current_presences, new_presences): - print("on_join", key, current_presences, new_presences) - - def on_leave(key, current_presences, left_presences): - print("on_leave", key, current_presences, left_presences) - - await channel.on_presence_sync(on_sync).on_presence_join(on_join).on_presence_leave( - on_leave - ).subscribe() - - await channel.track( - {"user_id": "1", "online_at": datetime.datetime.now().isoformat()} - ) - await asyncio.sleep(1) - - await channel.track( - {"user_id": "2", "online_at": datetime.datetime.now().isoformat()} - ) - await asyncio.sleep(1) - - await channel.untrack() - await asyncio.sleep(1) - - -async def main(): - URL = os.getenv("SUPABASE_URL") or "http://127.0.0.1:54321" - JWT = ( - os.getenv("SUPABASE_ANON_KEY") - or "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" - ) - - # Setup the broadcast socket and channel - socket = AsyncRealtimeClient(f"{URL}/realtime/v1", JWT, auto_reconnect=True) - await socket.connect() - - await test_broadcast_events(socket) - await test_postgres_changes(socket) - await test_presence(socket) - - -asyncio.run(main()) diff --git a/example/poetry.lock b/example/poetry.lock deleted file mode 100644 index 07df7e1..0000000 --- a/example/poetry.lock +++ /dev/null @@ -1,155 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "realtime" -version = "2.0.0" -description = "" -optional = false -python-versions = "^3.8" -files = [] -develop = true - -[package.dependencies] -python-dateutil = "^2.8.1" -typing-extensions = "^4.12.2" -websockets = ">=11,<13" - -[package.source] -type = "directory" -url = ".." - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "websockets" -version = "12.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "e5ba233a1edcfc8bf64d820adb246363d79e966049481c08ae9e473898eae244" diff --git a/example/pyproject.toml b/example/pyproject.toml deleted file mode 100644 index 10e764e..0000000 --- a/example/pyproject.toml +++ /dev/null @@ -1,15 +0,0 @@ -[tool.poetry] -name = "example" -version = "0.1.0" -description = "" -authors = ["Your Name "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11" -realtime = {path = "..", develop = true} -python-dotenv = "^1.0.1" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/example/supabase/.gitignore b/example/supabase/.gitignore deleted file mode 100644 index a3ad880..0000000 --- a/example/supabase/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Supabase -.branches -.temp -.env diff --git a/example/supabase/config.toml b/example/supabase/config.toml deleted file mode 100644 index be83465..0000000 --- a/example/supabase/config.toml +++ /dev/null @@ -1,202 +0,0 @@ -# A string used to distinguish different Supabase projects on the same host. Defaults to the -# working directory name when running `supabase init`. -project_id = "realtime-py-example" - -[api] -enabled = true -# Port to use for the API URL. -port = 54321 -# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API -# endpoints. `public` is always included. -schemas = ["public", "graphql_public"] -# Extra schemas to add to the search_path of every request. `public` is always included. -extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size -# for accidental or malicious requests. -max_rows = 1000 - -[api.tls] -enabled = false - -[db] -# Port to use for the local database URL. -port = 54322 -# Port used by db diff command to initialize the shadow database. -shadow_port = 54320 -# The database major version to use. This has to be the same as your remote database's. Run `SHOW -# server_version;` on the remote database to check. -major_version = 15 - -[db.pooler] -enabled = false -# Port to use for the local connection pooler. -port = 54329 -# Specifies when a server connection can be reused by other clients. -# Configure one of the supported pooler modes: `transaction`, `session`. -pool_mode = "transaction" -# How many server connections to allow per user/database pair. -default_pool_size = 20 -# Maximum number of client connections allowed. -max_client_conn = 100 - -[realtime] -enabled = true -# Bind realtime via either IPv4 or IPv6. (default: IPv4) -# ip_version = "IPv6" -# The maximum length in bytes of HTTP request headers. (default: 4096) -# max_header_length = 4096 - -[studio] -enabled = true -# Port to use for Supabase Studio. -port = 54323 -# External URL of the API server that frontend connects to. -api_url = "http://127.0.0.1" -# OpenAI API Key to use for Supabase AI in the Supabase Studio. -openai_api_key = "env(OPENAI_API_KEY)" - -# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they -# are monitored, and you can view the emails that would have been sent from the web interface. -[inbucket] -enabled = true -# Port to use for the email testing server web interface. -port = 54324 -# Uncomment to expose additional ports for testing user applications that send emails. -# smtp_port = 54325 -# pop3_port = 54326 - -[storage] -enabled = true -# The maximum file size allowed (e.g. "5MB", "500KB"). -file_size_limit = "50MiB" - -[storage.image_transformation] -enabled = true - -# Uncomment to configure local storage buckets -# [storage.buckets.images] -# public = false -# file_size_limit = "50MiB" -# allowed_mime_types = ["image/png", "image/jpeg"] - -[auth] -enabled = true -# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used -# in emails. -site_url = "http://127.0.0.1:3000" -# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000"] -# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). -jwt_expiry = 3600 -# If disabled, the refresh token will never expire. -enable_refresh_token_rotation = true -# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. -# Requires enable_refresh_token_rotation = true. -refresh_token_reuse_interval = 10 -# Allow/disallow new user signups to your project. -enable_signup = true -# Allow/disallow anonymous sign-ins to your project. -enable_anonymous_sign_ins = false -# Allow/disallow testing manual linking of accounts -enable_manual_linking = false - -[auth.email] -# Allow/disallow new user signups via email to your project. -enable_signup = true -# If enabled, a user will be required to confirm any email change on both the old, and new email -# addresses. If disabled, only the new email is required to confirm. -double_confirm_changes = true -# If enabled, users need to confirm their email address before signing in. -enable_confirmations = false -# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. -max_frequency = "1s" - -# Use a production-ready SMTP server -# [auth.email.smtp] -# host = "smtp.sendgrid.net" -# port = 587 -# user = "apikey" -# pass = "env(SENDGRID_API_KEY)" -# admin_email = "admin@email.com" -# sender_name = "Admin" - -# Uncomment to customize email template -# [auth.email.template.invite] -# subject = "You have been invited" -# content_path = "./supabase/templates/invite.html" - -[auth.sms] -# Allow/disallow new user signups via SMS to your project. -enable_signup = true -# If enabled, users need to confirm their phone number before signing in. -enable_confirmations = false -# Template for sending OTP to users -template = "Your code is {{ .Code }} ." -# Controls the minimum amount of time that must pass before sending another sms otp. -max_frequency = "5s" - -# Use pre-defined map of phone number to OTP for testing. -# [auth.sms.test_otp] -# 4152127777 = "123456" - -# Configure logged in session timeouts. -# [auth.sessions] -# Force log out after the specified duration. -# timebox = "24h" -# Force log out if the user has been inactive longer than the specified duration. -# inactivity_timeout = "8h" - -# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. -# [auth.hook.custom_access_token] -# enabled = true -# uri = "pg-functions:////" - -# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. -[auth.sms.twilio] -enabled = false -account_sid = "" -message_service_sid = "" -# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: -auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" - -# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, -# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, -# `twitter`, `slack`, `spotify`, `workos`, `zoom`. -[auth.external.apple] -enabled = false -client_id = "" -# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: -secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" -# Overrides the default auth redirectUrl. -redirect_uri = "" -# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, -# or any other third-party OIDC providers. -url = "" -# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. -skip_nonce_check = false - -[edge_runtime] -enabled = true -# Configure one of the supported request policies: `oneshot`, `per_worker`. -# Use `oneshot` for hot reload, or `per_worker` for load testing. -policy = "oneshot" -inspector_port = 8083 - -[analytics] -enabled = true -port = 54327 -# Configure one of the supported backends: `postgres`, `bigquery`. -backend = "postgres" - -# Experimental features may be deprecated any time -[experimental] -# Configures Postgres storage engine to use OrioleDB (S3) -orioledb_version = "" -# Configures S3 bucket URL, eg. .s3-.amazonaws.com -s3_host = "env(S3_HOST)" -# Configures S3 bucket region, eg. us-east-1 -s3_region = "env(S3_REGION)" -# Configures AWS_ACCESS_KEY_ID for S3 bucket -s3_access_key = "env(S3_ACCESS_KEY)" -# Configures AWS_SECRET_ACCESS_KEY for S3 bucket -s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/example/supabase/seed.sql b/example/supabase/seed.sql deleted file mode 100644 index e69de29..0000000 diff --git a/poetry.lock b/poetry.lock index 391bd2e..aeb0afd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,87 +13,92 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.11.12" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, - {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, - {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, - {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, - {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, - {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, - {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, - {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"}, - {file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"}, - {file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"}, - {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"}, + {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"}, + {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"}, + {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"}, + {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"}, + {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"}, + {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"}, + {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"}, + {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"}, + {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"}, + {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"}, + {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"}, ] [package.dependencies] @@ -1318,4 +1323,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "a39c9eb520538341b07b55619df3071894f59af8e88212f772f5f02830978cb8" +content-hash = "f3b0bb811f7ad3ed4204f5d12ac716a173cf12366bf44a0c20e790861f545c91" diff --git a/pyproject.toml b/pyproject.toml index f9e6f30..270d767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "realtime" -version = "2.3.0" # {x-release-please-version} +version = "2.4.0" # {x-release-please-version} description = "" authors = [ "Joel Lee ", @@ -15,7 +15,7 @@ python = "^3.9" websockets = ">=11,<15" python-dateutil = "^2.8.1" typing-extensions = "^4.12.2" -aiohttp = "^3.11.11" +aiohttp = "^3.11.12" [tool.poetry.dev-dependencies] pytest = "^8.3.4" diff --git a/realtime/_async/channel.py b/realtime/_async/channel.py index 4484a1f..0050e5e 100644 --- a/realtime/_async/channel.py +++ b/realtime/_async/channel.py @@ -33,9 +33,9 @@ class AsyncRealtimeChannel: """ - `Channel` is an abstraction for a topic listener for an existing socket connection. - Each Channel has its own topic and a list of event-callbacks that responds to messages. - Should only be instantiated through `connection.RealtimeClient().channel(topic)`. + Channel is an abstraction for a topic subscription on an existing socket connection. + Each Channel has its own topic and a list of event-callbacks that respond to messages. + Should only be instantiated through `AsyncRealtimeClient.channel(topic)`. """ def __init__( @@ -52,13 +52,14 @@ def __init__( :param params: Optional parameters for connection. """ self.socket = socket - self.params = params or RealtimeChannelOptions( - config={ + self.params = params or {} + if self.params.get("config") is None: + self.params["config"] = { "broadcast": {"ack": False, "self": False}, "presence": {"key": ""}, "private": False, } - ) + self.topic = topic self._joined_once = False self.bindings: Dict[str, List[Binding]] = {} @@ -97,7 +98,7 @@ def on_close(*args): logger.info(f"channel {self.topic} closed") self.rejoin_timer.reset() self.state = ChannelStates.CLOSED - self.socket.remove_channel(self) + self.socket._remove_channel(self) def on_error(payload, *args): if self.is_leaving or self.is_closed: @@ -148,12 +149,16 @@ async def subscribe( ] = None, ) -> AsyncRealtimeChannel: """ - Subscribe to the channel. + Subscribe to the channel. Can only be called once per channel instance. - :return: The Channel instance for method chaining. + :param callback: Optional callback function that receives subscription state updates + and any errors that occur during subscription + :return: The Channel instance for method chaining + :raises: Exception if called multiple times on the same channel instance """ if not self.socket.is_connected: await self.socket.connect() + if self._joined_once: raise Exception( "Tried to subscribe multiple times. 'subscribe' can only be called a single time per channel instance" @@ -249,6 +254,10 @@ def on_join_push_timeout(*args): return self async def unsubscribe(self): + """ + Unsubscribe from the channel and leave the topic. + Sets channel state to LEAVING and cleans up timers and pushes. + """ self.state = ChannelStates.LEAVING self.rejoin_timer.reset() @@ -269,6 +278,15 @@ def _close(*args): async def push( self, event: str, payload: Dict[str, Any], timeout: Optional[int] = None ) -> AsyncPush: + """ + Push a message to the channel. + + :param event: The event name to push + :param payload: The payload to send + :param timeout: Optional timeout in milliseconds + :return: AsyncPush instance representing the push operation + :raises: Exception if called before subscribing to the channel + """ if not self._joined_once: raise Exception( f"tried to push '{event}' to '{self.topic}' before joining. Use channel.subscribe() before pushing events" @@ -350,9 +368,9 @@ def on_broadcast( """ Set up a listener for a specific broadcast event. - :param event: The name of the broadcast event to listen for. - :param callback: The callback function to execute when the event is received. - :return: The Channel instance for method chaining. + :param event: The name of the broadcast event to listen for + :param callback: Function called with the payload when a matching broadcast is received + :return: The Channel instance for method chaining """ return self._on( "broadcast", @@ -369,13 +387,14 @@ def on_postgres_changes( filter: Optional[str] = None, ) -> AsyncRealtimeChannel: """ - Set up a listener for a specific Postgres changes event. - - :param event: The name of the Postgres changes event to listen for. - :param table: The table name for which changes should be monitored. - :param callback: The callback function to execute when the event is received. - :param schema: The database schema where the table exists. Default is 'public'. - :return: The Channel instance for method chaining. + Set up a listener for Postgres database changes. + + :param event: The type of database event to listen for (INSERT, UPDATE, DELETE, or *) + :param callback: Function called with the payload when a matching change is detected + :param table: The table name to monitor. Defaults to "*" for all tables + :param schema: The database schema to monitor. Defaults to "public" + :param filter: Optional filter string to apply + :return: The Channel instance for method chaining """ binding_filter = {"event": event, "schema": schema, "table": table} @@ -402,22 +421,24 @@ def on_system( # Presence methods async def track(self, user_status: Dict[str, Any]) -> None: """ - Track a user's presence. + Track presence status for the current user. - :param user_status: User's presence status. - :return: None + :param user_status: Dictionary containing the user's presence information """ await self.send_presence("track", user_status) async def untrack(self) -> None: """ - Untrack a user's presence. - - :return: None + Stop tracking presence for the current user. """ await self.send_presence("untrack", {}) def presence_state(self) -> RealtimePresenceState: + """ + Get the current state of presence on this channel. + + :return: Dictionary mapping presence keys to lists of presence payloads + """ return self.presence.state def on_presence_sync(self, callback: Callable[[], None]) -> AsyncRealtimeChannel: @@ -457,11 +478,10 @@ def on_presence_leave( # Broadcast methods async def send_broadcast(self, event: str, data: Any) -> None: """ - Sends a broadcast message to the current channel. + Send a broadcast message through this channel. - :param event: The name of the broadcast event. - :param data: The data to be sent with the message. - :return: An asyncio.Future object representing the send operation. + :param event: The name of the broadcast event + :param data: The payload to broadcast """ await self.push( ChannelEvents.broadcast, diff --git a/realtime/_async/client.py b/realtime/_async/client.py index 81f5c90..1cc8fd9 100644 --- a/realtime/_async/client.py +++ b/realtime/_async/client.py @@ -7,86 +7,97 @@ from functools import wraps from math import floor from typing import Any, Callable, Dict, List, Optional +from urllib.parse import urlencode, urlparse, urlunparse import websockets +from websockets.asyncio.client import ClientConnection, connect -from ..exceptions import NotConnectedError from ..message import Message from ..transformers import http_endpoint_url from ..types import ( + DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_TIMEOUT, PHOENIX_CHANNEL, - Callback, + VSN, ChannelEvents, - T_ParamSpec, - T_Retval, ) from ..utils import is_ws_url from .channel import AsyncRealtimeChannel, RealtimeChannelOptions -logger = logging.getLogger(__name__) - -def ensure_connection(func: Callback): +def deprecated(func: Callable) -> Callable: @wraps(func) - def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: - if not args[0].is_connected: - raise NotConnectedError(func.__name__) - + def wrapper(*args, **kwargs): + logger.warning(f"Warning: {func.__name__} is deprecated.") return func(*args, **kwargs) return wrapper +logger = logging.getLogger(__name__) + + class AsyncRealtimeClient: def __init__( self, url: str, - token: str, + token: Optional[str] = None, auto_reconnect: bool = True, params: Optional[Dict[str, Any]] = None, - hb_interval: int = 30, + hb_interval: int = DEFAULT_HEARTBEAT_INTERVAL, max_retries: int = 5, initial_backoff: float = 1.0, + timeout: int = DEFAULT_TIMEOUT, ) -> None: """ Initialize a RealtimeClient instance for WebSocket communication. :param url: WebSocket URL of the Realtime server. Starts with `ws://` or `wss://`. - Also accepts default Supabase URL: `http://` or `https://`. + Also accepts default Supabase URL: `http://` or `https://`. :param token: Authentication token for the WebSocket connection. - :param auto_reconnect: If True, automatically attempt to reconnect on disconnection. Defaults to False. - :param params: Optional parameters for the connection. Defaults to an empty dictionary. - :param hb_interval: Interval (in seconds) for sending heartbeat messages to keep the connection alive. Defaults to 30. + :param auto_reconnect: If True, automatically attempt to reconnect on disconnection. Defaults to True. + :param params: Optional parameters for the connection. Defaults to None. + :param hb_interval: Interval (in seconds) for sending heartbeat messages to keep the connection alive. Defaults to 25. :param max_retries: Maximum number of reconnection attempts. Defaults to 5. :param initial_backoff: Initial backoff time (in seconds) for reconnection attempts. Defaults to 1.0. + :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") - self.url = f"{re.sub(r'https://', 'wss://', re.sub(r'http://', 'ws://', url, flags=re.IGNORECASE), flags=re.IGNORECASE)}/websocket?apikey={token}" + 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}" self.http_endpoint = http_endpoint_url(url) - self.is_connected = False self.params = params or {} self.apikey = token self.access_token = token self.send_buffer: List[Callable] = [] self.hb_interval = hb_interval - self.ws_connection: Optional[websockets.client.WebSocketClientProtocol] = None + self.ws_connection: Optional[ClientConnection] = None self.ref = 0 self.auto_reconnect = auto_reconnect self.channels: Dict[str, AsyncRealtimeChannel] = {} self.max_retries = max_retries self.initial_backoff = initial_backoff - self.timeout = DEFAULT_TIMEOUT + self.timeout = timeout + self._listen_task: Optional[asyncio.Task] = None + self._heartbeat_task: Optional[asyncio.Task] = None + + @property + def is_connected(self) -> bool: + return self.ws_connection is not None async def _listen(self) -> None: """ An infinite loop that keeps listening. :return: None """ - while True: - try: - msg = await self.ws_connection.recv() + + if not self.ws_connection: + raise Exception("WebSocket connection not established") + + try: + async for msg in self.ws_connection: logger.info(f"receive: {msg}") msg = Message(**json.loads(msg)) @@ -94,18 +105,25 @@ async def _listen(self) -> None: if channel: channel._trigger(msg.event, msg.payload, msg.ref) - else: - logger.info(f"Channel {msg.topic} not found") + except websockets.exceptions.ConnectionClosedError as e: + logger.error( + f"WebSocket connection closed with code: {e.code}, reason: {e.reason}" + ) + if self.auto_reconnect: + logger.info("Initiating auto-reconnect sequence...") + + await self._reconnect() + else: + logger.error("Auto-reconnect disabled, terminating connection") - except websockets.exceptions.ConnectionClosed: - if self.auto_reconnect: - logger.info("Connection with server closed, trying to reconnect...") - await self.connect() - for topic, channel in self.channels.items(): - await channel._rejoin() - else: - logger.exception("Connection with the server closed.") - break + async def _reconnect(self) -> None: + self.ws_connection = None + await self.connect() + + if self.is_connected: + for topic, channel in self.channels.items(): + logger.info(f"Rejoining channel after reconnection: {topic}") + await channel._rejoin() async def connect(self) -> None: """ @@ -124,41 +142,51 @@ async def connect(self) -> None: - The initial backoff time and maximum retries are set during RealtimeClient initialization. - The backoff time doubles after each failed attempt, up to a maximum of 60 seconds. """ + + if self.is_connected: + logger.info("WebSocket connection already established") + return + retries = 0 backoff = self.initial_backoff + logger.info(f"Attempting to connect to WebSocket at {self.url}") + while retries < self.max_retries: try: - self.ws_connection = await websockets.connect(self.url) - if self.ws_connection.state is websockets.State.OPEN: - logger.info("Connection was successful") - return await self._on_connect() - else: - raise Exception("Failed to open WebSocket connection") + ws = await connect(self.url) + self.ws_connection = ws + logger.info("WebSocket connection established successfully") + return await self._on_connect() except Exception as e: retries += 1 + logger.error(f"Connection attempt failed: {str(e)}") + if retries >= self.max_retries or not self.auto_reconnect: logger.error( - f"Failed to establish WebSocket connection after {retries} attempts: {e}" + f"Connection failed permanently after {retries} attempts. Error: {str(e)}" ) raise else: - wait_time = backoff * (2 ** (retries - 1)) # Exponential backoff + wait_time = backoff * (2 ** (retries - 1)) logger.info( - f"Connection attempt {retries} failed. Retrying in {wait_time:.2f} seconds..." + f"Retry {retries}/{self.max_retries}: Next attempt in {wait_time:.2f}s (backoff={backoff}s)" ) await asyncio.sleep(wait_time) - backoff = min(backoff * 2, 60) # Cap the backoff at 60 seconds + backoff = min(backoff * 2, 60) raise Exception( f"Failed to establish WebSocket connection after {self.max_retries} attempts" ) + @deprecated async def listen(self): - await asyncio.gather(self._listen(), self._heartbeat()) + pass + + async def _on_connect(self) -> None: + self._listen_task = asyncio.create_task(self._listen()) + self._heartbeat_task = asyncio.create_task(self._heartbeat()) - async def _on_connect(self): - self.is_connected = True await self._flush_send_buffer() async def _flush_send_buffer(self): @@ -178,10 +206,23 @@ async def close(self) -> None: NotConnectedError: If the connection is not established when this method is called. """ - await self.ws_connection.close() - self.is_connected = False + if self.ws_connection: + await self.ws_connection.close() + + self.ws_connection = None + + if self._listen_task: + self._listen_task.cancel() + self._listen_task = None + + if self._heartbeat_task: + self._heartbeat_task.cancel() + self._heartbeat_task = None async def _heartbeat(self) -> None: + if not self.ws_connection: + raise Exception("WebSocket connection not established") + while self.is_connected: try: data = dict( @@ -191,43 +232,29 @@ async def _heartbeat(self) -> None: ref=None, ) await self.send(data) - # Use max to avoid hb_interval=0 bugs etc await asyncio.sleep(max(self.hb_interval, 15)) - except websockets.exceptions.ConnectionClosed: - # If ConnectionClosed then is_connected == False - self.is_connected = False - if self.auto_reconnect: - logger.info("Connection with server closed, trying to reconnect...") - await self.connect() - # If auto_reconnect and connect() then is_connected == True - self.is_connected = True - - ## Apply the new socket to every channel and rejoin. - for topic, channel in self.channels.items(): - logger.info(f"Rejoining to: {topic}") - channel.socket = self - await channel._rejoin() - # Wait before sending another phx_join message. - # Use max to avoid hb_interval=0 bugs etc - await asyncio.sleep(max(self.hb_interval, 15)) + except websockets.exceptions.ConnectionClosed as e: + logger.error( + f"Connection closed during heartbeat. Code: {e.code}, reason: {e.reason}" + ) + if self.auto_reconnect: + logger.info("Heartbeat failed - initiating reconnection sequence") + await self._reconnect() else: - # If ConnectionClosed and not auto_reconnect then is_connected == False - self.is_connected = False - logger.exception("Connection with the server closed.") + logger.error("Heartbeat failed - auto-reconnect disabled") break - else: - # Everything went Ok then is_connected == True - self.is_connected = True - @ensure_connection def channel( self, topic: str, params: Optional[RealtimeChannelOptions] = None ) -> AsyncRealtimeChannel: """ - :param topic: Initializes a channel and creates a two-way association with the socket - :return: Channel + Initialize a channel and create a two-way association with the socket. + + :param topic: The topic to subscribe to + :param params: Optional channel parameters + :return: AsyncRealtimeChannel instance """ topic = f"realtime:{topic}" chan = AsyncRealtimeChannel(self, topic, params) @@ -238,6 +265,9 @@ def channel( def get_channels(self) -> List[AsyncRealtimeChannel]: return list(self.channels.values()) + def _remove_channel(self, channel: AsyncRealtimeChannel) -> None: + del self.channels[channel.topic] + async def remove_channel(self, channel: AsyncRealtimeChannel) -> None: """ Unsubscribes and removes a channel from the socket @@ -246,7 +276,6 @@ async def remove_channel(self, channel: AsyncRealtimeChannel) -> None: """ if channel.topic in self.channels: await self.channels[channel.topic].unsubscribe() - del self.channels[channel.topic] if len(self.channels) == 0: await self.close() @@ -330,9 +359,6 @@ async def send(self, message: Dict[str, Any]) -> None: Returns: None - - Raises: - websockets.exceptions.WebSocketException: If there's an error sending the message. """ message = json.dumps(message) @@ -355,3 +381,17 @@ async def _leave_open_topic(self, topic: str): for ch in dup_channels: await ch.unsubscribe() + + def endpoint_url(self) -> str: + parsed_url = urlparse(self.url) + query = urlencode({**self.params, "vsn": VSN}, doseq=True) + return urlunparse( + ( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + parsed_url.params, + query, + parsed_url.fragment, + ) + ) diff --git a/realtime/types.py b/realtime/types.py index b3b1b13..e40159a 100644 --- a/realtime/types.py +++ b/realtime/types.py @@ -6,6 +6,8 @@ # Constants DEFAULT_TIMEOUT = 10 PHOENIX_CHANNEL = "phoenix" +VSN = "1.0.0" +DEFAULT_HEARTBEAT_INTERVAL = 25 # Type variables and custom types T_ParamSpec = ParamSpec("T_ParamSpec") diff --git a/realtime/version.py b/realtime/version.py index 1b1f776..f5017ab 100644 --- a/realtime/version.py +++ b/realtime/version.py @@ -1 +1 @@ -__version__ = "2.3.0" # {x-release-please-version} +__version__ = "2.4.0" # {x-release-please-version} diff --git a/tests/test_connection.py b/tests/test_connection.py index a03447e..08dcf83 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -7,6 +7,7 @@ from dotenv import load_dotenv from realtime import AsyncRealtimeChannel, AsyncRealtimeClient, RealtimeSubscribeStates +from realtime.types import DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_TIMEOUT load_dotenv() @@ -45,8 +46,23 @@ async def access_token() -> str: ) +def test_init_client(): + client = AsyncRealtimeClient(URL, ANON_KEY) + + assert client is not None + assert client.url.startswith("ws://") or client.url.startswith("wss://") + assert "/websocket" in client.url + assert client.url.split("apikey=")[1] == ANON_KEY + assert client.auto_reconnect is True + assert client.params == {} + assert client.hb_interval == DEFAULT_HEARTBEAT_INTERVAL + assert client.max_retries == 5 + assert client.initial_backoff == 1.0 + assert client.timeout == DEFAULT_TIMEOUT + + @pytest.mark.asyncio -async def test_set_auth(socket: AsyncRealtimeClient): +async def test_set_auth_with_invalid_jwt(socket: AsyncRealtimeClient): await socket.connect() with pytest.raises(ValueError): @@ -58,7 +74,6 @@ async def test_set_auth(socket: AsyncRealtimeClient): @pytest.mark.asyncio async def test_broadcast_events(socket: AsyncRealtimeClient): await socket.connect() - listen_task = asyncio.create_task(socket.listen()) channel = socket.channel( "test-broadcast", params={"config": {"broadcast": {"self": True}}} @@ -74,7 +89,7 @@ def broadcast_callback(payload): subscribe_event = asyncio.Event() await channel.on_broadcast("test-event", broadcast_callback).subscribe( - lambda state, error: ( + lambda state, _: ( subscribe_event.set() if state == RealtimeSubscribeStates.SUBSCRIBED else None @@ -94,7 +109,6 @@ def broadcast_callback(payload): assert received_events[2]["payload"]["message"] == "Event 3" await socket.close() - listen_task.cancel() @pytest.mark.asyncio @@ -102,7 +116,6 @@ async def test_postgrest_changes(socket: AsyncRealtimeClient): token = await access_token() await socket.connect() - listen_task = asyncio.create_task(socket.listen()) await socket.set_auth(token) @@ -146,7 +159,7 @@ def delete_callback(payload): ).on_system( lambda _: system_event.set() ).subscribe( - lambda state, error: ( + lambda state, _: ( subscribed_event.set() if state == RealtimeSubscribeStates.SUBSCRIBED else None @@ -192,7 +205,6 @@ def delete_callback(payload): assert received_events["delete"] == [delete] await socket.close() - listen_task.cancel() async def create_todo(access_token: str, todo: dict) -> str: diff --git a/tests/test_presence.py b/tests/test_presence.py index c44699f..ef1023e 100644 --- a/tests/test_presence.py +++ b/tests/test_presence.py @@ -27,8 +27,6 @@ def socket() -> AsyncRealtimeClient: async def test_presence(socket: AsyncRealtimeClient): await socket.connect() - listen_task = asyncio.create_task(socket.listen()) - channel: AsyncRealtimeChannel = socket.channel("room") join_events: List[Tuple[str, List[Dict], List[Dict]]] = [] @@ -106,7 +104,6 @@ def on_leave(key: str, current_presences: List[Dict], left_presences: List[Dict] assert leave_events[0] != leave_events[1] await socket.close() - listen_task.cancel() def test_transform_state_raw_presence_state():