Repository: sparckles/Robyn
Branch: main
Commit: 4c3be944961c
Files: 346
Total size: 1.1 MB
Directory structure:
gitextract_zt3t3gjh/
├── .cargo/
│ └── config
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── blank_issue.md
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.md
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── codspeed.yml
│ ├── lint-pr.yml
│ ├── preview-deployments.yml
│ ├── python-CI.yml
│ ├── release-CI.yml
│ └── rust-CI.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .well-known/
│ └── funding-manifest-urls
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── benchmark.sh
├── ci-local.sh
├── docs_src/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── jsconfig.json
│ ├── mdx/
│ │ ├── recma.mjs
│ │ ├── rehype.mjs
│ │ └── remark.mjs
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── prettier.config.js
│ ├── public/
│ │ ├── funding.json
│ │ └── llms.txt
│ ├── src/
│ │ ├── components/
│ │ │ ├── Button.jsx
│ │ │ ├── Card.jsx
│ │ │ ├── Container.jsx
│ │ │ ├── Footer.jsx
│ │ │ ├── Header.jsx
│ │ │ ├── Prose.jsx
│ │ │ ├── Section.jsx
│ │ │ ├── SimpleLayout.jsx
│ │ │ ├── SocialIcons.jsx
│ │ │ ├── Testimonials.jsx
│ │ │ ├── documentation/
│ │ │ │ ├── ApiDocs.jsx
│ │ │ │ ├── BottomNavbar.jsx
│ │ │ │ ├── Button.jsx
│ │ │ │ ├── Code.jsx
│ │ │ │ ├── Guides.jsx
│ │ │ │ ├── Heading.jsx
│ │ │ │ ├── HeroPattern.jsx
│ │ │ │ ├── LanguageSelector.jsx
│ │ │ │ ├── Layout.jsx
│ │ │ │ ├── Libraries.jsx
│ │ │ │ ├── MobileNavigation.jsx
│ │ │ │ ├── ModeToggle.jsx
│ │ │ │ ├── Navigation.jsx
│ │ │ │ ├── Prose.jsx
│ │ │ │ ├── Search.jsx
│ │ │ │ ├── SectionProvider.jsx
│ │ │ │ ├── Tag.jsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── BellIcon.jsx
│ │ │ │ │ ├── BoltIcon.jsx
│ │ │ │ │ ├── BookIcon.jsx
│ │ │ │ │ ├── CalendarIcon.jsx
│ │ │ │ │ ├── CartIcon.jsx
│ │ │ │ │ ├── ChatBubbleIcon.jsx
│ │ │ │ │ ├── CheckIcon.jsx
│ │ │ │ │ ├── ChevronRightLeftIcon.jsx
│ │ │ │ │ ├── ClipboardIcon.jsx
│ │ │ │ │ ├── CogIcon.jsx
│ │ │ │ │ ├── CopyIcon.jsx
│ │ │ │ │ ├── DocumentIcon.jsx
│ │ │ │ │ ├── EnvelopeIcon.jsx
│ │ │ │ │ ├── FaceSmileIcon.jsx
│ │ │ │ │ ├── FolderIcon.jsx
│ │ │ │ │ ├── LinkIcon.jsx
│ │ │ │ │ ├── ListIcon.jsx
│ │ │ │ │ ├── MagnifyingGlassIcon.jsx
│ │ │ │ │ ├── MapPinIcon.jsx
│ │ │ │ │ ├── PackageIcon.jsx
│ │ │ │ │ ├── PaperAirplaneIcon.jsx
│ │ │ │ │ ├── PaperClipIcon.jsx
│ │ │ │ │ ├── ShapesIcon.jsx
│ │ │ │ │ ├── ShirtIcon.jsx
│ │ │ │ │ ├── SquaresPlusIcon.jsx
│ │ │ │ │ ├── TagIcon.jsx
│ │ │ │ │ ├── UserIcon.jsx
│ │ │ │ │ └── UsersIcon.jsx
│ │ │ │ └── mdx.jsx
│ │ │ └── releases/
│ │ │ ├── Button.jsx
│ │ │ ├── FeedProvider.jsx
│ │ │ ├── FormattedDate.jsx
│ │ │ ├── IconLink.jsx
│ │ │ ├── Intro.jsx
│ │ │ ├── Layout.jsx
│ │ │ ├── SignUpForm.jsx
│ │ │ └── mdx.jsx
│ │ ├── lib/
│ │ │ ├── formatDate.js
│ │ │ ├── getAllArticles.js
│ │ │ └── remToPx.js
│ │ ├── pages/
│ │ │ ├── _app.jsx
│ │ │ ├── _document.jsx
│ │ │ ├── community.jsx
│ │ │ ├── documentation/
│ │ │ │ ├── en/
│ │ │ │ │ ├── api_reference/
│ │ │ │ │ │ ├── advanced_features.mdx
│ │ │ │ │ │ ├── advanced_routing.mdx
│ │ │ │ │ │ ├── agents.mdx
│ │ │ │ │ │ ├── ai.mdx
│ │ │ │ │ │ ├── architecture_deep_dive.mdx
│ │ │ │ │ │ ├── authentication.mdx
│ │ │ │ │ │ ├── const_requests.mdx
│ │ │ │ │ │ ├── cors.mdx
│ │ │ │ │ │ ├── dependency_injection.mdx
│ │ │ │ │ │ ├── exceptions.mdx
│ │ │ │ │ │ ├── file-uploads.mdx
│ │ │ │ │ │ ├── form_data.mdx
│ │ │ │ │ │ ├── future-roadmap.mdx
│ │ │ │ │ │ ├── getting_started.mdx
│ │ │ │ │ │ ├── graphql-support.mdx
│ │ │ │ │ │ ├── index.mdx
│ │ │ │ │ │ ├── mcps.mdx
│ │ │ │ │ │ ├── middlewares.mdx
│ │ │ │ │ │ ├── multiprocess_execution.mdx
│ │ │ │ │ │ ├── openapi.mdx
│ │ │ │ │ │ ├── pydantic.mdx
│ │ │ │ │ │ ├── redirection.mdx
│ │ │ │ │ │ ├── request_object.mdx
│ │ │ │ │ │ ├── robyn_env.mdx
│ │ │ │ │ │ ├── scaling.mdx
│ │ │ │ │ │ ├── server_sent_events.mdx
│ │ │ │ │ │ ├── templating.mdx
│ │ │ │ │ │ ├── timeout_configuration.mdx
│ │ │ │ │ │ ├── using_rust_directly.mdx
│ │ │ │ │ │ ├── websockets.mdx
│ │ │ │ │ │ └── zh/
│ │ │ │ │ │ └── getting_started.mdx
│ │ │ │ │ ├── architecture.mdx
│ │ │ │ │ ├── community-resources.mdx
│ │ │ │ │ ├── example_app/
│ │ │ │ │ │ ├── authentication-middlewares.mdx
│ │ │ │ │ │ ├── authentication.mdx
│ │ │ │ │ │ ├── deployment.mdx
│ │ │ │ │ │ ├── index.mdx
│ │ │ │ │ │ ├── modeling_routes.mdx
│ │ │ │ │ │ ├── monitoring_and_logging.mdx
│ │ │ │ │ │ ├── openapi.mdx
│ │ │ │ │ │ ├── real_time_notifications.mdx
│ │ │ │ │ │ ├── subrouters.mdx
│ │ │ │ │ │ ├── templates.mdx
│ │ │ │ │ │ └── zh/
│ │ │ │ │ │ ├── index.mdx
│ │ │ │ │ │ └── subrouters.mdx
│ │ │ │ │ ├── framework_performance_comparison.mdx
│ │ │ │ │ ├── hosting.mdx
│ │ │ │ │ ├── index.mdx
│ │ │ │ │ └── plugins.mdx
│ │ │ │ └── zh/
│ │ │ │ ├── api_reference/
│ │ │ │ │ ├── advanced_features.mdx
│ │ │ │ │ ├── authentication.mdx
│ │ │ │ │ ├── const_requests.mdx
│ │ │ │ │ ├── cors.mdx
│ │ │ │ │ ├── dependency_injection.mdx
│ │ │ │ │ ├── exceptions.mdx
│ │ │ │ │ ├── file-uploads.mdx
│ │ │ │ │ ├── form_data.mdx
│ │ │ │ │ ├── future-roadmap.mdx
│ │ │ │ │ ├── getting_started.mdx
│ │ │ │ │ ├── graphql-support.mdx
│ │ │ │ │ ├── index.mdx
│ │ │ │ │ ├── middlewares.mdx
│ │ │ │ │ ├── multiprocess_execution.mdx
│ │ │ │ │ ├── openapi.mdx
│ │ │ │ │ ├── pydantic.mdx
│ │ │ │ │ ├── redirection.mdx
│ │ │ │ │ ├── request_object.mdx
│ │ │ │ │ ├── robyn_env.mdx
│ │ │ │ │ ├── scaling.mdx
│ │ │ │ │ ├── server_sent_events.mdx
│ │ │ │ │ ├── templating.mdx
│ │ │ │ │ ├── timeout_configuration.mdx
│ │ │ │ │ ├── using_rust_directly.mdx
│ │ │ │ │ ├── views.mdx
│ │ │ │ │ └── websockets.mdx
│ │ │ │ ├── architecture.mdx
│ │ │ │ ├── community-resources.mdx
│ │ │ │ ├── example_app/
│ │ │ │ │ ├── authentication-middlewares.mdx
│ │ │ │ │ ├── authentication.mdx
│ │ │ │ │ ├── deployment.mdx
│ │ │ │ │ ├── index.mdx
│ │ │ │ │ ├── modeling_routes.mdx
│ │ │ │ │ ├── monitoring_and_logging.mdx
│ │ │ │ │ ├── openapi.mdx
│ │ │ │ │ ├── real_time_notifications.mdx
│ │ │ │ │ ├── subrouters_and_views.mdx
│ │ │ │ │ └── templates.mdx
│ │ │ │ ├── framework_performance_comparison.mdx
│ │ │ │ ├── hosting.mdx
│ │ │ │ ├── index.mdx
│ │ │ │ └── plugins.mdx
│ │ │ ├── index.jsx
│ │ │ └── releases/
│ │ │ └── index.jsx
│ │ └── styles/
│ │ ├── documentation.css
│ │ ├── prism.css
│ │ ├── releases/
│ │ │ ├── base.css
│ │ │ ├── components.css
│ │ │ ├── tailwind.css
│ │ │ ├── typography.css
│ │ │ └── utilities.css
│ │ └── tailwind.css
│ └── tailwind.config.js
├── examples/
│ ├── agents.py
│ ├── mcp.py
│ └── sse_example.py
├── integration_tests/
│ ├── __init__.py
│ ├── base_routes.py
│ ├── build/
│ │ └── index.html
│ ├── conftest.py
│ ├── downloads/
│ │ └── test.txt
│ ├── helpers/
│ │ ├── __init__.py
│ │ ├── http_methods_helpers.py
│ │ └── network_helpers.py
│ ├── index.html
│ ├── index.py
│ ├── openapi_config.json
│ ├── random_number.rs
│ ├── subroutes/
│ │ ├── __init__.py
│ │ ├── di_subrouter.py
│ │ └── file_api.py
│ ├── templates/
│ │ └── test.html
│ ├── test_add_route_without_decorator.py
│ ├── test_app.py
│ ├── test_authentication.py
│ ├── test_base_url.py
│ ├── test_basic_routes.py
│ ├── test_binary_output.py
│ ├── test_delete_requests.py
│ ├── test_dependency_injection.py
│ ├── test_easy_access_params.py
│ ├── test_exception_handling.py
│ ├── test_file_download.py
│ ├── test_get_requests.py
│ ├── test_json_types.py
│ ├── test_middlewares.py
│ ├── test_multipart_data.py
│ ├── test_openapi.py
│ ├── test_patch_requests.py
│ ├── test_post_requests.py
│ ├── test_put_requests.py
│ ├── test_pydantic.py
│ ├── test_request_json.py
│ ├── test_split_request_params.py
│ ├── test_sse.py
│ ├── test_static_files_with_api_routes.py
│ ├── test_status_code.py
│ ├── test_subrouter.py
│ └── test_web_sockets.py
├── llms.txt
├── noxfile.py
├── pyproject.toml
├── robyn/
│ ├── __init__.py
│ ├── __main__.py
│ ├── _param_utils.py
│ ├── ai.py
│ ├── argument_parser.py
│ ├── authentication.py
│ ├── cli.py
│ ├── dependency_injection.py
│ ├── env_populator.py
│ ├── events.py
│ ├── exceptions.py
│ ├── jsonify.py
│ ├── logger.py
│ ├── mcp.py
│ ├── openapi.py
│ ├── processpool.py
│ ├── py.typed
│ ├── pydantic_support.py
│ ├── reloader.py
│ ├── responses.py
│ ├── robyn.pyi
│ ├── router.py
│ ├── scaffold/
│ │ ├── mongo/
│ │ │ ├── Dockerfile
│ │ │ ├── app.py
│ │ │ └── requirements.txt
│ │ ├── no-db/
│ │ │ ├── Dockerfile
│ │ │ ├── app.py
│ │ │ └── requirements.txt
│ │ ├── postgres/
│ │ │ ├── Dockerfile
│ │ │ ├── app.py
│ │ │ ├── requirements.txt
│ │ │ └── supervisord.conf
│ │ ├── prisma/
│ │ │ ├── Dockerfile
│ │ │ ├── app.py
│ │ │ ├── requirements.txt
│ │ │ └── schema.prisma
│ │ ├── sqlalchemy/
│ │ │ ├── Dockerfile
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ ├── models.py
│ │ │ └── requirements.txt
│ │ ├── sqlite/
│ │ │ ├── Dockerfile
│ │ │ ├── app.py
│ │ │ └── requirements.txt
│ │ └── sqlmodel/
│ │ ├── Dockerfile
│ │ ├── app.py
│ │ ├── models.py
│ │ └── requirements.txt
│ ├── status_codes.py
│ ├── swagger.html
│ ├── templating.py
│ ├── types.py
│ └── ws.py
├── scripts/
│ ├── format.sh
│ └── release.sh
├── setup.py
├── src/
│ ├── asyncio.rs
│ ├── blocking.rs
│ ├── callbacks.rs
│ ├── conversion.rs
│ ├── executors/
│ │ ├── mod.rs
│ │ └── web_socket_executors.rs
│ ├── io_helpers/
│ │ └── mod.rs
│ ├── lib.rs
│ ├── routers/
│ │ ├── const_router.rs
│ │ ├── http_router.rs
│ │ ├── middleware_router.rs
│ │ ├── mod.rs
│ │ └── web_socket_router.rs
│ ├── runtime.rs
│ ├── server.rs
│ ├── shared_socket.rs
│ ├── types/
│ │ ├── cookie.rs
│ │ ├── function_info.rs
│ │ ├── headers.rs
│ │ ├── identity.rs
│ │ ├── mod.rs
│ │ ├── multimap.rs
│ │ ├── request.rs
│ │ └── response.rs
│ └── websockets/
│ ├── mod.rs
│ └── registry.rs
└── unit_tests/
├── test_cli.py
├── test_env_populator.py
├── test_openapi_issue_1270.py
├── test_request_object.py
└── test_unsupported_types.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .cargo/config
================================================
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [sansyrox] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
open_collective: robyn_oss # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: # Replace with a single Liberapay username
# issuehunt: # Replace with a single IssueHunt username
# otechie: # Replace with a single Otechie username
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/blank_issue.md
================================================
---
name: 📝 Blank Issue
about: Create a blank issue.
---
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug Report
description: Create a bug report
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report!
Please fill out the form below...
- type: textarea
id: description
attributes:
label: Bug Description
description: Please provide a clear and concise description of what the bug is, and what you would expect to happen.
placeholder: The bug is...
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: Please provide the steps to reproduce this bug. You can include your code here if it is relevant.
placeholder: |
1.
2.
3.
validations:
required: false
- type: dropdown
id: os
attributes:
label: Your operating system
options:
- Windows
- MacOS
- Linux
- Other (specify below)
validations:
required: false
- type: dropdown
id: py_version
attributes:
label: Your Python version (`python --version`)
options:
- 3.9
- 3.10
- 3.11
- 3.12
- 3.13
- Other (specify below)
validations:
required: false
- type: dropdown
id: robyn_version
attributes:
label: Your Robyn version
options:
- main branch
- latest
- Other (specify below)
validations:
required: false
- type: textarea
id: additional-info
attributes:
label: Additional Info
description: Any additional info that you think might be useful or relevant to this bug
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Discord Community Support
url: https://discord.gg/rkERZ5eNU8
about: You can ask and answer questions here.
- name: Documentation
url: https://robyn.tech
about: The Robyn documentation.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: 💡 Feature request
about: Suggest an idea to improve Robyn
labels: [enhancement]
---
================================================
FILE: .github/dependabot.yml
================================================
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
================================================
FILE: .github/pull_request_template.md
================================================
## Description
This PR fixes #
## Summary
This PR does....
## PR Checklist
Please ensure that:
- [ ] The PR contains a descriptive title
- [ ] The PR contains a descriptive summary of the changes
- [ ] You build and test your changes before submitting a PR.
- [ ] You have added relevant documentation
- [ ] You have added relevant tests. We prefer integration tests wherever possible
## Pre-Commit Instructions:
- [ ] Ensure that you have run the [pre-commit hooks](https://github.com/sparckles/robyn#%EF%B8%8F-to-develop-locally) on your PR.
================================================
FILE: .github/workflows/codspeed.yml
================================================
name: codspeed-benchmarks
on:
push:
branches:
- "main" # or "master"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
jobs:
benchmarks:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.7.5"
- name: Install the project + deps.
run: |
echo "# Syncing Project + Installing project."
uv sync --dev --group test --verbose
- name: Run benchmarks
uses: CodSpeedHQ/action@v3.5.0
with:
token: ${{ secrets.CODSPEED_TOKEN }}
run: uv run pytest integration_tests --codspeed
================================================
FILE: .github/workflows/lint-pr.yml
================================================
name: Lint PR
on:
pull_request:
branches: [main]
env:
UV_SYSTEM_PYTHON: 1
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install dependencies with uv
run: |
uv pip install ruff isort black
- name: Run linters
run: |
uv run ruff check .
uv run isort --check-only --diff .
================================================
FILE: .github/workflows/preview-deployments.yml
================================================
# CI to release the project for Linux, Windows, and MacOS
# The purpose of this action is to verify if the release builds are working or not.
name: Preview Release
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
UV_SYSTEM_PYTHON: 1
jobs:
macos:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Build wheels - x86_64
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: -i python --release --out dist
- name: Build wheels - universal2
uses: PyO3/maturin-action@v1
with:
target: universal2-apple-darwin
args: -i python --release --out dist
- name: Install build wheel - universal2
run: |
uv pip install --force-reinstall dist/robyn*_universal2.whl
cd ~ && python -c 'import robyn'
windows:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.target }}
- uses: dtolnay/rust-toolchain@stable
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: -i python --release --out dist
- name: Install build wheel
shell: bash
run: |
uv pip install --force-reinstall dist/robyn*.whl
cd ~ && python -c 'import robyn'
linux:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
target: [x86_64, i686]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Build Wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i python${{ matrix.python-version }} --release --out dist
- name: Install build wheel
if: matrix.target == 'x86_64'
run: |
uv pip install --force-reinstall dist/robyn*.whl
cd ~ && python -c 'import robyn'
linux-cross:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python:
[
{ version: "3.10", abi: "cp310-cp310" },
{ version: "3.11", abi: "cp311-cp311" },
{ version: "3.12", abi: "cp312-cp312" },
{ version: "3.13", abi: "cp313-cp313" },
{ version: "3.14", abi: "cp314-cp314" },
]
target: [aarch64, armv7]
steps:
- uses: actions/checkout@v3
- name: Build Wheels
uses: PyO3/maturin-action@v1
env:
PYO3_CROSS_LIB_DIR: /opt/python/${{ matrix.python.abi }}/lib
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i python${{matrix.python.version}} --release --out dist
- uses: uraimo/run-on-arch-action@v2
name: Install build wheel
with:
arch: ${{ matrix.target }}
distro: ubuntu22.04
githubToken: ${{ github.token }}
# Mount the dist directory as /artifacts in the container
dockerRunArgs: |
--volume "${PWD}/dist:/artifacts"
install: |
apt update -y
apt install -y software-properties-common
add-apt-repository -y ppa:deadsnakes/ppa
apt update -y
apt install -y gcc musl-dev python3-dev python${{ matrix.python.version }} python${{ matrix.python.version }}-venv
run: |
ls -lrth /artifacts
python${{ matrix.python.version }} -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip setuptools wheel
python -m pip install --force-reinstall /artifacts/robyn*.whl
cd ~ && python -c 'import robyn'
================================================
FILE: .github/workflows/python-CI.yml
================================================
# CI to test Robyn on major Linux, MacOS and Windows
on: [push, pull_request]
name: Python Continuous integration
jobs:
tests:
strategy:
fail-fast: false
matrix:
os: ["windows", "ubuntu", "macos"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
name: ${{ matrix.os }} tests with python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Set up Nox
uses: wntrblm/nox@2024.03.02
with:
python-versions: ${{ matrix.python-version }}
- name: Test with Nox
run: nox --non-interactive --error-on-missing-interpreter -p ${{ matrix.python-version }}
================================================
FILE: .github/workflows/release-CI.yml
================================================
# CI to release the project for Linux, Windows, and MacOS
name: Release CI
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
enable_linux_cross:
description: 'Enable Linux cross-compilation (ARM64/ARMv7)'
required: false
default: true
type: boolean
env:
UV_SYSTEM_PYTHON: 1
jobs:
macos:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Build wheels - x86_64
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: -i python --release --out dist
- name: Build wheels - universal2
uses: PyO3/maturin-action@v1
with:
target: universal2-apple-darwin
args: -i python --release --out dist
- name: Install build wheel - universal2
run: |
uv pip install --force-reinstall dist/robyn*_universal2.whl
cd ~ && python -c 'import robyn'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ github.job }}-universal-${{ matrix.python-version }}
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.target }}
- uses: dtolnay/rust-toolchain@stable
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: -i python --release --out dist
- name: Install build wheel
shell: bash
run: |
uv pip install --force-reinstall dist/robyn*.whl
cd ~ && python -c 'import robyn'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ github.job }}-${{ matrix.target }}-${{ matrix.python-version }}
path: dist
linux:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
target: [x86_64, i686]
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Build Wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: -i python${{ matrix.python-version }} --release --out dist
- name: Install build wheel
if: matrix.target == 'x86_64'
run: |
uv pip install --force-reinstall dist/robyn*.whl
cd ~ && python -c 'import robyn'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ github.job }}-${{ matrix.target }}-${{ matrix.python-version }}
path: dist
linux-cross:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.enable_linux_cross)
strategy:
matrix:
python:
[
{ version: "3.10", abi: "cp310-cp310" },
{ version: "3.11", abi: "cp311-cp311" },
{ version: "3.12", abi: "cp312-cp312" },
{ version: "3.13", abi: "cp313-cp313" },
]
target: [aarch64, armv7]
steps:
- uses: actions/checkout@v3
- name: Build Wheels
uses: PyO3/maturin-action@v1
env:
PYO3_CROSS_LIB_DIR: /opt/python/${{ matrix.python.abi }}/lib
with:
target: ${{ matrix.target }}
manylinux: auto
maturin-version: "v1.12.0"
args: -i python${{matrix.python.version}} --release --out dist
- uses: uraimo/run-on-arch-action@v2
name: Install build wheel
with:
arch: ${{ matrix.target }}
distro: ubuntu22.04
githubToken: ${{ github.token }}
# Mount the dist directory as /artifacts in the container
dockerRunArgs: |
--volume "${PWD}/dist:/artifacts"
install: |
apt update -y
apt install -y software-properties-common
add-apt-repository -y ppa:deadsnakes/ppa
apt update -y
apt install -y gcc musl-dev python3-dev python${{ matrix.python.version }} python${{ matrix.python.version }}-venv
run: |
ls -lrth /artifacts
python${{ matrix.python.version }} -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip setuptools wheel
python -m pip install --force-reinstall /artifacts/robyn*.whl
cd ~ && python -c 'import robyn'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ github.job }}-${{ matrix.target }}-${{ matrix.python.version }}-${{ matrix.python.abi }}
path: dist
merge:
name: Building Single Artifact
runs-on: ubuntu-latest
needs: [macos, windows, linux, linux-cross]
if: always() && needs.macos.result == 'success' && needs.windows.result == 'success' && needs.linux.result == 'success' && (needs.linux-cross.result == 'success' || needs.linux-cross.result == 'skipped')
steps:
- name: Downloading all Artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: wheels-*
merge-multiple: true
- run: |
echo "Listing directories"
ls -R
- name: Uploading Artifact's Bundle
uses: actions/upload-artifact@v4
with:
name: wheels
path: artifacts
release:
name: Release
runs-on: ubuntu-latest
needs: [macos, windows, linux, linux-cross, merge]
if: always() && needs.merge.result == 'success'
steps:
- uses: actions/download-artifact@v4
with:
name: wheels
- name: Install uv
uses: astral-sh/setup-uv@v3
- uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
uv pip install --upgrade twine
twine upload --skip-existing *.whl
================================================
FILE: .github/workflows/rust-CI.yml
================================================
# CI to build, test, format, and lint the Rust code
on: [push, pull_request]
name: Rust Continuous integration
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo test
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- run: cargo clippy
# -- -D warnings
================================================
FILE: .gitignore
================================================
.python-version
/target
# ignore pre compiled binaries
*.so
dist/
#python cache
*__pycache__
*tags
# DS_Store
*.DS_Store
.vscode
# pycharm
.idea
# python venv
.venv
venv
robyn.code-workspace
hello_robyn.py
robyn.env
# nox
.nox/
# new docs dependencies
# dependencies
docs_src/node_modules
docs_src/.pnp
docs_src/.pnp.js
# testing
docs_src/coverage
# next.js
docs_src/.next/
docs_src/out/
# production
docs_src/build
# misc
docs_src/.DS_Store
docs_src/*.pem
# debug
docs_src/npm-debug.log*
docs_src/yarn-debug.log*
docs_src/yarn-error.log*
docs_src/.pnpm-debug.log*
# local env files
docs_src/.env*.local
# vercel
docs_src/.vercel
# generated files
docs_src/public/rss/
#https://github.com/PyO3/maturin/issues/2141
*.pyd
================================================
FILE: .pre-commit-config.yaml
================================================
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.13
hooks:
- id: ruff
args:
- --fix
- id: ruff-format
ci:
autoupdate_schedule: weekly
================================================
FILE: .well-known/funding-manifest-urls
================================================
https://robyn.tech/funding.json
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [Unreleased](https://github.com/sparckles/robyn/tree/HEAD)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.26.1...HEAD)
**Implemented enhancements:**
- Allow empty returns on websocket handling [\#1263](https://github.com/sparckles/robyn/issues/1263)
**Closed issues:**
- Payload reached size limit. [\#463](https://github.com/sparckles/robyn/issues/463)
- Proposal to rename `params` with `path_params` [\#457](https://github.com/sparckles/robyn/issues/457)
**Merged pull requests:**
- feat: allow configurable payload sizes [\#465](https://github.com/sparckles/robyn/pull/465) ([sansyrox](https://github.com/sansyrox))
- docs: remove test pypi instructions from pr template [\#462](https://github.com/sparckles/robyn/pull/462) ([sansyrox](https://github.com/sansyrox))
- Rename params with path\_params [\#460](https://github.com/sparckles/robyn/pull/460) ([carlosm27](https://github.com/carlosm27))
- feat: Implement global CORS [\#458](https://github.com/sparckles/robyn/pull/458) ([sansyrox](https://github.com/sansyrox))
## [v0.26.1](https://github.com/sparckles/robyn/tree/v0.26.1) (2023-04-05)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.26.0...v0.26.1)
**Fixed bugs:**
- Can't access new or updated route while on dev option [\#439](https://github.com/sparckles/robyn/issues/439)
**Closed issues:**
- Add documentation for `robyn.env` file [\#454](https://github.com/sparckles/robyn/issues/454)
**Merged pull requests:**
- Release v0.26.1 [\#461](https://github.com/sparckles/robyn/pull/461) ([sansyrox](https://github.com/sansyrox))
- \[pre-commit.ci\] pre-commit autoupdate [\#459](https://github.com/sparckles/robyn/pull/459) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- \[pre-commit.ci\] pre-commit autoupdate [\#452](https://github.com/sparckles/robyn/pull/452) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- docs: Add docs for v0.26.0 [\#451](https://github.com/sparckles/robyn/pull/451) ([sansyrox](https://github.com/sansyrox))
- fix\(dev\): fix hot reloading with dev flag [\#446](https://github.com/sparckles/robyn/pull/446) ([AntoineRR](https://github.com/AntoineRR))
## [v0.26.0](https://github.com/sparckles/robyn/tree/v0.26.0) (2023-03-24)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.25.0...v0.26.0)
**Implemented enhancements:**
- \[Feature Request\] Robyn providing Status Codes? [\#423](https://github.com/sparckles/robyn/issues/423)
- \[Feature Request\] Allow global level Response headers [\#335](https://github.com/sparckles/robyn/issues/335)
**Fixed bugs:**
- \[BUG\] `uvloop` ModuleNotFoundError: No module named 'uvloop' on Ubuntu Docker Image [\#395](https://github.com/sparckles/robyn/issues/395)
**Closed issues:**
- \[Feature Request\] When Robyn can have a middleware mechanism like flask or django [\#350](https://github.com/sparckles/robyn/issues/350)
- Forced shutdown locks console. \[BUG\] [\#317](https://github.com/sparckles/robyn/issues/317)
**Merged pull requests:**
- \[pre-commit.ci\] pre-commit autoupdate [\#449](https://github.com/sparckles/robyn/pull/449) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- fix: Implement auto installation of uvloop on linux arm [\#445](https://github.com/sparckles/robyn/pull/445) ([sansyrox](https://github.com/sansyrox))
- chore: update rust dependencies [\#444](https://github.com/sparckles/robyn/pull/444) ([AntoineRR](https://github.com/AntoineRR))
- feat: Implement performance benchmarking [\#443](https://github.com/sparckles/robyn/pull/443) ([sansyrox](https://github.com/sansyrox))
- feat: expose request/connection info [\#441](https://github.com/sparckles/robyn/pull/441) ([r3b-fish](https://github.com/r3b-fish))
- Install the CodeSee workflow. [\#438](https://github.com/sparckles/robyn/pull/438) ([codesee-maps[bot]](https://github.com/apps/codesee-maps))
- \[pre-commit.ci\] pre-commit autoupdate [\#437](https://github.com/sparckles/robyn/pull/437) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- Replace integer status codes with Enum values of StatusCodes [\#436](https://github.com/sparckles/robyn/pull/436) ([Noborita9](https://github.com/Noborita9))
- added `star-history` [\#434](https://github.com/sparckles/robyn/pull/434) ([hemangjoshi37a](https://github.com/hemangjoshi37a))
- \[pre-commit.ci\] pre-commit autoupdate [\#433](https://github.com/sparckles/robyn/pull/433) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- feat: Robyn providing status codes [\#429](https://github.com/sparckles/robyn/pull/429) ([carlosm27](https://github.com/carlosm27))
- feat: Allow global level Response headers [\#410](https://github.com/sparckles/robyn/pull/410) ([ParthS007](https://github.com/ParthS007))
- feat: get rid of intermediate representations of requests and responses [\#397](https://github.com/sparckles/robyn/pull/397) ([AntoineRR](https://github.com/AntoineRR))
## [v0.25.0](https://github.com/sparckles/robyn/tree/v0.25.0) (2023-02-20)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.24.1...v0.25.0)
**Implemented enhancements:**
- using robyn with some frameworks [\#420](https://github.com/sparckles/robyn/issues/420)
**Fixed bugs:**
- Template Rendering is not working in some browsers [\#426](https://github.com/sparckles/robyn/issues/426)
**Closed issues:**
- \[Feature Request\] Show support for Python versions in the README [\#396](https://github.com/sparckles/robyn/issues/396)
- \[BUG\] The dev flag doesn't set the log level to DEBUG [\#385](https://github.com/sparckles/robyn/issues/385)
- \[BUG\] All tests are not passing on windows [\#372](https://github.com/sparckles/robyn/issues/372)
- \[Feature Request\] Add views/view controllers [\#221](https://github.com/sparckles/robyn/issues/221)
**Merged pull requests:**
- fix: Add proper headers to the templates return types [\#427](https://github.com/sparckles/robyn/pull/427) ([sansyrox](https://github.com/sansyrox))
- \[pre-commit.ci\] pre-commit autoupdate [\#425](https://github.com/sparckles/robyn/pull/425) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- docs: Add documentation for views [\#424](https://github.com/sparckles/robyn/pull/424) ([sansyrox](https://github.com/sansyrox))
- better way to compare type [\#421](https://github.com/sparckles/robyn/pull/421) ([jmishra01](https://github.com/jmishra01))
- style\(landing\_page\): fix the style of github logo on the landing page [\#419](https://github.com/sparckles/robyn/pull/419) ([sansyrox](https://github.com/sansyrox))
- docs: improve readme [\#418](https://github.com/sparckles/robyn/pull/418) ([AntoineRR](https://github.com/AntoineRR))
- docs: add dark mode to website [\#416](https://github.com/sparckles/robyn/pull/416) ([AntoineRR](https://github.com/AntoineRR))
- chore: improve issue templates [\#413](https://github.com/sparckles/robyn/pull/413) ([AntoineRR](https://github.com/AntoineRR))
- \[pre-commit.ci\] pre-commit autoupdate [\#412](https://github.com/sparckles/robyn/pull/412) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- fix: fixed CONTRIBUTE.md link into docs/README.md file, changing it f… [\#411](https://github.com/sparckles/robyn/pull/411) ([Kop3sh](https://github.com/Kop3sh))
- chore\(ci\): fix rust ci warnings [\#408](https://github.com/sparckles/robyn/pull/408) ([AntoineRR](https://github.com/AntoineRR))
- feat: Add view controllers [\#407](https://github.com/sparckles/robyn/pull/407) ([mikaeelghr](https://github.com/mikaeelghr))
- Fix docs: support version [\#404](https://github.com/sparckles/robyn/pull/404) ([Oluwaseun241](https://github.com/Oluwaseun241))
- fix: Fix Windows tests [\#402](https://github.com/sparckles/robyn/pull/402) ([sansyrox](https://github.com/sansyrox))
- docs: Update PyPi metadata [\#401](https://github.com/sparckles/robyn/pull/401) ([sansyrox](https://github.com/sansyrox))
- fix\(test\): fix tests on windows [\#400](https://github.com/sparckles/robyn/pull/400) ([AntoineRR](https://github.com/AntoineRR))
- fix: various improvements around the dev flag [\#388](https://github.com/sparckles/robyn/pull/388) ([AntoineRR](https://github.com/AntoineRR))
## [v0.24.1](https://github.com/sparckles/robyn/tree/v0.24.1) (2023-02-09)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.24.0...v0.24.1)
**Closed issues:**
- \[BUG\] \[Windows\] Terminal hanging after Ctrl+C is pressed on Robyn server [\#373](https://github.com/sparckles/robyn/issues/373)
**Merged pull requests:**
- \[pre-commit.ci\] pre-commit autoupdate [\#394](https://github.com/sparckles/robyn/pull/394) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- docs: add documentation regarding byte response [\#392](https://github.com/sparckles/robyn/pull/392) ([sansyrox](https://github.com/sansyrox))
- fix: fix terminal hijacking in windows [\#391](https://github.com/sparckles/robyn/pull/391) ([sansyrox](https://github.com/sansyrox))
- chore: fix requirements files and update packages [\#389](https://github.com/sparckles/robyn/pull/389) ([AntoineRR](https://github.com/AntoineRR))
- small correction in docs [\#387](https://github.com/sparckles/robyn/pull/387) ([tkanhe](https://github.com/tkanhe))
- \[pre-commit.ci\] pre-commit autoupdate [\#384](https://github.com/sparckles/robyn/pull/384) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- ci: build artifacts on every push and pull [\#378](https://github.com/sparckles/robyn/pull/378) ([sansyrox](https://github.com/sansyrox))
- test: organize and add tests [\#377](https://github.com/sparckles/robyn/pull/377) ([AntoineRR](https://github.com/AntoineRR))
- Changed Response to use body: bytes [\#375](https://github.com/sparckles/robyn/pull/375) ([madhavajay](https://github.com/madhavajay))
## [v0.24.0](https://github.com/sparckles/robyn/tree/v0.24.0) (2023-02-06)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.23.1...v0.24.0)
**Closed issues:**
- \[BUG\] Release builds are not working [\#386](https://github.com/sparckles/robyn/issues/386)
- \[BUG\] Can't send raw bytes [\#374](https://github.com/sparckles/robyn/issues/374)
## [v0.23.1](https://github.com/sparckles/robyn/tree/v0.23.1) (2023-01-30)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.23.0...v0.23.1)
**Closed issues:**
- \[BUG\] Return 500 status code when route is raising [\#381](https://github.com/sparckles/robyn/issues/381)
- \[BUG\] Return 404 status code when route isn't set [\#376](https://github.com/sparckles/robyn/issues/376)
- Add Appwrite as a sponsor in the README [\#348](https://github.com/sparckles/robyn/issues/348)
- \[BUG\] Get Stared failed on Windows [\#340](https://github.com/sparckles/robyn/issues/340)
- \[BUG\] Fix CI/CD pipeline [\#310](https://github.com/sparckles/robyn/issues/310)
**Merged pull requests:**
- chore\(ci\): fix robyn installation in test CI [\#383](https://github.com/sparckles/robyn/pull/383) ([AntoineRR](https://github.com/AntoineRR))
- fix: return 500 status code when route raise [\#382](https://github.com/sparckles/robyn/pull/382) ([AntoineRR](https://github.com/AntoineRR))
- fix: return 404 status code when route isn't found [\#380](https://github.com/sparckles/robyn/pull/380) ([AntoineRR](https://github.com/AntoineRR))
- ci: enable precommit hooks on everything [\#371](https://github.com/sparckles/robyn/pull/371) ([sansyrox](https://github.com/sansyrox))
- chore: run tests on linux, macos and windows and release builds on ta… [\#370](https://github.com/sparckles/robyn/pull/370) ([AntoineRR](https://github.com/AntoineRR))
- docs: add appwrite logo as sponsors [\#369](https://github.com/sparckles/robyn/pull/369) ([sansyrox](https://github.com/sansyrox))
- test: improve pytest fixtures [\#368](https://github.com/sparckles/robyn/pull/368) ([AntoineRR](https://github.com/AntoineRR))
- Move pre-commit hooks to use Ruff [\#364](https://github.com/sparckles/robyn/pull/364) ([patrick91](https://github.com/patrick91))
## [v0.23.0](https://github.com/sparckles/robyn/tree/v0.23.0) (2023-01-21)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.22.1...v0.23.0)
**Closed issues:**
- \[Feature Request\] Improve the release and testing pipeline [\#341](https://github.com/sparckles/robyn/issues/341)
**Merged pull requests:**
- ci: delete the test pypi workflow [\#367](https://github.com/sparckles/robyn/pull/367) ([sansyrox](https://github.com/sansyrox))
- docs: Add page icon to index page [\#365](https://github.com/sparckles/robyn/pull/365) ([Abdur-rahmaanJ](https://github.com/Abdur-rahmaanJ))
- test: speed up tests [\#362](https://github.com/sparckles/robyn/pull/362) ([AntoineRR](https://github.com/AntoineRR))
- Replace the default port with 8080 [\#352](https://github.com/sparckles/robyn/pull/352) ([sansyrox](https://github.com/sansyrox))
## [v0.22.1](https://github.com/sparckles/robyn/tree/v0.22.1) (2023-01-16)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.22.0...v0.22.1)
**Closed issues:**
- \[BUG\] Python 3.11 error: metadata-generation-failed [\#357](https://github.com/sparckles/robyn/issues/357)
**Merged pull requests:**
- ci: update precommit config [\#361](https://github.com/sparckles/robyn/pull/361) ([sansyrox](https://github.com/sansyrox))
- \[pre-commit.ci\] pre-commit autoupdate [\#359](https://github.com/sparckles/robyn/pull/359) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
- chore\(ci\): add python 3.11 to the build and test CI [\#358](https://github.com/sparckles/robyn/pull/358) ([AntoineRR](https://github.com/AntoineRR))
- Updates prose to format code block and docs [\#356](https://github.com/sparckles/robyn/pull/356) ([rachfop](https://github.com/rachfop))
## [v0.22.0](https://github.com/sparckles/robyn/tree/v0.22.0) (2023-01-14)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.21.0...v0.22.0)
**Closed issues:**
- AttributeError: 'Robyn' object has no attribute 'headers'\[BUG\] [\#353](https://github.com/sparckles/robyn/issues/353)
- \[Feature Request\] Allow support for multiple file types [\#344](https://github.com/sparckles/robyn/issues/344)
- \[Feature Request\] Investigate if we need an unit tests for Python functions created in Rust [\#311](https://github.com/sparckles/robyn/issues/311)
- \[Experimental Feature Request\] Story driven programming [\#258](https://github.com/sparckles/robyn/issues/258)
**Merged pull requests:**
- fix: windows support [\#354](https://github.com/sparckles/robyn/pull/354) ([sansyrox](https://github.com/sansyrox))
- fix: better handling of route return type [\#349](https://github.com/sparckles/robyn/pull/349) ([AntoineRR](https://github.com/AntoineRR))
## [v0.21.0](https://github.com/sparckles/robyn/tree/v0.21.0) (2023-01-06)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.20.0...v0.21.0)
**Closed issues:**
- \[Feature Request\] Support for image file type [\#343](https://github.com/sparckles/robyn/issues/343)
- Not able to see the added logs [\#342](https://github.com/sparckles/robyn/issues/342)
- \[Feature Request\] Hope robyn can support returning f-string format [\#338](https://github.com/sparckles/robyn/issues/338)
- \[Feature Request\] Refactor Robyn response to allow objects other than strings [\#336](https://github.com/sparckles/robyn/issues/336)
- \[BUG\] Custom headers not sent when const=False [\#323](https://github.com/sparckles/robyn/issues/323)
- \[Feature Request\] Add documentation for custom template support in v0.19.0 [\#321](https://github.com/sparckles/robyn/issues/321)
- \[BUG\] Always need to return a string in a route [\#305](https://github.com/sparckles/robyn/issues/305)
**Merged pull requests:**
- fix: fix the static file serving [\#347](https://github.com/sparckles/robyn/pull/347) ([sansyrox](https://github.com/sansyrox))
- feat: return Response from routes [\#346](https://github.com/sparckles/robyn/pull/346) ([AntoineRR](https://github.com/AntoineRR))
## [v0.20.0](https://github.com/sparckles/robyn/tree/v0.20.0) (2022-12-20)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.19.2...v0.20.0)
**Closed issues:**
- \[Feature Request\] Add an automated benchmark script [\#320](https://github.com/sparckles/robyn/issues/320)
**Merged pull requests:**
- feat: allow non string types in response [\#337](https://github.com/sparckles/robyn/pull/337) ([sansyrox](https://github.com/sansyrox))
- feat: add an auto benchmark script [\#329](https://github.com/sparckles/robyn/pull/329) ([AntoineRR](https://github.com/AntoineRR))
## [v0.19.2](https://github.com/sparckles/robyn/tree/v0.19.2) (2022-12-14)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.19.1...v0.19.2)
**Closed issues:**
- \[BUG\] The --dev flag not working on Ubuntu 20.04 [\#332](https://github.com/sparckles/robyn/issues/332)
- \[Feature Request\] Allow the ability of sending the headers from the same route [\#325](https://github.com/sparckles/robyn/issues/325)
**Merged pull requests:**
- fix: allow response headers and fix headers not working in const requests [\#331](https://github.com/sparckles/robyn/pull/331) ([sansyrox](https://github.com/sansyrox))
- fix: factorizing code [\#322](https://github.com/sparckles/robyn/pull/322) ([AntoineRR](https://github.com/AntoineRR))
## [v0.19.1](https://github.com/sparckles/robyn/tree/v0.19.1) (2022-12-03)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.19.0...v0.19.1)
## [v0.19.0](https://github.com/sparckles/robyn/tree/v0.19.0) (2022-12-02)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.18.3...v0.19.0)
**Closed issues:**
- \[Feature Request\] Allow the ability of sending the headers from the same route [\#326](https://github.com/sparckles/robyn/issues/326)
- \[Feature Request\] Allow the ability of sending the headers from the same route [\#324](https://github.com/sparckles/robyn/issues/324)
- \[BUG\] Error in Examples section in Documentation [\#314](https://github.com/sparckles/robyn/issues/314)
- \[BUG\] Wrong link for the blog post on Robyn [\#306](https://github.com/sparckles/robyn/issues/306)
- Add documentation about deployment [\#93](https://github.com/sparckles/robyn/issues/93)
- Add support for templates! [\#10](https://github.com/sparckles/robyn/issues/10)
**Merged pull requests:**
- docs: update hosting docs [\#319](https://github.com/sparckles/robyn/pull/319) ([sansyrox](https://github.com/sansyrox))
- Various improvements around the index method [\#318](https://github.com/sparckles/robyn/pull/318) ([AntoineRR](https://github.com/AntoineRR))
- Add Railway deployment process. [\#316](https://github.com/sparckles/robyn/pull/316) ([carlosm27](https://github.com/carlosm27))
- docs: fix middleware section in examples [\#315](https://github.com/sparckles/robyn/pull/315) ([sansyrox](https://github.com/sansyrox))
- docs: fix blog link in website [\#309](https://github.com/sparckles/robyn/pull/309) ([sansyrox](https://github.com/sansyrox))
- Router refactor [\#307](https://github.com/sparckles/robyn/pull/307) ([AntoineRR](https://github.com/AntoineRR))
## [v0.18.3](https://github.com/sparckles/robyn/tree/v0.18.3) (2022-11-10)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.18.2...v0.18.3)
**Closed issues:**
- \[BUG\] `--log-level` not working [\#300](https://github.com/sparckles/robyn/issues/300)
- \[Feature Request\] Refactor Code to include better types [\#254](https://github.com/sparckles/robyn/issues/254)
**Merged pull requests:**
- fix: log level not working [\#303](https://github.com/sparckles/robyn/pull/303) ([sansyrox](https://github.com/sansyrox))
- add route type enum [\#299](https://github.com/sparckles/robyn/pull/299) ([suhailmalik07](https://github.com/suhailmalik07))
## [v0.18.2](https://github.com/sparckles/robyn/tree/v0.18.2) (2022-11-05)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.18.1...v0.18.2)
**Closed issues:**
- \[Feature Request?\] Update `matchit` crate to the most recent version [\#291](https://github.com/sparckles/robyn/issues/291)
- \[Feature Request\] Add `@wraps` in route dectorators [\#285](https://github.com/sparckles/robyn/issues/285)
- \[Feature Request\] fix clippy issues [\#265](https://github.com/sparckles/robyn/issues/265)
**Merged pull requests:**
- style: add logging for url port and host [\#304](https://github.com/sparckles/robyn/pull/304) ([sansyrox](https://github.com/sansyrox))
- fix config of port and url [\#302](https://github.com/sparckles/robyn/pull/302) ([kimhyun5u](https://github.com/kimhyun5u))
- update rust packages to latest [\#298](https://github.com/sparckles/robyn/pull/298) ([suhailmalik07](https://github.com/suhailmalik07))
- fix: retain metadata of the route functions [\#295](https://github.com/sparckles/robyn/pull/295) ([sansyrox](https://github.com/sansyrox))
- `SocketHeld::new` refactor [\#294](https://github.com/sparckles/robyn/pull/294) ([Jamyw7g](https://github.com/Jamyw7g))
## [v0.18.1](https://github.com/sparckles/robyn/tree/v0.18.1) (2022-10-23)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.18.0...v0.18.1)
**Merged pull requests:**
- fix: replaced match with if let [\#293](https://github.com/sparckles/robyn/pull/293) ([Markaeus](https://github.com/Markaeus))
- Hotfix detecting robyn.env [\#292](https://github.com/sparckles/robyn/pull/292) ([Shending-Help](https://github.com/Shending-Help))
- fix: enable hot reload on windows [\#290](https://github.com/sparckles/robyn/pull/290) ([guilefoylegaurav](https://github.com/guilefoylegaurav))
## [v0.18.0](https://github.com/sparckles/robyn/tree/v0.18.0) (2022-10-12)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.5...v0.18.0)
**Closed issues:**
- \[BUG\] The --dev mode spawns more servers without clearing previous ones. [\#249](https://github.com/sparckles/robyn/issues/249)
- \[Feature\] Add support for Env variables and a robyn.yaml config file [\#97](https://github.com/sparckles/robyn/issues/97)
**Merged pull requests:**
- testing env support [\#288](https://github.com/sparckles/robyn/pull/288) ([Shending-Help](https://github.com/Shending-Help))
- Feature add support for env variables [\#286](https://github.com/sparckles/robyn/pull/286) ([Shending-Help](https://github.com/Shending-Help))
- fix: add proper kill process to conftest. \#249 [\#278](https://github.com/sparckles/robyn/pull/278) ([guilefoylegaurav](https://github.com/guilefoylegaurav))
## [v0.17.5](https://github.com/sparckles/robyn/tree/v0.17.5) (2022-09-14)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.4...v0.17.5)
**Closed issues:**
- \[BUG\] README.md Discord link is invalid [\#272](https://github.com/sparckles/robyn/issues/272)
- \[Feature Request\] Add Digital Ocean to list of sponsors in Robyn Docs [\#270](https://github.com/sparckles/robyn/issues/270)
- \[Feature Request\] Add PyCon USA lightning talk in resources section [\#204](https://github.com/sparckles/robyn/issues/204)
- \[Feature Request\] Add community/ resources section in Docs or README [\#203](https://github.com/sparckles/robyn/issues/203)
- \[Feature Request\] Update the new architecture on the docs website [\#191](https://github.com/sparckles/robyn/issues/191)
- Add examples section [\#101](https://github.com/sparckles/robyn/issues/101)
**Merged pull requests:**
- Don't run sync functions in pool [\#282](https://github.com/sparckles/robyn/pull/282) ([JackThomson2](https://github.com/JackThomson2))
- Add documentation of Adding GraphQL support | version 1 [\#275](https://github.com/sparckles/robyn/pull/275) ([sansyrox](https://github.com/sansyrox))
- docs: improve documentation [\#269](https://github.com/sparckles/robyn/pull/269) ([sansyrox](https://github.com/sansyrox))
## [v0.17.4](https://github.com/sparckles/robyn/tree/v0.17.4) (2022-08-25)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.3...v0.17.4)
**Closed issues:**
- \[BUG?\] Startup failure OSError: \[WinError 87\] The parameter is incorrect [\#252](https://github.com/sparckles/robyn/issues/252)
- \[Feature Request\] Add mypy for pyi\(stubs\) synchronisation [\#226](https://github.com/sparckles/robyn/issues/226)
- not working on mac/windows [\#140](https://github.com/sparckles/robyn/issues/140)
**Merged pull requests:**
- Father, forgive me, for I am adding inline types. [\#266](https://github.com/sparckles/robyn/pull/266) ([sansyrox](https://github.com/sansyrox))
## [v0.17.3](https://github.com/sparckles/robyn/tree/v0.17.3) (2022-08-17)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.2...v0.17.3)
**Merged pull requests:**
- fix: parse int status code to str [\#264](https://github.com/sparckles/robyn/pull/264) ([hougesen](https://github.com/hougesen))
## [v0.17.2](https://github.com/sparckles/robyn/tree/v0.17.2) (2022-08-11)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.1...v0.17.2)
**Fixed bugs:**
- Cannot run Robyn on Windows [\#139](https://github.com/sparckles/robyn/issues/139)
**Closed issues:**
- \[BUG\] Move away from circle ci [\#240](https://github.com/sparckles/robyn/issues/240)
- Migrate the community to discord [\#239](https://github.com/sparckles/robyn/issues/239)
- \[Feature Request\] Release on test pypi before releasing on the main PyPi [\#224](https://github.com/sparckles/robyn/issues/224)
- For 0.8.x [\#75](https://github.com/sparckles/robyn/issues/75)
- Add a layer of caching in front of router [\#59](https://github.com/sparckles/robyn/issues/59)
**Merged pull requests:**
- Windows fix [\#261](https://github.com/sparckles/robyn/pull/261) ([sansyrox](https://github.com/sansyrox))
- ci: enable fail fast for faster response time in the pipelines [\#260](https://github.com/sparckles/robyn/pull/260) ([sansyrox](https://github.com/sansyrox))
- ci: add github actions to publish every PR on test pypi [\#259](https://github.com/sparckles/robyn/pull/259) ([sansyrox](https://github.com/sansyrox))
- Fix typo in README [\#246](https://github.com/sparckles/robyn/pull/246) ([bartbroere](https://github.com/bartbroere))
- chore\(ci\): move pytest from CircleCi to Github Actions [\#241](https://github.com/sparckles/robyn/pull/241) ([AntoineRR](https://github.com/AntoineRR))
## [v0.17.1](https://github.com/sparckles/robyn/tree/v0.17.1) (2022-07-19)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.17.0...v0.17.1)
**Closed issues:**
- \[Feature Request\] add clippy in ci [\#236](https://github.com/sparckles/robyn/issues/236)
- \[BUG\] Headers not available [\#231](https://github.com/sparckles/robyn/issues/231)
- \[Feature Request\] Add an all contributor bot in the README of the repo [\#225](https://github.com/sparckles/robyn/issues/225)
**Merged pull requests:**
- Add Rust CI [\#237](https://github.com/sparckles/robyn/pull/237) ([AntoineRR](https://github.com/AntoineRR))
- Contributors added in Readme [\#235](https://github.com/sparckles/robyn/pull/235) ([orvil1026](https://github.com/orvil1026))
- fix external project link in README [\#234](https://github.com/sparckles/robyn/pull/234) ([touilleMan](https://github.com/touilleMan))
- fix: fix request headers not being propagated [\#232](https://github.com/sparckles/robyn/pull/232) ([sansyrox](https://github.com/sansyrox))
- Upgrade GitHub Actions and add Python 3.10 [\#230](https://github.com/sparckles/robyn/pull/230) ([cclauss](https://github.com/cclauss))
- OrbUp: Upgrade the CircleCI Orbs [\#229](https://github.com/sparckles/robyn/pull/229) ([cclauss](https://github.com/cclauss))
- CHANGELOG.md: Fix typo [\#228](https://github.com/sparckles/robyn/pull/228) ([cclauss](https://github.com/cclauss))
## [v0.17.0](https://github.com/sparckles/robyn/tree/v0.17.0) (2022-07-06)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.6...v0.17.0)
**Closed issues:**
- A refactor [\#176](https://github.com/sparckles/robyn/issues/176)
- \[Proposal\] Const Requests [\#48](https://github.com/sparckles/robyn/issues/48)
**Merged pull requests:**
- Add a const router [\#210](https://github.com/sparckles/robyn/pull/210) ([sansyrox](https://github.com/sansyrox))
## [v0.16.6](https://github.com/sparckles/robyn/tree/v0.16.6) (2022-07-02)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.5...v0.16.6)
## [v0.16.5](https://github.com/sparckles/robyn/tree/v0.16.5) (2022-07-01)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.4...v0.16.5)
**Closed issues:**
- \[Feature Request\] Add sponsors in the repo and website [\#212](https://github.com/sparckles/robyn/issues/212)
- \[Feature Request\] Add commitizen as a dev dependency [\#211](https://github.com/sparckles/robyn/issues/211)
- Add better logging [\#158](https://github.com/sparckles/robyn/issues/158)
- Remove freeport dependency [\#151](https://github.com/sparckles/robyn/issues/151)
- Add websocket support [\#104](https://github.com/sparckles/robyn/issues/104)
- Maintenance issue [\#56](https://github.com/sparckles/robyn/issues/56)
- Improve Readme [\#4](https://github.com/sparckles/robyn/issues/4)
**Merged pull requests:**
- fix: Fixes the crashing dev mode [\#218](https://github.com/sparckles/robyn/pull/218) ([sansyrox](https://github.com/sansyrox))
- feat: add commitizen as a dev dependency [\#216](https://github.com/sparckles/robyn/pull/216) ([sansyrox](https://github.com/sansyrox))
- Isort imports [\#205](https://github.com/sparckles/robyn/pull/205) ([sansyrox](https://github.com/sansyrox))
- Add bridged logger. Improves performance substantially. [\#201](https://github.com/sparckles/robyn/pull/201) ([sansyrox](https://github.com/sansyrox))
- Adds pre-commit hooks for black, flake8, isort [\#198](https://github.com/sparckles/robyn/pull/198) ([chrismoradi](https://github.com/chrismoradi))
- Resolves port open issue when app is killed \#183 [\#196](https://github.com/sparckles/robyn/pull/196) ([anandtripathi5](https://github.com/anandtripathi5))
- Removing unwraps [\#195](https://github.com/sparckles/robyn/pull/195) ([sansyrox](https://github.com/sansyrox))
## [v0.16.4](https://github.com/sparckles/robyn/tree/v0.16.4) (2022-05-30)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.3...v0.16.4)
**Closed issues:**
- \[Feature Request\] Remove extra logs [\#200](https://github.com/sparckles/robyn/issues/200)
- \[Feature Request\] Add precommit hook for black, flake8 and isort [\#194](https://github.com/sparckles/robyn/issues/194)
- \[BUG\] Get rid of Hashmap Clones and Unwraps! [\#186](https://github.com/sparckles/robyn/issues/186)
## [v0.16.3](https://github.com/sparckles/robyn/tree/v0.16.3) (2022-05-18)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.2...v0.16.3)
**Closed issues:**
- \[BUG\] Port not being free on app kill [\#183](https://github.com/sparckles/robyn/issues/183)
- Build failure [\#166](https://github.com/sparckles/robyn/issues/166)
## [v0.16.2](https://github.com/sparckles/robyn/tree/v0.16.2) (2022-05-09)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.1...v0.16.2)
## [v0.16.1](https://github.com/sparckles/robyn/tree/v0.16.1) (2022-05-09)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.16.0...v0.16.1)
**Closed issues:**
- Add Python stubs [\#130](https://github.com/sparckles/robyn/issues/130)
**Merged pull requests:**
- Setup types for Robyn [\#192](https://github.com/sparckles/robyn/pull/192) ([sansyrox](https://github.com/sansyrox))
- Fix build pipeline [\#190](https://github.com/sparckles/robyn/pull/190) ([sansyrox](https://github.com/sansyrox))
- fix typo :pencil2: in api docs. [\#189](https://github.com/sparckles/robyn/pull/189) ([sombralibre](https://github.com/sombralibre))
- Remove hashmap clones [\#187](https://github.com/sparckles/robyn/pull/187) ([sansyrox](https://github.com/sansyrox))
- Code clean up | Modularise rust code [\#185](https://github.com/sparckles/robyn/pull/185) ([sansyrox](https://github.com/sansyrox))
- Add experimental io-uring [\#184](https://github.com/sparckles/robyn/pull/184) ([sansyrox](https://github.com/sansyrox))
- Implement Response headers [\#179](https://github.com/sparckles/robyn/pull/179) ([sansyrox](https://github.com/sansyrox))
- Code cleanup [\#178](https://github.com/sparckles/robyn/pull/178) ([sansyrox](https://github.com/sansyrox))
## [v0.16.0](https://github.com/sparckles/robyn/tree/v0.16.0) (2022-04-29)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.15.1...v0.16.0)
**Closed issues:**
- \[Feature Request\] Add list of sponsors on the project website [\#182](https://github.com/sparckles/robyn/issues/182)
- Optional build feature for io\_uring [\#177](https://github.com/sparckles/robyn/issues/177)
- Create Custom headers for the response. [\#174](https://github.com/sparckles/robyn/issues/174)
## [v0.15.1](https://github.com/sparckles/robyn/tree/v0.15.1) (2022-03-24)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.15.0...v0.15.1)
**Closed issues:**
- Add middleware support [\#95](https://github.com/sparckles/robyn/issues/95)
**Merged pull requests:**
- Make websocket id accessible [\#173](https://github.com/sparckles/robyn/pull/173) ([sansyrox](https://github.com/sansyrox))
- Use Clippy tool optimized code [\#171](https://github.com/sparckles/robyn/pull/171) ([mrxiaozhuox](https://github.com/mrxiaozhuox))
- Modify headers [\#170](https://github.com/sparckles/robyn/pull/170) ([sansyrox](https://github.com/sansyrox))
- Update README.md [\#168](https://github.com/sparckles/robyn/pull/168) ([Polokghosh53](https://github.com/Polokghosh53))
## [v0.15.0](https://github.com/sparckles/robyn/tree/v0.15.0) (2022-03-17)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.14.0...v0.15.0)
**Closed issues:**
- \[BUG\] Unable to modify headers in middlewares [\#167](https://github.com/sparckles/robyn/issues/167)
- Add Pycon talk link to docs [\#102](https://github.com/sparckles/robyn/issues/102)
## [v0.14.0](https://github.com/sparckles/robyn/tree/v0.14.0) (2022-03-03)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.13.1...v0.14.0)
**Fixed bugs:**
- Build error [\#161](https://github.com/sparckles/robyn/issues/161)
**Merged pull requests:**
- Implement Custom Response objects. [\#165](https://github.com/sparckles/robyn/pull/165) ([sansyrox](https://github.com/sansyrox))
- Remove deprecated endpoints [\#162](https://github.com/sparckles/robyn/pull/162) ([sansyrox](https://github.com/sansyrox))
- Fix: default url param in app.start [\#160](https://github.com/sparckles/robyn/pull/160) ([sansyrox](https://github.com/sansyrox))
- Add middlewares [\#157](https://github.com/sparckles/robyn/pull/157) ([sansyrox](https://github.com/sansyrox))
- Remove arc\(ing\) [\#156](https://github.com/sparckles/robyn/pull/156) ([sansyrox](https://github.com/sansyrox))
## [v0.13.1](https://github.com/sparckles/robyn/tree/v0.13.1) (2022-02-19)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.13.0...v0.13.1)
## [v0.13.0](https://github.com/sparckles/robyn/tree/v0.13.0) (2022-02-15)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.12.1...v0.13.0)
## [v0.12.1](https://github.com/sparckles/robyn/tree/v0.12.1) (2022-02-13)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.12.0...v0.12.1)
**Closed issues:**
- \[BUG\] Default URL cannot be assigned [\#159](https://github.com/sparckles/robyn/issues/159)
- Upcoming release\(s\) [\#141](https://github.com/sparckles/robyn/issues/141)
## [v0.12.0](https://github.com/sparckles/robyn/tree/v0.12.0) (2022-01-21)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.11.1...v0.12.0)
**Closed issues:**
- Consider adding startup events [\#153](https://github.com/sparckles/robyn/issues/153)
- Remove poetry dependency [\#150](https://github.com/sparckles/robyn/issues/150)
**Merged pull requests:**
- Add Event handlers [\#154](https://github.com/sparckles/robyn/pull/154) ([sansyrox](https://github.com/sansyrox))
- Remove poetry [\#152](https://github.com/sparckles/robyn/pull/152) ([sansyrox](https://github.com/sansyrox))
- Use print instead of input after starting server [\#149](https://github.com/sparckles/robyn/pull/149) ([klaa97](https://github.com/klaa97))
- Fix dev server [\#148](https://github.com/sparckles/robyn/pull/148) ([sansyrox](https://github.com/sansyrox))
- URL queries [\#146](https://github.com/sparckles/robyn/pull/146) ([patchgamestudio](https://github.com/patchgamestudio))
- Add project wide flake8 settings [\#143](https://github.com/sparckles/robyn/pull/143) ([sansyrox](https://github.com/sansyrox))
## [v0.11.1](https://github.com/sparckles/robyn/tree/v0.11.1) (2022-01-11)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.11.0...v0.11.1)
## [v0.11.0](https://github.com/sparckles/robyn/tree/v0.11.0) (2022-01-07)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.10.0...v0.11.0)
**Fixed bugs:**
- Hot Reloading goes in an infinite loop [\#115](https://github.com/sparckles/robyn/issues/115)
**Closed issues:**
- Benchmarks to Björn, uvicorn etc. [\#142](https://github.com/sparckles/robyn/issues/142)
- Add Python linter setup [\#129](https://github.com/sparckles/robyn/issues/129)
- Add fixtures in testing [\#84](https://github.com/sparckles/robyn/issues/84)
## [v0.10.0](https://github.com/sparckles/robyn/tree/v0.10.0) (2021-12-20)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.9.0...v0.10.0)
**Closed issues:**
- Add PyPI classifiers [\#127](https://github.com/sparckles/robyn/issues/127)
- Robyn version 0.9.0 doesn't work on Mac M1 Models [\#120](https://github.com/sparckles/robyn/issues/120)
- Inconsistency in steps mentioned in Readme to run locally [\#119](https://github.com/sparckles/robyn/issues/119)
- Async web socket support [\#116](https://github.com/sparckles/robyn/issues/116)
- Reveal Logo to be removed from Future Roadmap [\#107](https://github.com/sparckles/robyn/issues/107)
- Dead Link for Test Drive Button on Robyn Landing Page [\#106](https://github.com/sparckles/robyn/issues/106)
- Add issue template, pr template and community guidelines [\#105](https://github.com/sparckles/robyn/issues/105)
- For v0.7.0 [\#72](https://github.com/sparckles/robyn/issues/72)
- Add better support for requests and response! [\#13](https://github.com/sparckles/robyn/issues/13)
**Merged pull requests:**
- Add async support in WS [\#134](https://github.com/sparckles/robyn/pull/134) ([sansyrox](https://github.com/sansyrox))
- Add help messages and simplify 'dev' option [\#128](https://github.com/sparckles/robyn/pull/128) ([Kludex](https://github.com/Kludex))
- Apply Python highlight on api.md [\#126](https://github.com/sparckles/robyn/pull/126) ([Kludex](https://github.com/Kludex))
- Update comparison.md [\#124](https://github.com/sparckles/robyn/pull/124) ([Kludex](https://github.com/Kludex))
- Update comparison.md [\#123](https://github.com/sparckles/robyn/pull/123) ([Kludex](https://github.com/Kludex))
- Fix readme documentation [\#122](https://github.com/sparckles/robyn/pull/122) ([sansyrox](https://github.com/sansyrox))
- Release v0.9.0 Changelog [\#121](https://github.com/sparckles/robyn/pull/121) ([sansyrox](https://github.com/sansyrox))
- \[FEAT\] Open Source Contribution Templates [\#118](https://github.com/sparckles/robyn/pull/118) ([shivaylamba](https://github.com/shivaylamba))
- FIX : Wrong link for Test Drive [\#117](https://github.com/sparckles/robyn/pull/117) ([shivaylamba](https://github.com/shivaylamba))
## [v0.9.0](https://github.com/sparckles/robyn/tree/v0.9.0) (2021-12-01)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.8.1...v0.9.0)
**Closed issues:**
- Add more HTTP methods [\#74](https://github.com/sparckles/robyn/issues/74)
**Merged pull requests:**
- Fix default url bug [\#111](https://github.com/sparckles/robyn/pull/111) ([sansyrox](https://github.com/sansyrox))
- Web socket integration attempt 2 [\#109](https://github.com/sparckles/robyn/pull/109) ([sansyrox](https://github.com/sansyrox))
## [v0.8.1](https://github.com/sparckles/robyn/tree/v0.8.1) (2021-11-17)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.8.0...v0.8.1)
**Fixed bugs:**
- The default start is running the server at '0.0.0.0' instead of '127.0.0.1' [\#110](https://github.com/sparckles/robyn/issues/110)
## [v0.8.0](https://github.com/sparckles/robyn/tree/v0.8.0) (2021-11-10)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.7.1...v0.8.0)
**Closed issues:**
- Share the TCP web socket across different cores [\#91](https://github.com/sparckles/robyn/issues/91)
- Improve the router [\#52](https://github.com/sparckles/robyn/issues/52)
- \[Stretch Goal\] Create a a way of writing async request [\#32](https://github.com/sparckles/robyn/issues/32)
- Improve the router [\#29](https://github.com/sparckles/robyn/issues/29)
**Merged pull requests:**
- Fix the failing testing suite! [\#100](https://github.com/sparckles/robyn/pull/100) ([sansyrox](https://github.com/sansyrox))
- Requests object is now optional [\#99](https://github.com/sparckles/robyn/pull/99) ([sansyrox](https://github.com/sansyrox))
- Add socket sharing [\#94](https://github.com/sparckles/robyn/pull/94) ([sansyrox](https://github.com/sansyrox))
## [v0.7.1](https://github.com/sparckles/robyn/tree/v0.7.1) (2021-10-28)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.7.0...v0.7.1)
**Closed issues:**
- Remove the solution using dockerisation of tests [\#98](https://github.com/sparckles/robyn/issues/98)
- Functions not working without request param [\#96](https://github.com/sparckles/robyn/issues/96)
- Add actix router [\#85](https://github.com/sparckles/robyn/issues/85)
- Request apart from GET are not working in directory subroutes [\#79](https://github.com/sparckles/robyn/issues/79)
- Add the ability to share the server across the network [\#69](https://github.com/sparckles/robyn/issues/69)
- Add the ability to view headers in the HTTP Methods [\#54](https://github.com/sparckles/robyn/issues/54)
- Add tests! [\#8](https://github.com/sparckles/robyn/issues/8)
## [v0.7.0](https://github.com/sparckles/robyn/tree/v0.7.0) (2021-10-03)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.6.1...v0.7.0)
**Closed issues:**
- Robyn the replacement of Quart [\#86](https://github.com/sparckles/robyn/issues/86)
- Add Pytest support for the test endpoints [\#81](https://github.com/sparckles/robyn/issues/81)
**Merged pull requests:**
- Finally completed router integration [\#90](https://github.com/sparckles/robyn/pull/90) ([sansyrox](https://github.com/sansyrox))
- Address clippy lints [\#89](https://github.com/sparckles/robyn/pull/89) ([SanchithHegde](https://github.com/SanchithHegde))
- Initial docs update [\#83](https://github.com/sparckles/robyn/pull/83) ([sansyrox](https://github.com/sansyrox))
- Add the basics of python testing [\#82](https://github.com/sparckles/robyn/pull/82) ([sansyrox](https://github.com/sansyrox))
- Add a new landing page [\#80](https://github.com/sparckles/robyn/pull/80) ([sansyrox](https://github.com/sansyrox))
## [v0.6.1](https://github.com/sparckles/robyn/tree/v0.6.1) (2021-08-30)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.6.0...v0.6.1)
**Closed issues:**
- Make a new release [\#71](https://github.com/sparckles/robyn/issues/71)
- Update to the pyo3 v0.14 [\#63](https://github.com/sparckles/robyn/issues/63)
- Add the support to serve static directories [\#55](https://github.com/sparckles/robyn/issues/55)
- Add support for mounting directory [\#38](https://github.com/sparckles/robyn/issues/38)
**Merged pull requests:**
- Add the base of http requests [\#78](https://github.com/sparckles/robyn/pull/78) ([sansyrox](https://github.com/sansyrox))
- Add default port and a variable url [\#77](https://github.com/sparckles/robyn/pull/77) ([sansyrox](https://github.com/sansyrox))
- Make the request object accessible in every route [\#76](https://github.com/sparckles/robyn/pull/76) ([sansyrox](https://github.com/sansyrox))
- Add the basics for circle ci and testing framework [\#67](https://github.com/sparckles/robyn/pull/67) ([sansyrox](https://github.com/sansyrox))
- Update to pyo3 v0.14 [\#65](https://github.com/sparckles/robyn/pull/65) ([sansyrox](https://github.com/sansyrox))
- Add the static directory serving [\#64](https://github.com/sparckles/robyn/pull/64) ([sansyrox](https://github.com/sansyrox))
- Create a request object [\#61](https://github.com/sparckles/robyn/pull/61) ([sansyrox](https://github.com/sansyrox))
- Add the ability to add body in PUT, PATCH and DELETE [\#60](https://github.com/sparckles/robyn/pull/60) ([sansyrox](https://github.com/sansyrox))
- Implement a working dev server [\#40](https://github.com/sparckles/robyn/pull/40) ([sansyrox](https://github.com/sansyrox))
- Use Actix as base [\#35](https://github.com/sparckles/robyn/pull/35) ([JackThomson2](https://github.com/JackThomson2))
## [v0.6.0](https://github.com/sparckles/robyn/tree/v0.6.0) (2021-08-11)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.5.3...v0.6.0)
**Closed issues:**
- Add body support for PUT, POST and PATCH [\#53](https://github.com/sparckles/robyn/issues/53)
- Away with limited internet access till 1st August [\#51](https://github.com/sparckles/robyn/issues/51)
- Add doc stings [\#42](https://github.com/sparckles/robyn/issues/42)
- OSX builds are failing [\#41](https://github.com/sparckles/robyn/issues/41)
- Add a dev server implementation [\#37](https://github.com/sparckles/robyn/issues/37)
- Mini Roadmap | A list of issues that would require fixing [\#19](https://github.com/sparckles/robyn/issues/19)
- Add support for Object/JSON Return Type! [\#9](https://github.com/sparckles/robyn/issues/9)
## [v0.5.3](https://github.com/sparckles/robyn/tree/v0.5.3) (2021-07-12)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.5.2...v0.5.3)
**Merged pull requests:**
- Improve the HTML file serving [\#46](https://github.com/sparckles/robyn/pull/46) ([sansyrox](https://github.com/sansyrox))
- Add the basics to add serving of static files [\#36](https://github.com/sparckles/robyn/pull/36) ([sansyrox](https://github.com/sansyrox))
## [v0.5.2](https://github.com/sparckles/robyn/tree/v0.5.2) (2021-07-11)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.5.1...v0.5.2)
## [v0.5.1](https://github.com/sparckles/robyn/tree/v0.5.1) (2021-07-10)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.5.0...v0.5.1)
**Closed issues:**
- Make html file serving more robust [\#45](https://github.com/sparckles/robyn/issues/45)
- Try to serve individual static files using vanilla rust [\#43](https://github.com/sparckles/robyn/issues/43)
- Error on import [\#16](https://github.com/sparckles/robyn/issues/16)
- Add multiple process sharing [\#2](https://github.com/sparckles/robyn/issues/2)
## [v0.5.0](https://github.com/sparckles/robyn/tree/v0.5.0) (2021-07-01)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.4.1...v0.5.0)
**Closed issues:**
- QPS drops drastically after processing many requests [\#31](https://github.com/sparckles/robyn/issues/31)
- Improve the way you parse TCP streams [\#30](https://github.com/sparckles/robyn/issues/30)
- Re-introduce thread pool for the sync functions \(maybe\) [\#22](https://github.com/sparckles/robyn/issues/22)
- Add async listener object in rust stream! [\#11](https://github.com/sparckles/robyn/issues/11)
**Merged pull requests:**
- Make the server http compliant [\#33](https://github.com/sparckles/robyn/pull/33) ([sansyrox](https://github.com/sansyrox))
## [v0.4.1](https://github.com/sparckles/robyn/tree/v0.4.1) (2021-06-26)
[Full Changelog](https://github.com/sparckles/robyn/compare/0.4.0...v0.4.1)
**Closed issues:**
- Add PyPi Metadata [\#5](https://github.com/sparckles/robyn/issues/5)
**Merged pull requests:**
- Build and publish wheels on GitHub Actions [\#26](https://github.com/sparckles/robyn/pull/26) ([messense](https://github.com/messense))
- Code cleanup using PyFunction type [\#25](https://github.com/sparckles/robyn/pull/25) ([sansyrox](https://github.com/sansyrox))
- Add non blocking sync functions [\#23](https://github.com/sparckles/robyn/pull/23) ([sansyrox](https://github.com/sansyrox))
- Add support for sync functions [\#20](https://github.com/sparckles/robyn/pull/20) ([sansyrox](https://github.com/sansyrox))
## [0.4.0](https://github.com/sparckles/robyn/tree/0.4.0) (2021-06-22)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.3.0...0.4.0)
**Closed issues:**
- Add support for Sync functions as well! [\#7](https://github.com/sparckles/robyn/issues/7)
## [v0.3.0](https://github.com/sparckles/robyn/tree/v0.3.0) (2021-06-21)
[Full Changelog](https://github.com/sparckles/robyn/compare/v0.2.3...v0.3.0)
**Closed issues:**
- Architecture link in readme redirects to raw content [\#18](https://github.com/sparckles/robyn/issues/18)
- Link pointing to the wrong destination [\#6](https://github.com/sparckles/robyn/issues/6)
**Merged pull requests:**
- Pure tokio [\#17](https://github.com/sparckles/robyn/pull/17) ([JackThomson2](https://github.com/JackThomson2))
- Remove Mutex lock on Threadpool and routes [\#15](https://github.com/sparckles/robyn/pull/15) ([JackThomson2](https://github.com/JackThomson2))
## [v0.2.3](https://github.com/sparckles/robyn/tree/v0.2.3) (2021-06-18)
[Full Changelog](https://github.com/sparckles/robyn/compare/c14f52e6faa79917e89de4220590da7bf28f6a65...v0.2.3)
**Closed issues:**
- Improve async runtime [\#3](https://github.com/sparckles/robyn/issues/3)
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
================================================
FILE: CODE_OF_CONDUCT.md
================================================
### Robyn Open Source Community Guidelines
- **Be friendly and patient**.
- **Be welcoming**.
- **Be respectful**.
- **Be careful in the words that we choose**.
================================================
FILE: CONTRIBUTING.md
================================================
## Contributing Guidelines
First off, thank you for considering contributing to `Robyn`. This guide details all the general information that one should know before contributing to the project.
Please stick as close as possible to the guidelines. That way, we ensure that you have a smooth experience contributing to this project.
### General Rules:
These are, in general, rules that you should be following while contributing to an Open-Source project :
- Be Nice, Be Respectful (BNBR)
- Check if the Issue you created, exists or not.
- While creating a new issue, make sure you describe the issue clearly.
- Make proper commit messages and document your PR well.
- Always add comments in your Code and explain it at points if possible, add Doctest.
- Always create a Pull Request from a Branch; Never from the Main.
- Follow proper code conventions because writing clean code is important.
- Issues would be assigned on a "First Come, First Served" basis.
- Do mention (@sansyrox) the project maintainer if your PR isn't reviewed within a few days.
## First time contributors:
Pushing files in your own repository is easy, but how to contribute to someone else's project? If you have the same question, then below are the steps that you can follow
to make your first contribution in this repository.
### Pull Request
**1.** The very first step includes forking the project. Click on the `fork` button as shown below to fork the project.
**2.** Clone the forked repository. Open up the GitBash/Command Line and type
```
git clone https://github.com//robyn.git
```
**3.** Navigate to the project directory.
```
cd robyn
```
**4.** Add a reference to the original repository.
```
git remote add upstream https://github.com/sparckles/robyn.git
```
**5.** See latest changes to the repo using
```
git remote -v
```
**6.** Create a new branch.
```
git checkout -b
```
**7.** Always take a pull from the upstream repository to your main branch to keep it even with the main project. This will save you from frequent merge conflicts.
```
git pull upstream main
```
**8.** You can make the required changes now. Make appropriate commits with proper commit messages.
**9.** Add and then commit your changes.
```
git add .
```
```
git commit -m ""
```
**10.** Push your local branch to the remote repository.
```
git push -u origin
```
**11.** Once you have pushed the changes to your repository, go to your forked repository. Click on the `Compare & pull request` button as shown below.
**12.** The image below is what the new page would look like. Give a proper title to your PR and describe the changes made by you in the description box.(Note - Sometimes there are PR templates which are to be filled as instructed.)
**13.** Open a pull request by clicking the `Create pull request` button.
`Voila, you have made your first contribution to this project`
## Issue
- Issues can be used to keep track of bugs, enhancements, or other requests. Creating an issue to let the project maintainers know about the changes you are planning to make before raising a PR is a good open-source practice.
Let's walk through the steps to create an issue:
**1.** On GitHub, navigate to the main page of the repository. [Here](https://github.com/sparckles/robyn.git) in this case.
**2.** Under your repository name, click on the `Issues` button.
**3.** Click on the `New issue` button.
**4.** Select one of the Issue Templates to get started.
**5.** Fill in the appropriate `Title` and `Issue description` and click on `Submit new issue`.
### Tutorials that may help you:
- [Git & GitHub Tutorial](https://www.youtube.com/watch?v=RGOj5yH7evk)
- [Resolve merge conflict](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-on-github)
================================================
FILE: Cargo.toml
================================================
[package]
name = "robyn"
version = "0.82.0"
authors = ["Sanskar Jethi "]
edition = "2021"
description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime."
license = "BSD License (BSD)"
homepage = "https://github.com/sparckles/robyn"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "robyn"
crate-type = ["cdylib", "rlib"]
[dependencies]
pyo3 = { version = "0.27.2", features = ["extension-module", "py-clone"]}
pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] }
pyo3-async-runtimes-macros = { version = "0.27" }
pyo3-log = "0.13.2"
tokio = { version = "1.40", features = ["full"] }
dashmap = "5.4.3"
anyhow = "1.0.69"
actix = "0.13.4"
actix-web-actors = "4.3.0"
actix-web = "4.4.2"
actix-http = "3.3.1"
actix-files = "0.6.2"
futures = "0.3.27"
futures-util = "0.3.27"
matchit = "0.7.3"
socket2 = { version = "0.5.1", features = ["all"] }
uuid = { version = "1.3.0", features = ["serde", "v4"] }
log = "0.4.17"
pythonize = "0.27"
serde = "1.0.187"
serde_json = "1.0.109"
once_cell = "1.8.0"
actix-multipart = "0.6.1"
parking_lot = "0.12.3"
crossbeam-channel = "0.5"
[features]
io-uring = ["actix-web/experimental-io-uring"]
[profile.release]
codegen-units = 1
lto = "fat"
panic = "abort"
strip = true
opt-level = 3
[profile.release.build-override]
opt-level = 3
[package.metadata.maturin]
name = "robyn"
================================================
FILE: LICENSE
================================================
BSD 2-Clause License
Copyright (c) 2021, Sanskar Jethi
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# Robyn
[](https://twitter.com/Robyn_oss)
[](https://pepy.tech/project/Robyn)
[](https://github.com/sparckles/Robyn/releases/)
[](https://github.com/sparckles/Robyn/blob/main/LICENSE)

[](https://deepwiki.com/sparckles/Robyn)
[](https://robyn.tech/documentation)
[](https://discord.gg/rkERZ5eNU8)
[](https://gurubase.io/g/robyn)
Robyn is a High-Performance, Community-Driven, and Innovator Friendly Web Framework with a Rust runtime. You can learn more by checking our [community resources](https://robyn.tech/documentation/en/community-resources#talks)!
Source: [TechEmpower Round 22](https://www.techempower.com/benchmarks/#section=data-r22&test=plaintext)
## 📦 Installation
You can simply use Pip for installation.
```bash
pip install robyn
```
Or, with [conda-forge](https://conda-forge.org/)
```bash
conda install -c conda-forge robyn
```
To install with all optional features (Pydantic validation, Jinja2 templating):
```bash
pip install "robyn[all]"
```
## 🤔 Usage
### 🚀 Define your API
To define your API, you can add the following code in an `app.py` file.
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def h(request):
return "Hello, world!"
app.start(port=8080)
```
### 🏃 Run your code
Simply run the app.py file you created. You will then have access to a server on the `localhost:8080`, that you can request from an other program. Robyn provides several options to customize your web server.
```
$ python3 app.py
```
To see the usage
```
usage: app.py [-h] [--processes PROCESSES] [--workers WORKERS] [--dev] [--log-level LOG_LEVEL]
Robyn, a fast async web framework with a rust runtime.
options:
-h, --help show this help message and exit
--processes PROCESSES
Choose the number of processes. [Default: 1]
--workers WORKERS Choose the number of workers. [Default: 1]
--dev Development mode. It restarts the server based on file changes.
--log-level LOG_LEVEL
Set the log level name
--create Create a new project template.
--docs Open the Robyn documentation.
--open-browser Open the browser on successful start.
--version Show the Robyn version.
--compile-rust-path COMPILE_RUST_PATH
Compile rust files in the given path.
--create-rust-file CREATE_RUST_FILE
Create a rust file with the given name.
--disable-openapi Disable the OpenAPI documentation.
--fast Enable the fast mode.
```
Log level can be `DEBUG`, `INFO`, `WARNING`, or `ERROR`.
When running the app using `--open-browser` a new browser window will open at the app location, e.g:
```
$ python3 app.py --open-browser
```
### 💻 Add more routes
You can add more routes to your API. Check out the routes in [this file](https://github.com/sparckles/Robyn/blob/main/integration_tests/base_routes.py) as examples.
### 🐍 Python Version Support
Robyn is compatible with the following Python versions:
> Python >= 3.10
It is recommended to use the latest version of Python for the best performances.
Please make sure you have the correct version of Python installed before starting to use
this project. You can check your Python version by running the following command in your
terminal:
```bash
python --version
```
## 💡 Features
- Under active development!
- A multithreaded Runtime
- Extensible
- A simple API
- Sync and Async Function Support
- Dynamic URL Routing
- Multi Core Scaling
- WebSockets
- Middlewares (before and after request hooks)
- Built in form data handling
- Dependency Injection
- Hot Reloading
- Direct Rust Integration
- Automatic OpenAPI generation
- Jinja2 Templating
- Static File Serving
- File Responses and Downloads
- Authentication Support
- CORS Configuration
- Streaming / SSE Responses
- Startup and Shutdown Events
- Exception Handling
- SubRouters
- Project Scaffolding via CLI
- Experimental io-uring Support
- **🤖 AI Agent Support** - Built-in agent routing and execution
- **🔌 MCP (Model Context Protocol)** - Connect to AI applications as a server
- Community First and truly FOSS!
## 🗒️ How to contribute
### 🏁 Get started
Please read the [code of conduct](https://github.com/sparckles/Robyn/blob/main/CODE_OF_CONDUCT.md) and go through [CONTRIBUTING.md](https://github.com/sparckles/Robyn/blob/main/CONTRIBUTING.md) before contributing to Robyn.
Feel free to open an issue for any clarifications or suggestions.
If you're feeling curious. You can take a look at a more detailed architecture [here](https://robyn.tech/documentation/architecture).
If you still need help to get started, feel free to reach out on our [community discord](https://discord.gg/rkERZ5eNU8).
### ⚙️ To Develop Locally
#### Prerequisites
Before starting, ensure you have the following installed:
- Python >= 3.10, <= 3.14
- Rust (latest stable)
- C compiler (gcc/clang)
#### Setup
- Clone the repository:
```
git clone https://github.com/sparckles/Robyn.git
```
- Setup a virtual environment:
```
python3 -m venv .venv
source .venv/bin/activate
```
- Install required packages
```
pip install pre-commit poetry maturin
```
- Install development dependencies
```
poetry install --with dev --with test
```
- Install pre-commit git hooks
```
pre-commit install
```
- Build & install Robyn Rust package
```
maturin develop
```
- Build & install Robyn Rust package (**experimental**)
```
maturin develop --cargo-extra-args="--features=io-uring"
```
- Run!
```
poetry run test_server
```
- Run all tests
```
pytest
```
- Run only the integration tests
```
pytest integration_tests
```
- Run only the unit tests (you don't need to be running the test_server for these)
```
pytest unit_tests
```
- Test (refer to `integration_tests/base_routes.py` for more endpoints)
```
curl http://localhost:8080/sync/str
```
- **tip:** One liners for testing changes!
```
maturin develop && poetry run test_server
maturin develop && pytest
```
- **tip:** For IO-uring support, you can use the following command:
```
maturin develop --cargo-extra-args="--features=io-uring"
```
- **tip:** To use your local Robyn version in other projects, you can install it using pip:
```
pip install -e path/to/robyn/target/wheels/robyn---.whl
```
e.g.
```
pip install -e /repos/Robyn/target/wheels/robyn-0.63.0-cp312-cp312-macosx_10_15_universal2.whl
```
#### Troubleshooting
If you face any issues, here are some common fixes:
- install `patchelf` with `pip install patchelf` if you face `patchelf` not found issue during `maturin develop` (esp. on Arch Linux)
- If you get Rust compilation errors, ensure you have a C compiler installed:
- Ubuntu/Debian: `sudo apt install build-essential`
- Fedora: `sudo dnf install gcc`
- macOS: Install Xcode Command Line Tools
- Windows: Install Visual Studio Build Tools
## ✨ Special thanks
### ✨ Contributors/Supporters
Thanks to all the contributors of the project. Robyn will not be what it is without all your support :heart:.
Special thanks to the [PyO3](https://pyo3.rs/v0.13.2/) community and [Andrew from PyO3-asyncio](https://github.com/awestlake87/pyo3-asyncio) for their amazing libraries and their support for my queries. 💖
### ✨ Sponsors
These sponsors help us make the magic happen!
[](https://www.digitalocean.com/?refcode=3f2b9fd4968d&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge)
[](https://github.com/appwrite)
## Star History
[](https://star-history.com/#sparckles/Robyn&Date)
================================================
FILE: benchmark.sh
================================================
#!/bin/sh
# Benchmark script to get info about Robyn's performances
# You can use this benchmark when developing on Robyn to test if your changes had a huge
# impact on performances. You cannot compare benchmarks from different machine and even
# several runs on the same machine can give very different results sometimes.
# Be aware of this when using this script!
Help() {
echo "Benchmark script to get info about Robyn's performances."
echo
echo "USAGE:"
echo " benchmark [-h|m|n|y]"
echo
echo "OPTIONS:"
echo " -h Print this help."
echo " -m Run 'maturin develop' to compile the Rust part of Robyn."
echo " -n Set the number of requests that oha sends."
echo " -y Skip prompt"
exit 0
}
yes_flag=false
run_maturin=false
number=100000
while getopts hymn: opt; do
case $opt in
h)
Help
;;
y)
yes_flag=true
;;
m)
run_maturin=true
;;
n)
number=$OPTARG
;;
?)
echo 'Error in command line parsing' >&2
Help
exit 1
;;
esac
done
# Prompt user to check if he installed the requirements for running the benchmark
if [ "$yes_flag" = false ]; then
echo "Make sure you are running this in your venv and you installed 'oha' using 'cargo install oha'"
echo "Do you want to proceed?"
while true; do
read -p "" yn
case $yn in
[Yy]* ) break;;
[Nn]* ) exit;;
* ) echo "Please answer yes or no.";;
esac
done
fi
# Compile Rust
if $run_maturin; then
maturin develop
fi
# Run the server in the background
python3 ./integration_tests/base_routes.py &
sleep 1
# oha will display benchmark results
oha -n "$number" http://localhost:8080/sync
# Kill subprocesses after exiting the script (python + robyn server)
# (see https://stackoverflow.com/questions/360201/how-do-i-kill-background-processes-jobs-when-my-shell-script-exits)
trap "trap - TERM && kill 0" INT TERM EXIT
================================================
FILE: ci-local.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
FAILED=()
PASSED=()
SKIPPED=()
run_step() {
local name="$1"
shift
echo -e "\n${CYAN}── $name ──${NC}"
echo -e "${YELLOW}$ $*${NC}"
if "$@"; then
PASSED+=("$name")
echo -e "${GREEN}✓ $name${NC}"
else
FAILED+=("$name")
echo -e "${RED}✗ $name${NC}"
fi
}
skip_step() {
local name="$1"
local reason="$2"
SKIPPED+=("$name ($reason)")
echo -e "\n${YELLOW}── $name [SKIPPED: $reason] ──${NC}"
}
usage() {
echo "Usage: $0 [rust|lint|python|all|fix]"
echo ""
echo "Mirrors the GitHub Actions CI workflows locally."
echo ""
echo " rust Rust CI: cargo check, test, fmt --check, clippy"
echo " lint Lint PR: ruff check, isort --check-only"
echo " python Python CI: nox test suite (current Python version)"
echo " all Everything (default)"
echo " fix Auto-fix: cargo fmt, ruff --fix, isort"
exit 0
}
# ── rust-CI.yml ───────────────────────────────────────────────────────────────
run_rust() {
echo -e "\n${CYAN}═══ Rust CI (.github/workflows/rust-CI.yml) ═══${NC}"
run_step "cargo check" cargo check
run_step "cargo test" cargo test
run_step "cargo fmt" cargo fmt --check
run_step "cargo clippy" cargo clippy
}
# ── lint-pr.yml ───────────────────────────────────────────────────────────────
run_lint() {
echo -e "\n${CYAN}═══ Lint PR (.github/workflows/lint-pr.yml) ═══${NC}"
if command -v ruff &>/dev/null; then
run_step "ruff check" ruff check .
else
skip_step "ruff check" "ruff not installed (pip install ruff)"
fi
if command -v isort &>/dev/null; then
run_step "isort check" isort --check-only --diff .
else
skip_step "isort check" "isort not installed (pip install isort)"
fi
}
# ── python-CI.yml ─────────────────────────────────────────────────────────────
run_python() {
echo -e "\n${CYAN}═══ Python CI (.github/workflows/python-CI.yml) ═══${NC}"
local pyver
pyver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
if command -v nox &>/dev/null; then
run_step "nox (python $pyver)" nox --non-interactive --error-on-missing-interpreter -p "$pyver"
else
skip_step "nox tests" "nox not installed (pip install nox)"
fi
}
# ── fix mode ──────────────────────────────────────────────────────────────────
run_fix() {
echo -e "\n${CYAN}═══ Auto-fix ═══${NC}"
run_step "cargo fmt" cargo fmt
command -v ruff &>/dev/null && run_step "ruff fix" ruff check --fix . || skip_step "ruff fix" "not installed"
command -v isort &>/dev/null && run_step "isort fix" isort . || skip_step "isort fix" "not installed"
}
# ── main ──────────────────────────────────────────────────────────────────────
MODE="${1:-all}"
case "$MODE" in
rust) run_rust ;;
lint) run_lint ;;
python) run_python ;;
fix) run_fix ;;
all) run_rust; run_lint; run_python ;;
-h|--help|help) usage ;;
*) echo "Unknown mode: $MODE"; usage ;;
esac
# ── summary ───────────────────────────────────────────────────────────────────
echo -e "\n${CYAN}═══ Summary ═══${NC}"
for s in "${PASSED[@]+"${PASSED[@]}"}"; do echo -e " ${GREEN}✓${NC} $s"; done
for s in "${SKIPPED[@]+"${SKIPPED[@]}"}"; do echo -e " ${YELLOW}⊘${NC} $s"; done
for s in "${FAILED[@]+"${FAILED[@]}"}"; do echo -e " ${RED}✗${NC} $s"; done
if [ ${#FAILED[@]} -gt 0 ]; then
echo -e "\n${RED}CI would fail: ${#FAILED[@]} check(s) failed.${NC}"
exit 1
else
echo -e "\n${GREEN}All checks passed. Safe to push.${NC}"
exit 0
fi
================================================
FILE: docs_src/.eslintrc.json
================================================
{
"extends": ["next","next/core-web-vitals"],
"rules": {
"react/no-unescaped-entities": "off"
}
}
================================================
FILE: docs_src/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# generated files
/public/rss/
================================================
FILE: docs_src/README.md
================================================
## Docs Base
This is the documentation website that will be used as a base for Robyn and Starfyre docs.
## Setup
1. Clone this repo
2. Run `npm install`
3. Run `npm run dev`
4. Open `http://localhost:3000` in your browser
================================================
FILE: docs_src/jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
================================================
FILE: docs_src/mdx/recma.mjs
================================================
import { mdxAnnotations } from 'mdx-annotations'
import recmaNextjsStaticProps from 'recma-nextjs-static-props'
import { recmaImportImages } from 'recma-import-images'
function recmaRemoveNamedExports() {
return (tree) => {
tree.body = tree.body.map((node) => {
if (node.type === 'ExportNamedDeclaration') {
return node.declaration
}
return node
})
}
}
export const recmaPlugins = [
mdxAnnotations.recma,
recmaRemoveNamedExports,
recmaNextjsStaticProps,
recmaImportImages,
]
================================================
FILE: docs_src/mdx/rehype.mjs
================================================
import { mdxAnnotations } from 'mdx-annotations'
import { visit } from 'unist-util-visit'
import rehypeMdxTitle from 'rehype-mdx-title'
import shiki from 'shiki'
import { toString } from 'mdast-util-to-string'
import * as acorn from 'acorn'
import { slugifyWithCounter } from '@sindresorhus/slugify'
import rehypeSlug from 'rehype-slug'
import { remarkRehypeWrap } from 'remark-rehype-wrap'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
function rehypeParseCodeBlocks() {
return (tree) => {
visit(tree, 'element', (node, _nodeIndex, parentNode) => {
if (node.tagName === 'code' && node.properties.className) {
parentNode.properties.language = node.properties.className[0]?.replace(
/^language-/,
''
)
}
})
}
}
let highlighter
function rehypeShiki() {
return async (tree) => {
highlighter =
highlighter ?? (await shiki.getHighlighter({ theme: 'css-variables' }))
visit(tree, 'element', (node) => {
if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') {
let codeNode = node.children[0]
let textNode = codeNode.children[0]
node.properties.code = textNode.value
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(
textNode.value,
node.properties.language
)
textNode.value = shiki.renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `${children}`,
},
})
}
}
})
}
}
function rehypeSlugify() {
return (tree) => {
let slugify = slugifyWithCounter()
visit(tree, 'element', (node) => {
if (node.tagName === 'h2' && !node.properties.id) {
node.properties.id = slugify(toString(node))
}
})
}
}
function rehypeAddMDXExports(getExports) {
return (tree) => {
let exports = Object.entries(getExports(tree))
for (let [name, value] of exports) {
for (let node of tree.children) {
if (
node.type === 'mdxjsEsm' &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
) {
return
}
}
let exportStr = `export const ${name} = ${value}`
tree.children.push({
type: 'mdxjsEsm',
value: exportStr,
data: {
estree: acorn.parse(exportStr, {
sourceType: 'module',
ecmaVersion: 'latest',
}),
},
})
}
}
}
function getSections(node) {
let sections = []
for (let child of node.children ?? []) {
if (child.type === 'element' && child.tagName === 'h2') {
sections.push(`{
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
...${child.properties.annotation}
}`)
} else if (child.children) {
sections.push(...getSections(child))
}
}
return sections
}
export const rehypePlugins = [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap', test: ['h2'] }],
[
remarkRehypeWrap,
{
node: { type: 'element', tagName: 'article' },
start: 'element[tagName=hr]',
transform: (article) => {
article.children.splice(0, 1)
let heading = article.children.find((n) => n.tagName === 'h2')
if (heading) {
article.properties = { ...heading.properties, title: toString(heading) }
heading.properties = {}
} else {
article.properties = {}
}
return article
},
},
],
mdxAnnotations.rehype,
rehypeParseCodeBlocks,
rehypeShiki,
rehypeSlugify,
rehypeMdxTitle,
[
rehypeAddMDXExports,
(tree) => ({
sections: `[${getSections(tree).join()}]`,
}),
],
]
================================================
FILE: docs_src/mdx/remark.mjs
================================================
import { mdxAnnotations } from 'mdx-annotations'
import remarkGfm from 'remark-gfm'
import remarkUnwrapImages from 'remark-unwrap-images'
export const remarkPlugins = [
mdxAnnotations.remark,
remarkGfm,
remarkUnwrapImages,
]
================================================
FILE: docs_src/next.config.mjs
================================================
import nextMDX from '@next/mdx'
import { remarkPlugins } from './mdx/remark.mjs'
import { rehypePlugins } from './mdx/rehype.mjs'
import { recmaPlugins } from './mdx/recma.mjs'
const withMDX = nextMDX({
options: {
remarkPlugins,
rehypePlugins,
recmaPlugins,
providerImportSource: '@mdx-js/react',
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
experimental: {
scrollRestoration: true,
},
i18n: {
locales: ['en', 'zh'],
defaultLocale: 'en',
localeDetection: false,
},
async redirects() {
return [
{
source: '/documentation',
destination: '/documentation/en',
permanent: false,
},
]
},
}
export default withMDX(nextConfig)
================================================
FILE: docs_src/package.json
================================================
{
"name": "tailwindui-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@algolia/autocomplete-core": "^1.9.3",
"@algolia/autocomplete-preset-algolia": "^1.9.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5",
"@next/mdx": "^13.0.2",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.4",
"@vercel/analytics": "^1.0.2",
"algoliasearch": "^4.17.2",
"autoprefixer": "^10.4.12",
"axios": "^1.4.0",
"clsx": "^1.2.1",
"fast-glob": "^3.2.11",
"feed": "^4.2.2",
"focus-visible": "^5.2.0",
"framer-motion": "^10.12.16",
"highlight.js": "^11.8.0",
"mdx-annotations": "^0.1.3",
"meilisearch": "^0.33.0",
"next": "13.4.2",
"next-mdx-remote": "^6.0.0",
"next-router-mock": "^0.9.3",
"postcss-focus-visible": "^6.0.4",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"recma-import-images": "^0.0.3",
"recma-nextjs-static-props": "^1.0.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-mdx-title": "^2.0.0",
"rehype-slug": "^5.1.0",
"remark-gfm": "^3.0.1",
"remark-rehype-wrap": "^0.0.2",
"remark-unwrap-images": "^3.0.1",
"shiki": "^0.14.2",
"tailwindcss": "^3.3.0",
"zustand": "^4.3.8"
},
"devDependencies": {
"eslint": "8.26.0",
"eslint-config-next": "13.0.2",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.6"
}
}
================================================
FILE: docs_src/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
'postcss-focus-visible': {
replaceWith: '[data-focus-visible-added]',
},
autoprefixer: {},
},
}
================================================
FILE: docs_src/prettier.config.js
================================================
module.exports = {
singleQuote: true,
semi: false,
plugins: [require('prettier-plugin-tailwindcss')],
}
================================================
FILE: docs_src/public/funding.json
================================================
{
"version": "v1.0.0",
"entity": {
"type": "individual",
"role": "owner",
"name": "Sanskar Jethi",
"email": "sansyrox@gmail.com",
"phone": "",
"description": "Sanskar is a FOSS engineer who created Robyn and Starfyre. Sanskar has created software for over half his life and used it for almost all of his.",
"webpageUrl": {
"url": "https://robyn.tech/"
}
},
"projects": [
{
"guid": "robyn",
"name": "robyn",
"description": "Robyn is one of the fastest Python web frameworks, which comes with a built in web server and a Rust runtime.",
"webpageUrl": {
"url": "https://robyn.tech/"
},
"repositoryUrl": {
"url": "https://github.com/sparckles/Robyn",
"wellKnown": "https://github.com/sparckles/Robyn/blob/main/.well-known/funding-manifest-urls"
},
"licenses": ["BSD 2-Clause \"Simplified\" License"],
"tags": ["programming", "python", "rust", "web", "backend", "async"]
}
],
"funding": {
"channels": [
{
"guid": "mybank",
"type": "bank",
"address": "",
"description": "Send me an email to get my bank details"
}
],
"plans": [
{
"guid": "mybank",
"status": "active",
"name": "Support Maintainer Part Time",
"description": "Support the maintainer for his work on Robyn part time.",
"amount": 500,
"currency": "GBP",
"frequency": "monthly",
"channels": ["mybank"]
},
{
"guid": "mybank",
"status": "active",
"name": "Support Maintainer Full Time",
"description": "Support the maintainer for his work on Robyn full time.",
"amount": 3000,
"currency": "GBP",
"frequency": "monthly",
"channels": ["mybank"]
},
{
"guid": "150",
"status": "active",
"name": " Support Contributor Part Time",
"description": "Support for one contributor per month",
"amount": 150,
"currency": "GBP",
"frequency": "monthly",
"channels": ["mybank"]
},
{
"guid": "500",
"status": "active",
"name": " Support Contributor Full Time",
"description": "Support for one contributor per month",
"amount": 500,
"currency": "GBP",
"frequency": "monthly",
"channels": ["mybank"]
}
],
"history": []
}
}
================================================
FILE: docs_src/public/llms.txt
================================================
# Robyn
> Robyn is a high-performance, community-driven, and innovator-friendly async web framework for Python with a Rust runtime. It combines Python's ease of use with Rust's performance.
## Quick Facts
- Version: 0.79.0
- Python: >= 3.10
- License: BSD 2.0
- Repository: https://github.com/sparckles/robyn
- Documentation: https://robyn.tech/documentation
- Discord: https://discord.gg/rkERZ5eNU8
## Installation
```bash
pip install robyn
```
## Basic Usage
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def index(request):
return "Hello, World!"
app.start(port=8080)
```
## Key Features
- **Rust Runtime**: Core server written in Rust using actix-web for high performance
- **Async/Sync Support**: Both async and sync route handlers supported
- **Multi-Process Scaling**: Built-in multiprocess execution via `--processes` and `--workers`
- **WebSockets**: Native WebSocket support
- **Middlewares**: Before/after request middlewares
- **Dependency Injection**: Built-in DI system
- **OpenAPI/Swagger**: Automatic OpenAPI documentation generation
- **Hot Reloading**: Development mode with `--dev` flag
- **AI Agents**: Built-in AI agent routing via `robyn.ai`
- **MCP Support**: Model Context Protocol server capabilities via `app.mcp`
- **Templating**: Jinja2 templating support (optional)
- **CORS**: Built-in CORS helper via `ALLOW_CORS()`
- **Authentication**: AuthenticationHandler base class for custom auth
- **Static Files**: Directory serving via `app.serve_directory()`
- **SSE**: Server-Sent Events support via `SSEResponse`
- **Easy Access Parameters**: Typed path/query params with automatic coercion in handler signatures
- **Direct Rust Integration**: Embed Rust code directly in routes
## Project Structure
```
robyn/
├── src/ # Rust source code
│ ├── lib.rs # PyO3 module entry point
│ ├── server.rs # Main HTTP server implementation
│ ├── types/ # Request, Response, Headers, Cookie types
│ ├── routers/ # HTTP, WebSocket, middleware routers
│ ├── executors/ # Route execution handlers
│ └── websockets/ # WebSocket implementation
├── robyn/ # Python package
│ ├── __init__.py # Main Robyn and SubRouter classes
│ ├── router.py # Python router implementation
│ ├── authentication.py # AuthenticationHandler
│ ├── dependency_injection.py
│ ├── openapi.py # OpenAPI generation
│ ├── mcp.py # MCP protocol support
│ ├── ai.py # AI agent support
│ ├── responses.py # Response helpers (serve_file, html, SSE)
│ ├── ws.py # WebSocket class
│ └── robyn.pyi # Type stubs
├── integration_tests/ # Integration test suite
├── unit_tests/ # Unit test suite
├── docs_src/ # Documentation (Next.js)
├── granian/ # Bundled Granian server (fork)
└── examples/ # Example applications
```
## Core Classes
### Robyn / SubRouter
Main application class and sub-router for modular routes.
```python
from robyn import Robyn, SubRouter
app = Robyn(__file__)
api = SubRouter(__file__, prefix="/api")
@api.get("/users")
def get_users(request):
return {"users": []}
app.include_router(api)
```
### Request Object
```python
request.method # HTTP method
request.url # Url object (scheme, host, path)
request.headers # Headers dict-like
request.query_params # QueryParams
request.path_params # Dict of URL params
request.body # Raw bytes
request.json() # Parse JSON body
request.form_data # Multipart form data
request.ip_addr # Client IP
request.identity # Identity (if authenticated)
```
### Response Object
```python
from robyn import Response
Response(
status_code=200,
headers={"Content-Type": "application/json"},
description="body content" # or body bytes
)
```
### Decorators
```python
@app.get("/path")
@app.post("/path")
@app.put("/path")
@app.delete("/path")
@app.patch("/path")
@app.head("/path")
@app.options("/path")
@app.before_request("/path") # Middleware before
@app.after_request("/path") # Middleware after
@app.startup_handler # Server startup
@app.shutdown_handler # Server shutdown
```
### WebSockets
```python
from robyn import WebSocketDisconnect
@app.websocket("/ws")
async def handler(websocket):
try:
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"Echo: {msg}")
except WebSocketDisconnect:
pass
@handler.on_connect
def on_connect(websocket):
return "Connected"
@handler.on_close
def on_close(websocket):
return "Closed"
```
### Easy Access Parameters
Declare typed path and query parameters directly in handler signatures. Works for both HTTP and WebSocket handlers.
```python
from typing import List, Optional
# HTTP: path params + query params with type coercion
@app.get("/items/:id")
async def get_item(id: int, q: str, page: int = 1):
return {"id": id, "q": q, "page": page}
# Optional, List, and bool params
@app.get("/search")
def search(name: str, tags: List[str], active: bool = False, age: Optional[int] = None):
return {"name": name, "tags": tags, "active": active, "age": age}
# WebSocket: typed query params on handler and callbacks
@app.websocket("/ws")
async def handler(websocket, room: str = "default", page: int = 1):
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"room={room} page={page} msg={msg}")
@handler.on_connect
def on_connect(websocket, room: str = "default"):
return f"connected to {room}"
```
### MCP (Model Context Protocol)
```python
@app.mcp.resource("time://current")
def get_time():
return datetime.now().isoformat()
@app.mcp.tool(name="calc", description="Calculate", input_schema={...})
def calculate(args):
return eval(args["expression"])
@app.mcp.prompt(name="explain", description="Explain code", arguments=[...])
def explain_prompt(args):
return f"Please explain: {args['code']}"
```
## CLI Commands
```bash
python app.py # Start server
python app.py --dev # Development mode (hot reload)
python app.py --processes 4 # Multi-process
python app.py --workers 2 # Workers per process
python app.py --log-level DEBUG # Log level
python app.py --open-browser # Open browser on start
python app.py --create # Create new project scaffold
python app.py --docs # Open documentation
```
## Development Setup
```bash
# Clone
git clone https://github.com/sparckles/robyn.git
cd robyn
# Virtual environment
python3 -m venv .venv && source .venv/bin/activate
# Install tools
pip install pre-commit poetry maturin
# Install dependencies
poetry install --with dev --with test
# Build Rust extension
maturin develop
# Run tests
pytest
```
## Key Dependencies
- **PyO3**: Rust-Python bindings
- **actix-web**: Rust HTTP server (via cookie crate)
- **orjson**: Fast JSON serialization
- **multiprocess**: Multi-process support
- **uvloop**: Fast event loop (non-Windows)
- **watchdog**: File watching for hot reload
## Configuration
Environment variables:
- `ROBYN_HOST`: Server host (default: 127.0.0.1)
- `ROBYN_PORT`: Server port (default: 8080)
- `ROBYN_DEV_MODE`: Enable dev mode
- `ROBYN_BROWSER_OPEN`: Open browser on start
- `ROBYN_CLIENT_TIMEOUT`: Client timeout seconds
- `ROBYN_KEEP_ALIVE_TIMEOUT`: Keep-alive timeout
## Documentation Structure
Main docs at `docs_src/src/pages/documentation/`:
- `api_reference/getting_started.mdx` - Quick start guide
- `api_reference/request_object.mdx` - Request handling
- `api_reference/middlewares.mdx` - Middleware usage
- `api_reference/websockets.mdx` - WebSocket guide
- `api_reference/authentication.mdx` - Auth patterns
- `api_reference/openapi.mdx` - OpenAPI docs
- `api_reference/agents.mdx` - AI agent integration
- `api_reference/mcps.mdx` - MCP server guide
- `example_app/` - Full example application tutorial
================================================
FILE: docs_src/src/components/Button.jsx
================================================
import Link from 'next/link'
import clsx from 'clsx'
const variantStyles = {
primary:
'font-semibold text-zinc-100 bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-700 active:text-zinc-100/70',
secondary:
'font-medium bg-zinc-800/50 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-50 active:bg-zinc-800/50 active:text-zinc-50/70',
}
export function Button({ variant = 'primary', className, href, ...props }) {
className = clsx(
'inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none',
variantStyles[variant],
className
)
return href ? (
) : (
)
}
================================================
FILE: docs_src/src/components/Card.jsx
================================================
import Link from 'next/link'
import clsx from 'clsx'
function ChevronRightIcon(props) {
return (
)
}
export function Card({ as: Component = 'div', className, children }) {
return (
{children}
)
}
Card.Link = function CardLink({ children, ...props }) {
return (
<>
{children}
>
)
}
Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
return (
{href ? {children} : children}
)
}
Card.Description = function CardDescription({ children }) {
return
{children}
}
Card.Cta = function CardCta({ children }) {
return (
}
================================================
FILE: docs_src/src/components/Section.jsx
================================================
import { useId } from 'react'
export function Section({ title, children }) {
let id = useId()
return (
{title}
{children}
)
}
================================================
FILE: docs_src/src/components/SimpleLayout.jsx
================================================
import { Container } from '@/components/Container'
export function SimpleLayout({ title, intro, children }) {
return (
{title}
{intro}
{children}
)
}
================================================
FILE: docs_src/src/components/SocialIcons.jsx
================================================
export function TwitterIcon(props) {
return (
)
}
export function InstagramIcon(props) {
return (
)
}
export function GitHubIcon(props) {
return (
)
}
export function LinkedInIcon(props) {
return (
)
}
export function DiscordIcon(props) {
return (
)
}
================================================
FILE: docs_src/src/components/Testimonials.jsx
================================================
import { useEffect } from 'react'
let testimonials = [
{
body: "Robyn has revolutionized the way I develop web solutions. Its seamless integration of Python's async capabilities with a Rust runtime not only ensures reliability and scalability but also provides quick project setup, a delightful user experience, and robust plugin support. With its exceptional speed and multithreaded efficiency, Robyn's real-time communication through WebSockets and dynamic URL routing has empowered me to create highly performant and interactive applications while maintaining full control over navigation and workflows. A game-changer for modern web development!",
author: {
name: 'Kunal Kushwaha',
handle: 'kunalstwt',
imageUrl: '/testimonials/kunalstwt.jpg',
title: 'DevRel manager at Civo',
},
},
{
body: "Having worked with a company building a Rust based open source search engine for over a year, I strongly believe in the notion that rewriting software with Rust can significantly improve software performance. Sanskar's idea to recreate Flask with Rust, was just incredible. Having used Robyn myself, it is refreshing to see such a performant Python framework and just the amazing developer ecosystem around it. Yes it still new and being developed, but I can say this with confidence that given the underlying Rust-based multithreaded run time will provide immense performance for running high throughout applications. I am glad to be one of the early sponsors and adopters for Robyn!",
author: {
name: 'Shivay Lamba',
handle: 'howdevelop',
imageUrl: '/testimonials/howdevelop.jpg',
title: 'Developer Experience Engineer at MeiliSearch',
},
},
{
body: "I'm impressed with Robyn. It's a fast asynchronous web framework for the Python ecosystem that's built on top of Rust. The syntax is similar to other popular web frameworks, so it's easy to learn and be productive with. I've been using it to build web applications and services, and I'm really happy with the results. I'm also impressed with the Robyn community. They are very supportive and the developers are very responsive to feedback",
author: {
name: 'Carlos A. Marcano Vargas',
handle: 'carlos_marcv',
imageUrl: '/testimonials/carlos_marcv.jpg',
title: 'Technical Writer',
},
},
// More testimonials...
{
body: 'Great to see a Community Driven Open Source project, achieve new heights! Robyn is built by the community for the community',
author: {
name: 'Eddie Jaoude',
handle: 'eddiejaoude',
imageUrl: '/testimonials/eddiejaoude.jpg',
title: 'Creator of EddieHub',
},
},
// More testimonials...
{
body: 'I used to be a Batman fan, but having met Robyn I now think the sidekick has become the hero. Free, OSS, straight forward and powerful, what is not to love?',
author: {
name: 'GrahamTheDev',
handle: 'GrahamTheDev',
imageUrl: '/testimonials/GrahamTheDev.jpg',
title: 'The Accessibility First DevRel ',
},
},
{
body: 'Having used both, Flask and Django for writing web applications in Python in the past, Robyn looks like their combined successor in terms of ergonomics and features available. Its reliance on a Rust runtime for performance and security is the cherry on the cake!',
author: {
name: 'Daniel Bodky',
handle: 'd_bodky',
imageUrl: '/testimonials/d_bodky.jpg',
title: 'Consultant, Trainer, Speaker @NETWAYS',
},
},
// More testimonials...
{
body: 'Robyn has made a big difference in my projects. Its flexible structure allows my work to adapt smoothly to my needs, even when I face complex challenges. The community-driven and open-source nature of Robyn makes it a welcoming place for developers like me. Plus, its simple yet powerful API has greatly streamlined my development process, reducing my wor oad. I highly recommend it!',
author: {
name: 'Julia Furst Morgado',
handle: 'juliafmorgado',
imageUrl: '/testimonials/juliafmorgado.jpg',
title: 'Global technologist @Veeam',
},
},
// More testimonials...
{
body: 'Robyn opens a new chapter in the Python web frameworks scene: the Rust powered one, where performance and safety are not the sole protagonists.',
author: {
name: 'Giovanni Barillari',
handle: 'gi0baro',
imageUrl: '/testimonials/gi0baro.jpeg',
title: 'Author of Granian and Emmett',
},
},
// More testimonials...
{
body: "I collaborate with Robyn's team and I must say, Sanskar does an excellent job maintaining the community. The project as a whole is immensely beneficial, both for collaboration and its practical uses. The tool is impressive, easy to use and the entire community is very welcoming to first-time contributors",
author: {
name: 'Jyoti Bisht',
handle: 'joeyousss',
imageUrl: '/testimonials/joeyousss.jpg',
title: 'Open Source Developer',
},
},
// More testimonials...
{
body: "Robyn is a breath of fresh air in web development. Merging Python's simplicity with Rust's speed, it offers a seamless experience for developers. Its features are precisely what today's web projects need. I'm particularly impressed with its community focus; it's evident that everyone's voice matters in shaping Robyn's journey. In a sea of web frameworks, Robyn stands out not just for its tech but also for its heart.",
author: {
name: 'Francesco Ciulla',
handle: 'FrancescoCiull4',
imageUrl: '/testimonials/FrancescoCiull4.jpg',
title: 'DevRel at daily.dev',
},
},
// More testimonials...
]
//shuflle testimonials
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
arr.slice(i * size, i * size + size)
)
export default function Testimonials() {
let chunkedTestimonials = [];
// Separate testimonials into groups of 3, 4, and 3
const firstGroup = chunk(testimonials.slice(0, 3), 3);
const secondGroup = chunk(testimonials.slice(3, 7), 4);
const thirdGroup = chunk(testimonials.slice(7), 3);
// Concatenate the groups in the desired order
chunkedTestimonials = firstGroup.concat(secondGroup, thirdGroup);
return (
Testimonials
Some amazing people have said nice things about us.
)
}
================================================
FILE: docs_src/src/components/documentation/ApiDocs.jsx
================================================
import { Button } from '@/components/documentation/Button'
import { Heading } from '@/components/documentation/Heading'
const guides = [
{
href: '/documentation/en/api_reference',
name: 'Installation',
description: 'Start using Robyn in your project.',
},
{
href: '/documentation/en/api_reference/getting_started',
name: 'Getting Started',
description: 'Start with creating basic routes in Robyn.',
},
{
href: '/documentation/en/api_reference/request_object',
name: 'The Request Object',
description: 'Learn about the Request Object in Robyn.',
},
{
href: '/documentation/en/api_reference/robyn_env',
name: 'The Robyn Env file',
description: 'Learn about the Robyn variables',
},
{
href: '/documentation/en/api_reference/middlewares',
name: 'Middlewares, Events and Websockets',
description: 'Learn about Middlewares, Events and Websockets in Robyn.',
},
{
href: '/documentation/en/api_reference/authentication',
name: 'Authentication',
description: 'Learn about Authentication in Robyn.',
},
{
href: '/documentation/en/api_reference/const_requests',
name: 'Const Requests and Multi Core Scaling',
description: 'Learn about Const Requests and Multi Core Scaling in Robyn.',
},
{
href: '/documentation/en/api_reference/cors',
name: 'CORS',
description: 'CORS',
},
{
href: '/documentation/en/api_reference/templating',
name: 'Templating',
description: 'Learn about Templating in Robyn.',
},
{
href: '/documentation/en/api_reference/redirection',
name: 'Redirection',
description: 'Learn how to redirect requests to different endpoints.',
},
{
href: '/documentation/en/api_reference/file-uploads',
name: 'File Uploads',
description:
'Learn how to upload and download files to your server using Robyn.',
},
{
href: '/documentation/en/api_reference/form_data',
name: 'Form Data and Multi Part Form Data',
description: 'Learn how to handle form data.',
},
{
href: '/documentation/en/api_reference/websockets',
name: 'Websockets',
description: 'Learn how to use Websockets in Robyn.',
},
{
href: '/documentation/en/api_reference/server_sent_events',
name: 'Server-Sent Events',
description: 'Learn how to implement Server-Sent Events for real-time communication.',
},
{
href: '/documentation/en/api_reference/exceptions',
name: 'Exceptions',
description: 'Learn how to handle exceptions in Robyn.',
},
{
href: '/documentation/en/api_reference/scaling',
name: 'Scaling the Application',
description: 'Learn how to scaled Robyn across multiple cores.',
},
{
href: '/documentation/en/api_reference/advanced_features',
name: 'Advanced Features',
description: 'Learn about advanced features in Robyn.',
},
{
href: '/documentation/en/api_reference/multiprocess_execution',
name: 'Multiprocess Execution',
description: 'Learn about the behaviour or variables during multithreading',
},
{
href: '/documentation/en/api_reference/using_rust_directly',
name: 'Direct Rust Usage',
description: 'Learn about directly using Rust in Robyn.',
},
{
href: '/documentation/en/api_reference/graphql-support',
name: 'GraphQL Support',
description: 'Learn about GraphQL Support in Robyn.',
},
{
href: '/documentation/en/api_reference/openapi',
name: 'OpenAPI Documentation',
description: 'Learn how to generate OpenAPI docs for your applications.',
},
{
href: '/documentation/en/api_reference/dependency_injection',
name: 'Dependency Injection',
description: 'Learn about Dependency Injection in Robyn.',
},
]
export function ApiDocs() {
return (
Api Docs
{guides.map((guide) => (
{guide.name}
{guide.description}
))}
)
}
================================================
FILE: docs_src/src/components/documentation/BottomNavbar.jsx
================================================
import { forwardRef } from 'react'
import Link from 'next/link'
import clsx from 'clsx'
import { motion, useScroll, useTransform } from 'framer-motion'
import { MobileNavigation } from '@/components/documentation/MobileNavigation'
import { useMobileNavigationStore } from '@/components/documentation/MobileNavigation'
import { MobileSearch, Search } from '@/components/documentation/Search'
export const BottomNavbar = forwardRef(function Header({ className }, ref) {
let { scrollY } = useScroll()
let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9])
let bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8])
return (
<>
)
}
================================================
FILE: docs_src/src/components/documentation/Libraries.jsx
================================================
import Image from 'next/image'
import { Button } from '@/components/documentation/Button'
import { Heading } from '@/components/documentation/Heading'
const libraries = [
{
href: '#',
name: 'PHP',
description:
'A popular general-purpose scripting language that is especially suited to web development.',
},
{
href: '#',
name: 'Ruby',
description:
'A dynamic, open source programming language with a focus on simplicity and productivity.',
},
{
href: '#',
name: 'Node.js',
description:
'Node.js® is an open-source, cross-platform JavaScript runtime environment.',
},
{
href: '#',
name: 'Python',
description:
'Python is a programming language that lets you work quickly and integrate systems more effectively.',
},
{
href: '#',
name: 'Go',
description:
'An open-source programming language supported by Google with built-in concurrency.',
},
]
export function Libraries() {
return (
Official libraries
{libraries.map((library) => (
{library.name}
{library.description}
))}
)
}
================================================
FILE: docs_src/src/components/documentation/MobileNavigation.jsx
================================================
import { createContext, Fragment, useContext } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { motion } from 'framer-motion'
import { create } from 'zustand'
import { BottomNavbar } from '@/components/documentation/BottomNavbar'
import { Navigation } from '@/components/documentation/Navigation'
function MenuIcon(props) {
return (
)
}
function XIcon(props) {
return (
)
}
const IsInsideMobileNavigationContext = createContext(false)
export function useIsInsideMobileNavigation() {
return useContext(IsInsideMobileNavigationContext)
}
export const useMobileNavigationStore = create((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}))
export function MobileNavigation() {
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let { isOpen, toggle, close } = useMobileNavigationStore()
let ToggleIcon = isOpen ? XIcon : MenuIcon
return (
{!isInsideMobileNavigation && (
)}
)
}
================================================
FILE: docs_src/src/components/documentation/ModeToggle.jsx
================================================
function SunIcon(props) {
return (
)
}
function MoonIcon(props) {
return (
)
}
================================================
FILE: docs_src/src/components/documentation/Navigation.jsx
================================================
import { useRef } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import clsx from 'clsx'
import { AnimatePresence, motion, useIsPresent } from 'framer-motion'
import { useIsInsideMobileNavigation } from '@/components/documentation/MobileNavigation'
import { useSectionStore } from '@/components/documentation/SectionProvider'
import { Tag } from '@/components/documentation/Tag'
import LanguageSelector from '@/components/documentation/LanguageSelector'
import { remToPx } from '@/lib/remToPx'
function useInitialValue(value, condition = true) {
let initialValue = useRef(value).current
return condition ? initialValue : value
}
function TopLevelNavItem({ href, children }) {
return (
{children}
)
}
function NavLink({ href, tag, active, isAnchorLink = false, children }) {
return (
{children}
{tag && (
{tag}
)}
)
}
function VisibleSectionHighlight({ group, pathname }) {
let [sections, visibleSections] = useInitialValue(
[
useSectionStore((s) => s.sections),
useSectionStore((s) => s.visibleSections),
],
useIsInsideMobileNavigation()
)
let isPresent = useIsPresent()
let firstVisibleSectionIndex = Math.max(
0,
[{ id: '_top' }, ...sections].findIndex(
(section) => section.id === visibleSections[0]
)
)
let itemHeight = remToPx(2)
let height = isPresent
? Math.max(1, visibleSections.length) * itemHeight
: itemHeight
let top =
group.links.findIndex((link) => link.href === pathname) * itemHeight +
firstVisibleSectionIndex * itemHeight
return (
)
}
function ActivePageMarker({ group, pathname }) {
let itemHeight = remToPx(2)
let offset = remToPx(0.25)
let activePageIndex = group.links.findIndex((link) => link.href === pathname)
let top = offset + activePageIndex * itemHeight
return (
)
}
function NavigationGroup({ group, className }) {
// If this is the mobile navigation then we always render the initial
// state, so that the state does not change during the close animation.
// The state will still update when we re-open (re-render) the navigation.
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let [router, sections] = useInitialValue(
[useRouter(), useSectionStore((s) => s.sections)],
isInsideMobileNavigation
)
let isActiveGroup =
group.links.findIndex((link) => link.href === router.pathname) !== -1
return (
{
if (
event.key === 'Escape' &&
!autocompleteState.isOpen &&
autocompleteState.query === ''
) {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
document.activeElement?.blur()
onClose()
} else {
inputProps.onKeyDown(event)
}
}}
/>
{autocompleteState.status === 'stalled' && (
)
}
export function Properties({ children }) {
return (
{children}
)
}
export function Property({ name, type, children }) {
return (
Name
{name}
Type
{type}
Description
{children}
)
}
================================================
FILE: docs_src/src/components/releases/Button.jsx
================================================
import Link from 'next/link'
import clsx from 'clsx'
function ButtonInner({ arrow = false, children }) {
return (
<>
{children} {arrow ? → : null}
>
)
}
export function Button({ href, className, arrow, children, ...props }) {
className = clsx(
className,
'group relative isolate flex-none rounded-md py-1.5 text-[0.8125rem]/6 font-semibold text-white',
arrow ? 'pl-2.5 pr-[calc(9/16*1rem)]' : 'px-2.5'
)
return href ? (
{children}
) : (
)
}
================================================
FILE: docs_src/src/components/releases/FeedProvider.jsx
================================================
import { createContext, useContext } from 'react'
let FeedContext = createContext({ isFeed: false })
export function FeedProvider({ children }) {
return (
{children}
)
}
export function useFeed() {
return useContext(FeedContext)
}
================================================
FILE: docs_src/src/components/releases/FormattedDate.jsx
================================================
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
export function FormattedDate({ date, ...props }) {
date = typeof date === 'string' ? new Date(date) : date
return (
)
}
================================================
FILE: docs_src/src/components/releases/IconLink.jsx
================================================
import Link from 'next/link'
import clsx from 'clsx'
export function IconLink({
children,
className,
compact = false,
large = false,
icon: Icon,
...props
}) {
return (
{children}
)
}
================================================
FILE: docs_src/src/components/releases/Intro.jsx
================================================
import { IconLink } from '@/components/releases/IconLink'
import { SignUpForm } from '@/components/releases/SignUpForm'
import {
InstagramIcon,
LinkedInIcon,
TwitterIcon,
} from '@/components/SocialIcons'
function BookIcon(props) {
return (
)
}
function GitHubIcon(props) {
return (
)
}
function FeedIcon(props) {
return (
)
}
export function Intro() {
return (
<>
All the latest Robyn releases,
right here.
Twitter
Release Page
>
)
}
================================================
FILE: docs_src/src/components/releases/Layout.jsx
================================================
import { useId } from 'react'
import { Intro } from '@/components/releases/Intro'
function Timeline() {
let id = useId()
return (
)
}
function FixedSidebar({ main }) {
return (
{main}
)
}
export function Layout({ children }) {
return (
<>
} />
{children}
>
)
}
================================================
FILE: docs_src/src/components/releases/SignUpForm.jsx
================================================
import { useId } from 'react'
import { Button } from '@/components/Button'
export function SignUpForm() {
let id = useId()
return (
)
}
================================================
FILE: docs_src/src/components/releases/mdx.jsx
================================================
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { useFeed } from '@/components/releases/FeedProvider'
import { FormattedDate } from '@/components/releases/FormattedDate'
export const a = Link
export const wrapper = function Wrapper({ children }) {
return children
}
export function H2(props) {
let { isFeed } = useFeed()
if (isFeed) {
return null
}
return (
)
}
export const p = function Paragraph({ children }) {
return
{children}
}
export const img = function Img(props) {
return (
)
}
function ContentWrapper({ className, children }) {
return (
{children}
)
}
function ArticleHeader({ id, date }) {
return (
)
}
export function Article({ id, title, date, children }) {
let { isFeed } = useFeed()
let heightRef = useRef(null)
let [heightAdjustment, setHeightAdjustment] = useState(0)
useEffect(() => {
let observer = new window.ResizeObserver(() => {
let { height } = heightRef.current.getBoundingClientRect()
let nextMultipleOf8 = 8 * Math.ceil(height / 8)
setHeightAdjustment(nextMultipleOf8 - height)
})
observer.observe(heightRef.current)
return () => {
observer.disconnect()
}
}, [])
if (isFeed) {
return (
{children}
)
}
return (
{children}
)
}
export const article = Article
export const h2 = H2
export const ul = function UnorderedList({ children }) {
return
{children}
}
export const code = function Code({ highlightedCode, ...props }) {
if (highlightedCode) {
return (
)
}
return
}
export const pre = function Pre({ children }) {
return
{children}
}
================================================
FILE: docs_src/src/lib/formatDate.js
================================================
export function formatDate(dateString) {
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
})
}
================================================
FILE: docs_src/src/lib/getAllArticles.js
================================================
import glob from 'fast-glob'
import * as path from 'path'
async function importArticle(articleFilename) {
let { meta, default: component } = await import(
`../pages/articles/${articleFilename}`
)
return {
slug: articleFilename.replace(/(\/index)?\.mdx$/, ''),
...meta,
component,
}
}
export async function getAllArticles() {
let articleFilenames = await glob(['*.mdx', '*/index.mdx'], {
cwd: path.join(process.cwd(), 'src/pages/articles'),
})
let articles = await Promise.all(articleFilenames.map(importArticle))
return articles.sort((a, z) => new Date(z.date) - new Date(a.date))
}
================================================
FILE: docs_src/src/lib/remToPx.js
================================================
export function remToPx(remValue) {
let rootFontSize =
typeof window === 'undefined'
? 16
: parseFloat(window.getComputedStyle(document.documentElement).fontSize)
return parseFloat(remValue) * rootFontSize
}
================================================
FILE: docs_src/src/pages/_app.jsx
================================================
import { useEffect, useRef } from 'react'
import { Footer, GithubButton } from '@/components/Footer'
import { Header } from '@/components/Header'
import '@/styles/tailwind.css'
import '@/styles/documentation.css'
import 'focus-visible'
import { Router, useRouter } from 'next/router'
import { MDXProvider } from '@mdx-js/react'
import { Layout } from '@/components/documentation/Layout'
import { Layout as ReleaseLayout } from '@/components/releases/Layout'
import * as mdxComponents from '@/components/documentation/mdx'
import { useMobileNavigationStore } from '@/components/documentation/MobileNavigation'
import { Analytics } from '@vercel/analytics/react'
function usePrevious(value) {
let ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function onRouteChange() {
useMobileNavigationStore.getState().close()
}
Router.events.on('routeChangeStart', onRouteChange)
Router.events.on('hashChangeStart', onRouteChange)
export default function App({ Component, pageProps, router }) {
let previousPathname = usePrevious(router.pathname)
let router_ = useRouter()
if (router_.pathname.includes('documentation')) {
return (
<>
>
)
} else if (router_.pathname.includes('release')) {
return (
<>
>
)
}
return (
<>
>
)
}
================================================
FILE: docs_src/src/pages/_document.jsx
================================================
import { Head, Html, Main, NextScript } from 'next/document'
import { Analytics } from '@vercel/analytics/react';
const modeScript = `
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
updateMode()
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
window.addEventListener('storage', updateModeWithoutTransitions)
function updateMode() {
let isSystemDarkMode = darkModeMediaQuery.matches
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
if (isDarkMode) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
if (isDarkMode === isSystemDarkMode) {
delete window.localStorage.isDarkMode
}
}
function disableTransitionsTemporarily() {
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
document.documentElement.classList.remove('[&_*]:!transition-none')
}, 0)
}
function updateModeWithoutTransitions() {
disableTransitionsTemporarily()
updateMode()
}
`
export default function Document() {
return (
)
}
================================================
FILE: docs_src/src/pages/community.jsx
================================================
import Head from 'next/head'
import Image from 'next/image'
import sparcklesLogo from '@/images/sparckles-logo.png'
import { useEffect, useState } from 'react'
function Contributors() {
const [contributors, setContributors] = useState([])
const [error, setError] = useState(null)
useEffect(() => {
const fetchContributors = async () => {
try {
const response = await fetch(
`https://api.github.com/repos/sparckles/robyn/contributors`
)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setContributors(data)
} catch (error) {
setError(error.toString())
}
}
fetchContributors()
}, [])
if (error) {
return
Robyn is a community project and is housed under the sparckles
organisation.
{/* Content section */}
Our Amazing Contributors
{/* Values section */}
Join us on our journey
Join us on our journey to spark new ideas, ignite the spirit of
open-source development, and break down the walls that limit
innovation and progress. Together, we can create a brighter future
for the Python ecosystem and the global software community.
Robyn is housed under an open source organisation called
Sparckles. We are a group of developers passionate about the open
source community. Come join us and help us empower the Python web
ecosystem.
{/* CTA section */}
Sparckles
Sparckles is an innovative open-source organization dedicated
to enhancing the Python ecosystem by developing cutting-edge
tools, fostering a vibrant community, and providing robust
infrastructure solutions.
>
)
}
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/advanced_features.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## Keep a track of client's IP address
Now that the portal was up and ready, Batman realised that the Joker was using the Gotham Police Dashboard too. So, he wanted to keep a track of the IP address of the client who was accessing his application. He used the following code to do so:
Batman scaled his application across multiple cores for better performance. He used the following command:
```python
from robyn import Robyn, Request
app = Robyn(__file__)
@app.get("/")
async def h(request: Request):
return f"hello to you, {request.ip_addr}"
```
---
## What's next?
Batman wondered about how to help users explore the endpoints in his application.
Robyn showed him the OpenAPI Documentation!
[OpenAPI Documentation](/documentation/en/api_reference/openapi)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/advanced_routing.mdx
================================================
export const description =
'Master Robyn\'s advanced routing features including parameter injection, route optimization, and complex URL patterns.'
# Advanced Routing and Parameter Injection
Robyn's routing system goes far beyond simple URL matching. It includes sophisticated parameter injection, route optimization, and flexible pattern matching that makes building complex APIs effortless.
## Understanding Parameter Injection
Robyn automatically analyzes your function signatures and injects the appropriate request components. This eliminates boilerplate code and makes handlers cleaner.
### The Injection Engine
The parameter injection system works in two phases:
1. **Function Introspection**: Robyn analyzes your function signature at registration time
2. **Runtime Injection**: For each request, Robyn provides the exact parameters your function needs
**Type-Based Injection**: Uses type annotations to determine what to inject
```python
from robyn import Request, QueryParams, Headers
from robyn.types import PathParams, RequestBody, RequestMethod
@app.post("/users/:user_id/posts/:post_id")
async def update_post(
# Robyn automatically injects these based on type annotations
request: Request, # Complete request object
path_params: PathParams, # {"user_id": "123", "post_id": "456"}
query_params: QueryParams, # ?draft=true&tags=python,web
headers: Headers, # All request headers
body: RequestBody, # Raw request body
method: RequestMethod # "POST"
):
user_id = path_params["user_id"]
post_id = path_params["post_id"]
is_draft = query_params.get("draft") == "true"
content_type = headers.get("content-type")
return {
"user_id": user_id,
"post_id": post_id,
"is_draft": is_draft,
"body_size": len(body),
"method": method
}
```
**Name-Based Injection**: Uses parameter names to inject components when type annotations aren't available
```python
# Reserved parameter names that Robyn recognizes
@app.get("/search/:category")
def search_handler(
query_params, # Injected by name
path_params, # Injected by name
headers, # Injected by name
request # Injected by name (full request object)
):
category = path_params["category"]
search_term = query_params.get("q", "")
user_agent = headers.get("user-agent", "")
return {
"category": category,
"search": search_term,
"user_agent": user_agent
}
```
### Complete List of Injectable Types
| Type Annotation | Reserved Name | Description |
|-----------------|---------------|-------------|
| `Request` | `request`, `req`, `r` | Complete request object |
| `QueryParams` | `query_params` | URL query parameters |
| `Headers` | `headers` | Request headers |
| `PathParams` | `path_params` | URL path parameters |
| `RequestBody` | `body` | Raw request body |
| `RequestMethod` | `method` | HTTP method (GET, POST, etc.) |
| `RequestURL` | `url` | Request URL information |
| `FormData` | `form_data` | Form-encoded data |
| `RequestFiles` | `files` | Uploaded files |
| `RequestIP` | `ip_addr` | Client IP address |
| `RequestIdentity` | `identity` | Authentication identity |
## Advanced URL Patterns
### Dynamic Route Parameters
Robyn supports multiple types of path parameters with flexible matching patterns.
While Robyn doesn't have built-in parameter validation, you can implement it in your handlers for type safety.
```python
import re
from robyn import HTTPException
@app.get("/users/:user_id")
def get_user(path_params):
user_id = path_params["user_id"]
# Validate that user_id is numeric
if not user_id.isdigit():
raise HTTPException(400, "user_id must be numeric")
user_id = int(user_id)
if user_id <= 0:
raise HTTPException(400, "user_id must be positive")
return {"user_id": user_id}
@app.get("/posts/:slug")
def get_post_by_slug(path_params):
slug = path_params["slug"]
# Validate slug format
if not re.match(r'^[a-z0-9-]+$', slug):
raise HTTPException(400, "Invalid slug format")
return {"slug": slug}
```
## Route Optimization
### Const Routes for Static Responses
Use `const=True` for responses that never change. These are cached in Rust memory and the handler function is never re-executed after startup.
When no middleware is registered, const routes take a fast path served entirely from the Rust layer without entering Python at all. When middleware is registered (including global before-request and after-request handlers), const routes still serve the cached response but middleware executes normally for every request. This means const routes are always safe to use alongside middleware.
```python
# Perfect for health checks, static configuration
@app.get("/health", const=True)
def health_check():
return {"status": "healthy", "version": "1.0"}
# API metadata that rarely changes
@app.get("/api/info", const=True)
def api_info():
return {
"name": "My API",
"version": "2.1.0",
"documentation": "/docs"
}
# Static configuration endpoints
@app.get("/config/public", const=True)
def public_config():
return {
"max_upload_size": "10MB",
"allowed_origins": ["https://myapp.com"]
}
```
### Route Priority and Ordering
Routes are matched in the order they're registered. More specific routes should be registered before general ones.
```python
# GOOD: Specific routes first
@app.get("/users/profile")
def get_current_user_profile():
return {"profile": "current_user"}
@app.get("/users/settings")
def get_user_settings():
return {"settings": "user_settings"}
@app.get("/users/:id")
def get_user(path_params):
return {"user_id": path_params["id"]}
# BAD: This would never be reached
# @app.get("/users/:id") # Registered first
# @app.get("/users/profile") # Never matched!
```
## Advanced Query Parameter Handling
### Query Parameter Parsing
Robyn provides rich query parameter handling with automatic type conversion helpers.
Configure authentication handlers on SubRouters and apply authentication to routes using `auth_required=True`.
```python
from robyn import SubRouter
from robyn.authentication import AuthenticationHandler
# Admin routes with authentication
admin = SubRouter(__file__, prefix="/admin")
class AdminAuth(AuthenticationHandler):
def authenticate(self, request):
auth_header = request.headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:] # Remove "Bearer "
return self.validate_admin_token(token)
def validate_admin_token(self, token):
# Your token validation logic
if token == "admin-secret-token":
return {"user": "admin"} # Return identity object
return None
# Configure the authentication handler for this SubRouter
admin.configure_authentication(AdminAuth())
# Routes must explicitly require authentication with auth_required=True
@admin.get("/users", auth_required=True)
def admin_users():
return {"admin_users": ["user1", "user2"]}
@admin.delete("/users/:id", auth_required=True)
def delete_user(path_params):
return {"deleted": path_params["id"]}
```
## Route Testing and Debugging
### Route Inspection
Debug your routes by inspecting the registered route table.
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/users/:id")
def get_user(path_params):
return {"user": path_params["id"]}
@app.post("/users")
def create_user(body):
return {"created": True}
# Debug: Print all registered routes
if __name__ == "__main__":
print("Registered routes:")
for route in app.get_routes():
print(f"{route.method} {route.path}")
app.start(port=8080)
```
### Route Performance Monitoring
Add timing middleware to monitor route performance.
```python
import time
from robyn import Robyn
app = Robyn(__file__)
@app.before_request
def timing_middleware(request):
request.start_time = time.time()
return request
@app.after_request
def timing_after_middleware(request, response):
duration = time.time() - request.start_time
print(f"{request.method} {request.url.path} - {duration:.3f}s")
response.headers["X-Response-Time"] = f"{duration:.3f}s"
return response
@app.get("/slow")
def slow_endpoint():
time.sleep(0.1) # Simulate work
return {"message": "slow response"}
```
## Best Practices
### 1. Parameter Injection Patterns
- **Use type annotations** for better IDE support and self-documenting code
- **Inject only what you need** to keep handlers focused
- **Validate parameters early** to provide clear error messages
### 2. Route Organization
- **Group related routes** using SubRouters
- **Order routes from specific to general** to avoid matching issues
- **Use consistent naming conventions** for path parameters
### 3. Performance Optimization
- **Use const routes** for static responses
- **Minimize parameter injection** in high-traffic endpoints
- **Cache expensive computations** rather than repeating them
### 4. Error Handling
- **Validate path parameters** early in handlers
- **Provide meaningful error messages** for invalid input
- **Use consistent error response formats** across your API
## What's Next?
Now that you've mastered advanced routing, explore other Robyn features:
- [Middleware Development Guide](/documentation/en/api_reference/middlewares_advanced)
- [Performance Optimization](/documentation/en/api_reference/performance_optimization)
- [WebSocket Advanced Features](/documentation/en/api_reference/websockets_advanced)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/agents.mdx
================================================
export const description =
'Robyn AI Agents provide intelligent functionality for building AI-powered applications using MCP (Model Context Protocol) implementation with file system access, task management, and context-aware capabilities.'
# Agents
This guide demonstrates how to build AI-powered agents using Robyn's MCP (Model Context Protocol) implementation.
## Overview
The agent system connects AI assistants like Claude Desktop to your development environment, providing seamless access to:
- File system operations (read, search, organize)
- Task and note management
- System monitoring and git integration
- Web content fetching and analysis
- Context-aware code analysis
## Quick Start
1. Run the MCP server:
```bash
python examples/agents.py
```
2. Connect your AI assistant to `http://localhost:8080/mcp`
3. Start using natural language commands:
- "What files are in my projects directory?"
- "Show me my recent git commits"
- "Create a note about today's standup meeting"
- "What processes are using the most CPU?"
- "Add a task to review the quarterly report"
## Configuration
The assistant creates the following structure:
```
~/Documents/
├── notes/ # Markdown notes
└── tasks.json # Task list
~/projects/ # Development projects
├── project1/
└── project2/
```
## Security
- File access restricted to home directory
- Safe mathematical expression evaluation
- Path validation for all file operations
- Read-only git operations
## Available Resources
### File System
- `fs://{path}` - Read files in home directory
- `fs://dir/{path}` - List directory contents
### Git Integration
- `git://repo/{repo_name}` - Repository status and commits
### System Monitoring
- `system://processes` - Running processes
- `system://stats` - System statistics
## Available Tools
- `create_note(title, content, tags)` - Create markdown notes
- `add_task(task, priority, due_date)` - Add tasks
- `complete_task(task_id)` - Mark tasks complete
- `search_files(query, directory)` - Search file contents
- `fetch_url_content(url, max_length)` - Download web content
## Available Prompts
- `analyze_file_structure(directory)` - Generate project analysis
- `code_review_request(file_path, focus_area)` - Create code reviews
- `task_prioritization(context)` - Organize and prioritize work
## Dependencies
Optional enhanced functionality:
```bash
pip install psutil # Enhanced system monitoring
```
## Implementation Examples
### Development Workflow
"Analyze my projects directory and help prioritize work based on recent activity"
### Project Analysis
"Review my web-app project structure and suggest improvements"
### Meeting Notes
"Create a note about today's architecture review with key decisions"
### Code Search
"Find all files mentioning 'authentication' and summarize approaches"
### Task Management
"Add high-priority task to refactor user service, due Friday"
## Integration Benefits
Connecting AI assistants to your development environment enables:
- Native file system browsing
- Context-aware project conversations
- Personalized code suggestions
- Real-time task management
- Workspace-specific code reviews
## Advanced Features
The MCP implementation includes:
- URI templates with parameter extraction
- Auto-generated schemas from type hints
- Async/sync operation handlers
- MCP-compliant error handling
- Type-safe parameter passing
Extend easily with custom resources, tools, and prompts for your specific workflow.
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/ai.mdx
================================================
export const description =
'Robyn AI provides agent and memory functionality for building intelligent applications with conversation history, context awareness, and pluggable AI runners.'
# AI Agent and Memory
Robyn includes built-in AI capabilities that allow you to create intelligent applications with conversation memory, context awareness, and pluggable agent runners. The AI module provides abstractions for memory storage and agent execution that can be easily integrated into your Robyn applications.
## Installation
The AI features are included with the base Robyn installation:
```bash
pip install robyn
```
## Quick Start
Here's a simple example of using Robyn's AI features:
```python
from robyn import Robyn
from robyn.ai import agent, memory
app = Robyn(__file__)
# Create memory instance
mem = memory(provider="inmemory", user_id="user123")
# Create agent with memory
chat_agent = agent(runner="simple", memory=mem)
@app.get("/chat")
async def chat_endpoint(request):
query = request.query_params.get("q", [""])[0]
if not query:
return {"error": "Query required"}
# Run agent with conversation history
result = await chat_agent.run(query, history=True)
return result
```
## Memory System
The memory system provides persistent storage for conversation history and context. It supports multiple providers and offers a consistent interface for storing and retrieving conversation data.
### Memory Providers
#### InMemory Provider
The simplest provider that stores data in memory. Data is lost when the application restarts.
```python
from robyn.ai import memory
# Create in-memory storage
mem = memory(provider="inmemory", user_id="user123")
# Add messages
await mem.add("Hello, how are you?")
await mem.add("I'm doing great, thanks!")
# Retrieve all messages
messages = await mem.get()
# Clear memory
await mem.clear()
```
### Memory API
The Memory class provides these key methods:
- `add(message, metadata=None)` - Store a message with optional metadata
- `get(query=None)` - Retrieve messages, optionally filtered by query
- `clear()` - Clear all stored messages for the user
## Agent System
Agents provide the execution layer for AI functionality. They can use different runners and integrate with memory for context-aware responses.
### Agent Runners
#### Simple Runner
A runner with OpenAI integration that provides intelligent responses:
```python
from robyn.ai import agent
# Create simple agent with OpenAI
from robyn.ai import configure
config = configure(openai_api_key="your-openai-key")
simple_agent = agent(runner="simple", config=config)
# Use the agent
result = await simple_agent.run("What's the weather like?")
# Returns structured response with AI-generated content
```
### Agent API
The Agent class provides:
- `run(query, history=False, **kwargs)` - Execute the agent with optional history context
- Automatic memory integration when provided
- Support for custom runners and configuration
## Complete Example
Here's a comprehensive example showing all features:
```python
from robyn import Robyn
from robyn.ai import agent, memory
app = Robyn(__file__)
# Create memory with InMemory provider
mem = memory(
provider="inmemory",
user_id="guest"
)
# Create agent with memory
chat_agent = agent(runner="simple", memory=mem)
@app.get("/")
async def home():
return {"message": "Robyn AI Chat API"}
@app.post("/chat")
async def chat(request):
"""Chat with AI agent"""
data = request.json()
query = data.get("query", "")
include_history = data.get("history", True)
if not query:
return {"error": "Query is required"}
try:
result = await chat_agent.run(query, history=include_history)
return {
"query": query,
"response": result.get("response"),
"history_included": include_history
}
except Exception as e:
return {"error": str(e)}
@app.get("/memory")
async def get_memory():
"""Retrieve conversation history"""
try:
memories = await mem.get()
return {"memories": memories, "count": len(memories)}
except Exception as e:
return {"error": str(e)}
@app.delete("/memory")
async def clear_memory():
"""Clear conversation history"""
try:
await mem.clear()
return {"message": "Memory cleared"}
except Exception as e:
return {"error": str(e)}
@app.post("/memory")
async def add_memory(request):
"""Add message to memory"""
data = request.json()
message = data.get("message", "")
metadata = data.get("metadata", {})
if not message:
return {"error": "Message is required"}
try:
await mem.add(message, metadata)
return {"message": "Added to memory"}
except Exception as e:
return {"error": str(e)}
if __name__ == "__main__":
app.start(host="127.0.0.1", port=8080)
```
## Advanced Usage
### Custom Memory Providers
You can create custom memory providers by extending the `MemoryProvider` abstract base class:
```python
from robyn.ai import MemoryProvider
from typing import Dict, List, Any, Optional
class CustomMemoryProvider(MemoryProvider):
async def store(self, user_id: str, data: Dict[str, Any]) -> None:
# Implement custom storage logic
pass
async def retrieve(self, user_id: str, query: Optional[str] = None) -> List[Dict[str, Any]]:
# Implement custom retrieval logic
return []
async def clear(self, user_id: str) -> None:
# Implement custom clearing logic
pass
# Use custom provider
from robyn.ai import Memory
custom_mem = Memory(provider=CustomMemoryProvider(), user_id="user123")
```
### Custom Agent Runners
Similarly, you can create custom agent runners:
```python
from robyn.ai import AgentRunner
from typing import Dict, Any
class CustomAgentRunner(AgentRunner):
async def run(self, query: str, **kwargs) -> Dict[str, Any]:
# Implement custom agent logic
return {
"response": f"Custom response to: {query}",
"processed": True
}
# Use custom runner
from robyn.ai import Agent
custom_agent = Agent(runner=CustomAgentRunner())
```
## Best Practices
1. **User Isolation**: Always use unique user IDs to isolate memory between different users
2. **Error Handling**: Wrap AI operations in try-catch blocks as external services may fail
3. **Memory Management**: Regularly clear or archive old memories to prevent unbounded growth
4. **Configuration**: Store sensitive configuration (API keys, etc.) in environment variables
5. **Testing**: Use the simple runner for development and testing before deploying complex agents
## Troubleshooting
### Common Issues
**ImportError for openai**: Install the required package:
```bash
pip install openai
```
**Memory not persisting**: Note that the in-memory provider loses data when the application restarts. Consider implementing a custom persistent provider for production use.
**Agent timeouts**: Complex operations may take time. Consider implementing timeout handling in your endpoints.
**Memory growing too large**: Implement periodic cleanup or use providers with built-in retention policies.
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/architecture_deep_dive.mdx
================================================
export const description =
'Deep dive into Robyn\'s unique hybrid Python-Rust architecture and how it delivers exceptional performance while maintaining Python\'s ease of use.'
# Architecture Deep Dive
Robyn's architecture is unique in the Python web framework landscape. It combines Python's expressiveness with Rust's performance through a carefully designed hybrid system. This deep dive explains how Robyn works under the hood and why it's so fast.
## The Hybrid Python-Rust Design
### Two-Layer Architecture
Robyn operates on two distinct but interconnected layers:
1. **Python Layer**: Provides the developer-facing API, routing configuration, and business logic
2. **Rust Layer**: Handles HTTP parsing, request routing, response generation, and I/O operations
The Python layer is where you write your application code. It handles:
- Route definitions and decorators
- Request parameter injection
- Middleware configuration
- Business logic execution
- Response formatting
```python
from robyn import Robyn, Request
app = Robyn(__file__)
@app.get("/users/:id")
async def get_user(request: Request, user_id: str):
# Business logic runs in Python
user = await fetch_user_from_db(user_id)
return {"user": user.to_dict()}
```
The Rust layer handles all performance-critical operations:
- HTTP request parsing
- URL routing and matching
- WebSocket connections
- Static file serving
- Response serialization
```rust
// This happens internally in Robyn's Rust core
impl HttpRouter {
pub fn route_request(&self, method: &str, path: &str) -> Option {
// High-performance routing using matchit crate
self.router.at(path).ok().map(|matched| {
RouteInfo {
handler: matched.value.clone(),
params: matched.params.clone(),
}
})
}
}
```
### PyO3 Bridge: Connecting Python and Rust
The magic happens through PyO3, which enables seamless communication between Python and Rust:
**Function Registration**: When you define a route handler in Python, Robyn registers it with the Rust runtime through PyO3 bindings.
```python
# Python side - route registration
@app.get("/api/data")
def handler(request):
return {"data": "example"}
# Internally, this creates a FunctionInfo object
# that's passed to the Rust runtime
```
**Request Execution Flow**: When a request arrives, the Rust layer routes it and then calls back into Python to execute your handler.
When you mark a route as `const`, Robyn caches the response in Rust memory and never re-executes the Python handler. If no middleware is registered, const routes are served entirely from the Rust layer without entering Python at all. When middleware is present, the cached response is still used but before-request and after-request middleware execute normally.
```python
# This response is cached in Rust memory
@app.get("/health", const=True)
def health_check():
return {"status": "healthy"}
# Without middleware: served directly from Rust, bypassing Python entirely
# With middleware: cached response is used, but middleware still runs
```
### Zero-Copy Request Handling
The Rust layer uses zero-copy techniques wherever possible:
- Request bodies are parsed once and shared between layers
- String data is referenced rather than copied
- Response buffers are reused across requests
### Async Runtime Integration
Robyn integrates with Python's asyncio while maintaining Rust's async runtime:
**Sync Handlers**: Executed in a thread pool to avoid blocking the async runtime
```python
@app.get("/sync")
def sync_handler(request):
# Runs in thread pool
time.sleep(1) # Won't block other requests
return "Done"
```
**Async Handlers**: Executed directly in the async runtime for maximum performance
```python
@app.get("/async")
async def async_handler(request):
# Runs in main async runtime
await asyncio.sleep(1)
return "Done"
```
## Advanced Parameter Injection
One of Robyn's most sophisticated features is its parameter injection system:
### Type-Based Injection
Robyn analyzes your function signatures and automatically injects the appropriate request components based on type annotations.
You can also use reserved parameter names without type annotations.
```python
@app.get("/simple/:id")
def simple_handler(query_params, path_params, headers):
# Parameters injected based on names
return {
"id": path_params["id"],
"search": query_params.get("q", ""),
"auth": headers.get("authorization")
}
```
## Memory Management
### Python Object Lifecycle
Robyn carefully manages Python objects across the Python-Rust boundary:
1. **Request Objects**: Created once per request and reused
2. **Response Objects**: Efficiently serialized and passed to Rust
3. **Handler References**: Stored in Rust and called via PyO3
### Rust Memory Safety
The Rust layer benefits from Rust's ownership system:
- No memory leaks from HTTP parsing
- Safe concurrent access to shared data
- Automatic cleanup of connection resources
## Scaling Architecture
### Multi-Process Mode
Robyn can spawn multiple processes to utilize all CPU cores.
```bash
# Spawn 4 worker processes
python app.py --processes 4 --workers 2
# Each process runs independently with shared-nothing architecture
```
### Multi-Worker Mode
Within each process, multiple worker threads handle requests concurrently.
```python
# Workers share the same Python interpreter
# but handle requests concurrently
app.start(port=8080, workers=4)
```
## WebSocket Architecture
### Persistent Connections
Robyn's WebSocket implementation maintains persistent connections in the Rust layer while allowing Python handlers to process messages:
**Connection Management**: Handled entirely in Rust for efficiency
**Message Processing**: Python handlers process individual messages
**Broadcasting**: Rust-based message distribution for high throughput
```python
from robyn import WebSocketDisconnect
@app.websocket("/chat")
async def websocket_handler(websocket):
try:
while True:
message = await websocket.receive_text()
response = process_chat_message(message)
await websocket.send_text(response)
except WebSocketDisconnect:
pass
```
## Why This Architecture Works
1. **Best of Both Worlds**: Python's productivity with Rust's performance
2. **Gradual Optimization**: Hot paths can be moved to Rust incrementally
3. **Memory Efficiency**: Minimal copying between layers
4. **Async Integration**: Seamless integration with Python's async/await
5. **Safety**: Rust's memory safety prevents common server vulnerabilities
This architecture allows Robyn to achieve performance comparable to pure Rust web frameworks while maintaining the ease of development that Python developers expect.
## What's Next?
Now that you understand Robyn's architecture, explore how to leverage its advanced features:
- [Advanced Routing and Parameter Injection](/documentation/en/api_reference/advanced_routing)
- [Performance Optimization Guide](/documentation/en/api_reference/performance_optimization)
- [WebSocket Deep Dive](/documentation/en/api_reference/websockets_advanced)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/authentication.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
After Creating a basic version of the app, Batman wanted to restrict the access to the Gotham Police Department. So, he enquired about the Authentication functionalities in Robyn.
## Authentication
As Batman found out, Robyn provides an easy way to add an authentication middleware to your application. You can then specify `auth_required=True` in your routes to make them accessible only to authenticated users.
```python
@app.get("/auth", auth_required=True)
async def auth(request: Request):
# This route method will only be executed if the user is authenticated
# Otherwise, a 401 response will be returned
return "Hello, world"
```
To add an authentication middleware, you can use the `configure_authentication` method. This method requires an `AuthenticationHandler` object as an argument. This object specifies how to authenticate a user, and uses a `TokenGetter` object to retrieve the token from the request. Robyn does currently provide a `BearerGetter` class that gets the token from the `Authorization` header, using the `Bearer` scheme. Here is an example of a basic authentication handler:
```python
class BasicAuthHandler(AuthenticationHandler):
def authenticate(self, request: Request) -> Optional[Identity]:
token = self.token_getter.get_token(request)
if token == "valid":
return Identity(claims={})
return None
app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter()))
```
The authenticate method should return an `Identity` object if the user is authenticated, or `None` otherwise. The Identity object can contain any data you want, and will be accessible in the route methods using the `request.identity` attribute.
Note: that this authentication system is basically only using a `before request` middleware under the hood. This means you can overlook it and create your own authentication system using middlewares if you want to. However, Robyn still provides this easy to implement solution that should suit most use cases.
---
## What's next?
Now, that Batman has learned about authentication, he wanted to know about certain optimization techniques that he could use to make his application faster. He found out about the following features
- [Const Requests and Multi Core Scaling](/documentation/en/api_reference/const_requests)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/const_requests.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
After authentication, Batman was worried about the website traffic during the rush hours. He was worried about the server crashing when Joker would try to break all the criminals from the Arkham asylum one more time. So, Robyn told him about the `Const Requests` feature and the multi-core scaling potential.
## Const Requests
Robyn told Batman that you can pre-compute the response for each route. This will compute the response even before execution. This will improve the response time bypassing the need to access the router.
```python
@app.get("/", const=True)
async def h():
return "Hello, world"
```
## Muli-core scaling
Robyn told Batman that he can use the `--workers` flag to scale the application to multiple cores. This will create multiple instances of the application and will distribute the load among them. This will improve the performance of the application.
```python
python3 app.py --workers=N --process=M
```
The authenticate method should return an `Identity` object if the user is authenticated, or `None` otherwise. The Identity object can contain any data you want, and will be accessible in the route methods using the `request.identity` attribute.
Note: that this authentication system is basically only using a `before request` middleware under the hood. This means you can overlook it and create your own authentication system using middlewares if you want to. However, Robyn still provides this easy to implement solution that should suit most use cases.
---
## What's next?
After making the application faster, Batman was happy and wanted to make a request from his Frontend Dashboard.
But he was faced with CORS issues! He asked Robyn about how to solve this issue. Robyn told him about the following features
- [CORS](/documentation/en/api_reference/cors)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/cors.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## CORS
Batman was annoyed on getting a CORS error whenever he tried to access the API.
## Scaling the Application
You can allow CORS for your application by adding the following code:
```python
from robyn import Robyn, ALLOW_CORS
app = Robyn(__file__)
ALLOW_CORS(app, origins = ["http://localhost:/"])
```
---
## What's next?
After fixing the CORS issues. Batman was satisfied but he wanted to learn about ways to have small frontend pages in the server itself.
Robyn told him about templates and how he can use them to render HTML pages.
- [Templating](/documentation/en/api_reference/templating)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/dependency_injection.mdx
================================================
export const description =
'On this page, we will learn about dependency injection in Robyn.'
## Dependency Injection
Batman wanted to learn about dependency injection in Robyn. Robyn introduced him to the concept of dependency injection and how it can be used in Robyn.
Robyn has two types of dependency injection:
One is for the application level and the other is for the router level.
### Application Level Dependency Injection
Application level dependency injection is used to inject dependencies into the application. These dependencies are available to all the requests.
Note: `router_dependencies`, `global_dependencies` are reserved parameters and **must** be named as such. The order of the parameters does not matter among them. However, the `router_dependencies` and `global_dependencies` must only come after the `request` parameter.
### WebSocket Dependency Injection
WebSockets support the same dependency injection system as HTTP routes. The `global_dependencies` and `router_dependencies` parameters work in the main handler, `on_connect`, and `on_close` callbacks.
```python {{ title: 'WebSocket DI' }}
from robyn import Robyn
import logging
app = Robyn(__file__)
app.inject_global(logger=logging.getLogger(__name__))
app.inject(cache=RedisCache())
@app.websocket("/chat")
async def chat(websocket, global_dependencies=None, router_dependencies=None):
logger = global_dependencies.get("logger")
cache = router_dependencies.get("cache")
logger.info(f"New connection: {websocket.id}")
while True:
message = await websocket.receive_text()
cache.set(f"ws_{websocket.id}", message)
await websocket.broadcast(f"User {websocket.id}: {message}")
@chat.on_connect
async def on_connect(websocket, global_dependencies=None):
logger = global_dependencies.get("logger")
logger.info(f"Client connected: {websocket.id}")
return "Connected"
```
---
## What's next?
Batman, being the familiar with the dark side wanted to know about Exceptions!
Robyn introduced him to the concept of exceptions and how he can use them to handle errors in his application.
- [Exceptions](/documentation/en/api_reference/exceptions)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/exceptions.mdx
================================================
## Custom Exception Handler
Batman learned how to create custom error handlers for different exception types in his application. He wrote the following code to handle exceptions and return a custom error response:
```python
@app.exception
def handle_exception(error: Exception):
return Response(status_code=500, description=f"error msg: {error}", headers={})
```
---
## What's next?
Now, Batman wanted to scale his application across multiple cores. Robyn led him to Scaling.
- [Scaling](/documentation/en/api_reference/scaling)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/file-uploads.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## File Uploads
Batman learned how to handle file uploads using Robyn. He created an endpoint to handle file uploads using the following code:
## Sending a File without MultiPart Form Data
Batman scaled his application across multiple cores for better performance. He used the following command:
```python
@app.post("/upload")
async def upload():
body = request.body
file = bytearray(body)
# write whatever filename
with open('test.txt', 'wb') as f:
f.write(file)
return {'message': 'success'}
```
## Sending a File with MultiPart Form Data
Batman scaled his application across multiple cores for better performance. He used the following command:
```python
@app.post("/sync/multipart-file")
def sync_multipart_file(request: Request):
files = request.files
file_names = files.keys()
return {"file_names": list(file_names)}
```
---
## File Downloads
Batman now wanted to allow users to download files from his application. He created an endpoint to handle file downloads using the following code:
### Serving Simple HTML Files
Batman scaled his application across multiple cores for better performance. He used the following command:
After serving other files, Batman wanted to serve directories, e.g. to serve a React build directory or just a simple HTML/CSS/JS directory. He was suggested to use the following code:
```python
from robyn import Robyn, serve_file, Request
app = Robyn(__file__)
app.serve_directory(
route="/test_dir",
directory_path=os.path.join(current_file_path, "build"),
index_file="index.html",
)
app.start(port=8080)
```
## What's next?
Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to handle form data
- [Form Data](/documentation/en/api_reference/form_data)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/form_data.mdx
================================================
export const description =
'On this page, we’ll dive into using the form data.'
## Form Data and Multi Part Form Data
Batman learned how to handle file uploads using Robyn. Now, he wanted to handle the form data.
## Handling Multi Part Form Data
Batman uploaded some multipart form data and wanted to handle it using the following code:
```python
@app.post("/upload")
async def upload(request: Request):
form_data = request.form_data
return form_data
```
## What's next?
Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard.
- [WebSockets](/documentation/en/api_reference/websockets)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/future-roadmap.mdx
================================================
export const description =
'On this page, we`ll take a look at the future roadmap for Robyn.'
- Add performance optimizations
- Pydantic Integration
- Implement Auto Const Requests
- Add ORM support, especially Prisma integration
- Improve Plugin Ecosystem
- Better Documentation
- Improve the Websockets
- Template Support
- Graphql integration with Strawberry
- Invest more time in the community around Robyn.
## Next Steps
- [Advanced Features](/documentation/en/api_reference/advanced_features)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/getting_started.mdx
================================================
export const description =
'On this page, we’ll dive into the different fundamentals of building web applications with Robyn, including examples and best practices.'
## Building Your First Robyn Application
Robyn is the fastest Python web framework, combining Python's simplicity with Rust's performance. Whether you're building APIs, web services, or full-stack applications, Robyn makes it easy to get started and scale.
### Quick Start
Install Robyn and create your first application in minutes:
```bash
pip install robyn
```
## Understanding Handler Types
Robyn supports both synchronous and asynchronous request handlers, allowing you to choose the best approach for your use case:
- **Synchronous handlers**: Perfect for CPU-bound operations, simple logic, or when you don't need to wait for external resources
- **Asynchronous handlers**: Ideal for I/O-bound operations like database calls, HTTP requests, file operations, or any task that involves waiting
**Synchronous handlers** are perfect for simple operations, calculations, or when you don't need to wait for external resources:
```python
from robyn import Robyn, Request
app = Robyn(__file__)
@app.get("/")
def h(request: Request):
return "Hello, world"
app.start(port=8080, host="0.0.0.0") # host is optional, defaults to 127.0.0.1
```
**Asynchronous handlers** are ideal for database operations, HTTP requests, file I/O, or any operation that involves waiting:
```python
from robyn import Request
@app.get("/")
async def h(request: Request) -> str:
return "Hello, world"
```
### Complete Example: User Management API
Here's a comprehensive example that demonstrates both sync and async handlers, proper error handling, and real-world patterns:
```python
from robyn import Robyn, Request
import asyncio
import json
import time
from typing import Dict, Any
app = Robyn(__file__)
# In-memory storage for demo (use a real database in production)
users: Dict[str, Dict[str, Any]] = {
"1": {"id": "1", "name": "Alice", "email": "alice@example.com", "created_at": "2024-01-01"},
"2": {"id": "2", "name": "Bob", "email": "bob@example.com", "created_at": "2024-01-02"}
}
# Const route for health checks (cached in Rust for max performance)
@app.get("/health", const=True)
def health_check():
return {"status": "healthy", "version": "1.0.0"}
# Async handler for database-like operations
@app.get("/users/:id")
async def get_user(path_params):
user_id = path_params["id"]
# Simulate async database lookup
await asyncio.sleep(0.01) # Simulate DB query time
if user_id in users:
return {"success": True, "user": users[user_id]}
else:
return {"success": False, "error": "User not found"}, 404
# Get all users with pagination
@app.get("/users")
async def list_users(query_params):
page = int(query_params.get("page", "1"))
limit = int(query_params.get("limit", "10"))
# Simulate async operation
await asyncio.sleep(0.01)
user_list = list(users.values())
start = (page - 1) * limit
end = start + limit
return {
"users": user_list[start:end],
"total": len(user_list),
"page": page,
"limit": limit
}
# Create new user
@app.post("/users")
async def create_user(body):
try:
data = json.loads(body)
# Validate required fields
if not data.get("name") or not data.get("email"):
return {"success": False, "error": "Name and email are required"}, 400
# Generate new ID
new_id = str(len(users) + 1)
new_user = {
"id": new_id,
"name": data["name"],
"email": data["email"],
"created_at": time.strftime("%Y-%m-%d")
}
# Simulate async database save
await asyncio.sleep(0.02)
users[new_id] = new_user
return {"success": True, "user": new_user}, 201
except json.JSONDecodeError:
return {"success": False, "error": "Invalid JSON"}, 400
# Sync handler for CPU-intensive operations
@app.post("/calculate")
def calculate_fibonacci(body):
try:
data = json.loads(body)
n = data.get("number", 10)
if n < 0 or n > 35: # Prevent excessive computation
return {"error": "Number must be between 0 and 35"}, 400
# CPU-intensive calculation (runs in thread pool)
def fib(x):
if x <= 1:
return x
return fib(x-1) + fib(x-2)
start_time = time.time()
result = fib(n)
calc_time = time.time() - start_time
return {
"input": n,
"result": result,
"calculation_time": f"{calc_time:.4f}s"
}
except json.JSONDecodeError:
return {"error": "Invalid JSON"}, 400
if __name__ == "__main__":
app.start(port=8080)
```
### Testing Your API
Once your server is running, you can test these endpoints using curl or any HTTP client:
```bash
# Test health endpoint
curl http://localhost:8080/health
# Get a user
curl http://localhost:8080/users/1
# List users with pagination
curl "http://localhost:8080/users?page=1&limit=5"
# Create a new user
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Charlie", "email": "charlie@example.com"}'
# Calculate fibonacci
curl -X POST http://localhost:8080/calculate \
-H "Content-Type: application/json" \
-d '{"number": 20}'
```
---
## Running Your Application
Robyn applications can be run in several ways, each optimized for different scenarios. Here are the most common approaches:
**Direct execution** - Run your application file directly with various optimization flags:
- `--dev`: Development mode with auto-reload
- `--fast`: Optimized settings for production
- `--processes N`: Scale across multiple CPU cores
- `--workers N`: Multiple workers per process
```bash
usage: app.py [-h] [--processes PROCESSES] [--workers WORKERS] [--log-level LOG_LEVEL] [--create] [--docs] [--open-browser] [--version]
Robyn, a fast async web framework with a rust runtime.
options:
-h, --help show this help message and exit
--processes PROCESSES
Choose the number of processes. [Default: 1]
--workers WORKERS Choose the number of workers. [Default: 1]
--dev Development mode. It restarts the server based on file changes.
--log-level LOG_LEVEL
Set the log level name
--create Create a new project template.
--docs Open the Robyn documentation.
--open-browser Open the browser on successful start.
--version Show the Robyn version.
--compile-rust-path COMPILE_RUST_PATH
Compile rust files in the given path.
--create-rust-file CREATE_RUST_FILE
Create a rust file with the given name.
--disable-openapi Disable the OpenAPI documentation.
--fast Fast mode. It sets the optimal values for processes, workers and log level. However, you can override them.
```
### Common Run Configurations
**Development Mode**: Best for local development with automatic reloading when files change.
```bash
# Basic development mode
python app.py --dev
# Development with custom port
python app.py --dev --port 3000
# Development with debug logging
python app.py --dev --log-level DEBUG
```
**Production Mode**: Optimized for performance with multiple processes and workers.
```bash
# Fast mode (automatic optimization)
python app.py --fast
# Custom scaling configuration
python app.py --processes 4 --workers 2
# Production with specific log level
python app.py --fast --log-level INFO
```
**Module execution** - Use Robyn's CLI module for additional features and consistent behavior across environments:
```bash
usage: python -m robyn app.py [-h] [--processes PROCESSES] [--workers WORKERS] [--dev] [--log-level LOG_LEVEL] [--create] [--docs] [--open-browser] [--version]
Robyn, a fast async web framework with a rust runtime.
options:
-h, --help show this help message and exit
--processes PROCESSES
Choose the number of processes. [Default: 1]
--workers WORKERS Choose the number of workers. [Default: 1]
--dev Development mode. It restarts the server based on file changes.
--log-level LOG_LEVEL
Set the log level name
--create Create a new project template.
--docs Open the Robyn documentation.
--open-browser Open the browser on successful start.
--version Show the Robyn version.
--compile-rust-path COMPILE_RUST_PATH
Compile rust files in the given path.
--create-rust-file CREATE_RUST_FILE
Create a rust file with the given name.
--disable-openapi Disable the OpenAPI documentation.
--fast Fast mode. It sets the optimal values for processes, workers and log level. However, you can override them.
```
---
## Handling Different HTTP Methods
Robyn supports all standard HTTP methods. Here's how to create a complete RESTful API with proper request handling:
**Complete REST API Example**: Here's a practical example showing all HTTP methods for a blog post API:
```python
from robyn import Robyn, Request
app = Robyn(__file__)
# In-memory storage for demo
posts = {
"1": {"id": "1", "title": "First Post", "content": "Hello World"},
"2": {"id": "2", "title": "Second Post", "content": "Learning Robyn"}
}
# GET - Retrieve all posts
@app.get("/posts")
def get_posts(query_params):
limit = int(query_params.get("limit", "10"))
posts_list = list(posts.values())[:limit]
return {"posts": posts_list, "total": len(posts)}
# GET - Retrieve specific post
@app.get("/posts/:id")
def get_post(path_params):
post_id = path_params["id"]
if post_id in posts:
return {"post": posts[post_id]}
return {"error": "Post not found"}, 404
# POST - Create new post
@app.post("/posts")
def create_post(request: Request):
data = request.json()
post_id = str(len(posts) + 1)
new_post = {
"id": post_id,
"title": data.get("title", ""),
"content": data.get("content", "")
}
posts[post_id] = new_post
return {"message": "Post created", "post": new_post}, 201
# PUT - Update entire post
@app.put("/posts/:id")
def update_post(request: Request, path_params):
post_id = path_params["id"]
if post_id not in posts:
return {"error": "Post not found"}, 404
data = request.json()
posts[post_id] = {
"id": post_id,
"title": data.get("title", ""),
"content": data.get("content", "")
}
return {"message": "Post updated", "post": posts[post_id]}
# PATCH - Partial update
@app.patch("/posts/:id")
def patch_post(request: Request, path_params):
post_id = path_params["id"]
if post_id not in posts:
return {"error": "Post not found"}, 404
data = request.json()
post = posts[post_id]
# Update only provided fields
if "title" in data:
post["title"] = data["title"]
if "content" in data:
post["content"] = data["content"]
return {"message": "Post updated", "post": post}
# DELETE - Remove post
@app.delete("/posts/:id")
def delete_post(path_params):
post_id = path_params["id"]
if post_id not in posts:
return {"error": "Post not found"}, 404
deleted_post = posts.pop(post_id)
return {"message": "Post deleted", "post": deleted_post}
```
---
## Working with JSON and Response Formats
Robyn automatically handles JSON serialization, but also provides flexible response formatting options for different use cases.
**Automatic JSON Handling**: Robyn automatically converts Python dictionaries and lists to JSON responses with the correct Content-Type headers.
**Path Parameters**: Extract dynamic segments from URLs using colon syntax (`:param`)
**Type-Safe Injection**: Use type annotations for automatic parameter injection with IDE support
Any request param can be used in the handler function either using type annotations or using the reserved names.
Do note that the type annotations will take precedence over the reserved names.
Robyn showed Batman example syntaxes of accessing the request params:
```python
from robyn.robyn import QueryParams, Headers
from robyn.types import PathParams, RequestMethod, RequestBody, RequestURL
@app.get("/untyped/query_params")
def untyped_basic(query_params):
return query_params.to_dict()
@app.get("/typed/query_params")
def typed_basic(query_data: QueryParams):
return query_data.to_dict()
@app.get("/untyped/path_params/:id")
def untyped_path_params(query_params: PathParams):
return query_params # contains the path params since the type annotations takes precedence over the reserved names
@app.post("/typed_untyped/combined")
def typed_untyped_combined(
query_params,
method_data: RequestMethod,
body_data: RequestBody,
url: RequestURL,
headers_item: Headers,
):
return {
"body": body_data,
"query_params": query_params.to_dict(),
"method": method_data,
"url": url.path,
"headers": headers_item.get("server"),
}
```
Type Aliases: `Request`, `QueryParams`, `Headers`, `PathParams`, `RequestBody`, `RequestMethod`, `RequestURL`, `FormData`, `RequestFiles`, `RequestIP`, `RequestIdentity`
Reserved Names: `r`, `req`, `request`, `query_params`, `headers`, `path_params`, `body`, `method`, `url`, `ip_addr`, `identity`, `form_data`, `files`
---
As Batman continued to develop his web application with Robyn, he explored more features and implemented them using code samples.
## Customizing Response Formats and Headers
After understanding the dynamic nature of Robyn, Batman, now wanted the ability to customize response formats and headers. Robyn showed him how to do this using dictionaries and Robyn's Response object.
### Using Dictionaries
Batman learned to customize response formats by returning dictionaries or using Robyn's Response object. He could also set status codes and headers for each response. For example, Batman created a response with a dictionary like this:
```python
from robyn import Request
@app.post("/dictionary")
async def dictionary(request: Request):
return {
"status_code": 200,
"description": "This is a regular response",
"type": "text",
"headers": {"Header": "header_value"},
}
```
### Using the Response object
Batman then wanted to return a binary output from his application. He could do this by setting the type of the response to "binary" and returning a bytes object. For example, he wrote:
```python
from robyn import Request, Response
@app.get("/binary_output_response_sync")
def binary_output_response_sync(request: Request):
return Response(
status_code=200,
headers={"Content-Type": "application/octet-stream"},
description="OK",
)
@app.get("/binary_output_async")
async def binary_output_async(request: Request):
return b"OK"
@app.get("/binary_output_response_async")
async def binary_output_response_async(request: Request):
return Response(
status_code=200,
headers={"Content-Type": "application/octet-stream"},
description="OK",
)
```
---
## Response Headers
Batman, being the world's greatest detective, spotted the `headers` field in the `Response` object. He, naturally wanted to know more about it. Robyn explained that he could use the `headers` field to set response headers. For example, he could set the `Content-Type` header to `application/json` by writing:
### Local Response Headers
Either, by using the `headers` field in the `Response` object:
You can set additional cookie attributes for security and control:
- **path**: Cookie path (default: "/")
- **domain**: Cookie domain
- **max_age**: Cookie lifetime in seconds
- **secure**: Only send over HTTPS
- **http_only**: Not accessible via JavaScript
- **same_site**: CSRF protection ("Strict", "Lax", or "None" - case insensitive)
To delete a cookie from the browser, use the `delete` method on the cookies collection. This sets `max_age=0` which tells the browser to remove the cookie.
You can iterate over cookies or access them by name:
```python
from robyn import Request, Response, Headers
@app.get("/debug")
def debug_cookies(request: Request):
response = Response(200, Headers({}), "Cookies set")
response.set_cookie("a", "1")
response.set_cookie("b", "2")
# Get all cookie names
names = response.cookies.keys()
# Iterate over cookies
for name in response.cookies:
print(f"Cookie: {name}")
# Check if cookie exists
if "a" in response.cookies:
print("Cookie 'a' exists")
return response
```
## Request Headers
Batman, now wanted to know how to read request headers. Robyn explained that he could use the `request.headers` field to read request headers. For example, he could read the `Content-Type` header by writing:
### Local Request Headers
Either, by using the `headers` field in the `Request` object:
```python
from robyn import Request
@app.get("/")
def binary_output_response_sync(request: Request):
headers = request.headers
print("These are the request headers: ", headers)
existing_header = headers.get("exisiting_header")
existing_header = headers.get("exisiting_header", "default_value")
exisiting_header = headers["exisiting_header"] # This syntax is also valid
headers.set("modified", "modified_value")
headers["new_header"] = "new_value" # This syntax is also valid
print("These are the modified request headers: ", headers)
return ""
```
`add_request_header` appends the header to the list of headers, while `set_request_header` replaces the header if it exists.
```python
app.set_request_header("server", "robyn")
```
---
## Status Codes
After learning about response formats and headers, Batman learned to set status codes for his responses.
```python
from robyn import status_codes, Request
@app.get("/response")
async def response(request: Request):
return Response(status_code=status_codes.HTTP_200_OK, headers=Headers({}), description="OK")
```
---
## What's next?
Great, now Robyn, what is the `Request` Object that you keep talking about?, Batman said. "Next section", said Robyn.
- [The Request Object](/documentation/en/api_reference/request_object)
Batman was also interested to know about the architecture of Robyn. "Next section", said Robyn.
- [Architecture](/documentation/en/architecture)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/graphql-support.mdx
================================================
export const description =
'On this page, we`ll understand how GraphQL support is provided in the existing Robyn codebase to ensure faster data fetching in modern webapps .'
## GraphQL Support [(With Strawberry 🍓)](https://strawberry.rocks/)
This is in a very early stage right now. We will have a much more stable version when we have a stable API for Views and View Controllers.
## Step 1: Creating a virtualenv
To ensure that there are isolated dependencies, we will use virtual environments.
```bash {{ title: 'pip' }}
python3 -m venv venv
```
## Step 2: Activate the virtualenv and install Robyn
```bash {{ title: 'pip' }}
source venv/bin/activate
```
```bash {{ title: 'pip' }}
pip install robyn strawberry-graphql
```
## Step 3: Coding the App
```python {{ title: 'python' }}
from typing import List, Optional
from robyn import Robyn, jsonify
import json
import dataclasses
import strawberry
import strawberry.utils.graphiql
@strawberry.type
class User:
name: str
@strawberry.type
class Query:
@strawberry.field
def user(self) -> Optional[User]:
return User(name="Hello")
schema = strawberry.Schema(Query)
app = Robyn(__file__)
@app.get("/", const=True)
async def get():
return strawberry.utils.graphiql.get_graphiql_html()
@app.post("/")
async def post(request):
body = request.json()
query = body["query"]
variables = body.get("variables", None)
context_value = {"request": request}
root_value = body.get("root_value", None)
operation_name = body.get("operation_name", None)
data = await schema.execute(
query,
variables,
context_value,
root_value,
operation_name,
)
return jsonify(
{
"data": (data.data),
**({"errors": data.errors} if data.errors else {}),
**({"extensions": data.extensions} if data.extensions else {}),
}
)
if __name__ == "__main__":
app.start(port=8080, host="0.0.0.0")
```
Let us try to decipher the usage line by line.
Now, we are initializing a Robyn app. For us, to serve a GraphQl app, we need to have a `get` route to return the `GraphiQL(ide)` and then a post route to process the `GraphQl` request.
We are populating the html page with the GraphiQL IDE using `strawberry`. We are using `const=True` to precompute this population. Essentially, making it very fast and bypassing the execution overhead in this get request.
Finally, we are getting params(body, query, variables, context_value, root_value, operation_name) from the `request` object.
```python {{ title: 'python' }}
@app.post("/")
async def post(request):
body = request.json()
query = body["query"]
variables = body.get("variables", None)
context_value = {"request": request}
root_value = body.get("root_value", None)
operation_name = body.get("operation_name", None)
data = await schema.execute(
query,
variables,
context_value,
root_value,
operation_name,
)
return jsonify(
{
"data": (data.data),
**({"errors": data.errors} if data.errors else {}),
**({"extensions": data.extensions} if data.extensions else {}),
}
)
```
The above is the example for just one route. You can do the same for as many as you like. :)
## What's next?
That's all folks. :D Keep an eye out for more updates on this page. We will be adding more examples and documentation as we go along.
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/index.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
Once upon a time in the city of Gotham, there was a powerful superhero named Robyn. Robyn had a unique set of abilities that allowed it to fetch information from the far corners of the internet. It could send requests and receive responses at lightning speed, and its prowess was admired by developers everywhere.
One day, Batman approached Robyn for help with building a web application. Batman had heard about Robyn's powerful features and wanted to harness them to create a remarkable application. Batman was looking for an ally and in Robyn, he found the best one!
## Installing Robyn
Robyn is a Python library that you can install using `pip` or `conda`
```bash {{ title: 'pip' }}
pip install robyn
```
```bash {{ title: 'conda' }}
conda install robyn -c conda-forge
```
While there are other more extensions of Robyn like
```bash {{ title: 'pip' }}
pip install "robyn[templating]"
```
```bash {{ title: 'conda' }}
conda install "robyn[templating]" -c conda-forge
```
It is recommended to install the base package first and then install the extensions as needed.
## What's next?
Now, we can start using Robyn to build our application.
- [Getting Started](/documentation/en/api_reference/getting_started)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/mcps.mdx
================================================
# Model Context Protocol (MCP)
> **⚠️ Experimental**: MCP support is experimental and may change.
Robyn supports MCP, allowing AI applications like Claude Desktop to connect to your application's resources and tools.
## Quick Start
```python
from robyn import Robyn
app = Robyn(__file__)
@app.mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
return f"Resource echo: {message}"
@app.mcp.tool()
def echo_tool(message: str) -> str:
return f"Tool echo: {message}"
@app.mcp.prompt()
def echo_prompt(message: str) -> str:
return f"Please process: {message}"
app.start()
```
## Features
- Auto-generated JSON schemas from function signatures
- URI templates with parameter extraction
- JSON-RPC 2.0 protocol compliance
- Type-aware parameter handling
## Decorator Reference
### `@app.mcp.resource(uri, name=None, description=None, mime_type=None)`
Register a resource that can be read by clients.
```python
@app.mcp.resource("user://{user_id}/profile")
def user_profile(user_id: str) -> str:
return f"Profile for user {user_id}"
```
### `@app.mcp.tool(name=None, description=None, input_schema=None)`
Register a tool that can be executed by AI models.
```python
@app.mcp.tool()
def greet(name: str, formal: bool = False) -> str:
if formal:
return f"Good day, {name}."
return f"Hi {name}!"
```
### `@app.mcp.prompt(name=None, description=None, arguments=None)`
Register a prompt template for AI workflows.
```python
@app.mcp.prompt()
def code_review(code: str, language: str = "python") -> str:
return f"Please review this {language} code: {code}"
```
## Type Support
Supported types: `str`, `int`, `float`, `bool`, `List`, `Dict`
## URI Templates
Extract parameters from URIs:
```python
@app.mcp.resource("user://{user_id}/posts/{post_id}")
def get_user_post(user_id: str, post_id: str) -> str:
return f"Post {post_id} from user {user_id}"
```
## Client Usage
Test with curl:
```bash
# List resources
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": {}}'
```
## Integration with Claude Desktop
To connect your Robyn MCP server to Claude Desktop:
1. Start your Robyn app with MCP endpoints
2. Configure Claude Desktop to connect to `http://localhost:8080/mcp`
3. Use the registered resources, tools, and prompts in your conversations
## Error Handling
```python
@app.mcp.tool()
def divide(a: float, b: float) -> str:
if b == 0:
raise ValueError("Division by zero")
return str(a / b)
```
## Testing
```bash
# Unit tests
python examples/mcp.py test-unit
# Live tests
python examples/mcp.py test-live
# All tests
python examples/mcp.py test-all
```
## Configuration
MCP runs at `/mcp` endpoint using JSON-RPC 2.0 over HTTP. No additional setup required.
## Claude Desktop Integration
1. Start your Robyn server
2. Configure Claude Desktop to connect to `http://localhost:8080/mcp`
3. Use resources, tools, and prompts in conversations
See `examples/mcp.py` for a complete example.
For more information: https://modelcontextprotocol.io/
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/middlewares.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## Working with Middlewares and Events
As Batman's application grew more complex, Robyn taught him about middlewares, startup and shutdown events, and even working with WebSockets. Batman learned how to create functions that could execute before or after a request, manage the application's life cycle, and handle real-time communication with clients using WebSockets.
## Handling Events
Batman discovered that he could add startup and shutdown events to manage his application's life cycle. He added the following code to define these events:
Batman was excited to learn that he could add events as functions as well as decorators.
Batman learned to use both sync and async functions for middlewares. He wrote the following code to add a middleware that would execute before and after each request.
A before request middleware is a function that executes before each request. It can modify the request object or perform any other operation before the request is processed.
An after request middleware is a function that executes after each request. It can modify the response object or perform any other operation after the request is processed.
Every before request middleware should accept a request object and return a request object. Every after-request middleware should accept a response object and return a response object on happy case scenario. After-request middlewares can also optionally accept the request object as the first parameter to access request data.
The execution of the before request middleware is stopped if any of the before request middleware returns a response object. The response object is returned to the client without executing the after request middleware or the main entry point code.
```python
from robyn import Request, Response
@app.before_request("/")
async def hello_before_request(request: Request):
request.headers.set("before", "sync_before_request")
return request
@app.after_request("/")
def hello_after_request(response: Response):
response.headers.set("after", "sync_after_request")
return response
```
```python {{ title: 'after_request with request access' }}
from robyn import Request, Response
@app.after_request("/")
def hello_after_request(request: Request, response: Response):
# Access request data in after_request
response.headers.set("request_path", request.url.path)
response.headers.set("after", "sync_after_request")
return response
```
---
## What's next?
Robyn - Great, you're now familiar with the certain advanced concepts of Robyn.
Batman - "Authentication! I want to learn about authentication. I want to make sure that only the right people can access my application."
Robyn - Yes, Authentication!
- [Authentication](/documentation/en/api_reference/authentication)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/multiprocess_execution.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## Multiprocess Execution
Batman wondered about the behaviour of variables in a Robyn multiprocessing environment.
Robyn reassured that it can indeed support them! i.e, handlers can be dispatched to multiple threads.
Any variable used in a multiprocessing environment is shared across multiple processes.
Whilst using multithreading in Robyn, the variables are not protected from multiple threads access by default.
If one needs a variable to be protected within a process, while accessing it from different threads, one can use `multiprocessing.Value` for achieving the required protection.
```python
import threading
import time
from multiprocessing import Value
from robyn import Robyn, Request
app = Robyn(__file__)
count: Value = Value("i", 0)
def counter():
while True:
count.value += 1
time.sleep(0.2)
print(count.value, "added 1")
@app.get("/")
def index(request: Request):
return f"{count.value}"
threading.Thread(target=counter, daemon=True).start()
app.start()
```
---
## What's next?
Batman wondered if it was possible to use Rust directly from Robyn's codebase.
Robyn showed him the path.
[Using Rust Directly](/documentation/en/api_reference/using_rust_directly)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/openapi.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## OpenAPI Docs a.k.a Swagger
After deploying the application, Batman got multiple queries from the users on how to use the endpoints. Robyn showed him how to generate OpenAPI specifications for his application.
Out of the box, the following endpoints are setup for you:
- `/docs` The Swagger UI
- `/openapi.json` The JSON Specification
To use a custom openapi configuration, you can:
- Place the `openapi.json` config file in the root directory.
- Or, pass the file path to the `openapi_file_path` parameter in the `Robyn()` constructor. (the parameter gets priority over the file).
However, if you don't want to generate the OpenAPI docs, you can disable it by passing `--disable-openapi` flag while starting the application.
```bash
python app.py --disable-openapi
```
## How to use?
- Query Params: The typing for query params can be added as `def get(r: Request, query_params: GetRequestParams)` where `GetRequestParams` is a subclass of `QueryParams`
- Path Params are defaulted to string type (ref: https://en.wikipedia.org/wiki/Query_string)
```python
from robyn.robyn import QueryParams
from robyn import Robyn, Request
app = Robyn(
file_object=__file__,
openapi=OpenAPI(
info=OpenAPIInfo(
title="Sample App",
description="This is a sample server application.",
termsOfService="https://example.com/terms/",
version="1.0.0",
contact=Contact(
name="API Support",
url="https://www.example.com/support",
email="support@example.com",
),
license=License(
name="BSD2.0",
url="https://opensource.org/license/bsd-2-clause",
),
externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"),
components=Components(),
),
),
)
@app.get("/")
async def welcome():
"""welcome endpoint"""
return "hi"
class GetRequestParams(QueryParams):
appointment_id: str
year: int
@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"])
async def get(r: Request, query_params: GetRequestParams):
"""Get Name by ID"""
return r.query_params
@app.delete("/users/:name", openapi_tags=["Name"])
async def delete(r: Request):
"""Delete Name by ID"""
return r.path_params
if __name__ == "__main__":
app.start()
```
## How does it work with subrouters?
```python
from robyn.robyn import QueryParams
from robyn import Request, SubRouter
subrouter: SubRouter = SubRouter(__name__, prefix="/sub")
@subrouter.get("/")
async def subrouter_welcome():
"""welcome subrouter"""
return "hiiiiii subrouter"
class SubRouterGetRequestParams(QueryParams):
_id: int
value: str
@subrouter.get("/name")
async def subrouter_get(r: Request, query_params: SubRouterGetRequestParams):
"""Get Name by ID"""
return r.query_params
@subrouter.delete("/:name")
async def subrouter_delete(r: Request):
"""Delete Name by ID"""
return r.path_params
app.include_router(subrouter)
```
## Other Specification Params
We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example using request & response bodies below:
```python
from robyn.types import JSONResponse, Body
class Initial(Body):
is_present: bool
letter: Optional[str]
class FullName(Body):
first: str
second: str
initial: Initial
class CreateItemBody(Body):
name: FullName
description: str
price: float
tax: float
class CreateResponse(JSONResponse):
success: bool
items_changed: int
@app.post("/")
def create_item(request: Request, body: CreateItemBody) -> CreateResponse:
return CreateResponse(success=True, items_changed=2)
```
With the reference documentation deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City.
## Using Pydantic Models
If you have Pydantic installed (`pip install "robyn[pydantic]"` or `pip install "robyn[all]"`), you can use Pydantic `BaseModel` classes directly as handler parameter annotations. Robyn will automatically validate the request body **and** generate a rich OpenAPI schema — including property types, required fields, defaults, and `$ref` for nested models.
```python
from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
email: str
age: int
active: bool = True
@app.post("/users", openapi_tags=["Users"])
def create_user(user: UserCreate) -> dict:
"""Create a new user"""
return {"name": user.name}
```
For the full guide on Pydantic validation, nested models, error responses, and OpenAPI integration, see the dedicated [Pydantic Integration](/documentation/en/api_reference/pydantic) page.
## What's next?
Batman wondered about whether Robyn handlers can be dispatched to multiple processes.
Robyn showed him the way!
[Multiprocess Execution](/documentation/en/api_reference/multiprocess_execution)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/pydantic.mdx
================================================
export const description =
'Learn how to use Pydantic models with Robyn for automatic request body validation and OpenAPI schema generation.'
## Pydantic Integration
Robyn supports [Pydantic](https://docs.pydantic.dev/) v2 as an optional dependency for automatic request body validation and rich OpenAPI schema generation. Validation is **opt-in per handler** — it only activates when you annotate a parameter with a Pydantic `BaseModel`. Handlers without Pydantic annotations are completely unaffected: no parsing, no validation, no overhead. When Pydantic is not installed at all, Robyn never imports it.
## Installation
Install Robyn with Pydantic support using the optional extra:
```bash {{ title: 'Pydantic only' }}
pip install "robyn[pydantic]"
```
```bash {{ title: 'All extras' }}
pip install "robyn[all]"
```
```bash {{ title: 'conda' }}
conda install robyn pydantic -c conda-forge
```
`robyn[all]` includes Pydantic, Jinja2 templating, and any future optional features.
## Basic Usage
Define a Pydantic `BaseModel` and use it as a type annotation on your handler parameter. Robyn will automatically parse the incoming JSON body, validate it against the model, and inject the validated instance into your handler.
```python {{ title: 'Synchronous' }}
from pydantic import BaseModel
from robyn import Robyn
app = Robyn(__file__)
class UserCreate(BaseModel):
name: str
email: str
age: int
active: bool = True
@app.post("/users")
def create_user(user: UserCreate):
"""Create a new user"""
return {
"name": user.name,
"email": user.email,
"age": user.age,
"active": user.active,
}
if __name__ == "__main__":
app.start()
```
```python {{ title: 'Asynchronous' }}
from pydantic import BaseModel
from robyn import Robyn
app = Robyn(__file__)
class UserCreate(BaseModel):
name: str
email: str
age: int
active: bool = True
@app.post("/users")
async def create_user(user: UserCreate):
"""Create a new user"""
return {
"name": user.name,
"email": user.email,
"age": user.age,
"active": user.active,
}
if __name__ == "__main__":
app.start()
```
## Validation Errors
When the request body fails validation, Robyn automatically returns a **422 Unprocessable Entity** response with structured error details. You do not need to write any error handling code.
For example, sending `{"name": "Alice", "email": "alice@example.com", "age": "not_a_number"}` would produce:
```json
{
"error": "Validation Error",
"detail": [
{
"type": "int_parsing",
"loc": ["age"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "not_a_number"
}
]
}
```
Missing required fields are also caught:
```json
{
"error": "Validation Error",
"detail": [
{
"type": "missing",
"loc": ["email"],
"msg": "Field required",
"input": {"name": "Alice", "age": 30}
}
]
}
```
## Nested Models
Pydantic models can reference other models. Robyn handles nested validation automatically.
```python
from pydantic import BaseModel
from robyn import Robyn
app = Robyn(__file__)
class Address(BaseModel):
street: str
city: str
zip_code: str
class UserWithAddress(BaseModel):
name: str
email: str
address: Address
@app.post("/users")
def create_user(data: UserWithAddress):
"""Create a user with an address"""
return {"name": data.name, "city": data.address.city}
```
If the nested `address` object is missing or malformed, Robyn returns a 422 with the full error path (e.g. `["address", "city"]`).
## Using with the Request Object
You can combine Pydantic parameters with the standard `Request` object in the same handler. This gives you access to headers, query params, and other request metadata alongside the validated body.
```python
from pydantic import BaseModel
from robyn import Robyn, Request
app = Robyn(__file__)
class UserCreate(BaseModel):
name: str
email: str
age: int
active: bool = True
@app.post("/users")
def create_user(request: Request, user: UserCreate):
"""Create a user — access both raw request and validated model"""
return {
"method": request.method,
"name": user.name,
"email": user.email,
}
```
## Returning Pydantic Models Directly
You can return a Pydantic model instance (or a list of them) directly from a handler. Robyn will automatically serialize it to JSON with the correct `Content-Type` header — no need to call `.model_dump()` manually.
```python {{ title: 'Single model' }}
@app.post("/users")
def create_user(user: UserCreate) -> UserCreate:
"""Validate and echo back the user"""
return user
```
```python {{ title: 'List of models' }}
@app.post("/users/batch")
def create_users(user: UserCreate) -> list[UserCreate]:
"""Return multiple model instances"""
return [user, user]
```
Both forms produce an `application/json` response. The single-model path uses Pydantic's Rust-based `model_dump_json()` for maximum throughput.
## How Validation Is Triggered
Pydantic validation is **annotation-driven, not method-driven**. The router inspects each handler's signature at registration time; any parameter annotated with a `BaseModel` subclass triggers automatic validation of `request.body` when that route is called. This works with every HTTP method — `POST`, `PUT`, `PATCH`, `DELETE`, or any other method that carries a body.
```python {{ title: 'PUT' }}
@app.put("/users/:id")
def update_user(user: UserCreate):
return {"updated": True, "name": user.name}
```
```python {{ title: 'PATCH' }}
@app.patch("/users/:id")
def patch_user(user: UserCreate):
return {"patched": True, "name": user.name}
```
## OpenAPI Integration
When you use Pydantic models, Robyn automatically generates rich JSON Schema in your OpenAPI specification at `/openapi.json`. This includes:
- **Property types** — `string`, `integer`, `boolean`, etc.
- **Required fields** — fields without defaults are listed in `required`
- **Default values** — shown in the schema
- **Nested models** — referenced via `$ref` and placed in `components/schemas`
```python
from pydantic import BaseModel
from robyn import Robyn, Request
app = Robyn(__file__)
class Address(BaseModel):
street: str
city: str
zip_code: str
class UserWithAddress(BaseModel):
name: str
email: str
address: Address
@app.post("/users", openapi_tags=["Users"])
def create_user(request: Request, data: UserWithAddress) -> dict:
"""Create a user with a nested address"""
return {"name": data.name, "city": data.address.city}
```
The generated `/openapi.json` will contain:
```json
{
"paths": {
"/users": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "title": "Name"},
"email": {"type": "string", "title": "Email"},
"address": {"$ref": "#/components/schemas/Address"}
},
"required": ["name", "email", "address"],
"title": "UserWithAddress"
}
}
}
}
}
}
},
"components": {
"schemas": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string", "title": "Street"},
"city": {"type": "string", "title": "City"},
"zip_code": {"type": "string", "title": "Zip Code"}
},
"required": ["street", "city", "zip_code"],
"title": "Address"
}
}
}
}
```
## Pydantic vs Body
Robyn supports two approaches for typed request bodies. Choose the one that fits your needs:
| Feature | `Body` subclass | Pydantic `BaseModel` |
|---|---|---|
| Installation | Built-in | `pip install "robyn[pydantic]"` |
| Validation | No automatic validation | Full validation with detailed errors |
| Error responses | Manual | Automatic 422 with structured errors |
| Return serialization | Manual `dict()` | Auto-serialize model to JSON |
| OpenAPI schema | Basic type inference | Full JSON Schema (types, required, defaults, `$ref`) |
| Nested models | Supported (basic) | Supported (with `$ref` in OpenAPI) |
| Performance overhead | None | Only when Pydantic is installed and used |
Both approaches work with OpenAPI documentation. If you need validation, use Pydantic. If you just need OpenAPI schema hints without validation, `Body` is sufficient.
## Important Notes
- **Opt-in per handler** — Validation only runs on handlers where a parameter is annotated with a Pydantic `BaseModel`. All other handlers (using `Body`, `Request`, path params, etc.) behave exactly as before with zero additional overhead.
- **One Pydantic body per handler** — Each handler can have at most one parameter annotated with a Pydantic model. The entire request body is parsed into that single model. If you need multiple model inputs, compose them into a single parent model with nested fields.
- **Request validation only** — Robyn validates *incoming* request bodies against Pydantic models but does not validate *outgoing* responses. When you return a model instance, it is serialized as-is without re-validation. This is a deliberate design choice for performance — if you constructed the model, it's already valid.
## What's next?
Batman wondered about whether Robyn handlers can be dispatched to multiple processes.
Robyn showed him the way!
[Multiprocess Execution](/documentation/en/api_reference/multiprocess_execution)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/redirection.mdx
================================================
## Redirection
Batman wanted to redirect some endpoints to others. Robyn helped him do so by the following:
```python
from robyn import Robyn, Response
app = Robyn(__file__)
@app.get("/")
async def index():
return Response(
status_code=307,
description="",
headers={"Location": "landing"},
)
@app.get("/landing")
def landing():
return "hii!"
```
---
## What's next?
Now, Batman wanted to have the ability to upload files to the server if any new villain appeared. Robyn introduced him to the file upload and some of the form data features.
- [File Uploads](/documentation/en/api_reference/file-uploads)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/request_object.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## Request Object
The request object is a dataclass that contains all the information about the request. It is available in the route handler as the first argument.
The request object is created in Rust side but is exposed to Python as a dataclass.
Attributes:
query_params (QueryParams): The query parameters of the request. `e.g. /user?id=123 -> {"id": [ "123" ]}`
headers (dict[str, str]): The headers of the request. `e.g. {"Content-Type": "application/json"}`
params (dict[str, str]): The parameters of the request. `e.g. /user/:id -> {"id": "123"}`
body (Union[str, bytes]): The raw body of the request. For JSON payloads, use the `json()` method to parse the body into a dict with proper type preservation.
method (str): The method of the request. `e.g. GET, POST, PUT, DELETE`
ip_addr (Optional[str]): The IP Address of the client
identity (Optional[Identity]): The identity of the client
```python
@dataclass
class Request:
"""
query_params: QueryParams
headers: Headers
path_params: dict[str, str]
body: Union[str, bytes]
method: str
url: Url
form_data: dict[str, str]
files: dict[str, bytes]
ip_addr: Optional[str]
identity: Optional[Identity]
"""
```
## Parsing JSON Body
The `request.json()` method parses the request body as JSON and returns a Python `dict` with full type preservation:
- JSON `null` becomes Python `None`
- JSON numbers become Python `int` or `float`
- JSON booleans become Python `bool`
- JSON strings become Python `str`
- JSON arrays become Python `list`
- JSON objects become Python `dict`
Nested structures are handled recursively up to a maximum depth of 128 levels.
```python
@app.post("/example")
async def handler(request: Request):
data = request.json() # Returns a dict with preserved types
# e.g. {"count": 42, "active": true, "tags": ["a", "b"]}
# -> {"count": 42, "active": True, "tags": ["a", "b"]}
return {"received": data}
```
If the body is not valid JSON or is not a JSON object, a `ValueError` will be raised.
## Extra Path Parameters
Robyn supports capturing extra path parameters using the `*extra` syntax in route definitions. This allows you to capture any additional segments in the URL path that come after the defined route.
For example, if you define a route like this:
```python
@app.get("/sync/extra/*extra")
def sync_param_extra(request: Request):
extra = request.path_params["extra"]
return extra
```
Any additional path segments after `/sync/extra/` will be captured in the `extra` parameter. For instance:
A request to `/sync/extra/foo/bar` would result in `extra = "foo/bar"`
A request to `/sync/extra/123/456/789` would result in `extra = "123/456/789"`
You can access the extra path parameters through `request.path_params["extra"]` in your route handler.
This feature is particularly useful when you need to handle dynamic, nested routes or when you want to capture an unknown number of path segments.
---
## Easy Access Parameters
Instead of manually extracting and converting query parameters and path parameters from the request object, you can declare them directly in your function signature with type annotations. Robyn will automatically resolve and coerce them for you.
Any handler parameter that doesn't match a known request component (`Request`, `QueryParams`, `Headers`, etc.) is treated as an individual path or query parameter.
**Basic usage** — path params and query params with type coercion and defaults.
```python {{ title: 'Typed Params' }}
@app.get("/items/:id")
async def get_item(id: int, q: str, page: int = 1):
# id is coerced from the path param string to int
# q is taken from ?q=...
# page defaults to 1 if not provided
return {"id": id, "q": q, "page": page}
```
```python {{ title: 'Mixed with Request' }}
@app.get("/items/:id")
async def get_item(request: Request, id: int, q: str = ""):
# request is still injected as usual
# id and q are resolved as individual params
return {"id": id, "q": q, "method": request.method}
```
**Optional, List, Bool, and Float params** — Robyn handles common Python types automatically.
- `Optional[T]` — resolves to `None` when not provided
- `List[T]` — collects repeated query params (e.g. `?tag=a&tag=b`)
- `bool` — accepts `true/false`, `1/0`, `yes/no`, `on/off`
- `float` — standard float coercion
**Error handling** — if a required parameter is missing or a value cannot be coerced to the declared type, Robyn returns a `400 Bad Request` response automatically.
```python {{ title: 'Automatic 400' }}
@app.get("/items/:id")
def get_item(id: int, q: str):
return {"id": id, "q": q}
# GET /items/42 -> 400 (missing required 'q')
# GET /items/abc?q=test -> 400 (cannot coerce 'abc' to int)
```
---
## What's next?
Now, Batman wanted to understand the configuration of the Robyn server. He was then introduced to the concept of Robyn env files.
- [Robyn Env](/documentation/en/api_reference/robyn_env)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/robyn_env.mdx
================================================
export const description = 'In this section, we will learn how to configure the server through a configuration file.'
## Configuring the server through an environment file
Batman wanted to configure the server through an environment file. Changing code continuously induced the risk of error.
## Environment Variables
- `ROBYN_PORT`: Specifies the port on which the Robyn server will listen.
- Default: `8080`
- Example: `ROBYN_PORT=3000`
- `ROBYN_HOST`: Specifies the host address for the Robyn server.
- Default: `127.0.0.1`
- Example: `ROBYN_HOST=0.0.0.0`
- `ROBYN_BROWSER_OPEN`: Open the browser on successful start.
- Default: `False`
- Example: `ROBYN_BROWSER_OPEN=True`
- `ROBYN_DEV_MODE`: Configures the dev mode
- Default: `False`
- Example: `ROBYN_DEV_MODE=True`
- `ROBYN_MAX_PAYLOAD_SIZE`: Sets the maximum payload size for HTTP requests and WebSocket messages in bytes.
- Default: `1000000` bytes
- Example: `ROBYN_MAX_PAYLOAD_SIZE=1000000`
You can have a `robyn.env` file to load them automatically in your environment.
These environment variables are typically set in a `robyn.env` file located at the root of the project. The server parses this file at startup to configure itself accordingly.
For more details on the structure and usage of the `robyn.env` file, refer to the documentation snippet:
```bash {{title: 'Sample project directory'}}
--project/
--robyn.env
--index.py
...
```
Sample `robyn.env` file:
```bash {{ title: 'Sample Robyn.env' }}
ROBYN_PORT=8080
ROBYN_HOST=127.0.0.1
RANDOM_ENV=123
ROBYN_BROWSER_OPEN=True
ROBYN_DEV_MODE=True
ROBYN_MAX_PAYLOAD_SIZE=1000000
```
With the web application deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City.
## What's next?
Batman - Thanks, Robyn. Now tell me more.
Robyn - Let us learn about the Middlewares and events now!
- [Middlewares](/documentation/en/api_reference/middlewares)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/scaling.mdx
================================================
export const description =
'Comprehensive guide to scaling Robyn applications from development to production, including multi-process configuration, load balancing, and deployment strategies.'
# Scaling and Production Deployment
Robyn is designed to scale efficiently from development to production. This guide covers scaling strategies, production deployment patterns, and performance optimization techniques.
## Understanding Robyn's Scaling Model
### Multi-Process Architecture
Robyn uses a **shared-nothing multi-process model** optimized for modern multi-core systems. Each process:
- Runs independently with its own Python interpreter and memory space
- Has its own Global Interpreter Lock (GIL), eliminating GIL contention
- Can handle multiple concurrent requests via worker threads
- Scales linearly across CPU cores for true parallelism
- Provides fault isolation - one process crash doesn't affect others
### Worker Threads
Within each process, multiple worker threads provide concurrency:
- Share the same Python interpreter and memory space
- Are subject to the GIL for CPU-bound tasks (use more processes for CPU work)
- Excel at I/O-bound operations where threads can release the GIL
- Handle database calls, HTTP requests, file operations efficiently
- Provide request-level concurrency within the same process
## Scaling Configuration
### Basic Scaling
**Single Process Development**: Start simple during development with auto-reload enabled.
```bash
# Development mode (single process, single worker)
python app.py --dev
# Development with custom port
python app.py --dev --port 3000
```
**Multi-Core Production**: Scale across all available CPU cores for maximum performance.
```bash
# Automatic optimization (recommended)
python app.py --fast
# Manual configuration for 8-core system
python app.py --processes 8 --workers 2
# I/O heavy workloads
python app.py --processes 4 --workers 4
# CPU heavy workloads
python app.py --processes 8 --workers 1
```
### Advanced Configuration
**Hardware-Specific Tuning**: Optimize based on your specific hardware and application characteristics.
```bash
# High-traffic web API (4-core system)
python app.py --processes 4 --workers 3 --log-level INFO
# Data processing service (8-core system)
python app.py --processes 8 --workers 1 --log-level WARNING
# Mixed workload (balanced approach)
python app.py --processes 6 --workers 2 --log-level INFO
# Maximum concurrency (16-core system)
python app.py --processes 8 --workers 4
```
### Configuration Guidelines
**Choosing the Right Configuration**: Use these guidelines to optimize for your specific use case.
```python
# For CPU-intensive applications
# Rule: processes = CPU cores, workers = 1
# Example: 8-core machine
python app.py --processes 8 --workers 1
# For I/O-intensive applications
# Rule: processes = CPU cores / 2, workers = 2-4
# Example: 8-core machine
python app.py --processes 4 --workers 3
# For balanced applications
# Rule: processes = CPU cores / 2, workers = 2
# Example: 8-core machine
python app.py --processes 4 --workers 2
# Memory considerations
# Each process uses ~50-100MB base memory
# Monitor with: ps aux | grep python
```
## Production Deployment Strategies
### Container Deployment
**Docker Containerization**: Deploy Robyn applications using Docker for consistent environments and easy scaling.
**Load Testing**: Use tools like wrk, Apache Bench, or Locust to test different configurations and find optimal settings.
```bash
# Install wrk for load testing
# macOS: brew install wrk
# Ubuntu: sudo apt install wrk
# Test baseline performance
wrk -t4 -c100 -d30s http://localhost:8080/health
# Test with different configurations
# Configuration 1: Single process
python app.py --processes 1 --workers 1 &
wrk -t4 -c100 -d30s http://localhost:8080/api/endpoint
# Configuration 2: Multi-process
python app.py --processes 4 --workers 2 &
wrk -t4 -c100 -d30s http://localhost:8080/api/endpoint
# Configuration 3: Fast mode
python app.py --fast &
wrk -t4 -c100 -d30s http://localhost:8080/api/endpoint
# Compare results and choose the best configuration
```
```python
# Python script for automated testing
import subprocess
import time
import requests
def test_configuration(processes, workers, duration=30):
# Start server
cmd = f"python app.py --processes {processes} --workers {workers}"
server = subprocess.Popen(cmd.split())
time.sleep(2) # Wait for startup
try:
# Run load test
wrk_cmd = f"wrk -t4 -c100 -d{duration}s http://localhost:8080/health"
result = subprocess.run(wrk_cmd.split(), capture_output=True, text=True)
return result.stdout
finally:
server.terminate()
time.sleep(1)
# Test different configurations
configs = [(1, 1), (2, 2), (4, 1), (4, 2), (8, 1)]
for processes, workers in configs:
print(f"\nTesting: {processes} processes, {workers} workers")
result = test_configuration(processes, workers)
print(result)
```
### Environment Variables
**Configuration via Environment**: Use environment variables for deployment flexibility.
```bash
# Production environment variables
export ROBYN_HOST=0.0.0.0
export ROBYN_PORT=8080
export ROBYN_LOG_LEVEL=INFO
export ROBYN_PROCESSES=4
export ROBYN_WORKERS=2
# Database configuration
export DATABASE_URL=postgresql://user:pass@localhost/db
export REDIS_URL=redis://localhost:6379/0
# Application settings
export SECRET_KEY=your-secret-key
export DEBUG=false
export ENVIRONMENT=production
# Start application with environment config
python app.py
```
```python
# app.py - Using environment variables
import os
from robyn import Robyn
app = Robyn(__file__)
# Configuration from environment
HOST = os.getenv("ROBYN_HOST", "127.0.0.1")
PORT = int(os.getenv("ROBYN_PORT", "8080"))
PROCESSES = int(os.getenv("ROBYN_PROCESSES", "1"))
WORKERS = int(os.getenv("ROBYN_WORKERS", "1"))
LOG_LEVEL = os.getenv("ROBYN_LOG_LEVEL", "INFO")
if __name__ == "__main__":
app.start(
host=HOST,
port=PORT,
processes=PROCESSES,
workers=WORKERS,
log_level=LOG_LEVEL
)
```
---
## What's next?
Now, Batman wanted to extend Robyn. Robyn told him about the advanced features.
- [Advanced Features](/documentation/en/api_reference/advanced_features)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/server_sent_events.mdx
================================================
export const description =
'Learn how to implement Server-Sent Events (SSE) in Robyn for real-time server-to-client communication.'
# Server-Sent Events (SSE)
After learning about [form data handling](/documentation/en/api_reference/form_data), Batman realized he needed a way to push real-time updates to his crime monitoring dashboard. Criminals don't wait for Batman to refresh his browser!
He discovered Server-Sent Events (SSE) - a perfect solution for one-way communication from server to client over HTTP. SSE allows Batman to stream live data to his dashboard without the complexity of full bidirectional communication.
"This is exactly what I need for my crime alerts!" Batman exclaimed. "I can push updates to the dashboard instantly when new crimes are detected."
Server-Sent Events are ideal for:
- Real-time notifications
- Live data feeds
- Progress updates
- Chat applications (server-to-client only)
- Dashboard updates
- Log streaming
## How does it work?
Batman can create Server-Sent Events streams by using the `SSEResponse` and `SSEMessage` classes. He can use both regular generators and async generators depending on his needs:
- **Regular generators**: Perfect for simple data streams or when working with synchronous operations
- **Async generators**: Ideal when Batman needs to perform async operations like database queries or API calls within the stream
```python {{ title: 'Basic SSE Stream' }}
from robyn import Robyn, SSEResponse, SSEMessage
import time
app = Robyn(__file__)
@app.get("/events")
def stream_events(request):
def event_generator():
for i in range(10):
yield SSEMessage(f"Event {i}", id=str(i))
time.sleep(1)
return SSEResponse(event_generator())
```
```python {{ title: 'JSON Data Stream' }}
from robyn import Robyn, SSEResponse, SSEMessage
import json
import time
app = Robyn(__file__)
@app.get("/events/json")
def stream_json_events(request):
def json_event_generator():
for i in range(5):
data = {
"id": i,
"message": f"Update {i}",
"timestamp": time.time()
}
yield SSEMessage(
json.dumps(data),
event="update",
id=str(i)
)
time.sleep(2)
return SSEResponse(json_event_generator())
```
```python {{ title: 'Async Generator Stream' }}
from robyn import Robyn, SSEResponse, SSEMessage
import asyncio
import time
app = Robyn(__file__)
@app.get("/events/async")
async def stream_async_events(request):
async def async_event_generator():
for i in range(8):
# Simulate async work like database calls
await asyncio.sleep(0.5)
yield SSEMessage(
f"Async message {i} - {time.strftime('%H:%M:%S')}",
event="async",
id=str(i)
)
return SSEResponse(async_event_generator())
```
---
## Async Generators
When Batman needs to perform async operations during his SSE streams - like fetching data from databases or making API calls - he uses async generators with `async def` and `await`. This allows him to handle multiple streams concurrently without blocking other operations.
The key difference is using `async def` for the generator function and `await` for async operations inside the generator:
```python {{ title: 'Database Stream' }}
from robyn import Robyn, SSEResponse, SSEMessage
import asyncio
import json
import time
app = Robyn(__file__)
@app.get("/events/database")
async def stream_database_events(request):
async def database_event_generator():
for i in range(10):
# Simulate async database query
await asyncio.sleep(0.3)
# Simulate fetching data from database
data = {
"crime_id": i,
"location": f"Gotham District {i}",
"severity": "high" if i % 2 == 0 else "low",
"timestamp": time.time()
}
yield SSEMessage(
json.dumps(data),
event="crime_alert",
id=str(i)
)
return SSEResponse(database_event_generator())
```
```python {{ title: 'API Integration Stream' }}
from robyn import Robyn, SSEResponse, SSEMessage
import asyncio
import aiohttp
import json
app = Robyn(__file__)
@app.get("/events/api")
async def stream_api_events(request):
async def api_event_generator():
async with aiohttp.ClientSession() as session:
for i in range(5):
try:
# Make async API call
async with session.get(f"https://api.example.com/data/{i}") as response:
data = await response.json()
yield SSEMessage(
json.dumps(data),
event="api_update",
id=str(i)
)
except Exception as e:
yield SSEMessage(
f"Error fetching data: {str(e)}",
event="error",
id=f"error_{i}"
)
await asyncio.sleep(1)
return SSEResponse(api_event_generator())
```
---
## What's next?
Batman has mastered Server-Sent Events and can now stream real-time updates to his crime dashboard. While SSE is perfect for one-way communication from server to client, Batman realizes he needs bidirectional communication for more interactive features like real-time chat with his allies.
Next, he wants to explore how to handle bidirectional communication with [WebSockets](/documentation/en/api_reference/websockets) for more interactive features.
If Batman needs to handle unexpected situations, he'll learn about [Exception handling](/documentation/en/api_reference/exceptions) to make his applications more robust.
For scaling his crime monitoring system across multiple processes, Batman will explore [Scaling the Application](/documentation/en/api_reference/scaling).
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/templating.mdx
================================================
export const description =
'On this page, we’ll dive into the different conversation endpoints you can use to manage conversations programmatically.'
## Templating.
Batman wanted to quickly render html pages on the website. He wanted to use a templating engine to render the html pages. Robyn told him that he can use the Jinja2 templating engine to render the html pages. He can use the `JinjaTemplate` class to render the html pages.
Batman was excited to learn that he could add events as functions as well as decorators.
```html
Results
Hello {{ framework }}! You're using {{ templating_engine }}.
```
## Supporting Custom Templating Engines
Batman was also super excited to know that Robyn allows the support of custom templating engines.
To do that, you need to import the `TemplateInterface` from `robyn.templating`
```python
from robyn.templating import TemplateInterface
```
Then You need to have a `render_template` method inside your implementation. So, an example would look like the following:
```python
class JinjaTemplate(TemplateInterface):
def __init__(self, directory, encoding="utf-8", followlinks=False):
self.env = Environment(
loader=FileSystemLoader(
searchpath=directory, encoding=encoding, followlinks=followlinks
)
)
def render_template(self, template_name: str, **kwargs):
return self.env.get_template(template_name).render(**kwargs)
```
## What's next?
Now, Batman wanted to have the ability to redirect the endpoints.
- [Redirection](/documentation/en/api_reference/redirection)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/timeout_configuration.mdx
================================================
export const description =
'Configure timeout settings to handle high-concurrency scenarios and prevent resource exhaustion.'
# Timeout Configuration
Robyn supports comprehensive timeout configuration to handle high-concurrency scenarios and prevent resource exhaustion like the "Too many open files" error. {{ className: 'lead' }}
## Configuration Options
### Method Parameters
Configure timeouts directly in the `app.start()` method:
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def hello(request):
return "Hello, world!"
# Configure timeout settings
app.start(
host="0.0.0.0",
port=8080,
client_timeout=30, # Client connection timeout (seconds)
keep_alive_timeout=20 # Keep-alive timeout (seconds)
)
```
### Environment Variables
Override configuration using environment variables:
```bash
# Set timeout configurations
export ROBYN_CLIENT_TIMEOUT=45
export ROBYN_KEEP_ALIVE_TIMEOUT=30
# Start your application
python app.py
```
## Configuration Parameters
| Parameter | Default | Description | Environment Variable |
|-----------|---------|-------------|---------------------|
| `client_timeout` | 30 | Maximum time (seconds) for client request processing | `ROBYN_CLIENT_TIMEOUT` |
| `keep_alive_timeout` | 20 | Time (seconds) to keep idle connections alive | `ROBYN_KEEP_ALIVE_TIMEOUT` |
## Usage Examples
### Basic Configuration
```python
# Minimal timeout configuration
app.start(client_timeout=30)
```
### High-Traffic Production Setup
```python
# Optimized for high-traffic scenarios
app.start(
host="0.0.0.0",
port=8080,
client_timeout=60, # Allow longer processing time
keep_alive_timeout=15 # Shorter keep-alive for faster turnover
)
```
### Development Setup
```python
# Development-friendly settings
app.start(
client_timeout=300, # Long timeout for debugging
keep_alive_timeout=60 # Longer keep-alive for testing
)
```
### Load Testing Configuration
```python
# Optimized for load testing with tools like wrk
app.start(
client_timeout=10, # Quick timeouts
keep_alive_timeout=5 # Fast connection turnover
)
```
## Environment Variable Priority
Environment variables take precedence over method parameters:
```python
# This will use ROBYN_CLIENT_TIMEOUT=60 if set, otherwise 30
app.start(client_timeout=30)
```
## Troubleshooting
### "Too Many Open Files" Error
If you encounter file descriptor exhaustion:
1. **Increase system limits:**
```bash
ulimit -n 65536
```
2. **Optimize timeout settings:**
```python
app.start(
client_timeout=15, # Shorter timeouts
keep_alive_timeout=5 # Faster connection cleanup
)
```
3. **Use environment variables for deployment:**
```bash
export ROBYN_CLIENT_TIMEOUT=15
export ROBYN_KEEP_ALIVE_TIMEOUT=5
```
### Performance Tuning
**For high-throughput APIs:**
- Lower `keep_alive_timeout` (5-15s)
- Moderate `client_timeout` (15-30s)
**For long-running operations:**
- Higher `client_timeout` (60-300s)
- Standard `keep_alive_timeout` (20-30s)
## Best Practices
1. **Always set explicit timeouts** in production
2. **Use environment variables** for deployment-specific configuration
3. **Test timeout settings** with realistic load patterns
4. **Start with conservative values** and tune based on metrics
## Migration Guide
### From Previous Versions
If upgrading from earlier Robyn versions, the default behavior changes:
**Before (infinite timeout):**
```python
# Previously: no timeout (could cause resource exhaustion)
app.start(host="0.0.0.0", port=8080)
```
**After (sensible defaults):**
```python
# Now: automatic 30s client timeout, 20s keep-alive
app.start(host="0.0.0.0", port=8080)
# Equivalent to:
app.start(
host="0.0.0.0",
port=8080,
client_timeout=30,
keep_alive_timeout=20
)
```
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/using_rust_directly.mdx
================================================
## Using Rust to extend Robyn
There may be occasions where Batman may be working with a high computation task, or a task that requires a lot of memory. In such cases, he may want to use Rust to implement that task. Robyn introduces a special way to do this. Not only you can use Rust to extend Python code, you can do it while maintaining the hot reloading nature of your codebase. Making it *feel* like an interpreted version in many situations.
The first thing you need to is to create a Rust file. Let's call it `hello_world.rs`. You can do it using the cli:
Then you can open the file and write your Rust code. For example, let's write a function that returns a string.
```rust
// hello_world.rs
// rustimport:pyo3
use pyo3::prelude::*;
#[pyfunction]
fn square(n: i32) -> i32 {
n * n
// this is another comment
}
```
Every Rust file that you create using the cli will have a special comment at the top of the file. This comment is used by Robyn to know which dependencies to import. In this case, we are importing the `pyo3` crate. You can import as many crates as you want. You can also import crates from crates.io. For example, if you want to use the `rusqlite` crate, you can do it like this:
```rust
// rustimport:pyo3
//:
//: [dependencies]
//: rusqlite = "0.19.0"
use pyo3::prelude::*;
#[pyfunction]
fn square(n: i32) -> i32 {
n * n * n
// this is another comment
}
```
Then you can import the function in your Python code and use it.
```python
from hello_world import square
print(square(5))
```
To run the code, you need to use the `--compile-rust-path` flag. This will compile the Rust code and run it. You can also use the `--dev` flag to watch for changes in the Rust code and recompile it on the fly.
```python
python -m robyn --compile-rust-path "." --dev
```
An example of a Robyn app with a Rust file that using the `rusqlite` crate to connect to a database and return the number of rows in a table: https://github.com/sansyrox/rusty-sql
## What's next?
Batman was curious to know what else he could do with Robyn.
Robyn told him to keep an eye on the GraphQl support.
[GraphQl Support](/documentation/en/api_reference/graphql_support)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/websockets.mdx
================================================
export const description =
'Learn how to use Robyn\'s WebSocket API for real-time, bidirectional communication — including message streaming, connect/close callbacks, broadcasting, and common patterns like live updates, presence tracking, and low-latency messaging.'
## WebSockets {{ tag: 'WebSockets', label: 'WebSockets' }}
After mastering [Server-Sent Events](/documentation/en/api_reference/server_sent_events) for one-way communication, Batman realized he needed something more powerful. When Commissioner Gordon wanted to chat with him in real-time during crisis situations, Batman needed bidirectional communication.
"SSE is great for pushing updates to my dashboard," Batman thought, "but I need two-way communication for coordinating with my allies!"
To handle real-time bidirectional communication, Batman learned how to work with WebSockets using Robyn's modern decorator-based API. Under the hood, messages flow through Rust channels for maximum performance — no Python GIL overhead during message dispatch.
The `receive_text()` method blocks until the next message arrives from the client. It is backed by a Rust `tokio::mpsc` channel, so the Python handler genuinely suspends without holding the GIL.
When the client disconnects, `receive_text()` raises `WebSocketDisconnect`. You can either catch it explicitly or let the internal wrapper handle it silently.
To send a message to all connected clients on the same WebSocket endpoint, use the `broadcast()` method.
```python {{ title: 'Broadcast' }}
@app.websocket("/chat")
async def handler(websocket):
while True:
msg = await websocket.receive_text()
# Send to all connected clients
await websocket.broadcast(f"User {websocket.id}: {msg}")
# Also send a confirmation to this client only
await websocket.send_text("Your message was sent")
```
---
## Query Parameters {{ tag: 'query_params', label: 'query_params' }}
You can access query parameters from the WebSocket connection URL via `websocket.query_params`.
```python {{ title: 'Query Params' }}
@app.websocket("/ws")
async def handler(websocket):
name = websocket.query_params.get("name")
role = websocket.query_params.get("role")
if name == "gordon" and role == "commissioner":
await websocket.broadcast("Gordon authorized!")
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"Hello {name}: {msg}")
```
---
## Easy Access Query Parameters {{ tag: 'easy_access', label: 'easy_access' }}
Instead of manually calling `websocket.query_params.get(...)`, you can declare typed query parameters directly in your handler, `on_connect`, and `on_close` signatures. Robyn will automatically resolve and coerce them — just like HTTP easy access parameters.
Parameters with defaults are optional. Parameters without defaults are required — if missing, the connection is rejected with an error message.
To programmatically close a WebSocket connection from the server side, use `websocket.close()`. This will:
1. Close the WebSocket connection.
2. Remove the client from the WebSocket registry.
3. Cause any pending `receive_text()` to raise `WebSocketDisconnect`.
```python {{ title: 'Server Close' }}
@app.websocket("/ws")
async def handler(websocket):
while True:
msg = await websocket.receive_text()
if msg == "quit":
await websocket.close()
break
await websocket.send_text(f"Got: {msg}")
```
---
## Connect and Close Callbacks {{ tag: 'Callbacks', label: 'Callbacks' }}
You can attach optional `on_connect` and `on_close` callbacks to your WebSocket handler. These are decorators on the handler function itself.
- `on_connect` is called when a new client connects. Its return value is sent to the client as the first message.
- `on_close` is called when the connection closes. Its return value is sent to the client as the final message.
Both callbacks receive a `websocket` object with access to `id` and `query_params`. Both are optional.
The `websocket` object passed to handlers exposes the following methods and properties:
| Method / Property | Description |
|---|---|
| `await websocket.receive_text()` | Block until next message; raises `WebSocketDisconnect` on close |
| `await websocket.receive_bytes()` | Block until next binary message; raises `WebSocketDisconnect` on close |
| `await websocket.receive_json()` | Same as `receive_text()` but JSON-decoded |
| `await websocket.send_text(data)` | Send string to this client |
| `await websocket.send_bytes(data)` | Send binary data to this client |
| `await websocket.send_json(data)` | Send JSON to this client |
| `await websocket.broadcast(data)` | Send to all clients on this endpoint |
| `await websocket.close()` | Close the connection server-side |
| `websocket.id` | Connection UUID string |
| `websocket.query_params` | Query parameters from the connection URL |
---
## What's next?
As the codebase grew, Batman wanted to onboard the justice league to help him manage the application.
Robyn told him about the different ways he could scale his application, and how to use views and subrouters to make his code more readable.
- [Views and SubRouters](/documentation/en/api_reference/views)
================================================
FILE: docs_src/src/pages/documentation/en/api_reference/zh/getting_started.mdx
================================================
export const description =
'开始使用Robyn构建您的第一个应用程序。'
# 入门指南
## 创建您的第一个Robyn应用程序
让我们从创建一个简单的Robyn应用程序开始。首先,使用pip安装Robyn:
```bash
pip install robyn
```
然后,创建一个新的Python文件 `app.py`:
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def hello_world():
return "Hello, World!"
if __name__ == "__main__":
app.start()
```
现在运行您的应用程序:
```bash
python app.py
```
访问 `http://localhost:8080`,您应该能看到 "Hello, World!" 消息。
## 命令行选项
Robyn提供了多个命令行选项来配置您的应用程序:
```bash
python app.py --help
```
输出将显示所有可用选项:
```text
options:
-h, --help 显示帮助信息
--processes PROCESSES
选择进程数量。[默认值: 1]
--workers WORKERS 选择工作线程数量。[默认值: 1]
--dev 开发模式。根据文件更改自动重启服务器。
--log-level LOG_LEVEL
设置日志级别
--create 创建新项目模板。
--docs 打开Robyn文档。
--open-browser 成功启动时打开浏览器。
--version 显示Robyn版本。
--compile-rust-path COMPILE_RUST_PATH
编译指定路径中的rust文件。
--create-rust-file CREATE_RUST_FILE
创建指定名称的rust文件。
--disable-openapi 禁用OpenAPI文档。
--fast 快速模式。设置最优的进程、工作线程和日志级别。但您可以覆盖这些设置。
```
## 路由装饰器
Robyn支持所有标准的HTTP方法:
```python
@app.get("/users")
async def get_users():
return {"users": ["user1", "user2"]}
@app.post("/users")
async def create_user(request):
return {"message": "用户已创建"}
@app.put("/users/:id")
async def update_user(request):
return {"message": "用户已更新"}
@app.delete("/users/:id")
async def delete_user(request):
return {"message": "用户已删除"}
```
## 请求参数
您可以通过多种方式访问请求数据:
```python
@app.post("/api/data")
async def handle_data(request):
# 访问JSON数据
json_data = request.json()
# 访问查询参数
query_params = request.query_params
# 访问路径参数
path_params = request.path_params
# 访问请求头
headers = request.headers
return {
"json": json_data,
"query": query_params,
"path": path_params,
"headers": headers
}
```
## 响应类型
Robyn支持多种响应类型:
```python
from robyn import Response, jsonify
@app.get("/text")
async def text_response():
return "纯文本响应"
@app.get("/json")
async def json_response():
return jsonify({"message": "JSON响应"})
@app.get("/custom")
async def custom_response():
return Response(
status_code=201,
description="自定义响应",
headers={"Content-Type": "text/plain"}
)
```
================================================
FILE: docs_src/src/pages/documentation/en/architecture.mdx
================================================
export const description = 'Robyn is a Python web server that employs the tokio runtime and leverages a blend of Python and Rust, enabling efficient request handling through a worker event cycle, multi-core scaling, and introducing "Const Requests" for optimized, cached responses in a multi-threaded environment.'
## Robyn Architecture Overview
Robyn's unique architecture combines Python's ease of development with Rust's performance. This hybrid design allows developers to write familiar Python code while benefiting from Rust's speed and memory safety.
## The Python-Rust Hybrid Design
### Two-Layer Architecture
Robyn operates on two interconnected but distinct layers:
**Python Layer (Developer Interface)**:
- Route definitions and decorators (`@app.get`, `@app.post`, etc.)
- Request parameter injection and validation
- Business logic execution
- Middleware configuration
- Response formatting
**Rust Layer (Performance Engine)**:
- HTTP request parsing and validation
- URL routing and pattern matching
- WebSocket connection management
- Static file serving
- Response serialization
- Memory management
### Communication Bridge
The layers communicate through **PyO3**, a Rust crate that enables seamless Python-Rust interoperability:
1. **Function Registration**: Python route handlers are registered with the Rust runtime at startup
2. **Request Flow**: Rust handles incoming HTTP requests and calls Python handlers via PyO3
3. **Response Processing**: Python responses are converted back to Rust for efficient HTTP serialization
### Why This Design Works
- **Best of Both Worlds**: Python's productivity with Rust's performance
- **Zero-Copy Operations**: Minimal data copying between layers
- **Memory Safety**: Rust prevents common server vulnerabilities
- **Async Integration**: Seamless integration with Python's asyncio
## Server Process Model
Robyn is built on a multi-process, multi-threaded model that maximizes hardware utilization:
## Master Process
The master process in Robyn is responsible for initializing the server, managing worker processes, and handling signals. It creates a socket and passes it to the worker processes, allowing them to accept connections. The master process is implemented in Python, providing a familiar interface for developers while leveraging Rust's performance for core operations.
```python
216:257:robyn/__init__.py
def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = True):
"""
Starts the server
:param host str: represents the host at which the server is listening
:param port int: represents the port number at which the server is listening
:param _check_port bool: represents if the port should be checked if it is already in use
"""
host = os.getenv("ROBYN_HOST", host)
port = int(os.getenv("ROBYN_PORT", port))
open_browser = bool(os.getenv("ROBYN_BROWSER_OPEN", self.config.open_browser))
if _check_port:
while self.is_port_in_use(port):
logger.error("Port %s is already in use. Please use a different port.", port)
try:
port = int(input("Enter a different port: "))
except Exception:
logger.error("Invalid port number. Please enter a valid port number.")
continue
logger.info("Robyn version: %s", __version__)
logger.info("Starting server at http://%s:%s", host, port)
mp.allow_connection_pickling()
run_processes(
host,
port,
self.directories,
self.request_headers,
self.router.get_routes(),
self.middleware_router.get_global_middlewares(),
self.middleware_router.get_route_middlewares(),
self.web_socket_router.get_routes(),
self.event_handlers,
self.config.workers,
self.config.processes,
self.response_headers,
open_browser,
)
```
## Worker Processes
Robyn uses multiple worker processes to handle incoming requests. Each worker process is capable of managing multiple threads, allowing for efficient concurrent processing. The number of worker processes can be configured using the `--processes` flag, with a default of 1.
```python
66:116:robyn/processpool.py
def init_processpool(
directories: List[Directory],
request_headers: Headers,
routes: List[Route],
global_middlewares: List[GlobalMiddleware],
route_middlewares: List[RouteMiddleware],
web_sockets: Dict[str, WebSocket],
event_handlers: Dict[Events, FunctionInfo],
socket: SocketHeld,
workers: int,
processes: int,
response_headers: Headers,
) -> List[Process]:
process_pool = []
if sys.platform.startswith("win32") or processes == 1:
spawn_process(
directories,
request_headers,
routes,
global_middlewares,
route_middlewares,
web_sockets,
event_handlers,
socket,
workers,
response_headers,
)
return process_pool
for _ in range(processes):
copied_socket = socket.try_clone()
process = Process(
target=spawn_process,
args=(
directories,
request_headers,
routes,
global_middlewares,
route_middlewares,
web_sockets,
event_handlers,
copied_socket,
workers,
response_headers,
),
)
process.start()
process_pool.append(process)
return process_pool
```
## Worker Threads
Within each worker process, Robyn utilizes multiple threads to handle requests concurrently. The number of worker threads can be configured using the `--workers` flag. By default, Robyn uses a single worker thread per process.
## Request Processing Flow
Understanding how requests flow through Robyn's hybrid architecture:
### 1. Request Arrival
```
HTTP Request → Rust HTTP Parser → Fast Validation
```
### 2. Routing and Matching
```
Validated Request → Rust Router (matchit crate) → Route Resolution
```
### 3. Parameter Extraction
```
Matched Route → Rust Parameter Parser → Path/Query/Header Extraction
```
### 4. Python Handler Execution
```
Extracted Parameters → PyO3 Bridge → Python Handler → Response
```
### 5. Response Processing
```
Python Response → Rust Serializer → HTTP Response → Client
```
## Rust Integration Deep Dive
Robyn's Rust integration is powered by the **Tokio** async runtime and several high-performance crates:
### Core Components
- **Tokio Runtime**: Handles async I/O and task scheduling
- **Actix Web**: Provides HTTP server functionality
- **PyO3**: Enables Python-Rust communication
- **matchit**: Ultra-fast URL routing with radix tree implementation
- **Serde**: JSON serialization/deserialization
### Performance Benefits
1. **Zero-allocation routing** using compiled radix trees
2. **Memory-efficient HTTP parsing** with minimal allocations
3. **Async task scheduling** without GIL interference
4. **Direct memory access** for static file serving
```python
76:107:src/server.rs
pub fn start(
&mut self,
py: Python,
socket: &PyCell,
workers: usize,
) -> PyResult<()> {
pyo3_log::init();
if STARTED
.compare_exchange(false, true, SeqCst, Relaxed)
.is_err()
{
debug!("Robyn is already running...");
return Ok(());
}
let raw_socket = socket.try_borrow_mut()?.get_socket();
let router = self.router.clone();
let const_router = self.const_router.clone();
let middleware_router = self.middleware_router.clone();
let web_socket_router = self.websocket_router.clone();
let global_request_headers = self.global_request_headers.clone();
let global_response_headers = self.global_response_headers.clone();
let directories = self.directories.clone();
let asyncio = py.import("asyncio")?;
let event_loop = asyncio.call_method0("new_event_loop")?;
asyncio.call_method1("set_event_loop", (event_loop,))?;
let startup_handler = self.startup_handler.clone();
let shutdown_handler = self.shutdown_handler.clone();
```
## Const Requests Optimization
Robyn's "Const Requests" feature provides significant performance improvements for static endpoints:
### How Const Requests Work
1. **Route Registration**: Routes marked with `const=True` are identified at startup
2. **Response Caching**: The first response is cached in Rust memory
3. **Direct Serving**: Subsequent requests bypass Python entirely
4. **Zero Overhead**: Responses are served directly from Rust with minimal CPU usage
### Performance Impact
- **10x faster response times** compared to regular Python handlers
- **Minimal memory usage** with efficient caching
- **No Python GIL contention** for cached responses
- **Ideal for**: Health checks, API metadata, configuration endpoints
### Example Usage
```python
from robyn import Robyn
app = Robyn(__file__)
# Regular route - executes Python on every request
@app.get("/dynamic")
def dynamic_endpoint():
return {"timestamp": time.time()} # Changes every request
# Const route - cached in Rust after first request
@app.get("/health", const=True)
def health_check():
return {"status": "healthy", "version": "1.0.0"} # Static response
# Perfect for API metadata
@app.get("/api/info", const=True)
def api_info():
return {
"name": "My API",
"version": "2.1.0",
"endpoints": ["/users", "/posts", "/health"]
}
```
### When to Use Const Routes
- **Health/status endpoints** that return consistent data
- **API documentation** or metadata endpoints
- **Configuration endpoints** with static values
- **Version information** endpoints
## Scaling Configuration Guide
### Understanding Processes vs Workers
**Processes**:
- Independent Python interpreters
- Share no memory (shared-nothing architecture)
- Each process has its own GIL
- Best for CPU-bound applications
- Recommended: 1 process per CPU core
**Workers** (within each process):
- Threads sharing the same Python interpreter
- Affected by Python's GIL
- Better for I/O-bound operations
- Recommended: 2-4 workers per process
### Configuration Strategies
#### CPU-Intensive Applications
```bash
# Favor more processes, fewer workers
python app.py --processes=8 --workers=1
# Example: Image processing, data analysis, calculations
```
#### I/O-Intensive Applications
```bash
# Favor fewer processes, more workers
python app.py --processes=2 --workers=8
# Example: Database queries, API calls, file operations
```
#### Balanced Applications
```bash
# General-purpose configuration
python app.py --processes=4 --workers=2
# Most web applications fit this pattern
```
### Hardware-Based Recommendations
For a system with **N CPU cores**:
| Application Type | Processes | Workers | Total Concurrency |
|-----------------|-----------|---------|-------------------|
| CPU-bound | N | 1 | N |
| I/O-bound | N/2 | 4 | 2N |
| Balanced | N/2 | 2 | N |
| High-traffic | N | 2 | 2N |
### Performance Testing
Always benchmark your specific application:
```bash
# Test different configurations
python app.py --processes=1 --workers=1 # Baseline
python app.py --processes=2 --workers=2 # Moderate scaling
python app.py --processes=4 --workers=1 # CPU-focused
python app.py --processes=2 --workers=4 # I/O-focused
python app.py --fast # Auto-optimized
```
## Scaling Considerations
- Robyn's multi-process model allows it to scale across multiple CPU cores effectively.
- The combination of Python and Rust allows for both ease of development and high performance.
- Const Requests feature can significantly improve performance for routes with constant output.
- When scaling, consider both the number of processes and workers to find the optimal configuration for your hardware and application needs.
## Development Mode
Robyn provides a development mode that can be activated using the `--dev` flag. This mode is designed for ease of development and includes features like hot reloading. Note that in development mode, multi-process and multi-worker configurations are disabled to ensure consistent behavior during development.
```python
92:101:robyn/argument_parser.py
if self.dev and (self.processes != 1 or self.workers != 1):
raise Exception("--processes and --workers shouldn't be used with --dev")
if self.dev and args.log_level is None:
self.log_level = "DEBUG"
elif args.log_level is None:
self.log_level = "INFO"
else:
self.log_level = args.log_level
```
By understanding these design principles and adjusting the configuration accordingly, developers can leverage Robyn's unique architecture to build high-performance web applications that efficiently utilize system resources.
## Design Diagram
================================================
FILE: docs_src/src/pages/documentation/en/community-resources.mdx
================================================
export const description =
'On this page, we`ll take a look at some community resources contributed by our amazing members to help developers get started with Robyn.'
## Talks
- [EuroPython 2022](https://www.youtube.com/watch?v=AutugvJNVkY&)
- [GeoPython 2022](https://www.youtube.com/watch?v=YCpbCQwbkd4)
- [PyCon US 2022](https://youtu.be/1IiL31tUEVk?t=2101)
- [PyCon Sweden 2021](https://www.youtube.com/watch?v=DK9teAs72Do)
## Blogs
- [Hello, Robyn!](https://www.sanskar.me/posts/hello-robyn)
## Next Steps
Batman was now interes
- [Hosting](/documentation/hosting)
================================================
FILE: docs_src/src/pages/documentation/en/example_app/authentication-middlewares.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Authentication and Authorization Middleware
Batman added middleware to the Robyn application to verify the JWT tokens and to restrict access to certain endpoints based on the user's role.
```python {{ title: 'Setting up Authentication Middlewares' }}
from robyn.authentication import AuthenticationHandler, BearerGetter, Identity
class BasicAuthHandler(AuthenticationHandler):
def authenticate(self, request: Request):
token = self.token_getter.get_token(request)
try:
payload = crud.decode_access_token(token)
username = payload["sub"]
except Exception:
return
with SessionLocal() as db:
user = crud.get_user_by_username(db, username=username)
return Identity(claims={"user": f"{ user }"})
app.configure_authentication(BasicAuthHandler(token_getter=BearerGetter()))
@app.get("/users/me", auth_required=True)
async def get_current_user(request):
user = request.identity.claims["user"]
return user
```
With the web application in place, the Gotham City Police Department could now efficiently manage crime data and track criminal activities in real-time. Batman had successfully used the Robyn web framework to build a real-world application to help fight crime in Gotham City.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/authentication.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Authentication and Authorization
To restrict access to the crime data, Batman added authentication and authorization to the application. He decided to use JWT (JSON Web Token) for authentication. He created a new table for users and added an endpoint for user registration.
## User Model
Batman added a new User model to represent the users who can access the application.
```python {{ title: 'Example request with basic auth' }}
# models.py
from sqlalchemy import Column, Integer, String, Boolean
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
def __repr__(self):
return "".format(
id=self.id,
username=self.username,
hashed_password=self.hashed_password,
)
```
Then in crud.py, he added a new method to create a user.
```python {{ title: 'Example request with basic auth' }}
# crud.py
# also need to do pip install passlib[bcrypt]
from sqlalchemy.orm import Session
from .models import User
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def get_user(db: Session, user_id: int):
return db.query(User).filter(User.id == user_id).first()
def create_user(db: Session, user: User):
hashed_password = get_password_hash(user.password)
db_user = User(username=user.username, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
```
## Authentication Utilities
Batman created utility functions to handle authentication, including hashing passwords and verifying passwords.
```python {{ title: 'Example request with bearer token' }}
# crud.py
# also need to do pip install passlib[bcrypt]
# pip install "python-jose[cryptography]"
from passlib.context import CryptContext
from jose import JWTError, jwt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
ALGORITHM = "HS256"
SECRET_KEY = "your_secret_key"
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str):
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
def authenticate_user(db: Session, username: str, password: str):
user = get_user_by_username(db, username)
if user is None:
return False
if not verify_password(password, user.hashed_password):
return False
created_token = create_access_token(data={"sub": user.username})
return created_token
```
## User Registration Endpoint
Batman added a new endpoint to allow users to register.
```python {{ title: 'Setting up Routes' }}
from . import crud
@app.post("/users/register")
async def register_user(request):
user = request.json()
with SessionLocal() as db:
created_user = crud.create_user(db, user)
return created_user
@app.post("/users/login")
async def login_user(request):
user = request.json()
with SessionLocal() as db:
token = crud.authenticate_user(db, **user)
if token is None:
raise HTTPException(status_code=401, detail="Invalid credentials")
return {"access_token": token}
```
================================================
FILE: docs_src/src/pages/documentation/en/example_app/deployment.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Deploying the Application
After thoroughly testing the web application and ensuring that all features were working as expected, Batman decided it was time to deploy it to a production server. He chose to use a robust and scalable platform, ensuring that his application would be available and performant at all times.
```python {{ title: 'Deploying the Application' }}
python app.py --processes=n --workers=m
```
With the web application deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/index.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
# Real Life Web Apps with Robyn
Batman was tasked with building a web application to manage the crime data in Gotham City. The application would allow the Gotham police department to store and retrieve data on criminal activities, suspects, and their locations. He decided to use the Robyn web framework to build this application efficiently and quickly.
You can find the source code for this application [here](https://github.com/sparckles/example_robyn_app).
## Installing Robyn
The first step was to install Robyn. Batman created a virtual environment and installed Robyn using pip.
```bash
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install robyn
```
## Creating a Robyn Application
Batman wanted to create a Robyn app and was about to create an `src/app.py` before he was told that Robyn comes with a CLI tool to create a new application. He ran the following command to create a new Robyn application.
```bash
$ python -m robyn --create
```
This, would result in the following output.
```bash
$ python3 -m robyn --create
? Directory Path: .
? Need Docker? (Y/N) Y
? Please select project type (Mongo/Postgres/Sqlalchemy/Prisma):
❯ No DB
Sqlite
Postgres
MongoDB
SqlAlchemy
Prisma
```
and the following directory structure.
Batman was asked a set of questions to configure the application. He chose to use the default values for most of the questions.
And he was done! The Robyn CLI created a new application with the following structure.
```bash
├── src
│ ├── app.py
├── Dockerfile
```
================================================
FILE: docs_src/src/pages/documentation/en/example_app/modeling_routes.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Crime Data Model and Database Connection
Batman designed a data model to represent crime data, including information about the crime, suspect, and location. He decided to use a SQLite database to store the data and used an ORM (Object Relational Mapping) library to interact with the database.
```python
# models.py
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Float
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = "sqlite:///./gotham_crime_data.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Crime(Base):
__tablename__ = "crimes"
id = Column(Integer, primary_key=True, index=True)
type = Column(String, index=True)
description = Column(String)
location = Column(String)
suspect_name = Column(String)
date_time = Column(DateTime)
latitude = Column(Float)
longitude = Column(Float)
```
## Setting up the Robyn Application
Batman set up a Robyn application and configured it to use the database session to access the SQLite database.
Based on the Database model, Batman created a few helper methods to interact with the database. These methods would be used by the endpoints to perform CRUD operations on the database.
```python {{ title: 'crud.py' }}
# crud.py
from sqlalchemy.orm import Session
from .models import Crime
def get_crime(db: Session, crime_id: int):
return db.query(Crime).filter(Crime.id == crime_id).first()
def get_crimes(db: Session, skip: int = 0, limit: int = 100):
return db.query(Crime).offset(skip).limit(limit).all()
def create_crime(db: Session, crime):
db_crime = Crime(**crime)
db.add(db_crime)
db.commit()
db.refresh(db_crime)
return db_crime
def update_crime(db: Session, crime_id: int, crime):
db_crime = get_crime(db, crime_id)
if db_crime is None:
return None
for key, value in crime.items():
setattr(db_crime, key, value)
db.commit()
db.refresh(db_crime)
return db_crime
def delete_crime(db: Session, crime_id: int):
db_crime = get_crime(db, crime_id)
if db_crime is None:
return False
db.delete(db_crime)
db.commit()
return True
```
## Crime Data Endpoints
Batman created various endpoints to manage crime data. These endpoints allowed the Gotham City Police Department to add, update, and retrieve crime data.
```python {{ title: 'Setting up Routes' }}
# __main__.py
from robyn import Robyn
from robyn.robyn import Request, Response
from sqlalchemy.orm import Session
app = Robyn(__file__)
@app.post("/crimes")
async def add_crime(request):
with SessionLocal() as db:
crime = request.json()
insertion = crud.create_crime(db, crime)
if insertion is None:
raise Exception("Crime not added")
return {
"description": "Crime added successfully",
"status_code": 200,
}
@app.get("/crimes")
async def get_crimes(request):
with SessionLocal() as db:
skip = request.query_params.get("skip", "0")
limit = request.query_params.get("limit", "100")
crimes = crud.get_crimes(db, skip=skip, limit=limit)
return crimes
@app.get("/crimes/:crime_id", auth_required=True)
async def get_crime(request):
crime_id = int(request.path_params.get("crime_id"))
with SessionLocal() as db:
crime = crud.get_crime(db, crime_id=crime_id)
if crime is None:
raise Exception("Crime not found")
return crime
@app.put("/crimes/:crime_id")
async def update_crime(request):
crime = request.json()
crime_id = int(request.path_params.get("crime_id"))
with SessionLocal() as db:
updated_crime = crud.update_crime(db, crime_id=crime_id, crime=crime)
if updated_crime is None:
raise Exception("Crime not found")
return updated_crime
@app.delete("/crimes/{crime_id}")
async def delete_crime(request):
crime_id = int(request.path_params.get("crime_id"))
with SessionLocal() as db:
success = crud.delete_crime(db, crime_id=crime_id)
if not success:
raise Exception("Crime not found")
return {"message": "Crime deleted successfully"}
```
================================================
FILE: docs_src/src/pages/documentation/en/example_app/monitoring_and_logging.mdx
================================================
## Monitoring and Logging
To keep an eye on the performance of his application and troubleshoot issues, Batman decided to implement monitoring and logging. He used a third-party library to integrate logging middleware, enabling him to track requests, errors, and performance metrics.
```python {{ title: 'Monitoring and Logging' }}
from robyn import Logger
logger = Logger(app)
@app.before_request()
async def log_request(request: Request):
logger.info(f"Received request: %s", request)
@app.after_request()
async def log_response(response: Response):
logger.info(f"Sending response: %s", response)
```
With monitoring and logging in place, Batman could now easily detect issues and analyze the performance of his web application, ensuring that it was always running optimally and ready to assist him in his fight against crime.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/openapi.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## OpenAPI Docs a.k.a Swagger
After deploying the application, Batman got multiple queries from the users on how to use the endpoints. Robyn showed him how to generate OpenAPI specifications for his application.
Out of the box, the following endpoints are setup for you:
- `/docs` The Swagger UI
- `/openapi.json` The JSON Specification
However, if you don't want to generate the OpenAPI docs, you can disable it by passing `--disable-openapi` flag while starting the application.
To use a custom openapi configuration, you can:
- Place the `openapi.json` config file in the root directory.
- Or, pass the file path to the `openapi_file_path` parameter in the `Robyn()` constructor. (the parameter gets priority over the file).
```bash
python app.py --disable-openapi
```
## How to use?
- Query Params: The typing for query params can be added as `def get(r: Request, query_params: GetRequestParams)` where `GetRequestParams` is a subclass of `QueryParams`
- Path Params are defaulted to string type (ref: https://en.wikipedia.org/wiki/Query_string)
```python
from robyn.robyn import QueryParams
from robyn import Robyn, Request
app = Robyn(
file_object=__file__,
openapi=OpenAPI(
info=OpenAPIInfo(
title="Sample App",
description="This is a sample server application.",
termsOfService="https://example.com/terms/",
version="1.0.0",
contact=Contact(
name="API Support",
url="https://www.example.com/support",
email="support@example.com",
),
license=License(
name="BSD2.0",
url="https://opensource.org/license/bsd-2-clause",
),
externalDocs=ExternalDocumentation(description="Find more info here", url="https://example.com/"),
components=Components(),
),
),
)
@app.get("/")
async def welcome():
"""welcome endpoint"""
return "hi"
class GetRequestParams(QueryParams):
appointment_id: str
year: int
@app.get("/api/v1/name", openapi_name="Name Route", openapi_tags=["Name"])
async def get(r: Request, query_params: GetRequestParams):
"""Get Name by ID"""
return r.query_params
@app.delete("/users/:name", openapi_tags=["Name"])
async def delete(r: Request):
"""Delete Name by ID"""
return r.path_params
if __name__ == "__main__":
app.start()
```
## How does it work with subrouters?
```python
from robyn.robyn import QueryParams
from robyn import Request, SubRouter
subrouter: SubRouter = SubRouter(__name__, prefix="/sub")
@subrouter.get("/")
async def subrouter_welcome():
"""welcome subrouter"""
return "hiiiiii subrouter"
class SubRouterGetRequestParams(QueryParams):
_id: int
value: str
@subrouter.get("/name")
async def subrouter_get(r: Request, query_params: SubRouterGetRequestParams):
"""Get Name by ID"""
return r.query_params
@subrouter.delete("/:name")
async def subrouter_delete(r: Request):
"""Delete Name by ID"""
return r.path_params
app.include_router(subrouter)
```
## Other Specification Params
We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example using request & response bodies below:
```python
from robyn.types import JSONResponse, Body
class Initial(Body):
is_present: bool
letter: Optional[str]
class FullName(Body):
first: str
second: str
initial: Initial
class CreateItemBody(Body):
name: FullName
description: str
price: float
tax: float
class CreateResponse(JSONResponse):
success: bool
items_changed: int
@app.post("/")
def create_item(request: Request, body: CreateItemBody) -> CreateResponse:
return CreateResponse(success=True, items_changed=2)
```
With the reference documentation deployed and running smoothly, Batman had a powerful new tool at his disposal. The Robyn framework had provided him with the flexibility, scalability, and performance needed to create an effective crime-fighting application, giving him a technological edge in his ongoing battle to protect Gotham City.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/real_time_notifications.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Real time notifications
Batman decided to implement real-time notifications for police officers using WebSockets. This would allow officers to receive instant updates on criminal activities, as well as alerts when a new crime is reported.
```python {{ title: 'Setting up Real-time Notifications' }}
from robyn import WebSocketDisconnect
@app.websocket("/notifications")
async def notify_handler(websocket):
try:
while True:
message = await websocket.receive_text()
await websocket.send_text(f"Received: {message}")
except WebSocketDisconnect:
pass
@notify_handler.on_connect
def notify_connect(websocket):
return "Connected to notifications"
@notify_handler.on_close
def notify_close(websocket):
return "Disconnected from notifications"
```
## Advanced Search and Filtering
To make it easier for the police officers to search for specific crimes or criminals, Batman added advanced search and filtering options to the application. He implemented a new endpoint that allows users to search based on various criteria like crime type, date, location, and status.
```python {{ title: 'Advanced Search and Filtering' }}
@app.get("/crimes/search")
async def search_crimes(request):
crime_type = request.query_params.get("crime_type")
date = request.query_params.get("date")
location = request.query_params.get("location")
status = request.query_params.get("status")
crimes = crud.search_crimes(db, crime_type=crime_type, date=date, location=location, status=status)
return crimes
```
With the new features in place, the Gotham City Police Department was able to use the web application more effectively to track criminal activities and deploy resources efficiently. Batman's work on the Robyn web framework had a significant impact on Gotham City's crime-fighting efforts, making it a safer place for its citizens.
Although Batman had achieved great success with the current implementation, he knew that there would always be room for improvement and new features to add. But for now, he could take a moment to appreciate his work and focus on his primary duty - protecting Gotham City as the Dark Knight.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/subrouters.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Code Organization with SubRouters
As the application grew, Batman needed a way to organize his routes better. He decided to use Robyn's SubRouter feature to group related routes together.
```python
from robyn import SubRouter
# Create a subrouter for crime-related routes
crime_router = SubRouter(__file__, prefix="/crimes")
@crime_router.get("/list")
def list_crimes():
return {"crimes": get_all_crimes()}
@crime_router.post("/report")
def report_crime(request):
crime_data = request.json()
return {"id": create_crime_report(crime_data)}
# Create a subrouter for suspect-related routes
suspect_router = SubRouter(__file__, prefix="/suspects")
@suspect_router.get("/list")
def list_suspects():
return {"suspects": get_all_suspects()}
@suspect_router.get("/:id")
def get_suspect(request, path_params):
suspect_id = path_params.id
return {"suspect": get_suspect_by_id(suspect_id)}
# Include the subrouters in the main app
app.include_router(crime_router)
app.include_router(suspect_router)
```
SubRouters help organize related routes under a common prefix, making the code more maintainable and easier to understand. In this example:
- All crime-related routes are under `/crimes`
- All suspect-related routes are under `/suspects`
This organization makes it clear which routes handle what functionality and keeps related code together.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/templates.mdx
================================================
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
## Templates
After implementing the backend, Batman decided to add a frontend to his application. He wanted to create a simple web page that would allow him to view the data he had collected. He also wanted to be able to add new data to the database and edit existing data.
This is when he was introduced to templates!
Templates are a powerful feature of the Robyn framework that allow you to create dynamic web pages using HTML and Python. They are a great way to add a frontend to your application without having to learn a new language or framework.
Robyn supports Jinja2 templates by default but provides an easy way to add other templating engines as well.
### Creating a Template
To create a template, you need to create a file with the `.html` extension in the a directory, usually it is convention to use the `templates` directory. For example, if you wanted to create a template called `index.html`, you would create a file called `index.html` in the `templates` directory.
So the folder structure would look like this:
```bash
├── app.py
├── templates
│ └── index.html
├── Dockerfile
└── requirements.txt
```
### Rendering a Template
Once you have created a template, you can render it by using the `render_template` function. This function takes the name of the template as its first argument and a dictionary of variables as its second argument.
For example, if you wanted to render the `index.html` template, you would use the following code:
```python {{ title: 'Rendering a Template' }}
import os
import pathlib
from robyn.templating import JinjaTemplate
current_file_path = pathlib.Path(__file__).parent.resolve()
jinja_template = JinjaTemplate(os.path.join(current_file_path, "templates"))
@app.get("/frontend")
async def get_frontend(request):
context = {"framework": "Robyn", "templating_engine": "Jinja2"}
return jinja_template.render_template("index.html", **context)
app.include_router(frontend)
```
Now Batman very happy that the application had come to completion. However, he was not satisfied with the current state of the application. He felt the code was all crammed in a single file and asked Robyn if there was a way to split the codebase in other parts.
This is Robyn introduced him to the concept of routers and views.
================================================
FILE: docs_src/src/pages/documentation/en/example_app/zh/index.mdx
================================================
export const description =
'欢迎来到Robyn API文档。您将找到全面的指南和文档,帮助您尽快开始使用Robyn,并在遇到困难时获得支持。'
# 使用Robyn构建实际应用
蝙蝠侠被委托开发一个Web应用程序来管理哥谭市的犯罪数据。该应用程序将允许哥谭警察局存储和检索有关犯罪活动、嫌疑人及其位置的数据。他决定使用Robyn Web框架来高效快速地构建这个应用程序。
您可以在[这里](https://github.com/sparckles/example_robyn_app)找到此应用程序的源代码。
## 安装Robyn
第一步是安装Robyn。蝙蝠侠创建了一个虚拟环境并使用pip安装了Robyn。
```bash
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install robyn
```
## 创建Robyn应用程序
蝙蝠侠正准备创建一个`src/app.py`文件,这时他得知Robyn提供了一个CLI工具来创建新应用程序。他运行以下命令来创建一个新的Robyn应用程序:
```bash
$ python -m robyn --create
```
这个示例应用程序将展示如何:
- 设计和实现RESTful API
- 处理身份验证和授权
- 使用中间件进行请求处理
- 实现实时通知
- 设置监控和日志记录
- 部署应用程序
- 生成API文档
- 组织和构建可维护的代码
================================================
FILE: docs_src/src/pages/documentation/en/example_app/zh/subrouters.mdx
================================================
export const description =
'欢迎来到Robyn API文档。您将找到全面的指南和文档,帮助您尽快开始使用Robyn,并在遇到困难时获得支持。'
## 使用SubRouters组织代码
随着应用程序的增长,蝙蝠侠需要一种更好的方式来组织他的路由。他决定使用Robyn的SubRouter功能来对相关路由进行分组。
```python
from robyn import SubRouter
# 创建处理犯罪相关路由的子路由器
crime_router = SubRouter(__file__, prefix="/crimes")
@crime_router.get("/list")
def list_crimes():
return {"crimes": get_all_crimes()}
@crime_router.post("/report")
def report_crime(request):
crime_data = request.json()
return {"id": create_crime_report(crime_data)}
# 创建处理嫌疑人相关路由的子路由器
suspect_router = SubRouter(__file__, prefix="/suspects")
@suspect_router.get("/list")
def list_suspects():
return {"suspects": get_all_suspects()}
@suspect_router.get("/:id")
def get_suspect(request, path_params):
suspect_id = path_params.id
return {"suspect": get_suspect_by_id(suspect_id)}
# 将子路由器包含在主应用程序中
app.include_router(crime_router)
app.include_router(suspect_router)
```
SubRouters帮助在公共前缀下组织相关路由,使代码更易于维护和理解。在这个例子中:
- 所有与犯罪相关的路由都在 `/crimes` 下
- 所有与嫌疑人相关的路由都在 `/suspects` 下
这种组织方式清晰地表明了哪些路由处理什么功能,并将相关代码保持在一起。
================================================
FILE: docs_src/src/pages/documentation/en/framework_performance_comparison.mdx
================================================
export const description =
'On this page, we`ll understand the performance comparison across different frameworks.'
# Performance comparison across different frameworks
## Read this before you scroll down
Before delving into the details, it is imperative to note that this comparison doesn’t aim to discredit any developers or the frameworks listed below. Mentioning the names of the frameworks is solely for elucidating a clear comparison. My profound inclination towards the Python web ecosystem has been significantly influenced by all these frameworks, and my intention is not to cause offense to anyone by listing them here.
Moreover, these tests were conducted on my development machine, and thus, the figures portrayed below are not absolute. The numbers only serve to indicate the relative performance of these frameworks under the specific testing conditions.
The [oha](https://github.com/hatoo/oha) tool was utilized to test 10,000 requests on the following frameworks, yielding the subsequent results:
1. Flask(Gunicorn)
```
Total: 5.5254 secs
Slowest: 0.0784 secs
Fastest: 0.0028 secs
Average: 0.0275 secs
Requests/sec: 1809.8082
```
1. FastAPI(Uvicorn)
```
Total: 4.1314 secs
Slowest: 0.0733 secs
Fastest: 0.0027 secs
Average: 0.0206 secs
Requests/sec: 2420.4851
```
1. Django(Gunicorn)
```
Total: 13.5070 secs
Slowest: 0.3635 secs
Fastest: 0.0249 secs
Average: 0.0674 secs
Requests/sec: 740.3558
```
1. Robyn(Doesn't need a *SGI)
```
Total: 1.8324 secs
Slowest: 0.0269 secs
Fastest: 0.0024 secs
Average: 0.0091 secs
Requests/sec: 5457.2339
```
1. Robyn (5 workers)
```
Total: 1.5592 secs
Slowest: 0.0211 secs
Fastest: 0.0017 secs
Average: 0.0078 secs
Requests/sec: 6413.6480
```
Robyn is able to serve the 10k requests in 1.8 seconds followed by Flask and FastAPI, which take around 5 seconds(using 5 workers on a dual-core machine). Finally, Django takes around 13.5070 seconds.
## Verbose Logs
Flask(Gunicorn)
```
Summary:
Success rate: 1.0000
Total: 5.5254 secs
Slowest: 0.0784 secs
Fastest: 0.0028 secs
Average: 0.0275 secs
Requests/sec: 1809.8082
Total data: 126.95 KiB
Size/request: 13 B
Size/sec: 22.98 KiB
Response time histogram:
0.007 [55] |
0.014 [641] |■■■■■
0.021 [2413] |■■■■■■■■■■■■■■■■■■■■
0.027 [3771] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.034 [1999] |■■■■■■■■■■■■■■■■
0.041 [737] |■■■■■■
0.048 [236] |■■
0.055 [75] |
0.062 [46] |
0.069 [24] |
0.076 [3] |
Latency distribution:
10% in 0.0178 secs
25% in 0.0223 secs
50% in 0.0266 secs
75% in 0.0317 secs
90% in 0.0378 secs
95% in 0.0419 secs
99% in 0.0551 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0071 secs, 0.0001 secs, 0.0443 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0010 secs
Status code distribution:
[200] 10000 responses
```
FastAPI(Uvicorn)
```
Summary:
Success rate: 1.0000
Total: 4.1314 secs
Slowest: 0.0733 secs
Fastest: 0.0027 secs
Average: 0.0206 secs
Requests/sec: 2420.4851
Total data: 166.02 KiB
Size/request: 17 B
Size/sec: 40.18 KiB
Response time histogram:
0.005 [175] |■
0.011 [1541] |■■■■■■■■■■■■■■■■
0.016 [2942] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.021 [2770] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.027 [1479] |■■■■■■■■■■■■■■■■
0.032 [608] |■■■■■■
0.038 [217] |■■
0.043 [103] |■
0.048 [53] |
0.054 [54] |
0.059 [58] |
Latency distribution:
10% in 0.0120 secs
25% in 0.0151 secs
50% in 0.0194 secs
75% in 0.0243 secs
90% in 0.0300 secs
95% in 0.0348 secs
99% in 0.0522 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0088 secs, 0.0073 secs, 0.0103 secs
DNS-lookup: 0.0001 secs, 0.0000 secs, 0.0008 secs
Status code distribution:
[200] 10000 responses
```
Robyn
```
Summary:
Success rate: 1.0000
Total: 1.8324 secs
Slowest: 0.0269 secs
Fastest: 0.0024 secs
Average: 0.0091 secs
Requests/sec: 5457.2339
Total data: 117.19 KiB
Size/request: 12 B
Size/sec: 63.95 KiB
Response time histogram:
0.002 [183] |■
0.004 [1669] |■■■■■■■■■■■■■■
0.007 [3724] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.009 [2631] |■■■■■■■■■■■■■■■■■■■■■■
0.011 [1060] |■■■■■■■■■
0.013 [496] |■■■■
0.016 [188] |■
0.018 [34] |
0.020 [12] |
0.022 [2] |
0.025 [1] |
Latency distribution:
10% in 0.0061 secs
25% in 0.0073 secs
50% in 0.0087 secs
75% in 0.0105 secs
90% in 0.0129 secs
95% in 0.0143 secs
99% in 0.0171 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0049 secs, 0.0035 secs, 0.0065 secs
DNS-lookup: 0.0001 secs, 0.0000 secs, 0.0010 secs
Status code distribution:
[200] 10000 responses
```
Django(Gunicorn)
```
Summary:
Success rate: 1.0000
Total: 13.5070 secs
Slowest: 0.3635 secs
Fastest: 0.0249 secs
Average: 0.0674 secs
Requests/sec: 740.3558
Total data: 102.01 MiB
Size/request: 10.45 KiB
Size/sec: 7.55 MiB
Response time histogram:
0.016 [283] |■
0.032 [2616] |■■■■■■■■■■■■■■■■■■
0.048 [4587] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.064 [1829] |■■■■■■■■■■■■
0.081 [362] |■■
0.097 [98] |
0.113 [105] |
0.129 [20] |
0.145 [7] |
0.161 [28] |
0.177 [65] |
Latency distribution:
10% in 0.0493 secs
25% in 0.0559 secs
50% in 0.0638 secs
75% in 0.0733 secs
90% in 0.0840 secs
95% in 0.0948 secs
99% in 0.1543 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0097 secs, 0.0001 secs, 0.0444 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0007 secs
Status code distribution:
[200] 10000 responses
```
Robyn(with 5 workers)
```
Summary:
Success rate: 1.0000
Total: 1.5592 secs
Slowest: 0.0211 secs
Fastest: 0.0017 secs
Average: 0.0078 secs
Requests/sec: 6413.6480
Total data: 126.95 KiB
Size/request: 13 B
Size/sec: 81.42 KiB
Response time histogram:
0.002 [30] |
0.004 [599] |■■■■■
0.005 [3336] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.007 [3309] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.009 [1614] |■■■■■■■■■■■■■■■
0.011 [749] |■■■■■■■
0.012 [253] |■■
0.014 [94] |
0.016 [14] |
0.018 [1] |
0.019 [1] |
Latency distribution:
10% in 0.0055 secs
25% in 0.0063 secs
50% in 0.0074 secs
75% in 0.0089 secs
90% in 0.0107 secs
95% in 0.0117 secs
99% in 0.0142 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0022 secs, 0.0013 secs, 0.0028 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0001 secs
Status code distribution:
[200] 10000 responses
```
================================================
FILE: docs_src/src/pages/documentation/en/hosting.mdx
================================================
export const description =
'On this page, we`ll understand how Robyn can be deployed on various cloud providers .'
The process of hosting a Robyn app on various cloud providers.
## Railway
We will be deploying the app on `Railway`.
A GitHub account is needed as a mandatory prerequisite.
We will deploy a sample "Hello World," demonstrating a simple GET route and serving an HTML file.
Directory structure:
```bash
app folder/
main.py
requirements.txt
index.html
```
Note - Railway looks for a main.py as an entry point instead of app.py. The build process will fail if there is no main.py file.
```python {{ title: 'python' }}
from robyn import Robyn, serve_html
app = Robyn(__file__)
@app.get("/hello")
async def h(request):
print(request)
return "Hello, world!"
@app.get("/")
async def get_page(request):
return serve_html("./index.html")
if __name__ == "__main__":
app.start(url="0.0.0.0", port=PORT)
```
```python {{ title: 'python' }}
Hello World, this is Robyn framework!
```
## Exposing Ports
The Railway documentation says the following about the listening to ports:
> The easiest way to get up and running is to have your application listen on 0.0.0.0:$PORT, where PORT is a Railway-provided environment variable.
So, passing the host as `0.0.0.0` to `app.start()` as an argument is necessary.
We need to create a Railway account to deploy this app on Railway. We can do so by going on the `Railway HomePage`.
Press the "Login" button and select "login with a GitHub account."
Then, we press the "New Project" button and select "Deploy from GitHub repo".
Then we select the repo we want to deploy. And click "Deploy Now".
Now, we click on our project's card.
Select "Variables" and press the "New Variable" button to set the environments variables.
Then, we go to the "Settings" tab and click on "Generate Domain."
We can generate a temporary domain under the "Domains" tab.
We can go to our domain `/hello` and confirm that the message "Hello World" is displayed.
## Next Steps
- [Future Roadmap](/documentation/en/api_reference/future-roadmap)
================================================
FILE: docs_src/src/pages/documentation/en/index.mdx
================================================
import { Guides } from '@/components/documentation/Guides'
import { ApiDocs } from '@/components/documentation/ApiDocs'
import { HeroPattern } from '@/components/documentation/HeroPattern'
export const description =
'Welcome to the Robyn API documentation. You will find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.'
export const sections = [
{ title: 'Example Application', id: 'guides' },
{ title: 'Api Reference', id: 'api_docs' },
]
# API Documentation
Welcome to the Robyn API documentation. You'll find comprehensive guides and documentation to help you start working with Robyn as quickly as possible, as well as support if you get stuck.
We have divided the documentation into two parts: the [Example Application](#guides) and the [API Docs](#api_docs).
## Getting started {{ anchor: false }}
The Example Application is a simple web application that demonstrates how to use the Robyn API. It is a great place to start if you are new to Robyn.
The API Reference contains detailed information about the Robyn API. It is a great place to start if you are already familiar with Robyn and want to learn more about the API.
================================================
FILE: docs_src/src/pages/documentation/en/plugins.mdx
================================================
## Plugins
Robyn is a versatile and extensible web framework that allows anyone to make plugins over the top of Robyn.
Plugins in Robyn allow you to enhance and customize the framework's functionality to suit your specific needs. Here are some noteworthy plugins that can supercharge your Robyn-based projects:
### Rate Limit Plugin
- Description: This plugin enables you to implement rate limiting for your Robyn application's routes. It helps prevent abuse, and brute-force attacks and ensures fair usage of your resources.
- GitHub repository: [robyn-rate-limits](https://github.com/IdoKendo/robyn_rate_limits)
- Installation:
`python -m pip install robyn-rate-limits`
- Usage:
```py
from robyn import Robyn, Request
from robyn_rate_limits import InMemoryStore
from robyn_rate_limits import RateLimiter
app = Robyn(__file__)
limiter = RateLimiter(store=InMemoryStore, calls_limit=3, limit_ttl=100)
@app.before_request()
def middleware(request: Request):
return limiter.handle_request(app, request)
@app.get("/")
def h():
return "Hello, World!"
app.start(port=8080)
```
In this example, robyn-rate-limits is used to enforce a rate limit of 3 requests per 100-seconds window for specific routes. If a client exceeds this limit, they will receive a "Too many requests" message.
The plugin integrates seamlessly with the Robyn web framework, enhancing the security and stability of your application by preventing excessive requests from a single client.
## What's next?
After exploring the plugins, Batman wanted to explore the community.So, Robyn pointed him to
- [Future Roadmap](/documentation/en/api_reference/future-roadmap)
================================================
FILE: docs_src/src/pages/documentation/zh/api_reference/advanced_features.mdx
================================================
export const description =
'在此页面中,我们将深入探讨如何通过不同的接口实现符合预期的交互。'
## 获取客户端 IP 地址
意识到小丑可能也在使用哥谭警察控制面板后,蝙蝠侠决定在他的应用程序中实现访问者 IP 地址追踪功能。
```python
@app.get("/auth", auth_required=True)
async def auth(request: Request):
# This route method will only be executed if the user is authenticated
# Otherwise, a 401 response will be returned
return "Hello, world"
```
)
}
export default function Home({ articles }) {
return (
<>
Robyn - A Fast, Innovator Friendly, and Community Driven Python Web
Framework.
{/*Twitter specific meta tags*/}
{/*LinkedIn specific meta tags*/}
Meet Robyn
A Fast, Innovator Friendly, and Community Driven Python Web
Framework
Robyn merges Python's async capabilities with a Rust runtime
for reliable, scalable web solutions. Experience quick project
scaffolding, enjoyable usage, and robust plugin support.
Fast. Innovator Friendly. Robust. Community Driven.
Fast.{' '}
Robyn, written in Rust, embodies speed and reliability. Our
multithreaded runtime creates a highly efficient and secure
environment optimized for peak performance.
Simple API.
With Robyn, complexity takes a backseat. Our API is simple yet
potent, built to streamline your development process and
minimize your workload.
Hacker Friendly.
While many focus on large-scale challenges that many of us might
never encounter, Robyn prioritizes the broader issues most
developers face. We foster innovation for all, ensuring both
common and complex challenges are met with expertise. At Robyn,
every developer feels at home.
Community First and Truly FOSS.
Rooted in a community-first ethos, Robyn is built with
collective effort and is a true representation of free and
open-source software.
Some of our stats
Robyn is going strong!
Robyn installs
{/*
*/}
3 million
Stars on Github
{/*
*/}
6k+
Community Contributors
{/*
*/}
70+
Community Members
500+
Ready to Dive In?
Start using Robyn today.
Go through a small tutorial or read the docs to get started.
================================================
FILE: integration_tests/conftest.py
================================================
import os
import pathlib
import platform
import signal
import socket
import subprocess
import time
from typing import List
import pytest
from integration_tests.helpers.network_helpers import get_network_host
def spawn_process(command: List[str]) -> subprocess.Popen:
if platform.system() == "Windows":
command[0] = "python"
process = subprocess.Popen(command, shell=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
return process
process = subprocess.Popen(command, preexec_fn=os.setsid)
return process
def kill_process(process: subprocess.Popen) -> None:
if platform.system() == "Windows":
process.send_signal(signal.CTRL_BREAK_EVENT)
process.kill()
return
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except ProcessLookupError:
pass
def start_server(domain: str, port: int, is_dev: bool = False) -> subprocess.Popen:
"""
Call this method to wait for the server to start
"""
# Start the server
current_file_path = pathlib.Path(__file__).parent.resolve()
base_routes = os.path.join(current_file_path, "./base_routes.py")
command = ["python3", base_routes]
if is_dev:
command.append("--dev")
# Ensure environment variables are properly set for the subprocess
env = os.environ.copy()
env["ROBYN_HOST"] = domain
env["ROBYN_PORT"] = str(port)
process = spawn_process(command)
# Wait for the server to be reachable
timeout = 15 # The maximum time we will wait for an answer
start_time = time.time()
while True:
current_time = time.time()
if current_time - start_time > timeout:
# Robyn didn't start correctly before timeout, kill the process and exit with an exception
kill_process(process)
raise ConnectionError(f"Could not reach Robyn server on {domain}:{port} after {timeout} seconds")
try:
sock = socket.create_connection((domain, port), timeout=2)
sock.close()
break # We were able to reach the server, exit the loop
except Exception:
time.sleep(0.5) # Longer delay before retrying
# Give the server a moment to fully initialize after accepting connections
time.sleep(1)
return process
@pytest.fixture(scope="session")
def session():
domain = "127.0.0.1"
port = 8080
os.environ["ROBYN_HOST"] = domain
process = start_server(domain, port)
yield
kill_process(process)
@pytest.fixture(scope="session")
def default_session():
domain = "127.0.0.1"
port = 8080
process = start_server(domain, port)
yield
kill_process(process)
@pytest.fixture(scope="session")
def global_session():
domain = get_network_host()
port = 8080
os.environ["ROBYN_HOST"] = domain
process = start_server(domain, port)
yield
kill_process(process)
@pytest.fixture(scope="session")
def dev_session():
domain = "127.0.0.1"
port = 8081
os.environ["ROBYN_HOST"] = domain
os.environ["ROBYN_PORT"] = str(port)
# This doesn't test is_dev=True!!!!
process = start_server(domain, port)
yield
kill_process(process)
@pytest.fixture(scope="session")
def test_session():
domain = "127.0.0.1"
port = 8080
os.environ["ROBYN_HOST"] = domain
os.environ["ROBYN_PORT"] = str(port)
process = start_server(domain, port, is_dev=True)
yield
kill_process(process)
# create robyn.env before test and delete it after test
@pytest.fixture
def env_file():
CONTENT = """ROBYN_PORT=8081
ROBYN_URL=127.0.0.1"""
path = pathlib.Path(__file__).parent
env_path = path / "robyn.env"
env_path.write_text(CONTENT)
yield
env_path.unlink()
del os.environ["ROBYN_PORT"]
del os.environ["ROBYN_HOST"]
================================================
FILE: integration_tests/downloads/test.txt
================================================
This is a test file for the downloading purpose
================================================
FILE: integration_tests/helpers/__init__.py
================================================
================================================
FILE: integration_tests/helpers/http_methods_helpers.py
================================================
from typing import Optional
import requests
BASE_URL = "http://127.0.0.1:8080"
def check_response(response: requests.Response, expected_status_code: int):
"""
Raises if the response status code is not the expected one or if one of the global
headers is not present in the response.
"""
assert response.status_code == expected_status_code
assert response.headers.get("global_after") == "global_after_request"
assert "server" in response.headers
assert response.headers.get("server") == "robyn"
def get(
endpoint: str,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a GET request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.get(f"{BASE_URL}/{endpoint}", headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def post(
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a POST request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
data Optional[dict]: The data to send with the request.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.post(f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def json_post(
endpoint: str,
json_data=None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a POST request with JSON body to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
json_data: The JSON-serializable data to send with the request (dict, list, etc.).
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.strip("/")
response = requests.post(f"{BASE_URL}/{endpoint}", json=json_data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def multipart_post(
endpoint: str,
files: Optional[dict] = None,
expected_status_code: int = 200,
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a POST request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
files Optional[dict]: The files to send with the request.
expected_status_code int: The expected status code of the response.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.post(f"{BASE_URL}/{endpoint}", files=files)
if should_check_response:
check_response(response, expected_status_code)
return response
def put(
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a PUT request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.put(f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def patch(
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a PATCH request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.patch(f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def delete(
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a DELETE request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.delete(f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
def head(
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a HEAD request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
response = requests.head(f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
# TODO - at some point this should be the defacto
# and every other method should be replaced with this
def generic_http_helper(
method: str,
endpoint: str,
data: Optional[dict] = None,
expected_status_code: int = 200,
headers: dict = {},
should_check_response: bool = True,
) -> requests.Response:
"""
Makes a request to the given endpoint and checks the response.
endpoint str: The endpoint to make the request to.
expected_status_code int: The expected status code of the response.
headers dict: The headers to send with the request.
should_check_response bool: A boolean to indicate if the status code and headers should be checked.
"""
endpoint = endpoint.lstrip("/")
if method not in ["get", "post", "put", "patch", "delete", "options", "trace"]:
raise ValueError(f"{method} method must be one of get, post, put, patch, delete")
if method == "get":
response = requests.get(f"{BASE_URL}/{endpoint}", headers=headers)
else:
response = requests.request(method, f"{BASE_URL}/{endpoint}", data=data, headers=headers)
if should_check_response:
check_response(response, expected_status_code)
return response
================================================
FILE: integration_tests/helpers/network_helpers.py
================================================
import platform
import socket
def get_network_host():
# windows doesn't support 0.0.0.0
if platform.system() == "Windows":
hostname = socket.gethostname()
ip_address = socket.gethostbyname(hostname)
return ip_address
else:
return "0.0.0.0"
================================================
FILE: integration_tests/index.html
================================================
Document
Hello world. How are you?
================================================
FILE: integration_tests/index.py
================================================
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def h():
return "Hello, world!"
app.start()
================================================
FILE: integration_tests/openapi_config.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "Robyn Test API",
"version": "1.0.0",
"description": null,
"termsOfService": null,
"contact": {
"name": null,
"url": null,
"email": null
},
"license": {
"name": null,
"url": null
},
"servers": [],
"externalDocs": {
"description": null,
"url": null
},
"components": {
"schemas": {},
"responses": {},
"parameters": {},
"examples": {},
"requestBodies": {},
"securitySchemes": {},
"links": {},
"callbacks": {},
"pathItems": {}
}
},
"paths": {
"/": {
"get": {
"summary": "",
"description": "No description provided",
"parameters": [],
"tags": [
"get"
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"text/plain": {
"schema": {}
}
}
}
}
}
}
},
"components": {
"schemas": {},
"responses": {},
"parameters": {},
"examples": {},
"requestBodies": {},
"securitySchemes": {},
"links": {},
"callbacks": {},
"pathItems": {}
},
"servers": [],
"externalDocs": null
}
================================================
FILE: integration_tests/random_number.rs
================================================
// rustimport:pyo3
use pyo3::prelude::*;
#[pyfunction]
fn say_hello() {
println!("Hello from random_number, implemented in Rust!")
}
// Uncomment the below to implement custom pyo3 binding code. Otherwise,
// rustimport will generate it for you for all functions annotated with
// #[pyfunction] and all structs annotated with #[pyclass].
//
//#[pymodule]
//fn random_number(_py: Python, m: &PyModule) -> PyResult<()> {
// m.add_function(wrap_pyfunction!(say_hello, m)?)?;
// Ok(())
//}
================================================
FILE: integration_tests/subroutes/__init__.py
================================================
from robyn import SubRouter, WebSocketDisconnect, jsonify
from .di_subrouter import di_subrouter
from .file_api import static_router
sub_router = SubRouter(__name__, prefix="/sub_router")
__all__ = ["sub_router", "di_subrouter", "static_router"]
@sub_router.websocket("/ws")
async def ws_handler(websocket):
try:
while True:
await websocket.receive_text()
await websocket.send_text("Message")
except WebSocketDisconnect:
pass
@ws_handler.on_connect
async def connect(websocket):
return "Hello world, from ws"
@ws_handler.on_close
async def close(websocket):
return jsonify({"message": "closed"})
@sub_router.get("/foo")
def get_foo():
return {"message": "foo"}
@sub_router.post("/foo")
def post_foo():
return {"message": "foo"}
@sub_router.put("/foo")
def put_foo():
return {"message": "foo"}
@sub_router.delete("/foo")
def delete_foo():
return {"message": "foo"}
@sub_router.patch("/foo")
def patch_foo():
return {"message": "foo"}
@sub_router.options("/foo")
def option_foo():
return {"message": "foo"}
@sub_router.trace("/foo")
def trace_foo():
return {"message": "foo"}
@sub_router.head("/foo")
def head_foo():
return {"message": "foo"}
@sub_router.post("/openapi_test", openapi_tags=["test subrouter tag"])
def sample_subrouter_openapi_endpoint():
"""Get subrouter openapi"""
return 200
================================================
FILE: integration_tests/subroutes/di_subrouter.py
================================================
from robyn import Request, SubRouter
di_subrouter = SubRouter(__file__, "/di_subrouter")
GLOBAL_DEPENDENCY = "GLOBAL DEPENDENCY OVERRIDE"
ROUTER_DEPENDENCY = "ROUTER DEPENDENCY"
di_subrouter.inject_global(GLOBAL_DEPENDENCY=GLOBAL_DEPENDENCY)
di_subrouter.inject(ROUTER_DEPENDENCY=ROUTER_DEPENDENCY)
@di_subrouter.get("/subrouter_router_di")
def sync_subrouter_route_dependency(r: Request, router_dependencies, global_dependencies):
return router_dependencies["ROUTER_DEPENDENCY"]
@di_subrouter.get("/subrouter_global_di")
def sync_subrouter_global_dependency(global_dependencies):
return global_dependencies["GLOBAL_DEPENDENCY"]
================================================
FILE: integration_tests/subroutes/file_api.py
================================================
"""
Test routes for issue #1251: static files + API routes at same base path.
Notes:
1. No need to test every method, just one is enough to ensure no conflict.
2. The static files are served from ./integration_tests to avoid conflict with the /test_dir route in main app.
3. The static file serving route is defined in a separate SubRouter to isolate it from the main app routes.
4. Serving api & files from the same /static path to test the fix.
"""
from robyn import Request, SubRouter
static_router = SubRouter(__name__, "/static")
@static_router.get("/build")
@static_router.post("/build")
async def build_handler(request: Request):
"""
Test route ensuring no conflict with static file serving at /static.
Although static files are served at /static, this API route should be reached
because /build is not a file, so the request falls through to the API handler.
"""
return f"{request.method}:{request.url.path} works"
================================================
FILE: integration_tests/templates/test.html
================================================
{# templates/test.html #}
Results
{{framework}} 🤝 {{templating_engine}}
================================================
FILE: integration_tests/test_add_route_without_decorator.py
================================================
from collections.abc import Callable
import pytest
from integration_tests.helpers.http_methods_helpers import get, post, put
@pytest.mark.benchmark
@pytest.mark.usefixtures("session")
@pytest.mark.parametrize(
"route,method",
[
("/sync/get/no_dec", get),
("/async/get/no_dec", get),
("/sync/put/no_dec", put),
("/async/put/no_dec", put),
("/sync/post/no_dec", post),
("/async/post/no_dec", post),
],
)
def test_exception_handling(route: str, method: Callable):
r = method(route, expected_status_code=200)
assert r.text == "Success!"
================================================
FILE: integration_tests/test_app.py
================================================
import pytest
from robyn import ALLOW_CORS, Robyn
from robyn.events import Events
from robyn.robyn import Headers
@pytest.mark.benchmark
def test_add_request_header():
app = Robyn(__file__)
app.set_request_header("server", "robyn")
assert app.request_headers.get_headers() == Headers({"server": "robyn"}).get_headers()
@pytest.mark.benchmark
def test_add_response_header():
app = Robyn(__file__)
app.add_response_header("content-type", "application/json")
assert app.response_headers.get_headers() == Headers({"content-type": "application/json"}).get_headers()
@pytest.mark.benchmark
def test_lifecycle_handlers():
def mock_startup_handler():
pass
async def mock_shutdown_handler():
pass
app = Robyn(__file__)
app.startup_handler(mock_startup_handler)
assert Events.STARTUP in app.event_handlers
startup = app.event_handlers[Events.STARTUP]
assert startup.handler == mock_startup_handler
assert startup.is_async is False
assert startup.number_of_params == 0
app.shutdown_handler(mock_shutdown_handler)
assert Events.SHUTDOWN in app.event_handlers
shutdown = app.event_handlers[Events.SHUTDOWN]
assert shutdown.handler == mock_shutdown_handler
assert shutdown.is_async is True
assert shutdown.number_of_params == 0
@pytest.mark.benchmark
def test_allow_cors():
app = Robyn(__file__)
ALLOW_CORS(app, ["*"])
headers = Headers({})
headers.set("Access-Control-Allow-Origin", "*")
headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS",
)
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization")
headers.set("Access-Control-Allow-Credentials", "true")
assert app.response_headers.get_headers() == headers.get_headers()
================================================
FILE: integration_tests/test_authentication.py
================================================
from urllib.parse import urlparse
import pytest
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_valid_authentication(session, function_type: str):
r = get(f"/{function_type}/auth", headers={"Authorization": "Bearer valid"})
assert r.text == "authenticated"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_valid_authentication_trailing_slash(session, function_type: str):
r = get(f"/{function_type}/auth/", headers={"Authorization": "Bearer valid"})
# Checks whether request is being sent to exact /trailing/ route.
assert urlparse(r.url).path == f"/{function_type}/auth/"
assert r.text == "authenticated"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_token(session, function_type: str):
r = get(
f"/{function_type}/auth",
headers={"Authorization": "Bearer invalid"},
should_check_response=False,
)
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_token_trailing_slash(session, function_type: str):
r = get(
f"/{function_type}/auth/",
headers={"Authorization": "Bearer invalid"},
should_check_response=False,
)
assert urlparse(r.url).path == f"/{function_type}/auth/"
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_header(session, function_type: str):
r = get(
f"/{function_type}/auth",
headers={"Authorization": "Bear valid"},
should_check_response=False,
)
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_header_trailing_slash(session, function_type: str):
r = get(
f"/{function_type}/auth/",
headers={"Authorization": "Bear valid"},
should_check_response=False,
)
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_no_token(session, function_type: str):
r = get(f"/{function_type}/auth", should_check_response=False)
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_invalid_authentication_no_token_trailing_slash(session, function_type: str):
r = get(f"/{function_type}/auth/", should_check_response=False)
assert r.status_code == 401
assert r.headers.get("WWW-Authenticate") == "BearerGetter"
================================================
FILE: integration_tests/test_base_url.py
================================================
import os
import pytest
import requests
from integration_tests.helpers.network_helpers import get_network_host
@pytest.mark.benchmark
def test_default_url_index_request(default_session):
BASE_URL = "http://127.0.0.1:8080"
res = requests.get(f"{BASE_URL}")
assert res.status_code == 200
@pytest.mark.benchmark
def test_local_index_request(session):
BASE_URL = "http://127.0.0.1:8080"
res = requests.get(f"{BASE_URL}")
assert os.getenv("ROBYN_HOST") == "127.0.0.1"
assert res.status_code == 200
@pytest.mark.benchmark
def test_global_index_request(global_session):
host = get_network_host()
BASE_URL = f"http://{host}:8080"
res = requests.get(f"{BASE_URL}")
assert os.getenv("ROBYN_HOST") == f"{host}"
assert res.status_code == 200
================================================
FILE: integration_tests/test_basic_routes.py
================================================
# Test the routes with:
# - the GET method
# - most common return types
# - sync and async
from typing import Optional
import pytest
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route,expected_text,expected_header_key,expected_header_value",
[
("/sync/str", "sync str get", None, None),
("/sync/dict", "sync dict get", "sync", "dict"),
("/sync/response", "sync response get", "sync", "response"),
("/sync/str/const", "sync str const get", None, None),
("/sync/dict/const", "sync dict const get", "sync_const", "dict"),
("/sync/response/const", "sync response const get", "sync_const", "response"),
("/async/str", "async str get", None, None),
("/async/dict", "async dict get", "async", "dict"),
("/async/response", "async response get", "async", "response"),
("/async/str/const", "async str const get", None, None),
("/async/dict/const", "async dict const get", "async_const", "dict"),
(
"/async/response/const",
"async response const get",
"async_const",
"response",
),
],
)
def test_basic_get(
route: str,
expected_text: str,
expected_header_key: Optional[str],
expected_header_value: Optional[str],
session,
):
res = get(route)
assert res.text == expected_text
if expected_header_key is not None:
assert expected_header_key in res.headers
assert res.headers[expected_header_key] == expected_header_value
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route, expected_json",
[
("/sync/json", {"sync json get": "json"}),
("/async/json", {"async json get": "json"}),
("/sync/json/const", {"sync json const get": "json"}),
("/async/json/const", {"async json const get": "json"}),
],
)
def test_json_get(route: str, expected_json: dict, session):
res = get(route)
for key in expected_json.keys():
assert key in res.json()
assert res.json()[key] == expected_json[key]
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route, expected_json",
[
(
"/sync/http/param",
{
"method": "GET",
"url": {
"host": "127.0.0.1:8080",
"path": "/sync/http/param",
"scheme": "http",
},
},
),
(
"/async/http/param",
{
"method": "GET",
"url": {
"host": "127.0.0.1:8080",
"path": "/async/http/param",
"scheme": "http",
},
},
),
],
)
def test_http_request_info_get(route: str, expected_json: dict, session):
res = get(route)
for key in expected_json.keys():
assert key in res.json()
assert res.json()[key] == expected_json[key]
================================================
FILE: integration_tests/test_binary_output.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get
BASE_URL = "http://127.0.0.1:8080"
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route, text",
[
("/sync/octet", "sync octet"),
("/async/octet", "async octet"),
("/sync/octet/response", "sync octet response"),
("/async/octet/response", "async octet response"),
],
)
def test_binary_output(route: str, text: str, session):
r = get(route)
assert r.headers.get("Content-Type") == "application/octet-stream"
assert r.text == text
================================================
FILE: integration_tests/test_delete_requests.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import delete
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_delete(function_type: str, session):
res = delete(f"/{function_type}/dict")
assert res.text == f"{function_type} dict delete"
assert function_type in res.headers
assert res.headers[function_type] == "dict"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_delete_with_param(function_type: str, session):
res = delete(f"/{function_type}/body", data={"hello": "world"})
assert res.text == "hello=world"
================================================
FILE: integration_tests/test_dependency_injection.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
def test_global_dependency_injection():
r = get("/sync/global_di")
assert r.status_code == 200
assert r.text == "GLOBAL DEPENDENCY"
@pytest.mark.benchmark
def test_router_dependency_injection():
r = get("/sync/router_di")
assert r.status_code == 200
assert r.text == "ROUTER DEPENDENCY"
@pytest.mark.benchmark
def test_subrouter_global_dependency_injection():
r = get("/di_subrouter/subrouter_global_di")
assert r.status_code == 200
assert r.text == "GLOBAL DEPENDENCY"
@pytest.mark.benchmark
def test_subrouter_router_dependency_injection():
r = get("/di_subrouter/subrouter_router_di")
assert r.status_code == 200
assert r.text == "ROUTER DEPENDENCY"
================================================
FILE: integration_tests/test_easy_access_params.py
================================================
import os
import pytest
from websocket import create_connection
from integration_tests.helpers.http_methods_helpers import get
WS_BASE_URL = f"ws://127.0.0.1:{os.environ.get('ROBYN_PORT', '8080')}"
# ===== HTTP: Path param + query param with type coercion =====
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_path_and_query_params(session, function_type):
r = get(f"/easy/{function_type}/42?q=hello&page=5")
assert r.json() == {"id": 42, "q": "hello", "page": 5}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_default_value(session, function_type):
r = get(f"/easy/{function_type}/42?q=hello")
assert r.json() == {"id": 42, "q": "hello", "page": 1}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_missing_required_param(session, function_type):
"""Missing required 'q' param should return 400."""
r = get(f"/easy/{function_type}/42", should_check_response=False)
assert r.status_code == 400
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_bad_type_coercion(session, function_type):
"""Path param :id declared as int but given 'abc' should return 400."""
r = get(f"/easy/{function_type}/abc?q=hello", should_check_response=False)
assert r.status_code == 400
# ===== HTTP: Optional params =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_optional_present(session, function_type):
r = get(f"/easy/{function_type}/optional?name=bob&age=30")
assert r.json() == {"name": "bob", "age": 30}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_optional_missing(session, function_type):
r = get(f"/easy/{function_type}/optional?name=bob")
assert r.json() == {"name": "bob", "age": None}
# ===== HTTP: List params =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_list_params(session, function_type):
r = get(f"/easy/{function_type}/list?tag=python&tag=rust&tag=web")
assert r.json() == {"tags": ["python", "rust", "web"]}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_list_single_value(session, function_type):
r = get(f"/easy/{function_type}/list?tag=python")
assert r.json() == {"tags": ["python"]}
# ===== HTTP: Bool params =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_bool_true(session, function_type):
r = get(f"/easy/{function_type}/bool?active=true")
assert r.json() == {"active": True}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_bool_false(session, function_type):
r = get(f"/easy/{function_type}/bool?active=false")
assert r.json() == {"active": False}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_bool_default(session, function_type):
r = get(f"/easy/{function_type}/bool")
assert r.json() == {"active": False}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_bool_numeric(session, function_type):
r = get(f"/easy/{function_type}/bool?active=1")
assert r.json() == {"active": True}
# ===== HTTP: Mixed (Request object + individual params) =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_mixed_with_request(session, function_type):
r = get(f"/easy/{function_type}/mixed/99?q=search")
assert r.json() == {"id": 99, "q": "search", "method": "GET"}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_mixed_with_default(session, function_type):
r = get(f"/easy/{function_type}/mixed/99")
assert r.json() == {"id": 99, "q": "", "method": "GET"}
# ===== HTTP: Float params =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_float(session, function_type):
r = get(f"/easy/{function_type}/float?price=19.99")
assert r.json() == {"price": 19.99}
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_easy_access_float_bad_value(session, function_type):
r = get(f"/easy/{function_type}/float?price=notanumber", should_check_response=False)
assert r.status_code == 400
# ===== WebSocket: Easy access query params =====
def test_easy_access_ws_with_params(session):
ws = create_connection(f"{WS_BASE_URL}/web_socket_easy_access?room=chat&page=5")
connect_msg = ws.recv()
assert connect_msg == "connected to chat"
ws.send("hello")
response = ws.recv()
assert response == "room=chat page=5 msg=hello"
ws.close()
def test_easy_access_ws_with_defaults(session):
ws = create_connection(f"{WS_BASE_URL}/web_socket_easy_access")
connect_msg = ws.recv()
assert connect_msg == "connected to default"
ws.send("hello")
response = ws.recv()
assert response == "room=default page=1 msg=hello"
ws.close()
================================================
FILE: integration_tests/test_exception_handling.py
================================================
from collections.abc import Callable
import pytest
from integration_tests.helpers.http_methods_helpers import get, post, put
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route,method",
[
("/sync/exception/get", get),
("/async/exception/get", get),
("/sync/exception/put", put),
("/async/exception/put", put),
("/sync/exception/post", post),
("/async/exception/post", post),
],
)
def test_exception_handling(
route: str,
method: Callable,
session,
):
r = method(route, expected_status_code=500)
assert r.text == "error msg: value error"
================================================
FILE: integration_tests/test_file_download.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_file_download(function_type: str, session):
r = get(f"/{function_type}/file/download")
assert r.headers.get("Content-Disposition") == "attachment; filename=test.txt"
assert r.text == "This is a test file for the downloading purpose"
================================================
FILE: integration_tests/test_get_requests.py
================================================
import pytest
import requests
from requests import Response
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_param(function_type: str, session):
r = get(f"/{function_type}/param/1")
assert r.text == "1"
r = get(f"/{function_type}/param/12345")
assert r.text == "12345"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_param_suffix(function_type: str, session):
r = get(f"/{function_type}/extra/foo/1/baz")
assert r.text == "foo/1/baz"
r = get(f"/{function_type}/extra/foo/bar/baz")
assert r.text == "foo/bar/baz"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_serve_html(function_type: str, session):
def check_response(r: Response):
assert r.text.startswith("")
assert "Hello world. How are you?" in r.text
check_response(get(f"/{function_type}/serve/html"))
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_template(function_type: str, session):
def check_response(r: Response):
assert r.text.startswith("\n\n")
assert "Jinja2" in r.text
assert "Robyn" in r.text
check_response(get(f"/{function_type}/template"))
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_queries(function_type: str, session):
r = get(f"/{function_type}/queries?hello=robyn")
assert r.json() == {"hello": ["robyn"]}
r = get(f"/{function_type}/queries")
assert r.json() == {}
@pytest.mark.benchmark
def test_trailing_slash(session):
r = requests.get("http://localhost:8080/trailing") # `integration_tests#get` strips the trailing slash, tests always pass!`
assert r.text == "Trailing slash test successful!"
r = requests.get("http://localhost:8080/trailing/")
assert r.text == "Trailing slash test successful!"
@pytest.mark.benchmark
@pytest.mark.parametrize("key, value", [("fakesession", "fake-cookie-session-value")])
def test_cookies(session, key, value):
response = get("/cookie", 200)
# Cookies should be sent via Set-Cookie header, accessible via response.cookies
assert response.cookies[key] == value
@pytest.mark.benchmark
def test_multiple_cookies(session):
response = get("/cookie/multiple", 200)
assert response.cookies["session"] == "abc123"
assert response.cookies["theme"] == "dark"
@pytest.mark.benchmark
def test_cookie_with_attributes(session):
response = get("/cookie/attributes", 200)
# Check the cookie value
assert response.cookies["secure_session"] == "secret123"
# Check the Set-Cookie header for attributes
set_cookie_header = response.headers.get("Set-Cookie", "")
assert "secure_session=secret123" in set_cookie_header
assert "Path=/" in set_cookie_header
assert "HttpOnly" in set_cookie_header
assert "Secure" in set_cookie_header
assert "SameSite=Strict" in set_cookie_header
assert "Max-Age=3600" in set_cookie_header
@pytest.mark.benchmark
def test_cookie_overwrite(session):
response = get("/cookie/overwrite", 200)
# Same-name cookies should be overwritten, final value should be used
assert response.cookies["session"] == "final-value"
================================================
FILE: integration_tests/test_json_types.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get, json_post
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_integer_type_preserved(function_type: str, session):
"""Test that integer values in JSON are preserved as integers, not strings"""
json_data = {"lid": 570, "count": 42}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["lid"]["value"] == 570
assert result["lid"]["type"] == "int"
assert result["count"]["value"] == 42
assert result["count"]["type"] == "int"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_null_type_preserved(function_type: str, session):
"""Test that null values in JSON are preserved as None, not string 'null'"""
json_data = {"start": None, "end": None}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["start"]["value"] is None
assert result["start"]["type"] == "NoneType"
assert result["end"]["value"] is None
assert result["end"]["type"] == "NoneType"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_boolean_type_preserved(function_type: str, session):
"""Test that boolean values in JSON are preserved as booleans, not strings"""
json_data = {"active": True, "deleted": False}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["active"]["value"] is True
assert result["active"]["type"] == "bool"
assert result["deleted"]["value"] is False
assert result["deleted"]["type"] == "bool"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_float_type_preserved(function_type: str, session):
"""Test that float values in JSON are preserved as floats"""
json_data = {"price": 19.99, "rate": 0.15}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["price"]["value"] == 19.99
assert result["price"]["type"] == "float"
assert result["rate"]["value"] == 0.15
assert result["rate"]["type"] == "float"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_string_type_preserved(function_type: str, session):
"""Test that string values in JSON remain strings"""
json_data = {"field_name": "mobile", "field_value": "111000111"}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["field_name"]["value"] == "mobile"
assert result["field_name"]["type"] == "str"
assert result["field_value"]["value"] == "111000111"
assert result["field_value"]["type"] == "str"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_array_type_preserved(function_type: str, session):
"""Test that array values in JSON are preserved as lists"""
json_data = {"items": [1, 2, 3], "tags": ["a", "b"]}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["items"]["value"] == [1, 2, 3]
assert result["items"]["type"] == "list"
assert result["tags"]["value"] == ["a", "b"]
assert result["tags"]["type"] == "list"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_nested_object_type_preserved(function_type: str, session):
"""Test that nested object values in JSON are preserved as dicts"""
json_data = {"user": {"name": "John", "age": 30}}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
assert result["user"]["value"] == {"name": "John", "age": 30}
assert result["user"]["type"] == "dict"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_mixed_types_preserved(function_type: str, session):
"""Test the exact scenario from the bug report - mixed types in one request"""
json_data = {
"lid": 570,
"start": None,
"field_name": "mobile",
"field_value": "111000111",
}
res = json_post(f"/{function_type}/request_json/types", json_data=json_data)
result = res.json()
# Integer should remain integer (not become "570")
assert result["lid"]["value"] == 570
assert result["lid"]["type"] == "int"
# None should remain None (not become "null")
assert result["start"]["value"] is None
assert result["start"]["type"] == "NoneType"
# Strings should remain strings
assert result["field_name"]["value"] == "mobile"
assert result["field_name"]["type"] == "str"
assert result["field_value"]["value"] == "111000111"
assert result["field_value"]["type"] == "str"
# ===== Top-level JSON Array Parsing Tests (Issue #1145) =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_top_level_array_of_strings(function_type: str, session):
"""Test that request.json() handles a top-level array of strings (exact scenario from #1145)"""
json_data = ["google_docs", "notion"]
res = json_post(f"/{function_type}/request_json/array", json_data=json_data)
result = res.json()
assert result["type"] == "list"
assert result["parsed"] == ["google_docs", "notion"]
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_top_level_array_of_objects(function_type: str, session):
"""Test that request.json() handles a top-level array of objects"""
json_data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
res = json_post(f"/{function_type}/request_json/array", json_data=json_data)
result = res.json()
assert result["type"] == "list"
assert result["parsed"] == [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_top_level_empty_array(function_type: str, session):
"""Test that request.json() handles an empty top-level array"""
json_data = []
res = json_post(f"/{function_type}/request_json/array", json_data=json_data)
result = res.json()
assert result["type"] == "list"
assert result["parsed"] == []
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_top_level_array_of_mixed_types(function_type: str, session):
"""Test that request.json() handles a top-level array with mixed types"""
json_data = [1, "two", True, None, {"key": "value"}]
res = json_post(f"/{function_type}/request_json/array", json_data=json_data)
result = res.json()
assert result["type"] == "list"
assert result["parsed"] == [1, "two", True, None, {"key": "value"}]
# ===== JSON List Serialization Tests (Issue #1300) =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_list_response_serialization(function_type: str, session):
"""Test that returning a list from a handler is properly serialized as JSON"""
res = get(f"/{function_type}/json/list")
# Check content type is application/json
assert res.headers["content-type"] == "application/json"
# Check that response is valid JSON (not Python str representation)
result = res.json()
assert isinstance(result, list)
assert len(result) == 3
# Verify the data structure and types are correct
assert result[0] == {"id": 1, "title": "First Post", "published": True}
assert result[1] == {"id": 2, "title": "Draft Post", "published": False}
assert result[2] == {"id": 3, "title": "Latest Post", "published": True}
# Verify booleans are proper JSON booleans (true/false), not Python (True/False)
# This is implicitly tested by res.json() succeeding, but let's verify the raw response too
assert "true" in res.text.lower()
assert "false" in res.text.lower()
assert "True" not in res.text # Python boolean should not appear
assert "False" not in res.text
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_empty_list_response_serialization(function_type: str, session):
"""Test that returning an empty list is properly serialized as JSON"""
res = get(f"/{function_type}/json/list/empty")
assert res.headers["content-type"] == "application/json"
result = res.json()
assert result == []
assert res.text == "[]"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_list_primitives_response_serialization(function_type: str, session):
"""Test that a list of primitives is properly serialized as JSON"""
res = get(f"/{function_type}/json/list/primitives")
assert res.headers["content-type"] == "application/json"
result = res.json()
assert result == [1, 2, 3, "four", True, None]
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_dict_response_auto_serialization(function_type: str, session):
"""Test that returning a dict from a handler is properly auto-serialized as JSON"""
res = get(f"/{function_type}/json/dict")
assert res.headers["content-type"] == "application/json"
result = res.json()
assert result["message"] == f"{function_type} dict"
assert result["count"] == 42
assert result["active"] is True
================================================
FILE: integration_tests/test_middlewares.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_middlewares(function_type: str, session):
r = get(f"/{function_type}/middlewares")
headers = r.headers
# We do not want the request headers to be in the response
assert not headers.get("before")
assert headers.get("after")
assert r.headers.get("after") == f"{function_type}_after_request"
assert r.text == f"{function_type} middlewares after"
@pytest.mark.benchmark
def test_global_middleware(session):
r = get("/sync/global/middlewares")
headers = r.headers
assert headers.get("global_after")
assert r.headers.get("global_after") == "global_after_request"
assert r.text == "sync global middlewares"
@pytest.mark.benchmark
def test_response_in_before_middleware(session):
r = get("/sync/middlewares/401", should_check_response=False)
assert r.status_code == 401
@pytest.mark.benchmark
@pytest.mark.parametrize(
"route",
[
"/sync/str/const",
"/async/str/const",
"/sync/dict/const",
"/async/dict/const",
"/sync/response/const",
"/async/response/const",
],
)
def test_global_middleware_applied_to_const_routes(route: str, session):
r = get(route)
assert r.headers.get("global_after") == "global_after_request", f"Global after-request middleware was not applied to const route {route}"
================================================
FILE: integration_tests/test_multipart_data.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import multipart_post
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync"])
def test_form_data(function_type: str, session):
res = multipart_post(f"/{function_type}/form_data", files={"hello": "world"})
assert "multipart" in res.text
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync"])
def test_multipart_file(function_type: str, session):
res = multipart_post(f"/{function_type}/multipart-file", files={"hello": "world"})
assert "hello" in res.text
================================================
FILE: integration_tests/test_openapi.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get, json_post
from robyn import Robyn
@pytest.mark.benchmark
def test_custom_openapi_spec():
app = Robyn(__file__, openapi_file_path="openapi_config.json")
openapi_spec = app.openapi.openapi_spec
assert isinstance(openapi_spec, dict)
assert "openapi" in openapi_spec
assert "info" in openapi_spec
assert "paths" in openapi_spec
assert "components" in openapi_spec
assert "servers" in openapi_spec
assert "externalDocs" in openapi_spec
assert openapi_spec["info"]["title"] == "Robyn Test API"
assert openapi_spec["info"]["version"] == "1.0.0"
@pytest.mark.benchmark
def test_docs_handler():
# should_check_response = False because check_response raises a
# failure if the global headers are not present in the response
# provided we are excluding headers for /docs and /openapi.json
html_response = get("/docs", should_check_response=False)
assert html_response.status_code == 200
@pytest.mark.benchmark
def test_json_handler():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
assert "openapi" in openapi_spec
assert "info" in openapi_spec
assert "paths" in openapi_spec
assert "components" in openapi_spec
assert "servers" in openapi_spec
assert "externalDocs" in openapi_spec
@pytest.mark.benchmark
def test_add_openapi_path():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "get"
endpoint = "/openapi_test"
openapi_description = "Get openapi"
openapi_tags = ["test tag"]
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert openapi_description == openapi_spec["paths"][endpoint][route_type]["description"]
assert openapi_tags == openapi_spec["paths"][endpoint][route_type]["tags"]
@pytest.mark.benchmark
def test_add_subrouter_paths():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
endpoint = "/sub_router/openapi_test"
openapi_description = "Get subrouter openapi"
openapi_tags = ["test subrouter tag"]
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert openapi_description == openapi_spec["paths"][endpoint][route_type]["description"]
assert openapi_tags == openapi_spec["paths"][endpoint][route_type]["tags"]
@pytest.mark.benchmark
def test_openapi_request_body():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
endpoint = "/openapi_request_body"
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "requestBody" in openapi_spec["paths"][endpoint][route_type]
assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"]
assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]
assert "schema" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]
assert "properties" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]
assert "name" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]
assert "description" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]
assert "price" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]
assert "tax" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]
assert "string" == openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["description"]["type"]
assert "number" == openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["price"]["type"]
assert "number" == openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["tax"]["type"]
assert "object" == openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["type"]
assert "first" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
assert "second" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
assert "initial" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
assert (
"object"
in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]["initial"][
"type"
]
)
assert (
"is_present"
in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]["initial"][
"properties"
]
)
assert (
"letter"
in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]["initial"][
"properties"
]
)
assert {"type": "string"} in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"][
"properties"
]["initial"]["properties"]["letter"]["anyOf"]
assert {"type": "null"} in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"][
"properties"
]["initial"]["properties"]["letter"]["anyOf"]
@pytest.mark.benchmark
def test_openapi_response_body():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
endpoint = "/openapi_request_body"
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "responses" in openapi_spec["paths"][endpoint][route_type]
assert "200" in openapi_spec["paths"][endpoint][route_type]["responses"]
assert openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["description"] == "Successful Response"
assert "content" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]
assert "application/json" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]
assert "schema" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]
assert "properties" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]
assert "success" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]
assert "items_changed" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]
assert (
"boolean" == openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["success"]["type"]
)
assert (
"integer"
== openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["items_changed"]["type"]
)
@pytest.mark.benchmark
def test_openapi_query_params():
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
endpoint = "/openapi_request_body"
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "parameters" in openapi_spec["paths"][endpoint][route_type]
assert "required" == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["name"]
assert "query" == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["in"]
assert {"type": "boolean"} == openapi_spec["paths"][endpoint][route_type]["parameters"][0]["schema"]
@pytest.mark.benchmark
def test_openapi_json_body_typed():
"""Test that a typed JsonBody subclass generates a proper requestBody schema in OpenAPI docs."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
endpoint = "/openapi_json_body"
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "requestBody" in openapi_spec["paths"][endpoint][route_type]
assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"]
assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]
assert "schema" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]
assert "properties" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]
properties = openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]
assert "fahrenheit" in properties
assert "number" == properties["fahrenheit"]["type"]
@pytest.mark.benchmark
def test_openapi_json_body_bare():
"""Test that a bare JsonBody generates a requestBody with empty properties in OpenAPI docs."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
assert isinstance(openapi_spec, dict)
route_type = "post"
# bare JsonBody routes should still have requestBody in the spec
endpoint = "/sync/json_body/bare"
assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "requestBody" in openapi_spec["paths"][endpoint][route_type]
assert "content" in openapi_spec["paths"][endpoint][route_type]["requestBody"]
assert "application/json" in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]
try:
import pydantic # noqa: F401
_HAS_PYDANTIC = True
except ImportError:
_HAS_PYDANTIC = False
@pytest.mark.benchmark
@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed")
def test_openapi_pydantic_request_body():
"""Pydantic model on a regular route should produce a full JSON Schema in
requestBody — no dedicated OpenAPI route needed."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/pydantic/user"
route = openapi_spec["paths"][endpoint]["post"]
assert route["tags"] == ["pydantic"]
assert route["description"] == "Create a user with Pydantic validation"
assert "requestBody" in route
schema = route["requestBody"]["content"]["application/json"]["schema"]
assert schema["type"] == "object"
assert schema["title"] == "UserCreate"
props = schema["properties"]
assert props["name"]["type"] == "string"
assert props["name"]["title"] == "Name"
assert props["email"]["type"] == "string"
assert props["email"]["title"] == "Email"
assert props["age"]["type"] == "integer"
assert props["age"]["title"] == "Age"
assert props["active"]["type"] == "boolean"
assert props["active"]["title"] == "Active"
assert props["active"]["default"] is True
assert set(schema["required"]) == {"name", "email", "age"}
assert "active" not in schema["required"]
assert "responses" in route
assert "200" in route["responses"]
assert "application/json" in route["responses"]["200"]["content"]
@pytest.mark.benchmark
@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed")
def test_openapi_pydantic_nested_model():
"""Nested Pydantic models on a regular route should use $ref and populate
components/schemas — no dedicated OpenAPI route needed."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/pydantic/nested"
route = openapi_spec["paths"][endpoint]["post"]
assert route["tags"] == ["pydantic"]
assert route["description"] == "Create a user with nested address"
schema = route["requestBody"]["content"]["application/json"]["schema"]
assert schema["type"] == "object"
assert schema["title"] == "UserWithAddress"
assert schema["properties"]["name"]["type"] == "string"
assert schema["properties"]["email"]["type"] == "string"
assert schema["properties"]["address"]["$ref"] == "#/components/schemas/Address"
assert set(schema["required"]) == {"name", "email", "address"}
assert "Address" in openapi_spec["components"]["schemas"]
address_schema = openapi_spec["components"]["schemas"]["Address"]
assert address_schema["type"] == "object"
assert address_schema["title"] == "Address"
assert address_schema["properties"]["street"]["type"] == "string"
assert address_schema["properties"]["street"]["title"] == "Street"
assert address_schema["properties"]["city"]["type"] == "string"
assert address_schema["properties"]["city"]["title"] == "City"
assert address_schema["properties"]["zip_code"]["type"] == "string"
assert address_schema["properties"]["zip_code"]["title"] == "Zip Code"
assert set(address_schema["required"]) == {"street", "city", "zip_code"}
assert "responses" in route
assert "200" in route["responses"]
assert "application/json" in route["responses"]["200"]["content"]
@pytest.mark.benchmark
@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed")
def test_openapi_pydantic_return_type():
"""When a route has a Pydantic model as return type annotation, the response
schema should reflect the full Pydantic model schema, not just 'object'."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/pydantic/return_model"
route = openapi_spec["paths"][endpoint]["post"]
assert route["tags"] == ["pydantic"]
assert route["description"] == "Return the validated Pydantic model directly"
response_schema = route["responses"]["200"]["content"]["application/json"]["schema"]
assert response_schema["type"] == "object"
assert response_schema["title"] == "UserCreate"
assert "properties" in response_schema
assert response_schema["properties"]["name"]["type"] == "string"
assert response_schema["properties"]["age"]["type"] == "integer"
assert set(response_schema["required"]) == {"name", "email", "age"}
@pytest.mark.benchmark
@pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed")
def test_openapi_pydantic_return_list_type():
"""When a route returns list[PydanticModel], the response schema should be
an array with items containing the full Pydantic model schema."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/pydantic/return_list"
route = openapi_spec["paths"][endpoint]["post"]
assert route["tags"] == ["pydantic"]
assert route["description"] == "Return a list of Pydantic models"
response_schema = route["responses"]["200"]["content"]["application/json"]["schema"]
assert response_schema["type"] == "array"
assert "items" in response_schema
items = response_schema["items"]
assert items["type"] == "object"
assert items["title"] == "UserCreate"
assert items["properties"]["name"]["type"] == "string"
assert items["properties"]["age"]["type"] == "integer"
# ===== TypedDict request body tests =====
@pytest.mark.benchmark
def test_openapi_typeddict_request_body():
"""TypedDict subclass used as a parameter annotation should produce a
requestBody schema in OpenAPI docs (issue #1254)."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/typeddict/body"
route = openapi_spec["paths"][endpoint]["post"]
assert route["tags"] == ["typeddict"]
assert "requestBody" in route
schema = route["requestBody"]["content"]["application/json"]["schema"]
assert "properties" in schema
assert "name" in schema["properties"]
assert "value" in schema["properties"]
assert schema["properties"]["name"]["type"] == "string"
assert schema["properties"]["value"]["type"] == "integer"
assert "responses" in route
assert "200" in route["responses"]
response_schema = route["responses"]["200"]["content"]["application/json"]["schema"]
assert "properties" in response_schema
assert "result" in response_schema["properties"]
assert "count" in response_schema["properties"]
@pytest.mark.benchmark
def test_openapi_typeddict_with_request():
"""TypedDict body combined with a Request param should still produce
a requestBody schema (issue #1254)."""
openapi_response = get("/openapi.json", should_check_response=False)
assert openapi_response.status_code == 200
openapi_spec = openapi_response.json()
endpoint = "/sync/typeddict/with_request"
route = openapi_spec["paths"][endpoint]["post"]
assert "requestBody" in route
schema = route["requestBody"]["content"]["application/json"]["schema"]
assert "properties" in schema
assert "name" in schema["properties"]
assert "value" in schema["properties"]
@pytest.mark.benchmark
def test_typeddict_body_injection_sync():
"""TypedDict-annotated parameter should receive the parsed JSON dict
at runtime, not the raw string (issue #1254)."""
response = json_post(
"/sync/typeddict/body",
json_data={"name": "alice", "value": 42},
)
assert response.status_code == 200
data = response.json()
assert data["result"] == "alice"
assert data["count"] == 42
@pytest.mark.benchmark
def test_typeddict_body_injection_async():
"""Async handler with TypedDict body should also receive parsed JSON."""
response = json_post(
"/async/typeddict/body",
json_data={"name": "bob", "value": 7},
)
assert response.status_code == 200
data = response.json()
assert data["result"] == "bob"
assert data["count"] == 7
@pytest.mark.benchmark
def test_typeddict_body_with_request_injection():
"""TypedDict body and Request object should coexist in the same handler."""
response = json_post(
"/sync/typeddict/with_request",
json_data={"name": "charlie", "value": 99},
)
assert response.status_code == 200
data = response.json()
assert data["method"] == "POST"
assert data["name"] == "charlie"
@pytest.mark.benchmark
def test_typeddict_body_invalid_json():
"""Sending invalid JSON to a TypedDict-annotated handler should return 400."""
import requests
response = requests.post(
"http://127.0.0.1:8080/sync/typeddict/body",
data="not valid json",
headers={"Content-Type": "application/json"},
)
assert response.status_code == 400
================================================
FILE: integration_tests/test_patch_requests.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import patch
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_patch(function_type: str, session):
res = patch(f"/{function_type}/dict")
assert res.text == f"{function_type} dict patch"
assert function_type in res.headers
assert res.headers[function_type] == "dict"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_patch_with_param(function_type: str, session):
res = patch(f"/{function_type}/body", data={"hello": "world"})
assert res.text == "hello=world"
================================================
FILE: integration_tests/test_post_requests.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import post
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_post(function_type: str, session):
res = post(f"/{function_type}/dict")
assert res.text == f"{function_type} dict post"
assert function_type in res.headers
assert res.headers[function_type] == "dict"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_post_with_param(function_type: str, session):
res = post(f"/{function_type}/body", data={"hello": "world"})
assert res.text == "hello=world"
================================================
FILE: integration_tests/test_put_requests.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import put
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_put(function_type: str, session):
res = put(f"/{function_type}/dict")
assert res.text == f"{function_type} dict put"
assert function_type in res.headers
assert res.headers[function_type] == "dict"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_put_with_param(function_type: str, session):
res = put(f"/{function_type}/body", data={"hello": "world"})
assert res.text == "hello=world"
================================================
FILE: integration_tests/test_pydantic.py
================================================
import pytest
import requests
from integration_tests.helpers.http_methods_helpers import json_post
BASE_URL = "http://127.0.0.1:8080"
try:
import pydantic
_HAS_PYDANTIC = True
except ImportError:
_HAS_PYDANTIC = False
pytestmark = pytest.mark.skipif(not _HAS_PYDANTIC, reason="pydantic not installed")
def _raw_post(endpoint: str, data: str, content_type: str = "application/json") -> requests.Response:
url = f"{BASE_URL}/{endpoint.lstrip('/')}"
return requests.post(url, data=data, headers={"Content-Type": content_type})
# ===== Valid Pydantic Body =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_valid_user_all_fields(function_type: str, session):
"""All fields provided explicitly — every field value must round-trip correctly."""
json_data = {"name": "Alice", "email": "alice@example.com", "age": 30, "active": False}
res = json_post(f"/{function_type}/pydantic/user", json_data=json_data)
result = res.json()
assert result["name"] == "Alice"
assert result["email"] == "alice@example.com"
assert result["age"] == 30
assert result["active"] is False
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_default_field_applied(function_type: str, session):
"""Omitting 'active' should use the model default (True) and the handler must see it."""
json_data = {"name": "Bob", "email": "bob@example.com", "age": 25}
res = json_post(f"/{function_type}/pydantic/user", json_data=json_data)
result = res.json()
assert result["name"] == "Bob"
assert result["email"] == "bob@example.com"
assert result["age"] == 25
assert result["active"] is True
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_string_to_int_coercion(function_type: str, session):
"""Pydantic v2 in lax mode (default) coerces '30' string to int 30."""
json_data = {"name": "Coerce", "email": "c@test.com", "age": "30"}
res = json_post(f"/{function_type}/pydantic/user", json_data=json_data)
result = res.json()
assert result["name"] == "Coerce"
assert result["age"] == 30
assert isinstance(result["age"], int)
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_extra_fields_ignored(function_type: str, session):
"""Extra fields not in the model should be silently ignored (pydantic v2 default)."""
json_data = {"name": "Eve", "email": "eve@example.com", "age": 28, "extra_field": "should_be_ignored", "another": 99}
res = json_post(f"/{function_type}/pydantic/user", json_data=json_data)
result = res.json()
assert result["name"] == "Eve"
assert result["age"] == 28
assert "extra_field" not in result
assert "another" not in result
# ===== Invalid Pydantic Body — error structure verification =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_missing_single_required_field(function_type: str, session):
"""Missing 'age' should produce exactly one error with correct loc, type, and msg."""
json_data = {"name": "Charlie", "email": "charlie@example.com"}
res = json_post(
f"/{function_type}/pydantic/user",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
errors = result["detail"]
assert isinstance(errors, list)
assert len(errors) == 1
err = errors[0]
assert err["loc"] == ["age"]
assert err["type"] == "missing"
assert "required" in err["msg"].lower()
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_missing_all_required_fields(function_type: str, session):
"""Sending {} should produce errors for all 3 required fields (name, email, age)."""
res = json_post(
f"/{function_type}/pydantic/user",
json_data={},
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
errors = result["detail"]
assert isinstance(errors, list)
error_fields = {tuple(e["loc"]) for e in errors}
assert ("name",) in error_fields
assert ("email",) in error_fields
assert ("age",) in error_fields
for err in errors:
assert err["type"] == "missing"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_wrong_type_error_detail(function_type: str, session):
"""Wrong type should produce error with correct loc and a meaningful msg."""
json_data = {"name": "Diana", "email": "diana@example.com", "age": "not_a_number"}
res = json_post(
f"/{function_type}/pydantic/user",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
errors = result["detail"]
age_errors = [e for e in errors if e["loc"] == ["age"]]
assert len(age_errors) == 1
assert "int" in age_errors[0]["type"]
assert "input" in age_errors[0]
assert age_errors[0]["input"] == "not_a_number"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_multiple_type_errors(function_type: str, session):
"""Multiple fields with wrong types should each produce their own error."""
json_data = {"name": 12345, "email": True, "age": "bad"}
res = json_post(
f"/{function_type}/pydantic/user",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
error_locs = {tuple(e["loc"]) for e in result["detail"]}
assert ("name",) in error_locs
assert ("email",) in error_locs
assert ("age",) in error_locs
# ===== Malformed / edge-case bodies =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_invalid_json_syntax(function_type: str, session):
"""Completely invalid JSON should return 422 with 'Invalid request body' error."""
res = _raw_post(f"/{function_type}/pydantic/user", data="not json at all {{{")
assert res.status_code == 422
result = res.json()
assert "error" in result
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_empty_body(function_type: str, session):
"""Empty body should return 422."""
res = _raw_post(f"/{function_type}/pydantic/user", data="")
assert res.status_code == 422
result = res.json()
assert "error" in result
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_json_array_body(function_type: str, session):
"""A JSON array instead of an object should return 422."""
res = _raw_post(f"/{function_type}/pydantic/user", data='[{"name": "X"}]')
assert res.status_code == 422
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_null_body(function_type: str, session):
"""JSON null body should return 422."""
res = _raw_post(f"/{function_type}/pydantic/user", data="null")
assert res.status_code == 422
# ===== Pydantic + Request object =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_with_request_object(function_type: str, session):
"""Handler receiving both Request and Pydantic model must see both correctly."""
json_data = {"name": "Frank", "email": "frank@example.com", "age": 35}
res = json_post(f"/{function_type}/pydantic/user_with_request", json_data=json_data)
result = res.json()
assert result["method"] == "POST"
assert result["name"] == "Frank"
assert result["email"] == "frank@example.com"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_with_request_validation_still_works(function_type: str, session):
"""Validation must still trigger 422 even when Request is in the signature."""
json_data = {"name": "Frank"} # missing email and age
res = json_post(
f"/{function_type}/pydantic/user_with_request",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
error_fields = {tuple(e["loc"]) for e in result["detail"]}
assert ("email",) in error_fields
assert ("age",) in error_fields
# ===== Nested Pydantic Models =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_nested_model_valid(function_type: str, session):
"""Valid nested model should be parsed and accessible through the parent."""
json_data = {
"name": "Grace",
"email": "grace@example.com",
"address": {"street": "123 Main St", "city": "Springfield", "zip_code": "62701"},
}
res = json_post(f"/{function_type}/pydantic/nested", json_data=json_data)
result = res.json()
assert result["name"] == "Grace"
assert result["city"] == "Springfield"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_nested_model_missing_nested_fields(function_type: str, session):
"""Missing fields in nested model should produce errors with correct nested loc paths."""
json_data = {
"name": "Grace",
"email": "grace@example.com",
"address": {"street": "123 Main St"}, # missing city and zip_code
}
res = json_post(
f"/{function_type}/pydantic/nested",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
errors = result["detail"]
error_locs = {tuple(e["loc"]) for e in errors}
assert ("address", "city") in error_locs
assert ("address", "zip_code") in error_locs
for err in errors:
assert err["type"] == "missing"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_nested_model_missing_entirely(function_type: str, session):
"""Missing the entire nested object should produce an error at the parent field."""
json_data = {"name": "Grace", "email": "grace@example.com"}
res = json_post(
f"/{function_type}/pydantic/nested",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
errors = result["detail"]
address_errors = [e for e in errors if e["loc"] == ["address"]]
assert len(address_errors) == 1
assert address_errors[0]["type"] == "missing"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_nested_model_wrong_type(function_type: str, session):
"""Passing a non-object for the nested model should return 422."""
json_data = {
"name": "Grace",
"email": "grace@example.com",
"address": "not an object",
}
res = json_post(
f"/{function_type}/pydantic/nested",
json_data=json_data,
expected_status_code=422,
should_check_response=False,
)
assert res.status_code == 422
result = res.json()
errors = result["detail"]
address_errors = [e for e in errors if "address" in e["loc"]]
assert len(address_errors) >= 1
# ===== Pydantic with PUT =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_put_valid(function_type: str, session):
"""Pydantic validation must work with PUT method."""
json_data = {"name": "Hank", "email": "hank@example.com", "age": 40}
res = requests.put(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
result = res.json()
assert result["updated"] is True
assert result["name"] == "Hank"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_put_invalid(function_type: str, session):
"""PUT with invalid body must also return 422 with proper error structure."""
json_data = {"name": "Hank"} # missing email and age
res = requests.put(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
error_fields = {tuple(e["loc"]) for e in result["detail"]}
assert ("email",) in error_fields
assert ("age",) in error_fields
# ===== Pydantic with PATCH =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_patch_valid(function_type: str, session):
"""Pydantic validation must work with PATCH method."""
json_data = {"name": "Iris", "email": "iris@example.com", "age": 29}
res = requests.patch(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
result = res.json()
assert result["patched"] is True
assert result["name"] == "Iris"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_patch_invalid(function_type: str, session):
"""PATCH with invalid body must also return 422."""
json_data = {"name": "Iris"} # missing email and age
res = requests.patch(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
error_fields = {tuple(e["loc"]) for e in result["detail"]}
assert ("email",) in error_fields
assert ("age",) in error_fields
# ===== Pydantic with DELETE =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_delete_valid(function_type: str, session):
"""Pydantic validation must work with DELETE method."""
json_data = {"name": "Zara", "email": "zara@example.com", "age": 33}
res = requests.delete(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
result = res.json()
assert result["deleted"] is True
assert result["name"] == "Zara"
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_delete_invalid(function_type: str, session):
"""DELETE with invalid body must also return 422 with proper error structure."""
json_data = {"name": "Zara"} # missing email and age
res = requests.delete(f"{BASE_URL}/{function_type}/pydantic/user", json=json_data)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
error_fields = {tuple(e["loc"]) for e in result["detail"]}
assert ("email",) in error_fields
assert ("age",) in error_fields
# ===== Returning Pydantic models directly =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_return_model_directly(function_type: str, session):
"""Returning a Pydantic model from a handler should auto-serialize to JSON."""
json_data = {"name": "Jack", "email": "jack@example.com", "age": 32}
res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data)
assert res.status_code == 200
assert "application/json" in res.headers.get("content-type", "")
result = res.json()
assert result["name"] == "Jack"
assert result["email"] == "jack@example.com"
assert result["age"] == 32
assert result["active"] is True # default field
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_return_model_preserves_all_fields(function_type: str, session):
"""Returned model should include every field, including those with defaults."""
json_data = {"name": "Kate", "email": "kate@example.com", "age": 27, "active": False}
res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data)
result = res.json()
assert result["name"] == "Kate"
assert result["email"] == "kate@example.com"
assert result["age"] == 27
assert result["active"] is False
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_return_model_validation_still_works(function_type: str, session):
"""Validation should still trigger 422 even when the route returns a model."""
json_data = {"name": "Kate"} # missing email and age
res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_model", json=json_data)
assert res.status_code == 422
result = res.json()
assert result["error"] == "Validation Error"
# ===== Returning lists of Pydantic models =====
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_pydantic_return_list_of_models(function_type: str, session):
"""Returning a list of Pydantic models should auto-serialize to a JSON array."""
json_data = {"name": "Leo", "email": "leo@example.com", "age": 45}
res = requests.post(f"{BASE_URL}/{function_type}/pydantic/return_list", json=json_data)
assert res.status_code == 200
assert "application/json" in res.headers.get("content-type", "")
result = res.json()
assert isinstance(result, list)
assert len(result) == 2
assert result[0]["name"] == "Leo"
assert result[1]["name"] == "Leo"
assert result[0]["active"] is True
================================================
FILE: integration_tests/test_request_json.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import post
@pytest.mark.parametrize(
"route, body, expected_result",
[
("/sync/request_json", '{"hello": "world"}', ""),
("/sync/request_json/key", '{"key": "world"}', "world"),
("/sync/request_json", '{"hello": "world"', "None"),
("/async/request_json", '{"hello": "world"}', ""),
("/async/request_json", '{"hello": "world"', "None"),
],
)
def test_request(route, body, expected_result):
res = post(route, body)
assert res.text == expected_result
================================================
FILE: integration_tests/test_split_request_params.py
================================================
import pytest
from integration_tests.helpers.http_methods_helpers import get, json_post, post
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_query_params(session, type_route, function_type):
r = get(f"/{function_type}/{type_route}/query_params?hello=robyn")
assert r.json() == {"hello": ["robyn"]}
r = get(f"/{function_type}/{type_route}/query_params?hello=robyn&a=1&b=2")
assert r.json() == {"hello": ["robyn"], "a": ["1"], "b": ["2"]}
r = get(f"/{function_type}/{type_route}/query_params")
assert r.json() == {}
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_headers(session, type_route, function_type):
r = get(f"/{function_type}/{type_route}/headers")
assert r.text == "robyn"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_path_params(session, type_route, function_type):
r = get(f"/{function_type}/{type_route}/path_params/123")
assert r.json() == {"id": "123"}
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_method(session, type_route, function_type):
r = get(f"/{function_type}/{type_route}/method")
assert r.text == "GET"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_body(session, type_route, function_type):
res = post(f"/{function_type}/{type_route}/body", data={"hello": "world"})
assert res.text == "hello=world"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
@pytest.mark.parametrize("type_route", ["split_request_untyped", "split_request_typed"])
def test_split_request_params_get_combined(session, type_route, function_type):
res = post(
f"/{function_type}/{type_route}/combined?hello=robyn&a=1&b=2",
data={"hello": "world"},
)
out = res.json()
assert out["query_params"] == {"hello": ["robyn"], "a": ["1"], "b": ["2"]}
assert out["body"] == "hello=world"
assert out["method"] == "POST"
assert out["url"] == f"/{function_type}/{type_route}/combined"
assert out["headers"] == "robyn"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_split_request_params_typed_untyped_post_combined(session, function_type):
res = post(
f"/{function_type}/split_request_typed_untyped/combined?hello=robyn&a=1&b=2",
data={"hello": "world"},
)
out = res.json()
assert out["query_params"] == {"hello": ["robyn"], "a": ["1"], "b": ["2"]}
assert out["body"] == "hello=world"
assert out["method"] == "POST"
assert out["url"] == f"/{function_type}/split_request_typed_untyped/combined"
assert out["headers"] == "robyn"
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_split_request_params_get_combined_failure(session, function_type):
# 'vishnu' is an unknown param with no default — now returns 400 (was 500 before easy-access params)
# because unresolved params are treated as missing required query params
res = post(f"/{function_type}/split_request_typed_untyped/combined/failure?hello=robyn&a=1&b=2", data={"hello": "world"}, should_check_response=False)
assert 400 == res.status_code
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_body_bare(session, function_type):
"""Test that bare JsonBody passes the parsed JSON dict to the handler."""
res = json_post(f"/{function_type}/json_body/bare", json_data={"hello": "world", "count": 42})
assert res.json() == {"hello": "world", "count": 42}
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_json_body_typed(session, function_type):
"""Test that typed JsonBody subclass passes the parsed JSON dict to the handler."""
res = json_post(f"/{function_type}/json_body/typed", json_data={"fahrenheit": 212})
result = res.json()
assert result["celsius"] == pytest.approx(100.0)
================================================
FILE: integration_tests/test_sse.py
================================================
import json
import pytest
import requests
from integration_tests.helpers.http_methods_helpers import BASE_URL
from robyn.responses import SSEMessage, SSEResponse, StreamingResponse
@pytest.mark.benchmark
def test_sse_basic_headers(session):
"""Test that SSE endpoints return correct headers"""
response = requests.get(f"{BASE_URL}/sse/basic", stream=True)
assert response.status_code == 200
assert response.headers.get("Content-Type") == "text/event-stream"
# Accept either clean optimized headers or legacy compatibility
cache_control = response.headers.get("Cache-Control")
assert cache_control in ["no-cache, no-store, must-revalidate", "no-cache, no-cache, no-store, must-revalidate"]
@pytest.mark.benchmark
def test_sse_basic_stream(session):
"""Test basic SSE streaming with simple data messages"""
response = requests.get(f"{BASE_URL}/sse/basic", stream=True, timeout=5)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if content.count("\n\n") >= 3: # Got 3 messages
break
# Parse events
events = []
for line in content.split("\n"):
if line.startswith("data: "):
events.append(line[6:]) # Remove 'data: ' prefix
assert len(events) >= 3
for i in range(3):
assert f"Test message {i}" in events
@pytest.mark.benchmark
def test_sse_formatted_messages(session):
"""Test SSE messages formatted with SSEMessage helper"""
response = requests.get(f"{BASE_URL}/sse/formatted", stream=True, timeout=5)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if content.count("\n\n") >= 3:
break
# Should contain event and id fields
assert "event: test" in content
assert "id: 0" in content
assert "id: 1" in content
assert "id: 2" in content
# Parse data lines
data_lines = []
for line in content.split("\n"):
if line.startswith("data: "):
data_lines.append(line[6:])
assert len(data_lines) >= 3
for i in range(3):
assert f"Formatted message {i}" in data_lines
@pytest.mark.benchmark
def test_sse_json_data(session):
"""Test SSE streaming with JSON data"""
response = requests.get(f"{BASE_URL}/sse/json", stream=True, timeout=5)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if content.count("\n\n") >= 3:
break
# Parse data lines
data_lines = []
for line in content.split("\n"):
if line.startswith("data: "):
data_lines.append(line[6:])
assert len(data_lines) >= 3
for i in range(3):
json_data = json.loads(data_lines[i])
assert json_data["id"] == i
assert json_data["message"] == f"JSON message {i}"
assert json_data["type"] == "test"
@pytest.mark.benchmark
def test_sse_named_events(session):
"""Test SSE with different event types"""
response = requests.get(f"{BASE_URL}/sse/named_events", stream=True, timeout=5)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if content.count("\n\n") >= 3:
break
# Should contain different event types
assert "event: start" in content
assert "event: progress" in content
assert "event: end" in content
# Should contain corresponding data
assert "data: Test started" in content
assert "data: Test in progress" in content
assert "data: Test completed" in content
@pytest.mark.benchmark
def test_sse_async_endpoint(session):
"""Test async SSE endpoint"""
response = requests.get(f"{BASE_URL}/sse/async", stream=True, timeout=5)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if content.count("\n\n") >= 3:
break
# Parse data lines
data_lines = []
for line in content.split("\n"):
if line.startswith("data: "):
data_lines.append(line[6:])
assert len(data_lines) >= 3
for i in range(3):
assert f"Async message {i}" in data_lines
@pytest.mark.benchmark
def test_sse_single_message(session):
"""Test SSE endpoint that sends only one message"""
response = requests.get(f"{BASE_URL}/sse/single", stream=True, timeout=3)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
if "\n\n" in content: # Got at least one message
break
# Parse data lines
data_lines = []
for line in content.split("\n"):
if line.startswith("data: "):
data_lines.append(line[6:])
assert len(data_lines) == 1
assert data_lines[0] == "Single message"
@pytest.mark.benchmark
def test_sse_empty_stream(session):
"""Test SSE endpoint that sends no messages"""
response = requests.get(f"{BASE_URL}/sse/empty", stream=True, timeout=2)
response.raise_for_status()
content = ""
for chunk in response.iter_content(chunk_size=1024, decode_unicode=True):
if chunk:
content += chunk
# Don't wait forever for empty stream
break
# Parse data lines
data_lines = []
for line in content.split("\n"):
if line.startswith("data: "):
data_lines.append(line[6:])
assert len(data_lines) == 0
@pytest.mark.benchmark
def test_sse_custom_headers(session):
"""Test SSE endpoint with custom headers; SSE responses should not include default CORS headers for cross-origin EventSource support"""
response = requests.get(f"{BASE_URL}/sse/with_headers", stream=True)
assert response.status_code == 200
assert response.headers.get("X-Custom-Header") == "custom-value"
assert response.headers.get("Content-Type") == "text/event-stream"
# SSE responses should not include default CORS headers
assert response.headers.get("Access-Control-Allow-Origin") is None
assert response.headers.get("Access-Control-Allow-Headers") is None
@pytest.mark.benchmark
def test_sse_custom_status_code(session):
"""Test SSE endpoint with custom status code"""
response = requests.get(f"{BASE_URL}/sse/status_code", stream=True)
assert response.status_code == 201
assert response.headers.get("Content-Type") == "text/event-stream"
@pytest.mark.benchmark
def test_sse_middleware_compatibility(session):
"""Test that SSE endpoints work with global middleware"""
response = requests.get(f"{BASE_URL}/sse/basic", stream=True)
# Should have global response headers from middleware
assert response.headers.get("server") == "robyn"
def test_sse_message_formatter():
"""Test the SSEMessage formatter utility function"""
# Test basic message
result = SSEMessage("Hello world")
assert "data: Hello world\n\n" in result
# Test with event type
result = SSEMessage("Hello", event="greeting")
assert "event: greeting\n" in result
assert "data: Hello\n\n" in result
# Test with ID
result = SSEMessage("Hello", id="123")
assert "id: 123\n" in result
assert "data: Hello\n\n" in result
# Test with retry
result = SSEMessage("Hello", retry=5000)
assert "retry: 5000\n" in result
assert "data: Hello\n\n" in result
# Test with all parameters
result = SSEMessage("Hello", event="test", id="456", retry=3000)
assert "event: test\n" in result
assert "id: 456\n" in result
assert "retry: 3000\n" in result
assert "data: Hello\n\n" in result
# Test multiline data
result = SSEMessage("Line 1\nLine 2")
assert "data: Line 1\ndata: Line 2\n\n" in result
def test_sse_message_edge_cases():
"""Test SSEMessage with edge cases"""
# Test empty message
result = SSEMessage("")
assert result == "data: \n\n"
# Test None message (should handle gracefully)
try:
result = SSEMessage(None)
assert "data:" in result
except TypeError:
# This is acceptable behavior
pass
# Test message with special characters
special_message = "Hello\nWorld\r\nWith\tTabs"
result = SSEMessage(special_message)
assert "data: Hello\ndata: World\ndata: With\tTabs\n\n" in result
def test_sse_response_classes():
"""Test that SSE response classes can be imported correctly"""
assert SSEResponse is not None
assert SSEMessage is not None
assert StreamingResponse is not None
# Test basic functionality
def simple_generator():
yield "data: test\n\n"
response = SSEResponse(simple_generator())
assert response.media_type == "text/event-stream"
assert response.status_code == 200
def test_sse_error_handling():
"""Test error handling in SSE streams"""
# Test with a non-existent endpoint
response = requests.get(f"{BASE_URL}/sse/nonexistent", stream=True)
assert response.status_code == 404
def test_sse_http_methods():
"""Test that SSE endpoints only work with GET"""
# GET should work
response = requests.get(f"{BASE_URL}/sse/basic", stream=True)
assert response.status_code == 200
# POST should not work (404 or 405)
response = requests.post(f"{BASE_URL}/sse/basic")
assert response.status_code in [404, 405]
@pytest.mark.benchmark
def test_sse_streaming_sync_real_time(session):
"""Test that sync SSE streaming happens in real-time with timing delays"""
import time
start_time = time.time()
response = requests.get(f"{BASE_URL}/sse/streaming_sync", stream=True, timeout=10)
response.raise_for_status()
messages_received = 0
message_times = []
content = ""
for chunk in response.iter_content(chunk_size=1, decode_unicode=True):
if chunk:
content += chunk
# Check for complete messages
while "\n\n" in content:
message_end = content.find("\n\n") + 2
message = content[:message_end]
content = content[message_end:]
if "data:" in message:
messages_received += 1
message_times.append(time.time() - start_time)
if messages_received >= 3: # Got all 3 data messages
break
if messages_received >= 3:
break
# Verify we got 3 messages
assert messages_received == 3
# Verify timing: each message should arrive ~0.5s after the previous
# Allow some tolerance for processing time (±200ms)
for i in range(1, len(message_times)):
time_diff = message_times[i] - message_times[i - 1]
assert 0.3 <= time_diff <= 0.8, f"Message {i} arrived {time_diff:.2f}s after previous (expected ~0.5s)"
@pytest.mark.benchmark
def test_sse_streaming_async_real_time(session):
"""Test that async SSE streaming happens in real-time with timing delays"""
import time
start_time = time.time()
response = requests.get(f"{BASE_URL}/sse/streaming_async", stream=True, timeout=10)
response.raise_for_status()
messages_received = 0
message_times = []
content = ""
for chunk in response.iter_content(chunk_size=1, decode_unicode=True):
if chunk:
content += chunk
# Check for complete messages
while "\n\n" in content:
message_end = content.find("\n\n") + 2
message = content[:message_end]
content = content[message_end:]
if "data:" in message and "event: async" in message:
messages_received += 1
message_times.append(time.time() - start_time)
if messages_received >= 3: # Got all 3 data messages
break
if messages_received >= 3:
break
# Verify we got 3 messages
assert messages_received == 3
# Verify timing: each message should arrive ~0.3s after the previous
# Allow some tolerance for processing time (±150ms)
for i in range(1, len(message_times)):
time_diff = message_times[i] - message_times[i - 1]
assert 0.15 <= time_diff <= 0.5, f"Async message {i} arrived {time_diff:.2f}s after previous (expected ~0.3s)"
@pytest.mark.benchmark
def test_sse_optimization_headers(session):
"""Test that optimized SSE headers are present"""
response = requests.get(f"{BASE_URL}/sse/streaming_sync", stream=True)
assert response.status_code == 200
# Check for optimization headers
assert response.headers.get("Content-Type") == "text/event-stream"
# Accept either clean optimized headers or legacy compatibility
cache_control = response.headers.get("Cache-Control")
assert cache_control in ["no-cache, no-store, must-revalidate", "no-cache, no-cache, no-store, must-revalidate"]
assert response.headers.get("Pragma") == "no-cache"
assert response.headers.get("Expires") == "0"
assert response.headers.get("X-Accel-Buffering") == "no" # Nginx buffering disabled
# Connection header might be managed by underlying HTTP infrastructure
connection = response.headers.get("Connection")
assert connection is None or connection == "keep-alive"
def test_sse_message_optimization():
"""Test that SSEMessage formatting is optimized"""
import time
# Test single-line fast path
start_time = time.perf_counter()
for _ in range(1000):
result = SSEMessage("Simple message", id="123")
single_line_time = time.perf_counter() - start_time
# Test multi-line path
start_time = time.perf_counter()
for _ in range(1000):
result = SSEMessage("Line 1\nLine 2\nLine 3", id="123")
multi_line_time = time.perf_counter() - start_time
# Single-line should be faster (this is more of a performance regression test)
assert single_line_time < multi_line_time * 2, "Single-line SSEMessage optimization may have regressed"
# Verify correctness
result = SSEMessage("Test", event="test", id="1", retry=1000)
expected_parts = ["event: test\n", "id: 1\n", "retry: 1000\n", "data: Test\n", "\n"]
for part in expected_parts:
assert part in result
================================================
FILE: integration_tests/test_static_files_with_api_routes.py
================================================
"""
Test for issue #1251: Verify that API routes work correctly
when static files are served from the same base path.
This test ensures that non-GET/HEAD HTTP methods properly fall through
to API handlers when a static file service is mounted at the same route.
"""
import pytest
from integration_tests.helpers.http_methods_helpers import get, post
# Notes:
# 1. The /static route serves the integration_tests having files & directories.
@pytest.mark.benchmark
def test_post_api_route_with_root_static_files(session):
"""Test that POST requests reach API handlers (issue #1251).
This ensures that non-GET/HEAD methods are not blocked by static file serving.
"""
response = post("/static/build")
assert response.status_code == 200
assert response.text == f"{response.request.method}:{response.request.path_url} works"
@pytest.mark.benchmark
def test_static_file_still_served_correctly(session):
"""Verify that actual static files are still served correctly."""
response = get("/static/build/index.html", should_check_response=False)
assert response.status_code == 200
# Should serve the index.html file
assert "html" in response.text.lower()
================================================
FILE: integration_tests/test_status_code.py
================================================
import pytest
import requests
from integration_tests.helpers.http_methods_helpers import BASE_URL, get
@pytest.mark.benchmark
def test_404_status_code(session):
get("/404", expected_status_code=404)
@pytest.mark.benchmark
def test_404_not_found(session):
r = get("/real/404", expected_status_code=404)
assert r.text == "Not found"
@pytest.mark.benchmark
def test_202_status_code(session):
get("/202", expected_status_code=202)
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_sync_500_internal_server_error(function_type: str, session):
get(f"/{function_type}/raise", expected_status_code=500)
# ===== Content-Type on error responses =====
@pytest.mark.benchmark
def test_404_not_found_content_type(session):
"""A request to a non-existent route should return Content-Type: text/plain"""
r = get("/real/404", expected_status_code=404)
assert r.text == "Not found"
content_type = r.headers.get("Content-Type", "")
assert "text/plain" in content_type
@pytest.mark.benchmark
@pytest.mark.parametrize("function_type", ["sync", "async"])
def test_500_error_content_type(function_type: str, session):
"""An unhandled exception should return Content-Type: text/plain"""
r = get(f"/{function_type}/raise", expected_status_code=500)
content_type = r.headers.get("Content-Type", "")
assert "text/plain" in content_type
@pytest.mark.benchmark
def test_405_method_not_allowed_content_type(session):
"""An unsupported HTTP method should return 405 with Content-Type: text/plain"""
response = requests.request("NONSTANDARD", f"{BASE_URL}/")
assert response.status_code == 405
content_type = response.headers.get("Content-Type", "")
assert "text/plain" in content_type
================================================
FILE: integration_tests/test_subrouter.py
================================================
import pytest
from websocket import create_connection
from integration_tests.helpers.http_methods_helpers import generic_http_helper, head
@pytest.mark.parametrize(
"http_method_type",
["get", "post", "put", "delete", "patch", "options", "trace"],
)
@pytest.mark.benchmark
def test_sub_router(http_method_type, session):
response = generic_http_helper(http_method_type, "sub_router/foo")
assert response.json() == {"message": "foo"}
@pytest.mark.benchmark
def test_sub_router_head(session):
response = head("sub_router/foo")
assert response.text == "" # response body is expected to be empty
@pytest.mark.benchmark
def test_sub_router_web_socket(session):
BASE_URL = "ws://127.0.0.1:8080"
ws = create_connection(f"{BASE_URL}/sub_router/ws")
assert ws.recv() == "Hello world, from ws"
ws.send("My name is?")
assert ws.recv() == "Message"
================================================
FILE: integration_tests/test_web_sockets.py
================================================
import json
import pytest
from websocket import create_connection
BASE_URL = "ws://127.0.0.1:8080"
@pytest.mark.benchmark
def test_web_socket_raw_benchmark(session):
ws = create_connection(f"{BASE_URL}/web_socket?one=hi&two=hello")
assert ws.recv() == "Hello world, from ws"
ws.send("My name is?")
# Messages may arrive in any order due to WebSocket broadcast behavior
received = sorted([ws.recv() for _ in range(3)])
expected = sorted(["This is a broadcast message", "This is a message to self", "Whaaat??"])
assert received == expected
ws.send("My name is?")
assert ws.recv() == "Whooo??"
ws.send("My name is?")
received = sorted([ws.recv() for _ in range(3)])
expected = sorted(["hi", "hello", "*chika* *chika* Slim Shady."])
assert received == expected
# this will close the connection
ws.send("test")
assert ws.recv() == "Connection closed"
def test_web_socket_json(session):
"""
Not using this as the benchmark test since this involves JSON marshalling/unmarshalling
which pollutes the benchmark measurement.
"""
ws = create_connection(f"{BASE_URL}/web_socket_json")
assert ws.recv() == "Hello world, from ws"
msg = "My name is?"
ws.send(msg)
resp = json.loads(ws.recv())
assert resp["resp"] == "Whaaat??"
assert resp["msg"] == msg
ws.send(msg)
resp = json.loads(ws.recv())
assert resp["resp"] == "Whooo??"
assert resp["msg"] == msg
ws.send(msg)
resp = json.loads(ws.recv())
assert resp["resp"] == "*chika* *chika* Slim Shady."
assert resp["msg"] == msg
def test_websocket_di(session):
"""Test dependency injection in WebSocket connect and handler phases."""
ws = create_connection(f"{BASE_URL}/web_socket_di")
# 1. on_connect should receive both global and router dependencies
assert ws.recv() == "connect: GLOBAL DEPENDENCY ROUTER DEPENDENCY"
# 2. Main handler should also receive both dependencies when processing messages
ws.send("test")
assert ws.recv() == "handler: GLOBAL DEPENDENCY ROUTER DEPENDENCY"
# Send another message to confirm DI is stable across multiple messages
ws.send("test again")
assert ws.recv() == "handler: GLOBAL DEPENDENCY ROUTER DEPENDENCY"
ws.close()
def test_websocket_large_payload(session):
"""Test that WebSocket can handle messages larger than the default 64KB frame size (#1269)"""
ws = create_connection(f"{BASE_URL}/web_socket_echo")
# Consume the empty connect message
ws.recv()
large_message = "A" * (128 * 1024) # 128KB, well above the old 64KB default
ws.send(large_message)
response = ws.recv()
assert response == large_message
assert len(response) == 128 * 1024
ws.close()
def test_websocket_empty_returns(session):
"""Test that WebSocket handlers can return nothing without causing errors"""
ws = create_connection(f"{BASE_URL}/web_socket_empty_returns")
# Connect handler returns None - no message should be received on connection
# We need to send a message to verify the connection is still active
ws.send("test message")
# Message handler returns None - no response should be sent
# The socket should still be open, not crashed
# We can verify this by closing the connection gracefully
ws.close()
# If we got here without exceptions, the test passed
================================================
FILE: llms.txt
================================================
# Robyn
> Robyn is a high-performance, community-driven, and innovator-friendly async web framework for Python with a Rust runtime. It combines Python's ease of use with Rust's performance.
## Quick Facts
- Version: 0.79.0
- Python: >= 3.10
- License: BSD 2.0
- Repository: https://github.com/sparckles/robyn
- Documentation: https://robyn.tech/documentation
- Discord: https://discord.gg/rkERZ5eNU8
## Installation
```bash
pip install robyn
```
## Basic Usage
```python
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
async def index(request):
return "Hello, World!"
app.start(port=8080)
```
## Key Features
- **Rust Runtime**: Core server written in Rust using actix-web for high performance
- **Async/Sync Support**: Both async and sync route handlers supported
- **Multi-Process Scaling**: Built-in multiprocess execution via `--processes` and `--workers`
- **WebSockets**: Native WebSocket support
- **Middlewares**: Before/after request middlewares
- **Dependency Injection**: Built-in DI system
- **OpenAPI/Swagger**: Automatic OpenAPI documentation generation
- **Hot Reloading**: Development mode with `--dev` flag
- **AI Agents**: Built-in AI agent routing via `robyn.ai`
- **MCP Support**: Model Context Protocol server capabilities via `app.mcp`
- **Templating**: Jinja2 templating support (optional)
- **CORS**: Built-in CORS helper via `ALLOW_CORS()`
- **Authentication**: AuthenticationHandler base class for custom auth
- **Static Files**: Directory serving via `app.serve_directory()`
- **SSE**: Server-Sent Events support via `SSEResponse`
- **Easy Access Parameters**: Typed path/query params with automatic coercion in handler signatures
- **Direct Rust Integration**: Embed Rust code directly in routes
## Project Structure
```
robyn/
├── src/ # Rust source code
│ ├── lib.rs # PyO3 module entry point
│ ├── server.rs # Main HTTP server implementation
│ ├── types/ # Request, Response, Headers, Cookie types
│ ├── routers/ # HTTP, WebSocket, middleware routers
│ ├── executors/ # Route execution handlers
│ └── websockets/ # WebSocket implementation
├── robyn/ # Python package
│ ├── __init__.py # Main Robyn and SubRouter classes
│ ├── router.py # Python router implementation
│ ├── authentication.py # AuthenticationHandler
│ ├── dependency_injection.py
│ ├── openapi.py # OpenAPI generation
│ ├── mcp.py # MCP protocol support
│ ├── ai.py # AI agent support
│ ├── responses.py # Response helpers (serve_file, html, SSE)
│ ├── ws.py # WebSocket class
│ └── robyn.pyi # Type stubs
├── integration_tests/ # Integration test suite
├── unit_tests/ # Unit test suite
├── docs_src/ # Documentation (Next.js)
├── granian/ # Bundled Granian server (fork)
└── examples/ # Example applications
```
## Core Classes
### Robyn / SubRouter
Main application class and sub-router for modular routes.
```python
from robyn import Robyn, SubRouter
app = Robyn(__file__)
api = SubRouter(__file__, prefix="/api")
@api.get("/users")
def get_users(request):
return {"users": []}
app.include_router(api)
```
### Request Object
```python
request.method # HTTP method
request.url # Url object (scheme, host, path)
request.headers # Headers dict-like
request.query_params # QueryParams
request.path_params # Dict of URL params
request.body # Raw bytes
request.json() # Parse JSON body
request.form_data # Multipart form data
request.ip_addr # Client IP
request.identity # Identity (if authenticated)
```
### Response Object
```python
from robyn import Response
Response(
status_code=200,
headers={"Content-Type": "application/json"},
description="body content" # or body bytes
)
```
### Decorators
```python
@app.get("/path")
@app.post("/path")
@app.put("/path")
@app.delete("/path")
@app.patch("/path")
@app.head("/path")
@app.options("/path")
@app.before_request("/path") # Middleware before
@app.after_request("/path") # Middleware after
@app.startup_handler # Server startup
@app.shutdown_handler # Server shutdown
```
### WebSockets
```python
from robyn import WebSocketDisconnect
@app.websocket("/ws")
async def handler(websocket):
try:
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"Echo: {msg}")
except WebSocketDisconnect:
pass
@handler.on_connect
def on_connect(websocket):
return "Connected"
@handler.on_close
def on_close(websocket):
return "Closed"
```
### Easy Access Parameters
Declare typed path and query parameters directly in handler signatures. Works for both HTTP and WebSocket handlers.
```python
from typing import List, Optional
# HTTP: path params + query params with type coercion
@app.get("/items/:id")
async def get_item(id: int, q: str, page: int = 1):
return {"id": id, "q": q, "page": page}
# Optional, List, and bool params
@app.get("/search")
def search(name: str, tags: List[str], active: bool = False, age: Optional[int] = None):
return {"name": name, "tags": tags, "active": active, "age": age}
# WebSocket: typed query params on handler and callbacks
@app.websocket("/ws")
async def handler(websocket, room: str = "default", page: int = 1):
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"room={room} page={page} msg={msg}")
@handler.on_connect
def on_connect(websocket, room: str = "default"):
return f"connected to {room}"
```
### MCP (Model Context Protocol)
```python
@app.mcp.resource("time://current")
def get_time():
return datetime.now().isoformat()
@app.mcp.tool(name="calc", description="Calculate", input_schema={...})
def calculate(args):
return eval(args["expression"])
@app.mcp.prompt(name="explain", description="Explain code", arguments=[...])
def explain_prompt(args):
return f"Please explain: {args['code']}"
```
## CLI Commands
```bash
python app.py # Start server
python app.py --dev # Development mode (hot reload)
python app.py --processes 4 # Multi-process
python app.py --workers 2 # Workers per process
python app.py --log-level DEBUG # Log level
python app.py --open-browser # Open browser on start
python app.py --create # Create new project scaffold
python app.py --docs # Open documentation
```
## Development Setup
```bash
# Clone
git clone https://github.com/sparckles/robyn.git
cd robyn
# Virtual environment
python3 -m venv .venv && source .venv/bin/activate
# Install tools
pip install pre-commit poetry maturin
# Install dependencies
poetry install --with dev --with test
# Build Rust extension
maturin develop
# Run tests
pytest
```
## Key Dependencies
- **PyO3**: Rust-Python bindings
- **actix-web**: Rust HTTP server (via cookie crate)
- **orjson**: Fast JSON serialization
- **multiprocess**: Multi-process support
- **uvloop**: Fast event loop (non-Windows)
- **watchdog**: File watching for hot reload
## Configuration
Environment variables:
- `ROBYN_HOST`: Server host (default: 127.0.0.1)
- `ROBYN_PORT`: Server port (default: 8080)
- `ROBYN_DEV_MODE`: Enable dev mode
- `ROBYN_BROWSER_OPEN`: Open browser on start
- `ROBYN_CLIENT_TIMEOUT`: Client timeout seconds
- `ROBYN_KEEP_ALIVE_TIMEOUT`: Keep-alive timeout
## Documentation Structure
Main docs at `docs_src/src/pages/documentation/`:
- `api_reference/getting_started.mdx` - Quick start guide
- `api_reference/request_object.mdx` - Request handling
- `api_reference/middlewares.mdx` - Middleware usage
- `api_reference/websockets.mdx` - WebSocket guide
- `api_reference/authentication.mdx` - Auth patterns
- `api_reference/openapi.mdx` - OpenAPI docs
- `api_reference/agents.mdx` - AI agent integration
- `api_reference/mcps.mdx` - MCP server guide
- `example_app/` - Full example application tutorial
================================================
FILE: noxfile.py
================================================
import sys
import nox
@nox.session(python=["3.10", "3.11", "3.12", "3.13", "3.14"])
def tests(session):
session.run("pip", "install", "poetry==1.3.0")
session.run("pip", "install", "maturin")
session.run(
"poetry",
"export",
"--with",
"test",
"--with",
"dev",
"--without-hashes",
"--output",
"requirements.txt",
)
session.run("pip", "install", "-r", "requirements.txt")
session.run("pip", "install", "-e", ".")
args = [
"maturin",
"build",
"-i",
"python",
"--out",
"dist",
]
if sys.platform == "darwin":
session.run("rustup", "target", "add", "x86_64-apple-darwin")
session.run("rustup", "target", "add", "aarch64-apple-darwin")
args.append("--target")
args.append("universal2-apple-darwin")
session.run(*args)
session.run("pip", "install", "--no-index", "--find-links=dist/", "robyn")
session.run("pytest")
@nox.session(python=["3.11"])
def lint(session):
session.run("pip", "install", "black", "ruff")
session.run("black", "robyn/", "integration_tests/")
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "robyn"
version = "0.82.0"
description = "A Super Fast Async Python Web Framework with a Rust runtime."
readme = "README.md"
authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Topic :: Internet :: WWW/HTTP",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = [
"inquirerpy == 0.3.4",
"multiprocess >= 0.70.18, < 0.71.0",
"orjson >= 3.11.5, < 4.0.0",
"rustimport == 1.3.4",
# conditional
"uvloop~=0.22.1; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'",
"watchdog >= 6.0.0, < 7.0.0",
]
[project.optional-dependencies]
"templating" = ["jinja2 >= 3.1.6, < 4.0.0"]
"pydantic" = ["pydantic >= 2.0.0, < 3.0.0"]
"all" = ["jinja2 >= 3.1.6, < 4.0.0", "pydantic >= 2.0.0, < 3.0.0"]
[project.urls]
Documentation = "https://robyn.tech/"
Repository = "https://github.com/sparckles/robyn"
Issues = "https://github.com/sparckles/robyn/issues"
Changelog = "https://github.com/sparckles/robyn/blob/main/CHANGELOG.md"
[project.scripts]
robyn = "robyn.cli:run"
test_server = "integration_tests.base_routes:main"
[dependency-groups]
dev = [
"black==23.1",
"commitizen==2.40",
"isort==5.11.5",
"maturin==1.7.4",
"pre-commit>=4.5.1,<5.0.0",
"ruff>=0.9.0",
]
test = [
"nox==2023.4.22",
"pytest>=9.0.2",
"pytest-codspeed>=4.2.0",
"requests==2.28.2",
"websocket-client==1.5.0",
]
[tool.poetry]
name = "robyn"
version = "0.82.0"
description = "A Super Fast Async Python Web Framework with a Rust runtime."
authors = ["Sanskar Jethi "]
[tool.poetry.dependencies]
python = "^3.10"
inquirerpy = "0.3.4"
maturin = "1.7.4"
watchdog = "^6.0.0"
multiprocess = "^0.70.18"
uvloop = { version = "0.22.1", markers = "sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')" }
jinja2 = { version = "^3.1.6", optional = true }
pydantic = { version = "^2.0.0", optional = true }
rustimport = "^1.3.4"
orjson = "^3.11.5"
[tool.poetry.extras]
templating = ["jinja2"]
pydantic = ["pydantic"]
all = ["jinja2", "pydantic"]
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
ruff = ">=0.9.0"
black = "23.1"
isort = "5.11.5"
pre-commit = "^4.5.1"
commitizen = "2.40"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
pytest = "^9.0.2"
pytest-codspeed = "^4.2.0"
requests = "2.28.2"
nox = "2023.4.22"
websocket-client = "1.5.0"
[tool.poetry.scripts]
test_server = { callable = "integration_tests.base_routes:main" }
[tool.ruff]
line-length = 160
exclude = ["src/*", ".git", "docs"]
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.isort]
profile = "black"
line_length = 160
[tool.black]
line-length = 160
target-version = ['py39']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.pytest.ini_options]
markers = [
"benchmark: marks tests as benchmarks for performance measurement (deselect with '-m \"not benchmark\"')",
]
[tool.maturin]
module-name = "robyn"
================================================
FILE: robyn/__init__.py
================================================
import inspect
import logging
import os
import socket
from abc import ABC
from pathlib import Path
from typing import Callable, List, Optional, Union
import multiprocess as mp # type: ignore
from robyn import status_codes
from robyn.argument_parser import Config
from robyn.authentication import AuthenticationHandler
from robyn.dependency_injection import DependencyMap
from robyn.env_populator import load_vars
from robyn.events import Events
from robyn.jsonify import jsonify
from robyn.logger import Colors, logger
from robyn.mcp import MCPApp
from robyn.openapi import OpenAPI
from robyn.processpool import run_processes
from robyn.reloader import compile_rust_files
from robyn.responses import SSEMessage, SSEResponse, StreamingResponse, html, serve_file, serve_html
from robyn.robyn import FunctionInfo, Headers, HttpMethod, Request, Response, WebSocketConnector, get_version
from robyn.router import MiddlewareRouter, MiddlewareType, Router, WebSocketRouter
from robyn.types import Directory, JsonBody
from robyn.ws import WebSocketAdapter, WebSocketDisconnect, create_websocket_decorator
__version__ = get_version()
def _normalize_endpoint(endpoint: Optional[str], treat_empty_as_root: bool = False) -> Optional[str]:
"""
Normalize an endpoint to ensure consistent routing.
Rules:
- Root "/" remains unchanged
- All other endpoints get leading slash added if missing
- Trailing slashes are removed from all endpoints except root
- Empty or blank strings are handled based on treat_empty_as_root flag
- treat_empty_as_root is used for prefixes where empty/blank strings are valid
Args:
endpoint: The endpoint path to normalize.
treat_empty_as_root (used for prefixes):
If True, empty/blank strings are converted to "/" (root).
If False, empty/blank strings return None (invalid endpoint).
Returns:
Normalized endpoint path or None if invalid.
"""
if endpoint is None or (not endpoint and not treat_empty_as_root):
return None
# Remove trailing slashes
endpoint = endpoint.strip().rstrip("/")
# Handle empty result
if not endpoint:
return "/"
# Add leading slash if missing
if not endpoint.startswith("/"):
endpoint = "/" + endpoint
return endpoint
config = Config()
if (compile_path := config.compile_rust_path) is not None:
compile_rust_files(compile_path)
print("Compiled rust files")
class BaseRobyn(ABC):
"""This is the python wrapper for the Robyn binaries."""
def __init__(
self,
file_object: str,
config: Config = Config(),
openapi_file_path: Optional[str] = None,
openapi: Optional[OpenAPI] = None,
dependencies: DependencyMap = DependencyMap(),
) -> None:
directory_path = os.path.dirname(os.path.abspath(file_object))
self.file_path = file_object
self.directory_path = directory_path
self.config = config
self.dependencies = dependencies
self.openapi = openapi
self.init_openapi(openapi_file_path)
if not bool(os.environ.get("ROBYN_CLI", False)):
# the env variables are already set when are running through the cli
load_vars(project_root=directory_path)
self._handle_dev_mode()
logging.basicConfig(level=self.config.log_level)
if self.config.log_level.lower() != "warn":
logger.info(
"SERVER IS RUNNING IN VERBOSE/DEBUG MODE. Set --log-level to WARN to run in production mode.",
color=Colors.BLUE,
)
self.router = Router()
self.middleware_router = MiddlewareRouter()
self.web_socket_router = WebSocketRouter()
self.request_headers: Headers = Headers({})
self.response_headers: Headers = Headers({})
self.excluded_response_headers_paths: Optional[List[str]] = None
self.directories: List[Directory] = []
self.event_handlers: dict = {}
self.exception_handler: Optional[Callable] = None
self.authentication_handler: Optional[AuthenticationHandler] = None
self.included_routers: List[Router] = []
self._mcp_app: Optional[MCPApp] = None
def init_openapi(self, openapi_file_path: Optional[str]) -> None:
if self.config.disable_openapi:
return
if self.openapi is None:
self.openapi = OpenAPI()
if openapi_file_path:
self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
elif Path(self.directory_path).joinpath("openapi.json").exists():
self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
else:
logger.debug("No OpenAPI spec file found; using auto-generated documentation only.", color=Colors.YELLOW)
def _handle_dev_mode(self):
cli_dev_mode = self.config.dev # --dev
env_dev_mode = os.getenv("ROBYN_DEV_MODE", "False").lower() == "true" # ROBYN_DEV_MODE=True
is_robyn = os.getenv("ROBYN_CLI", False)
if cli_dev_mode and not is_robyn:
raise SystemExit("Dev mode is not supported in the python wrapper. Please use the Robyn CLI. e.g. python3 -m robyn app.py --dev")
if env_dev_mode and not is_robyn:
logger.error("Ignoring ROBYN_DEV_MODE environment variable. Dev mode is not supported in the python wrapper.")
raise SystemExit("Dev mode is not supported in the python wrapper. Please use the Robyn CLI. e.g. python3 -m robyn app.py")
def add_route(
self,
route_type: Union[HttpMethod, str],
endpoint: str,
handler: Callable,
is_const: bool = False,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: Union[List[str], None] = None,
):
"""
Connect a URI to a handler
:param route_type str: route type between GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/TRACE
:param endpoint str: endpoint for the route added
:param handler function: represents the sync or async function passed as a handler for the route
:param is_const bool: represents if the handler is a const function or not
:param auth_required bool: represents if the route needs authentication or not
"""
""" We will add the status code here only
"""
injected_dependencies = self.dependencies.get_dependency_map(self)
list_openapi_tags: List[str] = openapi_tags if openapi_tags else []
if isinstance(route_type, str):
http_methods = {
"GET": HttpMethod.GET,
"POST": HttpMethod.POST,
"PUT": HttpMethod.PUT,
"DELETE": HttpMethod.DELETE,
"PATCH": HttpMethod.PATCH,
"HEAD": HttpMethod.HEAD,
"OPTIONS": HttpMethod.OPTIONS,
}
route_type = http_methods[route_type]
# Normalize endpoint before adding
normalized_endpoint = _normalize_endpoint(endpoint)
if normalized_endpoint is None:
raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint")
if auth_required:
self.middleware_router.add_auth_middleware(normalized_endpoint, route_type)(handler)
# Check if this exact route (method + normalized_endpoint) already exists
route_key = f"{route_type}:{normalized_endpoint}"
if not hasattr(self, "_added_routes"):
self._added_routes = set()
if route_key in self._added_routes:
# Route already exists, raise an error
raise ValueError(f"Route {route_type} {normalized_endpoint} already exists")
# Add to our tracking set
self._added_routes.add(route_key)
add_route_response = self.router.add_route(
route_type=route_type,
endpoint=normalized_endpoint,
handler=handler,
is_const=is_const,
auth_required=auth_required,
openapi_name=openapi_name,
openapi_tags=list_openapi_tags,
exception_handler=self.exception_handler,
injected_dependencies=injected_dependencies,
)
logger.info("Added route %s %s", route_type, normalized_endpoint)
return add_route_response
def inject(self, **kwargs):
"""
Injects the dependencies for the route
:param kwargs dict: the dependencies to be injected
"""
self.dependencies.add_router_dependency(self, **kwargs)
def inject_global(self, **kwargs):
"""
Injects the dependencies for the global routes
Ideally, this function should be a global function
:param kwargs dict: the dependencies to be injected
"""
self.dependencies.add_global_dependency(**kwargs)
def before_request(self, endpoint: Optional[str] = None) -> Callable[..., None]:
"""
You can use the @app.before_request decorator to call a method before routing to the specified endpoint
:param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes.
"""
return self.middleware_router.add_middleware(MiddlewareType.BEFORE_REQUEST, _normalize_endpoint(endpoint))
def after_request(self, endpoint: Optional[str] = None) -> Callable[..., None]:
"""
You can use the @app.after_request decorator to call a method after routing to the specified endpoint
:param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes.
"""
return self.middleware_router.add_middleware(MiddlewareType.AFTER_REQUEST, _normalize_endpoint(endpoint))
def serve_directory(
self,
route: str,
directory_path: str,
index_file: Optional[str] = None,
show_files_listing: bool = False,
):
"""
Serves a directory at the given route
:param route str: the route at which the directory is to be served
:param directory_path str: the path of the directory to be served
:param index_file str|None: the index file to be served
:param show_files_listing bool: if the files listing should be shown or not
"""
self.directories.append(Directory(route, directory_path, show_files_listing, index_file))
def add_request_header(self, key: str, value: str) -> None:
self.request_headers.append(key, value)
def add_response_header(self, key: str, value: str) -> None:
self.response_headers.append(key, value)
def set_request_header(self, key: str, value: str) -> None:
self.request_headers.set(key, value)
def set_response_header(self, key: str, value: str) -> None:
self.response_headers.set(key, value)
def exclude_response_headers_for(self, excluded_response_headers_paths: Optional[List[str]]):
"""
To exclude response headers from certain routes
@param exclude_paths: the paths to exclude response headers from
"""
self.excluded_response_headers_paths = excluded_response_headers_paths
def add_web_socket(self, endpoint: str, handlers) -> None:
self.web_socket_router.add_route(endpoint, handlers)
def websocket(self, endpoint: str):
"""
Modern WebSocket decorator backed by Rust channels.
Usage:
@app.websocket("/ws")
async def handler(websocket):
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"Echo: {msg}")
@handler.on_connect
def on_connect(websocket):
return "Welcome!"
@handler.on_close
def on_close(websocket):
return "Goodbye"
"""
return create_websocket_decorator(self)(endpoint)
def _add_event_handler(self, event_type: Events, handler: Callable) -> None:
logger.info("Added event %s handler", event_type)
if event_type not in {Events.STARTUP, Events.SHUTDOWN}:
return
is_async = inspect.iscoroutinefunction(handler)
self.event_handlers[event_type] = FunctionInfo(handler, is_async, 0, {}, {})
def startup_handler(self, handler: Callable) -> None:
self._add_event_handler(Events.STARTUP, handler)
def shutdown_handler(self, handler: Callable) -> None:
self._add_event_handler(Events.SHUTDOWN, handler)
def is_port_in_use(self, port: int) -> bool:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(("localhost", port)) == 0
except Exception:
raise Exception(f"Invalid port number: {port}")
def _add_openapi_routes(self, auth_required: bool = False):
if self.config.disable_openapi:
return
if self.openapi is None:
logger.error("No openAPI")
return
self.router.prepare_routes_openapi(self.openapi, self.included_routers)
self.add_route(
route_type=HttpMethod.GET,
endpoint="/openapi.json",
handler=self.openapi.get_openapi_config,
is_const=True,
auth_required=auth_required,
)
self.add_route(
route_type=HttpMethod.GET,
endpoint="/docs",
handler=self.openapi.get_openapi_docs_page,
is_const=True,
auth_required=auth_required,
)
self.exclude_response_headers_for(["/docs", "/openapi.json"])
def exception(self, exception_handler: Callable):
self.exception_handler = exception_handler
def get(
self,
endpoint: str,
const: bool = False,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["get"],
):
"""
The @app.get decorator to add a route with the GET method
:param endpoint str: endpoint for the route added
:param const bool: represents if the handler is a const function or not
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required, openapi_name, openapi_tags)
return inner
def post(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["post"],
):
"""
The @app.post decorator to add a route with POST method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def put(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["put"],
):
"""
The @app.put decorator to add a get route with PUT method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def delete(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["delete"],
):
"""
The @app.delete decorator to add a route with DELETE method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def patch(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["patch"],
):
"""
The @app.patch decorator to add a route with PATCH method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def head(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["head"],
):
"""
The @app.head decorator to add a route with HEAD method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def options(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["options"],
):
"""
The @app.options decorator to add a route with OPTIONS method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def connect(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["connect"],
):
"""
The @app.connect decorator to add a route with CONNECT method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def trace(
self,
endpoint: str,
auth_required: bool = False,
openapi_name: str = "",
openapi_tags: List[str] = ["trace"],
):
"""
The @app.trace decorator to add a route with TRACE method
:param endpoint str: endpoint for the route added
:param auth_required bool: represents if the route needs authentication or not
:param openapi_name: str -- the name of the endpoint in the openapi spec
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
"""
def inner(handler):
return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
return inner
def include_router(self, router: "SubRouter"):
"""
The method to include the routes from another router.
Merge another SubRouter's routes, middlewares, websocket routes, and dependencies into this router.
Note: This operation mutates the current router's internal collections (route list, middleware lists,
websocket routes, and dependencies) and does not deep-copy the included router. Callers should ensure
there are no path or name conflicts before including a router.
:param router SubRouter: the router object to include the routes from
"""
self.included_routers.append(router)
self.router.routes.extend(router.router.routes)
self.middleware_router.global_middlewares.extend(router.middleware_router.global_middlewares)
self.middleware_router.route_middlewares.extend(router.middleware_router.route_middlewares)
if not self.config.disable_openapi and self.openapi is not None:
self.openapi.add_subrouter_paths(self.openapi)
# extend the websocket routes
prefix = _normalize_endpoint(router.prefix, treat_empty_as_root=True)
if prefix == "/":
prefix = ""
for route, handlers in router.web_socket_router.routes.items():
normalized_route = _normalize_endpoint(route)
new_endpoint = f"{prefix}{normalized_route}"
self.web_socket_router.routes[new_endpoint] = handlers
self.dependencies.merge_dependencies(router)
def configure_authentication(self, authentication_handler: AuthenticationHandler):
"""
Configures the authentication handler for the application.
:param authentication_handler: the instance of a class inheriting the AuthenticationHandler base class
"""
self.authentication_handler = authentication_handler
self.middleware_router.set_authentication_handler(authentication_handler)
@property
def mcp(self):
"""
Get the MCP (Model Context Protocol) interface for this app.
Enables registering MCP resources, tools, and prompts that can be accessed
by MCP clients like Claude Desktop or other AI applications.
Returns:
MCPApp: MCP interface for registering handlers
Example:
@app.mcp.resource("file://documents", "Documents", "Access to document files")
def get_documents(params):
return "Document content here"
@app.mcp.tool("calculate", "Perform calculations", {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression to evaluate"}
},
"required": ["expression"]
})
def calculate_tool(args):
return eval(args["expression"])
"""
if self._mcp_app is None:
self._mcp_app = MCPApp(self)
return self._mcp_app
class Robyn(BaseRobyn):
def start(self, host: str = "127.0.0.1", port: int = 8080, _check_port: bool = True, client_timeout: int = 30, keep_alive_timeout: int = 20):
"""
Starts the server
:param host str: represents the host at which the server is listening
:param port int: represents the port number at which the server is listening
:param _check_port bool: represents if the port should be checked if it is already in use
:param client_timeout int: timeout for client connections in seconds (default: 30)
:param keep_alive_timeout int: timeout for keep-alive connections in seconds (default: 20)
"""
host = os.getenv("ROBYN_HOST", host)
port = int(os.getenv("ROBYN_PORT", port))
client_timeout = int(os.getenv("ROBYN_CLIENT_TIMEOUT", client_timeout))
keep_alive_timeout = int(os.getenv("ROBYN_KEEP_ALIVE_TIMEOUT", keep_alive_timeout))
open_browser = bool(os.getenv("ROBYN_BROWSER_OPEN", self.config.open_browser))
if _check_port:
while self.is_port_in_use(port):
logger.error("Port %s is already in use. Please use a different port.", port)
try:
port = int(input("Enter a different port: "))
except Exception:
logger.error("Invalid port number. Please enter a valid port number.")
continue
if not self.config.disable_openapi:
self._add_openapi_routes()
logger.info("Docs hosted at http://%s:%s/docs", host, port)
logger.info("Robyn version: %s", __version__)
logger.info("Starting server at http://%s:%s", host, port)
mp.allow_connection_pickling()
run_processes(
host,
port,
self.directories,
self.request_headers,
self.router.get_routes(),
self.middleware_router.get_global_middlewares(),
self.middleware_router.get_route_middlewares(),
self.web_socket_router.get_routes(),
self.event_handlers,
self.config.workers,
self.config.processes,
self.response_headers,
self.excluded_response_headers_paths,
open_browser,
client_timeout,
keep_alive_timeout,
)
class SubRouter(BaseRobyn):
def __init__(self, file_object: str, prefix: str = "", config: Config = Config(), openapi: OpenAPI = OpenAPI()) -> None:
super().__init__(file_object=file_object, config=config, openapi=openapi)
self.prefix = prefix
def __add_prefix(self, endpoint: str):
# Normalize prefix, treating empty as empty (not root)
normalized_prefix = _normalize_endpoint(self.prefix, treat_empty_as_root=True)
# Handle empty endpoint - should just be the prefix
if endpoint in ("", "/"):
return normalized_prefix if normalized_prefix else "/"
# Convert root prefix to empty to avoid double slashes when making endpoint
if normalized_prefix == "/":
normalized_prefix = "" # Empty prefix for root
# Normalize and validate endpoint
normalized_endpoint = _normalize_endpoint(endpoint)
if normalized_endpoint is None:
raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint")
return f"{normalized_prefix}{normalized_endpoint}"
def get(self, endpoint: str, const: bool = False, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["get"]):
return super().get(endpoint=self.__add_prefix(endpoint), const=const, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def post(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["post"]):
return super().post(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def put(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["put"]):
return super().put(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def delete(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["delete"]):
return super().delete(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def patch(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["patch"]):
return super().patch(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def head(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["head"]):
return super().head(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def trace(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["trace"]):
return super().trace(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def options(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["options"]):
return super().options(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
def websocket(self, endpoint: str):
"""
Modern WebSocket decorator for SubRouter with prefix support.
"""
return create_websocket_decorator(self)(endpoint)
def ALLOW_CORS(app: Robyn, origins: Union[List[str], str], headers: Union[List[str], str] = None):
"""
Configure CORS headers for the application.
Args:
app: Robyn application instance
origins: List of allowed origins or "*" for all origins
headers: List of allowed headers or "*" for all headers
"""
# Handle string input for origins
if isinstance(origins, str):
origins = [origins]
default_headers = ["Content-Type", "Authorization"]
if isinstance(headers, list):
headers = list(set(default_headers + headers))
headers = ", ".join(headers)
@app.before_request()
def cors_middleware(request):
origin = request.headers.get("Origin")
# If specific origins are set, validate the request origin
if origin and "*" not in origins and origin not in origins:
return Response(status_code=403, description="", headers={})
# Handle preflight requests
if request.method == "OPTIONS":
return Response(
status_code=204,
headers={
"Access-Control-Allow-Origin": origin if origin else (origins[0] if origins else "*"),
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS",
"Access-Control-Allow-Headers": str(headers) if headers else "Content-Type, Authorization",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "3600",
},
description="",
)
return request
# Set default CORS headers for all responses
if len(origins) == 1:
app.set_response_header("Access-Control-Allow-Origin", origins[0])
else:
# For multiple origins, we'll handle it dynamically in the response
app.set_response_header("Access-Control-Allow-Origin", "*")
app.set_response_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS")
app.set_response_header("Access-Control-Allow-Headers", str(headers) if headers else "Content-Type, Authorization")
app.set_response_header("Access-Control-Allow-Credentials", "true")
__all__ = [
"Robyn",
"Request",
"Response",
"status_codes",
"jsonify",
"serve_file",
"serve_html",
"html",
"StreamingResponse",
"SSEResponse",
"SSEMessage",
"ALLOW_CORS",
"SubRouter",
"AuthenticationHandler",
"Headers",
"WebSocketConnector",
"WebSocketAdapter",
"WebSocketDisconnect",
"JsonBody",
"MCPApp",
]
================================================
FILE: robyn/__main__.py
================================================
from robyn.cli import run
if __name__ == "__main__":
run()
================================================
FILE: robyn/_param_utils.py
================================================
"""
Shared utilities for resolving individual query/path parameters
from handler function signatures, with type coercion.
Used by both robyn/router.py (HTTP handlers) and robyn/ws.py (WebSocket handlers).
"""
import inspect
import logging
from typing import Any, Dict, Optional, Set, Tuple, Union
_logger = logging.getLogger(__name__)
_MISSING = object()
# Values that map to True/False from query string bool params
_BOOL_TRUE_STRINGS = frozenset({"true", "1", "yes", "on"})
_BOOL_FALSE_STRINGS = frozenset({"false", "0", "no", "off", ""})
class QueryParamValidationError(Exception):
"""Raised when a query or path parameter cannot be coerced to the expected type,
or when a required parameter is missing."""
def __init__(self, param_name: str, value: Optional[str], expected_type: type, message: Optional[str] = None):
self.param_name = param_name
self.value = value
self.expected_type = expected_type
if message:
self.detail = message
elif value is None:
self.detail = f"Missing required parameter: '{param_name}'"
else:
self.detail = f"Invalid value '{value}' for parameter '{param_name}': expected {expected_type.__name__}"
super().__init__(self.detail)
def unwrap_optional(annotation) -> Tuple[Any, bool]:
"""
If annotation is Optional[T] (i.e. Union[T, None]), return (T, True).
Otherwise return (annotation, False).
"""
origin = getattr(annotation, "__origin__", None)
if origin is Union:
args = annotation.__args__
non_none_args = [a for a in args if a is not type(None)]
if len(non_none_args) == 1 and type(None) in args:
return non_none_args[0], True
return annotation, False
def is_list_type(annotation) -> bool:
"""Check if annotation is List[T] or list[T]."""
origin = getattr(annotation, "__origin__", None)
return origin is list
def get_list_element_type(annotation) -> type:
"""Get the element type from List[T]. Defaults to str if not specified."""
args = getattr(annotation, "__args__", None)
if args and len(args) > 0:
return args[0]
return str
def coerce_value(value: str, target_type: type, param_name: str):
"""
Convert a string value to the target type.
Raises QueryParamValidationError on failure.
"""
if target_type is str or target_type is inspect.Parameter.empty:
return value
try:
if target_type is int:
return int(value)
if target_type is float:
return float(value)
if target_type is bool:
lower = value.lower()
if lower in _BOOL_TRUE_STRINGS:
return True
if lower in _BOOL_FALSE_STRINGS:
return False
raise ValueError(f"Cannot interpret '{value}' as bool")
# Fallback: try calling the type constructor (covers Enum, UUID, etc.)
return target_type(value)
except (ValueError, TypeError) as e:
raise QueryParamValidationError(param_name, value, target_type) from e
def resolve_individual_params(
unresolved_params: Dict[str, inspect.Parameter],
query_params,
path_params: Optional[Dict[str, str]],
route_param_names: Set[str],
) -> Dict[str, Any]:
"""
Resolve handler parameters as individual path or query parameters.
For each unresolved parameter:
1. If its name matches a route param name (from the endpoint pattern), look it up in path_params.
2. Otherwise, look it up in query_params.
3. Apply type coercion based on the parameter's annotation.
4. Fall back to the parameter's default value, or None for Optional types.
5. Raise QueryParamValidationError if a required parameter is missing.
Args:
unresolved_params: dict of param_name -> inspect.Parameter for params not yet resolved.
query_params: QueryParams object with .get() and .get_all() methods.
path_params: dict of path parameter values (may be None for WebSocket).
route_param_names: set of parameter names declared in the route pattern (e.g. from /:id).
Returns:
dict mapping param names to their resolved values.
"""
resolved = {}
for param_name, param in unresolved_params.items():
annotation = param.annotation
if annotation is inspect.Parameter.empty:
annotation = str
inner_type, is_optional = unwrap_optional(annotation)
is_list = is_list_type(inner_type)
elem_type = get_list_element_type(inner_type) if is_list else inner_type
raw_value = _MISSING
# 1. Check path params first
if path_params is not None and param_name in route_param_names:
pv = path_params.get(param_name)
if pv is not None:
raw_value = pv
# 2. Check query params
if raw_value is _MISSING and query_params is not None:
if is_list:
all_values = query_params.get_all(param_name)
if all_values is not None:
resolved[param_name] = [coerce_value(v, elem_type, param_name) for v in all_values]
continue
else:
qp_value = query_params.get(param_name, None)
if qp_value is not None:
raw_value = qp_value
# 3. Got a value — coerce it
if raw_value is not _MISSING:
resolved[param_name] = coerce_value(raw_value, inner_type, param_name)
continue
# 4. Use default value if available
if param.default is not inspect.Parameter.empty:
resolved[param_name] = param.default
continue
# 5. Optional with no default -> None
if is_optional:
resolved[param_name] = None
continue
# 6. Truly missing required parameter
raise QueryParamValidationError(param_name, None, elem_type)
return resolved
def parse_route_param_names(endpoint: str) -> Set[str]:
"""
Extract parameter names from a route endpoint pattern.
e.g. "/users/:id/posts/:post_id" -> {"id", "post_id"}
Walks the string character by character looking for ':' followed by
word characters (alphanumeric + underscore).
"""
names = set()
i = 0
length = len(endpoint)
while i < length:
if endpoint[i] == ":":
# Start of a param name — collect word characters
i += 1
start = i
while i < length and (endpoint[i].isalnum() or endpoint[i] == "_"):
i += 1
if i > start:
names.add(endpoint[start:i])
else:
i += 1
return names
================================================
FILE: robyn/ai.py
================================================
"""
This is an experimental AI integration module for Robyn framework.
A poc for the blog at https://sanskar.wtf/posts/the-future-of-robyn
Provides agent and memory functionality for building AI-powered applications for the demonstration of the vision mentioned in
"""
import logging
import os
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Union
logger = logging.getLogger(__name__)
class AIConfig:
"""Configuration class for AI providers and settings"""
def __init__(self, **kwargs):
self.config = kwargs
self._load_from_env()
def _load_from_env(self):
"""Load configuration from environment variables"""
env_vars = {
"OPENAI_API_KEY": "openai_api_key",
"ANTHROPIC_API_KEY": "anthropic_api_key",
"GOOGLE_API_KEY": "google_api_key",
"AI_MODEL": "model",
"AI_TEMPERATURE": "temperature",
"AI_MAX_TOKENS": "max_tokens",
}
for env_var, config_key in env_vars.items():
if env_var in os.environ and config_key not in self.config:
value = os.environ[env_var]
# Convert numeric values
if config_key in ["temperature", "max_tokens"]:
try:
value = float(value) if config_key == "temperature" else int(value)
except ValueError:
pass
self.config[config_key] = value
def get(self, key: str, default: Any = None) -> Any:
"""Get configuration value"""
return self.config.get(key, default)
def set(self, key: str, value: Any) -> None:
"""Set configuration value"""
self.config[key] = value
def update(self, **kwargs) -> None:
"""Update configuration with new values"""
self.config.update(kwargs)
def to_dict(self) -> Dict[str, Any]:
"""Get configuration as dictionary"""
return self.config.copy()
class MemoryProvider(ABC):
"""Abstract base class for memory providers"""
@abstractmethod
async def store(self, user_id: str, data: Dict[str, Any]) -> None:
"""Store data in memory"""
pass
@abstractmethod
async def retrieve(self, user_id: str, query: Optional[str] = None) -> List[Dict[str, Any]]:
"""Retrieve data from memory"""
pass
@abstractmethod
async def clear(self, user_id: str) -> None:
"""Clear memory for a user"""
pass
class InMemoryProvider(MemoryProvider):
"""Simple in-memory storage provider"""
def __init__(self):
self._storage: Dict[str, List[Dict[str, Any]]] = {}
async def store(self, user_id: str, data: Dict[str, Any]) -> None:
if user_id not in self._storage:
self._storage[user_id] = []
self._storage[user_id].append(data)
async def retrieve(self, user_id: str, query: Optional[str] = None) -> List[Dict[str, Any]]:
return self._storage.get(user_id, [])
async def clear(self, user_id: str) -> None:
if user_id in self._storage:
del self._storage[user_id]
class Memory:
"""Memory interface for storing and retrieving conversation history and context"""
def __init__(self, provider: Union[str, MemoryProvider], user_id: str, **kwargs):
self.user_id = user_id
if isinstance(provider, str):
if provider == "inmemory":
self.provider = InMemoryProvider()
else:
raise ValueError(f"Unknown memory provider: {provider}")
else:
self.provider = provider
async def add(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
"""Add a message to memory"""
data = {"message": message, "metadata": metadata or {}}
await self.provider.store(self.user_id, data)
async def get(self, query: Optional[str] = None) -> List[Dict[str, Any]]:
"""Get messages from memory"""
return await self.provider.retrieve(self.user_id, query)
async def clear(self) -> None:
"""Clear all memory for this user"""
await self.provider.clear(self.user_id)
class AgentRunner(ABC):
"""Abstract base class for agent runners"""
@abstractmethod
async def run(self, query: str, **kwargs) -> Dict[str, Any]:
"""Execute the agent with the given query"""
pass
class SimpleRunner(AgentRunner):
"""Simple runner with OpenAI integration and fallback responses"""
def __init__(self, **config):
self.config = AIConfig(**config)
self._client = None
def _get_client(self):
"""Get OpenAI client if API key is available"""
if self._client is None and self.config.get("openai_api_key"):
try:
import openai
self._client = openai.OpenAI(api_key=self.config.get("openai_api_key"))
except ImportError:
raise ImportError("openai package not installed. Install with: pip install openai")
return self._client
async def run(self, query: str, **kwargs) -> Dict[str, Any]:
"""Execute with OpenAI (requires API key)"""
client = self._get_client()
if not client:
raise ValueError("OpenAI API key is required. Set 'openai_api_key' in configuration.")
try:
# Use OpenAI
messages = [{"role": "user", "content": query}]
# Add conversation history
context = kwargs.get("context", {})
history = context.get("history", [])
if history:
for item in history[-10:]:
msg = item.get("message", str(item))
if msg.startswith("Query: "):
messages.insert(-1, {"role": "user", "content": msg[7:]})
elif msg.startswith("Response: "):
messages.insert(-1, {"role": "assistant", "content": msg[10:]})
response = client.chat.completions.create(
model=self.config.get("model", "gpt-4o"),
messages=messages,
temperature=self.config.get("temperature", 0.7),
max_tokens=self.config.get("max_tokens", 1000),
)
content = response.choices[0].message.content
metadata = {
"runner_type": "simple_openai",
"model": self.config.get("model", "gpt-4o"),
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
"total_tokens": response.usage.total_tokens,
},
}
if self.config.get("debug", False):
metadata["debug_info"] = {"config": self.config.to_dict()}
return {"response": content, "query": query, "metadata": metadata}
except Exception as e:
logger.error(f"Error in SimpleRunner: {e}")
raise e
class Agent:
"""AI Agent interface for handling queries with memory and execution"""
def __init__(self, runner: Union[str, AgentRunner], memory: Optional[Memory] = None, **kwargs):
self.memory = memory
if isinstance(runner, str):
if runner == "simple":
self.runner = SimpleRunner(**kwargs)
else:
raise ValueError(f"Unknown runner type: {runner}")
else:
self.runner = runner
async def run(self, query: str, history: bool = False, **kwargs) -> Dict[str, Any]:
"""Run the agent with the given query"""
context = {}
if self.memory and history:
context["history"] = await self.memory.get()
# Add context to kwargs
if context:
kwargs["context"] = context
# Execute the agent
result = await self.runner.run(query, **kwargs)
# Store the interaction in memory if available
if self.memory:
await self.memory.add(f"Query: {query}")
if "response" in result:
await self.memory.add(f"Response: {result['response']}")
return result
def memory(provider: str = "inmemory", user_id: str = "default", **kwargs) -> Memory:
"""
Create a memory instance with the specified provider
Args:
provider: Memory provider type ("inmemory")
user_id: User identifier for memory isolation
**kwargs: Additional configuration for the provider
Returns:
Memory instance
"""
return Memory(provider=provider, user_id=user_id, **kwargs)
def configure(**kwargs) -> AIConfig:
"""
Create and configure AI settings
Args:
**kwargs: Configuration options including API keys, model settings, etc.
Returns:
AIConfig instance
Example:
config = configure(
openai_api_key="your-key",
model="gpt-4",
temperature=0.7
)
"""
return AIConfig(**kwargs)
def agent(runner: str = "simple", memory: Optional[Memory] = None, config: Optional[AIConfig] = None, **kwargs) -> Agent:
"""
Create an agent instance with the specified runner
Args:
runner: Agent runner type ("simple")
memory: Optional memory instance for context
config: Optional AIConfig instance for configuration
**kwargs: Additional configuration for the runner
Returns:
Agent instance
Example:
# Simple usage
chat = agent()
# With configuration
config = configure(openai_api_key="your-key")
chat = agent(runner="simple", config=config)
# With memory
mem = memory(provider="inmemory", user_id="user123")
chat = agent(runner="simple", memory=mem)
"""
# Merge config if provided
if config:
kwargs.update(config.to_dict())
return Agent(runner=runner, memory=memory, **kwargs)
================================================
FILE: robyn/argument_parser.py
================================================
import argparse
import os
class Config:
def __init__(self) -> None:
parser = argparse.ArgumentParser(description="Robyn, a fast async web framework with a rust runtime.")
self.parser = parser
parser.add_argument(
"--processes",
type=int,
default=None,
required=False,
help="Choose the number of processes. [Default: 1]",
)
parser.add_argument(
"--workers",
type=int,
default=None,
required=False,
help="Choose the number of workers. [Default: 1]",
)
parser.add_argument(
"--dev",
dest="dev",
action="store_true",
default=None,
help="Development mode. It restarts the server based on file changes.",
)
parser.add_argument(
"--log-level",
dest="log_level",
default=None,
help="Set the log level name",
)
parser.add_argument(
"--create",
action="store_true",
default=False,
help="Create a new project template.",
)
parser.add_argument(
"--docs",
action="store_true",
default=False,
help="Open the Robyn documentation.",
)
parser.add_argument(
"--open-browser",
action="store_true",
default=False,
help="Open the browser on successful start.",
)
parser.add_argument(
"--version",
action="store_true",
default=False,
help="Show the Robyn version.",
)
parser.add_argument(
"--compile-rust-path",
dest="compile_rust_path",
default=None,
help="Compile rust files in the given path.",
)
parser.add_argument(
"--create-rust-file",
dest="create_rust_file",
default=None,
help="Create a rust file with the given name.",
)
parser.add_argument(
"--disable-openapi",
dest="disable_openapi",
action="store_true",
default=False,
help="Disable the OpenAPI documentation.",
)
parser.add_argument(
"--fast",
dest="fast",
action="store_true",
default=False,
help="Fast mode. It sets the optimal values for processes, workers and log level. However, you can override them.",
)
args, unknown_args = parser.parse_known_args()
self.fast = args.fast
self.dev = args.dev
self.processes = args.processes
self.workers = args.workers
self.create = args.create
self.docs = args.docs
self.open_browser = args.open_browser
self.version = args.version
self.compile_rust_path = args.compile_rust_path
self.create_rust_file = args.create_rust_file
self.file_path = None
self.disable_openapi = args.disable_openapi
self.log_level = args.log_level
if self.fast:
# doing this here before every other check
# so that processes, workers and log_level can be overridden
cpu_count: int = os.cpu_count() or 1
self.processes = self.processes or ((cpu_count * 2) + 1) or 1
self.workers = self.workers or 2
self.log_level = self.log_level or "WARNING"
self.processes = self.processes or 1
self.workers = self.workers or 1
# find something that ends with .py in unknown_args
for arg in unknown_args:
if arg.endswith(".py"):
self.file_path = arg
break
if self.fast and self.dev:
raise ValueError("--fast and --dev shouldn't be used together")
if self.dev and (self.processes != 1 or self.workers != 1):
raise ValueError("--processes and --workers shouldn't be used with --dev")
if self.dev and self.log_level is None:
self.log_level = "DEBUG"
elif self.log_level is None:
self.log_level = "INFO"
================================================
FILE: robyn/authentication.py
================================================
from abc import ABC, abstractmethod
from typing import Optional
from robyn.robyn import Headers, Identity, Request, Response
from robyn.status_codes import HTTP_401_UNAUTHORIZED
class AuthenticationNotConfiguredError(Exception):
"""
This exception is raised when the authentication is not configured.
"""
def __str__(self):
return "Authentication is not configured. Use app.configure_authentication() to configure it."
class TokenGetter(ABC):
@property
def scheme(self) -> str:
"""
Gets the scheme of the token.
:return: The scheme of the token.
"""
return self.__class__.__name__
@classmethod
@abstractmethod
def get_token(cls, request: Request) -> Optional[str]:
"""
Gets the token from the request.
This method should not decode the token. Decoding is the role of the authentication handler.
:param request: The request object.
:return: The encoded token.
"""
raise NotImplementedError()
@classmethod
@abstractmethod
def set_token(cls, request: Request, token: str):
"""
Sets the token in the request.
This method should not encode the token. Encoding is the role of the authentication handler.
:param request: The request object.
:param token: The encoded token.
"""
raise NotImplementedError()
class AuthenticationHandler(ABC):
def __init__(self, token_getter: TokenGetter):
"""
Creates a new instance of the AuthenticationHandler class.
This class is an abstract class used to authenticate a user.
:param token_getter: The token getter used to get the token from the request.
"""
self.token_getter = token_getter
@property
def unauthorized_response(self) -> Response:
return Response(
headers=Headers({"WWW-Authenticate": self.token_getter.scheme}),
description="Unauthorized",
status_code=HTTP_401_UNAUTHORIZED,
)
@abstractmethod
def authenticate(self, request: Request) -> Optional[Identity]:
"""
Authenticates the user.
:param request: The request object.
:return: The identity of the user.
"""
raise NotImplementedError()
class BearerGetter(TokenGetter):
"""
This class is used to get the token from the Authorization header.
The scheme of the header must be Bearer.
"""
@classmethod
def get_token(cls, request: Request) -> Optional[str]:
if request.headers.contains("authorization"):
authorization_header = request.headers.get("authorization")
else:
authorization_header = None
if not authorization_header or not authorization_header.startswith("Bearer "):
return None
return authorization_header[7:] # Remove the "Bearer " prefix
@classmethod
def set_token(cls, request: Request, token: str):
request.headers["Authorization"] = f"Bearer {token}"
================================================
FILE: robyn/cli.py
================================================
import os
import shutil
import subprocess
import sys
import webbrowser
from pathlib import Path
from typing import Optional
from InquirerPy.base.control import Choice
from InquirerPy.resolver import prompt
from robyn.env_populator import load_vars
from robyn.robyn import get_version
from .argument_parser import Config
from .reloader import create_rust_file, setup_reloader
SCAFFOLD_DIR = Path(__file__).parent / "scaffold"
CURRENT_WORKING_DIR = Path.cwd()
def create_robyn_app():
questions = [
{
"type": "input",
"message": "Directory Path:",
"name": "directory",
},
{
"type": "list",
"message": "Need Docker? (Y/N)",
"choices": [
Choice("Y", name="Y"),
Choice("N", name="N"),
],
"default": Choice("N", name="N"),
"name": "docker",
},
{
"type": "list",
"message": "Please select project type (Mongo/Postgres/Sqlalchemy/Prisma): ",
"choices": [
Choice("no-db", name="No DB"),
Choice("sqlite", name="Sqlite"),
Choice("postgres", name="Postgres"),
Choice("mongo", name="MongoDB"),
Choice("sqlalchemy", name="SqlAlchemy"),
Choice("prisma", name="Prisma"),
Choice("sqlmodel", name="SQLModel"),
],
"default": Choice("no-db", name="No DB"),
"name": "project_type",
},
]
result = prompt(questions=questions)
project_dir_path = Path(str(result["directory"])).resolve()
docker = result["docker"]
project_type = str(result["project_type"])
final_project_dir_path = (CURRENT_WORKING_DIR / project_dir_path).resolve()
print(f"Creating a new Robyn project '{final_project_dir_path}'...")
# Create a new directory for the project
os.makedirs(final_project_dir_path, exist_ok=True)
selected_project_template = (SCAFFOLD_DIR / Path(project_type)).resolve()
shutil.copytree(str(selected_project_template), str(final_project_dir_path), dirs_exist_ok=True)
# If docker is not needed, delete the docker file
if docker == "N":
os.remove(f"{final_project_dir_path}/Dockerfile")
print(f"New Robyn project created in '{final_project_dir_path}' ")
def docs():
print("Opening Robyn documentation... | Offline docs coming soon!")
webbrowser.open("https://robyn.tech")
def start_dev_server(config: Config, file_path: Optional[str] = None):
if file_path is None:
return
absolute_file_path = (Path.cwd() / file_path).resolve()
directory_path = absolute_file_path.parent
if config.dev and not os.environ.get("IS_RELOADER_RUNNING", False):
setup_reloader(str(directory_path), str(absolute_file_path))
return
def start_app_normally(config: Config):
command = [sys.executable]
for arg in sys.argv[1:]:
command.append(arg)
# Run the subprocess
subprocess.run(command, start_new_session=False)
def run():
config = Config()
if not config.file_path:
config.file_path = f"{os.getcwd()}/{__name__}"
load_vars(project_root=os.path.dirname(os.path.abspath(config.file_path)))
os.environ["ROBYN_CLI"] = "True"
if config.dev is None:
config.dev = os.getenv("ROBYN_DEV_MODE", False) == "True"
if config.create:
create_robyn_app()
elif file_name := config.create_rust_file:
create_rust_file(file_name)
elif config.version:
print(get_version())
elif config.docs:
docs()
elif config.dev:
print("Starting dev server...")
start_dev_server(config, config.file_path)
else:
try:
start_app_normally(config)
except KeyboardInterrupt:
# for the crash happening upon pressing Ctrl + C
pass
================================================
FILE: robyn/dependency_injection.py
================================================
"""This is Robyn's dependency injection file."""
from typing import Any
class DependencyMap:
def __init__(self) -> None:
self.global_dependency_map: dict[str, Any] = {}
# {'router': {'dependency_name': dependency_class}
self.router_dependency_map: dict[str, dict[str, Any]] = {}
def add_router_dependency(self, router, **kwargs):
"""Adds a dependency to a route.
Args:
router App Object: The route to add the dependency to.
kwargs (dict): The dependencies to add to the route.
"""
if router not in self.router_dependency_map:
self.router_dependency_map[router] = {}
self.router_dependency_map[router].update(**kwargs)
def add_global_dependency(self, **kwargs):
"""Adds a dependency to all routes.
Args:
kwargs (dict): The dependencies to add to all routes.
"""
for name, element in kwargs.items():
self.global_dependency_map.update({name: element})
def get_router_dependencies(self, router):
"""Gets the dependencies for a specific route.
Args:
router
Returns:
dict: The dependencies for the specified route.
"""
return self.router_dependency_map.get(router, {})
def get_global_dependencies(self):
"""Gets the dependencies for a route.
Args:
route (str): The route to get the dependencies for.
"""
return self.global_dependency_map
def merge_dependencies(self, target_router):
"""
Merge dependencies from this DependencyMap into another router's DependencyMap.
Args:
target_router: The router with which to merge dependencies.
This method iterates through the dependencies of this DependencyMap and adds any dependencies
that are not already present in the target router's DependencyMap.
"""
for dep_key in self.get_global_dependencies():
if dep_key in target_router.dependencies.get_global_dependencies():
continue
target_router.dependencies.get_global_dependencies()[dep_key] = self.get_global_dependencies()[dep_key]
def get_dependency_map(self, router) -> dict:
return {
"global_dependencies": self.get_global_dependencies(),
"router_dependencies": self.get_router_dependencies(router),
}
================================================
FILE: robyn/env_populator.py
================================================
import logging
import os
from pathlib import Path
logger = logging.getLogger(__name__)
# parse the configuration file returning a list of tuples (key, value) containing the environment variables
def parser(config_path=None, project_root=""):
"""Find robyn.env file in root of the project and parse it"""
if config_path is None:
config_path = Path(project_root) / "robyn.env"
if config_path.exists():
with open(config_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
yield line.strip().split("=")
# check for the environment variables set in cli and if not set them
def load_vars(variables=None, project_root=""):
"""Main function"""
if variables is None:
variables = parser(project_root=project_root)
for var in variables:
if var[0] in os.environ:
logger.info(" Variable %s already set", var[0])
continue
else:
os.environ[var[0]] = var[1]
logger.info(" Variable %s set to %s", var[0], var[1])
================================================
FILE: robyn/events.py
================================================
from enum import Enum
class Events(Enum):
STARTUP = "startup"
SHUTDOWN = "shutdown"
================================================
FILE: robyn/exceptions.py
================================================
import http
class HTTPException(Exception):
def __init__(self, status_code: int, detail: str | None = None) -> None:
if detail is None:
detail = http.HTTPStatus(status_code).phrase
self.status_code = status_code
self.detail = detail
def __str__(self) -> str:
return f"{self.status_code}: {self.detail}"
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(status_code={self.status_code}, detail={self.detail})"
class WebSocketException(Exception):
def __init__(self, code: int, reason: str | None = None) -> None:
self.code = code
self.reason = reason or ""
def __str__(self) -> str:
return f"{self.code}: {self.reason}"
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(code={self.code}, reason={self.reason})"
__all__ = ["HTTPException", "WebSocketException"]
================================================
FILE: robyn/jsonify.py
================================================
from typing import Any, Dict, List, Union
import orjson
def jsonify(data: Union[Dict[str, Any], List[Any]]) -> str:
"""
This function serializes input data to a json string
Attributes:
data: dict or list to serialize as JSON response
"""
output_binary = orjson.dumps(data)
output_str = output_binary.decode("utf-8")
return output_str
================================================
FILE: robyn/logger.py
================================================
import logging
from enum import Enum
from typing import Optional
class Colors(Enum):
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
class Logger:
HEADER = "\033[95m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
def __init__(self):
self.logger = logging.getLogger(__name__)
def _format_msg(
self,
msg: str,
color: Optional[Colors],
bold: bool,
underline: bool,
):
result = msg
if color is not None:
result = f"{color.value}{result}{Logger.ENDC}"
if bold:
result = f"{Logger.BOLD}{result}"
if underline:
result = f"{Logger.UNDERLINE}{result}"
return result
def error(
self,
msg: str,
*args,
color: Optional[Colors] = Colors.RED,
bold: bool = False,
underline: bool = False,
):
self.logger.error(self._format_msg(msg, color, bold, underline), *args)
def warn(
self,
msg: str,
*args,
color: Optional[Colors] = Colors.YELLOW,
bold: bool = False,
underline: bool = False,
):
self.logger.warning(self._format_msg(msg, color, bold, underline), *args)
def info(
self,
msg: str,
*args,
color: Optional[Colors] = Colors.GREEN,
bold: bool = False,
underline: bool = False,
):
self.logger.info(self._format_msg(msg, color, bold, underline), *args)
def debug(
self,
msg: str,
*args,
color: Colors = Colors.BLUE,
bold: bool = False,
underline: bool = False,
):
self.logger.debug(self._format_msg(msg, color, bold, underline), *args)
logger = Logger()
================================================
FILE: robyn/mcp.py
================================================
"""
Model Context Protocol (MCP) implementation for Robyn.
This module provides MCP server functionality following the JSON-RPC 2.0 specification.
It allows Robyn applications to serve as MCP servers, exposing resources, tools, and prompts
to MCP clients like Claude Desktop or other AI applications.
"""
import inspect
import json
import logging
import re
from dataclasses import asdict, dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
logger = logging.getLogger(__name__)
def _extract_uri_params(uri: str) -> List[str]:
"""Extract parameter names from URI template like 'echo://{message}'"""
return re.findall(r"\{(\w+)\}", uri)
def _generate_schema_from_function(func: Callable) -> Dict[str, Any]:
"""Generate JSON schema from function signature"""
sig = inspect.signature(func)
properties = {}
required = []
for param_name, param in sig.parameters.items():
# Skip 'self' parameter
if param_name == "self":
continue
param_schema = {"type": "string"} # Default to string
# Try to infer type from annotation
if param.annotation != inspect.Parameter.empty:
if param.annotation is str:
param_schema["type"] = "string"
elif param.annotation is int:
param_schema["type"] = "integer"
elif param.annotation is float:
param_schema["type"] = "number"
elif param.annotation is bool:
param_schema["type"] = "boolean"
elif hasattr(param.annotation, "__origin__"):
# Handle generic types like List, Dict, etc.
param_schema["type"] = "object"
properties[param_name] = param_schema
# Add to required if no default value
if param.default == inspect.Parameter.empty:
required.append(param_name)
return {"type": "object", "properties": properties, "required": required}
def _generate_prompt_args_from_function(func: Callable) -> List[Dict[str, Any]]:
"""Generate prompt arguments from function signature"""
sig = inspect.signature(func)
arguments = []
for param_name, param in sig.parameters.items():
if param_name == "self":
continue
arg_def = {"name": param_name, "description": f"Parameter {param_name}", "required": param.default == inspect.Parameter.empty}
arguments.append(arg_def)
return arguments
@dataclass
class MCPResource:
"""Represents an MCP resource"""
uri: str
name: str
description: Optional[str] = None
mimeType: Optional[str] = None
@dataclass
class MCPTool:
"""Represents an MCP tool"""
name: str
description: str
inputSchema: Dict[str, Any]
@dataclass
class MCPPrompt:
"""Represents an MCP prompt template"""
name: str
description: str
arguments: Optional[List[Dict[str, Any]]] = None
@dataclass
class MCPMessage:
"""JSON-RPC 2.0 message structure"""
jsonrpc: str = "2.0"
id: Optional[Union[str, int]] = None
method: Optional[str] = None
params: Optional[Dict[str, Any]] = None
result: Optional[Any] = None
error: Optional[Dict[str, Any]] = None
class MCPError(Exception):
"""MCP-specific error"""
def __init__(self, code: int, message: str, data: Optional[Any] = None):
self.code = code
self.message = message
self.data = data
super().__init__(message)
class MCPHandler:
"""Handles MCP protocol requests and responses"""
def __init__(self):
self.resources: Dict[str, Callable] = {}
self.tools: Dict[str, Callable] = {}
self.prompts: Dict[str, Callable] = {}
self.resource_metadata: Dict[str, MCPResource] = {}
self.tool_metadata: Dict[str, MCPTool] = {}
self.prompt_metadata: Dict[str, MCPPrompt] = {}
self.server_info = {"name": "robyn-mcp-server", "version": "1.0.0"}
def register_resource(self, uri: str, name: str, handler: Callable, description: Optional[str] = None, mime_type: Optional[str] = None):
"""Register a resource handler"""
self.resources[uri] = handler
self.resource_metadata[uri] = MCPResource(uri=uri, name=name, description=description, mimeType=mime_type)
def register_tool(self, name: str, handler: Callable, description: str, input_schema: Dict[str, Any]):
"""Register a tool handler"""
self.tools[name] = handler
self.tool_metadata[name] = MCPTool(name=name, description=description, inputSchema=input_schema)
def register_prompt(self, name: str, handler: Callable, description: str, arguments: Optional[List[Dict[str, Any]]] = None):
"""Register a prompt handler"""
self.prompts[name] = handler
self.prompt_metadata[name] = MCPPrompt(name=name, description=description, arguments=arguments)
async def handle_request(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
"""Handle an MCP JSON-RPC request"""
try:
# Parse the request
method = request_data.get("method")
params = request_data.get("params", {})
request_id = request_data.get("id")
# Handle different MCP methods
if method == "initialize":
result = await self._handle_initialize(params)
elif method == "resources/list":
result = await self._handle_list_resources(params)
elif method == "resources/read":
result = await self._handle_read_resource(params)
elif method == "tools/list":
result = await self._handle_list_tools(params)
elif method == "tools/call":
result = await self._handle_call_tool(params)
elif method == "prompts/list":
result = await self._handle_list_prompts(params)
elif method == "prompts/get":
result = await self._handle_get_prompt(params)
else:
raise MCPError(-32601, f"Method not found: {method}")
# Return successful response
return {"jsonrpc": "2.0", "id": request_id, "result": result}
except MCPError as e:
return {"jsonrpc": "2.0", "id": request_data.get("id"), "error": {"code": e.code, "message": e.message, "data": e.data}}
except Exception as e:
logger.exception("Error handling MCP request")
return {"jsonrpc": "2.0", "id": request_data.get("id"), "error": {"code": -32603, "message": "Internal error", "data": str(e)}}
async def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle MCP initialize request"""
return {
"protocolVersion": "2024-11-05",
"capabilities": {"resources": {"subscribe": False, "listChanged": False}, "tools": {}, "prompts": {}},
"serverInfo": self.server_info,
}
async def _handle_list_resources(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle resources/list request"""
resources = []
for uri, metadata in self.resource_metadata.items():
resources.append(asdict(metadata))
return {"resources": resources}
def _match_uri_template(self, requested_uri: str) -> Optional[Tuple[str, Dict[str, str]]]:
"""Match requested URI against registered URI templates"""
for template_uri in self.resources.keys():
# Extract parameter names from template
param_names = _extract_uri_params(template_uri)
if not param_names:
# Exact match for non-templated URIs
if requested_uri == template_uri:
return template_uri, {}
continue
# Create regex pattern from template
pattern = template_uri
for param_name in param_names:
# Use (.+) to match any characters including slashes for paths
# Use ([^/]+) for single segments
if param_name in ["path", "file_path", "directory"]:
pattern = pattern.replace(f"{{{param_name}}}", r"(.+)")
else:
pattern = pattern.replace(f"{{{param_name}}}", r"([^/]+)")
match = re.match(f"^{pattern}$", requested_uri)
if match:
# Extract parameter values
param_values = {}
for i, param_name in enumerate(param_names):
param_values[param_name] = match.group(i + 1)
return template_uri, param_values
return None
async def _handle_read_resource(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle resources/read request"""
uri = params.get("uri")
if not uri:
raise MCPError(-32602, "URI is required")
# Try to match URI template
match_result = self._match_uri_template(uri)
if not match_result:
raise MCPError(-32602, f"Resource not found: {uri}")
template_uri, uri_params = match_result
handler = self.resources[template_uri]
# Call the handler with appropriate parameters based on its signature
try:
sig = inspect.signature(handler)
handler_params = list(sig.parameters.keys())
if inspect.iscoroutinefunction(handler):
if uri_params:
# Use URI parameters for templated resources
content = await handler(**uri_params)
elif handler_params:
# Handler expects parameters, pass the full params dict
content = await handler(params)
else:
# Handler expects no parameters
content = await handler()
else:
if uri_params:
# Use URI parameters for templated resources
content = handler(**uri_params)
elif handler_params:
# Handler expects parameters, pass the full params dict
content = handler(params)
else:
# Handler expects no parameters
content = handler()
except TypeError as e:
# Handle parameter mismatch errors
raise MCPError(-32603, f"Handler parameter error: {str(e)}")
# Determine content type
metadata = self.resource_metadata[template_uri]
mime_type = metadata.mimeType or "text/plain"
return {"contents": [{"uri": uri, "mimeType": mime_type, "text": str(content)}]}
async def _handle_list_tools(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle tools/list request"""
tools = []
for name, metadata in self.tool_metadata.items():
tools.append(asdict(metadata))
return {"tools": tools}
async def _handle_call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle tools/call request"""
name = params.get("name")
arguments = params.get("arguments", {})
if not name or name not in self.tools:
raise MCPError(-32602, f"Tool not found: {name}")
handler = self.tools[name]
# Call the tool handler
if inspect.iscoroutinefunction(handler):
result = await handler(**arguments)
else:
result = handler(**arguments)
return {"content": [{"type": "text", "text": str(result)}]}
async def _handle_list_prompts(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle prompts/list request"""
prompts = []
for name, metadata in self.prompt_metadata.items():
prompts.append(asdict(metadata))
return {"prompts": prompts}
async def _handle_get_prompt(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Handle prompts/get request"""
name = params.get("name")
arguments = params.get("arguments", {})
if not name or name not in self.prompts:
raise MCPError(-32602, f"Prompt not found: {name}")
handler = self.prompts[name]
# Call the prompt handler
if inspect.iscoroutinefunction(handler):
result = await handler(**arguments)
else:
result = handler(**arguments)
# Ensure result is in the expected format
if isinstance(result, str):
messages = [{"role": "user", "content": {"type": "text", "text": result}}]
elif isinstance(result, list):
messages = result
else:
messages = [{"role": "user", "content": {"type": "text", "text": str(result)}}]
return {"description": self.prompt_metadata[name].description, "messages": messages}
class MCPApp:
"""MCP application wrapper for Robyn"""
def __init__(self, robyn_app):
self.app = robyn_app
self.handler = MCPHandler()
self._setup_routes()
def _setup_routes(self):
"""Setup MCP routes on the Robyn app"""
@self.app.post("/mcp")
async def handle_mcp_request(request):
"""Handle MCP JSON-RPC requests"""
try:
# Parse JSON request - try multiple approaches
try:
request_data = request.json()
except (ValueError, TypeError, AttributeError):
# Fallback to parsing body as string
body = request.body
if isinstance(body, str):
request_data = json.loads(body)
else:
request_data = json.loads(body.decode("utf-8"))
# Handle case where request.json() returns a string instead of dict
if isinstance(request_data, str):
request_data = json.loads(request_data)
# Handle the request
response = await self.handler.handle_request(request_data)
return response
except json.JSONDecodeError:
return {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}}
except Exception as e:
logger.exception("Error in MCP request handler")
return {"jsonrpc": "2.0", "id": None, "error": {"code": -32603, "message": "Internal error", "data": str(e)}}
def resource(self, uri: str = None, name: str = None, description: Optional[str] = None, mime_type: Optional[str] = None):
"""
Decorator to register an MCP resource
Args:
uri: Resource URI template (e.g., "echo://{message}")
name: Human-readable name (auto-generated if not provided)
description: Resource description (auto-generated if not provided)
mime_type: MIME type (defaults to "text/plain")
Example:
@app.mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
return f"Resource echo: {message}"
"""
def decorator(func: Callable):
# Auto-generate metadata if not provided
actual_uri = uri or f"function://{func.__name__}"
actual_name = name or func.__name__.replace("_", " ").title()
actual_description = description or func.__doc__ or f"Resource: {actual_name}"
actual_mime_type = mime_type or "text/plain"
self.handler.register_resource(actual_uri, actual_name, func, actual_description, actual_mime_type)
return func
return decorator
def tool(self, name: str = None, description: str = None, input_schema: Dict[str, Any] = None):
"""
Decorator to register an MCP tool
Args:
name: Tool name (defaults to function name)
description: Tool description (auto-generated if not provided)
input_schema: JSON schema for inputs (auto-generated if not provided)
Example:
@app.mcp.tool()
def echo_tool(message: str) -> str:
return f"Tool echo: {message}"
"""
def decorator(func: Callable):
# Auto-generate metadata if not provided
actual_name = name or func.__name__
actual_description = description or func.__doc__ or f"Tool: {func.__name__}"
actual_schema = input_schema or _generate_schema_from_function(func)
self.handler.register_tool(actual_name, func, actual_description, actual_schema)
return func
return decorator
def prompt(self, name: str = None, description: str = None, arguments: Optional[List[Dict[str, Any]]] = None):
"""
Decorator to register an MCP prompt
Args:
name: Prompt name (defaults to function name)
description: Prompt description (auto-generated if not provided)
arguments: Prompt arguments (auto-generated if not provided)
Example:
@app.mcp.prompt()
def echo_prompt(message: str) -> str:
return f"Please process this message: {message}"
"""
def decorator(func: Callable):
# Auto-generate metadata if not provided
actual_name = name or func.__name__
actual_description = description or func.__doc__ or f"Prompt: {func.__name__}"
actual_arguments = arguments or _generate_prompt_args_from_function(func)
self.handler.register_prompt(actual_name, func, actual_description, actual_arguments)
return func
return decorator
================================================
FILE: robyn/openapi.py
================================================
import inspect
import json
import logging
import re
import typing
from dataclasses import asdict, dataclass, field
from importlib import resources
from inspect import Signature
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, is_typeddict
from robyn.responses import html
from robyn.robyn import QueryParams, Response
from robyn.pydantic_support import get_pydantic_openapi_schema, is_pydantic_model
from robyn.types import Body, JsonBody
_logger = logging.getLogger(__name__)
class str_typed_dict(TypedDict):
key: str
value: str
@dataclass
class Contact:
"""
The contact information for the exposed API.
(https://swagger.io/specification/#contact-object)
@param name: Optional[str] The identifying name of the contact person/organization.
@param url: Optional[str] The URL pointing to the contact information. This MUST be in the form of a URL.
@param email: Optional[str] The email address of the contact person/organization. This MUST be in the form of an email address.
"""
name: Optional[str] = None
url: Optional[str] = None
email: Optional[str] = None
@dataclass
class License:
"""
The license information for the exposed API.
(https://swagger.io/specification/#license-object)
@param name: Optional[str] The license name used for the API.
@param url: Optional[str] A URL to the license used for the API. This MUST be in the form of a URL.
"""
name: Optional[str] = None
url: Optional[str] = None
@dataclass
class Server:
"""
An array of Server Objects, which provide connectivity information to a target server. If the servers property is
not provided, or is an empty array, the default value would be a Server Object with a url value of /.
(https://swagger.io/specification/#server-object)
@param url: str A URL to the target host. This URL supports Server Variables and MAY be relative,
to indicate that the host location is relative to the location where the OpenAPI document is being served.
Variable substitutions will be made when a variable is named in {brackets}.
@param description: Optional[str] An optional string describing the host designated by the URL.
"""
url: str
description: Optional[str] = None
@dataclass
class ExternalDocumentation:
"""
Additional external documentation for this operation.
(https://swagger.io/specification/#external-documentation-object)
@param description: Optional[str] A description of the target documentation.
@param url: Optional[str] The URL for the target documentation.
"""
description: Optional[str] = None
url: Optional[str] = None
@dataclass
class Components:
"""
Additional external documentation for this operation.
(https://swagger.io/specification/#components-object)
@param schemas: Optional[Dict[str, Dict]] An object to hold reusable Schema Objects.
@param responses: Optional[Dict[str, Dict]] An object to hold reusable Response Objects.
@param parameters: Optional[Dict[str, Dict]] An object to hold reusable Parameter Objects.
@param examples: Optional[Dict[str, Dict]] An object to hold reusable Example Objects.
@param requestBodies: Optional[Dict[str, Dict]] An object to hold reusable Request Body Objects.
@param securitySchemes: Optional[Dict[str, Dict]] An object to hold reusable Security Scheme Objects.
@param links: Optional[Dict[str, Dict]] An object to hold reusable Link Objects.
@param callbacks: Optional[Dict[str, Dict]] An object to hold reusable Callback Objects.
@param pathItems: Optional[Dict[str, Dict]] An object to hold reusable Callback Objects.
"""
schemas: Optional[Dict[str, Dict]] = field(default_factory=dict)
responses: Optional[Dict[str, Dict]] = field(default_factory=dict)
parameters: Optional[Dict[str, Dict]] = field(default_factory=dict)
examples: Optional[Dict[str, Dict]] = field(default_factory=dict)
requestBodies: Optional[Dict[str, Dict]] = field(default_factory=dict)
securitySchemes: Optional[Dict[str, Dict]] = field(default_factory=dict)
links: Optional[Dict[str, Dict]] = field(default_factory=dict)
callbacks: Optional[Dict[str, Dict]] = field(default_factory=dict)
pathItems: Optional[Dict[str, Dict]] = field(default_factory=dict)
@dataclass
class OpenAPIInfo:
"""
Provides metadata about the API. The metadata MAY be used by tooling as required.
(https://swagger.io/specification/#info-object)
@param title: str The title of the API.
@param version: str The version of the API.
@param description: Optional[str] A description of the API.
@param termsOfService: Optional[str] A URL to the Terms of Service for the API.
@param contact: Contact The contact information for the exposed API.
@param license: License The license information for the exposed API.
@param servers: list[Server] An list of Server objects representing the servers.
@param externalDocs: Optional[ExternalDocumentation] Additional external documentation.
@param components: Components An element to hold various schemas for the document.
"""
title: str = "Robyn API"
version: str = "1.0.0"
description: Optional[str] = None
termsOfService: Optional[str] = None
contact: Contact = field(default_factory=Contact)
license: License = field(default_factory=License)
servers: List[Server] = field(default_factory=list)
externalDocs: Optional[ExternalDocumentation] = field(default_factory=ExternalDocumentation)
components: Components = field(default_factory=Components)
@dataclass
class OpenAPI:
"""
This is the root object of the OpenAPI document.
@param info: OpenAPIInfo Provides metadata about the API.
@param openapi_spec: dict The content of openapi.json as a dict
"""
info: OpenAPIInfo = field(default_factory=OpenAPIInfo)
openapi_spec: dict = field(init=False)
openapi_file_override: bool = False # denotes whether there is an override or not.
def __post_init__(self):
"""
Initializes the openapi_spec dict
"""
if self.openapi_file_override:
return
self.openapi_spec = {
"openapi": "3.1.0",
"info": asdict(self.info),
"paths": {},
"components": asdict(self.info.components),
"servers": [asdict(server) for server in self.info.servers],
"externalDocs": asdict(self.info.externalDocs) if self.info.externalDocs.url else None,
}
def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str, openapi_tags: List[str], handler: Callable):
"""
Adds the given path to openapi spec
@param route_type: str the http method as string (get, post ...)
@param endpoint: str the endpoint to be added
@param openapi_name: str the name of the endpoint
@param openapi_tags: List[str] for grouping of endpoints
@param handler: Callable the handler function for the endpoint
"""
if self.openapi_file_override:
return
query_params = None
request_body = None
return_annotation = None
signature = inspect.signature(handler)
openapi_description = inspect.getdoc(handler) or ""
if signature:
parameters = signature.parameters
if "query_params" in parameters:
query_params = parameters["query_params"].default
if query_params is Signature.empty:
query_params = None
if "body" in parameters:
request_body = parameters["body"].default
if request_body is Signature.empty:
request_body = None
# priority to typing
for parameter in parameters:
param_annotation = parameters[parameter].annotation
if inspect.isclass(param_annotation):
if issubclass(param_annotation, JsonBody):
request_body = param_annotation
elif issubclass(param_annotation, Body):
request_body = param_annotation
elif issubclass(param_annotation, QueryParams):
query_params = param_annotation
elif is_pydantic_model(param_annotation):
request_body = param_annotation
elif is_typeddict(param_annotation):
request_body = param_annotation
if signature.return_annotation is not Signature.empty:
return_annotation = signature.return_annotation
modified_endpoint, path_obj = self.get_path_obj(
endpoint, openapi_name, openapi_description, openapi_tags, query_params, request_body, return_annotation
)
if modified_endpoint not in self.openapi_spec["paths"]:
self.openapi_spec["paths"][modified_endpoint] = {}
self.openapi_spec["paths"][modified_endpoint][route_type] = path_obj
def _merge_component_schemas(self, incoming: dict):
"""Merge incoming component schemas into the spec with collision detection.
If an incoming schema name already exists and the schemas differ,
a warning is logged. The incoming schema always wins so that the
most-recently-registered route's models are used (matching the
behaviour of path registration).
"""
existing = self.openapi_spec["components"]["schemas"]
for name, schema in incoming.items():
if name in existing and existing[name] != schema:
_logger.warning(
"OpenAPI component schema '%s' is defined by multiple models with different shapes — the later definition will be used",
name,
)
existing[name] = schema
def add_subrouter_paths(self, subrouter_openapi: "OpenAPI"):
"""
Adds the subrouter paths and component schemas to main router's openapi specs.
@param subrouter_openapi: OpenAPI the OpenAPI object of the current subrouter
"""
if self.openapi_file_override:
return
for path, path_obj in subrouter_openapi.openapi_spec["paths"].items():
self.openapi_spec["paths"][path] = path_obj
subrouter_schemas = subrouter_openapi.openapi_spec.get("components", {}).get("schemas", {})
if subrouter_schemas:
self._merge_component_schemas(subrouter_schemas)
def get_path_obj(
self,
endpoint: str,
name: str,
description: str,
tags: List[str],
query_params: Optional[str_typed_dict],
request_body: Optional[str_typed_dict],
return_annotation: Optional[str_typed_dict],
) -> Tuple[str, dict]:
"""
Get the "path" openapi object according to spec
@param endpoint: str the endpoint to be added
@param name: str the name of the endpoint
@param description: Optional[str] short description of the endpoint (to be fetched from the endpoint definition by default)
@param tags: List[str] for grouping of endpoints
@param query_params: Optional[TypedDict] query params for the function
@param request_body: Optional[TypedDict] request body for the function
@param return_annotation: Optional[TypedDict] return type of the endpoint handler
@return: (str, dict) a tuple containing the endpoint with path params wrapped in braces and the "path" openapi object
according to spec
"""
if not description:
description = "No description provided"
openapi_path_object: dict = {
"summary": name,
"description": description,
"parameters": [],
"tags": tags,
}
# robyn has paths like /:url/:etc whereas openapi requires path like /{url}/{path}
# this function is used for converting path params to the required form
# initialized with endpoint for handling endpoints without path params
endpoint_with_path_params_wrapped_in_braces = endpoint
path_param_names = re.findall(r":(\w+)", endpoint)
if path_param_names:
# Convert param syntax to OpenAPI's {param} syntax
# \w+ matches word characters (letters, digits, underscores) and does not match '/',
# so each :param is captured individually without swallowing intervening path segments.
endpoint_with_path_params_wrapped_in_braces = re.sub(r":(\w+)", r"{\1}", endpoint)
for name in path_param_names:
openapi_path_object["parameters"].append(
{
"name": name,
"in": "path",
"required": True,
"schema": {"type": "string"},
}
)
if query_params:
query_param_annotations = query_params.__annotations__ if query_params is str_typed_dict else typing.get_type_hints(query_params)
for query_param in query_param_annotations:
query_param_type = self.get_openapi_type(query_param_annotations[query_param])
openapi_path_object["parameters"].append(
{
"name": query_param,
"in": "query",
"required": False,
"schema": {"type": query_param_type},
}
)
if request_body:
if is_pydantic_model(request_body):
schema, component_schemas = get_pydantic_openapi_schema(request_body)
if component_schemas:
self._merge_component_schemas(component_schemas)
request_body_object = {"content": {"application/json": {"schema": schema}}}
else:
properties = {}
request_body_annotations = request_body.__annotations__ if request_body is TypedDict else typing.get_type_hints(request_body)
for body_item in request_body_annotations:
properties[body_item] = self.get_schema_object(body_item, request_body_annotations[body_item])
request_body_object = {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": properties,
}
}
}
}
openapi_path_object["requestBody"] = request_body_object
response_schema: dict = {}
response_type = "text/plain"
if return_annotation and return_annotation is not Response:
response_type = "application/json"
response_schema = self.get_schema_object("response object", return_annotation)
openapi_path_object["responses"] = {"200": {"description": "Successful Response", "content": {response_type: {"schema": response_schema}}}}
return endpoint_with_path_params_wrapped_in_braces, openapi_path_object
def get_openapi_type(self, typed_dict: str_typed_dict) -> str:
"""
Get actual type from the TypedDict annotations
@param typed_dict: TypedDict The TypedDict to be converted
@return: str the type inferred
"""
type_mapping = {
int: "integer",
str: "string",
bool: "boolean",
float: "number",
dict: "object",
list: "array",
}
for type_name in type_mapping:
if typed_dict is type_name:
return type_mapping[type_name]
# default to "string" if type is not found
return "string"
def get_schema_object(self, parameter: str, param_type: Any) -> dict:
"""
Get the schema object for request/response body
@param parameter: name of the parameter
@param param_type: Any the type to be inferred
@return: dict the properties object
"""
properties: dict = {
"title": parameter.capitalize(),
}
type_mapping: dict = {
int: "integer",
str: "string",
bool: "boolean",
float: "number",
dict: "object",
list: "array",
}
for type_name in type_mapping:
if param_type is type_name:
properties["type"] = type_mapping[type_name]
return properties
# Check if it's a generic type (like List[Object])
if hasattr(param_type, "__origin__"):
if param_type.__origin__ is list or param_type.__origin__ is List:
properties["type"] = "array"
# Handle the element type in the list
if hasattr(param_type, "__args__") and param_type.__args__:
item_type = param_type.__args__[0]
properties["items"] = self.get_schema_object(f"{parameter}_item", item_type)
return properties
# check for Pydantic models
if is_pydantic_model(param_type):
schema, component_schemas = get_pydantic_openapi_schema(param_type)
if component_schemas:
self._merge_component_schemas(component_schemas)
return schema
# check for Optional type
if param_type.__module__ == "typing":
properties["anyOf"] = [{"type": self.get_openapi_type(param_type.__args__[0])}, {"type": "null"}]
return properties
# check for custom classes and TypedDicts
elif inspect.isclass(param_type):
properties["type"] = "object"
properties["properties"] = {}
for e in param_type.__annotations__:
properties["properties"][e] = self.get_schema_object(e, param_type.__annotations__[e])
properties["type"] = "object"
return properties
def override_openapi(self, openapi_json_spec_path: Path):
"""
Set a pre-configured OpenAPI spec
@param openapi_json_spec_path: str the path to the json file
"""
with open(openapi_json_spec_path) as json_file:
json_file_content = json.load(json_file)
self.openapi_spec = dict(json_file_content)
self.openapi_file_override = True
def get_openapi_docs_page(self) -> Response:
"""
Handler to the swagger html page to be deployed to the endpoint `/docs`
@return: FileResponse the swagger html page
"""
with resources.open_text("robyn", "swagger.html") as path:
html_file = path.read()
return html(html_file)
def get_openapi_config(self) -> dict:
"""
Returns the openapi spec as a dict
@return: dict the openapi spec
"""
return self.openapi_spec
================================================
FILE: robyn/processpool.py
================================================
import asyncio
import signal
import sys
import webbrowser
from typing import Dict, List, Optional
from multiprocess import Process # type: ignore
from robyn.events import Events
from robyn.logger import logger
from robyn.robyn import FunctionInfo, Headers, Server, SocketHeld
from robyn.router import GlobalMiddleware, Route, RouteMiddleware
from robyn.types import Directory
def run_processes(
url: str,
port: int,
directories: List[Directory],
request_headers: Headers,
routes: List[Route],
global_middlewares: List[GlobalMiddleware],
route_middlewares: List[RouteMiddleware],
web_sockets: Dict[str, dict],
event_handlers: Dict[Events, FunctionInfo],
workers: int,
processes: int,
response_headers: Headers,
excluded_response_headers_paths: Optional[List[str]],
open_browser: bool,
client_timeout: int = 30,
keep_alive_timeout: int = 20,
) -> List[Process]:
socket = SocketHeld(url, port)
process_pool = init_processpool(
directories,
request_headers,
routes,
global_middlewares,
route_middlewares,
web_sockets,
event_handlers,
socket,
workers,
processes,
response_headers,
excluded_response_headers_paths,
client_timeout,
keep_alive_timeout,
)
def terminating_signal_handler(_sig, _frame):
logger.info("Terminating server!!", bold=True)
for process in process_pool:
process.kill()
signal.signal(signal.SIGINT, terminating_signal_handler)
signal.signal(signal.SIGTERM, terminating_signal_handler)
if open_browser:
logger.info("Opening browser...")
webbrowser.open_new_tab(f"http://{url}:{port}/")
logger.info("Press Ctrl + C to stop \n")
for process in process_pool:
process.join()
return process_pool
def init_processpool(
directories: List[Directory],
request_headers: Headers,
routes: List[Route],
global_middlewares: List[GlobalMiddleware],
route_middlewares: List[RouteMiddleware],
web_sockets: Dict[str, dict],
event_handlers: Dict[Events, FunctionInfo],
socket: SocketHeld,
workers: int,
processes: int,
response_headers: Headers,
excluded_response_headers_paths: Optional[List[str]],
client_timeout: int = 30,
keep_alive_timeout: int = 20,
) -> List[Process]:
process_pool: List = []
if sys.platform.startswith("win32") or processes == 1:
spawn_process(
directories,
request_headers,
routes,
global_middlewares,
route_middlewares,
web_sockets,
event_handlers,
socket,
workers,
response_headers,
excluded_response_headers_paths,
client_timeout,
keep_alive_timeout,
)
return process_pool
for _ in range(processes):
copied_socket = socket.try_clone()
process = Process(
target=spawn_process,
args=(
directories,
request_headers,
routes,
global_middlewares,
route_middlewares,
web_sockets,
event_handlers,
copied_socket,
workers,
response_headers,
excluded_response_headers_paths,
client_timeout,
keep_alive_timeout,
),
)
process.start()
process_pool.append(process)
return process_pool
def initialize_event_loop():
if sys.platform.startswith("win32") or sys.platform.startswith("linux-cross"):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
else:
# uv loop doesn't support windows or arm machines at the moment
# but uv loop is much faster than native asyncio
import uvloop
uvloop.install()
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)
return loop
def spawn_process(
directories: List[Directory],
request_headers: Headers,
routes: List[Route],
global_middlewares: List[GlobalMiddleware],
route_middlewares: List[RouteMiddleware],
web_sockets: Dict[str, dict],
event_handlers: Dict[Events, FunctionInfo],
socket: SocketHeld,
workers: int,
response_headers: Headers,
excluded_response_headers_paths: Optional[List[str]],
client_timeout: int = 30,
keep_alive_timeout: int = 20,
):
"""
This function is called by the main process handler to create a server runtime.
This functions allows one runtime per process.
:param directories List: the list of all the directories and related data
:param headers tuple: All the global headers in a tuple
:param routes Tuple[Route]: The routes tuple, containing the description about every route.
:param middlewares Tuple[Route]: The middleware routes tuple, containing the description about every route.
:param web_sockets list: This is a list of all the web socket routes
:param event_handlers Dict: This is an event dict that contains the event handlers
:param socket SocketHeld: This is the main tcp socket, which is being shared across multiple processes.
:param process_name string: This is the name given to the process to identify the process
:param workers int: This is the name given to the process to identify the process
"""
loop = initialize_event_loop()
server = Server()
# TODO: if we remove the dot access
# the startup time will improve in the server
for directory in directories:
server.add_directory(*directory.as_list())
server.apply_request_headers(request_headers)
server.apply_response_headers(response_headers)
server.set_response_headers_exclude_paths(excluded_response_headers_paths)
for route in routes:
route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags = route
server.add_route(route_type, endpoint, function, is_const)
for middleware_type, middleware_function in global_middlewares:
server.add_global_middleware(middleware_type, middleware_function)
for middleware_type, endpoint, function, route_type in route_middlewares:
server.add_middleware_route(middleware_type, endpoint, function, route_type)
if Events.STARTUP in event_handlers:
server.add_startup_handler(event_handlers[Events.STARTUP])
if Events.SHUTDOWN in event_handlers:
server.add_shutdown_handler(event_handlers[Events.SHUTDOWN])
for endpoint in web_sockets:
web_socket = web_sockets[endpoint]
server.add_web_socket_route(
endpoint,
web_socket["connect"],
web_socket["close"],
web_socket["message"],
)
try:
server.start(socket, workers)
loop = asyncio.get_event_loop()
loop.run_forever()
except KeyboardInterrupt:
loop.close()
================================================
FILE: robyn/py.typed
================================================
================================================
FILE: robyn/pydantic_support.py
================================================
"""
Optional Pydantic integration for Robyn.
All pydantic imports are lazy: pydantic is only loaded on the first call
to _ensure_pydantic(), so there is zero import-time overhead when pydantic
is not installed or not used. The module itself is imported at the top
level by router.py and openapi.py, but this is safe because it contains
no pydantic imports at module scope.
"""
import inspect
from typing import Any, Optional, Tuple
import orjson
__all__ = [
"is_pydantic_available",
"is_pydantic_model",
"detect_pydantic_params",
"validate_pydantic_body",
"get_pydantic_openapi_schema",
"serialize_pydantic_response",
"check_pydantic_installed_for_handler",
"PydanticBodyValidationError",
"PydanticNotInstalledError",
"MultiplePydanticBodyError",
]
_BaseModel = None
_ValidationError = None
_pydantic_checked = False
def _ensure_pydantic():
"""Lazy-load pydantic classes. Called at most once."""
global _BaseModel, _ValidationError, _pydantic_checked
if _pydantic_checked:
return
_pydantic_checked = True
try:
from pydantic import BaseModel, ValidationError
_BaseModel = BaseModel
_ValidationError = ValidationError
except ImportError:
_BaseModel = None
_ValidationError = None
def is_pydantic_available() -> bool:
_ensure_pydantic()
return _BaseModel is not None
def is_pydantic_model(annotation) -> bool:
"""Check if an annotation is a pydantic BaseModel subclass.
Returns False if pydantic is not installed or annotation is not a class."""
_ensure_pydantic()
if _BaseModel is None:
return False
return inspect.isclass(annotation) and issubclass(annotation, _BaseModel)
def detect_pydantic_params(handler_params) -> dict:
"""Scan pre-computed handler parameters for Pydantic BaseModel annotations.
Accepts the ``parameters`` OrderedDict from ``inspect.signature(handler)``.
Returns a dict mapping param_name -> model_class for params annotated with
a Pydantic BaseModel subclass. Returns empty dict if pydantic is not
installed or no params use Pydantic models.
"""
_ensure_pydantic()
if _BaseModel is None:
return {}
result = {}
for name, param in handler_params.items():
ann = param.annotation
if ann is inspect.Parameter.empty:
continue
if inspect.isclass(ann) and issubclass(ann, _BaseModel):
result[name] = ann
return result
def _sanitize_errors(errors: list) -> list:
"""Make pydantic error dicts JSON-serializable.
Only copies dicts that actually contain non-serializable values.
Pydantic v2 error dicts can contain bytes, tuples, and other
non-JSON-serializable values in 'input', 'loc', and 'ctx' fields.
"""
sanitized = []
for err in errors:
needs_copy = False
for key, val in err.items():
if (key == "input" and isinstance(val, bytes)) or key == "loc" or (key == "ctx" and isinstance(val, dict)):
needs_copy = True
break
if not needs_copy:
sanitized.append(err)
continue
clean = dict(err)
if "input" in clean and isinstance(clean["input"], bytes):
clean["input"] = clean["input"].decode("utf-8", errors="replace")
if "loc" in clean:
clean["loc"] = list(clean["loc"])
if "ctx" in clean and isinstance(clean["ctx"], dict):
clean["ctx"] = {k: str(v) for k, v in clean["ctx"].items()}
sanitized.append(clean)
return sanitized
def validate_pydantic_body(model_class, body: Any) -> Tuple[Any, Optional[dict]]:
"""Validate request body against a Pydantic model.
Uses model_validate_json for maximum performance — single-pass
parse+validate without an intermediate dict. model_validate_json
accepts str, bytes, and bytearray natively.
This function is only called from the request hot path *after*
_ensure_pydantic() has already been called at registration time,
so we skip the redundant check here.
Returns:
(validated_model_instance, None) on success
(None, error_detail_dict) on failure
"""
try:
return model_class.model_validate_json(body), None
except _ValidationError as e:
return None, {
"detail": _sanitize_errors(e.errors()),
"error": "Validation Error",
}
def get_pydantic_openapi_schema(model_class) -> Tuple[dict, dict]:
"""Get OpenAPI-compatible JSON Schema from a Pydantic model.
Uses ref_template so nested model references point to
#/components/schemas/{ModelName} in the OpenAPI spec.
Returns:
(schema, component_schemas) where:
- schema: the model's JSON Schema (without $defs)
- component_schemas: dict of model_name -> schema for components/schemas
"""
_ensure_pydantic()
if _BaseModel is None or not (inspect.isclass(model_class) and issubclass(model_class, _BaseModel)):
return {}, {}
full_schema = model_class.model_json_schema(ref_template="#/components/schemas/{model}")
component_schemas = full_schema.pop("$defs", {})
return full_schema, component_schemas
def serialize_pydantic_response(res) -> Optional[str]:
"""Serialize a Pydantic model (or list of models) to a JSON string.
Returns None when *res* is not a Pydantic type so the caller can fall
through to other serialisation paths.
This function is only called from the response hot path *after*
_ensure_pydantic() has already been called at registration time,
so we skip the redundant check here.
"""
if _BaseModel is None:
return None
if isinstance(res, _BaseModel):
return res.model_dump_json()
if isinstance(res, list) and res and isinstance(res[0], _BaseModel):
return orjson.dumps([item.model_dump(mode="python") for item in res]).decode("utf-8")
return None
class PydanticBodyValidationError(Exception):
"""Raised at request time when Pydantic body validation fails.
Carries the serializable error dict for the 422 response."""
def __init__(self, error_detail: dict):
self.error_detail = error_detail
super().__init__(error_detail.get("error", "Validation Error"))
class PydanticNotInstalledError(ImportError):
"""Raised at route registration when a handler uses a Pydantic model
but pydantic is not installed."""
def __init__(self, handler_name: str, param_name: str, model_name: str):
super().__init__(
f"Handler '{handler_name}' has parameter '{param_name}' annotated with "
f"Pydantic model '{model_name}', but pydantic is not installed. "
f'Install it with: pip install "robyn[pydantic]" or pip install "robyn[all]"'
)
class MultiplePydanticBodyError(TypeError):
"""Raised at route registration when a handler declares more than one
Pydantic body parameter."""
def __init__(self, handler_name: str, param_names: list):
super().__init__(
f"Handler '{handler_name}' has multiple Pydantic body parameters "
f"{param_names}. Only one Pydantic body parameter per handler is "
f"supported — the entire request body is parsed into that single model."
)
def check_pydantic_installed_for_handler(handler, pydantic_params: dict):
"""Validate Pydantic usage at startup.
Raises PydanticNotInstalledError if pydantic isn't available.
Raises MultiplePydanticBodyError if more than one body param is declared.
"""
if not pydantic_params:
return
if not is_pydantic_available():
first_param = next(iter(pydantic_params))
model = pydantic_params[first_param]
raise PydanticNotInstalledError(handler.__name__, first_param, model.__name__)
if len(pydantic_params) > 1:
raise MultiplePydanticBodyError(handler.__name__, list(pydantic_params.keys()))
================================================
FILE: robyn/reloader.py
================================================
import glob
import os
import signal
import subprocess
import sys
import time
from typing import List, Union
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from robyn.logger import Colors, logger
def compile_rust_files(directory_path: str) -> List[str]:
rust_files = glob.glob(os.path.join(directory_path, "**/*.rs"), recursive=True)
rust_binaries: list[str] = []
for rust_file in rust_files:
print(f"Compiling rust file: {rust_file}")
result = subprocess.run(
[sys.executable, "-m", "rustimport", "build", rust_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=False,
)
if result.returncode != 0:
print(f"Error compiling rust file: {rust_file} \n {result.stderr.decode('utf-8')} \n {result.stdout.decode('utf-8')}")
else:
print(f"Compiled rust file: {rust_file}")
rust_file_base = rust_file.removesuffix(".rs")
# Define the search pattern for the binary file
if sys.platform == "win32":
binary_extension = ".dll"
elif sys.platform == "darwin":
binary_extension = ".so"
elif sys.platform == "linux":
binary_extension = ".so"
else:
raise ValueError(f"Unsupported platform: {sys.platform}")
search_pattern = f"{rust_file_base}.*{binary_extension}"
# Use glob to find matching binary files
matching_binaries = glob.glob(search_pattern)
rust_binaries.extend(matching_binaries)
return rust_binaries
def create_rust_file(file_name: str) -> None:
if file_name.endswith(".rs"):
file_name = file_name.removesuffix(".rs")
rust_file = f"{file_name}.rs"
result = subprocess.run(
[sys.executable, "-m", "rustimport", "new", rust_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=False,
)
if result.returncode != 0:
print(
"Error creating rust file : %s %s",
result.stderr.decode("utf-8"),
result.stdout.decode("utf-8"),
)
else:
print("Created rust file : %s", rust_file)
def clean_rust_binaries(rust_binaries: List[str]) -> None:
for file in rust_binaries:
print("Cleaning rust file : %s", file)
os.remove(file)
def setup_reloader(directory_path: str, file_path: str) -> None:
event_handler = EventHandler(file_path, directory_path)
# sets the IS_RELOADER_RUNNING environment variable to True
event_handler.reload()
logger.info(
"Dev server initialized with the directory_path : %s",
directory_path,
color=Colors.BLUE,
)
def terminating_signal_handler(_sig, _frame):
event_handler.stop_server()
logger.info("Terminating reloader", bold=True)
observer.stop()
observer.join()
signal.signal(signal.SIGINT, terminating_signal_handler)
signal.signal(signal.SIGTERM, terminating_signal_handler)
observer = Observer()
observer.schedule(event_handler, path=directory_path, recursive=True)
observer.start()
try:
while observer.is_alive():
observer.join(1)
finally:
observer.stop()
observer.join()
event_handler.process.wait()
class EventHandler(FileSystemEventHandler):
def __init__(self, file_path: str, directory_path: str) -> None:
self.file_path = file_path
self.directory_path = directory_path
self.process: Union[subprocess.Popen[bytes], None] = None # Keep track of the subprocess
self.built_rust_binaries: List = [] # Keep track of the built rust binaries
self.last_reload = time.time() # Keep track of the last reload. EventHandler is initialized with the process.
def stop_server(self) -> None:
if self.process:
os.kill(self.process.pid, signal.SIGTERM) # Stop the subprocess using os.kill()
def reload(self) -> None:
self.stop_server()
print("Reloading the server")
new_env = os.environ.copy()
new_env["IS_RELOADER_RUNNING"] = "True" # This is used to check if a reloader is already running
# IS_RELOADER_RUNNING is specifically used for IPC between the reloader and the server
arguments = [arg for arg in sys.argv[1:] if not arg.startswith("--dev")]
clean_rust_binaries(self.built_rust_binaries)
self.built_rust_binaries = compile_rust_files(self.directory_path)
prev_process = self.process
if prev_process:
prev_process.kill()
self.process = subprocess.Popen(
[sys.executable, *arguments],
env=new_env,
)
self.last_reload = time.time()
def on_modified(self, event) -> None:
"""
This function is a callback that will start a new server on every even change
:param event FSEvent: a data structure with info about the events
"""
# Avoid reloading multiple times when watchdog detects multiple events
if time.time() - self.last_reload < 0.5:
return
time.sleep(0.2) # Wait for the file to be fully written
self.reload()
================================================
FILE: robyn/responses.py
================================================
import asyncio
import mimetypes
import os
from typing import AsyncGenerator, Generator, Optional, Union
from robyn.robyn import Headers, Response
class FileResponse:
def __init__(
self,
file_path: str,
status_code: Optional[int] = None,
headers: Optional[Headers] = None,
):
self.file_path = file_path
self.description = ""
self.status_code = status_code or 200
self.headers = headers or Headers({"Content-Disposition": "attachment"})
def html(html: str) -> Response:
"""
This function will help in serving a simple html string
:param html str: html to serve as a response
"""
return Response(
description=html,
status_code=200,
headers=Headers({"Content-Type": "text/html"}),
)
def serve_html(file_path: str) -> FileResponse:
"""
This function will help in serving a single html file
:param file_path str: file path to serve as a response
"""
return FileResponse(file_path, headers=Headers({"Content-Type": "text/html"}))
def serve_file(file_path: str, file_name: Optional[str] = None) -> FileResponse:
"""
This function will help in serving a file
:param file_path str: file path to serve as a response
:param file_name [str | None]: file name to serve as a response, defaults to None
"""
file_name = file_name or os.path.basename(file_path)
mime_type = mimetypes.guess_type(file_name)[0]
headers = Headers({"Content-Type": mime_type})
headers.append("Content-Disposition", f"attachment; filename={file_name}")
return FileResponse(
file_path,
headers=headers,
)
class AsyncGeneratorWrapper:
"""Optimized true-streaming wrapper for async generators"""
def __init__(self, async_gen: AsyncGenerator[str, None]):
self.async_gen = async_gen
self._loop = None
self._iterator = None
self._exhausted = False
def __iter__(self):
return self
def __next__(self):
if self._exhausted:
raise StopIteration
# Initialize the loop and iterator only once
if self._iterator is None:
self._init_async_iterator()
try:
# Get the next value from the async generator
# This is the key optimization - we don't buffer, we get one value at a time
return self._get_next_value()
except StopIteration:
self._exhausted = True
raise
def _init_async_iterator(self):
"""Initialize the async iterator with proper loop handling"""
try:
# Try to get the running event loop
self._loop = asyncio.get_running_loop()
except RuntimeError:
# No running loop, create a new one
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
# Create the async iterator
self._iterator = self.async_gen.__aiter__()
def _get_next_value(self):
"""Get the next value from async generator without buffering"""
try:
# Create a coroutine to get the next value
async def get_next():
return await self._iterator.__anext__()
# Run the coroutine to get the next value
return self._loop.run_until_complete(get_next())
except StopAsyncIteration:
# Convert StopAsyncIteration to StopIteration for sync generator protocol
raise StopIteration
except Exception as e:
# Log error and stop iteration
print(f"Error in async generator: {e}")
raise StopIteration
class StreamingResponse:
def __init__(
self,
content: Union[Generator[str, None, None], AsyncGenerator[str, None]],
status_code: Optional[int] = None,
headers: Optional[Headers] = None,
media_type: str = "text/event-stream",
):
# Convert async generator to sync generator if needed
# The Rust implementation detects async generators but falls back to Python wrapper
if hasattr(content, "__anext__"):
# This is an async generator - wrap it with optimized wrapper
self.content = AsyncGeneratorWrapper(content)
else:
# This is a sync generator - use as is
self.content = content
self.status_code = status_code or 200
self.headers = headers or Headers({})
self.media_type = media_type
# Set default SSE headers
if media_type == "text/event-stream":
self.headers.set("Content-Type", "text/event-stream")
# Cache-Control and Connection headers are set by Rust layer with optimized headers
def SSEResponse(
content: Union[Generator[str, None, None], AsyncGenerator[str, None]],
status_code: Optional[int] = None,
headers: Optional[Headers] = None,
) -> StreamingResponse:
"""
Create a Server-Sent Events (SSE) streaming response.
:param content: Generator or AsyncGenerator yielding SSE-formatted strings
:param status_code: HTTP status code (default: 200)
:param headers: Additional headers
:return: StreamingResponse configured for SSE
"""
return StreamingResponse(content=content, status_code=status_code, headers=headers, media_type="text/event-stream")
def SSEMessage(data: str, event: Optional[str] = None, id: Optional[str] = None, retry: Optional[int] = None) -> str:
"""
Optimized SSE message formatting with minimal allocations.
:param data: The message data
:param event: Optional event type
:param id: Optional event ID
:param retry: Optional retry time in milliseconds
:return: SSE-formatted string
"""
# Pre-calculate size to avoid multiple string concatenations
parts = []
# Add optional fields first
if event:
parts.append(f"event: {event}\n")
if id:
parts.append(f"id: {id}\n")
if retry:
parts.append(f"retry: {retry}\n")
# Handle data with optimized multi-line processing
if data:
data_str = str(data)
# Fast path for single-line data (most common case)
if "\n" not in data_str and "\r" not in data_str:
parts.append(f"data: {data_str}\n")
else:
# Multi-line data handling
normalized_data = data_str.replace("\r\n", "\n").replace("\r", "\n")
for line in normalized_data.split("\n"):
parts.append(f"data: {line}\n")
else:
parts.append("data: \n")
# Add the required double newline terminator
parts.append("\n")
# Single join operation for optimal performance
return "".join(parts)
================================================
FILE: robyn/robyn.pyi
================================================
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Callable, Optional, Union
def get_version() -> str:
pass
class SocketHeld:
def __init__(self, url: str, port: int):
pass
def try_clone(self) -> SocketHeld:
pass
class MiddlewareType(Enum):
"""
The middleware types supported by Robyn.
Attributes:
BEFORE_REQUEST: str
AFTER_REQUEST: str
"""
BEFORE_REQUEST: str
AFTER_REQUEST: str
class HttpMethod(Enum):
"""
The HTTP methods supported by Robyn.
Attributes:
GET: str
POST: str
PUT: str
DELETE: str
PATCH: str
OPTIONS: str
HEAD: str
TRACE: str
CONNECT: str
"""
GET: str
POST: str
PUT: str
DELETE: str
PATCH: str
OPTIONS: str
HEAD: str
TRACE: str
CONNECT: str
@dataclass
class FunctionInfo:
"""
The function info object passed to the route handler.
Attributes:
handler (Callable): The function to be called
is_async (bool): Whether the function is async or not
number_of_params (int): The number of parameters the function has
args (dict): The arguments of the function
kwargs (dict): The keyword arguments of the function
"""
handler: Callable
is_async: bool
number_of_params: int
args: dict
kwargs: dict
@dataclass
class Url:
"""
The url object passed to the route handler.
Attributes:
scheme (str): The scheme of the url. e.g. http, https
host (str): The host of the url. e.g. localhost,
path (str): The path of the url. e.g. /user
"""
scheme: str
host: str
path: str
@dataclass
class Identity:
claims: dict[str, str]
class QueryParams:
"""
The query params object passed to the route handler.
Attributes:
queries (dict[str, list[str]]): The query parameters of the request. e.g. /user?id=123 -> {"id": "123"}
"""
def set(self, key: str, value: str) -> None:
"""
Sets the value of the query parameter with the given key.
If the key already exists, the value will be appended to the list of values.
Args:
key (str): The key of the query parameter
value (str): The value of the query parameter
"""
pass
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
"""
Gets the last value of the query parameter with the given key.
Args:
key (str): The key of the query parameter
default (Optional[str]): The default value if the key does not exist
"""
pass
def empty(self) -> bool:
"""
Returns:
True if the query params are empty, False otherwise
"""
pass
def contains(self, key: str) -> bool:
"""
Returns:
True if the query params contain the key, False otherwise
Args:
key (str): The key of the query parameter
"""
pass
def get_first(self, key: str) -> Optional[str]:
"""
Gets the first value of the query parameter with the given key.
Args:
key (str): The key of the query parameter
"""
pass
def get_all(self, key: str) -> Optional[list[str]]:
"""
Gets all the values of the query parameter with the given key.
Args:
key (str): The key of the query parameter
"""
pass
def extend(self, other: QueryParams) -> None:
"""
Extends the query params with the other query params.
Args:
other (QueryParams): The other QueryParams object
"""
pass
def to_dict(self) -> dict[str, list[str]]:
"""
Returns:
The query params as a dictionary
"""
pass
def __contains__(self, key: str) -> bool:
pass
def __repr__(self) -> str:
pass
@dataclass
class Cookie:
"""
A cookie with optional attributes following RFC 6265.
Attributes:
value (str): The cookie value
path (Optional[str]): Cookie path (e.g. "/")
domain (Optional[str]): Cookie domain
max_age (Optional[int]): Max age in seconds
expires (Optional[str]): Expiry date (deprecated, use max_age instead)
secure (bool): Only send over HTTPS
http_only (bool): Not accessible via JavaScript
same_site (Optional[str]): "Strict", "Lax", or "None" (case-insensitive)
"""
value: str
path: Optional[str] = None
domain: Optional[str] = None
max_age: Optional[int] = None
expires: Optional[str] = None
secure: bool = False
http_only: bool = False
same_site: Optional[str] = None
@staticmethod
def deleted() -> "Cookie":
"""
Create a cookie configured for deletion (expires immediately with max_age=0).
Returns:
Cookie: A cookie that will be deleted by the browser
"""
pass
class Cookies:
"""A collection of cookies keyed by name."""
def __init__(self) -> None:
pass
def set(self, name: str, cookie: Cookie) -> None:
"""
Sets a cookie with the given name.
Args:
name (str): The name of the cookie
cookie (Cookie): The cookie object
"""
pass
def get(self, name: str) -> Optional[Cookie]:
"""
Gets the cookie with the given name.
Args:
name (str): The name of the cookie
"""
pass
def remove(self, name: str) -> None:
"""
Removes the cookie from the collection (does not delete it from the browser).
Args:
name (str): The name of the cookie
"""
pass
def delete(self, name: str) -> None:
"""
Mark a cookie for deletion by setting it to expire immediately.
This sets max_age=0 which tells the browser to delete the cookie.
Args:
name (str): The name of the cookie to delete
"""
pass
def is_empty(self) -> bool:
"""
Returns:
True if there are no cookies, False otherwise
"""
pass
def keys(self) -> list[str]:
"""
Returns:
A list of all cookie names
"""
pass
def __setitem__(self, name: str, cookie: Cookie) -> None:
pass
def __getitem__(self, name: str) -> Optional[Cookie]:
pass
def __contains__(self, name: str) -> bool:
pass
def __len__(self) -> int:
pass
def __iter__(self) -> "CookiesIter":
pass
def __repr__(self) -> str:
pass
class CookiesIter:
"""Iterator for Cookies collection."""
def __iter__(self) -> "CookiesIter":
pass
def __next__(self) -> str:
pass
class Headers:
def __init__(self, default_headers: Optional[dict]) -> None:
pass
def __getitem__(self, key: str) -> Optional[str]:
pass
def __setitem__(self, key: str, value: str) -> None:
pass
def set(self, key: str, value: str) -> None:
"""
Sets the value of the header with the given key.
If the key already exists, the value will be appended to the list of values.
Args:
key (str): The key of the header
value (str): The value of the header
"""
pass
def get(self, key: str) -> Optional[str]:
"""
Gets the last value of the header with the given key.
Args:
key (str): The key of the header
"""
pass
def populate_from_dict(self, headers: dict[str, str]) -> None:
"""
Populates the headers from a dictionary.
Args:
headers (dict[str, str]): The dictionary of headers
"""
pass
def contains(self, key: str) -> bool:
"""
Returns:
True if the headers contain the key, False otherwise
Args:
key (str): The key of the header
"""
pass
def append(self, key: str, value: str) -> None:
"""
Appends the value to the header with the given key.
Args:
key (str): The key of the header
value (str): The value of the header
"""
pass
def is_empty(self) -> bool:
"""
Returns:
True if the headers are empty, False otherwise
"""
pass
@dataclass
class Request:
"""
The request object passed to the route handler.
Attributes:
query_params (QueryParams): The query parameters of the request. e.g. /user?id=123 -> {"id": "123"}
headers Headers: The headers of the request. e.g. Headers({"Content-Type": "application/json"})
path_params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"}
body (Union[str, bytes]): The body of the request. If the request is a JSON, it will be a dict.
method (str): The method of the request. e.g. GET, POST, PUT etc.
url (Url): The url of the request. e.g. https://localhost/user
form_data (dict[str, str]): The form data of the request. e.g. {"name": "John"}
files (dict[str, bytes]): The files of the request. e.g. {"file": b"file"}
ip_addr (Optional[str]): The IP Address of the client
identity (Optional[Identity]): The identity of the client
"""
query_params: QueryParams
headers: Headers
path_params: dict[str, str]
body: Union[str, bytes]
method: str
url: Url
form_data: dict[str, str]
files: dict[str, bytes]
ip_addr: Optional[str]
identity: Optional[Identity]
def json(self) -> Union[dict, list]:
"""
Parse the request body as JSON and return a Python dict or list with preserved types.
JSON types are mapped to Python types as follows:
- null -> None
- bool -> bool
- number -> int or float
- string -> str
- array -> list
- object -> dict
Nested structures are handled recursively up to a maximum depth of 128.
Raises:
ValueError: If the body is not valid JSON.
"""
pass
@dataclass
class Response:
"""
The response object passed to the route handler.
Attributes:
status_code (int): The status code of the response. e.g. 200, 404, 500 etc.
response_type (Optional[str]): The response type of the response. e.g. text, json, html, file etc.
headers (Union[Headers, dict]): The headers of the response or Headers directly. e.g. {"Content-Type": "application/json"}
description (Union[str, bytes]): The body of the response. If the response is a JSON, it will be a dict.
file_path (Optional[str]): The file path of the response. e.g. /home/user/file.txt
cookies (Cookies): The cookies to set in the response.
"""
status_code: int
headers: Union[Headers, dict]
description: Union[str, bytes]
response_type: Optional[str] = None
file_path: Optional[str] = None
cookies: Cookies = None # Initialized automatically
def set_cookie(
self,
key: str,
value: str,
path: Optional[str] = None,
domain: Optional[str] = None,
max_age: Optional[int] = None,
expires: Optional[str] = None,
secure: bool = False,
http_only: bool = False,
same_site: Optional[str] = None,
) -> None:
"""
Sets a cookie in the response. If a cookie with the same key exists,
it will be overwritten.
Args:
key (str): The name of the cookie
value (str): The value of the cookie
path (Optional[str]): Cookie path (e.g. "/")
domain (Optional[str]): Cookie domain
max_age (Optional[int]): Max age in seconds
expires (Optional[str]): Expiry date (RFC 7231 format)
secure (bool): Only send over HTTPS
http_only (bool): Not accessible via JavaScript
same_site (Optional[str]): "Strict", "Lax", or "None"
"""
pass
class Server:
"""
The Server object used to create a Robyn server.
This object is used to create a Robyn server and add routes, middlewares, etc.
"""
def __init__(self) -> None:
pass
def add_directory(
self,
route: str,
directory_path: str,
show_files_listing: bool,
index_file: Optional[str],
) -> None:
pass
def apply_request_headers(self, headers: Headers) -> None:
pass
def apply_response_headers(self, headers: Headers) -> None:
pass
def set_response_headers_exclude_paths(self, excluded_response_headers_paths: Optional[list[str]] = None):
pass
def add_route(
self,
route_type: HttpMethod,
route: str,
function: FunctionInfo,
is_const: bool,
) -> None:
pass
def add_global_middleware(self, middleware_type: MiddlewareType, function: FunctionInfo) -> None:
pass
def add_middleware_route(
self,
middleware_type: MiddlewareType,
route: str,
function: FunctionInfo,
route_type: HttpMethod,
) -> None:
pass
def add_startup_handler(self, function: FunctionInfo) -> None:
pass
def add_shutdown_handler(self, function: FunctionInfo) -> None:
pass
def add_web_socket_route(
self,
route: str,
connect_route: FunctionInfo,
close_route: FunctionInfo,
message_route: FunctionInfo,
use_channel: bool,
) -> None:
pass
def start(self, socket: SocketHeld, workers: int, client_timeout: int, keep_alive_timeout: int) -> None:
pass
class WebSocketConnector:
"""
The WebSocketConnector object passed to the route handler.
Attributes:
id (str): The id of the client
query_params (QueryParams): The query parameters object
async_broadcast (Callable): The function to broadcast a message to all clients
async_send_to (Callable): The function to send a message to the client
sync_broadcast (Callable): The function to broadcast a message to all clients
sync_send_to (Callable): The function to send a message to the client
"""
id: str
query_params: QueryParams
async def async_broadcast(self, message: str) -> None:
"""
Broadcasts a message to all clients.
Args:
message (str): The message to broadcast
"""
pass
async def async_send_to(self, sender_id: str, message: str) -> None:
"""
Sends a message to a specific client.
Args:
sender_id (str): The id of the sender
message (str): The message to send
"""
pass
def sync_broadcast(self, message: str) -> None:
"""
Broadcasts a message to all clients.
Args:
message (str): The message to broadcast
"""
pass
def sync_send_to(self, sender_id: str, message: str) -> None:
"""
Sends a message to a specific client.
Args:
sender_id (str): The id of the sender
message (str): The message to send
"""
pass
def close(self) -> None:
"""
Closes the connection.
"""
pass
================================================
FILE: robyn/router.py
================================================
import inspect
import logging
from abc import ABC, abstractmethod
from functools import wraps
from types import CoroutineType
from typing import Callable, Dict, List, NamedTuple, Optional, Union, is_typeddict
from robyn import status_codes
from robyn._param_utils import QueryParamValidationError, parse_route_param_names, resolve_individual_params
from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError
from robyn.dependency_injection import DependencyMap
from robyn.jsonify import jsonify
from robyn.openapi import OpenAPI
from robyn.pydantic_support import (
PydanticBodyValidationError,
check_pydantic_installed_for_handler,
detect_pydantic_params,
serialize_pydantic_response,
validate_pydantic_body,
)
from robyn.responses import FileResponse, StreamingResponse
from robyn.robyn import FunctionInfo, Headers, HttpMethod, Identity, MiddlewareType, QueryParams, Request, Response, Url
from robyn.types import Body, Files, FormData, IPAddress, JsonBody, Method, PathParams
_logger = logging.getLogger(__name__)
def lower_http_method(method: HttpMethod):
return (str(method))[11:].lower()
class Route(NamedTuple):
route_type: HttpMethod
route: str
function: FunctionInfo
is_const: bool
auth_required: bool
openapi_name: str
openapi_tags: List[str]
class RouteMiddleware(NamedTuple):
middleware_type: MiddlewareType
route: str
function: FunctionInfo
route_type: HttpMethod
class GlobalMiddleware(NamedTuple):
middleware_type: MiddlewareType
function: FunctionInfo
class BaseRouter(ABC):
@abstractmethod
def add_route(*args) -> Union[Callable, CoroutineType, Dict]: ...
class Router(BaseRouter):
def __init__(self) -> None:
super().__init__()
self.routes: List[Route] = []
def _format_tuple_response(self, res: tuple) -> Response:
if len(res) != 3:
raise ValueError("Tuple should have 3 elements")
description, headers, status_code = res
description = self._format_response(description).description
new_headers: Headers = Headers(headers)
if new_headers.contains("Content-Type"):
headers.set("Content-Type", new_headers.get("Content-Type"))
return Response(
status_code=status_code,
headers=headers,
description=description,
)
def _format_response(
self,
res: Union[Dict, List, Response, StreamingResponse, bytes, tuple, str],
) -> Union[Response, StreamingResponse]:
if isinstance(res, Response):
return res
if isinstance(res, StreamingResponse):
return res
pydantic_json = serialize_pydantic_response(res)
if pydantic_json is not None:
return Response(
status_code=status_codes.HTTP_200_OK,
headers=Headers({"Content-Type": "application/json"}),
description=pydantic_json,
)
if isinstance(res, (dict, list)):
return Response(
status_code=status_codes.HTTP_200_OK,
headers=Headers({"Content-Type": "application/json"}),
description=jsonify(res),
)
if isinstance(res, FileResponse):
response: Response = Response(
status_code=res.status_code,
headers=res.headers,
description=res.file_path,
)
response.file_path = res.file_path
return response
if isinstance(res, bytes):
return Response(
status_code=status_codes.HTTP_200_OK,
headers=Headers({"Content-Type": "application/octet-stream"}),
description=res,
)
if isinstance(res, tuple):
return self._format_tuple_response(tuple(res))
return Response(
status_code=status_codes.HTTP_200_OK,
headers=Headers({"Content-Type": "text/plain"}),
description=str(res).encode("utf-8"),
)
def add_route( # type: ignore
self,
route_type: HttpMethod,
endpoint: str,
handler: Callable,
is_const: bool,
auth_required: bool,
openapi_name: str,
openapi_tags: List[str],
exception_handler: Optional[Callable],
injected_dependencies: dict,
) -> Union[Callable, CoroutineType]:
# Pre-compute handler signature ONCE at registration time.
# This avoids calling inspect.signature() on every request.
route_param_names = parse_route_param_names(endpoint)
handler_params = inspect.signature(handler).parameters
handler_param_names = set(handler_params.keys())
unused_route_params = route_param_names - handler_param_names
if unused_route_params:
_logger.warning(
"Route '%s' declares path params %s but handler '%s' doesn't use them",
endpoint,
unused_route_params,
handler.__name__,
)
# Detect Pydantic model params once at registration (zero cost if not used)
pydantic_params = detect_pydantic_params(handler_params)
check_pydantic_installed_for_handler(handler, pydantic_params)
if pydantic_params and route_type in (HttpMethod.GET, HttpMethod.HEAD):
_logger.warning(
"Handler '%s' on %s '%s' uses Pydantic body parameter(s) %s, but %s requests typically do not carry a request body",
handler.__name__,
route_type.name,
endpoint,
list(pydantic_params.keys()),
route_type.name,
)
def wrapped_handler(*args, **kwargs):
request = next(filter(lambda it: isinstance(it, Request), args), None)
if not request or (len(handler_params) == 1 and next(iter(handler_params)) is Request):
return handler(*args, **kwargs)
type_mapping = {
"request": Request,
"query_params": QueryParams,
"headers": Headers,
"path_params": PathParams,
"body": Body,
"method": Method,
"url": Url,
"form_data": FormData,
"files": Files,
"ip_addr": IPAddress,
"identity": Identity,
}
# Phase 1: Type-annotated request components
type_filtered_params = {}
for handler_param in iter(handler_params):
for type_name in type_mapping:
handler_param_type = handler_params[handler_param].annotation
handler_param_name = handler_params[handler_param].name
if handler_param_type is Request:
type_filtered_params[handler_param_name] = request
elif handler_param_type is type_mapping[type_name]:
type_filtered_params[handler_param_name] = getattr(request, type_name)
elif inspect.isclass(handler_param_type):
if issubclass(handler_param_type, JsonBody):
try:
type_filtered_params[handler_param_name] = request.json()
except ValueError as e:
return Response(
status_code=status_codes.HTTP_400_BAD_REQUEST,
headers=Headers({"Content-Type": "application/json"}),
description=jsonify({"error": f"Invalid JSON body: {e}"}),
)
elif issubclass(handler_param_type, Body):
type_filtered_params[handler_param_name] = getattr(request, "body")
elif issubclass(handler_param_type, QueryParams):
type_filtered_params[handler_param_name] = getattr(request, "query_params")
elif is_typeddict(handler_param_type):
try:
type_filtered_params[handler_param_name] = request.json()
except ValueError as e:
return Response(
status_code=status_codes.HTTP_400_BAD_REQUEST,
headers=Headers({"Content-Type": "application/json"}),
description=jsonify({"error": f"Invalid JSON body: {e}"}),
)
# Phase 1.5: Pydantic model body params (only runs if handler uses pydantic)
if pydantic_params:
for param_name, model_class in pydantic_params.items():
if param_name in type_filtered_params:
continue
validated, error = validate_pydantic_body(model_class, request.body)
if error is not None:
raise PydanticBodyValidationError(error)
type_filtered_params[param_name] = validated
# Phase 2: Reserved-name request components
request_components = {
"r": request,
"req": request,
"request": request,
"query_params": request.query_params,
"headers": request.headers,
"path_params": request.path_params,
"body": request.body,
"method": request.method,
"url": request.url,
"ip_addr": request.ip_addr,
"identity": request.identity,
"form_data": request.form_data,
"files": request.files,
"router_dependencies": injected_dependencies["router_dependencies"],
"global_dependencies": injected_dependencies["global_dependencies"],
**kwargs,
}
name_filtered_params = {k: v for k, v in request_components.items() if k in handler_params and k not in type_filtered_params}
filtered_params = dict(**type_filtered_params, **name_filtered_params)
# Phase 3: Individual path/query param resolution
unresolved_names = set(handler_params) - set(filtered_params)
if unresolved_names:
unresolved = {name: handler_params[name] for name in unresolved_names}
individual_params = resolve_individual_params(
unresolved,
request.query_params,
request.path_params,
route_param_names,
)
filtered_params.update(individual_params)
return handler(**filtered_params)
@wraps(handler)
async def async_inner_handler(*args, **kwargs):
try:
response = self._format_response(
await wrapped_handler(*args, **kwargs),
)
except QueryParamValidationError as err:
response = Response(
status_code=status_codes.HTTP_400_BAD_REQUEST,
headers=Headers({"Content-Type": "text/plain"}),
description=str(err),
)
except PydanticBodyValidationError as err:
response = Response(
status_code=status_codes.HTTP_422_UNPROCESSABLE_ENTITY,
headers=Headers({"Content-Type": "application/json"}),
description=jsonify(err.error_detail),
)
except Exception as err:
if exception_handler is None:
raise
response = self._format_response(
exception_handler(err),
)
return response
@wraps(handler)
def inner_handler(*args, **kwargs):
try:
response = self._format_response(
wrapped_handler(*args, **kwargs),
)
except QueryParamValidationError as err:
response = Response(
status_code=status_codes.HTTP_400_BAD_REQUEST,
headers=Headers({"Content-Type": "text/plain"}),
description=str(err),
)
except PydanticBodyValidationError as err:
response = Response(
status_code=status_codes.HTTP_422_UNPROCESSABLE_ENTITY,
headers=Headers({"Content-Type": "application/json"}),
description=jsonify(err.error_detail),
)
except Exception as err:
if exception_handler is None:
raise
response = self._format_response(
exception_handler(err),
)
return response
params = dict(handler_params)
new_injected_dependencies = {}
for dependency in injected_dependencies:
if dependency in params:
new_injected_dependencies[dependency] = injected_dependencies[dependency]
else:
_logger.debug(f"Dependency {dependency} is not used in the handler {handler.__name__}")
if inspect.iscoroutinefunction(handler):
function = FunctionInfo(
async_inner_handler,
True,
len(params),
params,
new_injected_dependencies,
)
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
return async_inner_handler
else:
function = FunctionInfo(
inner_handler,
False,
len(params),
params,
new_injected_dependencies,
)
self.routes.append(Route(route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags))
return inner_handler
def prepare_routes_openapi(self, openapi: OpenAPI, included_routers: List) -> None:
for route in self.routes:
openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)
# TODO! after include_routes does not immediately merge all the routes
# for router in included_routers:
# for route in router:
# openapi.add_openapi_path_obj(lower_http_method(route.route_type), route.route, route.openapi_name, route.openapi_tags, route.function.handler)
def get_routes(self) -> List[Route]:
return self.routes
class MiddlewareRouter(BaseRouter):
def __init__(self, dependencies: DependencyMap = DependencyMap()) -> None:
super().__init__()
self.global_middlewares: List[GlobalMiddleware] = []
self.route_middlewares: List[RouteMiddleware] = []
self.authentication_handler: Optional[AuthenticationHandler] = None
self.dependencies = dependencies
def set_authentication_handler(self, authentication_handler: AuthenticationHandler):
self.authentication_handler = authentication_handler
def add_route( # type: ignore
self,
middleware_type: MiddlewareType,
endpoint: str,
route_type: HttpMethod,
handler: Callable,
injected_dependencies: dict,
) -> Callable:
params = dict(inspect.signature(handler).parameters)
new_injected_dependencies = {}
for dependency in injected_dependencies:
if dependency in params:
new_injected_dependencies[dependency] = injected_dependencies[dependency]
else:
_logger.debug(f"Dependency {dependency} is not used in the middleware handler {handler.__name__}")
function = FunctionInfo(
handler,
inspect.iscoroutinefunction(handler),
len(params),
params,
new_injected_dependencies,
)
self.route_middlewares.append(RouteMiddleware(middleware_type, endpoint, function, route_type))
return handler
def add_auth_middleware(self, endpoint: str, route_type: HttpMethod):
"""
This method adds an authentication middleware to the specified endpoint.
"""
injected_dependencies: dict = {}
def decorator(handler):
@wraps(handler)
def inner_handler(request: Request, *args):
if not self.authentication_handler:
raise AuthenticationNotConfiguredError()
identity = self.authentication_handler.authenticate(request)
if identity is None:
return self.authentication_handler.unauthorized_response
request.identity = identity
return request
self.add_route(
MiddlewareType.BEFORE_REQUEST,
endpoint,
route_type,
inner_handler,
injected_dependencies,
)
return inner_handler
return decorator
# These inner functions are basically a wrapper around the closure(decorator) being returned.
# They take a handler, convert it into a closure and return the arguments.
# Arguments are returned as they could be modified by the middlewares.
def add_middleware(self, middleware_type: MiddlewareType, endpoint: Optional[str]) -> Callable[..., None]:
"""
This method adds a middleware to the router.
Rules:
If endpoint is None, the middleware is added as a global middleware.
If endpoint is provided, the middleware is added to that specific endpoint.
Only None is supported for global middleware, empty string is not supported.
empty string or "/" is considered as root endpoint.
Args:
middleware_type: The type of middleware to add (before_request, after_request).
endpoint: The endpoint to add the middleware to. If None, the middleware is added as a global middleware.
Returns:
A decorator that takes a handler and adds it as a middleware.
"""
# no dependency injection here
injected_dependencies: dict = {}
def inner(handler):
@wraps(handler)
async def async_inner_handler(*args, **kwargs):
return await handler(*args, **kwargs)
@wraps(handler)
def inner_handler(*args, **kwargs):
return handler(*args, **kwargs)
if endpoint is not None:
if inspect.iscoroutinefunction(handler):
self.add_route(
middleware_type,
endpoint,
HttpMethod.GET,
async_inner_handler,
injected_dependencies,
)
else:
self.add_route(middleware_type, endpoint, HttpMethod.GET, inner_handler, injected_dependencies)
else:
params = dict(inspect.signature(handler).parameters)
if inspect.iscoroutinefunction(handler):
self.global_middlewares.append(
GlobalMiddleware(
middleware_type,
FunctionInfo(
async_inner_handler,
True,
len(params),
params,
injected_dependencies,
),
)
)
else:
self.global_middlewares.append(
GlobalMiddleware(
middleware_type,
FunctionInfo(
inner_handler,
False,
len(params),
params,
injected_dependencies,
),
)
)
return inner
def get_route_middlewares(self) -> List[RouteMiddleware]:
return self.route_middlewares
def get_global_middlewares(self) -> List[GlobalMiddleware]:
return self.global_middlewares
class WebSocketRouter(BaseRouter):
def __init__(self) -> None:
super().__init__()
self.routes: Dict[str, dict] = {}
def add_route(self, endpoint: str, handlers: dict) -> None: # type: ignore
self.routes[endpoint] = handlers
def get_routes(self) -> Dict[str, dict]:
return self.routes
================================================
FILE: robyn/scaffold/mongo/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/mongo/app.py
================================================
from pymongo import MongoClient
from robyn import Robyn
app = Robyn(__file__)
db = MongoClient("URL HERE")
users = db.users # define a collection
@app.get("/")
def index():
return "Hello World!"
# create a route
@app.get("/users")
async def get_users():
all_users = await users.find().to_list(length=None)
return {"users": all_users}
# create a route to add a new user
@app.post("/users")
async def add_user(request):
user_data = await request.json()
result = await users.insert_one(user_data)
return {"success": True, "inserted_id": str(result.inserted_id)}
# create a route to fetch a single user by ID
@app.get("/users/{user_id}")
async def get_user(request):
user_id = request.path_params["user_id"]
user = await users.find_one({"_id": user_id})
if user:
return user
else:
return {"error": "User not found"}, 404
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/mongo/requirements.txt
================================================
robyn
pymongo
================================================
FILE: robyn/scaffold/no-db/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/no-db/app.py
================================================
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
def index():
return "Hello World!"
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/no-db/requirements.txt
================================================
robyn
================================================
FILE: robyn/scaffold/postgres/Dockerfile
================================================
# ---- Build the PostgreSQL Base ----
FROM postgres:latest AS postgres-base
ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=password
ENV POSTGRES_DB=postgresDB
# ---- Build the Python App ----
FROM python:3.11-bookworm
# Install supervisor
RUN apt-get update && apt-get install -y supervisor
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
# Copy PostgreSQL binaries from the first stage
COPY --from=postgres-base /usr/local/bin /usr/local/bin
COPY --from=postgres-base /usr/lib/postgresql /usr/lib/postgresql
COPY --from=postgres-base /usr/share/postgresql /usr/share/postgresql
COPY --from=postgres-base /var/lib/postgresql /var/lib/postgresql
# Add supervisord config
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 8080 5455
CMD ["/usr/bin/supervisord"]
================================================
FILE: robyn/scaffold/postgres/app.py
================================================
import psycopg2
from robyn import Robyn
DB_NAME = "postgresDB"
DB_HOST = "localhost"
DB_USER = "postgres"
DB_PASS = "password"
DB_PORT = "5455"
conn = psycopg2.connect(database=DB_NAME, host=DB_HOST, user=DB_USER, password=DB_PASS, port=DB_PORT)
app = Robyn(__file__)
# create a route to fetch all users
@app.get("/users")
def get_users():
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
all_users = cursor.fetchall()
return {"users": all_users}
@app.get("/")
def index():
return "Hello World!"
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/postgres/requirements.txt
================================================
robyn
psycopg2; platform_system=="Windows"
psycopg2-binary; platform_system!="Windows"
================================================
FILE: robyn/scaffold/postgres/supervisord.conf
================================================
[supervisord]
nodaemon=true
[program:python-app]
command=python3 /workspace/app.py --log-level=DEBUG
autostart=true
autorestart=true
redirect_stderr=true
[program:postgres]
command=postgres -c 'config_file=/etc/postgresql/postgresql.conf'
autostart=true
autorestart=true
redirect_stderr=true
================================================
FILE: robyn/scaffold/prisma/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
RUN python3 -m prisma generate
RUN python3 -m prisma migrate dev --name init
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/prisma/app.py
================================================
from prisma import Prisma
from prisma.models import User
from robyn import Robyn
app = Robyn(__file__)
prisma = Prisma(auto_register=True)
@app.startup_handler
async def startup_handler() -> None:
await prisma.connect()
@app.shutdown_handler
async def shutdown_handler() -> None:
if prisma.is_connected():
await prisma.disconnect()
@app.get("/")
async def h():
user = await User.prisma().create(
data={
"name": "Robert",
},
)
return user.json(indent=2)
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/prisma/requirements.txt
================================================
robyn
prisma
================================================
FILE: robyn/scaffold/prisma/schema.prisma
================================================
datasource db {
provider = "sqlite"
url = "file:dev.db"
}
generator py {
provider = "prisma-client-py"
}
model User {
id String @id @default(cuid())
name String
}
================================================
FILE: robyn/scaffold/sqlalchemy/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/sqlalchemy/__init__.py
================================================
================================================
FILE: robyn/scaffold/sqlalchemy/app.py
================================================
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
def index():
return "Hello World!"
if __name__ == "__main__":
# create a configured "Session" class
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/sqlalchemy/models.py
================================================
from sqlalchemy import Boolean, Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
engine = create_engine("sqlite+pysqlite:///:memory:", echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
if __name__ == "__main__":
Base.metadata.create_all(bind=engine)
================================================
FILE: robyn/scaffold/sqlalchemy/requirements.txt
================================================
robyn
SQLAlchemy
================================================
FILE: robyn/scaffold/sqlite/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/sqlite/app.py
================================================
import sqlite3
from robyn import Robyn
app = Robyn(__file__)
@app.get("/")
def index():
# your db name
conn = sqlite3.connect("example.db")
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS test")
cur.execute("CREATE TABLE test(column_1, column_2)")
res = cur.execute("SELECT name FROM sqlite_master")
th = res.fetchone()
table_name = th[0]
return f"Hello World! {table_name}"
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/sqlite/requirements.txt
================================================
robyn
================================================
FILE: robyn/scaffold/sqlmodel/Dockerfile
================================================
FROM python:3.11-bookworm
WORKDIR /workspace
COPY . .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
EXPOSE 8080
CMD ["python3", "app.py", "--log-level=DEBUG"]
================================================
FILE: robyn/scaffold/sqlmodel/app.py
================================================
from models import Hero
from sqlmodel import Session, SQLModel, create_engine, select
from robyn import Robyn
app = Robyn(__file__)
engine = create_engine("sqlite:///database.db", echo=True)
@app.get("/")
def index():
return "Hello World"
@app.get("/create")
def create():
SQLModel.metadata.create_all(bind=engine)
return "created tables"
@app.get("/insert")
def insert():
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
return "inserted"
@app.get("/select")
def get_data():
with Session(engine) as session:
statement = select(Hero).where(Hero.name == "Spider-Boy")
hero = session.exec(statement).first()
return hero
if __name__ == "__main__":
# create a configured "Session" class
app.start(host="0.0.0.0", port=8080)
================================================
FILE: robyn/scaffold/sqlmodel/models.py
================================================
from typing import Optional
from sqlmodel import Field, SQLModel
class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
secret_name: str
age: Optional[int] = None
================================================
FILE: robyn/scaffold/sqlmodel/requirements.txt
================================================
robyn
sqlmodel
================================================
FILE: robyn/status_codes.py
================================================
"""
HTTP codes
See HTTP Status Code Registry:
https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
And RFC 2324 - https://tools.ietf.org/html/rfc2324
"""
__all__ = (
"HTTP_100_CONTINUE",
"HTTP_101_SWITCHING_PROTOCOLS",
"HTTP_102_PROCESSING",
"HTTP_103_EARLY_HINTS",
"HTTP_200_OK",
"HTTP_201_CREATED",
"HTTP_202_ACCEPTED",
"HTTP_203_NON_AUTHORITATIVE_INFORMATION",
"HTTP_204_NO_CONTENT",
"HTTP_205_RESET_CONTENT",
"HTTP_206_PARTIAL_CONTENT",
"HTTP_207_MULTI_STATUS",
"HTTP_208_ALREADY_REPORTED",
"HTTP_226_IM_USED",
"HTTP_300_MULTIPLE_CHOICES",
"HTTP_301_MOVED_PERMANENTLY",
"HTTP_302_FOUND",
"HTTP_303_SEE_OTHER",
"HTTP_304_NOT_MODIFIED",
"HTTP_305_USE_PROXY",
"HTTP_306_RESERVED",
"HTTP_307_TEMPORARY_REDIRECT",
"HTTP_308_PERMANENT_REDIRECT",
"HTTP_400_BAD_REQUEST",
"HTTP_401_UNAUTHORIZED",
"HTTP_402_PAYMENT_REQUIRED",
"HTTP_403_FORBIDDEN",
"HTTP_404_NOT_FOUND",
"HTTP_405_METHOD_NOT_ALLOWED",
"HTTP_406_NOT_ACCEPTABLE",
"HTTP_407_PROXY_AUTHENTICATION_REQUIRED",
"HTTP_408_REQUEST_TIMEOUT",
"HTTP_409_CONFLICT",
"HTTP_410_GONE",
"HTTP_411_LENGTH_REQUIRED",
"HTTP_412_PRECONDITION_FAILED",
"HTTP_413_REQUEST_ENTITY_TOO_LARGE",
"HTTP_414_REQUEST_URI_TOO_LONG",
"HTTP_415_UNSUPPORTED_MEDIA_TYPE",
"HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE",
"HTTP_417_EXPECTATION_FAILED",
"HTTP_418_IM_A_TEAPOT",
"HTTP_421_MISDIRECTED_REQUEST",
"HTTP_422_UNPROCESSABLE_ENTITY",
"HTTP_423_LOCKED",
"HTTP_424_FAILED_DEPENDENCY",
"HTTP_425_TOO_EARLY",
"HTTP_426_UPGRADE_REQUIRED",
"HTTP_428_PRECONDITION_REQUIRED",
"HTTP_429_TOO_MANY_REQUESTS",
"HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE",
"HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS",
"HTTP_500_INTERNAL_SERVER_ERROR",
"HTTP_501_NOT_IMPLEMENTED",
"HTTP_502_BAD_GATEWAY",
"HTTP_503_SERVICE_UNAVAILABLE",
"HTTP_504_GATEWAY_TIMEOUT",
"HTTP_505_HTTP_VERSION_NOT_SUPPORTED",
"HTTP_506_VARIANT_ALSO_NEGOTIATES",
"HTTP_507_INSUFFICIENT_STORAGE",
"HTTP_508_LOOP_DETECTED",
"HTTP_510_NOT_EXTENDED",
"HTTP_511_NETWORK_AUTHENTICATION_REQUIRED",
)
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_102_PROCESSING = 102
HTTP_103_EARLY_HINTS = 103
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207
HTTP_208_ALREADY_REPORTED = 208
HTTP_226_IM_USED = 226
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_308_PERMANENT_REDIRECT = 308
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_418_IM_A_TEAPOT = 418
HTTP_421_MISDIRECTED_REQUEST = 421
HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424
HTTP_425_TOO_EARLY = 425
HTTP_426_UPGRADE_REQUIRED = 426
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
HTTP_507_INSUFFICIENT_STORAGE = 507
HTTP_508_LOOP_DETECTED = 508
HTTP_510_NOT_EXTENDED = 510
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
================================================
FILE: robyn/swagger.html
================================================
Robyn OpenAPI Docs
================================================
FILE: robyn/templating.py
================================================
from abc import ABC, abstractmethod
from jinja2 import Environment, FileSystemLoader
from robyn import status_codes
from .robyn import Headers, Response
class TemplateInterface(ABC):
def __init__(self): ...
@abstractmethod
def render_template(self, *args, **kwargs) -> Response: ...
class JinjaTemplate(TemplateInterface):
def __init__(self, directory, encoding="utf-8", followlinks=False):
self.env = Environment(loader=FileSystemLoader(searchpath=directory, encoding=encoding, followlinks=followlinks))
def render_template(self, template_name, **kwargs) -> Response:
rendered_template = self.env.get_template(template_name).render(**kwargs)
return Response(
status_code=status_codes.HTTP_200_OK,
description=rendered_template,
headers=Headers({"Content-Type": "text/html; charset=utf-8"}),
)
__all__ = ["TemplateInterface", "JinjaTemplate"]
================================================
FILE: robyn/types.py
================================================
from dataclasses import dataclass
from typing import Dict, NewType, Optional, TypedDict
from robyn._param_utils import QueryParamValidationError
@dataclass
class Directory:
route: str
directory_path: str
show_files_listing: bool
index_file: Optional[str]
def as_list(self):
return [
self.route,
self.directory_path,
self.show_files_listing,
self.index_file,
]
PathParams = NewType("PathParams", Dict[str, str])
Method = NewType("Method", str)
FormData = NewType("FormData", Dict[str, str])
Files = NewType("Files", Dict[str, bytes])
IPAddress = NewType("IPAddress", Optional[str])
class JSONResponse(TypedDict):
"""
A type alias for openapi response bodies. This class should be inherited by the response class type definition.
"""
pass
class Body:
"""
A type alias for openapi request bodies. This class should be inherited by the request body class annotation.
"""
pass
class JsonBody:
"""
A type alias for JSON request bodies. When used as a parameter type annotation,
the handler receives the parsed JSON (dict) from request.json() and the OpenAPI
docs will show a generic JSON request body input.
Can be subclassed with annotations to provide a typed schema in the OpenAPI docs:
class MyBody(JsonBody):
name: str
age: int
@app.post("/users")
def create_user(request: Request, data: MyBody):
# data is the parsed JSON dict
...
.. note::
The JSON body is parsed via ``request.json()`` during parameter
resolution, *before* the handler is invoked. If the request body is
not valid JSON, a 400 Bad Request response is returned automatically
with a JSON error message (e.g., ``{"error": "Invalid JSON body: ..."}``)
and the handler is never called. Because parsing happens before
handler invocation, the error **cannot** be caught with a try/except
inside the handler.
If you need custom error handling for malformed JSON, accept the raw
body instead (e.g., ``body: Body``) and call ``request.json()``
yourself inside a try/except block.
"""
pass
__all__ = ["JSONResponse", "Body", "JsonBody", "QueryParamValidationError", "Directory", "PathParams", "Method", "FormData", "Files", "IPAddress"]
================================================
FILE: robyn/ws.py
================================================
from __future__ import annotations
import asyncio
import inspect
import logging
import orjson
from robyn._param_utils import QueryParamValidationError, resolve_individual_params
from robyn.robyn import FunctionInfo, QueryParams, WebSocketConnector
_logger = logging.getLogger(__name__)
class WebSocketDisconnect(Exception):
"""Exception raised when a WebSocket connection is disconnected."""
def __init__(self, code: int = 1000, reason: str = ""):
self.code = code
self.reason = reason
super().__init__(f"WebSocket disconnected with code {code}: {reason}")
class WebSocketAdapter:
"""
Modern WebSocket interface backed by Rust channels.
Wraps a WebSocketConnector and a Rust WebSocketChannel to provide
a clean async API for WebSocket handlers.
"""
def __init__(self, websocket_connector: WebSocketConnector, channel=None):
self._connector = websocket_connector
self._channel = channel
async def receive_text(self) -> str:
"""Receive the next text message. Blocks until a message arrives.
Raises WebSocketDisconnect when the connection is closed."""
if self._channel is None:
raise WebSocketDisconnect(reason="No message channel available")
result = await self._channel.receive()
if result is None:
raise WebSocketDisconnect()
return result
async def receive_bytes(self) -> bytes:
"""Receive binary data (decoded from text)."""
text = await self.receive_text()
return text.encode("utf-8")
async def receive_json(self):
"""Receive and decode JSON data.
Raises WebSocketDisconnect when the connection is closed."""
text = await self.receive_text()
return orjson.loads(text)
async def send_text(self, data: str):
"""Send text data to this WebSocket client."""
await self._connector.async_send_to(self._connector.id, data)
async def send_bytes(self, data: bytes):
"""Send binary data (as text) to this WebSocket client."""
await self._connector.async_send_to(self._connector.id, data.decode("utf-8"))
async def send_json(self, data):
"""Send JSON data to this WebSocket client."""
await self.send_text(orjson.dumps(data).decode())
async def broadcast(self, data: str):
"""Broadcast text data to all connected WebSocket clients on this endpoint."""
await self._connector.async_broadcast(data)
async def close(self):
"""Close the WebSocket connection."""
self._connector.close()
@property
def id(self) -> str:
"""WebSocket connection ID."""
return self._connector.id
@property
def query_params(self):
"""Access query parameters from the connection URL."""
return self._connector.query_params
# Global storage for connection state (per-connection queues and tasks)
_connection_tasks: dict[str, asyncio.Task] = {}
def create_websocket_decorator(app_instance):
"""
Factory function to create a websocket decorator for an app instance.
Returns a decorator that registers a modern WebSocket endpoint
backed by Rust channels.
"""
def websocket(endpoint: str):
"""
Modern WebSocket decorator.
Usage:
@app.websocket("/ws")
async def handler(websocket):
while True:
msg = await websocket.receive_text()
await websocket.send_text(f"Echo: {msg}")
@handler.on_connect
def on_connect(websocket):
return "Welcome!"
@handler.on_close
def on_close(websocket):
return "Goodbye"
"""
def decorator(handler):
_on_connect_fn = None
_on_close_fn = None
def _resolve_ws_params(func_params, adapter):
"""
Resolve all handler params beyond the first positional (websocket).
Handles:
- global_dependencies / router_dependencies (DI)
- query_params (whole QueryParams object, by name or type annotation)
- individual query params (everything else, with type coercion)
"""
injected = app_instance.dependencies.get_dependency_map(app_instance)
resolved = {}
unresolved = {}
for idx, (param_name, param) in enumerate(func_params.items()):
# Skip the websocket adapter (first positional arg, passed separately)
if idx == 0 or param.annotation is WebSocketAdapter:
continue
# DI: global_dependencies
if param_name == "global_dependencies":
resolved[param_name] = injected.get("global_dependencies", {})
continue
# DI: router_dependencies
if param_name == "router_dependencies":
resolved[param_name] = injected.get("router_dependencies", {})
continue
# Whole QueryParams object (by type annotation or reserved name)
if param.annotation is QueryParams or param_name == "query_params":
resolved[param_name] = adapter.query_params
continue
# Everything else: individual query param
unresolved[param_name] = param
if unresolved:
individual = resolve_individual_params(
unresolved,
adapter.query_params,
path_params=None, # WebSocket has no path params yet
route_param_names=set(),
)
resolved.update(individual)
return resolved
# --- Connect handler (called by Rust on connection open) ---
async def connect_handler(ws):
"""Internal connect handler called by Rust.
Creates the adapter, starts the user's handler task,
and calls the user's on_connect callback."""
conn_id = ws.id
channel = ws.message_channel
# Create the adapter with the Rust channel
adapter = WebSocketAdapter(ws, channel)
# Build resolved kwargs for the main handler
try:
handler_params = inspect.signature(handler).parameters
handler_kwargs = _resolve_ws_params(handler_params, adapter)
except QueryParamValidationError as e:
_logger.warning("WebSocket connection rejected for %s: %s", endpoint, e.detail)
return f"Error: {e.detail}"
# Start the user's handler as a long-running asyncio task
async def _run_handler():
try:
await handler(adapter, **handler_kwargs)
except WebSocketDisconnect:
pass
except ConnectionError:
_logger.debug("Connection lost in WebSocket handler for %s", endpoint, exc_info=True)
except Exception:
_logger.exception("Error in WebSocket handler for %s", endpoint)
finally:
_connection_tasks.pop(conn_id, None)
task = asyncio.create_task(_run_handler())
_connection_tasks[conn_id] = task
# Call user's on_connect if defined
if _on_connect_fn is not None:
connect_adapter = WebSocketAdapter(ws, channel)
try:
connect_params = inspect.signature(_on_connect_fn).parameters
connect_kwargs = _resolve_ws_params(connect_params, connect_adapter)
except QueryParamValidationError as e:
_logger.warning("WebSocket on_connect rejected for %s: %s", endpoint, e.detail)
task = _connection_tasks.pop(conn_id, None)
if task is not None and not task.done():
task.cancel()
return f"Error: {e.detail}"
if asyncio.iscoroutinefunction(_on_connect_fn):
result = await _on_connect_fn(connect_adapter, **connect_kwargs)
else:
result = _on_connect_fn(connect_adapter, **connect_kwargs)
return result
return None
# --- Message handler (dummy for new-style; Rust pushes to channel instead) ---
async def message_handler(ws, msg):
"""Dummy message handler. In channel mode, Rust pushes messages
directly to the channel and never calls this."""
return None
# --- Close handler (called by Rust on connection close) ---
async def close_handler(ws):
"""Internal close handler called by Rust.
Waits for the handler task to finish and calls on_close."""
conn_id = ws.id
# Wait for the handler task to finish (it should exit because
# the channel was closed by Rust, triggering WebSocketDisconnect)
task = _connection_tasks.pop(conn_id, None)
if task is not None:
try:
await asyncio.wait_for(task, timeout=5.0)
except (asyncio.TimeoutError, asyncio.CancelledError):
if not task.done():
task.cancel()
except Exception:
_logger.debug("Unexpected error while awaiting handler task for %s", conn_id, exc_info=True)
if not task.done():
task.cancel()
# Call user's on_close if defined
if _on_close_fn is not None:
close_adapter = WebSocketAdapter(ws, None)
try:
close_params = inspect.signature(_on_close_fn).parameters
close_kwargs = _resolve_ws_params(close_params, close_adapter)
except QueryParamValidationError as e:
_logger.warning("WebSocket on_close param error for %s: %s", endpoint, e.detail)
return None
if asyncio.iscoroutinefunction(_on_close_fn):
result = await _on_close_fn(close_adapter, **close_kwargs)
else:
result = _on_close_fn(close_adapter, **close_kwargs)
return result
return None
# --- Build FunctionInfo objects for Rust ---
handlers = {}
# Connect handler FunctionInfo
connect_params = dict(inspect.signature(connect_handler).parameters)
handlers["connect"] = FunctionInfo(
connect_handler,
True, # is_async
len(connect_params),
connect_params,
{}, # no kwargs needed - DI handled in Python
)
# Message handler FunctionInfo (dummy, won't be called in channel mode)
message_params = dict(inspect.signature(message_handler).parameters)
handlers["message"] = FunctionInfo(
message_handler,
True,
len(message_params),
message_params,
{},
)
# Close handler FunctionInfo
close_params = dict(inspect.signature(close_handler).parameters)
handlers["close"] = FunctionInfo(
close_handler,
True,
len(close_params),
close_params,
{},
)
# --- Decorator methods for on_connect / on_close ---
def add_on_connect(connect_fn):
nonlocal _on_connect_fn
_on_connect_fn = connect_fn
return connect_fn
def add_on_close(close_fn):
nonlocal _on_close_fn
_on_close_fn = close_fn
return close_fn
handler.on_connect = add_on_connect
handler.on_close = add_on_close
# Register with the app
app_instance.add_web_socket(endpoint, handlers)
return handler
return decorator
return websocket
================================================
FILE: scripts/format.sh
================================================
#!/bin/bash
set -x
ruff format robyn integration_tests docs_src
================================================
FILE: scripts/release.sh
================================================
#!/bin/bash
set -x
maturin develop
poetry lock
================================================
FILE: setup.py
================================================
import sys
sys.stderr.write(
"""
===============================
Unsupported installation method
===============================
robyn doesn't support installation with `python setup.py install`.
Please use `python -m pip install .` instead.
"""
)
sys.exit(1)
================================================
FILE: src/asyncio.rs
================================================
use pyo3::{prelude::*, sync::PyOnceLock};
use std::convert::Into;
static CONTEXTVARS: PyOnceLock> = PyOnceLock::new();
static CONTEXT: PyOnceLock> = PyOnceLock::new();
fn contextvars(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
Ok(CONTEXTVARS
.get_or_try_init(py, || py.import("contextvars").map(Into::into))?
.bind(py))
}
#[allow(dead_code)]
pub(crate) fn empty_context(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
Ok(CONTEXT
.get_or_try_init(py, || {
contextvars(py)?
.getattr("Context")?
.call0()
.map(std::convert::Into::into)
})?
.bind(py))
}
#[inline(always)]
pub(crate) fn copy_context(py: Python) -> PyResult> {
unsafe {
let ptr = pyo3::ffi::PyContext_CopyCurrent();
Ok(Bound::from_owned_ptr_or_err(py, ptr)?.unbind())
}
}
================================================
FILE: src/blocking.rs
================================================
use crossbeam_channel as channel;
use pyo3::prelude::*;
use std::{
sync::{atomic, Arc},
thread, time,
};
pub(crate) struct BlockingTask {
inner: Box,
}
impl BlockingTask {
#[inline]
pub fn new(inner: T) -> BlockingTask
where
T: FnOnce(Python) + Send + 'static,
{
Self {
inner: Box::new(inner),
}
}
#[inline(always)]
pub fn run(self, py: Python) {
(self.inner)(py);
}
}
pub(crate) enum BlockingRunner {
Empty,
Mono(BlockingRunnerMono),
Pool(BlockingRunnerPool),
}
impl BlockingRunner {
pub fn new(max_threads: usize, idle_timeout: u64) -> Self {
match max_threads {
0 => Self::Empty,
1 => Self::Mono(BlockingRunnerMono::new()),
_ => Self::Pool(BlockingRunnerPool::new(max_threads, idle_timeout)),
}
}
#[inline]
pub fn run(&self, task: T) -> Result<(), channel::SendError>
where
T: FnOnce(Python) + Send + 'static,
{
match self {
Self::Mono(runner) => runner.run(task),
Self::Pool(runner) => runner.run(task),
Self::Empty => Ok(()),
}
}
}
pub(crate) struct BlockingRunnerMono {
queue: channel::Sender,
}
impl BlockingRunnerMono {
pub fn new() -> Self {
let (qtx, qrx) = channel::unbounded();
let ret = Self { queue: qtx };
thread::spawn(move || blocking_worker(qrx));
ret
}
#[inline]
pub fn run(&self, task: T) -> Result<(), channel::SendError>
where
T: FnOnce(Python) + Send + 'static,
{
self.queue.send(BlockingTask::new(task))
}
}
pub(crate) struct BlockingRunnerPool {
birth: time::Instant,
queue: channel::Sender,
tq: channel::Receiver,
threads: Arc,
tmax: usize,
idle_timeout: time::Duration,
spawning: atomic::AtomicBool,
spawn_tick: atomic::AtomicU64,
}
impl BlockingRunnerPool {
pub fn new(max_threads: usize, idle_timeout: u64) -> Self {
let (qtx, qrx) = channel::unbounded();
let ret = Self {
queue: qtx,
tq: qrx.clone(),
threads: Arc::new(1.into()),
tmax: max_threads,
birth: time::Instant::now(),
spawning: false.into(),
spawn_tick: 0.into(),
idle_timeout: time::Duration::from_secs(idle_timeout),
};
// always spawn the first thread
thread::spawn(move || blocking_worker(qrx));
ret
}
#[inline(always)]
fn spawn_thread(&self) {
let tick = self.birth.elapsed().as_micros() as u64;
if tick - self.spawn_tick.load(atomic::Ordering::Relaxed) < 350 {
return;
}
if self
.spawning
.compare_exchange(
false,
true,
atomic::Ordering::Relaxed,
atomic::Ordering::Relaxed,
)
.is_err()
{
return;
}
let queue = self.tq.clone();
let tcount = self.threads.clone();
let timeout = self.idle_timeout;
thread::spawn(move || {
tcount.fetch_add(1, atomic::Ordering::Release);
blocking_worker_idle(queue, timeout);
tcount.fetch_sub(1, atomic::Ordering::Release);
});
self.spawn_tick.store(
self.birth.elapsed().as_micros() as u64,
atomic::Ordering::Relaxed,
);
self.spawning.store(false, atomic::Ordering::Relaxed);
}
#[inline]
pub fn run(&self, task: T) -> Result<(), channel::SendError>
where
T: FnOnce(Python) + Send + 'static,
{
self.queue.send(BlockingTask::new(task))?;
if self.queue.len() > 1 && self.threads.load(atomic::Ordering::Acquire) < self.tmax {
self.spawn_thread();
}
Ok(())
}
}
fn blocking_worker(queue: channel::Receiver) {
Python::attach(|py| {
while let Ok(task) = py.detach(|| queue.recv()) {
task.run(py);
}
});
}
fn blocking_worker_idle(queue: channel::Receiver, timeout: time::Duration) {
Python::attach(|py| {
while let Ok(task) = py.detach(|| queue.recv_timeout(timeout)) {
task.run(py);
}
});
}
================================================
FILE: src/callbacks.rs
================================================
use pyo3::{exceptions::PyStopIteration, prelude::*, IntoPyObjectExt};
use std::sync::{atomic, Arc, OnceLock, RwLock};
use tokio::sync::Notify;
use crate::conversion::FutureResultToPy;
#[pyclass(frozen, freelist = 128, module = "robyn._robyn")]
pub(crate) struct PyEmptyAwaitable;
#[pymethods]
impl PyEmptyAwaitable {
fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __next__(&self) -> Option<()> {
None
}
}
#[pyclass(frozen, module = "robyn._robyn")]
pub(crate) struct PyDoneAwaitable {
result: PyResult>,
}
impl PyDoneAwaitable {
pub(crate) fn new(result: PyResult>) -> Self {
Self { result }
}
}
#[pymethods]
impl PyDoneAwaitable {
fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __next__(&self, py: Python) -> PyResult> {
self.result
.as_ref()
.map_err(|v| v.clone_ref(py))
.map(|v| Err(PyStopIteration::new_err(v.clone_ref(py))))?
}
}
#[pyclass(frozen, module = "robyn._robyn")]
pub(crate) struct PyErrAwaitable {
err: PyErr,
}
impl PyErrAwaitable {
pub(crate) fn new(err: PyErr) -> Self {
Self { err }
}
}
#[pymethods]
impl PyErrAwaitable {
fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __next__(&self, py: Python) -> PyResult<()> {
Err(self.err.clone_ref(py))
}
}
#[pyclass(frozen, module = "robyn._robyn")]
pub(crate) struct PyIterAwaitable {
result: OnceLock>>,
}
impl PyIterAwaitable {
pub(crate) fn new() -> Self {
Self {
result: OnceLock::new(),
}
}
#[inline]
pub(crate) fn set_result(pyself: Py, py: Python, result: FutureResultToPy) {
_ = pyself
.get()
.result
.set(result.into_pyobject(py).map(Bound::unbind));
pyself.drop_ref(py);
}
}
#[pymethods]
impl PyIterAwaitable {
fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
pyself
}
fn __next__(&self, py: Python) -> PyResult