Skip to content

Dependencies

Dependencies in SocketAPI allow you to inject reusable logic into your actions and channels. Using the Depends function with Python's Annotated type hints, you can create shared functionality like authentication, database connections, or data processing that can be reused across multiple endpoints.

Basic Dependency Definition

from typing import Annotated
from socketapi import Depends, SocketAPI

app = SocketAPI()

async def common_dependency(a: int, b: str) -> str:
    return "dependency result"

@app.action("greet")
async def greet(dep: Annotated[str, Depends(common_dependency)]):
    return {"message": dep}

This creates a dependency function common_dependency that can be injected into any action or channel.

Using Dependencies in Actions

When you use a dependency in an action, clients must provide the dependency's parameters within the action call:

Send:

{
    "type": "action",
    "channel": "greet",
    "data": {
        "dep": {
            "a": 42,
            "b": "hello"
        }
    }
}

Receive:

{
    "type": "action",
    "channel": "greet",
    "status": "completed",
    "data": {"message": "dependency result"}
}

The dependency function common_dependency receives the parameters a and b from the nested dep object in the request data.

Nested Dependencies

Dependencies can depend on other dependencies, creating a chain of reusable components:

async def common_dependency(a: int, b: str) -> str:
    return "dependency result"

async def nested_dependency(
    x: Annotated[str, Depends(common_dependency)]
) -> dict[str, str]:
    return {"x": x}

@app.action("process")
async def process(
    dep: Annotated[dict[str, str], Depends(nested_dependency)]
) -> dict[str, str]:
    return dep

Send:

{
    "type": "action",
    "channel": "process",
    "data": {
        "dep": {
            "x": {
                "a": 100,
                "b": "world"
            }
        }
    }
}

Receive:

{
    "type": "action",
    "channel": "process",
    "status": "completed",
    "data": {"x": "dependency result"}
}

The nested_dependency function depends on common_dependency, and the process action depends on nested_dependency. Parameters are nested accordingly in the request data.

Using Pydantic Models in Dependencies

Dependencies can use Pydantic models for complex data validation:

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class ComplexDataModel(BaseModel):
    name: str
    value: int
    address: Address

async def complex_data_dependency(complex_data: ComplexDataModel) -> ComplexDataModel:
    complex_data.value += 10
    return complex_data

@app.action("process_data")
async def process_data(
    dep: Annotated[ComplexDataModel, Depends(complex_data_dependency)]
) -> ComplexDataModel:
    return dep

Send:

{
    "type": "action",
    "channel": "process_data",
    "data": {
        "dep": {
            "complex_data": {
                "name": "Test",
                "value": 5,
                "address": {
                    "street": "123 Main St",
                    "city": "Anytown",
                    "zip_code": "12345"
                }
            }
        }
    }
}

Receive:

{
    "type": "action",
    "channel": "process_data",
    "status": "completed",
    "data": {
        "name": "Test",
        "value": 15,
        "address": {
            "street": "123 Main St",
            "city": "Anytown",
            "zip_code": "12345"
        }
    }
}

The dependency modifies the value field (adding 10) before returning it to the action.

Common Use Cases

Authentication

from typing import Annotated

class User(BaseModel):
    id: int
    username: str
    role: str

async def get_current_user(token: str) -> User:
    # Validate token and fetch user from database
    if token == "invalid":
        raise ValueError("Invalid token")
    return User(id=1, username="alice", role="admin")

@app.action("get_profile")
async def get_profile(
    current_user: Annotated[User, Depends(get_current_user)]
) -> User:
    return current_user

Send:

{
    "type": "action",
    "channel": "get_profile",
    "data": {
        "current_user": {
            "token": "valid_token_here"
        }
    }
}

Database Connection

class DatabaseConnection:
    def __init__(self, db_url: str):
        self.db_url = db_url

    async def query(self, sql: str):
        # Execute query
        return []

async def get_db(db_url: str = "postgresql://localhost/mydb") -> DatabaseConnection:
    db = DatabaseConnection(db_url)
    return db

@app.action("fetch_users")
async def fetch_users(
    db: Annotated[DatabaseConnection, Depends(get_db)]
):
    users = await db.query("SELECT * FROM users")
    return {"users": users}

Data Transformation

from datetime import datetime

class TransformedData(BaseModel):
    data: str
    timestamp: int
    processed: bool

async def transform_data(raw_data: str) -> TransformedData:
    return TransformedData(
        data=raw_data.upper(),
        timestamp=int(datetime.now().timestamp()),
        processed=True
    )

@app.action("submit_data")
async def submit_data(
    transformed: Annotated[TransformedData, Depends(transform_data)]
) -> TransformedData:
    # Save transformed data
    return transformed

Send:

{
    "type": "action",
    "channel": "submit_data",
    "data": {
        "transformed": {
            "raw_data": "hello world"
        }
    }
}

Receive:

{
    "type": "action",
    "channel": "submit_data",
    "status": "completed",
    "data": {
        "data": "HELLO WORLD",
        "timestamp": 1701234567,
        "processed": true
    }
}

Dependencies in Channels

Dependencies work the same way in channels as they do in actions:

from typing import Annotated

async def validate_token(token: str) -> dict:
    # Validate user token
    return {"user_id": 123, "valid": True}

@app.channel("private_updates")
async def private_updates(
    auth: Annotated[dict, Depends(validate_token), RequiredOnSubscribe],
    message: str = "Welcome"
):
    return {"message": message, "user_id": auth["user_id"]}

Subscribe:

{
    "type": "subscribe",
    "channel": "private_updates",
    "data": {
        "auth": {
            "token": "secret_token"
        }
    }
}

Receive:

{"type": "subscribed", "channel": "private_updates"}

Then receive initial data:

{
    "type": "data",
    "channel": "private_updates",
    "data": {"message": "Welcome", "user_id": 123}
}

Multiple Dependencies

Actions and channels can use multiple dependencies:

async def get_user(token: str) -> User:
    return User(id=1, username="alice", role="admin")

async def get_settings(theme: str = "dark") -> dict:
    return {"theme": theme, "notifications": True}

@app.action("get_dashboard")
async def get_dashboard(
    user: Annotated[User, Depends(get_user)],
    settings: Annotated[dict, Depends(get_settings)]
):
    return {
        "user": user,
        "settings": settings
    }

Send:

{
    "type": "action",
    "channel": "get_dashboard",
    "data": {
        "user": {
            "token": "valid_token"
        },
        "settings": {
            "theme": "light"
        }
    }
}