Skip to content

Commit 21393e7

Browse files
committed
Stub socketio + starlette app
1 parent a32ffd2 commit 21393e7

File tree

7 files changed

+282
-57
lines changed

7 files changed

+282
-57
lines changed

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ http = [
4444
"strawberry-graphql[debug-server]>=0.204.0",
4545
"uvicorn[standard]<1.0.0,>=0.34.0",
4646
]
47+
websocket = [
48+
"python-socketio>=5.12.1",
49+
"starlette>=0.45.3",
50+
"uvicorn[standard]<1.0.0,>=0.34.0",
51+
]
4752
dev = [
4853
"asynctest",
4954
"coverage",

src/socketio_app/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Union
2+
3+
import socketio
4+
from starlette.routing import Mount, Router
5+
6+
from common import AppConfig, application_init
7+
from socketio_app.namespaces.chat import ChatNamespace
8+
from socketio_app.web_routes import docs
9+
10+
11+
def create_app(
12+
test_config: Union[AppConfig, None] = None,
13+
) -> Router:
14+
_config = test_config or AppConfig()
15+
application_init(_config)
16+
17+
# SocketIO App
18+
sio = socketio.AsyncServer(async_mode="asgi")
19+
# Namespaces are the equivalent of Routes.
20+
sio.register_namespace(ChatNamespace("/chat"))
21+
22+
# Render /docs endpoint using starlette, and all the rest handled with Socket.io
23+
routes = [Mount("/docs", routes=docs.routes, name="docs"), Mount("", app=socketio.ASGIApp(sio), name="socketio")]
24+
25+
# No need for whole starlette, we're rendering a simple couple of endpoints
26+
# https://www.starlette.io/routing/#working-with-router-instances
27+
app = Router(routes=routes)
28+
29+
return app

src/socketio_app/namespaces/__init__.py

Whitespace-only changes.

src/socketio_app/namespaces/chat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import socketio
2+
3+
4+
class ChatNamespace(socketio.AsyncNamespace):
5+
def on_connect(self, sid, environ):
6+
pass
7+
8+
def on_disconnect(self, sid, reason):
9+
pass
10+
11+
async def on_echo_message(self, sid, data):
12+
await self.emit("echo_response", data)

src/socketio_app/web_routes/__init__.py

Whitespace-only changes.

src/socketio_app/web_routes/docs.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import json
2+
3+
from pydantic import BaseModel
4+
from starlette.requests import Request
5+
from starlette.responses import HTMLResponse, JSONResponse
6+
from starlette.routing import Route
7+
8+
from common import AppConfig
9+
from common.asyncapi import get_schema
10+
11+
12+
class PydanticResponse(JSONResponse):
13+
def render(self, content: BaseModel) -> bytes:
14+
return content.model_dump_json(
15+
exclude_unset=True,
16+
).encode("utf-8")
17+
18+
19+
async def asyncapi_json(request: Request) -> JSONResponse:
20+
return PydanticResponse(get_schema())
21+
22+
23+
ASYNCAPI_COMPONENT_VERSION = "latest"
24+
25+
ASYNCAPI_JS_DEFAULT_URL = (
26+
f"https://unpkg.com/@asyncapi/react-component@{ASYNCAPI_COMPONENT_VERSION}/browser/standalone/index.js"
27+
)
28+
NORMALIZE_CSS_DEFAULT_URL = "https://cdn.jsdelivr.net/npm/modern-normalize/modern-normalize.min.css"
29+
ASYNCAPI_CSS_DEFAULT_URL = (
30+
f"https://unpkg.com/@asyncapi/react-component@{ASYNCAPI_COMPONENT_VERSION}/styles/default.min.css"
31+
)
32+
33+
34+
# https://github.com/asyncapi/asyncapi-react/blob/v2.5.0/docs/usage/standalone-bundle.md
35+
async def get_asyncapi_html(
36+
request: Request,
37+
) -> HTMLResponse:
38+
app_config = AppConfig()
39+
"""Generate HTML for displaying an AsyncAPI document."""
40+
config = {
41+
"schema": {
42+
"url": "/docs/asyncapi.json",
43+
},
44+
"config": {
45+
"show": {
46+
"sidebar": request.query_params.get("sidebar", "true") == "true",
47+
"info": request.query_params.get("info", "true") == "true",
48+
"servers": request.query_params.get("servers", "true") == "true",
49+
"operations": request.query_params.get("operations", "true") == "true",
50+
"messages": request.query_params.get("messages", "true") == "true",
51+
"schemas": request.query_params.get("schemas", "true") == "true",
52+
"errors": request.query_params.get("errors", "true") == "true",
53+
},
54+
"expand": {
55+
"messageExamples": request.query_params.get("expand_message_examples") == "true",
56+
},
57+
"sidebar": {
58+
"showServers": "byDefault",
59+
"showOperations": "byDefault",
60+
},
61+
},
62+
}
63+
64+
return HTMLResponse(
65+
"""
66+
<!DOCTYPE html>
67+
<html>
68+
<head>
69+
"""
70+
f"""
71+
<title>{app_config.APP_NAME} AsyncAPI</title>
72+
"""
73+
"""
74+
<link rel="icon" href="https://www.asyncapi.com/favicon.ico">
75+
<link rel="icon" type="image/png" sizes="16x16" href="https://www.asyncapi.com/favicon-16x16.png">
76+
<link rel="icon" type="image/png" sizes="32x32" href="https://www.asyncapi.com/favicon-32x32.png">
77+
<link rel="icon" type="image/png" sizes="194x194" href="https://www.asyncapi.com/favicon-194x194.png">
78+
"""
79+
f"""
80+
<link rel="stylesheet" href="{NORMALIZE_CSS_DEFAULT_URL}">
81+
<link rel="stylesheet" href="{ASYNCAPI_CSS_DEFAULT_URL}">
82+
"""
83+
"""
84+
</head>
85+
86+
87+
<body>
88+
<div id="asyncapi"></div>
89+
"""
90+
f"""
91+
<script src="{ASYNCAPI_JS_DEFAULT_URL}"></script>
92+
<script>
93+
"""
94+
f"""
95+
AsyncApiStandalone.render(
96+
{json.dumps(config)},
97+
document.getElementById('asyncapi')
98+
);
99+
"""
100+
"""
101+
</script>
102+
</body>
103+
</html>
104+
"""
105+
)
106+
107+
108+
routes = [
109+
Route("/asyncapi.json", asyncapi_json, methods=["GET"]),
110+
Route("/", get_asyncapi_html, methods=["GET"]),
111+
]

0 commit comments

Comments
 (0)