From 21393e7ca626c6612f8c6bda3117aefd0766813d Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:11:24 +0000 Subject: [PATCH 01/17] Stub socketio + starlette app --- pyproject.toml | 5 + src/socketio_app/__init__.py | 29 ++++ src/socketio_app/namespaces/__init__.py | 0 src/socketio_app/namespaces/chat.py | 12 ++ src/socketio_app/web_routes/__init__.py | 0 src/socketio_app/web_routes/docs.py | 111 +++++++++++++++ uv.lock | 182 ++++++++++++++++-------- 7 files changed, 282 insertions(+), 57 deletions(-) create mode 100644 src/socketio_app/__init__.py create mode 100644 src/socketio_app/namespaces/__init__.py create mode 100644 src/socketio_app/namespaces/chat.py create mode 100644 src/socketio_app/web_routes/__init__.py create mode 100644 src/socketio_app/web_routes/docs.py diff --git a/pyproject.toml b/pyproject.toml index d41ac1af..9beb9377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,11 @@ http = [ "strawberry-graphql[debug-server]>=0.204.0", "uvicorn[standard]<1.0.0,>=0.34.0", ] +websocket = [ + "python-socketio>=5.12.1", + "starlette>=0.45.3", + "uvicorn[standard]<1.0.0,>=0.34.0", +] dev = [ "asynctest", "coverage", diff --git a/src/socketio_app/__init__.py b/src/socketio_app/__init__.py new file mode 100644 index 00000000..19775f58 --- /dev/null +++ b/src/socketio_app/__init__.py @@ -0,0 +1,29 @@ +from typing import Union + +import socketio +from starlette.routing import Mount, Router + +from common import AppConfig, application_init +from socketio_app.namespaces.chat import ChatNamespace +from socketio_app.web_routes import docs + + +def create_app( + test_config: Union[AppConfig, None] = None, +) -> Router: + _config = test_config or AppConfig() + application_init(_config) + + # SocketIO App + sio = socketio.AsyncServer(async_mode="asgi") + # Namespaces are the equivalent of Routes. + sio.register_namespace(ChatNamespace("/chat")) + + # Render /docs endpoint using starlette, and all the rest handled with Socket.io + routes = [Mount("/docs", routes=docs.routes, name="docs"), Mount("", app=socketio.ASGIApp(sio), name="socketio")] + + # No need for whole starlette, we're rendering a simple couple of endpoints + # https://www.starlette.io/routing/#working-with-router-instances + app = Router(routes=routes) + + return app diff --git a/src/socketio_app/namespaces/__init__.py b/src/socketio_app/namespaces/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/socketio_app/namespaces/chat.py b/src/socketio_app/namespaces/chat.py new file mode 100644 index 00000000..2934f5e1 --- /dev/null +++ b/src/socketio_app/namespaces/chat.py @@ -0,0 +1,12 @@ +import socketio + + +class ChatNamespace(socketio.AsyncNamespace): + def on_connect(self, sid, environ): + pass + + def on_disconnect(self, sid, reason): + pass + + async def on_echo_message(self, sid, data): + await self.emit("echo_response", data) diff --git a/src/socketio_app/web_routes/__init__.py b/src/socketio_app/web_routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/socketio_app/web_routes/docs.py b/src/socketio_app/web_routes/docs.py new file mode 100644 index 00000000..da553123 --- /dev/null +++ b/src/socketio_app/web_routes/docs.py @@ -0,0 +1,111 @@ +import json + +from pydantic import BaseModel +from starlette.requests import Request +from starlette.responses import HTMLResponse, JSONResponse +from starlette.routing import Route + +from common import AppConfig +from common.asyncapi import get_schema + + +class PydanticResponse(JSONResponse): + def render(self, content: BaseModel) -> bytes: + return content.model_dump_json( + exclude_unset=True, + ).encode("utf-8") + + +async def asyncapi_json(request: Request) -> JSONResponse: + return PydanticResponse(get_schema()) + + +ASYNCAPI_COMPONENT_VERSION = "latest" + +ASYNCAPI_JS_DEFAULT_URL = ( + f"https://unpkg.com/@asyncapi/react-component@{ASYNCAPI_COMPONENT_VERSION}/browser/standalone/index.js" +) +NORMALIZE_CSS_DEFAULT_URL = "https://cdn.jsdelivr.net/npm/modern-normalize/modern-normalize.min.css" +ASYNCAPI_CSS_DEFAULT_URL = ( + f"https://unpkg.com/@asyncapi/react-component@{ASYNCAPI_COMPONENT_VERSION}/styles/default.min.css" +) + + +# https://github.com/asyncapi/asyncapi-react/blob/v2.5.0/docs/usage/standalone-bundle.md +async def get_asyncapi_html( + request: Request, +) -> HTMLResponse: + app_config = AppConfig() + """Generate HTML for displaying an AsyncAPI document.""" + config = { + "schema": { + "url": "/docs/asyncapi.json", + }, + "config": { + "show": { + "sidebar": request.query_params.get("sidebar", "true") == "true", + "info": request.query_params.get("info", "true") == "true", + "servers": request.query_params.get("servers", "true") == "true", + "operations": request.query_params.get("operations", "true") == "true", + "messages": request.query_params.get("messages", "true") == "true", + "schemas": request.query_params.get("schemas", "true") == "true", + "errors": request.query_params.get("errors", "true") == "true", + }, + "expand": { + "messageExamples": request.query_params.get("expand_message_examples") == "true", + }, + "sidebar": { + "showServers": "byDefault", + "showOperations": "byDefault", + }, + }, + } + + return HTMLResponse( + """ + + + + """ + f""" + {app_config.APP_NAME} AsyncAPI + """ + """ + + + + + """ + f""" + + + """ + """ + + + + +
+ """ + f""" + + + + + """ + ) + + +routes = [ + Route("/asyncapi.json", asyncapi_json, methods=["GET"]), + Route("/", get_asyncapi_html, methods=["GET"]), +] diff --git a/uv.lock b/uv.lock index 1cff8db5..c7319379 100644 --- a/uv.lock +++ b/uv.lock @@ -103,6 +103,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, ] +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, +] + [[package]] name = "bootstrap-fastapi-service" version = "0.1.0" @@ -160,6 +169,11 @@ http = [ { name = "strawberry-graphql", extra = ["debug-server"] }, { name = "uvicorn", extra = ["standard"] }, ] +websocket = [ + { name = "python-socketio" }, + { name = "starlette" }, + { name = "uvicorn", extra = ["standard"] }, +] [package.metadata] requires-dist = [ @@ -215,6 +229,11 @@ http = [ { name = "strawberry-graphql", extras = ["debug-server"], specifier = ">=0.204.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, ] +websocket = [ + { name = "python-socketio", specifier = ">=5.12.1" }, + { name = "starlette", specifier = ">=0.45.3" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, +] [[package]] name = "bracex" @@ -2002,6 +2021,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] +[[package]] +name = "python-engineio" +version = "4.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/e0/a9e0fe427ce7f1b7dbf9531fa00ffe4b557c4a7bc8e71891c115af123170/python_engineio-4.11.2.tar.gz", hash = "sha256:145bb0daceb904b4bb2d3eb2d93f7dbb7bb87a6a0c4f20a94cc8654dec977129", size = 91381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/8f/978a0b913e3f8ad33a9a2fe204d32efe3d1ee34ecb1f2829c1cfbdd92082/python_engineio-4.11.2-py3-none-any.whl", hash = "sha256:f0971ac4c65accc489154fe12efd88f53ca8caf04754c46a66e85f5102ef22ad", size = 59239 }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -2011,6 +2042,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, ] +[[package]] +name = "python-socketio" +version = "5.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/d0/40ed38076e8aee94785d546d3e3a1cae393da5806a8530be877187e2875f/python_socketio-5.12.1.tar.gz", hash = "sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c", size = 119991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a3/c69806f30dd81df5a99d592e7db4c930c3a9b098555aa97b0eb866b20b11/python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386", size = 76947 }, +] + [[package]] name = "python-ulid" version = "3.0.0" @@ -2188,27 +2232,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/17/529e78f49fc6f8076f50d985edd9a2cf011d1dbadb1cdeacc1d12afc1d26/ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", size = 3599458 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/f8/3fafb7804d82e0699a122101b5bee5f0d6e17c3a806dcbc527bb7d3f5b7a/ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", size = 11668400 }, - { url = "https://files.pythonhosted.org/packages/2e/a6/2efa772d335da48a70ab2c6bb41a096c8517ca43c086ea672d51079e3d1f/ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", size = 11628395 }, - { url = "https://files.pythonhosted.org/packages/dc/d7/cd822437561082f1c9d7225cc0d0fbb4bad117ad7ac3c41cd5d7f0fa948c/ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", size = 11090052 }, - { url = "https://files.pythonhosted.org/packages/9e/67/3660d58e893d470abb9a13f679223368ff1684a4ef40f254a0157f51b448/ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", size = 11882221 }, - { url = "https://files.pythonhosted.org/packages/79/d1/757559995c8ba5f14dfec4459ef2dd3fcea82ac43bc4e7c7bf47484180c0/ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", size = 11424862 }, - { url = "https://files.pythonhosted.org/packages/c0/96/7915a7c6877bb734caa6a2af424045baf6419f685632469643dbd8eb2958/ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", size = 12626735 }, - { url = "https://files.pythonhosted.org/packages/0e/cc/dadb9b35473d7cb17c7ffe4737b4377aeec519a446ee8514123ff4a26091/ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", size = 13255976 }, - { url = "https://files.pythonhosted.org/packages/5f/c3/ad2dd59d3cabbc12df308cced780f9c14367f0321e7800ca0fe52849da4c/ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", size = 12752262 }, - { url = "https://files.pythonhosted.org/packages/c7/17/5f1971e54bd71604da6788efd84d66d789362b1105e17e5ccc53bba0289b/ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", size = 14401648 }, - { url = "https://files.pythonhosted.org/packages/30/24/6200b13ea611b83260501b6955b764bb320e23b2b75884c60ee7d3f0b68e/ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", size = 12414702 }, - { url = "https://files.pythonhosted.org/packages/34/cb/f5d50d0c4ecdcc7670e348bd0b11878154bc4617f3fdd1e8ad5297c0d0ba/ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b", size = 11859608 }, - { url = "https://files.pythonhosted.org/packages/d6/f4/9c8499ae8426da48363bbb78d081b817b0f64a9305f9b7f87eab2a8fb2c1/ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", size = 11485702 }, - { url = "https://files.pythonhosted.org/packages/18/59/30490e483e804ccaa8147dd78c52e44ff96e1c30b5a95d69a63163cdb15b/ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", size = 12067782 }, - { url = "https://files.pythonhosted.org/packages/3d/8c/893fa9551760b2f8eb2a351b603e96f15af167ceaf27e27ad873570bc04c/ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", size = 12483087 }, - { url = "https://files.pythonhosted.org/packages/23/15/f6751c07c21ca10e3f4a51ea495ca975ad936d780c347d9808bcedbd7182/ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", size = 9852302 }, - { url = "https://files.pythonhosted.org/packages/12/41/2d2d2c6a72e62566f730e49254f602dfed23019c33b5b21ea8f8917315a1/ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", size = 10850051 }, - { url = "https://files.pythonhosted.org/packages/c6/e6/3d6ec3bc3d254e7f005c543a661a41c3e788976d0e52a1ada195bd664344/ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", size = 10078251 }, +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 }, + { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 }, + { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 }, + { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 }, + { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 }, + { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 }, + { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 }, + { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 }, + { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 }, + { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 }, + { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 }, + { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 }, + { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 }, + { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 }, + { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 }, ] [[package]] @@ -2229,6 +2273,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 }, +] + [[package]] name = "six" version = "1.17.0" @@ -2258,47 +2314,47 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.37" +version = "2.0.38" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/20/93ea2518df4d7a14ebe9ace9ab8bb92aaf7df0072b9007644de74172b06c/sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb", size = 9626249 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/21/aaf0cd2e7ee56e464af7cba38a54f9c1203570181ec5d847711f33c9f520/SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e", size = 2102915 }, - { url = "https://files.pythonhosted.org/packages/fd/01/6615256759515f13bb7d7b49981326f1f4e80ff1bd92dccd53f99dab79ea/SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069", size = 2094095 }, - { url = "https://files.pythonhosted.org/packages/6a/f2/400252bda1bd67da7a35bb2ab84d10a8ad43975d42f15b207a9efb765446/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1", size = 3076482 }, - { url = "https://files.pythonhosted.org/packages/40/c6/e7e8e894c8f065f96ca202cdb00454d60d4962279b3eb5a81b8766dfa836/SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84", size = 3084750 }, - { url = "https://files.pythonhosted.org/packages/d6/ee/1cdab04b7760e48273f2592037df156afae044e2e6589157673bd2a830c0/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f", size = 3040575 }, - { url = "https://files.pythonhosted.org/packages/4d/af/2dd456bfd8d4b9750792ceedd828bddf83860f2420545e5effbaf722dae5/SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4", size = 3066113 }, - { url = "https://files.pythonhosted.org/packages/dd/d7/ad997559574f94d7bd895a8a63996afef518d07e9eaf5a2a9cbbcb877c16/SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72", size = 2075239 }, - { url = "https://files.pythonhosted.org/packages/d0/82/141fbed705a21af2d825068831da1d80d720945df60c2b97ddc5133b3714/SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636", size = 2099307 }, - { url = "https://files.pythonhosted.org/packages/7c/37/4915290c1849337be6d24012227fb3c30c575151eec2b182ee5f45e96ce7/SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c", size = 2104098 }, - { url = "https://files.pythonhosted.org/packages/4c/f5/8cce9196434014a24cc65f6c68faa9a887080932361ee285986c0a35892d/SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5", size = 2094492 }, - { url = "https://files.pythonhosted.org/packages/9c/54/2df4b3d0d11b384b6e9a8788d0f1123243f2d2356e2ccf626f93dcc1a09f/SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8", size = 3212789 }, - { url = "https://files.pythonhosted.org/packages/57/4f/e1db9475f940f1c54c365ed02d4f6390f884fc95a6a4022ece7725956664/SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b", size = 3212784 }, - { url = "https://files.pythonhosted.org/packages/89/57/d93212e827d1f03a6cd4d0ea13775957c2a95161330fa47449b91153bd09/SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087", size = 3149616 }, - { url = "https://files.pythonhosted.org/packages/5f/c2/759347419f69cf0bbb76d330fbdbd24cefb15842095fe86bca623759b9e8/SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9", size = 3169944 }, - { url = "https://files.pythonhosted.org/packages/22/04/a19ecb53aa19bb8cf491ecdb6bf8c1ac74959cd4962e119e91d4e2b8ecaa/SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989", size = 2074686 }, - { url = "https://files.pythonhosted.org/packages/7b/9d/6e030cc2c675539dbc5ef73aa97a3cbe09341e27ad38caed2b70c4273aff/SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba", size = 2099891 }, - { url = "https://files.pythonhosted.org/packages/86/62/e5de4a5e0c4f5ceffb2b461aaa2378c0ee00642930a8c38e5b80338add0f/SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef", size = 2102692 }, - { url = "https://files.pythonhosted.org/packages/01/44/3b65f4f16abeffd611da0ebab9e3aadfca45d041a78a67835c41c6d28289/SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4", size = 2093079 }, - { url = "https://files.pythonhosted.org/packages/a4/d8/e3a6622e86e3ae3a41ba470d1bb095c1f2dedf6b71feae0b4b94b5951017/SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4", size = 3242509 }, - { url = "https://files.pythonhosted.org/packages/3a/ef/5a53a6a60ac5a5d4ed28959317dac1ff72bc16773ccd9b3fe79713fe27f3/SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd", size = 3253368 }, - { url = "https://files.pythonhosted.org/packages/67/f2/30f5012379031cd5389eb06455282f926a4f99258e5ee5ccdcea27f30d67/SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098", size = 3188655 }, - { url = "https://files.pythonhosted.org/packages/fe/df/905499aa051605aeda62c1faf33d941ffb7fda291159ab1c24ef5207a079/SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb", size = 3215281 }, - { url = "https://files.pythonhosted.org/packages/94/54/f2769e7e356520f75016d82ca43ed85e47ba50e636a34124db4625ae5976/SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761", size = 2072972 }, - { url = "https://files.pythonhosted.org/packages/c2/7f/241f059e0b7edb85845368f43964d6b0b41733c2f7fffaa993f8e66548a5/SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff", size = 2098597 }, - { url = "https://files.pythonhosted.org/packages/45/d1/e63e56ceab148e69f545703a74b90c8c6dc0a04a857e4e63a4c07a23cf91/SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658", size = 2097968 }, - { url = "https://files.pythonhosted.org/packages/fd/e5/93ce63310347062bd42aaa8b6785615c78539787ef4380252fcf8e2dcee3/SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb", size = 2088445 }, - { url = "https://files.pythonhosted.org/packages/1b/8c/d0e0081c09188dd26040fc8a09c7d87f539e1964df1ac60611b98ff2985a/SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4", size = 3174880 }, - { url = "https://files.pythonhosted.org/packages/79/f7/3396038d8d4ea92c72f636a007e2fac71faae0b59b7e21af46b635243d09/SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94", size = 3188226 }, - { url = "https://files.pythonhosted.org/packages/ef/33/7a1d85716b29c86a744ed43690e243cb0e9c32e3b68a67a97eaa6b49ef66/SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0", size = 3121425 }, - { url = "https://files.pythonhosted.org/packages/27/11/fa63a77c88eb2f79bb8b438271fbacd66a546a438e4eaba32d62f11298e2/SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6", size = 3149589 }, - { url = "https://files.pythonhosted.org/packages/b6/04/fcdd103b6871f2110460b8275d1c4828daa806997b0fa5a01c1cd7fd522d/SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2", size = 2070746 }, - { url = "https://files.pythonhosted.org/packages/d4/7c/e024719205bdc1465b7b7d3d22ece8e1ad57bc7d76ef6ed78bb5f812634a/SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2", size = 2094612 }, - { url = "https://files.pythonhosted.org/packages/3b/36/59cc97c365f2f79ac9f3f51446cae56dfd82c4f2dd98497e6be6de20fb91/SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1", size = 1894113 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/08/9a90962ea72acd532bda71249a626344d855c4032603924b1b547694b837/sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", size = 9634782 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/10/16ed1503e18c0ec4e17a1819ff44604368607eed3db1e1d89d33269fe5b9/SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6", size = 2105151 }, + { url = "https://files.pythonhosted.org/packages/79/e5/2e9a0807cba2e625204d04bc39a18a47478e4bacae353ae8a7f2e784c341/SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444", size = 2096335 }, + { url = "https://files.pythonhosted.org/packages/c1/97/8fa5cc6ed994eab611dcf0bc431161308f297c6f896f02a3ebb8d8aa06ea/SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5", size = 3078705 }, + { url = "https://files.pythonhosted.org/packages/a9/99/505feb8a9bc7027addaa2b312b8b306319cacbbd8a5231c4123ca1fa082a/SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2", size = 3086958 }, + { url = "https://files.pythonhosted.org/packages/39/26/fb7cef8198bb2627ac527b2cf6c576588db09856d634d4f1017280f8ab64/SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03", size = 3042798 }, + { url = "https://files.pythonhosted.org/packages/cc/7c/b6f9e0ee4e8e993fdce42477f9290b2b8373e672fb1dc0272179f0aeafb4/SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443", size = 3068318 }, + { url = "https://files.pythonhosted.org/packages/e6/22/903497e8202960c4249ffc340ec8de63f7fbdd4856bdfe854f617e124e90/SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297", size = 2077434 }, + { url = "https://files.pythonhosted.org/packages/20/a8/08f6ceccff5e0abb4a22e2e91c44b0e39911fda06b5d0c905dfc642de57a/SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7", size = 2101608 }, + { url = "https://files.pythonhosted.org/packages/00/6c/9d3a638f297fce288ba12a4e5dbd08ef1841d119abee9300c100eba00217/SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d", size = 2106330 }, + { url = "https://files.pythonhosted.org/packages/0e/57/d5fdee56f418491267701965795805662b1744de40915d4764451390536d/SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2", size = 2096730 }, + { url = "https://files.pythonhosted.org/packages/42/84/205f423f8b28329c47237b7e130a7f93c234a49fab20b4534bd1ff26a06a/SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2", size = 3215023 }, + { url = "https://files.pythonhosted.org/packages/77/41/94a558d47bffae5a361b0cfb3721324ea4154829dd5432f80bd4cfeecbc9/SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6", size = 3214991 }, + { url = "https://files.pythonhosted.org/packages/74/a0/cc3c030e7440bd17ce67c1875f50edb41d0ef17b9c76fbc290ef27bbe37f/SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c", size = 3151854 }, + { url = "https://files.pythonhosted.org/packages/24/ab/8ba2588c2eb1d092944551354d775ef4fc0250badede324d786a4395d10e/SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3", size = 3172158 }, + { url = "https://files.pythonhosted.org/packages/e0/73/2a3d6217e8e6abb553ed410ce5adc0bdec7effd684716f0fbaee5831d677/SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba", size = 2076965 }, + { url = "https://files.pythonhosted.org/packages/a4/17/364a99c8c5698492c7fa40fc463bf388f05b0b03b74028828b71a79dc89d/SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae", size = 2102169 }, + { url = "https://files.pythonhosted.org/packages/5a/f8/6d0424af1442c989b655a7b5f608bc2ae5e4f94cdf6df9f6054f629dc587/SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", size = 2104927 }, + { url = "https://files.pythonhosted.org/packages/25/80/fc06e65fca0a19533e2bfab633a5633ed8b6ee0b9c8d580acf84609ce4da/SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", size = 2095317 }, + { url = "https://files.pythonhosted.org/packages/98/2d/5d66605f76b8e344813237dc160a01f03b987201e974b46056a7fb94a874/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", size = 3244735 }, + { url = "https://files.pythonhosted.org/packages/73/8d/b0539e8dce90861efc38fea3eefb15a5d0cfeacf818614762e77a9f192f9/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", size = 3255581 }, + { url = "https://files.pythonhosted.org/packages/ac/a5/94e1e44bf5bdffd1782807fcc072542b110b950f0be53f49e68b5f5eca1b/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", size = 3190877 }, + { url = "https://files.pythonhosted.org/packages/91/13/f08b09996dce945aec029c64f61c13b4788541ac588d9288e31e0d3d8850/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", size = 3217485 }, + { url = "https://files.pythonhosted.org/packages/13/8f/8cfe2ba5ba6d8090f4de0e658330c53be6b7bf430a8df1b141c2b180dcdf/SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", size = 2075254 }, + { url = "https://files.pythonhosted.org/packages/c2/5c/e3c77fae41862be1da966ca98eec7fbc07cdd0b00f8b3e1ef2a13eaa6cca/SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", size = 2100865 }, + { url = "https://files.pythonhosted.org/packages/21/77/caa875a1f5a8a8980b564cc0e6fee1bc992d62d29101252561d0a5e9719c/SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", size = 2100201 }, + { url = "https://files.pythonhosted.org/packages/f4/ec/94bb036ec78bf9a20f8010c807105da9152dd84f72e8c51681ad2f30b3fd/SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", size = 2090678 }, + { url = "https://files.pythonhosted.org/packages/7b/61/63ff1893f146e34d3934c0860209fdd3925c25ee064330e6c2152bacc335/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", size = 3177107 }, + { url = "https://files.pythonhosted.org/packages/a9/4f/b933bea41a602b5f274065cc824fae25780ed38664d735575192490a021b/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", size = 3190435 }, + { url = "https://files.pythonhosted.org/packages/f5/23/9e654b4059e385988de08c5d3b38a369ea042f4c4d7c8902376fd737096a/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", size = 3123648 }, + { url = "https://files.pythonhosted.org/packages/83/59/94c6d804e76ebc6412a08d2b086a8cb3e5a056cd61508e18ddaf3ec70100/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", size = 3151789 }, + { url = "https://files.pythonhosted.org/packages/b2/27/17f143013aabbe1256dce19061eafdce0b0142465ce32168cdb9a18c04b1/SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", size = 2073023 }, + { url = "https://files.pythonhosted.org/packages/e2/3e/259404b03c3ed2e7eee4c179e001a07d9b61070334be91124cf4ad32eec7/SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", size = 2096908 }, + { url = "https://files.pythonhosted.org/packages/aa/e4/592120713a314621c692211eba034d09becaf6bc8848fabc1dc2a54d8c16/SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", size = 1896347 }, ] [package.optional-dependencies] @@ -2792,6 +2848,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, ] +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +] + [[package]] name = "zipp" version = "3.21.0" From e1c597f40932708f28057e4b6cb6782003e79ae1 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:48:55 +0000 Subject: [PATCH 02/17] Set all the web routes in the base app --- src/socketio_app/__init__.py | 8 ++++++-- src/socketio_app/web_routes/docs.py | 7 ------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/socketio_app/__init__.py b/src/socketio_app/__init__.py index 19775f58..02eec6f2 100644 --- a/src/socketio_app/__init__.py +++ b/src/socketio_app/__init__.py @@ -1,7 +1,7 @@ from typing import Union import socketio -from starlette.routing import Mount, Router +from starlette.routing import Mount, Router, Route from common import AppConfig, application_init from socketio_app.namespaces.chat import ChatNamespace @@ -20,7 +20,11 @@ def create_app( sio.register_namespace(ChatNamespace("/chat")) # Render /docs endpoint using starlette, and all the rest handled with Socket.io - routes = [Mount("/docs", routes=docs.routes, name="docs"), Mount("", app=socketio.ASGIApp(sio), name="socketio")] + routes = [ + Route("/docs/asyncapi.json", docs.asyncapi_json, methods=["GET"]), + Route("/docs", docs.get_asyncapi_html, methods=["GET"]), + Mount("", app=socketio.ASGIApp(sio), name="socketio") + ] # No need for whole starlette, we're rendering a simple couple of endpoints # https://www.starlette.io/routing/#working-with-router-instances diff --git a/src/socketio_app/web_routes/docs.py b/src/socketio_app/web_routes/docs.py index da553123..954418b8 100644 --- a/src/socketio_app/web_routes/docs.py +++ b/src/socketio_app/web_routes/docs.py @@ -3,7 +3,6 @@ from pydantic import BaseModel from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse -from starlette.routing import Route from common import AppConfig from common.asyncapi import get_schema @@ -103,9 +102,3 @@ async def get_asyncapi_html( """ ) - - -routes = [ - Route("/asyncapi.json", asyncapi_json, methods=["GET"]), - Route("/", get_asyncapi_html, methods=["GET"]), -] From 1735df021eb40d28a6b94a76857a92c15547fcd0 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:49:42 +0000 Subject: [PATCH 03/17] Created script and run configuration for dev environment --- .idea/runConfigurations/Socket_io.xml | 26 ++++++++++++++++++++++++++ src/socketio_app/dev_server.py | 4 ++++ 2 files changed, 30 insertions(+) create mode 100644 .idea/runConfigurations/Socket_io.xml create mode 100644 src/socketio_app/dev_server.py diff --git a/.idea/runConfigurations/Socket_io.xml b/.idea/runConfigurations/Socket_io.xml new file mode 100644 index 00000000..9fbcd7ed --- /dev/null +++ b/.idea/runConfigurations/Socket_io.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/src/socketio_app/dev_server.py b/src/socketio_app/dev_server.py new file mode 100644 index 00000000..10db1c6c --- /dev/null +++ b/src/socketio_app/dev_server.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8000, reload=True) From 3873b41c77d2e34a5f3a36e947aec258eaff6995 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:10:02 +0000 Subject: [PATCH 04/17] Execute uvicorn programmatically --- .idea/runConfigurations/HTTP_App.xml | 19 ------------------- .idea/runConfigurations/Socket_io.xml | 26 -------------------------- docker-compose.yaml | 12 +++--------- src/http_app/__main__.py | 8 ++++++++ src/http_app/dev_server.py | 8 ++++++++ src/socketio_app/__main__.py | 8 ++++++++ src/socketio_app/dev_server.py | 4 ++++ 7 files changed, 31 insertions(+), 54 deletions(-) delete mode 100644 .idea/runConfigurations/HTTP_App.xml delete mode 100644 .idea/runConfigurations/Socket_io.xml create mode 100644 src/http_app/__main__.py create mode 100644 src/http_app/dev_server.py create mode 100644 src/socketio_app/__main__.py diff --git a/.idea/runConfigurations/HTTP_App.xml b/.idea/runConfigurations/HTTP_App.xml deleted file mode 100644 index 0b113f4f..00000000 --- a/.idea/runConfigurations/HTTP_App.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Socket_io.xml b/.idea/runConfigurations/Socket_io.xml deleted file mode 100644 index 9fbcd7ed..00000000 --- a/.idea/runConfigurations/Socket_io.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index b239de7d..4a950e99 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,15 +16,9 @@ services: - otel-collector command: - opentelemetry-instrument - - uvicorn - - http_app:create_app - - --host - - 0.0.0.0 - - --port - - "8000" - - --factory - # Remember to disable the reloader in order to allow otel instrumentation - - --reload + - python + - ./http_app/dev_server.py + # Production image http: diff --git a/src/http_app/__main__.py b/src/http_app/__main__.py new file mode 100644 index 00000000..6142baab --- /dev/null +++ b/src/http_app/__main__.py @@ -0,0 +1,8 @@ +import uvicorn + +from common import AppConfig +from common.logs import init_logger + +if __name__ == "__main__": + init_logger(AppConfig()) + uvicorn.run("http_app:create_app", factory=True, host="0.0.0.0", port=8000) diff --git a/src/http_app/dev_server.py b/src/http_app/dev_server.py new file mode 100644 index 00000000..2c05736e --- /dev/null +++ b/src/http_app/dev_server.py @@ -0,0 +1,8 @@ +import uvicorn + +from common import AppConfig +from common.logs import init_logger + +if __name__ == "__main__": + init_logger(AppConfig()) + uvicorn.run("http_app:create_app", factory=True, host="0.0.0.0", port=8000, reload=True) diff --git a/src/socketio_app/__main__.py b/src/socketio_app/__main__.py new file mode 100644 index 00000000..b25fde4f --- /dev/null +++ b/src/socketio_app/__main__.py @@ -0,0 +1,8 @@ +import uvicorn + +from common import AppConfig +from common.logs import init_logger + +if __name__ == "__main__": + init_logger(AppConfig()) + uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8000) diff --git a/src/socketio_app/dev_server.py b/src/socketio_app/dev_server.py index 10db1c6c..b546be38 100644 --- a/src/socketio_app/dev_server.py +++ b/src/socketio_app/dev_server.py @@ -1,4 +1,8 @@ import uvicorn +from common import AppConfig +from common.logs import init_logger + if __name__ == "__main__": + init_logger(AppConfig()) uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8000, reload=True) From 5e85f57c277cbab212b4eec173ea02de54b69bac Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:29:02 +0000 Subject: [PATCH 05/17] Run socketio on port 8001 --- src/socketio_app/__main__.py | 2 +- src/socketio_app/dev_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/socketio_app/__main__.py b/src/socketio_app/__main__.py index b25fde4f..a660947e 100644 --- a/src/socketio_app/__main__.py +++ b/src/socketio_app/__main__.py @@ -5,4 +5,4 @@ if __name__ == "__main__": init_logger(AppConfig()) - uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8000) + uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8001) diff --git a/src/socketio_app/dev_server.py b/src/socketio_app/dev_server.py index b546be38..84a51b42 100644 --- a/src/socketio_app/dev_server.py +++ b/src/socketio_app/dev_server.py @@ -5,4 +5,4 @@ if __name__ == "__main__": init_logger(AppConfig()) - uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8000, reload=True) + uvicorn.run("socketio_app:create_app", factory=True, host="0.0.0.0", port=8001, reload=True) From 29e760cd66e327148bcbc3d17e51874a7b1f8db2 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:32:20 +0000 Subject: [PATCH 06/17] Update deps --- pyproject.toml | 4 ++-- uv.lock | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9beb9377..e06bbb9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,12 @@ dependencies = [ "opentelemetry-instrumentor-dramatiq", "orjson<4.0.0,>=3.10.12", "pydantic<3.0.0,>=2.2.1", + "pydantic-asyncapi>=0.2.1", "pydantic-settings<3.0.0,>=2.0.3", "rich<14.0.0,>=13.2.0", "SQLAlchemy[asyncio,mypy]<3.0.0,>=2.0.0", "sqlalchemy-bind-manager", "structlog<25.1.1,>=25.1.0", - "pydantic-asyncapi>=0.2.1", ] [dependency-groups] @@ -44,7 +44,7 @@ http = [ "strawberry-graphql[debug-server]>=0.204.0", "uvicorn[standard]<1.0.0,>=0.34.0", ] -websocket = [ +socketio = [ "python-socketio>=5.12.1", "starlette>=0.45.3", "uvicorn[standard]<1.0.0,>=0.34.0", diff --git a/uv.lock b/uv.lock index c7319379..772d20d3 100644 --- a/uv.lock +++ b/uv.lock @@ -169,7 +169,7 @@ http = [ { name = "strawberry-graphql", extra = ["debug-server"] }, { name = "uvicorn", extra = ["standard"] }, ] -websocket = [ +socketio = [ { name = "python-socketio" }, { name = "starlette" }, { name = "uvicorn", extra = ["standard"] }, @@ -229,7 +229,7 @@ http = [ { name = "strawberry-graphql", extras = ["debug-server"], specifier = ">=0.204.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, ] -websocket = [ +socketio = [ { name = "python-socketio", specifier = ">=5.12.1" }, { name = "starlette", specifier = ">=0.45.3" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, @@ -1068,13 +1068,16 @@ wheels = [ [[package]] name = "jsbeautifier" -version = "1.15.1" +version = "1.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "editorconfig" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/3e/dd37e1a7223247e3ef94714abf572415b89c4e121c4af48e9e4c392e2ca0/jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24", size = 75606 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/fb/309b9b87222957a1314087e8ac5103463444c692b2a082532a463641d4a1/jsbeautifier-1.15.2.tar.gz", hash = "sha256:6aff11af2c6cb9a2ce135f33a5b223cf5ee676ab7ff5da0edac01e23734f5755", size = 75266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/20/40b00db549c49766c0d499acedf93ba3a17072c44bad3b097e7fd90f8e80/jsbeautifier-1.15.2-py3-none-any.whl", hash = "sha256:d599aed6dcb0d5431190e5ad7335900d5fdc67236082fe6b6d3fb61d568d7417", size = 94708 }, +] [[package]] name = "libcst" @@ -1325,7 +1328,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.2" +version = "9.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1340,9 +1343,9 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/75/fb8f772d4acf5439a446aedbe6e49b4c42a4bc4f8c866c930a7b0c3be2f8/mkdocs_material-9.6.2.tar.gz", hash = "sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f", size = 3942567 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/1e/65b4fda4debf5e337b2ad4e692423dba4f5c77f49c4dee170c47a7dbac25/mkdocs_material-9.6.3.tar.gz", hash = "sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832", size = 3942608 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/17/b97aa245d43933acd416361d4f34612baec8ad4a6337339d45448cde728d/mkdocs_material-9.6.2-py3-none-any.whl", hash = "sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753", size = 8688648 }, + { url = "https://files.pythonhosted.org/packages/11/a4/e0da0bc6a7dbfda6a786427f82a0caa4dd1f163249a5a5e5dccbb50c5f1e/mkdocs_material-9.6.3-py3-none-any.whl", hash = "sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf", size = 8688709 }, ] [[package]] From ec8f582f386d867e4c7a997c2c48bea06ec62ddc Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:40:50 +0000 Subject: [PATCH 07/17] Update Dockerfile and docker-compose.yaml --- Dockerfile | 14 ++++++++++++- Makefile | 7 +++++-- docker-compose.yaml | 48 ++++++++++++--------------------------------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/Dockerfile b/Dockerfile index d3a080a2..162cd292 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,11 @@ FROM base_builder AS http_builder RUN --mount=type=cache,target=~/.cache/uv \ uv sync --no-dev --group http --no-install-project --frozen --no-editable +# Installs requirements to run production http application +FROM base_builder AS socketio_builder +RUN --mount=type=cache,target=~/.cache/uv \ + uv sync --no-dev --group socketio --no-install-project --frozen --no-editable + # Create the base app with the common python packages FROM base AS base_app USER nonroot @@ -75,7 +80,14 @@ FROM base_app AS http_app COPY --from=http_builder /venv /venv COPY --chown=nonroot:nonroot src/http_app ./http_app # Run CMD using array syntax, so it's uses `exec` and runs as PID1 -CMD ["opentelemetry-instrument", "uvicorn", "http_app:create_app", "--host", "0.0.0.0", "--port", "8000", "--factory"] +CMD ["opentelemetry-instrument", "python", "-m", "http_app"] + +# Copy the socketio python package and requirements from relevant builder +FROM base_app AS socketio_app +COPY --from=socketio_builder_builder /venv /venv +COPY --chown=nonroot:nonroot src/socketio_app ./socketio_app +# Run CMD using array syntax, so it's uses `exec` and runs as PID1 +CMD ["opentelemetry-instrument", "python", "-m", "socketio_app"] # Copy the dramatiq python package and requirements from relevant builder FROM base_app AS dramatiq_app diff --git a/Makefile b/Makefile index 95f185a5..8d2ea47f 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,11 @@ containers: # To build shared container layers only once we build a single container before the other ones docker compose build --build-arg UID=`id -u` -dev: - uv run uvicorn http_app:create_app --host 0.0.0.0 --port 8000 --factory --reload +dev-http: + uv run ./src/http_app/dev_server.py + +dev-socketio: + uv run ./src/socketio_app/dev_server.py otel: OTEL_SERVICE_NAME=bootstrap-fastapi OTEL_TRACES_EXPORTER=none OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none uv run opentelemetry-instrument uvicorn http_app:create_app --host 0.0.0.0 --port 8000 --factory diff --git a/docker-compose.yaml b/docker-compose.yaml index 4a950e99..f1865d05 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,5 @@ services: - dev: + dev-http: &dev build: dockerfile: Dockerfile context: . @@ -13,28 +13,23 @@ services: volumes: - '.:/app' depends_on: + - redis - otel-collector command: - opentelemetry-instrument - python - ./http_app/dev_server.py - - # Production image - http: - build: - dockerfile: Dockerfile - context: . - target: http_app - depends_on: - - otel-collector - env_file: local.env + dev-socketio: + <<: *dev environment: - OTEL_SERVICE_NAME: "bootstrap-fastapi-http" + OTEL_SERVICE_NAME: "bootstrap-socketio-dev" ports: - - '8001:8000' - volumes: - - './src/sqlite.db:/app/sqlite.db' + - '8001:8001' + command: + - opentelemetry-instrument + - python + - ./socketio_app/dev_server.py ######################### #### Helper services #### @@ -49,19 +44,10 @@ services: image: redis dramatiq-worker: - build: - dockerfile: Dockerfile - context: . - target: dramatiq_app - env_file: local.env + <<: *dev environment: OTEL_SERVICE_NAME: "bootstrap-fastapi-dramatiq-worker" - working_dir: "/app/src" - volumes: - - '.:/app' - depends_on: - - redis - - otel-collector + ports: [] command: - opentelemetry-instrument - dramatiq @@ -128,7 +114,7 @@ services: depends_on: - kratos - auth-ui - - dev + - dev-http ports: # Public traffic port - "8080:4455" @@ -155,11 +141,3 @@ services: - "make" - "test" - ci-test: - build: - dockerfile: Dockerfile - context: . - target: dev - command: - - "make" - - "ci-test" From 448b1e0b621be433b6f53ac0836f6fd29f8aecbb Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:17:30 +0000 Subject: [PATCH 08/17] Create separate services for otel testing --- .idea/runConfigurations/Dev_Stack.xml | 19 +++++++++++++++++ .idea/runConfigurations/FastAPI_app.xml | 26 +++++++++++++++++++++++ .idea/runConfigurations/Otel_Stack.xml | 19 +++++++++++++++++ .idea/runConfigurations/Socket_io_app.xml | 26 +++++++++++++++++++++++ docker-compose.yaml | 24 +++++++++++++++++---- src/socketio_app/namespaces/chat.py | 7 ++++++ tests/socketio_app/__init__.py | 0 7 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 .idea/runConfigurations/Dev_Stack.xml create mode 100644 .idea/runConfigurations/FastAPI_app.xml create mode 100644 .idea/runConfigurations/Otel_Stack.xml create mode 100644 .idea/runConfigurations/Socket_io_app.xml create mode 100644 tests/socketio_app/__init__.py diff --git a/.idea/runConfigurations/Dev_Stack.xml b/.idea/runConfigurations/Dev_Stack.xml new file mode 100644 index 00000000..30ae08c1 --- /dev/null +++ b/.idea/runConfigurations/Dev_Stack.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/FastAPI_app.xml b/.idea/runConfigurations/FastAPI_app.xml new file mode 100644 index 00000000..48f3cf26 --- /dev/null +++ b/.idea/runConfigurations/FastAPI_app.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Otel_Stack.xml b/.idea/runConfigurations/Otel_Stack.xml new file mode 100644 index 00000000..74f06162 --- /dev/null +++ b/.idea/runConfigurations/Otel_Stack.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Socket_io_app.xml b/.idea/runConfigurations/Socket_io_app.xml new file mode 100644 index 00000000..7262f6e2 --- /dev/null +++ b/.idea/runConfigurations/Socket_io_app.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index f1865d05..001af74e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,8 +5,6 @@ services: context: . target: dev env_file: local.env - environment: - OTEL_SERVICE_NAME: "bootstrap-fastapi-dev" ports: - '8000:8000' working_dir: "/app/src" @@ -16,11 +14,28 @@ services: - redis - otel-collector command: - - opentelemetry-instrument - python - ./http_app/dev_server.py dev-socketio: + <<: *dev + ports: + - '8001:8001' + command: + - python + - ./socketio_app/dev_server.py + + otel-http: + <<: *dev + environment: + OTEL_SERVICE_NAME: "bootstrap-fastapi-dev" + command: + - opentelemetry-instrument + - python + - -m + - http_app + + otel-socketio: <<: *dev environment: OTEL_SERVICE_NAME: "bootstrap-socketio-dev" @@ -29,7 +44,8 @@ services: command: - opentelemetry-instrument - python - - ./socketio_app/dev_server.py + - -m + - socketio_app ######################### #### Helper services #### diff --git a/src/socketio_app/namespaces/chat.py b/src/socketio_app/namespaces/chat.py index 2934f5e1..f3dd2dce 100644 --- a/src/socketio_app/namespaces/chat.py +++ b/src/socketio_app/namespaces/chat.py @@ -1,5 +1,9 @@ +import logging + import socketio +from common.tracing import trace_function + class ChatNamespace(socketio.AsyncNamespace): def on_connect(self, sid, environ): @@ -8,5 +12,8 @@ def on_connect(self, sid, environ): def on_disconnect(self, sid, reason): pass + @trace_function() async def on_echo_message(self, sid, data): + # Note: this log line is only used to verify opentelemetry instrumentation works + logging.info("received message") await self.emit("echo_response", data) diff --git a/tests/socketio_app/__init__.py b/tests/socketio_app/__init__.py new file mode 100644 index 00000000..e69de29b From d1b5354ffd7818b356feed7ec8e518e71ff63985 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:20:19 +0000 Subject: [PATCH 09/17] Lint --- pyproject.toml | 2 ++ src/socketio_app/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e06bbb9a..dc636aa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,5 +157,7 @@ ignore = [ [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore unused imports on init files +"__main__.py" = ["S104"] # Ignore 0.0.0.0 bindings for startup script +"dev_server.py" = ["S104"] # Ignore 0.0.0.0 bindings for startup script "tests/**/*.py" = ["S101"] # Allow assert usage on tests "src/migrations/env.py" = ["E501"] # Allow long lines diff --git a/src/socketio_app/__init__.py b/src/socketio_app/__init__.py index 02eec6f2..d8d6385d 100644 --- a/src/socketio_app/__init__.py +++ b/src/socketio_app/__init__.py @@ -1,7 +1,7 @@ from typing import Union import socketio -from starlette.routing import Mount, Router, Route +from starlette.routing import Mount, Route, Router from common import AppConfig, application_init from socketio_app.namespaces.chat import ChatNamespace @@ -23,7 +23,7 @@ def create_app( routes = [ Route("/docs/asyncapi.json", docs.asyncapi_json, methods=["GET"]), Route("/docs", docs.get_asyncapi_html, methods=["GET"]), - Mount("", app=socketio.ASGIApp(sio), name="socketio") + Mount("", app=socketio.ASGIApp(sio), name="socketio"), ] # No need for whole starlette, we're rendering a simple couple of endpoints From e3564a5859828bfa1186eb8f839936760650ac41 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:33:29 +0000 Subject: [PATCH 10/17] Move asyncapi documentation endpoint --- docs/api-documentation.md | 2 +- src/http_app/routes/__init__.py | 4 ++-- src/http_app/routes/{docs_ws.py => asyncapi.py} | 4 ++-- .../http_app/routes/{test_docs_ws.py => test_asyncapi.py} | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/http_app/routes/{docs_ws.py => asyncapi.py} (97%) rename tests/http_app/routes/{test_docs_ws.py => test_asyncapi.py} (93%) diff --git a/docs/api-documentation.md b/docs/api-documentation.md index c26367cd..2aaa7cf8 100644 --- a/docs/api-documentation.md +++ b/docs/api-documentation.md @@ -5,7 +5,7 @@ on `/docs` and `/redoc` paths using OpenAPI format. AsyncAPI documentation is rendered using the [AsyncAPI react components](https://github.com/asyncapi/asyncapi-react). -It is available on `/docs/ws` path. +It is available on `/asyncapi` path. ## API versioning diff --git a/src/http_app/routes/__init__.py b/src/http_app/routes/__init__.py index b8d96a91..579b30d2 100644 --- a/src/http_app/routes/__init__.py +++ b/src/http_app/routes/__init__.py @@ -2,7 +2,7 @@ from http_app.routes import ( api, - docs_ws, + asyncapi, events, graphql, hello, @@ -15,7 +15,7 @@ def init_routes(app: FastAPI) -> None: app.include_router(api.router) - app.include_router(docs_ws.router) + app.include_router(asyncapi.router) app.include_router(ping.router) app.include_router(hello.router) app.include_router(events.router) diff --git a/src/http_app/routes/docs_ws.py b/src/http_app/routes/asyncapi.py similarity index 97% rename from src/http_app/routes/docs_ws.py rename to src/http_app/routes/asyncapi.py index 90213c3e..f2f45213 100644 --- a/src/http_app/routes/docs_ws.py +++ b/src/http_app/routes/asyncapi.py @@ -10,7 +10,7 @@ from common.asyncapi import get_schema from http_app.dependencies import get_app_config -router = APIRouter(prefix="/docs/ws") +router = APIRouter(prefix="/asyncapi") @router.get( @@ -49,7 +49,7 @@ async def get_asyncapi_html( """Generate HTML for displaying an AsyncAPI document.""" config = { "schema": { - "url": "/docs/ws/asyncapi.json", + "url": "/asyncapi/asyncapi.json", }, "config": { "show": { diff --git a/tests/http_app/routes/test_docs_ws.py b/tests/http_app/routes/test_asyncapi.py similarity index 93% rename from tests/http_app/routes/test_docs_ws.py rename to tests/http_app/routes/test_asyncapi.py index 7cf62525..7dbc2622 100644 --- a/tests/http_app/routes/test_docs_ws.py +++ b/tests/http_app/routes/test_asyncapi.py @@ -15,14 +15,14 @@ ) -@patch("http_app.routes.docs_ws.get_schema", return_value=fake_schema) +@patch("http_app.routes.asyncapi.get_schema", return_value=fake_schema) async def test_asyncapi_json_is_whatever_returned_by_schema( mock_schema: MagicMock, testapp: FastAPI, ): ac = TestClient(app=testapp, base_url="http://test") response = ac.get( - "/docs/ws/asyncapi.json", + "/asyncapi/asyncapi.json", ) assert response.status_code == status.HTTP_200_OK @@ -51,7 +51,7 @@ async def test_ws_docs_renders_config_based_on_params( config = json.dumps( { "schema": { - "url": "/docs/ws/asyncapi.json", + "url": "/asyncapi/asyncapi.json", }, "config": { "show": { @@ -76,7 +76,7 @@ async def test_ws_docs_renders_config_based_on_params( ac = TestClient(app=testapp, base_url="http://test") response = ac.get( - "/docs/ws", + "/asyncapi", params={ "sidebar": sidebar, "info": info, From 07248f8e1720c5426b2dd7edda214b0558918ae5 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:36:26 +0000 Subject: [PATCH 11/17] Remove websocket endpoint from FastAPI application --- src/http_app/routes/__init__.py | 3 - src/http_app/routes/ws/__init__.py | 6 -- src/http_app/routes/ws/chat.py | 77 ---------------------- tests/http_app/routes/ws/__init__.py | 0 tests/http_app/routes/ws/test_chat.py | 92 --------------------------- 5 files changed, 178 deletions(-) delete mode 100644 src/http_app/routes/ws/__init__.py delete mode 100644 src/http_app/routes/ws/chat.py delete mode 100644 tests/http_app/routes/ws/__init__.py delete mode 100644 tests/http_app/routes/ws/test_chat.py diff --git a/src/http_app/routes/__init__.py b/src/http_app/routes/__init__.py index 579b30d2..b8528a7f 100644 --- a/src/http_app/routes/__init__.py +++ b/src/http_app/routes/__init__.py @@ -10,8 +10,6 @@ user_registered_hook, ) -from . import ws - def init_routes(app: FastAPI) -> None: app.include_router(api.router) @@ -21,4 +19,3 @@ def init_routes(app: FastAPI) -> None: app.include_router(events.router) app.include_router(user_registered_hook.router) app.include_router(graphql.router, prefix="/graphql") - app.include_router(ws.router) diff --git a/src/http_app/routes/ws/__init__.py b/src/http_app/routes/ws/__init__.py deleted file mode 100644 index a0c6d0ba..00000000 --- a/src/http_app/routes/ws/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter - -from . import chat - -router = APIRouter(prefix="/ws") -router.include_router(chat.router) diff --git a/src/http_app/routes/ws/chat.py b/src/http_app/routes/ws/chat.py deleted file mode 100644 index ef53e44b..00000000 --- a/src/http_app/routes/ws/chat.py +++ /dev/null @@ -1,77 +0,0 @@ -from fastapi import APIRouter -from starlette.websockets import WebSocket, WebSocketDisconnect - -from common.asyncapi import ( - register_channel, - register_channel_operation, - register_server, -) -from domains.books.events import BookUpdatedV1 - -router = APIRouter(prefix="/chat") - - -class ConnectionManager: - def __init__(self): - self.active_connections: list[WebSocket] = [] - - async def connect(self, websocket: WebSocket): - await websocket.accept() - self.active_connections.append(websocket) - - def disconnect(self, websocket: WebSocket): - self.active_connections.remove(websocket) - - async def send_personal_message(self, message: str, websocket: WebSocket): - await websocket.send_text(message) - - async def broadcast(self, message: str): - for connection in self.active_connections: - await connection.send_text(message) - - -manager = ConnectionManager() - - -@router.websocket("/{client_id}") -async def websocket_endpoint(websocket: WebSocket, client_id: int): - await manager.connect(websocket) - try: - while True: - data = await websocket.receive_text() - await manager.send_personal_message(f"You wrote: {data}", websocket) - await manager.broadcast(f"Client #{client_id} says: {data}") - except WebSocketDisconnect: - manager.disconnect(websocket) - await manager.broadcast(f"Client #{client_id} left the chat") - - -""" -In websocket case we create a server per route. -If we create other routes we can create more servers -""" -register_server( - id="chat", - # TODO: Inject host using config? - host="localhost/endpoint", - protocol="ws", -) -register_channel( - id="ChatChannel", - address="chat", - title="Chat channel", - description="A channel supporting send and receive chat messages between clients", - server_id="chat", -) -register_channel_operation( - channel_id="ChatChannel", - operation_type="send", - messages=[BookUpdatedV1], - operation_name="SendMessage", -) -register_channel_operation( - channel_id="ChatChannel", - operation_type="receive", - messages=[BookUpdatedV1], - operation_name="ReceiveMessage", -) diff --git a/tests/http_app/routes/ws/__init__.py b/tests/http_app/routes/ws/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/http_app/routes/ws/test_chat.py b/tests/http_app/routes/ws/test_chat.py deleted file mode 100644 index fc3bb76f..00000000 --- a/tests/http_app/routes/ws/test_chat.py +++ /dev/null @@ -1,92 +0,0 @@ -from unittest.mock import patch - -import pytest -from fastapi.testclient import TestClient - -from http_app.routes.ws.chat import ConnectionManager - - -@pytest.fixture -def test_client(testapp): - return TestClient(testapp) - - -@pytest.fixture -def connection_manager(): - return ConnectionManager() - - -def test_websocket_connection(test_client): - manager = ConnectionManager() - with patch("http_app.routes.ws.chat.manager", manager): - with test_client.websocket_connect("/ws/chat/1") as websocket: - websocket.send_text("Hello!") - data = websocket.receive_text() - - assert data == "You wrote: Hello!" - broadcast = websocket.receive_text() - assert broadcast == "Client #1 says: Hello!" - - -def test_multiple_clients(test_client): - manager = ConnectionManager() - with patch("http_app.routes.ws.chat.manager", manager): - with test_client.websocket_connect("/ws/chat/1") as websocket1: - with test_client.websocket_connect("/ws/chat/2") as websocket2: - # Client 1 sends message - websocket1.send_text("Hello from client 1") - - # Client 1 receives personal message - data1 = websocket1.receive_text() - assert data1 == "You wrote: Hello from client 1" - - # Both clients receive broadcast - broadcast1 = websocket1.receive_text() - broadcast2 = websocket2.receive_text() - assert broadcast1 == "Client #1 says: Hello from client 1" - assert broadcast2 == "Client #1 says: Hello from client 1" - - -def test_client_disconnect(test_client): - manager = ConnectionManager() - with patch("http_app.routes.ws.chat.manager", manager): - with test_client.websocket_connect("/ws/chat/1") as websocket1: - with test_client.websocket_connect("/ws/chat/2") as websocket2: - # Close first client - websocket1.close() - - # Second client should receive disconnect message - disconnect_message = websocket2.receive_text() - assert disconnect_message == "Client #1 left the chat" - - -async def test_connection_manager(): - manager = ConnectionManager() - - # Mock WebSocket for testing - class MockWebSocket: - def __init__(self): - self.received_messages = [] - - async def accept(self): - pass - - async def send_text(self, message: str): - self.received_messages.append(message) - - # Test connect - websocket = MockWebSocket() - await manager.connect(websocket) - assert len(manager.active_connections) == 1 - - # Test personal message - await manager.send_personal_message("test message", websocket) - assert websocket.received_messages[-1] == "test message" - - # Test broadcast - await manager.broadcast("broadcast message") - assert websocket.received_messages[-1] == "broadcast message" - - # Test disconnect - manager.disconnect(websocket) - assert len(manager.active_connections) == 0 From 45e766301f691295c790b9e62b23d6472492e885 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:45:34 +0000 Subject: [PATCH 12/17] Use nested models to test all paths in asyncapi implementation --- tests/common/test_asyncapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/common/test_asyncapi.py b/tests/common/test_asyncapi.py index 90048f9e..e6496520 100644 --- a/tests/common/test_asyncapi.py +++ b/tests/common/test_asyncapi.py @@ -36,6 +36,7 @@ class SomeTestMessage(BaseModel): class AnotherTestMessage(BaseModel): status: bool code: int + nested: SomeTestMessage # Test cases From b439b49190861b4dabf5af95ac93d3c2b197d7af Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:45:59 +0000 Subject: [PATCH 13/17] Ignore untyped socketio package --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dc636aa2..9088df36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,12 @@ module = [ ] ignore_missing_imports = true +[[tool.mypy.overrides]] +module = [ + "socketio.*" +] +ignore_missing_imports = true + [tool.pytest.ini_options] minversion = "6.0" addopts = "-n auto --cov-report=term-missing" From d5a1263938688caa28ac3f57c28a821c4134ca50 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 22:52:05 +0000 Subject: [PATCH 14/17] Don't calculate coverage for startup scripts --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9088df36..b3339ba6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,8 @@ omit = [ "src/common/config.py", "src/common/logs/*", "src/dramatiq_worker/__init__.py", + "src/**/__main__.py", + "src/**/dev_server.py", ] # It's not necessary to configure concurrency here # because pytest-cov takes care of that From 54a5743820ac88a26060101818c07f3712db550d Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Fri, 7 Feb 2025 23:09:12 +0000 Subject: [PATCH 15/17] Export traces to jaeger service --- docker-compose.yaml | 9 +++++++++ otel-collector-config.yaml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 001af74e..1a3d7476 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -51,8 +51,17 @@ services: #### Helper services #### ######################### + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "6831:6831/udp" # UDP port for Jaeger agent + - "16686:16686" # Web UI + - "14268:14268" # HTTP port for spans + otel-collector: image: otel/opentelemetry-collector-contrib + depends_on: + - jaeger volumes: - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml diff --git a/otel-collector-config.yaml b/otel-collector-config.yaml index 8a18bf14..d767dd00 100644 --- a/otel-collector-config.yaml +++ b/otel-collector-config.yaml @@ -7,7 +7,7 @@ receivers: exporters: debug: verbosity: detailed - otlp: + otlp/jaeger: endpoint: jaeger:4317 tls: insecure: true @@ -22,5 +22,5 @@ service: exporters: [debug] traces: receivers: [otlp] - exporters: [debug] + exporters: [otlp/jaeger] processors: [batch] From 2493392294216f9920cdd8f336f91c50ef6d4bd9 Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sat, 8 Feb 2025 00:13:27 +0000 Subject: [PATCH 16/17] Add tests --- tests/socketio_app/namespaces/__init__.py | 0 tests/socketio_app/namespaces/test_chat.py | 47 +++++++++++ tests/socketio_app/test_app_factory.py | 55 +++++++++++++ tests/socketio_app/web_routes/__init__.py | 0 tests/socketio_app/web_routes/test_docs.py | 96 ++++++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 tests/socketio_app/namespaces/__init__.py create mode 100644 tests/socketio_app/namespaces/test_chat.py create mode 100644 tests/socketio_app/test_app_factory.py create mode 100644 tests/socketio_app/web_routes/__init__.py create mode 100644 tests/socketio_app/web_routes/test_docs.py diff --git a/tests/socketio_app/namespaces/__init__.py b/tests/socketio_app/namespaces/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/socketio_app/namespaces/test_chat.py b/tests/socketio_app/namespaces/test_chat.py new file mode 100644 index 00000000..b50c0c40 --- /dev/null +++ b/tests/socketio_app/namespaces/test_chat.py @@ -0,0 +1,47 @@ +from unittest.mock import AsyncMock, patch + +import pytest + +from socketio_app import ChatNamespace + + +@pytest.fixture +def chat_namespace(): + return ChatNamespace("/chat") + + +@pytest.mark.asyncio +async def test_on_connect(chat_namespace): + sid = "test_session_id" + environ = {} + + # Test that connect doesn't raise any exceptions + chat_namespace.on_connect(sid, environ) + + +@pytest.mark.asyncio +async def test_on_disconnect(chat_namespace): + sid = "test_session_id" + reason = "test_reason" + + # Test that disconnect doesn't raise any exceptions + chat_namespace.on_disconnect(sid, reason) + + +@pytest.mark.asyncio +async def test_on_echo_message(chat_namespace): + sid = "test_session_id" + test_data = {"message": "Hello, World!"} + + # Mock the emit method + chat_namespace.emit = AsyncMock() + + # Mock the logging + with patch("logging.info") as mock_log: + await chat_namespace.on_echo_message(sid, test_data) + + # Verify logging was called + mock_log.assert_called_once_with("received message") + + # Verify emit was called with correct arguments + chat_namespace.emit.assert_called_once_with("echo_response", test_data) diff --git a/tests/socketio_app/test_app_factory.py b/tests/socketio_app/test_app_factory.py new file mode 100644 index 00000000..293f467c --- /dev/null +++ b/tests/socketio_app/test_app_factory.py @@ -0,0 +1,55 @@ +from unittest.mock import patch + +import socketio +from starlette.routing import Mount, Router + +from common import AppConfig +from socketio_app import create_app + + +def test_create_app_returns_router(): + """Test that create_app returns a Router instance""" + app = create_app() + assert isinstance(app, Router) + + +def test_create_app_with_custom_config(): + """Test that create_app accepts custom config""" + test_config = AppConfig(DEBUG=True) + with patch("common.bootstrap.init_storage", return_value=None): + app = create_app(test_config=test_config) + + assert isinstance(app, Router) + + +def test_create_app_routes(): + """Test that create_app creates all expected routes""" + with patch("common.bootstrap.init_storage", return_value=None): + app = create_app() + + # Check that we have exactly 3 routes (docs JSON, docs HTML, and socketio mount) + assert len(app.routes) == 3 + + # Check routes paths and methods + routes = [(route.path, getattr(route, "methods", None)) for route in app.routes] + assert ("/docs/asyncapi.json", {"GET", "HEAD"}) in routes + assert ("/docs", {"GET", "HEAD"}) in routes + + # Check that one route is a Mount instance for socketio + mount_routes = [route for route in app.routes if isinstance(route, Mount)] + assert len(mount_routes) == 1 + assert mount_routes[0].name == "socketio" + assert isinstance(mount_routes[0].app, socketio.ASGIApp) + + +def test_create_app_socketio_namespace(): + """Test that socketio server has the chat namespace registered""" + with patch("common.bootstrap.init_storage", return_value=None): + app = create_app() + + # Find the socketio mount + socketio_mount = next(route for route in app.routes if isinstance(route, Mount)) + sio_app = socketio_mount.app + + # Check that the chat namespace is registered + assert "/chat" in sio_app.engineio_server.namespace_handlers diff --git a/tests/socketio_app/web_routes/__init__.py b/tests/socketio_app/web_routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/socketio_app/web_routes/test_docs.py b/tests/socketio_app/web_routes/test_docs.py new file mode 100644 index 00000000..73ce51ec --- /dev/null +++ b/tests/socketio_app/web_routes/test_docs.py @@ -0,0 +1,96 @@ +from unittest.mock import Mock, patch + +import pytest +from pydantic import BaseModel +from starlette.requests import Request + +from socketio_app.web_routes.docs import ( + ASYNCAPI_CSS_DEFAULT_URL, + ASYNCAPI_JS_DEFAULT_URL, + NORMALIZE_CSS_DEFAULT_URL, + PydanticResponse, + asyncapi_json, + get_asyncapi_html, +) + + +# Test model +class TestModel(BaseModel): + name: str + value: int + + +# Fixtures +@pytest.fixture +def test_model(): + return TestModel(name="test", value=42) + + +@pytest.fixture +def mock_request(): + return Mock(spec=Request) + + +@pytest.fixture +def mock_app_config(): + with patch("socketio_app.web_routes.docs.AppConfig") as mock: + mock.return_value.APP_NAME = "Test App" + yield mock + + +# Tests for PydanticResponse +def test_pydantic_response_render(test_model): + response = PydanticResponse(test_model) + expected = b'{"name":"test","value":42}' + assert response.render(test_model) == expected + + +# Tests for asyncapi_json endpoint +async def test_asyncapi_json(mock_request, test_model): + with patch("socketio_app.web_routes.docs.get_schema") as mock_get_schema: + mock_get_schema.return_value = test_model + response = await asyncapi_json(mock_request) + assert isinstance(response, PydanticResponse) + assert response.body == b'{"name":"test","value":42}' + + +# Tests for get_asyncapi_html endpoint +async def test_get_asyncapi_html_default_params(mock_request, mock_app_config): + mock_request.query_params = {} + response = await get_asyncapi_html(mock_request) + + assert response.status_code == 200 + assert response.headers["content-type"] == "text/html; charset=utf-8" + + content = response.body.decode() + assert "Test App AsyncAPI" in content + assert ASYNCAPI_JS_DEFAULT_URL in content + assert NORMALIZE_CSS_DEFAULT_URL in content + assert ASYNCAPI_CSS_DEFAULT_URL in content + assert '"sidebar": true' in content + assert '"info": true' in content + + +async def test_get_asyncapi_html_custom_params(mock_request, mock_app_config): + mock_request.query_params = { + "sidebar": "false", + "info": "false", + "servers": "false", + "operations": "false", + "messages": "false", + "schemas": "false", + "errors": "false", + "expand_message_examples": "true", + } + + response = await get_asyncapi_html(mock_request) + content = response.body.decode() + + assert '"sidebar": false' in content + assert '"info": false' in content + assert '"servers": false' in content + assert '"operations": false' in content + assert '"messages": false' in content + assert '"schemas": false' in content + assert '"errors": false' in content + assert '"messageExamples": true' in content From f92c89ea367dbab6a2996896cfeef786e1c0256d Mon Sep 17 00:00:00 2001 From: Federico Busetti <729029+febus982@users.noreply.github.com> Date: Sat, 8 Feb 2025 00:16:29 +0000 Subject: [PATCH 17/17] Update README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46b36741..c6156641 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This template provides out of the box some commonly used functionalities: * Sync and Async API Documentation using [FastAPI](https://fastapi.tiangolo.com/) and [AsyncAPI](https://www.asyncapi.com/en) * Async tasks execution using [Dramatiq](https://dramatiq.io/index.html) +* Websocket application using [Socket.io](https://python-socketio.readthedocs.io/en/stable/index.html) * Repository pattern for databases using [SQLAlchemy](https://www.sqlalchemy.org/) and [SQLAlchemy bind manager](https://febus982.github.io/sqlalchemy-bind-manager/stable/) * Database migrations using [Alembic](https://alembic.sqlalchemy.org/en/latest/) (configured supporting both sync and async SQLAlchemy engines) * Database fixtures support using customized [Alembic](https://alembic.sqlalchemy.org/en/latest/) configuration @@ -50,7 +51,8 @@ Using Docker: * `make containers`: Build containers * `docker compose run --rm dev make migrate`: Run database migrations -* `docker compose up dev`: Run HTTP application with hot reload +* `docker compose up dev-http`: Run HTTP application with hot reload +* `docker compose up dev-socketio`: Run HTTP application with hot reload * `docker compose up dramatiq-worker`: Run the dramatiq worker * `docker compose run --rm test`: Run test suite @@ -61,7 +63,8 @@ Locally: * `make dev-dependencies`: Install dev requirements * `make update-dependencies`: Updates requirements * `make migrate`: Run database migrations -* `make dev`: Run HTTP application with hot reload +* `make dev-http`: Run HTTP application with hot reload +* `make dev-socketio`: Run HTTP application with hot reload * `make test`: Run test suite ## Other commands for development