Skip to content

Commit

Permalink
Merge branch 'main' into split-request-params
Browse files Browse the repository at this point in the history
  • Loading branch information
VishnuSanal authored Sep 25, 2024
2 parents 62d0abd + 399f5b1 commit 6e192f0
Show file tree
Hide file tree
Showing 11 changed files with 584 additions and 471 deletions.
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ Please ensure that:

## Pre-Commit Instructions:

- [ ] Ensure that you have run the [pre-commit hooks](https://github.com/sansyrox/robyn#%EF%B8%8F-to-develop-locally) on your PR.
- [ ] Ensure that you have run the [pre-commit hooks](https://github.com/sparckles/robyn#%EF%B8%8F-to-develop-locally) on your PR.

878 changes: 439 additions & 439 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ cd robyn
**4.** Add a reference to the original repository.

```
git remote add upstream https://github.com/sansyrox/robyn.git
git remote add upstream https://github.com/sparckles/robyn.git
```

**5.** See latest changes to the repo using
Expand Down Expand Up @@ -98,7 +98,7 @@ git push -u origin <YOUR_BRANCH_NAME>

Let's walk through the steps to create an issue:

**1.** On GitHub, navigate to the main page of the repository. [Here](https://github.com/sansyrox/robyn.git) in this case.
**1.** On GitHub, navigate to the main page of the repository. [Here](https://github.com/sparckles/robyn.git) in this case.

**2.** Under your repository name, click on the `Issues` button.
<br><br><img src="https://www.stevejgordon.co.uk/wp-content/uploads/2018/01/GitHubIssueTab.png" width="750" /><br>
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ authors = ["Sanskar Jethi <sansyrox@gmail.com>"]
edition = "2021"
description = "A web server that is fast!"
license = "BSD License (BSD)"
homepage = "https://github.com/sansyrox/robyn"
homepage = "https://github.com/sparckles/robyn"
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Twitter](https://badgen.net/badge/icon/twitter?icon=twitter&label)](https://twitter.com/Robyn_oss)
[![Downloads](https://static.pepy.tech/personalized-badge/Robyn?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Downloads)](https://pepy.tech/project/Robyn)
[![GitHub tag](https://img.shields.io/github/tag/sparckles/Robyn?include_prereleases=&sort=semver&color=black)](https://github.com/sparckles/Robyn/releases/)
[![License](https://img.shields.io/badge/License-BSD_2.0-black)](#license)
[![License](https://img.shields.io/badge/License-BSD_2.0-black)](https://github.com/sparckles/Robyn/blob/main/LICENSE)
![Python](https://img.shields.io/badge/Support-Version%20%E2%89%A5%203.8-brightgreen)

[![view - Documentation](https://img.shields.io/badge/view-Documentation-blue?style=for-the-badge)](https://robyn.tech/documentation)
Expand Down
25 changes: 19 additions & 6 deletions docs_src/src/pages/documentation/api_reference/openapi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,13 @@ app.include_router(subrouter)

## Other Specification Params

We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example of using request body below.
We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example using request & response bodies below:

<CodeGroup title="Request Body">
<CodeGroup title="Request & Response Body">

```python {{ title: 'untyped' }}
from robyn.types import JSONResponse

class Initial(TypedDict):
is_present: bool
letter: Optional[str]
Expand All @@ -240,12 +242,19 @@ class CreateItemBody(TypedDict):
tax: float


class CreateResponse(JSONResponse):
success: bool
items_changed: int


@app.post("/")
def create_item(request, body=CreateItemBody):
return request.body
def create_item(request: Request, body=CreateItemBody) -> CreateResponse:
return CreateResponse(success=True, items_changed=2)
```

```python {{ title: 'typed' }}
from robyn.types import JSONResponse

class Initial(TypedDict):
is_present: bool
letter: Optional[str]
Expand All @@ -263,10 +272,14 @@ class CreateItemBody(TypedDict):
price: float
tax: float

class CreateResponse(JSONResponse):
success: bool
items_changed: int


@app.post("/")
def create_item(request: Request, body=CreateItemBody):
return request.body
def create_item(request: Request, body=CreateItemBody) -> CreateResponse:
return CreateResponse(success=True, items_changed=2)
```

</CodeGroup>
Expand Down
25 changes: 19 additions & 6 deletions docs_src/src/pages/documentation/example_app/openapi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,13 @@ app.include_router(subrouter)

## Other Specification Params

We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example of using request body below.
We support all the params mentioned in the latest OpenAPI specifications (https://swagger.io/specification/). See an example using request & response bodies below:

<CodeGroup title="Request Body">
<CodeGroup title="Request & Response Body">

```python {{ title: 'untyped' }}
from robyn.types import JSONResponse

class Initial(TypedDict):
is_present: bool
letter: Optional[str]
Expand All @@ -198,12 +200,19 @@ class CreateItemBody(TypedDict):
tax: float


class CreateResponse(JSONResponse):
success: bool
items_changed: int


@app.post("/")
def create_item(request, body=CreateItemBody):
return request.body
def create_item(request: Request, body=CreateItemBody) -> CreateResponse:
return {"success": True, "items_changed": 2}
```

```python {{ title: 'typed' }}
from robyn.types import JSONResponse

class Initial(TypedDict):
is_present: bool
letter: Optional[str]
Expand All @@ -221,10 +230,14 @@ class CreateItemBody(TypedDict):
price: float
tax: float

class CreateResponse(JSONResponse):
success: bool
items_changed: int


@app.post("/")
def create_item(request: Request, body=CreateItemBody):
return request.body
def create_item(request: Request, body=CreateItemBody) -> CreateResponse:
return CreateResponse(success=True, items_changed=2)
```

</CodeGroup>
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ class CreateItemBody(TypedDict):


@app.post("/openapi_request_body")
def create_item(request, body=CreateItemBody):
def create_item(request, body=CreateItemBody) -> CreateItemBody:
return request.body


Expand Down
75 changes: 75 additions & 0 deletions integration_tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,78 @@ def test_openapi_request_body():
assert {"type": "null"} in openapi_spec["paths"][endpoint][route_type]["requestBody"]["content"]["application/json"]["schema"]["properties"]["name"][
"properties"
]["initial"]["properties"]["letter"]["anyOf"]


@pytest.mark.benchmark
def test_openapi_response_body():
openapi_spec = get("/openapi.json").json()

assert isinstance(openapi_spec, dict)

route_type = "post"
endpoint = "/openapi_request_body"

assert endpoint in openapi_spec["paths"]
assert route_type in openapi_spec["paths"][endpoint]
assert "responses" in openapi_spec["paths"][endpoint][route_type]
assert "200" in openapi_spec["paths"][endpoint][route_type]["responses"]

assert openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["description"] == "Successful Response"

assert "content" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]

assert "application/json" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]
assert "schema" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]
assert "properties" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]

assert "name" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]
assert "description" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]
assert "price" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]
assert "tax" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]

assert (
"string"
== openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["description"]["type"]
)
assert "number" == openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["price"]["type"]
assert "number" == openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["tax"]["type"]

assert "object" == openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["type"]

assert (
"first" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
)
assert (
"second" in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
)
assert (
"initial"
in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"]
)

assert (
"object"
in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"][
"initial"
]["type"]
)

assert (
"is_present"
in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"][
"initial"
]["properties"]
)
assert (
"letter"
in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"]["properties"][
"initial"
]["properties"]
)

assert {"type": "string"} in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"][
"properties"
]["initial"]["properties"]["letter"]["anyOf"]
assert {"type": "null"} in openapi_spec["paths"][endpoint][route_type]["responses"]["200"]["content"]["application/json"]["schema"]["properties"]["name"][
"properties"
]["initial"]["properties"]["letter"]["anyOf"]
33 changes: 22 additions & 11 deletions robyn/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from inspect import Signature
from typing import Callable, Dict, List, Optional, TypedDict, Any

from robyn import Response
from robyn.responses import FileResponse, html


Expand Down Expand Up @@ -162,6 +163,7 @@ def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str

query_params = None
request_body = None
return_annotation = None

signature = inspect.signature(handler)
openapi_description = inspect.getdoc(handler) or ""
Expand All @@ -179,11 +181,12 @@ def add_openapi_path_obj(self, route_type: str, endpoint: str, openapi_name: str
if request_body is Signature.empty:
request_body = None

return_annotation = signature.return_annotation
if signature.return_annotation is not Signature.empty:
return_annotation = signature.return_annotation

return_type = "text/plain" if return_annotation == Signature.empty or return_annotation is str else "application/json"

modified_endpoint, path_obj = self.get_path_obj(endpoint, openapi_name, openapi_description, openapi_tags, query_params, request_body, return_type)
modified_endpoint, path_obj = self.get_path_obj(
endpoint, openapi_name, openapi_description, openapi_tags, query_params, request_body, return_annotation
)

if modified_endpoint not in self.openapi_spec["paths"]:
self.openapi_spec["paths"][modified_endpoint] = {}
Expand All @@ -208,7 +211,7 @@ def get_path_obj(
tags: List[str],
query_params: Optional[TypedDict],
request_body: Optional[TypedDict],
return_type: str,
return_annotation: Optional[TypedDict],
) -> (str, dict):
"""
Get the "path" openapi object according to spec
Expand All @@ -219,7 +222,7 @@ def get_path_obj(
@param tags: List[str] for grouping of endpoints
@param query_params: Optional[TypedDict] query params for the function
@param request_body: Optional[TypedDict] request body for the function
@param return_type: str return type of the endpoint handler
@param return_annotation: Optional[TypedDict] return type of the endpoint handler
@return: (str, dict) a tuple containing the endpoint with path params wrapped in braces and the "path" openapi object
according to spec
Expand All @@ -233,7 +236,6 @@ def get_path_obj(
"description": description,
"parameters": [],
"tags": tags,
"responses": {"200": {"description": "Successful Response", "content": {return_type: {"schema": {}}}}},
}

# robyn has paths like /:url/:etc whereas openapi requires path like /{url}/{path}
Expand Down Expand Up @@ -280,7 +282,7 @@ def get_path_obj(
properties = {}

for body_item in request_body.__annotations__:
properties[body_item] = self.get_properties_object(body_item, request_body.__annotations__[body_item])
properties[body_item] = self.get_schema_object(body_item, request_body.__annotations__[body_item])

request_body_object = {
"content": {
Expand All @@ -295,6 +297,15 @@ def get_path_obj(

openapi_path_object["requestBody"] = request_body_object

response_schema = {}
response_type = "text/plain"

if return_annotation and return_annotation is not Response:
response_type = "application/json"
response_schema = self.get_schema_object("response object", return_annotation)

openapi_path_object["responses"] = {"200": {"description": "Successful Response", "content": {response_type: {"schema": response_schema}}}}

return endpoint_with_path_params_wrapped_in_braces, openapi_path_object

def get_openapi_type(self, typed_dict: TypedDict) -> str:
Expand All @@ -320,9 +331,9 @@ def get_openapi_type(self, typed_dict: TypedDict) -> str:
# default to "string" if type is not found
return "string"

def get_properties_object(self, parameter: str, param_type: Any) -> dict:
def get_schema_object(self, parameter: str, param_type: Any) -> dict:
"""
Get the properties object for request body
Get the schema object for request/response body
@param parameter: name of the parameter
@param param_type: Any the type to be inferred
Expand Down Expand Up @@ -358,7 +369,7 @@ def get_properties_object(self, parameter: str, param_type: Any) -> dict:
properties["properties"] = {}

for e in param_type.__annotations__:
properties["properties"][e] = self.get_properties_object(e, param_type.__annotations__[e])
properties["properties"][e] = self.get_schema_object(e, param_type.__annotations__[e])

properties["type"] = "object"

Expand Down
7 changes: 4 additions & 3 deletions robyn/types.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from dataclasses import dataclass
from typing import Dict, Optional, Union, NewType
from typing import Dict, Optional, Union, NewType, TypedDict

from robyn.robyn import Identity, Url


@dataclass
class Directory:
route: str
Expand All @@ -19,7 +18,6 @@ def as_list(self):
self.index_file,
]


PathParams = NewType("PathParams", Dict[str, str])
RequestBody = NewType("RequestBody", Union[str, bytes])
RequestMethod = NewType("RequestMethod", str)
Expand All @@ -28,3 +26,6 @@ def as_list(self):
RequestFiles = NewType("RequestFiles", Dict[str, bytes])
RequestIP = NewType("RequestIP", Optional[str])
RequestIdentity = NewType("RequestIdentity", Optional[Identity])

class JSONResponse(TypedDict):
pass

0 comments on commit 6e192f0

Please sign in to comment.