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.
- robyn/__init__.py +757 -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-311-darwin.so +0 -0
- robyn/robyn.pyi +421 -0
- robyn/router.py +410 -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.73.0.dist-info/METADATA +32 -0
- robyn-0.73.0.dist-info/RECORD +57 -0
- robyn-0.73.0.dist-info/WHEEL +4 -0
- robyn-0.73.0.dist-info/entry_points.txt +3 -0
- robyn-0.73.0.dist-info/licenses/LICENSE +25 -0
robyn/openapi.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import typing
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from inspect import Signature
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict
|
|
9
|
+
|
|
10
|
+
from robyn.responses import html
|
|
11
|
+
from robyn.robyn import QueryParams, Response
|
|
12
|
+
from robyn.types import Body
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class str_typed_dict(TypedDict):
|
|
16
|
+
key: str
|
|
17
|
+
value: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Contact:
|
|
22
|
+
"""
|
|
23
|
+
The contact information for the exposed API.
|
|
24
|
+
(https://swagger.io/specification/#contact-object)
|
|
25
|
+
|
|
26
|
+
@param name: Optional[str] The identifying name of the contact person/organization.
|
|
27
|
+
@param url: Optional[str] The URL pointing to the contact information. This MUST be in the form of a URL.
|
|
28
|
+
@param email: Optional[str] The email address of the contact person/organization. This MUST be in the form of an email address.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: Optional[str] = None
|
|
32
|
+
url: Optional[str] = None
|
|
33
|
+
email: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class License:
|
|
38
|
+
"""
|
|
39
|
+
The license information for the exposed API.
|
|
40
|
+
(https://swagger.io/specification/#license-object)
|
|
41
|
+
|
|
42
|
+
@param name: Optional[str] The license name used for the API.
|
|
43
|
+
@param url: Optional[str] A URL to the license used for the API. This MUST be in the form of a URL.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: Optional[str] = None
|
|
47
|
+
url: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Server:
|
|
52
|
+
"""
|
|
53
|
+
An array of Server Objects, which provide connectivity information to a target server. If the servers property is
|
|
54
|
+
not provided, or is an empty array, the default value would be a Server Object with a url value of /.
|
|
55
|
+
(https://swagger.io/specification/#server-object)
|
|
56
|
+
|
|
57
|
+
@param url: str A URL to the target host. This URL supports Server Variables and MAY be relative,
|
|
58
|
+
to indicate that the host location is relative to the location where the OpenAPI document is being served.
|
|
59
|
+
Variable substitutions will be made when a variable is named in {brackets}.
|
|
60
|
+
@param description: Optional[str] An optional string describing the host designated by the URL.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
url: str
|
|
64
|
+
description: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class ExternalDocumentation:
|
|
69
|
+
"""
|
|
70
|
+
Additional external documentation for this operation.
|
|
71
|
+
(https://swagger.io/specification/#external-documentation-object)
|
|
72
|
+
|
|
73
|
+
@param description: Optional[str] A description of the target documentation.
|
|
74
|
+
@param url: Optional[str] The URL for the target documentation.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
description: Optional[str] = None
|
|
78
|
+
url: Optional[str] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class Components:
|
|
83
|
+
"""
|
|
84
|
+
Additional external documentation for this operation.
|
|
85
|
+
(https://swagger.io/specification/#components-object)
|
|
86
|
+
|
|
87
|
+
@param schemas: Optional[Dict[str, Dict]] An object to hold reusable Schema Objects.
|
|
88
|
+
@param responses: Optional[Dict[str, Dict]] An object to hold reusable Response Objects.
|
|
89
|
+
@param parameters: Optional[Dict[str, Dict]] An object to hold reusable Parameter Objects.
|
|
90
|
+
@param examples: Optional[Dict[str, Dict]] An object to hold reusable Example Objects.
|
|
91
|
+
@param requestBodies: Optional[Dict[str, Dict]] An object to hold reusable Request Body Objects.
|
|
92
|
+
@param securitySchemes: Optional[Dict[str, Dict]] An object to hold reusable Security Scheme Objects.
|
|
93
|
+
@param links: Optional[Dict[str, Dict]] An object to hold reusable Link Objects.
|
|
94
|
+
@param callbacks: Optional[Dict[str, Dict]] An object to hold reusable Callback Objects.
|
|
95
|
+
@param pathItems: Optional[Dict[str, Dict]] An object to hold reusable Callback Objects.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
schemas: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
99
|
+
responses: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
100
|
+
parameters: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
101
|
+
examples: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
102
|
+
requestBodies: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
103
|
+
securitySchemes: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
104
|
+
links: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
105
|
+
callbacks: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
106
|
+
pathItems: Optional[Dict[str, Dict]] = field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class OpenAPIInfo:
|
|
111
|
+
"""
|
|
112
|
+
Provides metadata about the API. The metadata MAY be used by tooling as required.
|
|
113
|
+
(https://swagger.io/specification/#info-object)
|
|
114
|
+
|
|
115
|
+
@param title: str The title of the API.
|
|
116
|
+
@param version: str The version of the API.
|
|
117
|
+
@param description: Optional[str] A description of the API.
|
|
118
|
+
@param termsOfService: Optional[str] A URL to the Terms of Service for the API.
|
|
119
|
+
@param contact: Contact The contact information for the exposed API.
|
|
120
|
+
@param license: License The license information for the exposed API.
|
|
121
|
+
@param servers: list[Server] An list of Server objects representing the servers.
|
|
122
|
+
@param externalDocs: Optional[ExternalDocumentation] Additional external documentation.
|
|
123
|
+
@param components: Components An element to hold various schemas for the document.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
title: str = "Robyn API"
|
|
127
|
+
version: str = "1.0.0"
|
|
128
|
+
description: Optional[str] = None
|
|
129
|
+
termsOfService: Optional[str] = None
|
|
130
|
+
contact: Contact = field(default_factory=Contact)
|
|
131
|
+
license: License = field(default_factory=License)
|
|
132
|
+
servers: List[Server] = field(default_factory=list)
|
|
133
|
+
externalDocs: Optional[ExternalDocumentation] = field(default_factory=ExternalDocumentation)
|
|
134
|
+
components: Components = field(default_factory=Components)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class OpenAPI:
|
|
139
|
+
"""
|
|
140
|
+
This is the root object of the OpenAPI document.
|
|
141
|
+
|
|
142
|
+
@param info: OpenAPIInfo Provides metadata about the API.
|
|
143
|
+
@param openapi_spec: dict The content of openapi.json as a dict
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
info: OpenAPIInfo = field(default_factory=OpenAPIInfo)
|
|
147
|
+
openapi_spec: dict = field(init=False)
|
|
148
|
+
openapi_file_override: bool = False # denotes whether there is an override or not.
|
|
149
|
+
|
|
150
|
+
def __post_init__(self):
|
|
151
|
+
"""
|
|
152
|
+
Initializes the openapi_spec dict
|
|
153
|
+
"""
|
|
154
|
+
if self.openapi_file_override:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
self.openapi_spec = {
|
|
158
|
+
"openapi": "3.1.0",
|
|
159
|
+
"info": asdict(self.info),
|
|
160
|
+
"paths": {},
|
|
161
|
+
"components": asdict(self.info.components),
|
|
162
|
+
"servers": [asdict(server) for server in self.info.servers],
|
|
163
|
+
"externalDocs": asdict(self.info.externalDocs) if self.info.externalDocs.url else None,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str, openapi_tags: List[str], handler: Callable):
|
|
167
|
+
"""
|
|
168
|
+
Adds the given path to openapi spec
|
|
169
|
+
|
|
170
|
+
@param route_type: str the http method as string (get, post ...)
|
|
171
|
+
@param endpoint: str the endpoint to be added
|
|
172
|
+
@param openapi_name: str the name of the endpoint
|
|
173
|
+
@param openapi_tags: List[str] for grouping of endpoints
|
|
174
|
+
@param handler: Callable the handler function for the endpoint
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
if self.openapi_file_override:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
query_params = None
|
|
181
|
+
request_body = None
|
|
182
|
+
return_annotation = None
|
|
183
|
+
|
|
184
|
+
signature = inspect.signature(handler)
|
|
185
|
+
openapi_description = inspect.getdoc(handler) or ""
|
|
186
|
+
|
|
187
|
+
if signature:
|
|
188
|
+
parameters = signature.parameters
|
|
189
|
+
|
|
190
|
+
if "query_params" in parameters:
|
|
191
|
+
query_params = parameters["query_params"].default
|
|
192
|
+
|
|
193
|
+
if query_params is Signature.empty:
|
|
194
|
+
query_params = None
|
|
195
|
+
|
|
196
|
+
if "body" in parameters:
|
|
197
|
+
request_body = parameters["body"].default
|
|
198
|
+
|
|
199
|
+
if request_body is Signature.empty:
|
|
200
|
+
request_body = None
|
|
201
|
+
|
|
202
|
+
# priority to typing
|
|
203
|
+
for parameter in parameters:
|
|
204
|
+
param_annotation = parameters[parameter].annotation
|
|
205
|
+
|
|
206
|
+
if inspect.isclass(param_annotation):
|
|
207
|
+
if issubclass(param_annotation, Body):
|
|
208
|
+
request_body = param_annotation
|
|
209
|
+
elif issubclass(param_annotation, QueryParams):
|
|
210
|
+
query_params = param_annotation
|
|
211
|
+
|
|
212
|
+
if signature.return_annotation is not Signature.empty:
|
|
213
|
+
return_annotation = signature.return_annotation
|
|
214
|
+
|
|
215
|
+
modified_endpoint, path_obj = self.get_path_obj(
|
|
216
|
+
endpoint, openapi_name, openapi_description, openapi_tags, query_params, request_body, return_annotation
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if modified_endpoint not in self.openapi_spec["paths"]:
|
|
220
|
+
self.openapi_spec["paths"][modified_endpoint] = {}
|
|
221
|
+
self.openapi_spec["paths"][modified_endpoint][route_type] = path_obj
|
|
222
|
+
|
|
223
|
+
def add_subrouter_paths(self, subrouter_openapi: "OpenAPI"):
|
|
224
|
+
"""
|
|
225
|
+
Adds the subrouter paths to main router's openapi specs
|
|
226
|
+
|
|
227
|
+
@param subrouter_openapi: OpenAPI the OpenAPI object of the current subrouter
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if self.openapi_file_override:
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
paths = subrouter_openapi.openapi_spec["paths"]
|
|
234
|
+
|
|
235
|
+
for path in paths:
|
|
236
|
+
self.openapi_spec["paths"][path] = paths[path]
|
|
237
|
+
|
|
238
|
+
def get_path_obj(
|
|
239
|
+
self,
|
|
240
|
+
endpoint: str,
|
|
241
|
+
name: str,
|
|
242
|
+
description: str,
|
|
243
|
+
tags: List[str],
|
|
244
|
+
query_params: Optional[str_typed_dict],
|
|
245
|
+
request_body: Optional[str_typed_dict],
|
|
246
|
+
return_annotation: Optional[str_typed_dict],
|
|
247
|
+
) -> Tuple[str, dict]:
|
|
248
|
+
"""
|
|
249
|
+
Get the "path" openapi object according to spec
|
|
250
|
+
|
|
251
|
+
@param endpoint: str the endpoint to be added
|
|
252
|
+
@param name: str the name of the endpoint
|
|
253
|
+
@param description: Optional[str] short description of the endpoint (to be fetched from the endpoint definition by default)
|
|
254
|
+
@param tags: List[str] for grouping of endpoints
|
|
255
|
+
@param query_params: Optional[TypedDict] query params for the function
|
|
256
|
+
@param request_body: Optional[TypedDict] request body for the function
|
|
257
|
+
@param return_annotation: Optional[TypedDict] return type of the endpoint handler
|
|
258
|
+
|
|
259
|
+
@return: (str, dict) a tuple containing the endpoint with path params wrapped in braces and the "path" openapi object
|
|
260
|
+
according to spec
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
if not description:
|
|
264
|
+
description = "No description provided"
|
|
265
|
+
|
|
266
|
+
openapi_path_object: dict = {
|
|
267
|
+
"summary": name,
|
|
268
|
+
"description": description,
|
|
269
|
+
"parameters": [],
|
|
270
|
+
"tags": tags,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# robyn has paths like /:url/:etc whereas openapi requires path like /{url}/{path}
|
|
274
|
+
# this function is used for converting path params to the required form
|
|
275
|
+
# initialized with endpoint for handling endpoints without path params
|
|
276
|
+
endpoint_with_path_params_wrapped_in_braces = endpoint
|
|
277
|
+
|
|
278
|
+
endpoint_path_params_split = endpoint.split(":")
|
|
279
|
+
|
|
280
|
+
if len(endpoint_path_params_split) > 1:
|
|
281
|
+
endpoint_without_path_params = endpoint_path_params_split[0]
|
|
282
|
+
|
|
283
|
+
endpoint_with_path_params_wrapped_in_braces = (
|
|
284
|
+
endpoint_without_path_params[:-1] if endpoint_without_path_params.endswith("/") else endpoint_without_path_params
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for path_param in endpoint_path_params_split[1:]:
|
|
288
|
+
path_param_name = path_param[:-1] if path_param.endswith("/") else path_param
|
|
289
|
+
|
|
290
|
+
openapi_path_object["parameters"].append(
|
|
291
|
+
{
|
|
292
|
+
"name": path_param_name,
|
|
293
|
+
"in": "path",
|
|
294
|
+
"required": True,
|
|
295
|
+
"schema": {"type": "string"},
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
endpoint_with_path_params_wrapped_in_braces += "/{" + path_param_name + "}"
|
|
299
|
+
|
|
300
|
+
if query_params:
|
|
301
|
+
query_param_annotations = query_params.__annotations__ if query_params is str_typed_dict else typing.get_type_hints(query_params)
|
|
302
|
+
|
|
303
|
+
for query_param in query_param_annotations:
|
|
304
|
+
query_param_type = self.get_openapi_type(query_param_annotations[query_param])
|
|
305
|
+
|
|
306
|
+
openapi_path_object["parameters"].append(
|
|
307
|
+
{
|
|
308
|
+
"name": query_param,
|
|
309
|
+
"in": "query",
|
|
310
|
+
"required": False,
|
|
311
|
+
"schema": {"type": query_param_type},
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if request_body:
|
|
316
|
+
properties = {}
|
|
317
|
+
|
|
318
|
+
request_body_annotations = request_body.__annotations__ if request_body is TypedDict else typing.get_type_hints(request_body)
|
|
319
|
+
|
|
320
|
+
for body_item in request_body_annotations:
|
|
321
|
+
properties[body_item] = self.get_schema_object(body_item, request_body_annotations[body_item])
|
|
322
|
+
|
|
323
|
+
request_body_object = {
|
|
324
|
+
"content": {
|
|
325
|
+
"application/json": {
|
|
326
|
+
"schema": {
|
|
327
|
+
"type": "object",
|
|
328
|
+
"properties": properties,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
openapi_path_object["requestBody"] = request_body_object
|
|
335
|
+
|
|
336
|
+
response_schema: dict = {}
|
|
337
|
+
response_type = "text/plain"
|
|
338
|
+
|
|
339
|
+
if return_annotation and return_annotation is not Response:
|
|
340
|
+
response_type = "application/json"
|
|
341
|
+
response_schema = self.get_schema_object("response object", return_annotation)
|
|
342
|
+
|
|
343
|
+
openapi_path_object["responses"] = {"200": {"description": "Successful Response", "content": {response_type: {"schema": response_schema}}}}
|
|
344
|
+
|
|
345
|
+
return endpoint_with_path_params_wrapped_in_braces, openapi_path_object
|
|
346
|
+
|
|
347
|
+
def get_openapi_type(self, typed_dict: str_typed_dict) -> str:
|
|
348
|
+
"""
|
|
349
|
+
Get actual type from the TypedDict annotations
|
|
350
|
+
|
|
351
|
+
@param typed_dict: TypedDict The TypedDict to be converted
|
|
352
|
+
@return: str the type inferred
|
|
353
|
+
"""
|
|
354
|
+
type_mapping = {
|
|
355
|
+
int: "integer",
|
|
356
|
+
str: "string",
|
|
357
|
+
bool: "boolean",
|
|
358
|
+
float: "number",
|
|
359
|
+
dict: "object",
|
|
360
|
+
list: "array",
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for type_name in type_mapping:
|
|
364
|
+
if typed_dict is type_name:
|
|
365
|
+
return type_mapping[type_name]
|
|
366
|
+
|
|
367
|
+
# default to "string" if type is not found
|
|
368
|
+
return "string"
|
|
369
|
+
|
|
370
|
+
def get_schema_object(self, parameter: str, param_type: Any) -> dict:
|
|
371
|
+
"""
|
|
372
|
+
Get the schema object for request/response body
|
|
373
|
+
|
|
374
|
+
@param parameter: name of the parameter
|
|
375
|
+
@param param_type: Any the type to be inferred
|
|
376
|
+
@return: dict the properties object
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
properties: dict = {
|
|
380
|
+
"title": parameter.capitalize(),
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
type_mapping: dict = {
|
|
384
|
+
int: "integer",
|
|
385
|
+
str: "string",
|
|
386
|
+
bool: "boolean",
|
|
387
|
+
float: "number",
|
|
388
|
+
dict: "object",
|
|
389
|
+
list: "array",
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for type_name in type_mapping:
|
|
393
|
+
if param_type is type_name:
|
|
394
|
+
properties["type"] = type_mapping[type_name]
|
|
395
|
+
return properties
|
|
396
|
+
|
|
397
|
+
# Check if it's a generic type (like List[Object])
|
|
398
|
+
if hasattr(param_type, "__origin__"):
|
|
399
|
+
if param_type.__origin__ is list or param_type.__origin__ is List:
|
|
400
|
+
properties["type"] = "array"
|
|
401
|
+
# Handle the element type in the list
|
|
402
|
+
if hasattr(param_type, "__args__") and param_type.__args__:
|
|
403
|
+
item_type = param_type.__args__[0]
|
|
404
|
+
properties["items"] = self.get_schema_object(f"{parameter}_item", item_type)
|
|
405
|
+
return properties
|
|
406
|
+
|
|
407
|
+
# check for Optional type
|
|
408
|
+
if param_type.__module__ == "typing":
|
|
409
|
+
properties["anyOf"] = [{"type": self.get_openapi_type(param_type.__args__[0])}, {"type": "null"}]
|
|
410
|
+
return properties
|
|
411
|
+
# check for custom classes and TypedDicts
|
|
412
|
+
elif inspect.isclass(param_type):
|
|
413
|
+
properties["type"] = "object"
|
|
414
|
+
|
|
415
|
+
properties["properties"] = {}
|
|
416
|
+
|
|
417
|
+
for e in param_type.__annotations__:
|
|
418
|
+
properties["properties"][e] = self.get_schema_object(e, param_type.__annotations__[e])
|
|
419
|
+
|
|
420
|
+
properties["type"] = "object"
|
|
421
|
+
|
|
422
|
+
return properties
|
|
423
|
+
|
|
424
|
+
def override_openapi(self, openapi_json_spec_path: Path):
|
|
425
|
+
"""
|
|
426
|
+
Set a pre-configured OpenAPI spec
|
|
427
|
+
@param openapi_json_spec_path: str the path to the json file
|
|
428
|
+
"""
|
|
429
|
+
with open(openapi_json_spec_path) as json_file:
|
|
430
|
+
json_file_content = json.load(json_file)
|
|
431
|
+
self.openapi_spec = dict(json_file_content)
|
|
432
|
+
self.openapi_file_override = True
|
|
433
|
+
|
|
434
|
+
def get_openapi_docs_page(self) -> Response:
|
|
435
|
+
"""
|
|
436
|
+
Handler to the swagger html page to be deployed to the endpoint `/docs`
|
|
437
|
+
@return: FileResponse the swagger html page
|
|
438
|
+
"""
|
|
439
|
+
with resources.open_text("robyn", "swagger.html") as path:
|
|
440
|
+
html_file = path.read()
|
|
441
|
+
return html(html_file)
|
|
442
|
+
|
|
443
|
+
def get_openapi_config(self) -> dict:
|
|
444
|
+
"""
|
|
445
|
+
Returns the openapi spec as a dict
|
|
446
|
+
@return: dict the openapi spec
|
|
447
|
+
"""
|
|
448
|
+
return self.openapi_spec
|
robyn/processpool.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
import webbrowser
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from multiprocess import Process # type: ignore
|
|
8
|
+
|
|
9
|
+
from robyn.events import Events
|
|
10
|
+
from robyn.logger import logger
|
|
11
|
+
from robyn.robyn import FunctionInfo, Headers, Server, SocketHeld
|
|
12
|
+
from robyn.router import GlobalMiddleware, Route, RouteMiddleware
|
|
13
|
+
from robyn.types import Directory
|
|
14
|
+
from robyn.ws import WebSocket
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_processes(
|
|
18
|
+
url: str,
|
|
19
|
+
port: int,
|
|
20
|
+
directories: List[Directory],
|
|
21
|
+
request_headers: Headers,
|
|
22
|
+
routes: List[Route],
|
|
23
|
+
global_middlewares: List[GlobalMiddleware],
|
|
24
|
+
route_middlewares: List[RouteMiddleware],
|
|
25
|
+
web_sockets: Dict[str, WebSocket],
|
|
26
|
+
event_handlers: Dict[Events, FunctionInfo],
|
|
27
|
+
workers: int,
|
|
28
|
+
processes: int,
|
|
29
|
+
response_headers: Headers,
|
|
30
|
+
excluded_response_headers_paths: Optional[List[str]],
|
|
31
|
+
open_browser: bool,
|
|
32
|
+
client_timeout: int = 30,
|
|
33
|
+
keep_alive_timeout: int = 20,
|
|
34
|
+
) -> List[Process]:
|
|
35
|
+
socket = SocketHeld(url, port)
|
|
36
|
+
|
|
37
|
+
process_pool = init_processpool(
|
|
38
|
+
directories,
|
|
39
|
+
request_headers,
|
|
40
|
+
routes,
|
|
41
|
+
global_middlewares,
|
|
42
|
+
route_middlewares,
|
|
43
|
+
web_sockets,
|
|
44
|
+
event_handlers,
|
|
45
|
+
socket,
|
|
46
|
+
workers,
|
|
47
|
+
processes,
|
|
48
|
+
response_headers,
|
|
49
|
+
excluded_response_headers_paths,
|
|
50
|
+
client_timeout,
|
|
51
|
+
keep_alive_timeout,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def terminating_signal_handler(_sig, _frame):
|
|
55
|
+
logger.info("Terminating server!!", bold=True)
|
|
56
|
+
for process in process_pool:
|
|
57
|
+
process.kill()
|
|
58
|
+
|
|
59
|
+
signal.signal(signal.SIGINT, terminating_signal_handler)
|
|
60
|
+
signal.signal(signal.SIGTERM, terminating_signal_handler)
|
|
61
|
+
|
|
62
|
+
if open_browser:
|
|
63
|
+
logger.info("Opening browser...")
|
|
64
|
+
webbrowser.open_new_tab(f"http://{url}:{port}/")
|
|
65
|
+
|
|
66
|
+
logger.info("Press Ctrl + C to stop \n")
|
|
67
|
+
for process in process_pool:
|
|
68
|
+
process.join()
|
|
69
|
+
|
|
70
|
+
return process_pool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def init_processpool(
|
|
74
|
+
directories: List[Directory],
|
|
75
|
+
request_headers: Headers,
|
|
76
|
+
routes: List[Route],
|
|
77
|
+
global_middlewares: List[GlobalMiddleware],
|
|
78
|
+
route_middlewares: List[RouteMiddleware],
|
|
79
|
+
web_sockets: Dict[str, WebSocket],
|
|
80
|
+
event_handlers: Dict[Events, FunctionInfo],
|
|
81
|
+
socket: SocketHeld,
|
|
82
|
+
workers: int,
|
|
83
|
+
processes: int,
|
|
84
|
+
response_headers: Headers,
|
|
85
|
+
excluded_response_headers_paths: Optional[List[str]],
|
|
86
|
+
client_timeout: int = 30,
|
|
87
|
+
keep_alive_timeout: int = 20,
|
|
88
|
+
) -> List[Process]:
|
|
89
|
+
process_pool: List = []
|
|
90
|
+
if sys.platform.startswith("win32") or processes == 1:
|
|
91
|
+
spawn_process(
|
|
92
|
+
directories,
|
|
93
|
+
request_headers,
|
|
94
|
+
routes,
|
|
95
|
+
global_middlewares,
|
|
96
|
+
route_middlewares,
|
|
97
|
+
web_sockets,
|
|
98
|
+
event_handlers,
|
|
99
|
+
socket,
|
|
100
|
+
workers,
|
|
101
|
+
response_headers,
|
|
102
|
+
excluded_response_headers_paths,
|
|
103
|
+
client_timeout,
|
|
104
|
+
keep_alive_timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return process_pool
|
|
108
|
+
|
|
109
|
+
for _ in range(processes):
|
|
110
|
+
copied_socket = socket.try_clone()
|
|
111
|
+
process = Process(
|
|
112
|
+
target=spawn_process,
|
|
113
|
+
args=(
|
|
114
|
+
directories,
|
|
115
|
+
request_headers,
|
|
116
|
+
routes,
|
|
117
|
+
global_middlewares,
|
|
118
|
+
route_middlewares,
|
|
119
|
+
web_sockets,
|
|
120
|
+
event_handlers,
|
|
121
|
+
copied_socket,
|
|
122
|
+
workers,
|
|
123
|
+
response_headers,
|
|
124
|
+
excluded_response_headers_paths,
|
|
125
|
+
client_timeout,
|
|
126
|
+
keep_alive_timeout,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
process.start()
|
|
130
|
+
process_pool.append(process)
|
|
131
|
+
|
|
132
|
+
return process_pool
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def initialize_event_loop():
|
|
136
|
+
if sys.platform.startswith("win32") or sys.platform.startswith("linux-cross"):
|
|
137
|
+
loop = asyncio.new_event_loop()
|
|
138
|
+
asyncio.set_event_loop(loop)
|
|
139
|
+
return loop
|
|
140
|
+
else:
|
|
141
|
+
# uv loop doesn't support windows or arm machines at the moment
|
|
142
|
+
# but uv loop is much faster than native asyncio
|
|
143
|
+
import uvloop
|
|
144
|
+
|
|
145
|
+
uvloop.install()
|
|
146
|
+
loop = uvloop.new_event_loop()
|
|
147
|
+
asyncio.set_event_loop(loop)
|
|
148
|
+
return loop
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def spawn_process(
|
|
152
|
+
directories: List[Directory],
|
|
153
|
+
request_headers: Headers,
|
|
154
|
+
routes: List[Route],
|
|
155
|
+
global_middlewares: List[GlobalMiddleware],
|
|
156
|
+
route_middlewares: List[RouteMiddleware],
|
|
157
|
+
web_sockets: Dict[str, WebSocket],
|
|
158
|
+
event_handlers: Dict[Events, FunctionInfo],
|
|
159
|
+
socket: SocketHeld,
|
|
160
|
+
workers: int,
|
|
161
|
+
response_headers: Headers,
|
|
162
|
+
excluded_response_headers_paths: Optional[List[str]],
|
|
163
|
+
client_timeout: int = 30,
|
|
164
|
+
keep_alive_timeout: int = 20,
|
|
165
|
+
):
|
|
166
|
+
"""
|
|
167
|
+
This function is called by the main process handler to create a server runtime.
|
|
168
|
+
This functions allows one runtime per process.
|
|
169
|
+
|
|
170
|
+
:param directories List: the list of all the directories and related data
|
|
171
|
+
:param headers tuple: All the global headers in a tuple
|
|
172
|
+
:param routes Tuple[Route]: The routes tuple, containing the description about every route.
|
|
173
|
+
:param middlewares Tuple[Route]: The middleware routes tuple, containing the description about every route.
|
|
174
|
+
:param web_sockets list: This is a list of all the web socket routes
|
|
175
|
+
:param event_handlers Dict: This is an event dict that contains the event handlers
|
|
176
|
+
:param socket SocketHeld: This is the main tcp socket, which is being shared across multiple processes.
|
|
177
|
+
:param process_name string: This is the name given to the process to identify the process
|
|
178
|
+
:param workers int: This is the name given to the process to identify the process
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
loop = initialize_event_loop()
|
|
182
|
+
|
|
183
|
+
server = Server()
|
|
184
|
+
|
|
185
|
+
# TODO: if we remove the dot access
|
|
186
|
+
# the startup time will improve in the server
|
|
187
|
+
for directory in directories:
|
|
188
|
+
server.add_directory(*directory.as_list())
|
|
189
|
+
|
|
190
|
+
server.apply_request_headers(request_headers)
|
|
191
|
+
|
|
192
|
+
server.apply_response_headers(response_headers)
|
|
193
|
+
|
|
194
|
+
server.set_response_headers_exclude_paths(excluded_response_headers_paths)
|
|
195
|
+
|
|
196
|
+
for route in routes:
|
|
197
|
+
route_type, endpoint, function, is_const, auth_required, openapi_name, openapi_tags = route
|
|
198
|
+
server.add_route(route_type, endpoint, function, is_const)
|
|
199
|
+
|
|
200
|
+
for middleware_type, middleware_function in global_middlewares:
|
|
201
|
+
server.add_global_middleware(middleware_type, middleware_function)
|
|
202
|
+
|
|
203
|
+
for middleware_type, endpoint, function, route_type in route_middlewares:
|
|
204
|
+
server.add_middleware_route(middleware_type, endpoint, function, route_type)
|
|
205
|
+
|
|
206
|
+
if Events.STARTUP in event_handlers:
|
|
207
|
+
server.add_startup_handler(event_handlers[Events.STARTUP])
|
|
208
|
+
|
|
209
|
+
if Events.SHUTDOWN in event_handlers:
|
|
210
|
+
server.add_shutdown_handler(event_handlers[Events.SHUTDOWN])
|
|
211
|
+
|
|
212
|
+
for endpoint in web_sockets:
|
|
213
|
+
web_socket = web_sockets[endpoint]
|
|
214
|
+
server.add_web_socket_route(
|
|
215
|
+
endpoint,
|
|
216
|
+
web_socket.methods["connect"],
|
|
217
|
+
web_socket.methods["close"],
|
|
218
|
+
web_socket.methods["message"],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
server.start(socket, workers)
|
|
223
|
+
loop = asyncio.get_event_loop()
|
|
224
|
+
loop.run_forever()
|
|
225
|
+
except KeyboardInterrupt:
|
|
226
|
+
loop.close()
|
robyn/py.typed
ADDED
|
File without changes
|