Skip to content

Security Middleware

Shield (headers), CORS, CSRF, rate limiting strategies and backends

Version: 2026-08-11 Audience: Core maintainers, security engineers, application developers Purpose: Document Shield (security headers), CORS, CSRF protection, and rate limiting. The four security middleware components


Sillo’s security layer consists of four independent middleware components, each handling a specific aspect of HTTP security:

flowchart TD
    REQ[Incoming Request] --> SHIELD[Shield]
    SHIELD --> CORS[CORSMiddleware]
    CORS --> CSRF[CSRFMiddleware]
    CSRF --> RL[RateLimitMiddleware]
    RL --> HANDLER[Route Handler]
    HANDLER --> RESP[Response]
    RL -->|429| RESP
    CSRF -->|403| RESP
    CORS -->|preflight| RESP
    SHIELD -->|SSL redirect| RESP
MiddlewarePurposeDefault State
ShieldHTTP security headers (CSP, HSTS, XSS, etc.)Enabled (headers on every response)
CORSMiddlewareCross-origin request handlingDisabled (must be explicitly configured)
CSRFMiddlewareCSRF token validationDisabled (enabled=False by default)
RateLimitMiddlewareRequest rate limitingDisabled (must be explicitly added)

None of the four extends BaseMiddleware. Each is plain raw ASGI — __init__(self, ...) setting self.app = None, and async def __call__(self, scope, receive, send) — registered the way it always has been, as an already-configured instance (app.use(Shield(...))), which SilloApp.use() recognises isn’t a bare class and binds next_app onto directly. See the middleware architecture reference (§19) for the shared pieces this makes possible without a shared base class: HttpContext built directly inside __call__ rather than through the dispatch bridge, and ResponseHeaders for editing a response’s headers in place.

None of them shares a dispatch/call_next hook, either — each names its own methods for what it actually does:

MiddlewareMethods
Shieldapply_security_headers(headers)
CORSMiddlewarecheck_request(ctx), apply_cors_headers(origin, headers)
CSRFMiddlewarevalidate(ctx), set_token_cookie(ctx, headers)
RateLimitMiddlewarecheck(ctx), set_limit_headers(headers, result)

The “before” half (reading the request, deciding whether to short-circuit) runs in __call__ itself before the downstream app; the “after” half (editing response headers) runs from a closure __call__ wraps send in, called when the downstream app’s http.response.start message passes through.


File: core/sillo/security/shield.py

Shield injects comprehensive HTTP security headers into every response. It also handles SSL redirect. A backward-compatibility alias exists at core/sillo/middleware/security.py:

SecurityMiddleware = Shield
class Shield:
def __init__(
self,
# Content Security Policy
csp_enabled: bool = True,
csp_policy: dict[str, str | list[str]] | None = None,
csp_report_only: bool = False,
# HSTS
hsts_enabled: bool = True,
hsts_max_age: int = 31536000,
hsts_include_subdomains: bool = True,
hsts_preload: bool = False,
# XSS Protection
xss_protection: bool = True,
xss_mode: str = "block",
# Frame Options
frame_options: str = "DENY",
frame_options_allow_from: str | None = None,
# Content Type Options
content_type_options: bool = True,
# Referrer Policy
referrer_policy: str = "strict-origin-when-cross-origin",
# Permissions Policy
permissions_policy: dict[str, str | list[str]] | None = None,
# SSL/HTTPS
ssl_redirect: bool = False,
ssl_host: str | None = None,
ssl_permanent: bool = True,
# Cache Control
cache_control: str = "no-store, no-cache, must-revalidate, proxy-revalidate",
# Cross-Origin Options
cross_origin_opener_policy: str = "same-origin",
cross_origin_embedder_policy: str = "require-corp",
cross_origin_resource_policy: str = "same-origin",
# Expect-CT
expect_ct: bool = False,
expect_ct_max_age: int = 86400,
expect_ct_enforce: bool = False,
expect_ct_report_uri: str | None = None,
# Trusted Types
trusted_types: bool = False,
trusted_types_policies: list[str] | None = None,
# Server
hide_server: bool = True,
server_header: str | None = None,
...
)
HeaderDefault ValuePurpose
Content-Security-Policydefault-src 'self'; script-src 'self'; ...Prevents XSS, data injection
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS
X-XSS-Protection1; mode=blockLegacy XSS filter
X-Frame-OptionsDENYPrevents clickjacking
X-Content-Type-OptionsnosniffPrevents MIME sniffing
Referrer-Policystrict-origin-when-cross-originControls referrer information
Permissions-Policy(empty by default)Controls browser feature access
Cache-Controlno-store, no-cache, must-revalidate, proxy-revalidatePrevents caching sensitive data
Cross-Origin-Opener-Policysame-originIsolates browsing context
Cross-Origin-Embedder-Policyrequire-corpControls cross-origin embedding
Cross-Origin-Resource-Policysame-originControls cross-origin reads
X-DNS-Prefetch-ControloffControls DNS prefetching
X-Download-OptionsnoopenPrevents IE file download execution
{
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'"],
"img-src": ["'self'"],
"connect-src": ["'self'"],
"font-src": ["'self'"],
"object-src": ["'none'"],
"media-src": ["'self'"],
"frame-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
}

When ssl_redirect=True, any HTTP request is redirected to HTTPS:

from sillo import redirect
# Inside __call__, before the downstream app runs
if self.ssl_redirect and ctx.url.scheme != "https":
redirect_url = f"https://{self.ssl_host or ctx.url.hostname}{ctx.url.path}"
response = redirect(url=redirect_url, status_code=301 if self.ssl_permanent else 302)
await response(scope, receive, send)
return
  • ssl_host: override the hostname (e.g. for load balancers)
  • ssl_permanent: True for 301, False for 302
def _build_csp_header(self) -> str:
policies = []
for directive, sources in self.csp_policy.items():
if isinstance(sources, str):
sources = [sources]
policies.append(f"{directive} {' '.join(sources)}")
return "; ".join(policies)

When trusted_types=True, the CSP header is extended with:

require-trusted-types-for 'script'; trusted-types <policies>
  • hide_server=True (default): removes the Server header
  • hide_server=False, server_header="MyApp/1.0": sets a custom server header

apply_security_headers(headers) — the ResponseHeaders editor for the http.response.start message — builds a dict from every header the response already has, adds Shield’s own into it, and writes the result back with headers.set_headers(computed, override_all=True). override_all=True matters: set_headers() defaults to appending each entry rather than replacing, and computed already contains the response’s own pre-existing headers alongside Shield’s additions, so the default would append a second, duplicate copy of every header Shield never meant to touch — Content-Type and Content-Length included. This was a real bug in the dispatch-based version of Shield, fixed as part of moving it to raw ASGI.


CORSMiddleware: Cross-Origin Resource Sharing

Section titled “CORSMiddleware: Cross-Origin Resource Sharing”

Files:

  • core/sillo/security/cors/config.py: CorsConfig
  • core/sillo/security/cors/_middleware.py: CORSMiddleware
class CorsConfig:
def __init__(
self,
allow_origins: list[str] | None = None,
blacklist_origins: list[str] | None = None,
allow_methods: list[str] | None = None,
blacklist_headers: list[str] | None = None,
allow_headers: list[str] | None = None,
allow_credentials: bool = True,
allow_origin_regex: str | None = None,
expose_headers: list[str] | None = None,
max_age: int = 600,
strict_origin_checking: bool = False,
dynamic_origin_validator: Callable[[str | None], bool] | None = None,
debug: bool = False,
custom_error_status: int = 400,
custom_error_messages: dict[str, str] | None = None,
)
ParameterDefaultPurpose
allow_origins[]List of allowed origins. ["*"] allows all.
blacklist_origins[]Origins always denied (checked before allow)
allow_methodsGET, POST, PUT, DELETE, PATCH, OPTIONSAllowed HTTP methods
blacklist_headers[]Headers always denied
allow_headers[]Additional allowed headers (safelisted headers always included)
allow_credentialsTrueWhether to include Access-Control-Allow-Credentials
allow_origin_regexNoneRegex pattern for allowed origins
expose_headers[]Headers exposed to the browser
max_age600Preflight cache duration (seconds)
strict_origin_checkingFalseReject requests without Origin header
dynamic_origin_validatorNoneCallable for runtime origin validation
def is_allowed_origin(self, origin: str | None) -> bool:
if origin in self.blacklist_origins:
return False
if "*" in self.allow_origins:
return True
if self.allow_origin_regex and self.allow_origin_regex.fullmatch(origin):
return True
if self.dynamic_origin_validator and callable(self.dynamic_origin_validator):
return self.dynamic_origin_validator(origin)
return origin in self.allow_origins

Validation order:

  1. Blacklist check (always first)
  2. Wildcard "*" check
  3. Regex pattern match
  4. Dynamic validator callback
  5. Exact match in allow_origins
sequenceDiagram
    participant Browser
    participant CORS as CORSMiddleware

    Browser->>CORS: OPTIONS /api/data
    Note right of Browser: Origin: https://app.example.com
    Note right of Browser: Access-Control-Request-Method: POST
    Note right of Browser: Access-Control-Request-Headers: Content-Type

    CORS->>CORS: is_allowed_origin(origin)
    CORS->>CORS: is_allowed_method(requested_method)
    CORS->>CORS: Check requested headers

    alt All checks pass
        CORS-->>Browser: 201 OK
        Note left of CORS: Access-Control-Allow-Origin: https://app.example.com
        Note left of CORS: Access-Control-Allow-Methods: POST
        Note left of CORS: Access-Control-Allow-Headers: content-type
        Note left of CORS: Access-Control-Max-Age: 600
    else Check fails
        CORS-->>Browser: 400 CORS request denied.
    end

For non-preflight requests, the middleware:

  1. Runs the downstream app (check_request found nothing to reject or answer)
  2. Once its response starts, apply_cors_headers sets Access-Control-Allow-Origin if the origin is allowed
  3. Sets Access-Control-Allow-Credentials if configured
  4. Sets Access-Control-Expose-Headers if configured

The following headers are always allowed (per the CORS spec):

SAFELISTED_HEADERS = {"accept", "accept-language", "content-language", "content-type"}

CSRFMiddleware: Cross-Site Request Forgery Protection

Section titled “CSRFMiddleware: Cross-Site Request Forgery Protection”

Files:

  • core/sillo/security/csrf/config.py: CSRFConfig
  • core/sillo/security/csrf/_middleware.py: CSRFMiddleware
class CSRFConfig:
def __init__(
self,
enabled: bool = False, # DISABLED by default
required_urls: list[str] | None = None,
exempt_urls: list[str] | None = None,
sensitive_cookies: list[str] | None = None,
safe_methods: list[str] | None = None,
cookie_name: str = "csrftoken",
cookie_path: str = "/",
cookie_domain: str | None = None,
cookie_secure: bool = False,
cookie_httponly: bool = True,
cookie_samesite: Literal["lax", "none", "strict"] = "lax",
header_name: str = "X-CSRFToken",
secret_key: str | None = None,
)
ParameterDefaultPurpose
enabledFalseCSRF protection is disabled by default
required_urls["*"]URL patterns requiring CSRF validation
exempt_urls[]URL patterns exempt from CSRF validation
sensitive_cookies[]Cookies that trigger CSRF validation on exempt URLs
safe_methodsGET, HEAD, OPTIONS, TRACEHTTP methods that skip CSRF validation
cookie_name"csrftoken"Name of the CSRF cookie
header_name"X-CSRFToken"Name of the CSRF header
secret_keyNoneSecret for signing tokens (required for operation)

The middleware uses an internal URLSafeSerializer to sign tokens:

def _generate_csrf_token(self) -> str:
return self.serializer.dumps(secrets.token_urlsafe(32))

A random token is generated with secrets.token_urlsafe(32), then signed with HMAC-SHA256. The signed token is stored in a cookie and must be submitted back in the X-CSRFToken header.

sequenceDiagram
    participant Client
    participant MW as CSRFMiddleware

    Note over Client,MW: GET /form (safe method)
    Client->>MW: GET /form
    MW->>MW: Generate CSRF token
    MW-->>Client: Set-Cookie: csrftoken=<signed_token>

    Note over Client,MW: POST /form (unsafe method)
    Client->>MW: POST /form
    Note right of Client: Cookie: csrftoken=<signed_token>
    Note right of Client: Header: X-CSRFToken=<signed_token>
    MW->>MW: Read cookie token
    MW->>MW: Read header token
    MW->>MW: _csrf_tokens_match(cookie, header)
    MW->>MW: Verify signature + constant-time compare
    MW-->>Client: 200 OK (or 403 if mismatch)
def _csrf_tokens_match(self, token1, token2) -> bool:
try:
decoded1 = self.serializer.loads(token1)
decoded2 = self.serializer.loads(token2)
return secrets.compare_digest(decoded1, decoded2)
except BadSignature:
return False

Both tokens are decoded (signature verified) and then compared with constant-time comparison.

def _url_is_required(self, url: str) -> bool:
if not self.required_urls:
return False
if "*" in self.required_urls:
return True
for required_url in self.required_urls:
match = re.match(required_url, url)
if match and match.group() == url:
return True
return False

URL patterns are matched as regexes with exact-match semantics (the match must cover the entire URL).

flowchart TD
    A[Request received] --> B{CSRF enabled?}
    B -->|no| C[Continue to handler]
    B -->|yes| D{Safe method?}
    D -->|yes| C
    D -->|no| E{URL required?}
    E -->|no| C
    E -->|yes| F[Read cookie token]
    F --> G{Cookie present?}
    G -->|no| H[403 CSRF token missing from cookies]
    G -->|yes| I[Read header token]
    I --> J{Header present?}
    J -->|no| K[403 CSRF token missing from headers]
    J -->|yes| L{Tokens match?}
    L -->|no| M[403 CSRF token incorrect]
    L -->|yes| C

Files:

  • core/sillo/security/ratelimit/config.py: RateLimitConfig
  • core/sillo/security/ratelimit/_middleware.py: RateLimitMiddleware
  • core/sillo/security/ratelimit/__init__.py: RateLimit convenience class
  • core/sillo/security/ratelimit/strategies/base.py: RateLimitStrategy (abstract)
  • core/sillo/security/ratelimit/strategies/token_bucket.py: TokenBucketStrategy
  • core/sillo/security/ratelimit/strategies/fixed_window.py: FixedWindowStrategy
  • core/sillo/security/ratelimit/strategies/sliding_window.py: SlidingWindowStrategy
  • core/sillo/security/ratelimit/backends/base.py: RateLimitBackend (abstract), RateLimitResult
  • core/sillo/security/ratelimit/backends/memory.py: InMemoryBackend
  • core/sillo/security/ratelimit/backends/redis.py: RedisBackend
  • core/sillo/security/ratelimit/backends/record.py: RecordBackend
  • core/sillo/security/ratelimit/models.py: RateLimitCounter
from sillo import HttpContext
class RateLimitConfig:
def __init__(
self,
limit: int = 60,
window: int = 60,
strategy: str | Any = "token",
backend: str | Any = "memory",
key_func: Callable[[HttpContext], str | None] | None = None,
namespace: str = "sillo_rl",
cost: int = 1,
include_headers: bool = True,
fail_open: bool = True,
on_exceed: str | Callable = "deny",
)
ParameterDefaultPurpose
limit60Maximum requests per window
window60Time window in seconds
strategy"token"Algorithm: "token", "fixed", "sliding", or a strategy instance
backend"memory"Storage: "memory", "redis", "record", or a backend instance
key_funcClient IPFunction to extract rate-limit key from request
namespace"sillo_rl"Prefix for backend keys
cost1Tokens consumed per request
include_headersTrueEmit X-RateLimit-* headers
fail_openTrueAllow requests if backend fails
on_exceed"deny""deny" (returns 429) or a callable

All strategies implement RateLimitStrategy.hit():

class RateLimitStrategy(ABC):
@abstractmethod
async def hit(self, backend, key, limit, window, cost=1, now=None) -> RateLimitResult: ...

File: core/sillo/security/ratelimit/strategies/token_bucket.py

Maintains a bucket of limit tokens refilled at limit / window tokens per second. Each request consumes cost tokens. Allows short bursts up to limit then smoothly throttles.

refill_rate = limit / window # tokens per second
tokens = min(limit, state["tokens"] + elapsed * refill_rate)
if tokens < cost:
# Denied — calculate retry_after
...
tokens -= cost

Characteristics:

  • Smooth rate limiting with burst support
  • Best client experience
  • State: {"tokens": float, "last": float}

File: core/sillo/security/ratelimit/strategies/fixed_window.py

Counts requests within a fixed time window starting at the first hit. Resets completely when the window elapses.

window_start = int(now // window) * window
if state is None or state.get("window_start") != window_start:
state = {"window_start": window_start, "count": 0}

Characteristics:

  • Simplest algorithm
  • Allows bursts at window boundaries (the classic “double count” at the edge)
  • Cheap and predictable
  • State: {"window_start": float, "count": int}

File: core/sillo/security/ratelimit/strategies/sliding_window.py

Tracks individual request timestamps and only counts those within the last window seconds. Eliminates the boundary double-count problem.

cutoff = now - window
hits = [t for t in hits if t > cutoff]
if len(hits) + cost > limit:
# Denied
...

Characteristics:

  • Most accurate: no boundary issues
  • State grows with request volume (pruned each hit)
  • State: {"hits": list[float]}

All backends implement RateLimitBackend:

class RateLimitBackend:
async def fetch_state(self, key: str) -> dict | None: ...
async def save_state(self, key: str, state: dict, ttl: int) -> None: ...
async def clear(self) -> None: ...

File: core/sillo/security/ratelimit/backends/memory.py

Process-local storage using a dict with asyncio.Lock for coroutine safety.

class InMemoryBackend(RateLimitBackend):
def __init__(self):
self._store: dict[str, tuple[dict, float]] = {}
self._lock = asyncio.Lock()
  • Suitable for single-instance deployments and tests
  • State expired lazily by timestamp (no background cleanup)
  • Lost on process restart

File: core/sillo/security/ratelimit/backends/redis.py

Redis-backed shared storage using JSON serialization and a Lua script for atomic read-modify-write.

class RedisBackend(RateLimitBackend):
def __init__(self, url="redis://localhost:6379/0", prefix="sillo:ratelimit:", **kwargs):
import redis.asyncio as aioredis
self._client = aioredis.from_url(url, **kwargs)
self._script = self._client.register_script(_LUA_SET)
  • Recommended for multi-instance deployments
  • Atomic operations via Lua script
  • Requires redis package

File: core/sillo/security/ratelimit/backends/record.py

Stores state in the application database via the RateLimitCounter Tortoise model.

  • Uses sillo.record ORM
  • No external dependencies beyond the database
  • Single-instance-level atomicity

File: core/sillo/security/ratelimit/backends/base.py

@dataclass
class RateLimitResult:
allowed: bool # Whether the ctx is permitted
limit: int # The configured maximum
remaining: int # Requests left in window
reset_at: float # Unix timestamp when window resets
retry_after: int # Seconds to wait before retrying (0 when allowed)

File: core/sillo/security/ratelimit/_middleware.py

class RateLimitMiddleware:
def __init__(self, config=None, **kwargs): ...
async def __call__(self, scope, receive, send) -> None: ...

Plain raw ASGI, not a BaseMiddleware subclass. __call__ calls check before running the downstream app, and wraps send to call set_limit_headers on the http.response.start message:

from sillo import HttpContext
async def check(self, ctx: HttpContext):
key = self.config._key_func(ctx)
if key is None:
return None # No key → skip limiting
full_key = f"{self.config.namespace}:{key}"
try:
return await self._strategy.hit(
self._backend, full_key, self.config.limit,
self.config.window, cost=self.config.cost,
)
except Exception:
if not self.config.fail_open:
raise
return None # Backend failed, fail_open=True
async def __call__(self, scope, receive, send) -> None:
app = self._inner()
if scope["type"] != "http":
await app(scope, receive, send)
return
ctx = HttpContext(scope, receive)
result = await self.check(ctx)
if result is not None and not result.allowed:
response = self._deny(ctx, result) # 429, downstream app never runs
await response(scope, receive, send)
return
async def send_with_limit_headers(message):
if message["type"] == "http.response.start":
self.set_limit_headers(ResponseHeaders(message), result)
await send(message)
await app(scope, receive, send_with_limit_headers)
def set_limit_headers(self, headers, result) -> None:
if result is None or not self.config.include_headers:
return
headers.set_header("X-RateLimit-Limit", str(result.limit), override=True)
headers.set_header("X-RateLimit-Remaining", str(result.remaining), override=True)
headers.set_header("X-RateLimit-Reset", str(int(result.reset_at)), override=True)

The headers go on only when the request was allowed through. A denied request gets its counts from _deny, which builds the 429 with them already set.

Notice result is a local variable threaded through the send_with_limit_headers closure, not stored on self. It used to be (self._last_result = result) in the dispatch-based version — a real bug, since one middleware instance serves every concurrent request the application handles: a second request’s result landing on self between it being set and _set_limit_headers reading it back would have stamped the wrong numbers on the first request’s response. Fixed as part of moving this to raw ASGI, where each request’s result naturally lives in its own __call__ invocation instead.

429 response:

from sillo import HttpContext, json
def _deny(self, ctx: HttpContext, result):
retry_after = max(int(result.retry_after), 1)
return json(
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Slow down and retry later.",
"retry_after": retry_after,
},
status_code=429,
headers={
"X-RateLimit-Limit": str(result.limit),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(int(result.reset_at)),
"Retry-After": str(retry_after),
},
)

File: core/sillo/security/ratelimit/__init__.py

class RateLimit(RateLimitMiddleware):
def __init__(self, limit=60, window=60, strategy="token", backend="memory",
key_func=None, namespace="sillo_rl", cost=1, include_headers=True,
fail_open=True, on_exceed="deny", **kwargs):
config = RateLimitConfig(limit=limit, window=window, ...)
super().__init__(config=config, **kwargs)

Usage:

app.use(RateLimit(limit=100, window=60, backend="redis"))
flowchart TD
    subgraph "Middleware"
        MW[RateLimitMiddleware]
    end

    subgraph "Strategies"
        TB[TokenBucketStrategy]
        FW[FixedWindowStrategy]
        SW[SlidingWindowStrategy]
    end

    subgraph "Backends"
        MEM[InMemoryBackend]
        REDIS[RedisBackend]
        REC[RecordBackend]
    end

    MW --> TB
    MW --> FW
    MW --> SW
    TB --> MEM
    TB --> REDIS
    TB --> REC
    FW --> MEM
    FW --> REDIS
    FW --> REC
    SW --> MEM
    SW --> REDIS
    SW --> REC

from sillo.security import Shield, CORSMiddleware, CSRFMiddleware
from sillo.security.ratelimit import RateLimit
app = SilloApp()
# 1. Shield — adds security headers to every response
app.use(Shield())
# 2. CORS — handles cross-origin requests
app.use(CORSMiddleware(CorsConfig(allow_origins=["https://app.example.com"])))
# 3. CSRF — validates tokens on unsafe methods
app.use(CSRFMiddleware(CSRFConfig(enabled=True, secret_key="...")))
# 4. Rate limiting — protects against abuse
app.use(RateLimit(limit=100, window=60, backend="redis"))

Why this order?

  • Shield runs first to ensure headers are on every response (including error responses)
  • CORS runs before CSRF because preflight requests (OPTIONS) should bypass CSRF
  • Rate limiting runs last to count all requests, including those rejected by CSRF

The security middleware runs independently of authentication. A typical full stack:

app.use(Shield())
app.use(CORSMiddleware(cors_config))
app.use(SessionMiddleware(config=session_config))
app.use(AuthenticationMiddleware(user_model=User, backend=[JWTAuthBackend(...)]))
app.use(RateLimit(limit=100, window=60))

ComponentFileLines
Shieldcore/sillo/security/shield.py18-245
SecurityMiddleware aliascore/sillo/middleware/security.py5
CorsConfigcore/sillo/security/cors/config.py5-125
CORSMiddlewarecore/sillo/security/cors/_middleware.py20-249
CSRFConfigcore/sillo/security/csrf/config.py5-117
CSRFMiddlewarecore/sillo/security/csrf/_middleware.py14-162
RateLimitConfigcore/sillo/security/ratelimit/config.py13-71
RateLimitMiddlewarecore/sillo/security/ratelimit/_middleware.py26-100
RateLimitcore/sillo/security/ratelimit/__init__.py51-87
RateLimitStrategycore/sillo/security/ratelimit/strategies/base.py19-64
TokenBucketStrategycore/sillo/security/ratelimit/strategies/token_bucket.py19-57
FixedWindowStrategycore/sillo/security/ratelimit/strategies/fixed_window.py18-56
SlidingWindowStrategycore/sillo/security/ratelimit/strategies/sliding_window.py19-55
RateLimitResultcore/sillo/security/ratelimit/backends/base.py16-33
RateLimitBackendcore/sillo/security/ratelimit/backends/base.py36-49
InMemoryBackendcore/sillo/security/ratelimit/backends/memory.py17-45
RedisBackendcore/sillo/security/ratelimit/backends/redis.py27-71
RecordBackendcore/sillo/security/ratelimit/backends/record.py16-29
RateLimitCounter modelcore/sillo/security/ratelimit/models.py19