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.
Files changed (57) hide show
  1. robyn/__init__.py +782 -0
  2. robyn/__main__.py +4 -0
  3. robyn/ai.py +308 -0
  4. robyn/argument_parser.py +129 -0
  5. robyn/authentication.py +96 -0
  6. robyn/cli.py +136 -0
  7. robyn/dependency_injection.py +71 -0
  8. robyn/env_populator.py +35 -0
  9. robyn/events.py +6 -0
  10. robyn/exceptions.py +32 -0
  11. robyn/jsonify.py +13 -0
  12. robyn/logger.py +80 -0
  13. robyn/mcp.py +461 -0
  14. robyn/openapi.py +448 -0
  15. robyn/processpool.py +226 -0
  16. robyn/py.typed +0 -0
  17. robyn/reloader.py +164 -0
  18. robyn/responses.py +208 -0
  19. robyn/robyn.cpython-314-x86_64-linux-gnu.so +0 -0
  20. robyn/robyn.pyi +562 -0
  21. robyn/router.py +426 -0
  22. robyn/scaffold/mongo/Dockerfile +12 -0
  23. robyn/scaffold/mongo/app.py +43 -0
  24. robyn/scaffold/mongo/requirements.txt +2 -0
  25. robyn/scaffold/no-db/Dockerfile +12 -0
  26. robyn/scaffold/no-db/app.py +12 -0
  27. robyn/scaffold/no-db/requirements.txt +1 -0
  28. robyn/scaffold/postgres/Dockerfile +32 -0
  29. robyn/scaffold/postgres/app.py +31 -0
  30. robyn/scaffold/postgres/requirements.txt +3 -0
  31. robyn/scaffold/postgres/supervisord.conf +14 -0
  32. robyn/scaffold/prisma/Dockerfile +15 -0
  33. robyn/scaffold/prisma/app.py +32 -0
  34. robyn/scaffold/prisma/requirements.txt +2 -0
  35. robyn/scaffold/prisma/schema.prisma +13 -0
  36. robyn/scaffold/sqlalchemy/Dockerfile +12 -0
  37. robyn/scaffold/sqlalchemy/__init__.py +0 -0
  38. robyn/scaffold/sqlalchemy/app.py +13 -0
  39. robyn/scaffold/sqlalchemy/models.py +21 -0
  40. robyn/scaffold/sqlalchemy/requirements.txt +2 -0
  41. robyn/scaffold/sqlite/Dockerfile +12 -0
  42. robyn/scaffold/sqlite/app.py +22 -0
  43. robyn/scaffold/sqlite/requirements.txt +1 -0
  44. robyn/scaffold/sqlmodel/Dockerfile +11 -0
  45. robyn/scaffold/sqlmodel/app.py +46 -0
  46. robyn/scaffold/sqlmodel/models.py +10 -0
  47. robyn/scaffold/sqlmodel/requirements.txt +2 -0
  48. robyn/status_codes.py +137 -0
  49. robyn/swagger.html +32 -0
  50. robyn/templating.py +30 -0
  51. robyn/types.py +44 -0
  52. robyn/ws.py +67 -0
  53. robyn-0.76.0.dist-info/METADATA +303 -0
  54. robyn-0.76.0.dist-info/RECORD +57 -0
  55. robyn-0.76.0.dist-info/WHEEL +5 -0
  56. robyn-0.76.0.dist-info/entry_points.txt +3 -0
  57. 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
+ ]