Metadata-Version: 2.4
Name: socketspec
Version: 0.1.1
Summary: FastAPI-style WebSocket framework with built-in docs and testing
Project-URL: Homepage, https://github.com/ByteCraftByLaiba/socketspec
Project-URL: Documentation, https://ByteCraftByLaiba.github.io/socketspec/
Project-URL: Repository, https://github.com/ByteCraftByLaiba/socketspec
Project-URL: Issues, https://github.com/ByteCraftByLaiba/socketspec/issues
Project-URL: Changelog, https://github.com/ByteCraftByLaiba/socketspec/blob/main/CHANGELOG.md
Author-email: Laiba Shahab <its.laiba.shahab@email.com>
License: Apache-2.0
License-File: AUTHORS
License-File: LICENSE
License-File: NOTICE
Keywords: async,fastapi,pydantic,realtime,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: anyio>=4.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pyjwt>=2.8
Requires-Dist: websockets>=12.0
Provides-Extra: all
Requires-Dist: django>=4.2; extra == 'all'
Requires-Dist: fastapi>=0.100; extra == 'all'
Requires-Dist: redis>=5.0; extra == 'all'
Requires-Dist: uvicorn>=0.29; extra == 'all'
Provides-Extra: dev
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pre-commit>=3.7; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: towncrier>=23.0; extra == 'dev'
Requires-Dist: uvicorn>=0.29; extra == 'dev'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Requires-Dist: uvicorn>=0.29; extra == 'fastapi'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# SocketSpec

**FastAPI-style WebSocket framework with built-in interactive docs and testing.**

[![CI](https://github.com/ByteCraftByLaiba/socketspec/actions/workflows/ci.yml/badge.svg)](https://github.com/ByteCraftByLaiba/socketspec/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/socketspec)](https://pypi.org/project/socketspec/)
[![Python](https://img.shields.io/pypi/pyversions/socketspec)](https://pypi.org/project/socketspec/)
[![Coverage](https://img.shields.io/badge/coverage-%E2%89%A590%25-brightgreen)](https://github.com/ByteCraftByLaiba/socketspec)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)

---

## Install

```bash
pip install socketspec[fastapi]
```

---

## Quickstart

```python
from fastapi import FastAPI
from pydantic import BaseModel
from socketspec import SocketApp
from socketspec.adapters.fastapi import mount

socket = SocketApp(docs=True)

class ChatMessage(BaseModel):
    room: str
    text: str

class MessageAck(BaseModel):
    status: str
    message_id: str

@socket.on(
    "send_message",
    description="Send a chat message to a room.",
    tags=["chat"],
    emits=[Emits("message_ack", model=MessageAck, description="Delivery confirmation")],
    broadcasts=[Broadcasts("new_message", room="chat:{room}", description="Delivered to room members")],
)
async def send_message(conn, payload: ChatMessage) -> None:
    await conn.emit("message_ack", {"status": "ok", "message_id": "abc123"})
    await socket.rooms.broadcast(
        "chat:" + payload.room,
        "new_message",
        {"from": conn.id, "text": payload.text},
    )

app = FastAPI()
mount(socket, app, path="/ws")
```

```bash
uvicorn main:app --reload
```

---

## Docs UI

Open **`/socket-docs`** in your browser after starting the server.

<!-- screenshot: add after first live demo -->

- Click an event card to expand its schema
- Hit **Try it out** to send a live WebSocket message
- See the server response appear inline, without leaving the browser
- Open a second tab and connect as a different user to test broadcasts

---

## Why SocketSpec

| Feature | python-socketio | channels (Django) | SocketSpec |
|---|---|---|---|
| FastAPI-native | ✗ | ✗ | ✅ |
| Pydantic payload validation | ✗ | ✗ | ✅ |
| Built-in interactive docs | ✗ | ✗ | ✅ |
| `TestClient` for unit tests | ✗ | partial | ✅ |
| Room guards / permissions | manual | manual | ✅ |
| Dependency injection (`Depends`) | ✗ | ✗ | ✅ |
| Type-safe (`mypy --strict`) | ✗ | ✗ | ✅ |

---

## Core Concepts

**Event handlers** look exactly like FastAPI route handlers:

```python
@socket.on("join_room", tags=["rooms"])
async def join_room(conn: Connection, payload: JoinPayload) -> None:
    await socket.rooms.join(conn, f"chat:{payload.room_id}")
```

**Rooms** with pattern-based guards:

```python
@socket.room_guard("admin:{room}")
async def admin_only(conn: Connection, room: str) -> bool:
    return conn.identity.role == "admin"
```

**Testing** without a real server:

```python
from socketspec.testing import TestClient

async def test_send_message():
    async with TestClient(socket) as client:
        conn = await client.connect()
        await conn.send("send_message", {"room": "general", "text": "hello"})
        response = await conn.receive()
        assert response["event"] == "message_ack"
```

---

## Links

- [Documentation](https://ByteCraftByLaiba.github.io/socketspec/)
- [GitHub](https://github.com/ByteCraftByLaiba/socketspec)
- [PyPI](https://pypi.org/project/socketspec/)
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)

---

## License

Apache 2.0 — see [LICENSE](LICENSE).
Created and maintained by [Laiba Shahab](https://github.com/ByteCraftByLaiba).
