robyn 0.76.0__cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robyn/__init__.py +782 -0
- robyn/__main__.py +4 -0
- robyn/ai.py +308 -0
- robyn/argument_parser.py +129 -0
- robyn/authentication.py +96 -0
- robyn/cli.py +136 -0
- robyn/dependency_injection.py +71 -0
- robyn/env_populator.py +35 -0
- robyn/events.py +6 -0
- robyn/exceptions.py +32 -0
- robyn/jsonify.py +13 -0
- robyn/logger.py +80 -0
- robyn/mcp.py +461 -0
- robyn/openapi.py +448 -0
- robyn/processpool.py +226 -0
- robyn/py.typed +0 -0
- robyn/reloader.py +164 -0
- robyn/responses.py +208 -0
- robyn/robyn.cpython-314-x86_64-linux-gnu.so +0 -0
- robyn/robyn.pyi +562 -0
- robyn/router.py +426 -0
- robyn/scaffold/mongo/Dockerfile +12 -0
- robyn/scaffold/mongo/app.py +43 -0
- robyn/scaffold/mongo/requirements.txt +2 -0
- robyn/scaffold/no-db/Dockerfile +12 -0
- robyn/scaffold/no-db/app.py +12 -0
- robyn/scaffold/no-db/requirements.txt +1 -0
- robyn/scaffold/postgres/Dockerfile +32 -0
- robyn/scaffold/postgres/app.py +31 -0
- robyn/scaffold/postgres/requirements.txt +3 -0
- robyn/scaffold/postgres/supervisord.conf +14 -0
- robyn/scaffold/prisma/Dockerfile +15 -0
- robyn/scaffold/prisma/app.py +32 -0
- robyn/scaffold/prisma/requirements.txt +2 -0
- robyn/scaffold/prisma/schema.prisma +13 -0
- robyn/scaffold/sqlalchemy/Dockerfile +12 -0
- robyn/scaffold/sqlalchemy/__init__.py +0 -0
- robyn/scaffold/sqlalchemy/app.py +13 -0
- robyn/scaffold/sqlalchemy/models.py +21 -0
- robyn/scaffold/sqlalchemy/requirements.txt +2 -0
- robyn/scaffold/sqlite/Dockerfile +12 -0
- robyn/scaffold/sqlite/app.py +22 -0
- robyn/scaffold/sqlite/requirements.txt +1 -0
- robyn/scaffold/sqlmodel/Dockerfile +11 -0
- robyn/scaffold/sqlmodel/app.py +46 -0
- robyn/scaffold/sqlmodel/models.py +10 -0
- robyn/scaffold/sqlmodel/requirements.txt +2 -0
- robyn/status_codes.py +137 -0
- robyn/swagger.html +32 -0
- robyn/templating.py +30 -0
- robyn/types.py +44 -0
- robyn/ws.py +67 -0
- robyn-0.76.0.dist-info/METADATA +303 -0
- robyn-0.76.0.dist-info/RECORD +57 -0
- robyn-0.76.0.dist-info/WHEEL +5 -0
- robyn-0.76.0.dist-info/entry_points.txt +3 -0
- robyn-0.76.0.dist-info/licenses/LICENSE +25 -0
robyn/__init__.py
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Callable, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import multiprocess as mp # type: ignore
|
|
10
|
+
|
|
11
|
+
from robyn import status_codes
|
|
12
|
+
from robyn.argument_parser import Config
|
|
13
|
+
from robyn.authentication import AuthenticationHandler
|
|
14
|
+
from robyn.dependency_injection import DependencyMap
|
|
15
|
+
from robyn.env_populator import load_vars
|
|
16
|
+
from robyn.events import Events
|
|
17
|
+
from robyn.jsonify import jsonify
|
|
18
|
+
from robyn.logger import Colors, logger
|
|
19
|
+
from robyn.mcp import MCPApp
|
|
20
|
+
from robyn.openapi import OpenAPI
|
|
21
|
+
from robyn.processpool import run_processes
|
|
22
|
+
from robyn.reloader import compile_rust_files
|
|
23
|
+
from robyn.responses import SSEMessage, SSEResponse, StreamingResponse, html, serve_file, serve_html
|
|
24
|
+
from robyn.robyn import FunctionInfo, Headers, HttpMethod, Request, Response, WebSocketConnector, get_version
|
|
25
|
+
from robyn.router import MiddlewareRouter, MiddlewareType, Router, WebSocketRouter
|
|
26
|
+
from robyn.types import Directory
|
|
27
|
+
from robyn.ws import WebSocket
|
|
28
|
+
|
|
29
|
+
__version__ = get_version()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalize_endpoint(endpoint: Optional[str], treat_empty_as_root: bool = False) -> Optional[str]:
|
|
33
|
+
"""
|
|
34
|
+
Normalize an endpoint to ensure consistent routing.
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Root "/" remains unchanged
|
|
38
|
+
- All other endpoints get leading slash added if missing
|
|
39
|
+
- Trailing slashes are removed from all endpoints except root
|
|
40
|
+
- Empty or blank strings are handled based on treat_empty_as_root flag
|
|
41
|
+
- treat_empty_as_root is used for prefixes where empty/blank strings are valid
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
endpoint: The endpoint path to normalize.
|
|
45
|
+
treat_empty_as_root (used for prefixes):
|
|
46
|
+
If True, empty/blank strings are converted to "/" (root).
|
|
47
|
+
If False, empty/blank strings return None (invalid endpoint).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Normalized endpoint path or None if invalid.
|
|
51
|
+
"""
|
|
52
|
+
if endpoint is None or (not endpoint and not treat_empty_as_root):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Remove trailing slashes
|
|
56
|
+
endpoint = endpoint.strip().rstrip("/")
|
|
57
|
+
|
|
58
|
+
# Handle empty result
|
|
59
|
+
if not endpoint:
|
|
60
|
+
return "/"
|
|
61
|
+
|
|
62
|
+
# Add leading slash if missing
|
|
63
|
+
if not endpoint.startswith("/"):
|
|
64
|
+
endpoint = "/" + endpoint
|
|
65
|
+
|
|
66
|
+
return endpoint
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
config = Config()
|
|
70
|
+
|
|
71
|
+
if (compile_path := config.compile_rust_path) is not None:
|
|
72
|
+
compile_rust_files(compile_path)
|
|
73
|
+
print("Compiled rust files")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BaseRobyn(ABC):
|
|
77
|
+
"""This is the python wrapper for the Robyn binaries."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
file_object: str,
|
|
82
|
+
config: Config = Config(),
|
|
83
|
+
openapi_file_path: Optional[str] = None,
|
|
84
|
+
openapi: Optional[OpenAPI] = None,
|
|
85
|
+
dependencies: DependencyMap = DependencyMap(),
|
|
86
|
+
) -> None:
|
|
87
|
+
directory_path = os.path.dirname(os.path.abspath(file_object))
|
|
88
|
+
self.file_path = file_object
|
|
89
|
+
self.directory_path = directory_path
|
|
90
|
+
self.config = config
|
|
91
|
+
self.dependencies = dependencies
|
|
92
|
+
self.openapi = openapi
|
|
93
|
+
|
|
94
|
+
self.init_openapi(openapi_file_path)
|
|
95
|
+
|
|
96
|
+
if not bool(os.environ.get("ROBYN_CLI", False)):
|
|
97
|
+
# the env variables are already set when are running through the cli
|
|
98
|
+
load_vars(project_root=directory_path)
|
|
99
|
+
|
|
100
|
+
self._handle_dev_mode()
|
|
101
|
+
|
|
102
|
+
logging.basicConfig(level=self.config.log_level)
|
|
103
|
+
|
|
104
|
+
if self.config.log_level.lower() != "warn":
|
|
105
|
+
logger.info(
|
|
106
|
+
"SERVER IS RUNNING IN VERBOSE/DEBUG MODE. Set --log-level to WARN to run in production mode.",
|
|
107
|
+
color=Colors.BLUE,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.router = Router()
|
|
111
|
+
self.middleware_router = MiddlewareRouter()
|
|
112
|
+
self.web_socket_router = WebSocketRouter()
|
|
113
|
+
self.request_headers: Headers = Headers({})
|
|
114
|
+
self.response_headers: Headers = Headers({})
|
|
115
|
+
self.excluded_response_headers_paths: Optional[List[str]] = None
|
|
116
|
+
self.directories: List[Directory] = []
|
|
117
|
+
self.event_handlers: dict = {}
|
|
118
|
+
self.exception_handler: Optional[Callable] = None
|
|
119
|
+
self.authentication_handler: Optional[AuthenticationHandler] = None
|
|
120
|
+
self.included_routers: List[Router] = []
|
|
121
|
+
self._mcp_app: Optional[MCPApp] = None
|
|
122
|
+
|
|
123
|
+
def init_openapi(self, openapi_file_path: Optional[str]) -> None:
|
|
124
|
+
if self.config.disable_openapi:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if self.openapi is None:
|
|
128
|
+
self.openapi = OpenAPI()
|
|
129
|
+
|
|
130
|
+
if openapi_file_path:
|
|
131
|
+
self.openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
|
|
132
|
+
elif Path(self.directory_path).joinpath("openapi.json").exists():
|
|
133
|
+
self.openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
|
|
134
|
+
else:
|
|
135
|
+
logger.debug("No OpenAPI spec file found; using auto-generated documentation only.", color=Colors.YELLOW)
|
|
136
|
+
|
|
137
|
+
def _handle_dev_mode(self):
|
|
138
|
+
cli_dev_mode = self.config.dev # --dev
|
|
139
|
+
env_dev_mode = os.getenv("ROBYN_DEV_MODE", "False").lower() == "true" # ROBYN_DEV_MODE=True
|
|
140
|
+
is_robyn = os.getenv("ROBYN_CLI", False)
|
|
141
|
+
|
|
142
|
+
if cli_dev_mode and not is_robyn:
|
|
143
|
+
raise SystemExit("Dev mode is not supported in the python wrapper. Please use the Robyn CLI. e.g. python3 -m robyn app.py --dev")
|
|
144
|
+
|
|
145
|
+
if env_dev_mode and not is_robyn:
|
|
146
|
+
logger.error("Ignoring ROBYN_DEV_MODE environment variable. Dev mode is not supported in the python wrapper.")
|
|
147
|
+
raise SystemExit("Dev mode is not supported in the python wrapper. Please use the Robyn CLI. e.g. python3 -m robyn app.py")
|
|
148
|
+
|
|
149
|
+
def add_route(
|
|
150
|
+
self,
|
|
151
|
+
route_type: Union[HttpMethod, str],
|
|
152
|
+
endpoint: str,
|
|
153
|
+
handler: Callable,
|
|
154
|
+
is_const: bool = False,
|
|
155
|
+
auth_required: bool = False,
|
|
156
|
+
openapi_name: str = "",
|
|
157
|
+
openapi_tags: Union[List[str], None] = None,
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Connect a URI to a handler
|
|
161
|
+
|
|
162
|
+
:param route_type str: route type between GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS/TRACE
|
|
163
|
+
:param endpoint str: endpoint for the route added
|
|
164
|
+
:param handler function: represents the sync or async function passed as a handler for the route
|
|
165
|
+
:param is_const bool: represents if the handler is a const function or not
|
|
166
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
""" We will add the status code here only
|
|
170
|
+
"""
|
|
171
|
+
injected_dependencies = self.dependencies.get_dependency_map(self)
|
|
172
|
+
|
|
173
|
+
list_openapi_tags: List[str] = openapi_tags if openapi_tags else []
|
|
174
|
+
|
|
175
|
+
if isinstance(route_type, str):
|
|
176
|
+
http_methods = {
|
|
177
|
+
"GET": HttpMethod.GET,
|
|
178
|
+
"POST": HttpMethod.POST,
|
|
179
|
+
"PUT": HttpMethod.PUT,
|
|
180
|
+
"DELETE": HttpMethod.DELETE,
|
|
181
|
+
"PATCH": HttpMethod.PATCH,
|
|
182
|
+
"HEAD": HttpMethod.HEAD,
|
|
183
|
+
"OPTIONS": HttpMethod.OPTIONS,
|
|
184
|
+
}
|
|
185
|
+
route_type = http_methods[route_type]
|
|
186
|
+
|
|
187
|
+
# Normalize endpoint before adding
|
|
188
|
+
normalized_endpoint = _normalize_endpoint(endpoint)
|
|
189
|
+
|
|
190
|
+
if normalized_endpoint is None:
|
|
191
|
+
raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint")
|
|
192
|
+
|
|
193
|
+
if auth_required:
|
|
194
|
+
self.middleware_router.add_auth_middleware(normalized_endpoint, route_type)(handler)
|
|
195
|
+
|
|
196
|
+
# Check if this exact route (method + normalized_endpoint) already exists
|
|
197
|
+
route_key = f"{route_type}:{normalized_endpoint}"
|
|
198
|
+
if not hasattr(self, "_added_routes"):
|
|
199
|
+
self._added_routes = set()
|
|
200
|
+
|
|
201
|
+
if route_key in self._added_routes:
|
|
202
|
+
# Route already exists, raise an error
|
|
203
|
+
raise ValueError(f"Route {route_type} {normalized_endpoint} already exists")
|
|
204
|
+
|
|
205
|
+
# Add to our tracking set
|
|
206
|
+
self._added_routes.add(route_key)
|
|
207
|
+
|
|
208
|
+
add_route_response = self.router.add_route(
|
|
209
|
+
route_type=route_type,
|
|
210
|
+
endpoint=normalized_endpoint,
|
|
211
|
+
handler=handler,
|
|
212
|
+
is_const=is_const,
|
|
213
|
+
auth_required=auth_required,
|
|
214
|
+
openapi_name=openapi_name,
|
|
215
|
+
openapi_tags=list_openapi_tags,
|
|
216
|
+
exception_handler=self.exception_handler,
|
|
217
|
+
injected_dependencies=injected_dependencies,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
logger.info("Added route %s %s", route_type, normalized_endpoint)
|
|
221
|
+
|
|
222
|
+
return add_route_response
|
|
223
|
+
|
|
224
|
+
def inject(self, **kwargs):
|
|
225
|
+
"""
|
|
226
|
+
Injects the dependencies for the route
|
|
227
|
+
|
|
228
|
+
:param kwargs dict: the dependencies to be injected
|
|
229
|
+
"""
|
|
230
|
+
self.dependencies.add_router_dependency(self, **kwargs)
|
|
231
|
+
|
|
232
|
+
def inject_global(self, **kwargs):
|
|
233
|
+
"""
|
|
234
|
+
Injects the dependencies for the global routes
|
|
235
|
+
Ideally, this function should be a global function
|
|
236
|
+
|
|
237
|
+
:param kwargs dict: the dependencies to be injected
|
|
238
|
+
"""
|
|
239
|
+
self.dependencies.add_global_dependency(**kwargs)
|
|
240
|
+
|
|
241
|
+
def before_request(self, endpoint: Optional[str] = None) -> Callable[..., None]:
|
|
242
|
+
"""
|
|
243
|
+
You can use the @app.before_request decorator to call a method before routing to the specified endpoint
|
|
244
|
+
|
|
245
|
+
:param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes.
|
|
246
|
+
"""
|
|
247
|
+
return self.middleware_router.add_middleware(MiddlewareType.BEFORE_REQUEST, _normalize_endpoint(endpoint))
|
|
248
|
+
|
|
249
|
+
def after_request(self, endpoint: Optional[str] = None) -> Callable[..., None]:
|
|
250
|
+
"""
|
|
251
|
+
You can use the @app.after_request decorator to call a method after routing to the specified endpoint
|
|
252
|
+
|
|
253
|
+
:param endpoint str|None: endpoint to server the route. If None, the middleware will be applied to all the routes.
|
|
254
|
+
"""
|
|
255
|
+
return self.middleware_router.add_middleware(MiddlewareType.AFTER_REQUEST, _normalize_endpoint(endpoint))
|
|
256
|
+
|
|
257
|
+
def serve_directory(
|
|
258
|
+
self,
|
|
259
|
+
route: str,
|
|
260
|
+
directory_path: str,
|
|
261
|
+
index_file: Optional[str] = None,
|
|
262
|
+
show_files_listing: bool = False,
|
|
263
|
+
):
|
|
264
|
+
"""
|
|
265
|
+
Serves a directory at the given route
|
|
266
|
+
|
|
267
|
+
:param route str: the route at which the directory is to be served
|
|
268
|
+
:param directory_path str: the path of the directory to be served
|
|
269
|
+
:param index_file str|None: the index file to be served
|
|
270
|
+
:param show_files_listing bool: if the files listing should be shown or not
|
|
271
|
+
"""
|
|
272
|
+
self.directories.append(Directory(route, directory_path, show_files_listing, index_file))
|
|
273
|
+
|
|
274
|
+
def add_request_header(self, key: str, value: str) -> None:
|
|
275
|
+
self.request_headers.append(key, value)
|
|
276
|
+
|
|
277
|
+
def add_response_header(self, key: str, value: str) -> None:
|
|
278
|
+
self.response_headers.append(key, value)
|
|
279
|
+
|
|
280
|
+
def set_request_header(self, key: str, value: str) -> None:
|
|
281
|
+
self.request_headers.set(key, value)
|
|
282
|
+
|
|
283
|
+
def set_response_header(self, key: str, value: str) -> None:
|
|
284
|
+
self.response_headers.set(key, value)
|
|
285
|
+
|
|
286
|
+
def exclude_response_headers_for(self, excluded_response_headers_paths: Optional[List[str]]):
|
|
287
|
+
"""
|
|
288
|
+
To exclude response headers from certain routes
|
|
289
|
+
@param exclude_paths: the paths to exclude response headers from
|
|
290
|
+
"""
|
|
291
|
+
self.excluded_response_headers_paths = excluded_response_headers_paths
|
|
292
|
+
|
|
293
|
+
def add_web_socket(self, endpoint: str, ws: WebSocket) -> None:
|
|
294
|
+
self.web_socket_router.add_route(endpoint, ws)
|
|
295
|
+
|
|
296
|
+
def _add_event_handler(self, event_type: Events, handler: Callable) -> None:
|
|
297
|
+
logger.info("Added event %s handler", event_type)
|
|
298
|
+
if event_type not in {Events.STARTUP, Events.SHUTDOWN}:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
is_async = inspect.iscoroutinefunction(handler)
|
|
302
|
+
self.event_handlers[event_type] = FunctionInfo(handler, is_async, 0, {}, {})
|
|
303
|
+
|
|
304
|
+
def startup_handler(self, handler: Callable) -> None:
|
|
305
|
+
self._add_event_handler(Events.STARTUP, handler)
|
|
306
|
+
|
|
307
|
+
def shutdown_handler(self, handler: Callable) -> None:
|
|
308
|
+
self._add_event_handler(Events.SHUTDOWN, handler)
|
|
309
|
+
|
|
310
|
+
def is_port_in_use(self, port: int) -> bool:
|
|
311
|
+
try:
|
|
312
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
313
|
+
return s.connect_ex(("localhost", port)) == 0
|
|
314
|
+
except Exception:
|
|
315
|
+
raise Exception(f"Invalid port number: {port}")
|
|
316
|
+
|
|
317
|
+
def _add_openapi_routes(self, auth_required: bool = False):
|
|
318
|
+
if self.config.disable_openapi:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
if self.openapi is None:
|
|
322
|
+
logger.error("No openAPI")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
self.router.prepare_routes_openapi(self.openapi, self.included_routers)
|
|
326
|
+
|
|
327
|
+
self.add_route(
|
|
328
|
+
route_type=HttpMethod.GET,
|
|
329
|
+
endpoint="/openapi.json",
|
|
330
|
+
handler=self.openapi.get_openapi_config,
|
|
331
|
+
is_const=True,
|
|
332
|
+
auth_required=auth_required,
|
|
333
|
+
)
|
|
334
|
+
self.add_route(
|
|
335
|
+
route_type=HttpMethod.GET,
|
|
336
|
+
endpoint="/docs",
|
|
337
|
+
handler=self.openapi.get_openapi_docs_page,
|
|
338
|
+
is_const=True,
|
|
339
|
+
auth_required=auth_required,
|
|
340
|
+
)
|
|
341
|
+
self.exclude_response_headers_for(["/docs", "/openapi.json"])
|
|
342
|
+
|
|
343
|
+
def exception(self, exception_handler: Callable):
|
|
344
|
+
self.exception_handler = exception_handler
|
|
345
|
+
|
|
346
|
+
def get(
|
|
347
|
+
self,
|
|
348
|
+
endpoint: str,
|
|
349
|
+
const: bool = False,
|
|
350
|
+
auth_required: bool = False,
|
|
351
|
+
openapi_name: str = "",
|
|
352
|
+
openapi_tags: List[str] = ["get"],
|
|
353
|
+
):
|
|
354
|
+
"""
|
|
355
|
+
The @app.get decorator to add a route with the GET method
|
|
356
|
+
|
|
357
|
+
:param endpoint str: endpoint for the route added
|
|
358
|
+
:param const bool: represents if the handler is a const function or not
|
|
359
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
360
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
361
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def inner(handler):
|
|
365
|
+
return self.add_route(HttpMethod.GET, endpoint, handler, const, auth_required, openapi_name, openapi_tags)
|
|
366
|
+
|
|
367
|
+
return inner
|
|
368
|
+
|
|
369
|
+
def post(
|
|
370
|
+
self,
|
|
371
|
+
endpoint: str,
|
|
372
|
+
auth_required: bool = False,
|
|
373
|
+
openapi_name: str = "",
|
|
374
|
+
openapi_tags: List[str] = ["post"],
|
|
375
|
+
):
|
|
376
|
+
"""
|
|
377
|
+
The @app.post decorator to add a route with POST method
|
|
378
|
+
|
|
379
|
+
:param endpoint str: endpoint for the route added
|
|
380
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
381
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
382
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def inner(handler):
|
|
386
|
+
return self.add_route(HttpMethod.POST, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
387
|
+
|
|
388
|
+
return inner
|
|
389
|
+
|
|
390
|
+
def put(
|
|
391
|
+
self,
|
|
392
|
+
endpoint: str,
|
|
393
|
+
auth_required: bool = False,
|
|
394
|
+
openapi_name: str = "",
|
|
395
|
+
openapi_tags: List[str] = ["put"],
|
|
396
|
+
):
|
|
397
|
+
"""
|
|
398
|
+
The @app.put decorator to add a get route with PUT method
|
|
399
|
+
|
|
400
|
+
:param endpoint str: endpoint for the route added
|
|
401
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
402
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
403
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
def inner(handler):
|
|
407
|
+
return self.add_route(HttpMethod.PUT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
408
|
+
|
|
409
|
+
return inner
|
|
410
|
+
|
|
411
|
+
def delete(
|
|
412
|
+
self,
|
|
413
|
+
endpoint: str,
|
|
414
|
+
auth_required: bool = False,
|
|
415
|
+
openapi_name: str = "",
|
|
416
|
+
openapi_tags: List[str] = ["delete"],
|
|
417
|
+
):
|
|
418
|
+
"""
|
|
419
|
+
The @app.delete decorator to add a route with DELETE method
|
|
420
|
+
|
|
421
|
+
:param endpoint str: endpoint for the route added
|
|
422
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
423
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
424
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
def inner(handler):
|
|
428
|
+
return self.add_route(HttpMethod.DELETE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
429
|
+
|
|
430
|
+
return inner
|
|
431
|
+
|
|
432
|
+
def patch(
|
|
433
|
+
self,
|
|
434
|
+
endpoint: str,
|
|
435
|
+
auth_required: bool = False,
|
|
436
|
+
openapi_name: str = "",
|
|
437
|
+
openapi_tags: List[str] = ["patch"],
|
|
438
|
+
):
|
|
439
|
+
"""
|
|
440
|
+
The @app.patch decorator to add a route with PATCH method
|
|
441
|
+
|
|
442
|
+
:param endpoint str: endpoint for the route added
|
|
443
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
444
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
445
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
def inner(handler):
|
|
449
|
+
return self.add_route(HttpMethod.PATCH, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
450
|
+
|
|
451
|
+
return inner
|
|
452
|
+
|
|
453
|
+
def head(
|
|
454
|
+
self,
|
|
455
|
+
endpoint: str,
|
|
456
|
+
auth_required: bool = False,
|
|
457
|
+
openapi_name: str = "",
|
|
458
|
+
openapi_tags: List[str] = ["head"],
|
|
459
|
+
):
|
|
460
|
+
"""
|
|
461
|
+
The @app.head decorator to add a route with HEAD method
|
|
462
|
+
|
|
463
|
+
:param endpoint str: endpoint for the route added
|
|
464
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
465
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
466
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
def inner(handler):
|
|
470
|
+
return self.add_route(HttpMethod.HEAD, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
471
|
+
|
|
472
|
+
return inner
|
|
473
|
+
|
|
474
|
+
def options(
|
|
475
|
+
self,
|
|
476
|
+
endpoint: str,
|
|
477
|
+
auth_required: bool = False,
|
|
478
|
+
openapi_name: str = "",
|
|
479
|
+
openapi_tags: List[str] = ["options"],
|
|
480
|
+
):
|
|
481
|
+
"""
|
|
482
|
+
The @app.options decorator to add a route with OPTIONS method
|
|
483
|
+
|
|
484
|
+
:param endpoint str: endpoint for the route added
|
|
485
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
486
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
487
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def inner(handler):
|
|
491
|
+
return self.add_route(HttpMethod.OPTIONS, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
492
|
+
|
|
493
|
+
return inner
|
|
494
|
+
|
|
495
|
+
def connect(
|
|
496
|
+
self,
|
|
497
|
+
endpoint: str,
|
|
498
|
+
auth_required: bool = False,
|
|
499
|
+
openapi_name: str = "",
|
|
500
|
+
openapi_tags: List[str] = ["connect"],
|
|
501
|
+
):
|
|
502
|
+
"""
|
|
503
|
+
The @app.connect decorator to add a route with CONNECT method
|
|
504
|
+
|
|
505
|
+
:param endpoint str: endpoint for the route added
|
|
506
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
507
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
508
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
def inner(handler):
|
|
512
|
+
return self.add_route(HttpMethod.CONNECT, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
513
|
+
|
|
514
|
+
return inner
|
|
515
|
+
|
|
516
|
+
def trace(
|
|
517
|
+
self,
|
|
518
|
+
endpoint: str,
|
|
519
|
+
auth_required: bool = False,
|
|
520
|
+
openapi_name: str = "",
|
|
521
|
+
openapi_tags: List[str] = ["trace"],
|
|
522
|
+
):
|
|
523
|
+
"""
|
|
524
|
+
The @app.trace decorator to add a route with TRACE method
|
|
525
|
+
|
|
526
|
+
:param endpoint str: endpoint for the route added
|
|
527
|
+
:param auth_required bool: represents if the route needs authentication or not
|
|
528
|
+
:param openapi_name: str -- the name of the endpoint in the openapi spec
|
|
529
|
+
:param openapi_tags: List[str] -- for grouping of endpoints in the openapi spec
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
def inner(handler):
|
|
533
|
+
return self.add_route(HttpMethod.TRACE, endpoint, handler, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
534
|
+
|
|
535
|
+
return inner
|
|
536
|
+
|
|
537
|
+
def include_router(self, router: "SubRouter"):
|
|
538
|
+
"""
|
|
539
|
+
The method to include the routes from another router.
|
|
540
|
+
Merge another SubRouter's routes, middlewares, websocket routes, and dependencies into this router.
|
|
541
|
+
Note: This operation mutates the current router's internal collections (route list, middleware lists,
|
|
542
|
+
websocket routes, and dependencies) and does not deep-copy the included router. Callers should ensure
|
|
543
|
+
there are no path or name conflicts before including a router.
|
|
544
|
+
|
|
545
|
+
:param router SubRouter: the router object to include the routes from
|
|
546
|
+
"""
|
|
547
|
+
self.included_routers.append(router)
|
|
548
|
+
|
|
549
|
+
self.router.routes.extend(router.router.routes)
|
|
550
|
+
self.middleware_router.global_middlewares.extend(router.middleware_router.global_middlewares)
|
|
551
|
+
self.middleware_router.route_middlewares.extend(router.middleware_router.route_middlewares)
|
|
552
|
+
|
|
553
|
+
if not self.config.disable_openapi and self.openapi is not None:
|
|
554
|
+
self.openapi.add_subrouter_paths(self.openapi)
|
|
555
|
+
|
|
556
|
+
# extend the websocket routes
|
|
557
|
+
prefix = router.prefix
|
|
558
|
+
for route in router.web_socket_router.routes:
|
|
559
|
+
new_endpoint = f"{prefix}{route}"
|
|
560
|
+
self.web_socket_router.routes[new_endpoint] = router.web_socket_router.routes[route]
|
|
561
|
+
|
|
562
|
+
self.dependencies.merge_dependencies(router)
|
|
563
|
+
|
|
564
|
+
def configure_authentication(self, authentication_handler: AuthenticationHandler):
|
|
565
|
+
"""
|
|
566
|
+
Configures the authentication handler for the application.
|
|
567
|
+
|
|
568
|
+
:param authentication_handler: the instance of a class inheriting the AuthenticationHandler base class
|
|
569
|
+
"""
|
|
570
|
+
self.authentication_handler = authentication_handler
|
|
571
|
+
self.middleware_router.set_authentication_handler(authentication_handler)
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def mcp(self):
|
|
575
|
+
"""
|
|
576
|
+
Get the MCP (Model Context Protocol) interface for this app.
|
|
577
|
+
|
|
578
|
+
Enables registering MCP resources, tools, and prompts that can be accessed
|
|
579
|
+
by MCP clients like Claude Desktop or other AI applications.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
MCPApp: MCP interface for registering handlers
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
@app.mcp.resource("file://documents", "Documents", "Access to document files")
|
|
586
|
+
def get_documents(params):
|
|
587
|
+
return "Document content here"
|
|
588
|
+
|
|
589
|
+
@app.mcp.tool("calculate", "Perform calculations", {
|
|
590
|
+
"type": "object",
|
|
591
|
+
"properties": {
|
|
592
|
+
"expression": {"type": "string", "description": "Math expression to evaluate"}
|
|
593
|
+
},
|
|
594
|
+
"required": ["expression"]
|
|
595
|
+
})
|
|
596
|
+
def calculate_tool(args):
|
|
597
|
+
return eval(args["expression"])
|
|
598
|
+
"""
|
|
599
|
+
if self._mcp_app is None:
|
|
600
|
+
self._mcp_app = MCPApp(self)
|
|
601
|
+
return self._mcp_app
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class Robyn(BaseRobyn):
|
|
605
|
+
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):
|
|
606
|
+
"""
|
|
607
|
+
Starts the server
|
|
608
|
+
|
|
609
|
+
:param host str: represents the host at which the server is listening
|
|
610
|
+
:param port int: represents the port number at which the server is listening
|
|
611
|
+
:param _check_port bool: represents if the port should be checked if it is already in use
|
|
612
|
+
:param client_timeout int: timeout for client connections in seconds (default: 30)
|
|
613
|
+
:param keep_alive_timeout int: timeout for keep-alive connections in seconds (default: 20)
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
host = os.getenv("ROBYN_HOST", host)
|
|
617
|
+
port = int(os.getenv("ROBYN_PORT", port))
|
|
618
|
+
client_timeout = int(os.getenv("ROBYN_CLIENT_TIMEOUT", client_timeout))
|
|
619
|
+
keep_alive_timeout = int(os.getenv("ROBYN_KEEP_ALIVE_TIMEOUT", keep_alive_timeout))
|
|
620
|
+
open_browser = bool(os.getenv("ROBYN_BROWSER_OPEN", self.config.open_browser))
|
|
621
|
+
|
|
622
|
+
if _check_port:
|
|
623
|
+
while self.is_port_in_use(port):
|
|
624
|
+
logger.error("Port %s is already in use. Please use a different port.", port)
|
|
625
|
+
try:
|
|
626
|
+
port = int(input("Enter a different port: "))
|
|
627
|
+
except Exception:
|
|
628
|
+
logger.error("Invalid port number. Please enter a valid port number.")
|
|
629
|
+
continue
|
|
630
|
+
|
|
631
|
+
if not self.config.disable_openapi:
|
|
632
|
+
self._add_openapi_routes()
|
|
633
|
+
logger.info("Docs hosted at http://%s:%s/docs", host, port)
|
|
634
|
+
|
|
635
|
+
logger.info("Robyn version: %s", __version__)
|
|
636
|
+
logger.info("Starting server at http://%s:%s", host, port)
|
|
637
|
+
|
|
638
|
+
mp.allow_connection_pickling()
|
|
639
|
+
|
|
640
|
+
run_processes(
|
|
641
|
+
host,
|
|
642
|
+
port,
|
|
643
|
+
self.directories,
|
|
644
|
+
self.request_headers,
|
|
645
|
+
self.router.get_routes(),
|
|
646
|
+
self.middleware_router.get_global_middlewares(),
|
|
647
|
+
self.middleware_router.get_route_middlewares(),
|
|
648
|
+
self.web_socket_router.get_routes(),
|
|
649
|
+
self.event_handlers,
|
|
650
|
+
self.config.workers,
|
|
651
|
+
self.config.processes,
|
|
652
|
+
self.response_headers,
|
|
653
|
+
self.excluded_response_headers_paths,
|
|
654
|
+
open_browser,
|
|
655
|
+
client_timeout,
|
|
656
|
+
keep_alive_timeout,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class SubRouter(BaseRobyn):
|
|
661
|
+
def __init__(self, file_object: str, prefix: str = "", config: Config = Config(), openapi: OpenAPI = OpenAPI()) -> None:
|
|
662
|
+
super().__init__(file_object=file_object, config=config, openapi=openapi)
|
|
663
|
+
self.prefix = prefix
|
|
664
|
+
|
|
665
|
+
def __add_prefix(self, endpoint: str):
|
|
666
|
+
# Normalize prefix, treating empty as empty (not root)
|
|
667
|
+
normalized_prefix = _normalize_endpoint(self.prefix, treat_empty_as_root=True)
|
|
668
|
+
|
|
669
|
+
# Handle empty endpoint - should just be the prefix
|
|
670
|
+
if endpoint in ("", "/"):
|
|
671
|
+
return normalized_prefix if normalized_prefix else "/"
|
|
672
|
+
|
|
673
|
+
# Convert root prefix to empty to avoid double slashes when making endpoint
|
|
674
|
+
if normalized_prefix == "/":
|
|
675
|
+
normalized_prefix = "" # Empty prefix for root
|
|
676
|
+
|
|
677
|
+
# Normalize and validate endpoint
|
|
678
|
+
normalized_endpoint = _normalize_endpoint(endpoint)
|
|
679
|
+
if normalized_endpoint is None:
|
|
680
|
+
raise ValueError("Endpoint cannot be blank, do specify '/' for root endpoint")
|
|
681
|
+
|
|
682
|
+
return f"{normalized_prefix}{normalized_endpoint}"
|
|
683
|
+
|
|
684
|
+
def get(self, endpoint: str, const: bool = False, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["get"]):
|
|
685
|
+
return super().get(endpoint=self.__add_prefix(endpoint), const=const, auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
686
|
+
|
|
687
|
+
def post(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["post"]):
|
|
688
|
+
return super().post(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
689
|
+
|
|
690
|
+
def put(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["put"]):
|
|
691
|
+
return super().put(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
692
|
+
|
|
693
|
+
def delete(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["delete"]):
|
|
694
|
+
return super().delete(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
695
|
+
|
|
696
|
+
def patch(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["patch"]):
|
|
697
|
+
return super().patch(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
698
|
+
|
|
699
|
+
def head(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["head"]):
|
|
700
|
+
return super().head(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
701
|
+
|
|
702
|
+
def trace(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["trace"]):
|
|
703
|
+
return super().trace(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
704
|
+
|
|
705
|
+
def options(self, endpoint: str, auth_required: bool = False, openapi_name: str = "", openapi_tags: List[str] = ["options"]):
|
|
706
|
+
return super().options(endpoint=self.__add_prefix(endpoint), auth_required=auth_required, openapi_name=openapi_name, openapi_tags=openapi_tags)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def ALLOW_CORS(app: Robyn, origins: Union[List[str], str], headers: Union[List[str], str] = None):
|
|
710
|
+
"""
|
|
711
|
+
Configure CORS headers for the application.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
app: Robyn application instance
|
|
715
|
+
origins: List of allowed origins or "*" for all origins
|
|
716
|
+
headers: List of allowed headers or "*" for all headers
|
|
717
|
+
"""
|
|
718
|
+
# Handle string input for origins
|
|
719
|
+
if isinstance(origins, str):
|
|
720
|
+
origins = [origins]
|
|
721
|
+
|
|
722
|
+
default_headers = ["Content-Type", "Authorization"]
|
|
723
|
+
if isinstance(headers, list):
|
|
724
|
+
headers = list(set(default_headers + headers))
|
|
725
|
+
headers = ", ".join(headers)
|
|
726
|
+
|
|
727
|
+
@app.before_request()
|
|
728
|
+
def cors_middleware(request):
|
|
729
|
+
origin = request.headers.get("Origin")
|
|
730
|
+
|
|
731
|
+
# If specific origins are set, validate the request origin
|
|
732
|
+
if origin and "*" not in origins and origin not in origins:
|
|
733
|
+
return Response(status_code=403, description="", headers={})
|
|
734
|
+
|
|
735
|
+
# Handle preflight requests
|
|
736
|
+
if request.method == "OPTIONS":
|
|
737
|
+
return Response(
|
|
738
|
+
status_code=204,
|
|
739
|
+
headers={
|
|
740
|
+
"Access-Control-Allow-Origin": origin if origin else (origins[0] if origins else "*"),
|
|
741
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS",
|
|
742
|
+
"Access-Control-Allow-Headers": str(headers) if headers else "Content-Type, Authorization",
|
|
743
|
+
"Access-Control-Allow-Credentials": "true",
|
|
744
|
+
"Access-Control-Max-Age": "3600",
|
|
745
|
+
},
|
|
746
|
+
description="",
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
return request
|
|
750
|
+
|
|
751
|
+
# Set default CORS headers for all responses
|
|
752
|
+
if len(origins) == 1:
|
|
753
|
+
app.set_response_header("Access-Control-Allow-Origin", origins[0])
|
|
754
|
+
else:
|
|
755
|
+
# For multiple origins, we'll handle it dynamically in the response
|
|
756
|
+
app.set_response_header("Access-Control-Allow-Origin", "*")
|
|
757
|
+
|
|
758
|
+
app.set_response_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS")
|
|
759
|
+
app.set_response_header("Access-Control-Allow-Headers", str(headers) if headers else "Content-Type, Authorization")
|
|
760
|
+
app.set_response_header("Access-Control-Allow-Credentials", "true")
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
__all__ = [
|
|
764
|
+
"Robyn",
|
|
765
|
+
"Request",
|
|
766
|
+
"Response",
|
|
767
|
+
"status_codes",
|
|
768
|
+
"jsonify",
|
|
769
|
+
"serve_file",
|
|
770
|
+
"serve_html",
|
|
771
|
+
"html",
|
|
772
|
+
"StreamingResponse",
|
|
773
|
+
"SSEResponse",
|
|
774
|
+
"SSEMessage",
|
|
775
|
+
"ALLOW_CORS",
|
|
776
|
+
"SubRouter",
|
|
777
|
+
"AuthenticationHandler",
|
|
778
|
+
"Headers",
|
|
779
|
+
"WebSocketConnector",
|
|
780
|
+
"WebSocket",
|
|
781
|
+
"MCPApp",
|
|
782
|
+
]
|