robyn 0.73.0__cp311-cp311-macosx_10_12_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.

Potentially problematic release.


This version of robyn might be problematic. Click here for more details.

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