Skip to content

Commit

Permalink
feat: add form data handling and file handling (sparckles#778)
Browse files Browse the repository at this point in the history
* feat: add form data handling and file handling

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
sansyrox and pre-commit-ci[bot] authored Apr 2, 2024
1 parent 7309086 commit e253acb
Show file tree
Hide file tree
Showing 14 changed files with 496 additions and 48 deletions.
260 changes: 240 additions & 20 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pythonize = "0.19.0"
serde = "1.0.178"
serde_json = "1.0.104"
once_cell = "1.8.0"
actix-multipart = "0.6.1"

[features]
io-uring = ["actix-web/experimental-io-uring"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ python --version
- Multi Core Scaling
- WebSockets!
- Middlewares
- Built in form data handling
- Dependency Injection
- Hot Reloading
- Direct Rust Integration
Expand Down
5 changes: 5 additions & 0 deletions docs_src/src/components/documentation/ApiDocs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ const guides = [
description:
'Learn how to upload and download files to your server using Robyn.',
},
{
href: '/documentation/api_reference/form_data',
name: 'Form Data',
description: 'Learn how to handle form data.',
},
{
href: '/documentation/api_reference/websockets',
name: 'Websockets',
Expand Down
4 changes: 4 additions & 0 deletions docs_src/src/components/documentation/Navigation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ export const navigation = [
href: '/documentation/api_reference/file-uploads',
title: 'File Uploads',
},
{
href: '/documentation/api_reference/form_data',
title: 'Form Data',
},
{
href: '/documentation/api_reference/websockets',
title: 'Websockets',
Expand Down
48 changes: 41 additions & 7 deletions docs_src/src/pages/documentation/api_reference/file-uploads.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const description =
Batman learned how to handle file uploads using Robyn. He created an endpoint to handle file uploads using the following code:


## Scaling the Application
## Sending a File without MultiPart Form Data

<Row>
<Col>
Expand All @@ -17,8 +17,6 @@ Batman scaled his application across multiple cores for better performance. He u
<CodeGroup title="Request" tag="GET" label="/hello_world">

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

@app.post("/upload")
async def upload():
body = request.body
Expand All @@ -31,8 +29,6 @@ Batman scaled his application across multiple cores for better performance. He u
return {'message': 'success'}
```
```python {{ title: 'typed' }}
from robyn.robyn import File

@app.post("/upload")
async def upload():
body = request.body
Expand All @@ -49,6 +45,44 @@ Batman scaled his application across multiple cores for better performance. He u
</Col>
</Row>


## Sending a File with MultiPart Form Data

<Row>
<Col>
Batman scaled his application across multiple cores for better performance. He used the following command:
</Col>
<Col sticky>

<CodeGroup title="Request" tag="GET" label="/hello_world">

```python {{ title: 'untyped' }}
@app.post("/upload")
async def upload(request):
file = request.files['filename']

# write whatever filename
with open('filename', 'wb') as f:
f.write(file)

return {'message': 'success'}
```
```python {{ title: 'typed' }}
@app.post("/upload")
async def upload(request: Request):
file = request.files['filename']

# write whatever filename
with open('filename', 'wb') as f:
f.write(file)

return {'message': 'success'}
```

</CodeGroup>
</Col>
</Row>

---

## File Downloads
Expand Down Expand Up @@ -186,9 +220,9 @@ Batman scaled his application across multiple cores for better performance. He u

## What's next?

Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard.
Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to handle form data

- [WebSockets](/documentation/api_reference/websockets)
- [Form Data](/documentation/api_reference/form_data)



43 changes: 43 additions & 0 deletions docs_src/src/pages/documentation/api_reference/form_data.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const description =
'On this page, we’ll dive into using the form data.'

## Fom Data

Batman learned how to handle file uploads using Robyn. Now, he wanted to handle the form data


## Handling Form Data

<Row>
<Col>
Batman uploaded some multipart form data and wanted to handle it using the following code:
</Col>
<Col sticky>

<CodeGroup title="Request" tag="GET" label="/hello_world">

```python {{ title: 'untyped' }}
@app.post("/upload")
async def upload(request):
form_data = request.from_data

return form_data
```
```python {{ title: 'typed' }}
@app.post("/upload")
async def upload(request):
form_data = request.from_data

return form_data
```

</CodeGroup>
</Col>
</Row>


## What's next?

Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard.

- [WebSockets](/documentation/api_reference/websockets)
17 changes: 17 additions & 0 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,17 @@ async def file_download_async():
return serve_file(file_path)


# Multipart file


@app.post("/sync/multipart-file")
def sync_multipart_file(request: Request):
file = request.files["test.png"]
with open("test.png", "wb") as f:
f.write(file)
return "File uploaded"


# Queries


Expand Down Expand Up @@ -548,6 +559,11 @@ async def async_body_post(request: Request):
return request.body


@app.post("/sync/form_data")
def sync_form_data(request: Request):
return request.form_data


# JSON Request


Expand Down Expand Up @@ -631,6 +647,7 @@ async def async_dict_delete():

@app.delete("/sync/body")
def sync_body_delete(request: Request):
print(request.body)
return request.body


Expand Down
2 changes: 1 addition & 1 deletion integration_tests/helpers/http_methods_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ def check_response(response: requests.Response, expected_status_code: int):
headers is not present in the response.
"""
assert response.status_code == expected_status_code
print(response.headers)
assert response.headers.get("global_after") == "global_after_request"
assert "server" in response.headers
assert response.headers.get("server") == "robyn"
print("This is the response text", response)


def get(
Expand Down
9 changes: 7 additions & 2 deletions robyn/robyn.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,13 @@ class Request:
Attributes:
query_params (QueryParams): The query parameters of the request. e.g. /user?id=123 -> {"id": "123"}
headers Headers: The headers of the request. e.g. Headers({"Content-Type": "application/json"})
params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"}
path_params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"}
body (Union[str, bytes]): The body of the request. If the request is a JSON, it will be a dict.
method (str): The method of the request. e.g. GET, POST, PUT, DELETE
url (Url): The url of the request. e.g. https://localhost/user
form_data (dict[str, str]): The form data of the request. e.g. {"name": "John"}
files (dict[str, bytes]): The files of the request. e.g. {"file": b"file"}
ip_addr (Optional[str]): The IP Address of the client
identity (Optional[Identity]): The identity of the client
"""

query_params: QueryParams
Expand All @@ -258,6 +261,8 @@ class Request:
body: Union[str, bytes]
method: str
url: Url
form_data: dict[str, str]
files: dict[str, bytes]
ip_addr: Optional[str]
identity: Optional[Identity]

Expand Down
9 changes: 4 additions & 5 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ use std::{env, thread};

use actix_files::Files;
use actix_http::KeepAlive;
use actix_web::web::Bytes;
use actix_web::*;

// pyO3 module
Expand Down Expand Up @@ -190,18 +189,18 @@ impl Server {
move |router: web::Data<Arc<HttpRouter>>,
const_router: web::Data<Arc<ConstRouter>>,
middleware_router: web::Data<Arc<MiddlewareRouter>>,
payload: web::Payload,
global_request_headers,
global_response_headers,
body,
req| {
pyo3_asyncio::tokio::scope_local(task_locals.clone(), async move {
index(
router,
payload,
const_router,
middleware_router,
global_request_headers,
global_response_headers,
body,
req,
)
.await
Expand Down Expand Up @@ -366,14 +365,14 @@ impl Default for Server {
/// path, and returns a Future of a Response.
async fn index(
router: web::Data<Arc<HttpRouter>>,
payload: web::Payload,
const_router: web::Data<Arc<ConstRouter>>,
middleware_router: web::Data<Arc<MiddlewareRouter>>,
global_request_headers: web::Data<Arc<Headers>>,
global_response_headers: web::Data<Arc<Headers>>,
body: Bytes,
req: HttpRequest,
) -> impl Responder {
let mut request = Request::from_actix_request(&req, body, &global_request_headers);
let mut request = Request::from_actix_request(&req, payload, &global_request_headers).await;

// Before middleware
// Global
Expand Down
17 changes: 8 additions & 9 deletions src/types/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ impl Headers {

let new_value = value.downcast::<PyList>();

if new_value.is_err() {
if let Ok(new_value) = new_value {
let value: Vec<String> = new_value.iter().map(|x| x.to_string()).collect();
headers.entry(key).or_insert_with(Vec::new).extend(value);
} else {
let value = value.to_string();
headers.entry(key).or_insert_with(Vec::new).push(value);
} else {
let value: Vec<String> =
new_value.unwrap().iter().map(|x| x.to_string()).collect();
headers.entry(key).or_insert_with(Vec::new).extend(value);
}
}
Headers { headers }
Expand Down Expand Up @@ -100,12 +99,12 @@ impl Headers {
let key = key.to_string().to_lowercase();
let new_value = value.downcast::<PyList>();

if new_value.is_err() {
if let Ok(new_value) = new_value {
let value: Vec<String> = new_value.iter().map(|x| x.to_string()).collect();
self.headers.entry(key).or_default().extend(value);
} else {
let value = value.to_string();
self.headers.entry(key).or_default().push(value);
} else {
let value: Vec<String> = new_value.unwrap().iter().map(|x| x.to_string()).collect();
self.headers.entry(key).or_default().extend(value);
}
}
}
Expand Down
Loading

0 comments on commit e253acb

Please sign in to comment.