{"id":13288462,"url":"https://github.com/endrekrohn/fastws","last_synced_at":"2025-03-10T06:33:33.221Z","repository":{"id":179260520,"uuid":"662725818","full_name":"endrekrohn/fastws","owner":"endrekrohn","description":"FastWS framework around FastAPI with auto-documentation of WebSockets using AsyncAPI.","archived":false,"fork":false,"pushed_at":"2023-12-22T11:14:18.000Z","size":246,"stargazers_count":18,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-24T15:13:27.234Z","etag":null,"topics":["asyncapi","fastapi","pydantic","python"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/fastws","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/endrekrohn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-05T18:47:59.000Z","updated_at":"2024-04-20T13:24:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"46e34515-a737-480e-b48a-6719cf4ae98a","html_url":"https://github.com/endrekrohn/fastws","commit_stats":null,"previous_names":["endrekrohn/fastws"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endrekrohn%2Ffastws","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endrekrohn%2Ffastws/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endrekrohn%2Ffastws/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/endrekrohn%2Ffastws/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/endrekrohn","download_url":"https://codeload.github.com/endrekrohn/fastws/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242805420,"owners_count":20187995,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncapi","fastapi","pydantic","python"],"created_at":"2024-07-29T16:56:54.882Z","updated_at":"2025-03-10T06:33:30.551Z","avatar_url":"https://github.com/endrekrohn.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FastWS\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://github.com/endrekrohn/fastws\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/endrekrohn/fastws/assets/assets/fastws.png\" alt=\"FastWS\"/\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\n**Source Code**: \u003ca href=\"https://github.com/endrekrohn/fastws\" target=\"_blank\"\u003ehttps://github.com/endrekrohn/fastws\u003c/a\u003e\n\n---\n\nFastWS is a wrapper around FastAPI to create better WebSocket applications with auto-documentation using \u003ca href=\"https://www.asyncapi.com/\" target=\"_blank\"\u003eAsyncAPI\u003c/a\u003e, in a similar fashion as FastAPIs existing use of OpenAPI.\n\nThe current supported AsyncAPI verison is `2.4.0`. Once version `3.0.0` is released the plan is to upgrade to this standard.\n\n---\n## Example project\n\nIf you are familiar with FastAPI and want to look at an example project using FastWS \u003ca href=\"https://github.com/endrekrohn/fastws-example\" target=\"_blank\"\u003elook here\u003c/a\u003e👨‍💻\n\n---\n\n## Requirements\n\nPython 3.11+\n\n`FastWS` uses Pydantic v2 and FastAPI.\n\n## Installation\n\n\n```console\n$ pip install fastws\n```\n\n\nYou will also need an ASGI server, for production such as \u003ca href=\"https://www.uvicorn.org\" class=\"external-link\" target=\"_blank\"\u003eUvicorn\u003c/a\u003e or \u003ca href=\"https://github.com/pgjones/hypercorn\" class=\"external-link\" target=\"_blank\"\u003eHypercorn\u003c/a\u003e.\n\n\u003cdiv class=\"termy\"\u003e\n\n```console\n$ pip install \"uvicorn[standard]\"\n```\n\n\u003c/div\u003e\n\n## Example\n\n### Create it\n\n* Create a file `main.py` with:\n\n```Python\nfrom contextlib import asynccontextmanager\nfrom typing import Annotated\n\nfrom fastapi import Depends, FastAPI\nfrom fastws import Client, FastWS\n\nservice = FastWS()\n\n\n@service.send(\"ping\", reply=\"ping\")\nasync def send_event_a():\n    return\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    service.setup(app)\n    yield\n\n\napp = FastAPI(lifespan=lifespan)\n\n\n@app.websocket(\"/\")\nasync def fastws_stream(client: Annotated[Client, Depends(service.manage)]):\n    await service.serve(client)\n```\n\nWe can look at the generated documentation at `http://localhost:\u003cport\u003e/asyncapi`.\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"https://github.com/endrekrohn/fastws\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/endrekrohn/fastws/assets/assets/asyncapi_example.png\" alt=\"AsyncAPI Docs\"/\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n### Example breakdown\n\nFirst we import and initialize the service.\n\n\n```Python\nfrom fastws import Client, FastWS\n\nservice = FastWS()\n```\n\n#### Define event\n\nNext up we connect an operation (a WebSocket message) to the service, using the decorator `@service.send(...)`. We need to define the operation using a string similar to how we define an HTTP-endpoint using a path.\n\nThe operation-identificator is in this case `\"ping\"`, meaning we will use this string to identify what type of message we are receiving.\n\n```Python\n@service.send(\"ping\", reply=\"ping\")\nasync def send_event_a():\n    return\n```\n\nIf we want to define an `payload` for the operation we can extend the example:\n\n```Python\nfrom pydantic import BaseModel\n\nclass PingPayload(BaseModel):\n    foo: str\n\n@service.send(\"ping\", reply=\"ping\")\nasync def send_event_a(payload: PingPayload):\n    return\n```\n\nAn incoming message should now have the following format. (We will later view this in the generated AsyncAPI-documentation).\n\n```json\n{\n    \"type\": \"ping\",\n    \"payload\": {\n        \"foo\": \"bar\"\n    }\n}\n```\n#### Connect service\n\nNext up we connect the service to our running FastAPI application.\n\n```Python\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    service.setup(app)\n    yield\n\n\napp = FastAPI(lifespan=lifespan)\n\n\n@app.websocket(\"/\")\nasync def fastws_stream(client: Annotated[Client, Depends(service.manage)]):\n    await service.serve(client)\n```\n\nThe function `service.setup(app)` inside FastAPIs lifespan registers two endpoints\n- `/asyncapi.json`, to retrieve our API definition \n- `/asyncapi`, to view the AsyncAPI documentation UI.\n\nYou can override both of these URLs when initializing the service, or set them to `None` to avoid registering the endpoints at all.\n\n## Routing\n\nTo spread out our service we can use the `OperationRouter`-class.\n\n```Python\n# feature_1.py\nfrom fastws import Client, OperationRouter\nfrom pydantic import BaseModel\n\nrouter = OperationRouter(prefix=\"user.\")\n\n\nclass SubscribePayload(BaseModel):\n    topic: str\n\n\nclass SubscribeResponse(BaseModel):\n    detail: str\n    topics: set[str]\n\n\n@router.send(\"subscribe\", reply=\"subscribe.response\")\nasync def subscribe_to_topic(\n    payload: SubscribePayload,\n    client: Client,\n) -\u003e SubscribeResponse:\n    client.subscribe(payload.topic)\n    return SubscribeResponse(\n        detail=f\"Subscribed to {payload.topic}\",\n        topics=client.topics,\n    )\n```\n\nWe can then include the router in our main service.\n\n```Python\n# main.py\nfrom fastws import Client, FastWS\n\nfrom feature_1 import router\n\nservice = FastWS()\nservice.include_router(router)\n```\n\n## Operations, `send` and `recv`\n\nThe service enables two types of operations. Let us define these operations clearly:\n\n- `send`: An operation where API user sends a message to the API server.\n  \n  **Note**: Up to AsyncAPI version `2.6.0` this refers to a `publish`-operation, but is changing to `send` in version `3.0.0`.\n\n- `recv`: An operation where API server sends a message to the API user.\n  \n  **Note**: Up to AsyncAPI version `2.6.0` this refers to a `subscribe`-operation, but is changing to `receive` in version `3.0.0`.\n\n\n### The `send`-operation\n\nThe above examples have only displayed the use of `send`-operations.\n\nWhen using the functions `FastWS.client_send(message, client)` or `FastWS.serve(client)`, we implicitly send some arguments. These keyword-arguments have the following keywords and types:\n\n- `client` with type `fastws.application.Client` \n- `app` with type `fastws.application.FastWS`\n- `payload`, optional with type defined in the function processing the message.\n\nA `send`-operation can therefore access the following arguments:\n\n```Python\nfrom fastws import Client, FastWS\nfrom pydantic import BaseModel\n\nclass Something(BaseModel):\n    foo: str\n\n\nclass Thing(BaseModel):\n    bar: str\n\n\n@router.send(\"foo\", reply=\"bar\")\nasync def some_function(\n    payload: Something,\n    client: Client,\n    app: FastWS,\n) -\u003e Thing:\n    print(f\"{app.connections=}\")\n    print(f\"{client.uid=}\")\n\n    return Thing(bar=client.uid)\n```\n\n### The `recv`-operation\n\nWhen using the function `FastWS.server_send(message, topic)`, we implicitly send some arguments. These keyword-arguments have the keywords and types:\n\n- `app` with type `fastws.application.FastWS`\n- Optional `payload` with type defined in the function processing the message.\n\nA `recv`-operation can therefore access the following arguments:\n\n```Python\nfrom fastws import FastWS\nfrom pydantic import BaseModel\n\nclass AlertPayload(BaseModel):\n    message: str\n\n\n@router.recv(\"alert\")\nasync def recv_client(payload: AlertPayload, app: FastWS) -\u003e str:\n    return \"hey there!\"\n```\n\nIf we want create a message on the server side we can do the following:\n\n```Python\nfrom fastapi import FastAPI\nfrom fastws import FastWS\n\nservice = FastWS()\napp = FastAPI()\n\n@app.post(\"/\")\nasync def alert_on_topic_foobar(message: str):\n    await service.server_send(\n        Message(type=\"alert\", payload={\"message\": message}),\n        topic=\"foobar\",\n    )\n    return \"ok\"\n```\n\nIn the example above all connections subscribed to the topic `foobar` will recieve a message the the payload `\"hey there!\"`.\n\nIn this way you can on the server-side choose to publish messages from anywhere to any topic. This is especially useful if you have a persistent connection to Redis or similar that reads messages from some channel and want to propagate these to your users.\n\n## Authentication\n\nThere are to ways to tackle authentication using `FastWS`.\n\n### By defining `auth_handler`\n\nOne way is to provide a custom `auth_handler` when initializing the service. Below is an example where the API user must provide a secret message within a timeout to authenticate.\n\n```Python\nimport asyncio\nimport logging\nfrom fastapi import WebSocket\nfrom fastws import FastWS\n\n\ndef custom_auth(to_wait: float = 5):\n    async def handler(ws: WebSocket) -\u003e bool:\n        await ws.accept()\n        try:\n            initial_msg = await asyncio.wait_for(\n                ws.receive_text(),\n                timeout=to_wait,\n            )\n            return initial_msg == \"SECRET_HUSH_HUSH\"\n        except asyncio.exceptions.TimeoutError:\n            logging.info(\"Took to long to provide authentication\")\n\n        return False\n\n    return handler\n\n\nservice = FastWS(auth_handler=custom_auth())\n```\n\n### By using FastAPI dependency\n\nIf you want to use your own FastAPI dependency to handle authentication before it enters the FastWS service you will have to set `auto_ws_accept` to `False`.\n\n```Python\nimport asyncio\nfrom typing import Annotated\n\nfrom fastapi import Depends, FastAPI, WebSocket, WebSocketException, status\nfrom fastws import Client, FastWS\n\nservice = FastWS(auto_ws_accept=False)\n\napp = FastAPI()\n\n\nasync def custom_dep(ws: WebSocket):\n    await ws.accept()\n    initial_msg = await asyncio.wait_for(\n        ws.receive_text(),\n        timeout=5,\n    )\n    if initial_msg == \"SECRET_HUSH_HUSH\":\n        return\n    raise WebSocketException(\n        code=status.WS_1008_POLICY_VIOLATION,\n        reason=\"Not authenticated\",\n    )\n\n\n@app.websocket(\"/\")\nasync def fastws_stream(\n    client: Annotated[Client, Depends(service.manage)],\n    _=Depends(custom_dep),\n):\n    await service.serve(client)\n```\n\n## Heartbeats and connection lifespan\n\nTo handle a WebSocket's lifespan at an application level, FastWS tries to help you by using `asyncio.timeout()`-context managers in its `serve(client)`-function.\n\nYou can set the both:\n- `heartbeat_interval`: Meaning a client needs to send a message within this time.\n- `max_connection_lifespan`: Meaning all connections will disconnect when exceeding this time.\n\nThese must set during initialization:\n\n```Python\nfrom fastws import FastWS\n\nservice = FastWS(\n    heartbeat_interval=10,\n    max_connection_lifespan=300,\n)\n```\n\nBoth `heartbeat_interval` and `max_connection_lifespan` can be set to None to disable any restrictions. Note this is the default.\n\nPlease note that you can also set restrictions in your ASGI-server. These restrictions apply at a protocol/server-level and are different from the restrictions set by your application. Applicable settings for [uvicorn](https://www.uvicorn.org/#command-line-options):\n- `--ws-ping-interval` INTEGER\n- `--ws-ping-timeout` INTEGER\n- `--ws-max-size` INTEGER","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fendrekrohn%2Ffastws","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fendrekrohn%2Ffastws","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fendrekrohn%2Ffastws/lists"}