1
1
use actix_http:: { body:: BoxBody , StatusCode } ;
2
- use actix_web:: { HttpRequest , HttpResponse , HttpResponseBuilder , Responder } ;
2
+ use actix_web:: { HttpRequest , HttpResponse , HttpResponseBuilder , Responder , Error , web :: Bytes } ;
3
3
use pyo3:: {
4
4
exceptions:: PyIOError ,
5
5
prelude:: * ,
6
- types:: { PyBytes , PyDict } ,
6
+ types:: { PyBytes , PyDict , PyList } ,
7
7
} ;
8
+ use futures:: stream:: Stream ;
9
+ use futures_util:: StreamExt ;
10
+ use std:: pin:: Pin ;
8
11
9
12
use crate :: io_helpers:: { apply_hashmap_headers, read_file} ;
10
13
use crate :: types:: { check_body_type, check_description_type, get_description_from_pyobject} ;
11
14
12
15
use super :: headers:: Headers ;
13
16
14
- #[ derive( Debug , Clone , FromPyObject ) ]
17
+ #[ derive( Debug , Clone ) ]
18
+ pub enum ResponseBody {
19
+ Static ( Vec < u8 > ) ,
20
+ Streaming ( Vec < Vec < u8 > > ) ,
21
+ }
22
+
23
+ #[ derive( Debug , Clone ) ]
15
24
pub struct Response {
16
25
pub status_code : u16 ,
17
26
pub response_type : String ,
18
27
pub headers : Headers ,
19
- // https://pyo3.rs/v0.19.2/function.html?highlight=from_py_#per-argument-options
20
- #[ pyo3( from_py_with = "get_description_from_pyobject" ) ]
21
- pub description : Vec < u8 > ,
28
+ pub body : ResponseBody ,
22
29
pub file_path : Option < String > ,
23
30
}
24
31
32
+ impl < ' a > FromPyObject < ' a > for Response {
33
+ fn extract ( ob : & ' a PyAny ) -> PyResult < Self > {
34
+ let status_code = ob. getattr ( "status_code" ) ?. extract ( ) ?;
35
+ let response_type = ob. getattr ( "response_type" ) ?. extract ( ) ?;
36
+ let headers = ob. getattr ( "headers" ) ?. extract ( ) ?;
37
+ let description = ob. getattr ( "description" ) ?;
38
+ let file_path = ob. getattr ( "file_path" ) ?. extract ( ) ?;
39
+
40
+ let body = if let Ok ( iter) = description. iter ( ) {
41
+ let mut chunks = Vec :: new ( ) ;
42
+ for item in iter {
43
+ let item = item?;
44
+ let chunk = if item. is_instance_of :: < pyo3:: types:: PyBytes > ( ) {
45
+ item. extract :: < Vec < u8 > > ( ) ?
46
+ } else if item. is_instance_of :: < pyo3:: types:: PyString > ( ) {
47
+ item. extract :: < String > ( ) ?. into_bytes ( )
48
+ } else if item. is_instance_of :: < pyo3:: types:: PyInt > ( ) {
49
+ item. extract :: < i64 > ( ) ?. to_string ( ) . into_bytes ( )
50
+ } else {
51
+ return Err ( PyErr :: new :: < pyo3:: exceptions:: PyTypeError , _ > (
52
+ "Stream items must be bytes, str, or int"
53
+ ) ) ;
54
+ } ;
55
+ chunks. push ( chunk) ;
56
+ }
57
+ ResponseBody :: Streaming ( chunks)
58
+ } else {
59
+ ResponseBody :: Static ( get_description_from_pyobject ( description) ?)
60
+ } ;
61
+
62
+ Ok ( Response {
63
+ status_code,
64
+ response_type,
65
+ headers,
66
+ body,
67
+ file_path,
68
+ } )
69
+ }
70
+ }
71
+
25
72
impl Responder for Response {
26
73
type Body = BoxBody ;
27
74
28
75
fn respond_to ( self , _req : & HttpRequest ) -> HttpResponse < Self :: Body > {
29
76
let mut response_builder =
30
77
HttpResponseBuilder :: new ( StatusCode :: from_u16 ( self . status_code ) . unwrap ( ) ) ;
31
78
apply_hashmap_headers ( & mut response_builder, & self . headers ) ;
32
- response_builder. body ( self . description )
79
+
80
+ match self . body {
81
+ ResponseBody :: Static ( data) => response_builder. body ( data) ,
82
+ ResponseBody :: Streaming ( chunks) => {
83
+ let stream = Box :: pin (
84
+ futures:: stream:: iter ( chunks. into_iter ( ) )
85
+ . map ( |chunk| Ok :: < Bytes , Error > ( Bytes :: from ( chunk) ) )
86
+ ) as Pin < Box < dyn Stream < Item = Result < Bytes , Error > > > > ;
87
+ response_builder. streaming ( stream)
88
+ }
89
+ }
33
90
}
34
91
}
35
92
@@ -44,7 +101,7 @@ impl Response {
44
101
status_code : 404 ,
45
102
response_type : "text" . to_string ( ) ,
46
103
headers,
47
- description : "Not found" . to_owned ( ) . into_bytes ( ) ,
104
+ body : ResponseBody :: Static ( "Not found" . to_owned ( ) . into_bytes ( ) ) ,
48
105
file_path : None ,
49
106
}
50
107
}
@@ -59,7 +116,7 @@ impl Response {
59
116
status_code : 500 ,
60
117
response_type : "text" . to_string ( ) ,
61
118
headers,
62
- description : "Internal server error" . to_owned ( ) . into_bytes ( ) ,
119
+ body : ResponseBody :: Static ( "Internal server error" . to_owned ( ) . into_bytes ( ) ) ,
63
120
file_path : None ,
64
121
}
65
122
}
@@ -68,11 +125,21 @@ impl Response {
68
125
impl ToPyObject for Response {
69
126
fn to_object ( & self , py : Python ) -> PyObject {
70
127
let headers = self . headers . clone ( ) . into_py ( py) . extract ( py) . unwrap ( ) ;
71
- // The description should only be either string or binary.
72
- // it should raise an exception otherwise
73
- let description = match String :: from_utf8 ( self . description . to_vec ( ) ) {
74
- Ok ( description) => description. to_object ( py) ,
75
- Err ( _) => PyBytes :: new ( py, & self . description . to_vec ( ) ) . into ( ) ,
128
+
129
+ let description = match & self . body {
130
+ ResponseBody :: Static ( data) => {
131
+ match String :: from_utf8 ( data. to_vec ( ) ) {
132
+ Ok ( description) => description. to_object ( py) ,
133
+ Err ( _) => PyBytes :: new ( py, data) . into ( ) ,
134
+ }
135
+ } ,
136
+ ResponseBody :: Streaming ( chunks) => {
137
+ let list = PyList :: empty ( py) ;
138
+ for chunk in chunks {
139
+ list. append ( PyBytes :: new ( py, chunk) ) . unwrap ( ) ;
140
+ }
141
+ list. to_object ( py)
142
+ }
76
143
} ;
77
144
78
145
let response = PyResponse {
@@ -111,15 +178,22 @@ impl PyResponse {
111
178
headers : & PyAny ,
112
179
description : Py < PyAny > ,
113
180
) -> PyResult < Self > {
114
- check_body_type ( py, & description) ?;
181
+ // Check if description is an iterator/generator
182
+ let is_stream = Python :: with_gil ( |py| {
183
+ description. as_ref ( py) . iter ( ) . is_ok ( )
184
+ } ) ;
185
+
186
+ if is_stream {
187
+ // For streaming responses, we don't need to check body type
188
+ // as we'll validate each chunk when it's yielded
189
+ } else {
190
+ check_body_type ( py, & description) ?;
191
+ }
115
192
116
193
let headers_output: Py < Headers > = if let Ok ( headers_dict) = headers. downcast :: < PyDict > ( ) {
117
- // Here you'd have logic to create a Headers instance from a PyDict
118
- // For simplicity, let's assume you have a method `from_dict` on Headers for this
119
- let headers = Headers :: new ( Some ( headers_dict) ) ; // Hypothetical method
194
+ let headers = Headers :: new ( Some ( headers_dict) ) ;
120
195
Py :: new ( py, headers) ?
121
196
} else if let Ok ( headers) = headers. extract :: < Py < Headers > > ( ) {
122
- // If it's already a Py<Headers>, use it directly
123
197
headers
124
198
} else {
125
199
return Err ( PyErr :: new :: < pyo3:: exceptions:: PyTypeError , _ > (
@@ -129,8 +203,7 @@ impl PyResponse {
129
203
130
204
Ok ( Self {
131
205
status_code,
132
- // we should be handling based on headers but works for now
133
- response_type : "text" . to_string ( ) ,
206
+ response_type : if is_stream { "stream" . to_string ( ) } else { "text" . to_string ( ) } ,
134
207
headers : headers_output,
135
208
description,
136
209
file_path : None ,
@@ -139,7 +212,16 @@ impl PyResponse {
139
212
140
213
#[ setter]
141
214
pub fn set_description ( & mut self , py : Python , description : Py < PyAny > ) -> PyResult < ( ) > {
142
- check_description_type ( py, & description) ?;
215
+ // Check if description is an iterator/generator
216
+ let is_stream = description. as_ref ( py) . iter ( ) . is_ok ( ) ;
217
+
218
+ if is_stream {
219
+ self . response_type = "stream" . to_string ( ) ;
220
+ } else {
221
+ check_description_type ( py, & description) ?;
222
+ self . response_type = "text" . to_string ( ) ;
223
+ }
224
+
143
225
self . description = description;
144
226
Ok ( ( ) )
145
227
}
0 commit comments