Compare commits
696 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b24c35ac7 | ||
|
|
f80a6ae228 | ||
|
|
9b3fbe4593 | ||
|
|
222eca64d7 | ||
|
|
1b687f3feb | ||
|
|
2228104bff | ||
|
|
402c3752c4 | ||
|
|
69a8bb5e1f | ||
|
|
0d76f9e030 | ||
|
|
eb8f65c58b | ||
|
|
e0e27a671e | ||
|
|
b65eb69d9f | ||
|
|
8118e542fb | ||
|
|
c866759bd4 | ||
|
|
40776e5324 | ||
|
|
621343112d | ||
|
|
eb06e6ba51 | ||
|
|
da91b16244 | ||
|
|
918e2ba8d0 | ||
|
|
f50dc83829 | ||
|
|
e8a9b4743b | ||
|
|
f34226425e | ||
|
|
e27c7ba36f | ||
|
|
1aad527956 | ||
|
|
173c62acb6 | ||
|
|
76605d7dfe | ||
|
|
32be1a6496 | ||
|
|
c7d43aa544 | ||
|
|
198bf55b7b | ||
|
|
75378d3567 | ||
|
|
55cb371569 | ||
|
|
5bb97d25d0 | ||
|
|
b2017cae77 | ||
|
|
35af903d4a | ||
|
|
426e00b6f4 | ||
|
|
8e62b3e438 | ||
|
|
4265ad5f23 | ||
|
|
c181eb0539 | ||
|
|
e0f06753c6 | ||
|
|
8aafd72ef0 | ||
|
|
48549ce97b | ||
|
|
be0f3731b4 | ||
|
|
b755431b93 | ||
|
|
04ff393875 | ||
|
|
7841274300 | ||
|
|
235687d983 | ||
|
|
3d75e6ed95 | ||
|
|
eb9af8bceb | ||
|
|
39ea434513 | ||
|
|
f0a956467c | ||
|
|
e48bd08095 | ||
|
|
5d00717f39 | ||
|
|
3fff685c44 | ||
|
|
1e75265eed | ||
|
|
b6ac3ef445 | ||
|
|
421f78f3e6 | ||
|
|
b71fdcfc20 | ||
|
|
021e9b228a | ||
|
|
00d4533022 | ||
|
|
fd5faeb5dd | ||
|
|
e7c8035ed7 | ||
|
|
e427e38da8 | ||
|
|
1f24abc3d2 | ||
|
|
76e62779ba | ||
|
|
1af343ef50 | ||
|
|
412ffd1592 | ||
|
|
b141fec573 | ||
|
|
d2e14abfd5 | ||
|
|
d4abca0480 | ||
|
|
529f5822ee | ||
|
|
395d85a12f | ||
|
|
4379a4b067 | ||
|
|
ad8e1cbf62 | ||
|
|
dc5a70b0de | ||
|
|
b5d1f52ea4 | ||
|
|
221cf235b5 | ||
|
|
7720e31a31 | ||
|
|
d812affef0 | ||
|
|
5c19eb34bf | ||
|
|
e18ebaee3d | ||
|
|
dbcbf12456 | ||
|
|
c04b44057c | ||
|
|
60aa60f48e | ||
|
|
2848d7c80e | ||
|
|
9fcdacb624 | ||
|
|
cf1713b085 | ||
|
|
f049a4ca67 | ||
|
|
55f860da2f | ||
|
|
b5369e611c | ||
|
|
3d1dd1c6ac | ||
|
|
10a363b275 | ||
|
|
d865c5e2b6 | ||
|
|
9fac37588c | ||
|
|
aac0d58417 | ||
|
|
b37e6187d4 | ||
|
|
20138ee85f | ||
|
|
6dc569cde5 | ||
|
|
77cf0b678a | ||
|
|
2dfb061063 | ||
|
|
e4669e2581 | ||
|
|
df47cf72d3 | ||
|
|
ba1b34e375 | ||
|
|
950b5ee529 | ||
|
|
041c48de19 | ||
|
|
599fbcee6e | ||
|
|
ce2df8030c | ||
|
|
47e761bbe2 | ||
|
|
0646baa18d | ||
|
|
38997c1b47 | ||
|
|
acaafabc23 | ||
|
|
6a80bdafa6 | ||
|
|
cf30ed745c | ||
|
|
a399fb4044 | ||
|
|
24b946e850 | ||
|
|
236daf48ff | ||
|
|
4942af27dc | ||
|
|
3adb90071b | ||
|
|
29b4a2a08c | ||
|
|
e1331fc0a2 | ||
|
|
3802f8ff65 | ||
|
|
4b0abdbe7c | ||
|
|
81889fd7a3 | ||
|
|
aac99c45c0 | ||
|
|
566a6369a5 | ||
|
|
4fdf340d04 | ||
|
|
ddd7145153 | ||
|
|
3f22b644b6 | ||
|
|
639c9f579d | ||
|
|
735b8665f1 | ||
|
|
199fa50a9d | ||
|
|
aac5ad8504 | ||
|
|
349c108ebc | ||
|
|
3b464782ef | ||
|
|
3d97fd8d2a | ||
|
|
c102e76146 | ||
|
|
beee7b68bf | ||
|
|
f47e571d92 | ||
|
|
4b5320a8f0 | ||
|
|
30c2c89c6b | ||
|
|
4a1d1a0dc1 | ||
|
|
360adc9130 | ||
|
|
cc21abe843 | ||
|
|
9a27555763 | ||
|
|
aaef2fbd01 | ||
|
|
5bb640ca17 | ||
|
|
0e5c7a62cb | ||
|
|
1b33e05f74 | ||
|
|
53a04309ff | ||
|
|
dc411651b6 | ||
|
|
514540b90b | ||
|
|
a5249d1f5d | ||
|
|
21aa3f6578 | ||
|
|
0024edbbb9 | ||
|
|
23cb39b557 | ||
|
|
48de321869 | ||
|
|
c6d68009d2 | ||
|
|
2cab267405 | ||
|
|
6bdc0d2e5e | ||
|
|
3eed81c1eb | ||
|
|
b447807b36 | ||
|
|
2771c8c32e | ||
|
|
21a88bc2d3 | ||
|
|
57c1838f68 | ||
|
|
52b0254ec6 | ||
|
|
49631542ce | ||
|
|
4b80ffb9eb | ||
|
|
9efa7c116d | ||
|
|
5d9c8d59a0 | ||
|
|
1a60201f68 | ||
|
|
f3186abf09 | ||
|
|
6bcc0d3c7f | ||
|
|
57b9a57dde | ||
|
|
28994f4b64 | ||
|
|
588b4712bf | ||
|
|
d3b6208057 | ||
|
|
ef80953b1b | ||
|
|
72db1188c7 | ||
|
|
0858d3c544 | ||
|
|
5c5656f981 | ||
|
|
58a9c92d75 | ||
|
|
a6dc4646db | ||
|
|
8ff553e926 | ||
|
|
848a5c61f0 | ||
|
|
d49000e9f4 | ||
|
|
a82145c4e6 | ||
|
|
181edb7235 | ||
|
|
ff2ae11ac8 | ||
|
|
3f841f3b21 | ||
|
|
181977ad4e | ||
|
|
e155fe403d | ||
|
|
bf5438d573 | ||
|
|
0e4aaf8856 | ||
|
|
5c44ce1637 | ||
|
|
974fe25a11 | ||
|
|
58bae83558 | ||
|
|
5d309af86f | ||
|
|
ec857d1c53 | ||
|
|
2f84cdd708 | ||
|
|
7cc02e84ed | ||
|
|
87c2a5bc97 | ||
|
|
826c2e0f4e | ||
|
|
b5e25e13b7 | ||
|
|
f9653114d1 | ||
|
|
6b7e19891b | ||
|
|
a677f14423 | ||
|
|
dddce3f30d | ||
|
|
be93d670a3 | ||
|
|
68d4bb6ffe | ||
|
|
a27471178a | ||
|
|
66fcb0cc8f | ||
|
|
05d0ddc281 | ||
|
|
b1890f50b6 | ||
|
|
b44c707e94 | ||
|
|
4c7675939a | ||
|
|
fa1b7de52a | ||
|
|
666f8c8d3c | ||
|
|
996c0b3280 | ||
|
|
f9d428de8b | ||
|
|
a17b3f1b84 | ||
|
|
f39512aa63 | ||
|
|
23ee9f64d4 | ||
|
|
ad68739df7 | ||
|
|
a50d8421b8 | ||
|
|
3ea1b07906 | ||
|
|
c3683662c2 | ||
|
|
861865807a | ||
|
|
18930082e2 | ||
|
|
6a14e49479 | ||
|
|
c530d5f016 | ||
|
|
ac5e1a6ebd | ||
|
|
bb6de53f28 | ||
|
|
bfcd499cc2 | ||
|
|
3e88ec18e2 | ||
|
|
1fb640c313 | ||
|
|
bece3d2bcf | ||
|
|
307d866bb6 | ||
|
|
95c9514a44 | ||
|
|
768be433d6 | ||
|
|
4d4f38fb35 | ||
|
|
15ad07f03d | ||
|
|
0b53c413a7 | ||
|
|
931397c7e1 | ||
|
|
ef2cc7ebf5 | ||
|
|
8d3fd75ec2 | ||
|
|
a42b254c33 | ||
|
|
d24e1ae110 | ||
|
|
2e7badab4e | ||
|
|
7cf3d49f00 | ||
|
|
25037006bf | ||
|
|
f611eb2c2b | ||
|
|
e12c10b087 | ||
|
|
9527e5ded8 | ||
|
|
23b4b20b4f | ||
|
|
7f9ecd659c | ||
|
|
bb31d465f2 | ||
|
|
834468e8e7 | ||
|
|
4720513672 | ||
|
|
a480110d43 | ||
|
|
0a2c95cc10 | ||
|
|
9d2e32902d | ||
|
|
77b6413526 | ||
|
|
9e502099e0 | ||
|
|
c6a7e44ae7 | ||
|
|
1bf06312b8 | ||
|
|
c35721abbd | ||
|
|
7f3c417078 | ||
|
|
69511c2783 | ||
|
|
158a94d34c | ||
|
|
db58bd68f5 | ||
|
|
e65f08a2c8 | ||
|
|
ab8f616385 | ||
|
|
6dc6f9bbb5 | ||
|
|
7754bb995b | ||
|
|
d1fefce61c | ||
|
|
c3abdab9c4 | ||
|
|
8b13e103fd | ||
|
|
5f94f65f4f | ||
|
|
fc0d69616c | ||
|
|
656f5b93d6 | ||
|
|
436d37c079 | ||
|
|
ed0081fcf7 | ||
|
|
140062f8a3 | ||
|
|
75f5fa7c06 | ||
|
|
9152a1a266 | ||
|
|
ade89ab795 | ||
|
|
95cfdee8b8 | ||
|
|
63a27cc5e2 | ||
|
|
472face796 | ||
|
|
8d537a6d0b | ||
|
|
b3101d339e | ||
|
|
85acddddba | ||
|
|
c9d747d97f | ||
|
|
0bba267808 | ||
|
|
1036242064 | ||
|
|
5fd62098bd | ||
|
|
74cc7be922 | ||
|
|
b3814ca89a | ||
|
|
b75a321e4a | ||
|
|
9caa4fec4a | ||
|
|
a0cba1aee1 | ||
|
|
97018ad62f | ||
|
|
a7d17fae44 | ||
|
|
6ce0050979 | ||
|
|
bc035fca78 | ||
|
|
df914a92e4 | ||
|
|
1b939a6823 | ||
|
|
81b6d988ec | ||
|
|
7e9b65feca | ||
|
|
6f098b3d21 | ||
|
|
5ddb0488f2 | ||
|
|
3e87314adf | ||
|
|
f6d4a06661 | ||
|
|
ff17fc95e6 | ||
|
|
c5a46f1cea | ||
|
|
0b072189c4 | ||
|
|
5b22d1486a | ||
|
|
9eb48c2b0d | ||
|
|
ff0632001c | ||
|
|
28bd09a2ea | ||
|
|
c6aaa9b09c | ||
|
|
20d9ec1fd2 | ||
|
|
2c45c2d3c0 | ||
|
|
18829e648a | ||
|
|
a64c636a33 | ||
|
|
5796f211c1 | ||
|
|
ae09dec05e | ||
|
|
afd51e0823 | ||
|
|
0bbf826b21 | ||
|
|
02d1900e2f | ||
|
|
73da11b04c | ||
|
|
4af07e3731 | ||
|
|
7f60f85cd4 | ||
|
|
4c66cb1854 | ||
|
|
35b92e1511 | ||
|
|
e5d3fe52c5 | ||
|
|
63fe7c0a86 | ||
|
|
c5f137c715 | ||
|
|
66923bc0e3 | ||
|
|
8bf7b5a323 | ||
|
|
36d4d85849 | ||
|
|
5f0e05f3bf | ||
|
|
235e5511eb | ||
|
|
6fb60ae0b1 | ||
|
|
6b2883074b | ||
|
|
7fe418d1b7 | ||
|
|
f872ceb0d9 | ||
|
|
0f10a36b40 | ||
|
|
3c45c9170f | ||
|
|
a0730aeb44 | ||
|
|
e5fdc7fdd0 | ||
|
|
015c87b5e1 | ||
|
|
d20a49e500 | ||
|
|
adb7331670 | ||
|
|
084f0d27a3 | ||
|
|
522a0beec0 | ||
|
|
bf46bcf376 | ||
|
|
f330c3f8c5 | ||
|
|
77a51c1e05 | ||
|
|
144f215705 | ||
|
|
51b01b6b44 | ||
|
|
09885534c6 | ||
|
|
b9dfec38c2 | ||
|
|
2ef8120073 | ||
|
|
52ff2e0e63 | ||
|
|
8cf7dce33f | ||
|
|
9d3bb4a37a | ||
|
|
c30437448b | ||
|
|
7e3496f8aa | ||
|
|
46ac79f4dc | ||
|
|
833b14e353 | ||
|
|
e9eca25792 | ||
|
|
1854ad133c | ||
|
|
2b5e723ea5 | ||
|
|
9a18906edd | ||
|
|
93cb7582c2 | ||
|
|
b4529639f6 | ||
|
|
f0a59fccf8 | ||
|
|
46dbaf95a6 | ||
|
|
d418b03708 | ||
|
|
765e90ecfa | ||
|
|
ff1e88dde6 | ||
|
|
62ebcba647 | ||
|
|
429e90183b | ||
|
|
875790e862 | ||
|
|
25edbe6805 | ||
|
|
e148b50d6a | ||
|
|
06d46d56cd | ||
|
|
edd8770c67 | ||
|
|
daedda8547 | ||
|
|
df9d897e75 | ||
|
|
fcd8e5e5ad | ||
|
|
6c003f71f4 | ||
|
|
5b704478d9 | ||
|
|
60eb528d68 | ||
|
|
1cf730d957 | ||
|
|
171110b445 | ||
|
|
22699db855 | ||
|
|
18405b3908 | ||
|
|
f0a55b5cbb | ||
|
|
04a0774ee5 | ||
|
|
3a8cfb1f45 | ||
|
|
dcc19d17d4 | ||
|
|
1ef69adc6f | ||
|
|
75a4df0f32 | ||
|
|
8ba1b5fc35 | ||
|
|
a09471ac6c | ||
|
|
a916eea684 | ||
|
|
511998d8e1 | ||
|
|
e3cf50f791 | ||
|
|
42ba5298a7 | ||
|
|
ee79750a22 | ||
|
|
1787f8617f | ||
|
|
748ca28185 | ||
|
|
9c68d713ba | ||
|
|
fc69678206 | ||
|
|
aebd717039 | ||
|
|
1ddb01ac44 | ||
|
|
3e279cd670 | ||
|
|
724c03630a | ||
|
|
b00b2561e5 | ||
|
|
c5b50fe3cf | ||
|
|
df9884de3c | ||
|
|
65ae7669f9 | ||
|
|
179606feb1 | ||
|
|
536140340e | ||
|
|
5d293df64b | ||
|
|
6188891a53 | ||
|
|
9774661cfe | ||
|
|
563bc34fb5 | ||
|
|
9c95ab3a28 | ||
|
|
b776c37b36 | ||
|
|
5b3f92b70f | ||
|
|
1562b81522 | ||
|
|
be1016ace6 | ||
|
|
ee27c689e1 | ||
|
|
fdbf452ced | ||
|
|
3d9927dee0 | ||
|
|
1456b128d2 | ||
|
|
166f77cb86 | ||
|
|
5577838905 | ||
|
|
9c15982299 | ||
|
|
63c24122db | ||
|
|
1396ca903d | ||
|
|
d1fb5bdc30 | ||
|
|
e27812bf3e | ||
|
|
11a3cf9b99 | ||
|
|
a90d70feae | ||
|
|
466b34735c | ||
|
|
7ca9116e37 | ||
|
|
decd3e737c | ||
|
|
f35442ad1b | ||
|
|
2b296435b3 | ||
|
|
19ee1dfecc | ||
|
|
7da4596ef8 | ||
|
|
a379ef6781 | ||
|
|
7beb065be3 | ||
|
|
38b9091513 | ||
|
|
96db3c9601 | ||
|
|
43c4fc8e33 | ||
|
|
986ff101ce | ||
|
|
94c83c445f | ||
|
|
625865412f | ||
|
|
46677e69ce | ||
|
|
5fbca5b823 | ||
|
|
879fab120f | ||
|
|
391b24bc17 | ||
|
|
d713533d26 | ||
|
|
24f745a334 | ||
|
|
86f3101861 | ||
|
|
fd823c63ab | ||
|
|
fa69892f70 | ||
|
|
cfc53d0d26 | ||
|
|
97c2056e4a | ||
|
|
0ad0164171 | ||
|
|
df0e285b6f | ||
|
|
e92f1b8c28 | ||
|
|
410f86c960 | ||
|
|
85f27320e7 | ||
|
|
9a3fac90e1 | ||
|
|
6984f6eec4 | ||
|
|
d05f502fc8 | ||
|
|
ba41ab8f67 | ||
|
|
250bb7e29d | ||
|
|
48a26fd5df | ||
|
|
3af26540ec | ||
|
|
7d9de068d9 | ||
|
|
d174917a07 | ||
|
|
af398fc4c4 | ||
|
|
878ef446a2 | ||
|
|
668f6477bb | ||
|
|
01a770cbca | ||
|
|
23a1174aa2 | ||
|
|
414020e75b | ||
|
|
ed74bccad6 | ||
|
|
0eedde445c | ||
|
|
88bf78213f | ||
|
|
d342461a51 | ||
|
|
dffaaf8751 | ||
|
|
313edadf47 | ||
|
|
c9ce33dfe6 | ||
|
|
0f50ac7205 | ||
|
|
e807c08275 | ||
|
|
893977365c | ||
|
|
0cac45809f | ||
|
|
489ca3c207 | ||
|
|
313535c599 | ||
|
|
7f1e0557c9 | ||
|
|
0860f84a39 | ||
|
|
2fe9e78b6d | ||
|
|
2ba30f2022 | ||
|
|
b3b27cab34 | ||
|
|
694207a86d | ||
|
|
90138c4bae | ||
|
|
58a833e987 | ||
|
|
86c5a569d5 | ||
|
|
19592e8eea | ||
|
|
8e6678d526 | ||
|
|
e792a1e030 | ||
|
|
f0e818a28c | ||
|
|
69bd63b742 | ||
|
|
b40f30f2e5 | ||
|
|
1fbde87ec2 | ||
|
|
f9dc34c8fa | ||
|
|
f7186f5331 | ||
|
|
6a680e4db0 | ||
|
|
f6b69f412f | ||
|
|
5aed18862d | ||
|
|
62bf213a6e | ||
|
|
e5c32e9b48 | ||
|
|
b87dc37fbb | ||
|
|
002d4cb37c | ||
|
|
ff321fc355 | ||
|
|
c3386dec84 | ||
|
|
927d2761f7 | ||
|
|
5e5f513088 | ||
|
|
9fcf725061 | ||
|
|
601e015f00 | ||
|
|
3289e8403a | ||
|
|
104a7c7d05 | ||
|
|
7560660ec7 | ||
|
|
40ccb4a0dd | ||
|
|
f90288f5dc | ||
|
|
3bf79898d9 | ||
|
|
1d6e11ca10 | ||
|
|
6e903ee7d5 | ||
|
|
21fb1dff7e | ||
|
|
da924a359c | ||
|
|
a5066f15dc | ||
|
|
2dca53a696 | ||
|
|
bfbcdf8b86 | ||
|
|
42e1d18470 | ||
|
|
435dc72aa1 | ||
|
|
66c380548b | ||
|
|
13f81e9a6f | ||
|
|
68c8796adb | ||
|
|
4232f5342e | ||
|
|
28c31359bf | ||
|
|
a36e815227 | ||
|
|
ff3d33d5e0 | ||
|
|
d015d6b103 | ||
|
|
0d160fbeaa | ||
|
|
fedd7920a8 | ||
|
|
6afac06596 | ||
|
|
91b2b40b9a | ||
|
|
56ecb6a3ea | ||
|
|
64f73f624f | ||
|
|
20b78b68a6 | ||
|
|
d8a6d7e02f | ||
|
|
d1cc14201b | ||
|
|
a57ec9e669 | ||
|
|
1a8961587c | ||
|
|
fa13ad8849 | ||
|
|
8b23dec322 | ||
|
|
4e8aac4b41 | ||
|
|
9c72b557ec | ||
|
|
207ec1e032 | ||
|
|
ff5d4276bc | ||
|
|
26c9618e63 | ||
|
|
9c306899ba | ||
|
|
4b7e7aab33 | ||
|
|
2921e9f868 | ||
|
|
70b7606cb8 | ||
|
|
0cae91d525 | ||
|
|
2c2a28e46b | ||
|
|
c75d484f23 | ||
|
|
072f7fec30 | ||
|
|
a73379495f | ||
|
|
0b914866eb | ||
|
|
f12186d024 | ||
|
|
4a2835dc84 | ||
|
|
55dc45de33 | ||
|
|
39aa64ff68 | ||
|
|
94bb91f2fa | ||
|
|
f553ca95a5 | ||
|
|
b44e8167d7 | ||
|
|
36c1122d20 | ||
|
|
ad13529eaa | ||
|
|
04c8f4a5ed | ||
|
|
850446195f | ||
|
|
865506546f | ||
|
|
fe72fadedd | ||
|
|
9cf9d3ad5c | ||
|
|
426d0e6af1 | ||
|
|
483b442b7d | ||
|
|
9420b0c947 | ||
|
|
dcfb7345f0 | ||
|
|
f9ab24a077 | ||
|
|
f932d16ba7 | ||
|
|
7d9acc3c36 | ||
|
|
72f735124f | ||
|
|
6ea5a4719a | ||
|
|
f8c50b7f1e | ||
|
|
feb1f1d71a | ||
|
|
550afc27dc | ||
|
|
e6a828572a | ||
|
|
6ea43d8e6d | ||
|
|
d13af4bdc4 | ||
|
|
53a365dd2b | ||
|
|
6726affa7e | ||
|
|
6ecf2a6eb2 | ||
|
|
a359e11f97 | ||
|
|
54b2d74068 | ||
|
|
c99aa7279b | ||
|
|
4fa568ce8a | ||
|
|
874698b93f | ||
|
|
b286fc1e4a | ||
|
|
1cf1024332 | ||
|
|
6b391b701b | ||
|
|
efc90f8f5a | ||
|
|
6535ba7c24 | ||
|
|
5c29c3d160 | ||
|
|
742d4bff78 | ||
|
|
7726ffa3f7 | ||
|
|
b442d78ebb | ||
|
|
d44edb5930 | ||
|
|
d5633b3705 | ||
|
|
3b68dc72e7 | ||
|
|
51611c3934 | ||
|
|
747b7567d7 | ||
|
|
797891d6cf | ||
|
|
286dc3c32b | ||
|
|
a66ba21c3d | ||
|
|
b139810b6a | ||
|
|
dddc18d77c | ||
|
|
56f56d008a | ||
|
|
81a8a99b6e | ||
|
|
07aa0ee7ad | ||
|
|
b66a6bddbc | ||
|
|
d57d90fe6b | ||
|
|
de6c646ee8 | ||
|
|
4839ede64f | ||
|
|
84f5faf653 | ||
|
|
281077bc26 | ||
|
|
ed5fe9ae9f | ||
|
|
1866e4ef44 | ||
|
|
a6a07c3b3a | ||
|
|
b2af8e640c | ||
|
|
7a3f5d508b | ||
|
|
7e1fd03104 | ||
|
|
758415d326 | ||
|
|
1660041470 | ||
|
|
1783df883e | ||
|
|
b2be821637 | ||
|
|
051ff2b325 | ||
|
|
4d6f9ffd7c | ||
|
|
d614823013 | ||
|
|
48aa51b739 | ||
|
|
41c6125e1b | ||
|
|
bb3d48f98b | ||
|
|
b5e46e83e2 | ||
|
|
2340910b46 | ||
|
|
d8c4c1525d | ||
|
|
6713ef7726 | ||
|
|
ae7555b065 | ||
|
|
ee6ff0cc60 | ||
|
|
94b2352c2c | ||
|
|
cf3f943feb | ||
|
|
55c4d583b9 | ||
|
|
0e1bb6ab04 | ||
|
|
7944cff7a5 | ||
|
|
8b08a370c5 | ||
|
|
2d5fd2fe1c | ||
|
|
b5e50ecb75 | ||
|
|
e00c9d0ee0 | ||
|
|
be9c9f045a | ||
|
|
75fca1b9c7 | ||
|
|
1436fb3ef4 | ||
|
|
2bfd127218 | ||
|
|
de5da63d5c | ||
|
|
fb419eaa36 | ||
|
|
cf2a363e5e | ||
|
|
7401facc21 | ||
|
|
579afe012b | ||
|
|
4f856e8783 | ||
|
|
eb059183f7 | ||
|
|
d193a1eb70 |
@@ -1,7 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = sanic, tests
|
||||
omit = site-packages
|
||||
source = sanic
|
||||
omit = site-packages, sanic/utils.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
directory = coverage
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,4 +12,6 @@ settings.py
|
||||
.cache/*
|
||||
.python-version
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
|
||||
26
.travis.yml
26
.travis.yml
@@ -1,10 +1,25 @@
|
||||
sudo: false
|
||||
dist: precise
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py35
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py35-no-ext
|
||||
python: 3.5
|
||||
- env: TOX_ENV=py36
|
||||
python: 3.6
|
||||
- env: TOX_ENV=py36-no-ext
|
||||
python: 3.6
|
||||
- env: TOX_ENV=flake8
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
install: pip install -U tox
|
||||
script: tox -e $TOX_ENV
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
@@ -12,3 +27,4 @@ deploy:
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
74
CONDUCT.md
Normal file
74
CONDUCT.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at sanic-maintainers@googlegroups.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
We are committed to providing a friendly, safe and welcoming environment for all,
|
||||
regardless of gender, sexual orientation, disability, ethnicity, religion,
|
||||
or similar personal characteristic.
|
||||
Our [code of conduct](./CONDUCT.md) sets the standards for behavior.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests.
|
||||
2. All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project.
|
||||
3. All pull requests must pass flake8 checks.
|
||||
4. All pull requests must be consistent with the existing code.
|
||||
5. If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
6. If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
7. An example must be one of the following:
|
||||
* Example of how to use Sanic
|
||||
* Example of how to use Sanic extensions
|
||||
* Example of how to use Sanic and asynchronous library
|
||||
|
||||
## Documentation
|
||||
|
||||
Sanic's documentation is built
|
||||
using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in
|
||||
Markdown and can be found in the `docs` folder, while the module reference is
|
||||
automatically generated using `sphinx-apidoc`.
|
||||
|
||||
To generate the documentation from scratch:
|
||||
|
||||
```bash
|
||||
sphinx-apidoc -fo docs/_api/ sanic
|
||||
sphinx-build -b html docs docs/_build
|
||||
```
|
||||
|
||||
The HTML documentation will be created in the `docs/_build` folder.
|
||||
|
||||
## Warning
|
||||
|
||||
One of the main goals of Sanic is speed. Code that lowers the performance of
|
||||
Sanic without significant gains in usability, security, or features may not be
|
||||
merged. Please don't let this intimidate you! If you have any concerns about an
|
||||
idea, open an issue for discussion and help.
|
||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM python:3.6
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install tox
|
||||
4
MANIFEST.in
Normal file
4
MANIFEST.in
Normal file
@@ -0,0 +1,4 @@
|
||||
include README.rst
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
4
Makefile
Normal file
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
test:
|
||||
find . -name "*.pyc" -delete
|
||||
docker build -t sanic/test-image .
|
||||
docker run -t sanic/test-image tox
|
||||
52
README.rst
52
README.rst
@@ -1,5 +1,5 @@
|
||||
Sanic
|
||||
=================================
|
||||
=====
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|
||||
@@ -9,33 +9,7 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Benchmarks
|
||||
----------
|
||||
|
||||
All tests were run on an AWS medium instance running ubuntu, using 1
|
||||
process. Each script delivered a small JSON response and was tested with
|
||||
wrk using 100 connections. Pypy was tested for Falcon and Flask but did
|
||||
not speed up requests.
|
||||
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Server | Implementation | Requests/sec | Avg Latency |
|
||||
+===========+=======================+================+===============+
|
||||
| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Bottle | gunicorn + meinheld | 13,596 | 7.36ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
| Tornado | Python 3.5 | 2,138 | 46.66ms |
|
||||
+-----------+-----------------------+----------------+---------------+
|
||||
If you have a project that utilizes Sanic make sure to comment on the `issue <https://github.com/channelcat/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
@@ -59,6 +33,13 @@ Installation
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
|
||||
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
|
||||
installation.
|
||||
|
||||
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
@@ -74,13 +55,20 @@ Documentation
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
|
||||
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
|
||||
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* File output
|
||||
* Examples of integrations with 3rd-party modules
|
||||
* RESTful router
|
||||
* http2
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
225
docs/Makefile
225
docs/Makefile
@@ -1,20 +1,225 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = Sanic
|
||||
SOURCEDIR = .
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " epub3 to make an epub3"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@echo " dummy to check syntax errors of document sources"
|
||||
|
||||
.PHONY: help Makefile
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiographite.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiographite.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/aiographite"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiographite"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: epub3
|
||||
epub3:
|
||||
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||
@echo
|
||||
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
.PHONY: dummy
|
||||
dummy:
|
||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||
@echo
|
||||
@echo "Build finished. Dummy builder generates no files."
|
||||
|
||||
39
docs/conf.py
39
docs/conf.py
@@ -13,6 +13,9 @@ import sys
|
||||
# Add support for Markdown documentation using Recommonmark
|
||||
from recommonmark.parser import CommonMarkParser
|
||||
|
||||
# Add support for auto-doc
|
||||
from recommonmark.transform import AutoStructify
|
||||
|
||||
# Ensure that sanic is present in the path, to allow sphinx-apidoc to
|
||||
# autogenerate documentation from docstrings
|
||||
root_directory = os.path.dirname(os.getcwd())
|
||||
@@ -22,7 +25,7 @@ import sanic
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = []
|
||||
extensions = ['sphinx.ext.autodoc']
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
@@ -68,7 +71,6 @@ pygments_style = 'sphinx'
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
@@ -80,13 +82,11 @@ html_theme = 'sphinx_rtd_theme'
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Sanicdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
@@ -110,21 +110,14 @@ latex_elements = {
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'),
|
||||
]
|
||||
|
||||
latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation',
|
||||
'Sanic contributors', 'manual'), ]
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'sanic', 'Sanic Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)]
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
@@ -132,13 +125,10 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Sanic', 'Sanic Documentation',
|
||||
author, 'Sanic', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic',
|
||||
'One line description of project.', 'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
@@ -150,8 +140,15 @@ epub_copyright = copyright
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
|
||||
# -- Custom Settings -------------------------------------------------------
|
||||
|
||||
suppress_warnings = ['image.nonlocal_uri']
|
||||
|
||||
|
||||
# app setup hook
|
||||
def setup(app):
|
||||
app.add_config_value('recommonmark_config', {
|
||||
'enable_eval_rst': True,
|
||||
'enable_auto_doc_ref': True,
|
||||
}, True)
|
||||
app.add_transform(AutoStructify)
|
||||
|
||||
@@ -9,19 +9,24 @@ Guides
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
sanic/request_data
|
||||
sanic/response
|
||||
sanic/static_files
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/blueprints
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/decorators
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/contributing
|
||||
sanic/api_reference
|
||||
|
||||
|
||||
Module Documentation
|
||||
@@ -30,4 +35,5 @@ Module Documentation
|
||||
.. toctree::
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
265
docs/make.bat
265
docs/make.bat
@@ -1,19 +1,64 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=Sanic
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. epub3 to make an epub3
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
echo. coverage to run coverage check of the documentation if enabled
|
||||
echo. dummy to check syntax errors of document sources
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
REM Check if sphinx-build is available and fallback to Python version if any
|
||||
%SPHINXBUILD% 1>NUL 2>NUL
|
||||
if errorlevel 9009 goto sphinx_python
|
||||
goto sphinx_ok
|
||||
|
||||
:sphinx_python
|
||||
|
||||
set SPHINXBUILD=python -m sphinx.__init__
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
@@ -26,11 +71,211 @@ if errorlevel 9009 (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
:sphinx_ok
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiographite.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiographite.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub3" (
|
||||
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %~dp0
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "coverage" (
|
||||
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of coverage in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/coverage/python.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dummy" (
|
||||
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. Dummy builder generates no files.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
popd
|
||||
|
||||
150
docs/sanic/api_reference.rst
Normal file
150
docs/sanic/api_reference.rst
Normal file
@@ -0,0 +1,150 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
sanic.app module
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.app
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.blueprints module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: sanic.blueprints
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.config module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.config
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.constants module
|
||||
----------------------
|
||||
|
||||
.. automodule:: sanic.constants
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.cookies module
|
||||
--------------------
|
||||
|
||||
.. automodule:: sanic.cookies
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.exceptions module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: sanic.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.handlers module
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.handlers
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.log module
|
||||
----------------
|
||||
|
||||
.. automodule:: sanic.log
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.request module
|
||||
--------------------
|
||||
|
||||
.. automodule:: sanic.request
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.response module
|
||||
---------------------
|
||||
|
||||
.. automodule:: sanic.response
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.router module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.router
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.server module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.server
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.static module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.static
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.testing module
|
||||
--------------------
|
||||
|
||||
.. automodule:: sanic.testing
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.views module
|
||||
------------------
|
||||
|
||||
.. automodule:: sanic.views
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.websocket module
|
||||
----------------------
|
||||
|
||||
.. automodule:: sanic.websocket
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
sanic.worker module
|
||||
-------------------
|
||||
|
||||
.. automodule:: sanic.worker
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: sanic
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -55,13 +55,18 @@ will look like:
|
||||
|
||||
Blueprints have much the same functionality as an application instance.
|
||||
|
||||
### WebSocket routes
|
||||
|
||||
WebSocket handlers can be registered on a blueprint using the `@bp.websocket`
|
||||
decorator or `bp.add_websocket_route` method.
|
||||
|
||||
### Middleware
|
||||
|
||||
Using blueprints allows you to also register middleware globally.
|
||||
|
||||
```python
|
||||
@bp.middleware
|
||||
async def halt_request(request):
|
||||
async def print_on_request(request):
|
||||
print("I am a spy")
|
||||
|
||||
@bp.middleware('request')
|
||||
@@ -111,7 +116,7 @@ bp = Blueprint('my_blueprint')
|
||||
async def setup_connection(app, loop):
|
||||
global database
|
||||
database = mysql.connect(host='127.0.0.1'...)
|
||||
|
||||
|
||||
@bp.listener('after_server_stop')
|
||||
async def close_connection(app, loop):
|
||||
await database.close()
|
||||
@@ -131,13 +136,13 @@ can be used to implement our API versioning scheme.
|
||||
from sanic.response import text
|
||||
from sanic import Blueprint
|
||||
|
||||
blueprint_v1 = Blueprint('v1')
|
||||
blueprint_v2 = Blueprint('v2')
|
||||
blueprint_v1 = Blueprint('v1', url_prefix='/v1')
|
||||
blueprint_v2 = Blueprint('v2', url_prefix='/v2')
|
||||
|
||||
@blueprint_v1.route('/')
|
||||
async def api_v1_root(request):
|
||||
return text('Welcome to version 1 of our documentation')
|
||||
|
||||
|
||||
@blueprint_v2.route('/')
|
||||
async def api_v2_root(request):
|
||||
return text('Welcome to version 2 of our documentation')
|
||||
@@ -153,8 +158,8 @@ from sanic import Sanic
|
||||
from blueprints import blueprint_v1, blueprint_v2
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.blueprint(blueprint_v1)
|
||||
app.blueprint(blueprint_v2)
|
||||
app.blueprint(blueprint_v1, url_prefix='/v1')
|
||||
app.blueprint(blueprint_v2, url_prefix='/v2')
|
||||
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
```
|
||||
@@ -164,10 +169,10 @@ app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
If you wish to generate a URL for a route inside of a blueprint, remember that the endpoint name
|
||||
takes the format `<blueprint_name>.<handler_name>`. For example:
|
||||
|
||||
```
|
||||
```python
|
||||
@blueprint_v1.route('/')
|
||||
async def root(request):
|
||||
url = app.url_for('v1.post_handler', post_id=5)
|
||||
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
You can also use `async` syntax.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
class SimpleAsyncView(HTTPMethodView):
|
||||
|
||||
async def get(self, request):
|
||||
return text('I am async get method')
|
||||
|
||||
app.add_route(SimpleAsyncView.as_view(), '/')
|
||||
|
||||
```
|
||||
|
||||
## URL parameters
|
||||
|
||||
If you need any URL parameters, as discussed in the routing guide, include them
|
||||
@@ -128,4 +146,4 @@ view.add(['POST', 'PUT'], lambda request: text('I am a post/put method'))
|
||||
app.add_route(view, '/')
|
||||
```
|
||||
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
Note: currently you cannot build a URL for a CompositionView using `url_for`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Configuration
|
||||
|
||||
Any reasonably complex application will need configuration that is not baked into the acutal code. Settings might be different for different environments or installations.
|
||||
Any reasonably complex application will need configuration that is not baked into the actual code. Settings might be different for different environments or installations.
|
||||
|
||||
## Basics
|
||||
|
||||
@@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From environment variables.
|
||||
|
||||
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env=False)
|
||||
```
|
||||
|
||||
### From an Object
|
||||
|
||||
If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module:
|
||||
@@ -44,7 +52,7 @@ You could use a class or any other object as well.
|
||||
|
||||
### From a File
|
||||
|
||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_file(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||
Usually you will want to load configuration from a file that is not part of the distributed application. You can load configuration from a file using `from_pyfile(/path/to/config_file)`. However, that requires the program to know the path to the config file. So instead you can specify the location of the config file in an environment variable and tell Sanic to use that to find the config file:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
@@ -58,7 +66,7 @@ $ MYAPP_SETTINGS=/path/to/config_file python3 myapp.py
|
||||
INFO: Goin' Fast @ http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase varibales are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
The config files are regular Python files which are executed in order to load them. This allows you to use arbitrary logic for constructing the right configuration. Only uppercase variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
|
||||
```
|
||||
# config_file
|
||||
@@ -71,8 +79,8 @@ DB_USER = 'appuser'
|
||||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
@@ -4,10 +4,39 @@ Thank you for your interest! Sanic is always looking for contributors. If you
|
||||
don't feel comfortable contributing code, adding docstrings to the source files
|
||||
is very appreciated.
|
||||
|
||||
## Installation
|
||||
|
||||
To develop on sanic (and mainly to just run the tests) it is highly recommend to
|
||||
install from sources.
|
||||
|
||||
So assume you have already cloned the repo and are in the working directory with
|
||||
a virtual environment already set up, then run:
|
||||
|
||||
```bash
|
||||
python setup.py develop && pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
* `python -m pip install pytest`
|
||||
* `python -m pytest tests`
|
||||
To run the tests for sanic it is recommended to use tox like so:
|
||||
|
||||
```bash
|
||||
tox
|
||||
```
|
||||
|
||||
See it's that simple!
|
||||
|
||||
## Pull requests!
|
||||
|
||||
So the pull request approval rules are pretty simple:
|
||||
1. All pull requests must pass unit tests
|
||||
* All pull requests must be reviewed and approved by at least
|
||||
one current collaborator on the project
|
||||
* All pull requests must pass flake8 checks
|
||||
* If you decide to remove/change anything from any common interface
|
||||
a deprecation message should accompany it.
|
||||
* If you implement a new feature you should have at least one unit
|
||||
test to accompany it.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Cookies
|
||||
|
||||
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||
both read and write cookies, which are stored as key-value pairs.
|
||||
|
||||
## Reading cookies
|
||||
|
||||
A user's cookies can be accessed `Request` object's `cookie` dictionary.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
test_cookie = request.cookies.get('test')
|
||||
return text("Test cookie set to: {}".format(test_cookie))
|
||||
```
|
||||
|
||||
## Writing cookies
|
||||
|
||||
When returning a response, cookies can be set on the `Response` object.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("There's a cookie up in this response")
|
||||
response.cookies['test'] = 'It worked!'
|
||||
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
||||
response.cookies['test']['httponly'] = True
|
||||
return response
|
||||
```
|
||||
|
||||
## Deleting cookies
|
||||
|
||||
Cookies can be removed semantically or explicitly.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("Time to eat some cookies muahaha")
|
||||
|
||||
# This cookie will be set to expire in 0 seconds
|
||||
del response.cookies['kill_me']
|
||||
|
||||
# This cookie will self destruct in 5 seconds
|
||||
response.cookies['short_life'] = 'Glad to be here'
|
||||
response.cookies['short_life']['max-age'] = 5
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
# This cookie will remain unchanged
|
||||
response.cookies['favorite_color'] = 'blue'
|
||||
response.cookies['favorite_color'] = 'pink'
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
Response cookies can be set like dictionary values and have the following
|
||||
parameters available:
|
||||
|
||||
- `expires` (datetime): The time for the cookie to expire on the
|
||||
client's browser.
|
||||
- `path` (string): The subset of URLs to which this cookie applies. Defaults to /.
|
||||
- `comment` (string): A comment (metadata).
|
||||
- `domain` (string): Specifies the domain for which the cookie is valid. An
|
||||
explicitly specified domain must always start with a dot.
|
||||
- `max-age` (number): Number of seconds the cookie should live for.
|
||||
- `secure` (boolean): Specifies whether the cookie will only be sent via
|
||||
HTTPS.
|
||||
- `httponly` (boolean): Specifies whether the cookie cannot be read by
|
||||
Javascript.
|
||||
87
docs/sanic/cookies.rst
Normal file
87
docs/sanic/cookies.rst
Normal file
@@ -0,0 +1,87 @@
|
||||
Cookies
|
||||
=======
|
||||
|
||||
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||
both read and write cookies, which are stored as key-value pairs.
|
||||
|
||||
.. warning::
|
||||
|
||||
Cookies can be freely altered by the client. Therefore you cannot just store
|
||||
data such as login information in cookies as-is, as they can be freely altered
|
||||
by the client. To ensure data you store in cookies is not forged or tampered
|
||||
with by the client, use something like `itsdangerous`_ to cryptographically
|
||||
sign the data.
|
||||
|
||||
|
||||
Reading cookies
|
||||
---------------
|
||||
|
||||
A user's cookies can be accessed via the ``Request`` object's ``cookies`` dictionary.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
test_cookie = request.cookies.get('test')
|
||||
return text("Test cookie set to: {}".format(test_cookie))
|
||||
|
||||
Writing cookies
|
||||
---------------
|
||||
|
||||
When returning a response, cookies can be set on the ``Response`` object.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("There's a cookie up in this response")
|
||||
response.cookies['test'] = 'It worked!'
|
||||
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
||||
response.cookies['test']['httponly'] = True
|
||||
return response
|
||||
|
||||
Deleting cookies
|
||||
----------------
|
||||
|
||||
Cookies can be removed semantically or explicitly.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic.response import text
|
||||
|
||||
@app.route("/cookie")
|
||||
async def test(request):
|
||||
response = text("Time to eat some cookies muahaha")
|
||||
|
||||
# This cookie will be set to expire in 0 seconds
|
||||
del response.cookies['kill_me']
|
||||
|
||||
# This cookie will self destruct in 5 seconds
|
||||
response.cookies['short_life'] = 'Glad to be here'
|
||||
response.cookies['short_life']['max-age'] = 5
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
# This cookie will remain unchanged
|
||||
response.cookies['favorite_color'] = 'blue'
|
||||
response.cookies['favorite_color'] = 'pink'
|
||||
del response.cookies['favorite_color']
|
||||
|
||||
return response
|
||||
|
||||
Response cookies can be set like dictionary values and have the following
|
||||
parameters available:
|
||||
|
||||
- ``expires`` (datetime): The time for the cookie to expire on the client's browser.
|
||||
- ``path`` (string): The subset of URLs to which this cookie applies. Defaults to /.
|
||||
- ``comment`` (string): A comment (metadata).
|
||||
- ``domain`` (string): Specifies the domain for which the cookie is valid. An
|
||||
explicitly specified domain must always start with a dot.
|
||||
- ``max-age`` (number): Number of seconds the cookie should live for.
|
||||
- ``secure`` (boolean): Specifies whether the cookie will only be sent via HTTPS.
|
||||
- ``httponly`` (boolean): Specifies whether the cookie cannot be read by Javascript.
|
||||
|
||||
.. _itsdangerous: https://pythonhosted.org/itsdangerous/
|
||||
39
docs/sanic/decorators.md
Normal file
39
docs/sanic/decorators.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Handler Decorators
|
||||
|
||||
Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.
|
||||
|
||||
## Authorization Decorator
|
||||
|
||||
Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response.
|
||||
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
from sanic.response import json
|
||||
|
||||
def authorized():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({status: 'authorized'})
|
||||
```
|
||||
|
||||
@@ -7,15 +7,6 @@ keyword arguments:
|
||||
- `host` *(default `"127.0.0.1"`)*: Address to host the server on.
|
||||
- `port` *(default `8000`)*: Port to host the server on.
|
||||
- `debug` *(default `False`)*: Enables debug output (slows server).
|
||||
- `before_start` *(default `None`)*: Function or list of functions to be executed
|
||||
before the server starts accepting connections.
|
||||
- `after_start` *(default `None`)*: Function or list of functions to be executed
|
||||
after the server starts accepting connections.
|
||||
- `before_stop` *(default `None`)*: Function or list of functions to be
|
||||
executed when a stop signal is received before it is
|
||||
respected.
|
||||
- `after_stop` *(default `None`)*: Function or list of functions to be executed
|
||||
when all requests are complete.
|
||||
- `ssl` *(default `None`)*: `SSLContext` for SSL encryption of worker(s).
|
||||
- `sock` *(default `None`)*: Socket for the server to accept connections from.
|
||||
- `workers` *(default `1`)*: Number of worker processes to spawn.
|
||||
@@ -53,3 +44,35 @@ directly run by the interpreter.
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=1337, workers=4)
|
||||
```
|
||||
|
||||
## Running via Gunicorn
|
||||
|
||||
[Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX.
|
||||
It’s a pre-fork worker model ported from Ruby’s Unicorn project.
|
||||
|
||||
In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker`
|
||||
for Gunicorn `worker-class` argument:
|
||||
|
||||
```
|
||||
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
||||
```
|
||||
|
||||
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
|
||||
after it has processed a given number of requests. This can be a convenient way to help limit the effects
|
||||
of the memory leak.
|
||||
|
||||
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||
|
||||
## Asynchronous support
|
||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||
to run the app in general.
|
||||
|
||||
Here is an incomplete example (please see `run_async.py` in examples for something more practical):
|
||||
|
||||
```python
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
loop.run_forever()
|
||||
```
|
||||
|
||||
@@ -17,6 +17,19 @@ def i_am_ready_to_die(request):
|
||||
raise ServerError("Something bad happened", status_code=500)
|
||||
```
|
||||
|
||||
You can also use the `abort` function with the appropriate status code:
|
||||
|
||||
```python
|
||||
from sanic.exceptions import abort
|
||||
from sanic.response import text
|
||||
|
||||
@app.route('/youshallnotpass')
|
||||
def no_no(request):
|
||||
abort(401)
|
||||
# this won't happen
|
||||
text("OK")
|
||||
```
|
||||
|
||||
## Handling exceptions
|
||||
|
||||
To override Sanic's default handling of an exception, the `@app.exception`
|
||||
|
||||
@@ -5,5 +5,23 @@ A list of Sanic extensions created by the community.
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
- [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models.
|
||||
- [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request
|
||||
- [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic.
|
||||
- [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config.
|
||||
- [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the
|
||||
`Babel` library
|
||||
- [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter.
|
||||
- [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers.
|
||||
- [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose.
|
||||
- [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic
|
||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
||||
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
||||
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
||||
- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/))
|
||||
|
||||
@@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers. This means y
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
|
||||
Sanic aspires to be simple:
|
||||
-------------------
|
||||
Sanic aspires to be simple
|
||||
---------------------------
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
||||
119
docs/sanic/logging.md
Normal file
119
docs/sanic/logging.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Logging
|
||||
|
||||
|
||||
Sanic allows you to do different types of logging (access log, error log) on the requests based on the [python3 logging API](https://docs.python.org/3/howto/logging.html). You should have some basic knowledge on python3 logging if you want to create a new configuration.
|
||||
|
||||
### Quick Start
|
||||
|
||||
A simple example using default settings would be like this:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.config import LOGGING
|
||||
|
||||
# The default logging handlers are ['accessStream', 'errorStream']
|
||||
# but we change it to use other handlers here for demo purpose
|
||||
LOGGING['loggers']['network']['handlers'] = [
|
||||
'accessSysLog', 'errorSysLog']
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return response.text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=LOGGING)
|
||||
```
|
||||
|
||||
And to close logging, simply assign log_config=None:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=None)
|
||||
```
|
||||
|
||||
This would skip calling logging functions when handling requests.
|
||||
And you could even do further in production to gain extra speed:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
# disable internal messages
|
||||
app.run(debug=False, log_config=None)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`:
|
||||
|
||||
- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For internal information console outputs.
|
||||
|
||||
|
||||
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For requests information logging in console
|
||||
|
||||
|
||||
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For error message and traceback logging in console.
|
||||
|
||||
|
||||
- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For requests information logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For error message and traceback logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
And `filters`:
|
||||
|
||||
- accessFilter (using sanic.log.DefaultFilter)<br>
|
||||
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
|
||||
|
||||
|
||||
- errorFilter (using sanic.log.DefaultFilter)<br>
|
||||
The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
|
||||
|
||||
There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- sanic:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
|
||||
- network:<br>
|
||||
Used to log requests from network, and any information from those requests.
|
||||
|
||||
#### Log format:
|
||||
|
||||
In addition to default parameters provided by python (asctime, levelname, message),
|
||||
Sanic provides additional parameters for network logger with accessFilter:
|
||||
|
||||
- host (str)<br>
|
||||
request.ip
|
||||
|
||||
|
||||
- request (str)<br>
|
||||
request.method + " " + request.url
|
||||
|
||||
|
||||
- status (int)<br>
|
||||
response.status
|
||||
|
||||
|
||||
- byte (int)<br>
|
||||
len(response.body)
|
||||
|
||||
|
||||
The default access log format is
|
||||
```python
|
||||
%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: %(request)s %(message)s %(status)d %(byte)d
|
||||
```
|
||||
@@ -1,9 +1,13 @@
|
||||
# Middleware
|
||||
# Middleware And Listeners
|
||||
|
||||
Middleware are functions which are executed before or after requests to the
|
||||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
|
||||
|
||||
## Middleware
|
||||
|
||||
There are two types of middleware: request and response. Both are declared
|
||||
using the `@app.middleware` decorator, with the decorator's parameter being a
|
||||
string representing its type: `'request'` or `'response'`. Response middleware
|
||||
@@ -64,3 +68,45 @@ async def halt_request(request):
|
||||
async def halt_response(request, response):
|
||||
return text('I halted the response')
|
||||
```
|
||||
|
||||
## Listeners
|
||||
|
||||
If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners:
|
||||
|
||||
- `before_server_start`
|
||||
- `after_server_start`
|
||||
- `before_server_stop`
|
||||
- `after_server_stop`
|
||||
|
||||
These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.
|
||||
|
||||
For example:
|
||||
|
||||
```python
|
||||
@app.listener('before_server_start')
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
@app.listener('after_server_start')
|
||||
async def notify_server_started(app, loop):
|
||||
print('Server successfully started!')
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
async def notify_server_stopping(app, loop):
|
||||
print('Server shutting down!')
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
async def close_db(app, loop):
|
||||
await app.db.close()
|
||||
```
|
||||
|
||||
If you want to schedule a background task to run after the loop has started,
|
||||
Sanic provides the `add_task` method to easily do so.
|
||||
|
||||
```python
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds())
|
||||
```
|
||||
|
||||
@@ -9,30 +9,34 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "message": request.json })
|
||||
```
|
||||
|
||||
|
||||
- `args` (dict) - Query string variables. A query string is the section of a
|
||||
URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed,
|
||||
the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`.
|
||||
The request's `query_string` variable holds the unparsed string value.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||
```
|
||||
|
||||
- `raw_args` (dict) - On many cases you would need to access the url arguments in
|
||||
a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the
|
||||
`raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`.
|
||||
|
||||
- `files` (dictionary of `File` objects) - List of files that have a name, body, and type
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/files")
|
||||
def post_json(request):
|
||||
test_file = request.files.get('test')
|
||||
@@ -50,7 +54,7 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({ "received": True, "form_data": request.form, "test": request.form.get('test') })
|
||||
@@ -58,17 +62,41 @@ The following variables are accessible as properties on `Request` objects:
|
||||
|
||||
- `body` (bytes) - Posted raw body. This property allows retrieval of the
|
||||
request's raw data, regardless of content type.
|
||||
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
@app.route("/users", methods=["POST",])
|
||||
def create_user(request):
|
||||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
|
||||
|
||||
```python
|
||||
from sanic.response import json
|
||||
from sanic import Blueprint
|
||||
|
||||
bp = Blueprint('my_blueprint')
|
||||
|
||||
@bp.route('/')
|
||||
async def bp_root(request):
|
||||
if request.app.config['DEBUG']:
|
||||
return json({'status': 'debug'})
|
||||
else:
|
||||
return json({'status': 'production'})
|
||||
|
||||
```
|
||||
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
|
||||
- `scheme`: The URL scheme associated with the request: `http` or `https`
|
||||
- `host`: The host associated with the request: `localhost:8080`
|
||||
- `path`: The path of the request: `/posts/1/`
|
||||
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||
- `uri_template`: Template for matching route handler: `/posts/<id>/`
|
||||
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
The request properties which return a dictionary actually return a subclass of
|
||||
|
||||
112
docs/sanic/response.md
Normal file
112
docs/sanic/response.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Response
|
||||
|
||||
Use functions in `sanic.response` module to create responses.
|
||||
|
||||
## Plain Text
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/text')
|
||||
def handle_request(request):
|
||||
return response.text('Hello world!')
|
||||
```
|
||||
|
||||
## HTML
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/html')
|
||||
def handle_request(request):
|
||||
return response.html('<p>Hello world!</p>')
|
||||
```
|
||||
|
||||
## JSON
|
||||
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json({'message': 'Hello world!'})
|
||||
```
|
||||
|
||||
## File
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/file')
|
||||
async def handle_request(request):
|
||||
return await response.file('/srv/www/whatever.png')
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
@app.route("/streaming")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
response.write('foo')
|
||||
response.write('bar')
|
||||
return response.stream(streaming_fn, content_type='text/plain')
|
||||
```
|
||||
|
||||
## File Streaming
|
||||
For large files, a combination of File and Streaming above
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
@app.route('/big_file.png')
|
||||
async def handle_request(request):
|
||||
return await response.file_stream('/srv/www/whatever.png')
|
||||
```
|
||||
|
||||
## Redirect
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
def handle_request(request):
|
||||
return response.redirect('/json')
|
||||
```
|
||||
|
||||
## Raw
|
||||
|
||||
Response without encoding the body
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/raw')
|
||||
def handle_request(request):
|
||||
return response.raw('raw data')
|
||||
```
|
||||
|
||||
## Modify headers or status
|
||||
|
||||
To modify headers or status code, pass the `headers` or `status` argument to those functions:
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/json')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
)
|
||||
```
|
||||
@@ -11,7 +11,7 @@ from sanic.response import json
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({ "hello": "world" })
|
||||
```
|
||||
```
|
||||
|
||||
When the url `http://server.url/` is accessed (the base url of the server), the
|
||||
final `/` is matched by the router to the handler function, `test`, which then
|
||||
@@ -52,7 +52,7 @@ async def integer_handler(request, integer_arg):
|
||||
async def number_handler(request, number_arg):
|
||||
return text('Number - {}'.format(number_arg))
|
||||
|
||||
@app.route('/person/<name:[A-z]>')
|
||||
@app.route('/person/<name:[A-z]+>')
|
||||
async def person_handler(request, name):
|
||||
return text('Person - {}'.format(name))
|
||||
|
||||
@@ -64,9 +64,9 @@ async def folder_handler(request, folder_id):
|
||||
|
||||
## HTTP request types
|
||||
|
||||
By default, a route defined on a URL will be avaialble for only GET requests to that URL.
|
||||
By default, a route defined on a URL will be available for only GET requests to that URL.
|
||||
However, the `@app.route` decorator accepts an optional parameter, `methods`,
|
||||
whicl allows the handler function to work with any of the HTTP methods in the list.
|
||||
which allows the handler function to work with any of the HTTP methods in the list.
|
||||
|
||||
```python
|
||||
from sanic.response import text
|
||||
@@ -81,6 +81,19 @@ async def get_handler(request):
|
||||
|
||||
```
|
||||
|
||||
There is also an optional `host` argument (which can be a list or a string). This restricts a route to the host or hosts provided. If there is a also a route with no host, it will be the default.
|
||||
|
||||
```python
|
||||
@app.route('/get', methods=['GET'], host='example.com')
|
||||
async def get_handler(request):
|
||||
return text('GET request - {}'.format(request.args))
|
||||
|
||||
# if the host header doesn't match example.com, this route will be used
|
||||
@app.route('/get', methods=['GET'])
|
||||
async def get_handler(request):
|
||||
return text('GET request in default - {}'.format(request.args))
|
||||
```
|
||||
|
||||
There are also shorthand method decorators:
|
||||
|
||||
```python
|
||||
@@ -145,9 +158,59 @@ Other things to keep in mind when using `url_for`:
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', arg_two='two')
|
||||
# /posts/5?arg_one=one&arg_two=two
|
||||
```
|
||||
- Multivalue argument can be passed to `url_for`. For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'])
|
||||
# /posts/5?arg_one=one&arg_one=two
|
||||
```
|
||||
- Also some special arguments (`_anchor`, `_external`, `_scheme`, `_method`, `_server`) passed to `url_for` will have special url building (`_method` is not support now and will be ignored). For example:
|
||||
```python
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _anchor='anchor')
|
||||
# /posts/5?arg_one=one#anchor
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _external=True)
|
||||
# //server/posts/5?arg_one=one
|
||||
# _external requires passed argument _server or SERVER_NAME in app.config or url will be same as no _external
|
||||
|
||||
url = app.url_for('post_handler', post_id=5, arg_one='one', _scheme='http', _external=True)
|
||||
# http://server/posts/5?arg_one=one
|
||||
# when specifying _scheme, _external must be True
|
||||
|
||||
# you can pass all special arguments one time
|
||||
url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, _anchor='anchor', _scheme='http', _external=True, _server='another_server:8888')
|
||||
# http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor
|
||||
```
|
||||
- All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown.
|
||||
|
||||
## WebSocket routes
|
||||
|
||||
Routes for the WebSocket protocol can be defined with the `@app.websocket`
|
||||
decorator:
|
||||
|
||||
```python
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
```
|
||||
|
||||
Alternatively, the `app.add_websocket_route` method can be used instead of the
|
||||
decorator:
|
||||
|
||||
```python
|
||||
async def feed(request, ws):
|
||||
pass
|
||||
|
||||
app.add_websocket_route(my_websocket_handler, '/feed')
|
||||
```
|
||||
|
||||
Handlers for a WebSocket route are passed the request as first argument, and a
|
||||
WebSocket protocol object as second argument. The protocol object has `send`
|
||||
and `recv` methods to send and receive data respectively.
|
||||
|
||||
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
||||
package by Aymeric Augustin.
|
||||
|
||||
@@ -9,4 +9,12 @@ Optionally pass in an SSLContext:
|
||||
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile")
|
||||
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
app.run(host="0.0.0.0", port=8443, ssl=context)
|
||||
|
||||
You can also pass in the locations of a certificate and key as a dictionary:
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"}
|
||||
app.run(host="0.0.0.0", port=8443, ssl=ssl)
|
||||
|
||||
106
docs/sanic/streaming.md
Normal file
106
docs/sanic/streaming.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Streaming
|
||||
|
||||
## Request Streaming
|
||||
|
||||
Sanic allows you to get request data by stream, as below. When the request ends, `request.stream.get()` returns `None`. Only post, put and patch decorator have stream argument.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import stream, text
|
||||
|
||||
bp = Blueprint('blueprint_request_stream')
|
||||
app = Sanic('request_stream')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
@stream_decorator
|
||||
async def post(self, request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
|
||||
@app.post('/stream', stream=True)
|
||||
async def handler(request):
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode('utf-8').replace('1', 'A')
|
||||
response.write(body)
|
||||
return stream(streaming)
|
||||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
async def bp_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8').replace('1', 'A')
|
||||
return text(result)
|
||||
|
||||
|
||||
async def post_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
app.blueprint(bp)
|
||||
app.add_route(SimpleView.as_view(), '/method_view')
|
||||
view = CompositionView()
|
||||
view.add(['POST'], post_handler, stream=True)
|
||||
app.add_route(view, '/composition_view')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='127.0.0.1', port=8000)
|
||||
```
|
||||
|
||||
## Response Streaming
|
||||
|
||||
Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import stream
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
async def sample_streaming_fn(response):
|
||||
response.write('foo,')
|
||||
response.write('bar')
|
||||
|
||||
return stream(sample_streaming_fn, content_type='text/csv')
|
||||
```
|
||||
|
||||
This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides:
|
||||
|
||||
```python
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
async def stream_from_db(response):
|
||||
conn = await asyncpg.connect(database='test')
|
||||
async with conn.transaction():
|
||||
async for record in conn.cursor('SELECT generate_series(0, 10)'):
|
||||
response.write(record[0])
|
||||
|
||||
return stream(stream_from_db)
|
||||
```
|
||||
@@ -1,51 +1,127 @@
|
||||
# Testing
|
||||
|
||||
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||
Sanic endpoints can be tested locally using the `test_client` object, which
|
||||
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||
library. The `sanic_endpoint_test` function runs a local server, issues a
|
||||
configurable request to an endpoint, and returns the result. It takes the
|
||||
following arguments:
|
||||
library.
|
||||
|
||||
- `app` An instance of a Sanic app.
|
||||
- `method` *(default `'get'`)* A string representing the HTTP method to use.
|
||||
- `uri` *(default `'/'`)* A string representing the endpoint to test.
|
||||
The `test_client` exposes `get`, `post`, `put`, `delete`, `patch`, `head` and `options` methods
|
||||
for you to run against your application. A simple example (using pytest) is like follows:
|
||||
|
||||
```python
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
|
||||
def test_index_put_not_allowed():
|
||||
request, response = app.test_client.put('/')
|
||||
assert response.status == 405
|
||||
```
|
||||
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and
|
||||
your test request is executed against your application, using `aiohttp`.
|
||||
|
||||
The `test_client` methods accept the following arguments and keyword arguments:
|
||||
|
||||
- `uri` *(default `'/'`)* A string representing the URI to test.
|
||||
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||
original request will be returned by the function. If set to `True`, the
|
||||
return value is a tuple of `(request, response)`, if `False` only the
|
||||
response is returned.
|
||||
- `loop` *(default `None`)* The event loop to use.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||
server in debug mode.
|
||||
- `server_kwargs` *(default `{}`) a dict of additional arguments to pass into `app.run` before the test request is run.
|
||||
- `debug` *(default `False`)* A boolean which determines whether to run the server in debug mode.
|
||||
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which
|
||||
are passed directly to the aiohttp ClientSession request. For example, to
|
||||
supply data with a GET request, `method` would be `get` and the keyword
|
||||
argument `params={'value', 'key'}` would be supplied. More information about
|
||||
The function further takes the `*request_args` and `**request_kwargs`, which are passed directly to the aiohttp ClientSession request.
|
||||
|
||||
For example, to supply data to a GET request, you would do the following:
|
||||
|
||||
```python
|
||||
def test_get_request_includes_data():
|
||||
params = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.get('/', params=params)
|
||||
assert request.args.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
And to supply data to a JSON POST request:
|
||||
|
||||
```python
|
||||
def test_post_json_request_includes_data():
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
request, response = app.test_client.post('/', data=json.dumps(data))
|
||||
assert request.json.get('key1') == 'value1'
|
||||
```
|
||||
|
||||
|
||||
More information about
|
||||
the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
Below is a complete example of an endpoint test,
|
||||
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
|
||||
`/challenge` endpoint responds to a GET request with a supplied challenge
|
||||
string.
|
||||
|
||||
## pytest-sanic
|
||||
|
||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
|
||||
Just write tests like,
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import aiohttp
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
# Import the Sanic app, usually created with Sanic(__name__)
|
||||
from external_server import app
|
||||
|
||||
def test_endpoint_challenge():
|
||||
# Create the challenge data
|
||||
request_data = {'challenge': 'dummy_challenge'}
|
||||
|
||||
# Send the request to the endpoint, using the default `get` method
|
||||
request, response = sanic_endpoint_test(app,
|
||||
uri='/challenge',
|
||||
params=request_data)
|
||||
|
||||
# Assert that the server responds with the challenge string
|
||||
assert response.text == request_data['challenge']
|
||||
async def test_sanic_db_find_by_id(app):
|
||||
"""
|
||||
Let's assume that, in db we have,
|
||||
{
|
||||
"id": "123",
|
||||
"name": "Kobe Bryant",
|
||||
"team": "Lakers",
|
||||
}
|
||||
"""
|
||||
doc = await app.db["players"].find_by_id("123")
|
||||
assert doc.name == "Kobe Bryant"
|
||||
assert doc.team == "Lakers"
|
||||
```
|
||||
|
||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port,
|
||||
test_server, test_client.
|
||||
|
||||
```python
|
||||
@pytest.yield_fixture
|
||||
def app():
|
||||
app = Sanic("test_sanic_app")
|
||||
|
||||
@app.route("/test_get", methods=['GET'])
|
||||
async def test_get(request):
|
||||
return response.json({"GET": True})
|
||||
|
||||
@app.route("/test_post", methods=['POST'])
|
||||
async def test_post(request):
|
||||
return response.json({"POST": True})
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_cli(loop, app, test_client):
|
||||
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
|
||||
|
||||
|
||||
#########
|
||||
# Tests #
|
||||
#########
|
||||
|
||||
async def test_fixture_test_client_get(test_cli):
|
||||
"""
|
||||
GET request
|
||||
"""
|
||||
resp = await test_cli.get('/test_get')
|
||||
assert resp.status == 200
|
||||
resp_json = await resp.json()
|
||||
assert resp_json == {"GET": True}
|
||||
|
||||
async def test_fixture_test_client_post(test_cli):
|
||||
"""
|
||||
POST request
|
||||
"""
|
||||
resp = await test_cli.post('/test_post')
|
||||
assert resp.status == 200
|
||||
resp_json = await resp.json()
|
||||
assert resp_json == {"POST": True}
|
||||
```
|
||||
|
||||
50
docs/sanic/versioning.md
Normal file
50
docs/sanic/versioning.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Versioning
|
||||
|
||||
You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number.
|
||||
|
||||
## Per route
|
||||
|
||||
You can pass a version number to the routes directly.
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/text', version=1)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 1')
|
||||
|
||||
@app.route('/text', version=2)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 2')
|
||||
|
||||
app.run(port=80)
|
||||
```
|
||||
|
||||
Then with curl:
|
||||
|
||||
```bash
|
||||
curl localhost/v1/text
|
||||
curl localhost/v2/text
|
||||
```
|
||||
|
||||
## Global blueprint version
|
||||
|
||||
You can also pass a version number to the blueprint, which will apply to all routes.
|
||||
|
||||
```python
|
||||
from sanic import response
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
bp = Blueprint('test', version=1)
|
||||
|
||||
@bp.route('/html')
|
||||
def handle_request(request):
|
||||
return response.html('<p>Hello world!</p>')
|
||||
```
|
||||
|
||||
Then with curl:
|
||||
|
||||
```bash
|
||||
curl localhost/v1/html
|
||||
```
|
||||
@@ -15,4 +15,5 @@ dependencies:
|
||||
- httptools>=0.0.9
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- websockets>=3.2
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
@@ -1,28 +0,0 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
import aiohttp
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
async def fetch(session, url):
|
||||
"""
|
||||
Use session object to perform 'get' request on url
|
||||
"""
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
"""
|
||||
Download and serve example JSON
|
||||
"""
|
||||
url = "https://api.github.com/repos/channelcat/sanic"
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await fetch(session, url)
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
@@ -1,11 +1,12 @@
|
||||
from sanic import Sanic
|
||||
from sanic import Blueprint
|
||||
from sanic.response import json, text
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic(__name__)
|
||||
blueprint = Blueprint('name', url_prefix='/my_blueprint')
|
||||
blueprint2 = Blueprint('name', url_prefix='/my_blueprint2')
|
||||
blueprint2 = Blueprint('name2', url_prefix='/my_blueprint2')
|
||||
blueprint3 = Blueprint('name3', url_prefix='/my_blueprint3')
|
||||
|
||||
|
||||
@blueprint.route('/foo')
|
||||
@@ -18,7 +19,17 @@ async def foo2(request):
|
||||
return json({'msg': 'hi from blueprint2'})
|
||||
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
app.register_blueprint(blueprint2)
|
||||
@blueprint3.websocket('/foo')
|
||||
async def foo3(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
|
||||
app.blueprint(blueprint)
|
||||
app.blueprint(blueprint2)
|
||||
app.blueprint(blueprint3)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"""
|
||||
Example of caching using aiocache package. To run it you will need a Redis
|
||||
instance running in localhost:6379.
|
||||
|
||||
Running this example you will see that the first call lasts 3 seconds and
|
||||
the rest are instant because the value is retrieved from the Redis.
|
||||
|
||||
If you want more info about the package check
|
||||
https://github.com/argaen/aiocache
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiocache
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.log import log
|
||||
from aiocache import cached
|
||||
from aiocache.serializers import JsonSerializer
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
aiocache.settings.set_defaults(
|
||||
class_="aiocache.RedisCache"
|
||||
)
|
||||
|
||||
|
||||
@cached(key="my_custom_key", serializer=JsonSerializer())
|
||||
async def expensive_call():
|
||||
log.info("Expensive has been called")
|
||||
await asyncio.sleep(3)
|
||||
return {"test": True}
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.info("Received GET /")
|
||||
return json(await expensive_call())
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())
|
||||
@@ -1,22 +1,21 @@
|
||||
"""
|
||||
Example intercepting uncaught exceptions using Sanic's error handler framework.
|
||||
|
||||
This may be useful for developers wishing to use Sentry, Airbrake, etc.
|
||||
or a custom system to log and monitor unexpected errors in production.
|
||||
|
||||
First we create our own class inheriting from Handler in sanic.exceptions,
|
||||
and pass in an instance of it when we create our Sanic instance. Inside this
|
||||
class' default handler, we can do anything including sending exceptions to
|
||||
an external service.
|
||||
"""
|
||||
from sanic.exceptions import Handler, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.exceptions import SanicException
|
||||
"""
|
||||
Imports and code relevant for our CustomHandler class
|
||||
(Ordinarily this would be in a separate file)
|
||||
"""
|
||||
|
||||
|
||||
class CustomHandler(Handler):
|
||||
class CustomHandler(ErrorHandler):
|
||||
|
||||
def default(self, request, exception):
|
||||
# Here, we have access to the exception object
|
||||
@@ -38,11 +37,10 @@ server's error_handler to an instance of our CustomHandler
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
handler = CustomHandler(sanic=app)
|
||||
handler = CustomHandler()
|
||||
app.error_handler = handler
|
||||
|
||||
|
||||
@@ -50,8 +48,7 @@ app.error_handler = handler
|
||||
async def test(request):
|
||||
# Here, something occurs which causes an unexpected exception
|
||||
# This exception will flow to our custom handler.
|
||||
1 / 0
|
||||
return json({"test": True})
|
||||
raise SanicException('You Broke It!')
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
## To use this example:
|
||||
# curl -d '{"name": "John Doe"}' localhost:8000
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import html
|
||||
from jinja2 import Template
|
||||
|
||||
template = Template('Hello {{ name }}!')
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
data = request.json
|
||||
return html(template.render(**data))
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -8,10 +8,12 @@ app = Sanic(__name__)
|
||||
|
||||
sem = None
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def init(sanic, loop):
|
||||
global sem
|
||||
CONCURRENCY_PER_WORKER = 4
|
||||
sem = asyncio.Semaphore(CONCURRENCY_PER_WORKER, loop=loop)
|
||||
concurrency_per_worker = 4
|
||||
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
|
||||
|
||||
async def bounded_fetch(session, url):
|
||||
"""
|
||||
@@ -33,4 +35,4 @@ async def test(request):
|
||||
return json(response)
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, workers=2, before_start=init)
|
||||
app.run(host="0.0.0.0", port=8000, workers=2)
|
||||
|
||||
28
examples/modify_header_example.py
Normal file
28
examples/modify_header_example.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Modify header or status in response
|
||||
"""
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'Hello world!'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=200
|
||||
)
|
||||
|
||||
|
||||
@app.route('/unauthorized')
|
||||
def handle_request(request):
|
||||
return response.json(
|
||||
{'message': 'You are not authorized'},
|
||||
headers={'X-Served-By': 'sanic'},
|
||||
status=404
|
||||
)
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
import json
|
||||
from sanic import response
|
||||
import logging
|
||||
|
||||
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||
@@ -15,9 +14,11 @@ log = logging.getLogger()
|
||||
|
||||
# Set logger to override default basicConfig
|
||||
sanic = Sanic()
|
||||
|
||||
|
||||
@sanic.route("/")
|
||||
def test(request):
|
||||
log.info("received request; responding with 'hey'")
|
||||
return text("hey")
|
||||
return response.text("hey")
|
||||
|
||||
sanic.run(host="0.0.0.0", port=8000)
|
||||
|
||||
18
examples/redirect_example.py
Normal file
18
examples/redirect_example.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def handle_request(request):
|
||||
return response.redirect('/redirect')
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
async def test(request):
|
||||
return response.json({"Redirected": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
10
examples/request_stream/client.py
Normal file
10
examples/request_stream/client.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import requests
|
||||
|
||||
# Warning: This is a heavy process.
|
||||
|
||||
data = ""
|
||||
for i in range(1, 250000):
|
||||
data += str(i)
|
||||
|
||||
r = requests.post('http://0.0.0.0:8000/stream', data=data)
|
||||
print(r.text)
|
||||
65
examples/request_stream/server.py
Normal file
65
examples/request_stream/server.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from sanic import Sanic
|
||||
from sanic.views import CompositionView
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.views import stream as stream_decorator
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import stream, text
|
||||
|
||||
bp = Blueprint('blueprint_request_stream')
|
||||
app = Sanic('request_stream')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
@stream_decorator
|
||||
async def post(self, request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
|
||||
@app.post('/stream', stream=True)
|
||||
async def handler(request):
|
||||
async def streaming(response):
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
body = body.decode('utf-8').replace('1', 'A')
|
||||
response.write(body)
|
||||
return stream(streaming)
|
||||
|
||||
|
||||
@bp.put('/bp_stream', stream=True)
|
||||
async def bp_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8').replace('1', 'A')
|
||||
return text(result)
|
||||
|
||||
|
||||
async def post_handler(request):
|
||||
result = ''
|
||||
while True:
|
||||
body = await request.stream.get()
|
||||
if body is None:
|
||||
break
|
||||
result += body.decode('utf-8')
|
||||
return text(result)
|
||||
|
||||
app.blueprint(bp)
|
||||
app.add_route(SimpleView.as_view(), '/method_view')
|
||||
view = CompositionView()
|
||||
view.add(['POST'], post_handler, stream=True)
|
||||
app.add_route(view, '/composition_view')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
@@ -1,6 +1,6 @@
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
from sanic.config import Config
|
||||
from sanic.exceptions import RequestTimeout
|
||||
|
||||
@@ -11,11 +11,11 @@ app = Sanic(__name__)
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
await asyncio.sleep(3)
|
||||
return text('Hello, world!')
|
||||
return response.text('Hello, world!')
|
||||
|
||||
|
||||
@app.exception(RequestTimeout)
|
||||
def timeout(request, exception):
|
||||
return text('RequestTimeout from error_handler.', 408)
|
||||
return response.text('RequestTimeout from error_handler.', 408)
|
||||
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from multiprocessing import Event
|
||||
from sanic import response
|
||||
from signal import signal, SIGINT
|
||||
import asyncio
|
||||
import uvloop
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"answer": "42"})
|
||||
return response.json({"answer": "42"})
|
||||
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8001)
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
task = asyncio.ensure_future(server)
|
||||
signal(SIGINT, lambda s, f: loop.stop())
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
""" To run this example you need additional aiopg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
import aiopg
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
database_name = os.environ['DATABASE_NAME']
|
||||
database_host = os.environ['DATABASE_HOST']
|
||||
database_user = os.environ['DATABASE_USER']
|
||||
database_password = os.environ['DATABASE_PASSWORD']
|
||||
|
||||
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||
database_password,
|
||||
database_host,
|
||||
database_name)
|
||||
|
||||
|
||||
async def get_pool():
|
||||
return await aiopg.create_pool(connection)
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
async def prepare_db(app, loop):
|
||||
"""
|
||||
Let's create some table and add some data
|
||||
"""
|
||||
async with aiopg.create_pool(connection) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||
await cur.execute("""CREATE TABLE sanic_polls (
|
||||
id serial primary key,
|
||||
question varchar(50),
|
||||
pub_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await cur.execute("""INSERT INTO sanic_polls
|
||||
(id, question, pub_date) VALUES ({}, {}, now())
|
||||
""".format(i, i))
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handle(request):
|
||||
result = []
|
||||
async def test_select():
|
||||
async with aiopg.create_pool(connection) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("SELECT question, pub_date FROM sanic_polls")
|
||||
async for row in cur:
|
||||
result.append({"question": row[0], "pub_date": row[1]})
|
||||
res = await test_select()
|
||||
return json({'polls': result})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0',
|
||||
port=8000,
|
||||
debug=True,
|
||||
before_start=prepare_db)
|
||||
@@ -1,66 +0,0 @@
|
||||
""" To run this example you need additional aiopg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import uvloop
|
||||
from aiopg.sa import create_engine
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
database_name = os.environ['DATABASE_NAME']
|
||||
database_host = os.environ['DATABASE_HOST']
|
||||
database_user = os.environ['DATABASE_USER']
|
||||
database_password = os.environ['DATABASE_PASSWORD']
|
||||
|
||||
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||
database_password,
|
||||
database_host,
|
||||
database_name)
|
||||
|
||||
metadata = sa.MetaData()
|
||||
|
||||
polls = sa.Table('sanic_polls', metadata,
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('question', sa.String(50)),
|
||||
sa.Column("pub_date", sa.DateTime))
|
||||
|
||||
|
||||
app = Sanic(name=__name__)
|
||||
|
||||
|
||||
async def prepare_db(app, loop):
|
||||
""" Let's add some data
|
||||
|
||||
"""
|
||||
async with create_engine(connection) as engine:
|
||||
async with engine.acquire() as conn:
|
||||
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||
await conn.execute("""CREATE TABLE sanic_polls (
|
||||
id serial primary key,
|
||||
question varchar(50),
|
||||
pub_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await conn.execute(
|
||||
polls.insert().values(question=i,
|
||||
pub_date=datetime.datetime.now())
|
||||
)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handle(request):
|
||||
async with create_engine(connection) as engine:
|
||||
async with engine.acquire() as conn:
|
||||
result = []
|
||||
async for row in conn.execute(polls.select()):
|
||||
result.append({"question": row.question, "pub_date": row.pub_date})
|
||||
return json({"polls": result})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=prepare_db)
|
||||
@@ -1,58 +0,0 @@
|
||||
""" To run this example you need additional asyncpg package
|
||||
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
from asyncpg import create_pool
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
DB_CONFIG = {
|
||||
'host': '<host>',
|
||||
'user': '<username>',
|
||||
'password': '<password>',
|
||||
'port': '<port>',
|
||||
'database': '<database>'
|
||||
}
|
||||
|
||||
def jsonify(records):
|
||||
"""
|
||||
Parse asyncpg record response into JSON format
|
||||
"""
|
||||
return [{key: value for key, value in
|
||||
zip(r.keys(), r.values())} for r in records]
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
async def create_db(app, loop):
|
||||
"""
|
||||
Create some table and add some data
|
||||
"""
|
||||
async with create_pool(**DB_CONFIG) as pool:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
await connection.execute('DROP TABLE IF EXISTS sanic_post')
|
||||
await connection.execute("""CREATE TABLE sanic_post (
|
||||
id serial primary key,
|
||||
content varchar(50),
|
||||
post_date timestamp
|
||||
);""")
|
||||
for i in range(0, 100):
|
||||
await connection.execute(f"""INSERT INTO sanic_post
|
||||
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def handler(request):
|
||||
async with create_pool(**DB_CONFIG) as pool:
|
||||
async with pool.acquire() as connection:
|
||||
async with connection.transaction():
|
||||
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||
return json({'posts': jsonify(results)})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, before_start=create_db)
|
||||
@@ -1,78 +0,0 @@
|
||||
## You need the following additional packages for this example
|
||||
# aiopg
|
||||
# peewee_async
|
||||
# peewee
|
||||
|
||||
|
||||
## sanic imports
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
## peewee_async related imports
|
||||
import peewee
|
||||
from peewee_async import Manager, PostgresqlDatabase
|
||||
|
||||
# we instantiate a custom loop so we can pass it to our db manager
|
||||
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
## from peewee_async docs:
|
||||
# Also there’s no need to connect and re-connect before executing async queries
|
||||
# with manager! It’s all automatic. But you can run Manager.connect() or
|
||||
# Manager.close() when you need it.
|
||||
|
||||
|
||||
# let's create a simple key value store:
|
||||
class KeyValue(peewee.Model):
|
||||
key = peewee.CharField(max_length=40, unique=True)
|
||||
text = peewee.TextField(default='')
|
||||
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
# create table synchronously
|
||||
KeyValue.create_table(True)
|
||||
|
||||
# OPTIONAL: close synchronous connection
|
||||
database.close()
|
||||
|
||||
# OPTIONAL: disable any future syncronous calls
|
||||
objects.database.allow_sync = False # this will raise AssertionError on ANY sync call
|
||||
|
||||
|
||||
app = Sanic('peewee_example')
|
||||
|
||||
@app.route('/post/<key>/<value>')
|
||||
async def post(request, key, value):
|
||||
"""
|
||||
Save get parameters to database
|
||||
"""
|
||||
obj = await objects.create(KeyValue, key=key, text=value)
|
||||
return json({'object_id': obj.id})
|
||||
|
||||
|
||||
@app.route('/get')
|
||||
async def get(request):
|
||||
"""
|
||||
Load all objects from database
|
||||
"""
|
||||
all_objects = await objects.execute(KeyValue.select())
|
||||
serialized_obj = []
|
||||
for obj in all_objects:
|
||||
serialized_obj.append({
|
||||
'id': obj.id,
|
||||
'key': obj.key,
|
||||
'value': obj.text}
|
||||
)
|
||||
|
||||
return json({'objects': serialized_obj})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=8000, before_start=setup)
|
||||
@@ -1,12 +1,13 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.response import json, text
|
||||
from sanic import response
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
app = Sanic(__name__)
|
||||
@@ -8,37 +10,50 @@ app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def test_async(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/sync", methods=['GET', 'POST'])
|
||||
def test_sync(request):
|
||||
return json({"test": True})
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/dynamic/<name>/<id:int>")
|
||||
def test_params(request, name, id):
|
||||
return text("yeehaww {} {}".format(name, id))
|
||||
def test_params(request, name, i):
|
||||
return response.text("yeehaww {} {}".format(name, i))
|
||||
|
||||
|
||||
@app.route("/exception")
|
||||
def exception(request):
|
||||
raise ServerError("It's dead jim")
|
||||
|
||||
|
||||
@app.route("/await")
|
||||
async def test_await(request):
|
||||
import asyncio
|
||||
await asyncio.sleep(5)
|
||||
return text("I'm feeling sleepy")
|
||||
return response.text("I'm feeling sleepy")
|
||||
|
||||
|
||||
@app.route("/file")
|
||||
async def test_file(request):
|
||||
return await response.file(os.path.abspath("setup.py"))
|
||||
|
||||
|
||||
@app.route("/file_stream")
|
||||
async def test_file_stream(request):
|
||||
return await response.file_stream(os.path.abspath("setup.py"),
|
||||
chunk_size=1024)
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Exceptions
|
||||
# ----------------------------------------------- #
|
||||
|
||||
|
||||
@app.exception(ServerError)
|
||||
async def test(request, exception):
|
||||
return json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code)
|
||||
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
|
||||
status=exception.status_code)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
@@ -47,29 +62,43 @@ async def test(request, exception):
|
||||
|
||||
@app.route("/json")
|
||||
def post_json(request):
|
||||
return json({"received": True, "message": request.json})
|
||||
return response.json({"received": True, "message": request.json})
|
||||
|
||||
|
||||
@app.route("/form")
|
||||
def post_json(request):
|
||||
return json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
|
||||
|
||||
|
||||
@app.route("/query_string")
|
||||
def query_string(request):
|
||||
return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string})
|
||||
return response.json({"parsed": True, "args": request.args, "url": request.url,
|
||||
"query_string": request.query_string})
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
# Run Server
|
||||
# ----------------------------------------------- #
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def before_start(app, loop):
|
||||
log.info("SERVER STARTING")
|
||||
|
||||
|
||||
@app.listener('after_server_start')
|
||||
def after_start(app, loop):
|
||||
log.info("OH OH OH OH OHHHHHHHH")
|
||||
|
||||
|
||||
@app.listener('before_server_stop')
|
||||
def before_stop(app, loop):
|
||||
log.info("SERVER STOPPING")
|
||||
|
||||
|
||||
@app.listener('after_server_stop')
|
||||
def after_stop(app, loop):
|
||||
log.info("TRIED EVERYTHING")
|
||||
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop)
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
23
examples/unix_socket.py
Normal file
23
examples/unix_socket.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
import socket
|
||||
import os
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/test")
|
||||
async def test(request):
|
||||
return response.text("OK")
|
||||
|
||||
if __name__ == '__main__':
|
||||
server_address = './uds_socket'
|
||||
# Make sure the socket does not already exist
|
||||
try:
|
||||
os.unlink(server_address)
|
||||
except OSError:
|
||||
if os.path.exists(server_address):
|
||||
raise
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_address)
|
||||
app.run(sock=sock)
|
||||
20
examples/url_for_example.py
Normal file
20
examples/url_for_example.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
# generate a URL for the endpoint `post_handler`
|
||||
url = app.url_for('post_handler', post_id=5)
|
||||
# the URL is `/posts/5`, redirect to it
|
||||
return response.redirect(url)
|
||||
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
return response.text('Post - {}'.format(post_id))
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -1,4 +1,4 @@
|
||||
from sanic.response import text
|
||||
from sanic import response
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
@@ -11,29 +11,29 @@ from sanic.blueprints import Blueprint
|
||||
app = Sanic()
|
||||
bp = Blueprint("bp", host="bp.example.com")
|
||||
|
||||
|
||||
@app.route('/', host=["example.com",
|
||||
"somethingelse.com",
|
||||
"therestofyourdomains.com"])
|
||||
async def hello(request):
|
||||
return text("Some defaults")
|
||||
return response.text("Some defaults")
|
||||
|
||||
@app.route('/', host="example.com")
|
||||
async def hello(request):
|
||||
return text("Answer")
|
||||
|
||||
@app.route('/', host="sub.example.com")
|
||||
async def hello(request):
|
||||
return text("42")
|
||||
return response.text("42")
|
||||
|
||||
|
||||
@bp.route("/question")
|
||||
async def hello(request):
|
||||
return text("What is the meaning of life?")
|
||||
return response.text("What is the meaning of life?")
|
||||
|
||||
|
||||
@bp.route("/answer")
|
||||
async def hello(request):
|
||||
return text("42")
|
||||
return response.text("42")
|
||||
|
||||
app.register_blueprint(bp)
|
||||
app.blueprint(bp)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
29
examples/websocket.html
Normal file
29
examples/websocket.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/feed'),
|
||||
messages = document.createElement('ul');
|
||||
ws.onmessage = function (event) {
|
||||
var messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li'),
|
||||
content = document.createTextNode('Received: ' + event.data);
|
||||
message.appendChild(content);
|
||||
messages.appendChild(message);
|
||||
};
|
||||
document.body.appendChild(messages);
|
||||
window.setInterval(function() {
|
||||
data = 'bye!'
|
||||
ws.send(data);
|
||||
var messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li'),
|
||||
content = document.createTextNode('Sent: ' + data);
|
||||
message.appendChild(content);
|
||||
messages.appendChild(message);
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
24
examples/websocket.py
Normal file
24
examples/websocket.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import file
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return await file('websocket.html')
|
||||
|
||||
|
||||
@app.websocket('/feed')
|
||||
async def feed(request, ws):
|
||||
while True:
|
||||
data = 'hello!'
|
||||
print('Sending: ' + data)
|
||||
await ws.send(data)
|
||||
data = await ws.recv()
|
||||
print('Received: ' + data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
aiocache
|
||||
aiofiles
|
||||
aiohttp
|
||||
aiohttp==1.3.5
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
bottle
|
||||
coverage
|
||||
falcon
|
||||
gunicorn
|
||||
httptools
|
||||
kyoukai
|
||||
flake8
|
||||
pytest
|
||||
recommonmark
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
tornado
|
||||
tox
|
||||
ujson
|
||||
uvloop
|
||||
gunicorn
|
||||
|
||||
3
requirements-docs.txt
Normal file
3
requirements-docs.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
recommonmark
|
||||
@@ -2,3 +2,4 @@ aiofiles
|
||||
httptools
|
||||
ujson
|
||||
uvloop
|
||||
websockets
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .sanic import Sanic
|
||||
from .blueprints import Blueprint
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.3.1'
|
||||
__version__ = '0.6.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from .log import log
|
||||
from .sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.app import Sanic
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(prog='sanic')
|
||||
parser.add_argument('--host', dest='host', type=str, default='127.0.0.1')
|
||||
parser.add_argument('--port', dest='port', type=int, default=8000)
|
||||
parser.add_argument('--cert', dest='cert', type=str,
|
||||
help='location of certificate for SSL')
|
||||
parser.add_argument('--key', dest='key', type=str,
|
||||
help='location of keyfile for SSL.')
|
||||
parser.add_argument('--workers', dest='workers', type=int, default=1, )
|
||||
parser.add_argument('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
@@ -24,13 +28,17 @@ if __name__ == "__main__":
|
||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
if args.cert is not None or args.key is not None:
|
||||
ssl = {'cert': args.cert, 'key': args.key}
|
||||
else:
|
||||
ssl = None
|
||||
|
||||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug)
|
||||
except ImportError:
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
except ImportError as e:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(module_name))
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
|
||||
746
sanic/app.py
Normal file
746
sanic/app.py
Normal file
@@ -0,0 +1,746 @@
|
||||
import logging
|
||||
import logging.config
|
||||
import re
|
||||
import warnings
|
||||
from asyncio import get_event_loop, ensure_future, CancelledError
|
||||
from collections import deque, defaultdict
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
from ssl import create_default_context, Purpose
|
||||
|
||||
from sanic.config import Config, LOGGING
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.log import log
|
||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
|
||||
from sanic.static import register as static_register
|
||||
from sanic.testing import SanicTestClient
|
||||
from sanic.views import CompositionView
|
||||
from sanic.websocket import WebSocketProtocol, ConnectionClosed
|
||||
|
||||
|
||||
class Sanic:
|
||||
|
||||
def __init__(self, name=None, router=None, error_handler=None,
|
||||
load_env=True, request_class=None,
|
||||
log_config=LOGGING):
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
# Get name from previous stack frame
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config(load_env=load_env)
|
||||
self.log_config = log_config
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.listeners = defaultdict(list)
|
||||
self.is_running = False
|
||||
self.is_request_stream = False
|
||||
self.websocket_enabled = False
|
||||
self.websocket_tasks = []
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
"""Synonymous with asyncio.get_event_loop().
|
||||
|
||||
Only supported when using the `app.run` method.
|
||||
"""
|
||||
if not self.is_running:
|
||||
raise SanicException(
|
||||
'Loop can only be retrieved after the app has started '
|
||||
'running. Not supported with `create_server` function')
|
||||
return get_event_loop()
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def add_task(self, task):
|
||||
"""Schedule a task to run later, after the loop has started.
|
||||
Different from asyncio.ensure_future in that it does not
|
||||
also return a future, and the actual ensure_future call
|
||||
is delayed until before server start.
|
||||
|
||||
:param task: future, couroutine or awaitable
|
||||
"""
|
||||
@self.listener('before_server_start')
|
||||
def run(app, loop):
|
||||
if callable(task):
|
||||
loop.create_task(task())
|
||||
else:
|
||||
loop.create_task(task)
|
||||
|
||||
# Decorator
|
||||
def listener(self, event):
|
||||
"""Create a listener from a decorated function.
|
||||
|
||||
:param event: event to listen to
|
||||
"""
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
return decorator
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False, version=None):
|
||||
"""Decorate a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param stream:
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
if stream:
|
||||
self.is_request_stream = True
|
||||
|
||||
def response(handler):
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host, strict_slashes=strict_slashes,
|
||||
version=version)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, version=None):
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:param host:
|
||||
:return: function or class instance
|
||||
"""
|
||||
stream = False
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
_handler = getattr(handler.view_class, method.lower(), None)
|
||||
if _handler:
|
||||
methods.add(method)
|
||||
if hasattr(_handler, 'is_stream'):
|
||||
stream = True
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
for _handler in handler.handlers.values():
|
||||
if hasattr(_handler, 'is_stream'):
|
||||
stream = True
|
||||
break
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)(handler)
|
||||
return handler
|
||||
|
||||
# Decorator
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
"""Decorate a function to be registered as a websocket route
|
||||
:param uri: path of the URL
|
||||
:param host:
|
||||
:return: decorated function
|
||||
"""
|
||||
self.enable_websocket()
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
def response(handler):
|
||||
async def websocket_handler(request, *args, **kwargs):
|
||||
request.app = self
|
||||
try:
|
||||
protocol = request.transport.get_protocol()
|
||||
except AttributeError:
|
||||
# On Python3.5 the Transport classes in asyncio do not
|
||||
# have a get_protocol() method as in uvloop
|
||||
protocol = request.transport._protocol
|
||||
ws = await protocol.websocket_handshake(request)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.append(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
pass
|
||||
self.websocket_tasks.remove(fut)
|
||||
await ws.close()
|
||||
|
||||
self.router.add(uri=uri, handler=websocket_handler,
|
||||
methods=frozenset({'GET'}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None,
|
||||
strict_slashes=False):
|
||||
"""A helper method to register a function as a websocket route."""
|
||||
return self.websocket(uri, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
|
||||
Websocket is enabled automatically if websocket routes are
|
||||
added to the application.
|
||||
"""
|
||||
if not self.websocket_enabled:
|
||||
# if the server is stopped, we want to cancel any ongoing
|
||||
# websocket tasks, to allow the server to exit promptly
|
||||
@self.listener('before_server_stop')
|
||||
def cancel_websocket_tasks(app, loop):
|
||||
for task in self.websocket_tasks:
|
||||
task.cancel()
|
||||
|
||||
self.websocket_enabled = enable
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""Decorate a function to be registered as a handler for exceptions
|
||||
|
||||
:param exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
def response(handler):
|
||||
for exception in exceptions:
|
||||
if isinstance(exception, (tuple, list)):
|
||||
for e in exception:
|
||||
self.error_handler.add(e, handler)
|
||||
else:
|
||||
self.error_handler.add(exception, handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def register_middleware(self, middleware, attach_to='request'):
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
self.response_middleware.appendleft(middleware)
|
||||
return middleware
|
||||
|
||||
# Decorator
|
||||
def middleware(self, middleware_or_request):
|
||||
"""Decorate and register middleware to be called before a request.
|
||||
Can either be called as @app.middleware or @app.middleware('request')
|
||||
"""
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if callable(middleware_or_request):
|
||||
return self.register_middleware(middleware_or_request)
|
||||
|
||||
else:
|
||||
return partial(self.register_middleware,
|
||||
attach_to=middleware_or_request)
|
||||
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||
use_modified_since=True, use_content_range=False,
|
||||
stream_large_files=False):
|
||||
"""Register a root to serve files from. The input can either be a
|
||||
file or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, \
|
||||
'A blueprint with the name "%s" is already registered. ' \
|
||||
'Blueprint names must be unique.' % \
|
||||
(blueprint.name,)
|
||||
else:
|
||||
self.blueprints[blueprint.name] = blueprint
|
||||
self._blueprint_order.append(blueprint)
|
||||
blueprint.register(self, options)
|
||||
|
||||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
if self.debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method"
|
||||
" instead",
|
||||
DeprecationWarning)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs):
|
||||
"""Build a URL based on a view name and the values provided.
|
||||
|
||||
In order to build a URL, all request parameters must be supplied as
|
||||
keyword arguments, and each parameter must pass the test for the
|
||||
specified parameter type. If these conditions are not met, a
|
||||
`URLBuildError` will be thrown.
|
||||
|
||||
Keyword arguments that are not request parameters will be included in
|
||||
the output URL's query string.
|
||||
|
||||
:param view_name: string referencing the view name
|
||||
:param \*\*kwargs: keys and values that are used to build request
|
||||
parameters and query string arguments.
|
||||
|
||||
:return: the built URL
|
||||
|
||||
Raises:
|
||||
URLBuildError
|
||||
"""
|
||||
# find the route by the supplied view name
|
||||
uri, route = self.router.find_route_by_view_name(view_name)
|
||||
|
||||
if not uri or not route:
|
||||
raise URLBuildError(
|
||||
'Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
|
||||
if uri != '/' and uri.endswith('/'):
|
||||
uri = uri[:-1]
|
||||
|
||||
out = uri
|
||||
|
||||
# find all the parameters we will need to build in the URL
|
||||
matched_params = re.findall(
|
||||
self.router.parameter_pattern, uri)
|
||||
|
||||
# _method is only a placeholder now, don't know how to support it
|
||||
kwargs.pop('_method', None)
|
||||
anchor = kwargs.pop('_anchor', '')
|
||||
# _external need SERVER_NAME in config or pass _server arg
|
||||
external = kwargs.pop('_external', False)
|
||||
scheme = kwargs.pop('_scheme', '')
|
||||
if scheme and not external:
|
||||
raise ValueError('When specifying _scheme, _external must be True')
|
||||
|
||||
netloc = kwargs.pop('_server', None)
|
||||
if netloc is None and external:
|
||||
netloc = self.config.get('SERVER_NAME', '')
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(
|
||||
match)
|
||||
# we only want to match against each individual parameter
|
||||
specific_pattern = '^{}$'.format(pattern)
|
||||
supplied_param = None
|
||||
|
||||
if kwargs.get(name):
|
||||
supplied_param = kwargs.get(name)
|
||||
del kwargs[name]
|
||||
else:
|
||||
raise URLBuildError(
|
||||
'Required parameter `{}` was not passed to url_for'.format(
|
||||
name))
|
||||
|
||||
supplied_param = str(supplied_param)
|
||||
# determine if the parameter supplied by the caller passes the test
|
||||
# in the URL
|
||||
passes_pattern = re.match(specific_pattern, supplied_param)
|
||||
|
||||
if not passes_pattern:
|
||||
if _type != str:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` does not '
|
||||
'match pattern for type `{}`: {}'.format(
|
||||
supplied_param, name, _type.__name__, pattern))
|
||||
else:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` '
|
||||
'does not satisfy pattern {}'.format(
|
||||
supplied_param, name, pattern))
|
||||
raise URLBuildError(msg)
|
||||
|
||||
# replace the parameter in the URL with the supplied value
|
||||
replacement_regex = '(<{}.*?>)'.format(name)
|
||||
|
||||
out = re.sub(
|
||||
replacement_regex, supplied_param, out)
|
||||
|
||||
# parse the remainder of the keyword arguments into a querystring
|
||||
query_string = urlencode(kwargs, doseq=True) if kwargs else ''
|
||||
# scheme://netloc/path;parameters?query#fragment
|
||||
out = urlunparse((scheme, netloc, out, '', query_string, anchor))
|
||||
|
||||
return out
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, write_callback, stream_callback):
|
||||
"""Take a request from the HTTP Server and return a response object
|
||||
to be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param write_callback: Synchronous response function to be
|
||||
called with the response as the only argument
|
||||
:param stream_callback: Coroutine that handles streaming a
|
||||
StreamingHTTPResponse if produced by the handler.
|
||||
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.app = self
|
||||
response = await self._run_request_middleware(request)
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs, uri = self.router.get(request)
|
||||
request.uri_template = uri
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
"handler from the router"))
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
|
||||
try:
|
||||
response = self.error_handler.response(request, e)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
finally:
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
try:
|
||||
response = await self._run_response_middleware(request,
|
||||
response)
|
||||
except:
|
||||
log.exception(
|
||||
'Exception occured in one of response middleware handlers'
|
||||
)
|
||||
|
||||
# pass the response to the correct callback
|
||||
if isinstance(response, StreamingHTTPResponse):
|
||||
await stream_callback(response)
|
||||
else:
|
||||
write_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Testing
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
@property
|
||||
def test_client(self):
|
||||
return SanicTestClient(self)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host=None, port=None, debug=False, ssl=None,
|
||||
sock=None, workers=1, protocol=None,
|
||||
backlog=100, stop_event=None, register_sys_signals=True,
|
||||
log_config=None):
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param ssl: SSLContext, or location of certificate and key
|
||||
for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param backlog:
|
||||
:param stop_event:
|
||||
:param register_sys_signals:
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
self.log_config = log_config
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
workers=workers, protocol=protocol, backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
has_log=self.log_config is not None)
|
||||
|
||||
try:
|
||||
self.is_running = True
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers)
|
||||
except:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
raise
|
||||
finally:
|
||||
self.is_running = False
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
def __call__(self):
|
||||
"""gunicorn compatibility"""
|
||||
return self
|
||||
|
||||
async def create_server(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, protocol=None,
|
||||
backlog=100, stop_event=None,
|
||||
log_config=LOGGING):
|
||||
"""Asynchronous version of `run`.
|
||||
|
||||
NOTE: This does not support multiprocessing and is not the preferred
|
||||
way to run a Sanic application.
|
||||
"""
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
loop=get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, run_async=True,
|
||||
has_log=log_config is not None)
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
async def _run_request_middleware(self, request):
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
return response
|
||||
return None
|
||||
|
||||
async def _run_response_middleware(self, request, response):
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
return response
|
||||
|
||||
def _helper(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False, has_log=True):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
cert = ssl.get('cert') or ssl.get('certificate')
|
||||
key = ssl.get('key') or ssl.get('keyfile')
|
||||
if cert is None or key is None:
|
||||
raise ValueError("SSLContext or certificate and key required.")
|
||||
context = create_default_context(purpose=Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(cert, keyfile=key)
|
||||
ssl = context
|
||||
if stop_event is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'request_class': self.request_class,
|
||||
'is_request_stream': self.is_request_stream,
|
||||
'router': self.router,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'signal': Signal(),
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'keep_alive': self.config.KEEP_ALIVE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog,
|
||||
'has_log': has_log,
|
||||
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
||||
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
||||
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, reverse in (
|
||||
("before_server_start", "before_start", False),
|
||||
("after_server_start", "after_start", False),
|
||||
("before_server_stop", "before_stop", True),
|
||||
("after_server_stop", "after_stop", True),
|
||||
):
|
||||
listeners = self.listeners[event_name].copy()
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
if host and port:
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
@@ -1,7 +1,11 @@
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods', 'host',
|
||||
'strict_slashes', 'stream', 'version'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
@@ -10,10 +14,10 @@ FutureStatic = namedtuple('Route',
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
"""
|
||||
Creates a new blueprint
|
||||
:param name: Unique name of the blueprint
|
||||
def __init__(self, name, url_prefix=None, host=None, version=None):
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
"""
|
||||
self.name = name
|
||||
@@ -21,15 +25,15 @@ class Blueprint:
|
||||
self.host = host
|
||||
|
||||
self.routes = []
|
||||
self.websocket_routes = []
|
||||
self.exceptions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
self.statics = []
|
||||
self.version = version
|
||||
|
||||
def register(self, app, options):
|
||||
"""
|
||||
Registers the blueprint to the sanic app.
|
||||
"""
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
|
||||
@@ -40,19 +44,38 @@ class Blueprint:
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
|
||||
version = future.version or self.version
|
||||
|
||||
app.route(
|
||||
uri=uri,
|
||||
uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
# prefixed properly in the router
|
||||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
if future.args or future.kwargs:
|
||||
app.middleware(*future.args,
|
||||
**future.kwargs)(future.middleware)
|
||||
app.register_middleware(future.middleware,
|
||||
*future.args,
|
||||
**future.kwargs)
|
||||
else:
|
||||
app.middleware(future.middleware)
|
||||
app.register_middleware(future.middleware)
|
||||
|
||||
# Exceptions
|
||||
for future in self.exceptions:
|
||||
@@ -65,32 +88,77 @@ class Blueprint:
|
||||
app.static(uri, future.file_or_directory,
|
||||
*future.args, **future.kwargs)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Creates a blueprint route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
# Event listeners
|
||||
for event, listeners in self.listeners.items():
|
||||
for listener in listeners:
|
||||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False, version=None):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
route = FutureRoute(
|
||||
handler, uri, methods, host, strict_slashes, stream, version)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=None, host=None):
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, version=None):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:return: function or class instance
|
||||
"""
|
||||
Creates a blueprint route from a function.
|
||||
:param handler: Function to handle uri request.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
:param methods: List of acceptable HTTP methods.
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, version=version)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=False, version=None):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
self.routes.append(route)
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||
False, version)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.websocket(uri=uri, host=host, version=version)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
"""
|
||||
Create a listener from a decorated function.
|
||||
"""Create a listener from a decorated function.
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
def decorator(listener):
|
||||
@@ -99,9 +167,7 @@ class Blueprint:
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint middleware from a decorated function.
|
||||
"""
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
@@ -116,9 +182,7 @@ class Blueprint:
|
||||
return register_middleware
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint exception from a decorated function.
|
||||
"""
|
||||
"""Create a blueprint exception from a decorated function."""
|
||||
def decorator(handler):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
@@ -126,32 +190,45 @@ class Blueprint:
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
"""
|
||||
Creates a blueprint static route from a decorated function.
|
||||
:param uri: Endpoint at which the route will be accessible.
|
||||
"""Create a blueprint static route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=["GET"], host=host)
|
||||
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
|
||||
134
sanic/config.py
134
sanic/config.py
@@ -1,9 +1,106 @@
|
||||
import os
|
||||
import sys
|
||||
import syslog
|
||||
import platform
|
||||
import types
|
||||
|
||||
from sanic.log import DefaultFilter
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
_address_dict = {
|
||||
'Windows': ('localhost', 514),
|
||||
'Darwin': '/var/run/syslog',
|
||||
'Linux': '/dev/log',
|
||||
'FreeBSD': '/var/run/log'
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'filters': {
|
||||
'accessFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [0, 10, 20]
|
||||
},
|
||||
'errorFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [30, 40, 50]
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
'access': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
|
||||
'%(request)s %(message)s %(status)d %(byte)d',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'internal': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'accessStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'errorStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
# before you use accessSysLog, be sure that log levels
|
||||
# 0, 10, 20 have been enabled in you syslog configuration
|
||||
# otherwise you won't be able to see the output in syslog
|
||||
# logging file.
|
||||
'accessSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access'
|
||||
},
|
||||
'errorSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'sanic': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['internal', 'errorStream']
|
||||
},
|
||||
'network': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['accessStream', 'errorStream']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# this happens when using container or systems without syslog
|
||||
# keep things in config would cause file not exists error
|
||||
_addr = LOGGING['handlers']['accessSysLog']['address']
|
||||
if type(_addr) is str and not os.path.exists(_addr):
|
||||
LOGGING['handlers'].pop('accessSysLog')
|
||||
LOGGING['handlers'].pop('errorSysLog')
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
super().__init__(defaults or {})
|
||||
self.LOGO = """
|
||||
▄▄▄▄▄
|
||||
@@ -26,8 +123,15 @@ class Config(dict):
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
|
||||
if load_env:
|
||||
self.load_environment_vars()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -39,8 +143,9 @@ class Config(dict):
|
||||
self[attr] = value
|
||||
|
||||
def from_envvar(self, variable_name):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
"""Load a configuration from an environment variable pointing to
|
||||
a configuration file.
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
||||
"""
|
||||
@@ -52,8 +157,9 @@ class Config(dict):
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
"""Updates the values in the config from a Python file. Only the uppercase
|
||||
variables in that module are stored in the config.
|
||||
"""Update the values in the config from a Python file.
|
||||
Only the uppercase variables in that module are stored in the config.
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
@@ -69,7 +175,7 @@ class Config(dict):
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object.
|
||||
"""Update the values from the given object.
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
@@ -88,3 +194,19 @@ class Config(dict):
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self):
|
||||
"""
|
||||
Looks for any ``SANIC_`` prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(SANIC_PREFIX):
|
||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||
try:
|
||||
self[config_key] = int(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime
|
||||
import re
|
||||
import string
|
||||
|
||||
@@ -20,7 +19,7 @@ _Translator.update({
|
||||
|
||||
|
||||
def _quote(str):
|
||||
r"""Quote a string for use in a cookie header.
|
||||
"""Quote a string for use in a cookie header.
|
||||
If the string does not need to be double-quoted, then just return the
|
||||
string. Otherwise, surround the string in doublequotes and quote
|
||||
(with a \) special characters.
|
||||
@@ -39,8 +38,7 @@ _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
|
||||
class CookieJar(dict):
|
||||
"""
|
||||
CookieJar dynamically writes headers as cookies are added and removed
|
||||
"""CookieJar dynamically writes headers as cookies are added and removed
|
||||
It gets around the limitation of one header per name by using the
|
||||
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
||||
"""
|
||||
@@ -75,9 +73,7 @@ class CookieJar(dict):
|
||||
|
||||
|
||||
class Cookie(dict):
|
||||
"""
|
||||
This is a stripped down version of Morsel from SimpleCookie #gottagofast
|
||||
"""
|
||||
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
@@ -107,16 +103,21 @@ class Cookie(dict):
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age' and isinstance(value, int):
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
elif key == 'expires' and isinstance(value, datetime):
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
elif key in self._flags:
|
||||
if self[key]:
|
||||
output.append(self._keys[key])
|
||||
if key == 'max-age':
|
||||
try:
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key == 'expires':
|
||||
try:
|
||||
output.append('%s=%s' % (
|
||||
self._keys[key],
|
||||
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||
))
|
||||
except AttributeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key in self._flags and self[key]:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
|
||||
@@ -128,9 +129,8 @@ class Cookie(dict):
|
||||
|
||||
|
||||
class MultiHeader:
|
||||
"""
|
||||
Allows us to set a header within response that has a unique key,
|
||||
but may contain duplicate header names
|
||||
"""String-holding object which allow us to set a header within response
|
||||
that has a unique key, but may contain duplicate header names
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from .response import text, html
|
||||
from .log import log
|
||||
from traceback import format_exc, extract_tb
|
||||
import sys
|
||||
from sanic.response import ALL_STATUS_CODES, COMMON_STATUS_CODES
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
<style>
|
||||
@@ -52,6 +49,10 @@ TRACEBACK_STYLE = '''
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.tb-border {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
background-color: #e2eafb;
|
||||
}
|
||||
@@ -68,20 +69,35 @@ TRACEBACK_WRAPPER_HTML = '''
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
<p class="summary">
|
||||
{inner_html}
|
||||
<div class="summary">
|
||||
<p>
|
||||
<b>{exc_name}: {exc_value}</b>
|
||||
while handling uri <code>{uri}</code>
|
||||
while handling path <code>{path}</code>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
TRACEBACK_WRAPPER_INNER_HTML = '''
|
||||
<h1>{exc_name}</h1>
|
||||
<h3><code>{exc_value}</code></h3>
|
||||
<div class="tb-wrapper">
|
||||
<p class="tb-header">Traceback (most recent call last):</p>
|
||||
{frame_html}
|
||||
</div>
|
||||
'''
|
||||
|
||||
TRACEBACK_BORDER = '''
|
||||
<div class="tb-border">
|
||||
<b><i>
|
||||
The above exception was the direct cause of the
|
||||
following exception:
|
||||
</i></b>
|
||||
</div>
|
||||
'''
|
||||
|
||||
TRACEBACK_LINE_HTML = '''
|
||||
<div class="frame-line">
|
||||
<p class="frame-descriptor">
|
||||
@@ -101,31 +117,50 @@ INTERNAL_SERVER_ERROR_HTML = '''
|
||||
'''
|
||||
|
||||
|
||||
_sanic_exceptions = {}
|
||||
|
||||
|
||||
def add_status_code(code):
|
||||
"""
|
||||
Decorator used for adding exceptions to _sanic_exceptions.
|
||||
"""
|
||||
def class_decorator(cls):
|
||||
cls.status_code = code
|
||||
_sanic_exceptions[code] = cls
|
||||
return cls
|
||||
return class_decorator
|
||||
|
||||
|
||||
class SanicException(Exception):
|
||||
|
||||
def __init__(self, message, status_code=None):
|
||||
super().__init__(message)
|
||||
|
||||
if status_code is not None:
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
@add_status_code(404)
|
||||
class NotFound(SanicException):
|
||||
status_code = 404
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(400)
|
||||
class InvalidUsage(SanicException):
|
||||
status_code = 400
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
status_code = 500
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(SanicException):
|
||||
status_code = 500
|
||||
class URLBuildError(ServerError):
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
status_code = 404
|
||||
pass
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
@@ -133,93 +168,95 @@ class FileNotFound(NotFound):
|
||||
self.relative_url = relative_url
|
||||
|
||||
|
||||
@add_status_code(408)
|
||||
class RequestTimeout(SanicException):
|
||||
status_code = 408
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
pass
|
||||
|
||||
|
||||
class Handler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = []
|
||||
self.cached_handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
frames = extract_tb(tb)
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
pass
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
"Content-Range": "bytes */%s" % (content_range.total,)
|
||||
}
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exc_type.__name__,
|
||||
exc_value=exc_value,
|
||||
frame_html=''.join(frame_html),
|
||||
uri=request.url)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers.append((exception, handler))
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
pass
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""
|
||||
Fetches and executes an exception handler and returns a response object
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
pass
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.lookup(exception)
|
||||
try:
|
||||
response = handler and handler(
|
||||
request=request, exception=exception)
|
||||
if response is None:
|
||||
response = self.default(request=request, exception=exception)
|
||||
except:
|
||||
log.error(format_exc())
|
||||
if self.debug:
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, request.url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def default(self, request, exception):
|
||||
log.error(format_exc())
|
||||
if isinstance(exception, SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500))
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
:param message: Message describing the exception.
|
||||
:param scheme: Name of the authentication scheme to be used.
|
||||
|
||||
When present, kwargs is used to complete the WWW-Authentication header.
|
||||
|
||||
Examples::
|
||||
|
||||
# With a Basic auth-scheme, realm MUST be present:
|
||||
raise Unauthorized("Auth required.", "Basic", realm="Restricted Area")
|
||||
|
||||
# With a Digest auth-scheme, things are a bit more complicated:
|
||||
raise Unauthorized("Auth required.",
|
||||
"Digest",
|
||||
realm="Restricted Area",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
|
||||
# With a Bearer auth-scheme, realm is optional so you can write:
|
||||
raise Unauthorized("Auth required.", "Bearer")
|
||||
|
||||
# or, if you want to specify the realm:
|
||||
raise Unauthorized("Auth required.", "Bearer", realm="Restricted Area")
|
||||
"""
|
||||
def __init__(self, message, scheme, **kwargs):
|
||||
super().__init__(message)
|
||||
|
||||
values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()]
|
||||
challenge = ', '.join(values)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
}
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
"""
|
||||
Raise an exception based on SanicException. Returns the HTTP response
|
||||
message appropriate for the given status code, unless provided.
|
||||
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages
|
||||
in response.py for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
message = COMMON_STATUS_CODES.get(status_code,
|
||||
ALL_STATUS_CODES.get(status_code))
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode('utf8')
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
raise sanic_exception(message=message, status_code=status_code)
|
||||
|
||||
171
sanic/handlers.py
Normal file
171
sanic/handlers.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import sys
|
||||
from traceback import format_exc, extract_tb
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
from sanic.log import log
|
||||
from sanic.response import text, html
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = []
|
||||
self.cached_handlers = {}
|
||||
self.debug = False
|
||||
|
||||
def _render_exception(self, exception):
|
||||
frames = extract_tb(exception.__traceback__)
|
||||
|
||||
frame_html = []
|
||||
for frame in frames:
|
||||
frame_html.append(TRACEBACK_LINE_HTML.format(frame))
|
||||
|
||||
return TRACEBACK_WRAPPER_INNER_HTML.format(
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
frame_html=''.join(frame_html))
|
||||
|
||||
def _render_traceback_html(self, exception, request):
|
||||
exc_type, exc_value, tb = sys.exc_info()
|
||||
exceptions = []
|
||||
|
||||
while exc_value:
|
||||
exceptions.append(self._render_exception(exc_value))
|
||||
exc_value = exc_value.__cause__
|
||||
|
||||
return TRACEBACK_WRAPPER_HTML.format(
|
||||
style=TRACEBACK_STYLE,
|
||||
exc_name=exception.__class__.__name__,
|
||||
exc_value=exception,
|
||||
inner_html=TRACEBACK_BORDER.join(reversed(exceptions)),
|
||||
path=request.path)
|
||||
|
||||
def add(self, exception, handler):
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(exception, self._missing)
|
||||
if handler is self._missing:
|
||||
for exception_class, handler in self.handlers:
|
||||
if isinstance(exception, exception_class):
|
||||
self.cached_handlers[type(exception)] = handler
|
||||
return handler
|
||||
self.cached_handlers[type(exception)] = None
|
||||
handler = None
|
||||
return handler
|
||||
|
||||
def response(self, request, exception):
|
||||
"""Fetches and executes an exception handler and returns a response
|
||||
object
|
||||
|
||||
:param request: Request
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.lookup(exception)
|
||||
response = None
|
||||
try:
|
||||
if handler:
|
||||
response = handler(request=request, exception=exception)
|
||||
if response is None:
|
||||
response = self.default(request=request, exception=exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level='error'):
|
||||
"""
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
"""
|
||||
getattr(log, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500),
|
||||
headers=getattr(exception, 'headers', dict())
|
||||
)
|
||||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""Class responsible for parsing request header"""
|
||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get('Range')
|
||||
if _range is None:
|
||||
raise HeaderNotFound('Range Header Not Found')
|
||||
unit, _, value = tuple(map(str.strip, _range.partition('=')))
|
||||
if unit != 'bytes':
|
||||
raise InvalidRangeType(
|
||||
'%s is not a valid Range Type' % (unit,), self)
|
||||
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
|
||||
try:
|
||||
self.start = int(start_b) if start_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (start_b,), self)
|
||||
try:
|
||||
self.end = int(end_b) if end_b else None
|
||||
except ValueError:
|
||||
raise ContentRangeError(
|
||||
'\'%s\' is invalid for Content Range' % (end_b,), self)
|
||||
if self.end is None:
|
||||
if self.start is None:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total
|
||||
else:
|
||||
if self.start is None:
|
||||
# this case represents `Content-Range: bytes -5`
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total
|
||||
if self.start >= self.end:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
self.size = self.end - self.start
|
||||
self.headers = {
|
||||
'Content-Range': "bytes %s-%s/%s" % (
|
||||
self.start, self.end, self.total)}
|
||||
|
||||
def __bool__(self):
|
||||
return self.size > 0
|
||||
15
sanic/log.py
15
sanic/log.py
@@ -1,3 +1,18 @@
|
||||
import logging
|
||||
|
||||
|
||||
class DefaultFilter(logging.Filter):
|
||||
|
||||
def __init__(self, param=None):
|
||||
self.param = param
|
||||
|
||||
def filter(self, record):
|
||||
if self.param is None:
|
||||
return True
|
||||
if record.levelno in self.param:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
log = logging.getLogger('sanic')
|
||||
netlog = logging.getLogger('network')
|
||||
|
||||
149
sanic/request.py
149
sanic/request.py
@@ -1,12 +1,23 @@
|
||||
import sys
|
||||
import json
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs
|
||||
from ujson import loads as json_loads
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from urllib.parse import parse_qs, urlunparse
|
||||
|
||||
from .log import log
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
except ImportError:
|
||||
if sys.version_info[:2] == (3, 5):
|
||||
def json_loads(data):
|
||||
# on Python 3.5 json.loads only supports str not bytes
|
||||
return json.loads(data.decode())
|
||||
else:
|
||||
json_loads = json.loads
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import log
|
||||
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
@@ -16,8 +27,7 @@ DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
"""Hosts a dict with lists as values where get returns the first
|
||||
value of the list and getlist returns the whole shebang
|
||||
"""
|
||||
|
||||
@@ -31,27 +41,22 @@ class RequestParameters(dict):
|
||||
|
||||
|
||||
class Request(dict):
|
||||
"""
|
||||
Properties of an HTTP request such as URL, headers, etc.
|
||||
"""
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'query_string', 'body',
|
||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip',
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr'
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
self.app = None
|
||||
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
self.transport = transport
|
||||
self.query_string = None
|
||||
if url_parsed.query:
|
||||
self.query_string = url_parsed.query.decode('utf-8')
|
||||
|
||||
# Init but do not inhale
|
||||
self.body = []
|
||||
@@ -59,7 +64,9 @@ class Request(dict):
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = None
|
||||
self.uri_template = None
|
||||
self._cookies = None
|
||||
self.stream = None
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
@@ -67,19 +74,26 @@ class Request(dict):
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
"""
|
||||
Attempts to return the auth header token.
|
||||
"""Attempt to return the auth header token.
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
prefixes = ('Bearer', 'Token')
|
||||
auth_header = self.headers.get('Authorization')
|
||||
|
||||
if auth_header is not None:
|
||||
return auth_header.split()[1]
|
||||
for prefix in prefixes:
|
||||
if prefix in auth_header:
|
||||
return auth_header.partition(prefix)[-1].strip()
|
||||
|
||||
return auth_header
|
||||
|
||||
@property
|
||||
@@ -118,14 +132,17 @@ class Request(dict):
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
else:
|
||||
self.parsed_args = {}
|
||||
|
||||
self.parsed_args = RequestParameters()
|
||||
return self.parsed_args
|
||||
|
||||
@property
|
||||
def raw_args(self):
|
||||
return {k: v[0] for k, v in self.args.items()}
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
||||
cookie = self.headers.get('Cookie')
|
||||
if cookie is not None:
|
||||
cookies = SimpleCookie()
|
||||
cookies.load(cookie)
|
||||
@@ -138,19 +155,87 @@ class Request(dict):
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_ip'):
|
||||
self._ip = self.transport.get_extra_info('peername')
|
||||
self._ip = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def remote_addr(self):
|
||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
||||
|
||||
:return: original client ip.
|
||||
"""
|
||||
if not hasattr(self, '_remote_addr'):
|
||||
forwarded_for = self.headers.get('X-Forwarded-For', '').split(',')
|
||||
remote_addrs = [
|
||||
addr for addr in [
|
||||
addr.strip() for addr in forwarded_for
|
||||
] if addr
|
||||
]
|
||||
if len(remote_addrs) > 0:
|
||||
self._remote_addr = remote_addrs[0]
|
||||
else:
|
||||
self._remote_addr = ''
|
||||
return self._remote_addr
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
if self.app.websocket_enabled \
|
||||
and self.headers.get('upgrade') == 'websocket':
|
||||
scheme = 'ws'
|
||||
else:
|
||||
scheme = 'http'
|
||||
|
||||
if self.transport.get_extra_info('sslcontext'):
|
||||
scheme += 's'
|
||||
|
||||
return scheme
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
# it appears that httptools doesn't return the host
|
||||
# so pull it from the headers
|
||||
return self.headers.get('Host', '')
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return self.headers.get('Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def match_info(self):
|
||||
"""return matched info after resolving route"""
|
||||
return self.app.router.get(self)[2]
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._parsed_url.path.decode('utf-8')
|
||||
|
||||
@property
|
||||
def query_string(self):
|
||||
if self._parsed_url.query:
|
||||
return self._parsed_url.query.decode('utf-8')
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return urlunparse((
|
||||
self.scheme,
|
||||
self.host,
|
||||
self.path,
|
||||
None,
|
||||
self.query_string,
|
||||
None))
|
||||
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
|
||||
|
||||
def parse_multipart_form(body, boundary):
|
||||
"""
|
||||
Parses a request body and returns fields and files
|
||||
"""Parse a request body and returns fields and files
|
||||
|
||||
:param body: Bytes request body
|
||||
:param boundary: Bytes multipart boundary
|
||||
:param body: bytes request body
|
||||
:param boundary: bytes multipart boundary
|
||||
:return: fields (RequestParameters), files (RequestParameters)
|
||||
"""
|
||||
files = RequestParameters()
|
||||
@@ -172,15 +257,15 @@ def parse_multipart_form(body, boundary):
|
||||
break
|
||||
|
||||
colon_index = form_line.index(':')
|
||||
form_header_field = form_line[0:colon_index]
|
||||
form_header_field = form_line[0:colon_index].lower()
|
||||
form_header_value, form_parameters = parse_header(
|
||||
form_line[colon_index + 2:])
|
||||
|
||||
if form_header_field == 'Content-Disposition':
|
||||
if form_header_field == 'content-disposition':
|
||||
if 'filename' in form_parameters:
|
||||
file_name = form_parameters['filename']
|
||||
field_name = form_parameters.get('name')
|
||||
elif form_header_field == 'Content-Type':
|
||||
elif form_header_field == 'content-type':
|
||||
file_type = form_header_value
|
||||
|
||||
post_data = form_part[line_index:-4]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from aiofiles import open as open_async
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
|
||||
from ujson import dumps as json_dumps
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from .cookies import CookieJar
|
||||
from aiofiles import open as open_async
|
||||
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
@@ -73,67 +77,28 @@ ALL_STATUS_CODES = {
|
||||
}
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
self.body = body.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
self.body = str(body).encode()
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self._cookies = None
|
||||
|
||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout:
|
||||
timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout
|
||||
class BaseHTTPResponse:
|
||||
def _encode_body(self, data):
|
||||
try:
|
||||
# Try to encode it regularly
|
||||
return data.encode()
|
||||
except AttributeError:
|
||||
# Convert it to a str if you can't
|
||||
return str(data).encode()
|
||||
|
||||
def _parse_headers(self):
|
||||
headers = b''
|
||||
if self.headers:
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
|
||||
except AttributeError:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
for name, value in self.headers.items():
|
||||
try:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
name.encode(), value.encode('utf-8')))
|
||||
except AttributeError:
|
||||
headers += (
|
||||
b'%b: %b\r\n' % (
|
||||
str(name).encode(), str(value).encode('utf-8')))
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Content-Type: %b\r\n'
|
||||
b'Content-Length: %d\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
self.content_type.encode(),
|
||||
len(self.body),
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
self.body
|
||||
)
|
||||
return headers
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
@@ -142,32 +107,179 @@ class HTTPResponse:
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None, **kwargs):
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'transport', 'streaming_fn',
|
||||
'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
self.content_type = content_type
|
||||
self.streaming_fn = streaming_fn
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self._cookies = None
|
||||
|
||||
def write(self, data):
|
||||
"""Writes a chunk of data to the streaming response.
|
||||
|
||||
:param data: bytes-ish data to be written.
|
||||
"""
|
||||
if type(data) != bytes:
|
||||
data = self._encode_body(data)
|
||||
|
||||
self.transport.write(
|
||||
b"%x\r\n%b\r\n" % (len(data), data))
|
||||
|
||||
async def stream(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
"""Streams headers, runs the `streaming_fn` callback that writes
|
||||
content to the response body, then finalizes the response body.
|
||||
"""
|
||||
headers = self.get_headers(
|
||||
version, keep_alive=keep_alive,
|
||||
keep_alive_timeout=keep_alive_timeout)
|
||||
self.transport.write(headers)
|
||||
|
||||
await self.streaming_fn(self)
|
||||
self.transport.write(b'0\r\n\r\n')
|
||||
|
||||
def get_headers(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
|
||||
self.headers['Transfer-Encoding'] = 'chunked'
|
||||
self.headers.pop('Content-Length', None)
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
timeout_header,
|
||||
headers
|
||||
)
|
||||
|
||||
|
||||
class HTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
self.content_type = content_type
|
||||
|
||||
if body is not None:
|
||||
self.body = self._encode_body(body)
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self._cookies = None
|
||||
|
||||
def output(
|
||||
self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||
# This is all returned in a kind-of funky way
|
||||
# We tried to make this as fast as possible in pure python
|
||||
timeout_header = b''
|
||||
if keep_alive and keep_alive_timeout is not None:
|
||||
timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout
|
||||
self.headers['Content-Length'] = self.headers.get(
|
||||
'Content-Length', len(self.body))
|
||||
self.headers['Content-Type'] = self.headers.get(
|
||||
'Content-Type', self.content_type)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
version.encode(),
|
||||
self.status,
|
||||
status,
|
||||
b'keep-alive' if keep_alive else b'close',
|
||||
timeout_header,
|
||||
headers,
|
||||
self.body
|
||||
)
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None,
|
||||
content_type="application/json", **kwargs):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
|
||||
:param body: Response data to be serialized.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param \**kwargs: Remaining arguments that are passed to the json encoder.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type="application/json")
|
||||
status=status, content_type=content_type)
|
||||
|
||||
|
||||
def text(body, status=200, headers=None):
|
||||
def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
"""
|
||||
Returns response object with body in text format.
|
||||
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/plain; charset=utf-8")
|
||||
return HTTPResponse(
|
||||
body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
"""
|
||||
Returns response object without encoding the body.
|
||||
|
||||
:param body: Response data.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
|
||||
|
||||
def html(body, status=200, headers=None):
|
||||
"""
|
||||
Returns response object with body in html format.
|
||||
|
||||
:param body: Response data to be encoded.
|
||||
:param status: Response code.
|
||||
:param headers: Custom Headers.
|
||||
@@ -176,17 +288,24 @@ def html(body, status=200, headers=None):
|
||||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None):
|
||||
"""
|
||||
Returns response object with file data.
|
||||
async def file(location, mime_type=None, headers=None, _range=None):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
out_stream = await _file.read()
|
||||
if _range:
|
||||
await _file.seek(_range.start)
|
||||
out_stream = await _file.read(_range.size)
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
else:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
@@ -196,16 +315,90 @@ async def file(location, mime_type=None, headers=None):
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None,
|
||||
_range=None):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param chunk_size: The size of each chunk in the stream (in bytes)
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
|
||||
_file = await open_async(location, mode='rb')
|
||||
|
||||
async def _streaming_fn(response):
|
||||
nonlocal _file, chunk_size
|
||||
try:
|
||||
if _range:
|
||||
chunk_size = min((_range.size, chunk_size))
|
||||
await _file.seek(_range.start)
|
||||
to_send = _range.size
|
||||
while to_send > 0:
|
||||
content = await _file.read(chunk_size)
|
||||
if len(content) < 1:
|
||||
break
|
||||
to_send -= len(content)
|
||||
response.write(content)
|
||||
else:
|
||||
while True:
|
||||
content = await _file.read(chunk_size)
|
||||
if len(content) < 1:
|
||||
break
|
||||
response.write(content)
|
||||
finally:
|
||||
await _file.close()
|
||||
return # Returning from this fn closes the stream
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
if _range:
|
||||
headers['Content-Range'] = 'bytes %s-%s/%s' % (
|
||||
_range.start, _range.end, _range.total)
|
||||
return StreamingHTTPResponse(streaming_fn=_streaming_fn,
|
||||
status=200,
|
||||
headers=headers,
|
||||
content_type=mime_type)
|
||||
|
||||
|
||||
def stream(
|
||||
streaming_fn, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
"""Accepts an coroutine `streaming_fn` which can be used to
|
||||
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
|
||||
|
||||
Example usage::
|
||||
|
||||
@app.route("/")
|
||||
async def index(request):
|
||||
async def streaming_fn(response):
|
||||
await response.write('foo')
|
||||
await response.write('bar')
|
||||
|
||||
return stream(streaming_fn, content_type='text/plain')
|
||||
|
||||
:param streaming_fn: A coroutine accepts a response and
|
||||
writes content to that response.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
"""
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn,
|
||||
headers=headers,
|
||||
content_type=content_type,
|
||||
status=status
|
||||
)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
"""
|
||||
Aborts execution and causes a 302 redirect (by default).
|
||||
"""Abort execution and cause a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
:param headers: optional dict of headers to include in the new request
|
||||
:param status: status code (int) of the new request, defaults to 302
|
||||
:param content_type:
|
||||
the content type (string) of the response
|
||||
:param content_type: the content type (string) of the response
|
||||
:returns: the redirecting Response
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
155
sanic/router.py
155
sanic/router.py
@@ -1,12 +1,14 @@
|
||||
import re
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
from .exceptions import NotFound, InvalidUsage
|
||||
from .views import CompositionView
|
||||
|
||||
from sanic.exceptions import NotFound, InvalidUsage
|
||||
from sanic.views import CompositionView
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name', 'uri'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
|
||||
REGEX_TYPES = {
|
||||
@@ -14,6 +16,7 @@ REGEX_TYPES = {
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
'path': (str, r'[^/].*?'),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
@@ -32,8 +35,7 @@ class RouteDoesNotExist(Exception):
|
||||
|
||||
|
||||
class Router:
|
||||
"""
|
||||
Router supports basic routing with parameters and method checks
|
||||
"""Router supports basic routing with parameters and method checks
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -68,14 +70,17 @@ class Router:
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
self.routes_always_check = []
|
||||
self.hosts = None
|
||||
self.hosts = set()
|
||||
|
||||
def parse_parameter_string(self, parameter_string):
|
||||
"""
|
||||
Parse a parameter string into its constituent name, type, and pattern
|
||||
For example:
|
||||
`parse_parameter_string('<param_one:[A-z]')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
@classmethod
|
||||
def parse_parameter_string(cls, parameter_string):
|
||||
"""Parse a parameter string into its constituent name, type, and
|
||||
pattern
|
||||
|
||||
For example::
|
||||
|
||||
parse_parameter_string('<param_one:[A-z]>')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
|
||||
:param parameter_string: String to parse
|
||||
:return: tuple containing
|
||||
@@ -93,33 +98,70 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None):
|
||||
"""
|
||||
Adds a handler to the route list
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||
version=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: Path to match
|
||||
:param methods: Array of accepted method names.
|
||||
If none are provided, any method is allowed
|
||||
:param handler: Request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:param uri: path to match
|
||||
:param methods: sequence of accepted method names. If none are
|
||||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:param strict_slashes: strict to trailing slash
|
||||
:param version: current version of the route or blueprint. See
|
||||
docs for further details.
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
if host is not None:
|
||||
# we want to track if there are any
|
||||
# vhosts on the Router instance so that we can
|
||||
# default to the behavior without vhosts
|
||||
if self.hosts is None:
|
||||
self.hosts = set(host)
|
||||
if version is not None:
|
||||
if uri.startswith('/'):
|
||||
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
|
||||
else:
|
||||
if isinstance(host, list):
|
||||
host = set(host)
|
||||
self.hosts.add(host)
|
||||
uri = "/".join(["/v{}".format(str(version)), uri])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/'
|
||||
and not self.routes_all.get(uri + '/', False)
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/'
|
||||
and not self.routes_all.get(uri[:-1], False)
|
||||
and not uri == '/'
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host)
|
||||
|
||||
def _add(self, uri, methods, handler, host=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
:param methods: sequence of accepted method names. If none are
|
||||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:return: Nothing
|
||||
"""
|
||||
if host is not None:
|
||||
if isinstance(host, str):
|
||||
uri = host + uri
|
||||
self.hosts.add(host)
|
||||
|
||||
else:
|
||||
for h in host:
|
||||
self.add(uri, methods, handler, h)
|
||||
if not isinstance(host, Iterable):
|
||||
raise ValueError("Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host))
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
return
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
@@ -138,10 +180,10 @@ class Router:
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search('(^|[^^]){1}/', pattern):
|
||||
if re.search(r'(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(pattern, '/'):
|
||||
elif re.search(r'/', pattern):
|
||||
properties['unhashable'] = True
|
||||
|
||||
return '({})'.format(pattern)
|
||||
@@ -200,7 +242,7 @@ class Router:
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name)
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
@@ -239,8 +281,7 @@ class Router:
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name):
|
||||
"""
|
||||
Find a route in the router based on the specified view name.
|
||||
"""Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:return: tuple containing (uri, Route)
|
||||
@@ -255,26 +296,30 @@ class Router:
|
||||
return (None, None)
|
||||
|
||||
def get(self, request):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
error
|
||||
|
||||
:param request: Request object
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
if self.hosts is None:
|
||||
return self._get(request.url, request.method, '')
|
||||
else:
|
||||
return self._get(request.url, request.method,
|
||||
# No virtual hosts specified; default behavior
|
||||
if not self.hosts:
|
||||
return self._get(request.path, request.method, '')
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
try:
|
||||
return self._get(request.path, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.path, request.method, '')
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
"""
|
||||
Gets a request handler based on the URL of the request, or raises an
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
error. Internal method for caching.
|
||||
:param url: Request URL
|
||||
:param method: Request method
|
||||
|
||||
:param url: request URL
|
||||
:param method: request method
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
url = host + url
|
||||
@@ -316,4 +361,18 @@ class Router:
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs
|
||||
return route_handler, [], kwargs, route.uri
|
||||
|
||||
def is_stream_handler(self, request):
|
||||
""" Handler for request is stream or not.
|
||||
:param request: Request object
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, InvalidUsage):
|
||||
return False
|
||||
if (hasattr(handler, 'view_class') and
|
||||
hasattr(handler.view_class, request.method.lower())):
|
||||
handler = getattr(handler.view_class, request.method.lower())
|
||||
return hasattr(handler, 'is_stream')
|
||||
|
||||
513
sanic/sanic.py
513
sanic/sanic.py
@@ -1,513 +0,0 @@
|
||||
import logging
|
||||
from asyncio import get_event_loop
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from inspect import isawaitable, stack, getmodulename
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from urllib.parse import urlencode, urlunparse
|
||||
import warnings
|
||||
|
||||
from .config import Config
|
||||
from .constants import HTTP_METHODS
|
||||
from .exceptions import Handler
|
||||
from .exceptions import ServerError, URLBuildError
|
||||
from .log import log
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve, serve_multiple, HttpProtocol
|
||||
from .static import register as static_register
|
||||
|
||||
|
||||
class Sanic:
|
||||
def __init__(self, name=None, router=None,
|
||||
error_handler=None):
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not logging.root.handlers and log.level == logging.NOTSET:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or Handler()
|
||||
self.config = Config()
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.processes = None
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
# Fix case where the user did not prefix the URL with a /
|
||||
# and will probably get confused as to why it's not working
|
||||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
def response(handler):
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None):
|
||||
return self.route(uri, methods=["GET"], host=host)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
"""
|
||||
A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
||||
:param handler: function or class instance
|
||||
:param uri: path of the URL
|
||||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
methods = frozenset(HTTP_METHODS)
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
return handler
|
||||
|
||||
def remove_route(self, uri, clean_cache=True, host=None):
|
||||
self.router.remove(uri, clean_cache, host)
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a handler for exceptions
|
||||
|
||||
:param \*exceptions: exceptions
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
def response(handler):
|
||||
for exception in exceptions:
|
||||
self.error_handler.add(exception, handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Decorator
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""
|
||||
Decorates and registers middleware to be called before a request
|
||||
can either be called as @app.middleware or @app.middleware('request')
|
||||
"""
|
||||
attach_to = 'request'
|
||||
|
||||
def register_middleware(middleware):
|
||||
if attach_to == 'request':
|
||||
self.request_middleware.append(middleware)
|
||||
if attach_to == 'response':
|
||||
self.response_middleware.appendleft(middleware)
|
||||
return middleware
|
||||
|
||||
# Detect which way this was called, @middleware or @middleware('AT')
|
||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||
return register_middleware(args[0])
|
||||
else:
|
||||
attach_to = args[0]
|
||||
return register_middleware
|
||||
|
||||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern='.+',
|
||||
use_modified_since=True):
|
||||
"""
|
||||
Registers a root to serve files from. The input can either be a file
|
||||
or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""
|
||||
Registers a blueprint on the application.
|
||||
|
||||
:param blueprint: Blueprint object
|
||||
:param options: option dictionary with blueprint defaults
|
||||
:return: Nothing
|
||||
"""
|
||||
if blueprint.name in self.blueprints:
|
||||
assert self.blueprints[blueprint.name] is blueprint, \
|
||||
'A blueprint with the name "%s" is already registered. ' \
|
||||
'Blueprint names must be unique.' % \
|
||||
(blueprint.name,)
|
||||
else:
|
||||
self.blueprints[blueprint.name] = blueprint
|
||||
self._blueprint_order.append(blueprint)
|
||||
blueprint.register(self, options)
|
||||
|
||||
def register_blueprint(self, *args, **kwargs):
|
||||
# TODO: deprecate 1.0
|
||||
if self.debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Use of register_blueprint will be deprecated in "
|
||||
"version 1.0. Please use the blueprint method"
|
||||
" instead",
|
||||
DeprecationWarning)
|
||||
return self.blueprint(*args, **kwargs)
|
||||
|
||||
def url_for(self, view_name: str, **kwargs):
|
||||
"""Builds a URL based on a view name and the values provided.
|
||||
|
||||
In order to build a URL, all request parameters must be supplied as
|
||||
keyword arguments, and each parameter must pass the test for the
|
||||
specified parameter type. If these conditions are not met, a
|
||||
`URLBuildError` will be thrown.
|
||||
|
||||
Keyword arguments that are not request parameters will be included in
|
||||
the output URL's query string.
|
||||
|
||||
:param view_name: A string referencing the view name
|
||||
:param **kwargs: keys and values that are used to build request
|
||||
parameters and query string arguments.
|
||||
|
||||
:return: the built URL
|
||||
|
||||
Raises:
|
||||
URLBuildError
|
||||
"""
|
||||
# find the route by the supplied view name
|
||||
uri, route = self.router.find_route_by_view_name(view_name)
|
||||
|
||||
if not uri or not route:
|
||||
raise URLBuildError(
|
||||
'Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
|
||||
out = uri
|
||||
|
||||
# find all the parameters we will need to build in the URL
|
||||
matched_params = re.findall(
|
||||
self.router.parameter_pattern, uri)
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(
|
||||
match)
|
||||
# we only want to match against each individual parameter
|
||||
specific_pattern = '^{}$'.format(pattern)
|
||||
supplied_param = None
|
||||
|
||||
if kwargs.get(name):
|
||||
supplied_param = kwargs.get(name)
|
||||
del kwargs[name]
|
||||
else:
|
||||
raise URLBuildError(
|
||||
'Required parameter `{}` was not passed to url_for'.format(
|
||||
name))
|
||||
|
||||
supplied_param = str(supplied_param)
|
||||
# determine if the parameter supplied by the caller passes the test
|
||||
# in the URL
|
||||
passes_pattern = re.match(specific_pattern, supplied_param)
|
||||
|
||||
if not passes_pattern:
|
||||
if _type != str:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` does not '
|
||||
'match pattern for type `{}`: {}'.format(
|
||||
supplied_param, name, _type.__name__, pattern))
|
||||
else:
|
||||
msg = (
|
||||
'Value "{}" for parameter `{}` '
|
||||
'does not satisfy pattern {}'.format(
|
||||
supplied_param, name, pattern))
|
||||
raise URLBuildError(msg)
|
||||
|
||||
# replace the parameter in the URL with the supplied value
|
||||
replacement_regex = '(<{}.*?>)'.format(name)
|
||||
|
||||
out = re.sub(
|
||||
replacement_regex, supplied_param, out)
|
||||
|
||||
# parse the remainder of the keyword arguments into a querystring
|
||||
if kwargs:
|
||||
query_string = urlencode(kwargs)
|
||||
out = urlunparse((
|
||||
'', '', out,
|
||||
'', query_string, ''
|
||||
))
|
||||
|
||||
return out
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Request Handling
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def converted_response_type(self, response):
|
||||
pass
|
||||
|
||||
async def handle_request(self, request, response_callback):
|
||||
"""
|
||||
Takes a request from the HTTP Server and returns a response object to
|
||||
be sent back The HTTP Server only expects a response object, so
|
||||
exception handling must be done here
|
||||
|
||||
:param request: HTTP Request object
|
||||
:param response_callback: Response function to be called with the
|
||||
response as the only argument
|
||||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
response = False
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
for middleware in self.request_middleware:
|
||||
response = middleware(request)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
if response:
|
||||
break
|
||||
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
("'None' was returned while requesting a "
|
||||
"handler from the router"))
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Response Middleware
|
||||
# -------------------------------------------- #
|
||||
|
||||
if self.response_middleware:
|
||||
for middleware in self.response_middleware:
|
||||
_response = middleware(request, response)
|
||||
if isawaitable(_response):
|
||||
_response = await _response
|
||||
if _response:
|
||||
response = _response
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
# -------------------------------------------- #
|
||||
# Response Generation Failed
|
||||
# -------------------------------------------- #
|
||||
|
||||
try:
|
||||
response = self.error_handler.response(request, e)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
response = HTTPResponse(
|
||||
"Error while handling error: {}\nStack: {}".format(
|
||||
e, format_exc()))
|
||||
else:
|
||||
response = HTTPResponse(
|
||||
"An error occurred while handling an error")
|
||||
|
||||
response_callback(response)
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Execution
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, workers=1, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None, register_sys_signals=True):
|
||||
"""
|
||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||
signal. On termination, drains connections before closing.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param debug: Enables debug output (slows server)
|
||||
:param before_start: Functions to be executed before the server starts
|
||||
accepting connections
|
||||
:param after_start: Functions to be executed after the server starts
|
||||
accepting connections
|
||||
:param before_stop: Functions to be executed when a stop signal is
|
||||
received before it is respected
|
||||
:param after_stop: Functions to be executed when all requests are
|
||||
complete
|
||||
:param ssl: SSLContext for SSL encryption of worker(s)
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param workers: Number of processes
|
||||
received before it is respected
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock, workers=workers,
|
||||
loop=loop, protocol=protocol, backlog=backlog,
|
||||
stop_event=stop_event, register_sys_signals=register_sys_signals)
|
||||
try:
|
||||
if workers == 1:
|
||||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers, stop_event)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
|
||||
log.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
get_event_loop().stop()
|
||||
|
||||
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None,
|
||||
before_stop=None, after_stop=None, ssl=None,
|
||||
sock=None, loop=None, protocol=HttpProtocol,
|
||||
backlog=100, stop_event=None):
|
||||
"""
|
||||
Asynchronous version of `run`.
|
||||
"""
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, before_start=before_start,
|
||||
after_start=after_start, before_stop=before_stop,
|
||||
after_stop=after_stop, ssl=ssl, sock=sock, loop=loop,
|
||||
protocol=protocol, backlog=backlog, stop_event=stop_event,
|
||||
run_async=True)
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
def _helper(self, host="127.0.0.1", port=8000, debug=False,
|
||||
before_start=None, after_start=None, before_stop=None,
|
||||
after_stop=None, ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False):
|
||||
"""
|
||||
Helper function used by `run` and `create_server`.
|
||||
"""
|
||||
|
||||
if loop is not None:
|
||||
if debug:
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version"
|
||||
" 0.4.0 https://github.com/channelcat/sanic/"
|
||||
"pull/335 has more information.",
|
||||
DeprecationWarning)
|
||||
|
||||
self.error_handler.debug = debug
|
||||
self.debug = debug
|
||||
self.loop = loop = get_event_loop()
|
||||
|
||||
server_settings = {
|
||||
'protocol': protocol,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'sock': sock,
|
||||
'ssl': ssl,
|
||||
'debug': debug,
|
||||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog
|
||||
}
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Register start/stop events
|
||||
# -------------------------------------------- #
|
||||
|
||||
for event_name, settings_name, args, reverse in (
|
||||
("before_server_start", "before_start", before_start, False),
|
||||
("after_server_start", "after_start", after_start, False),
|
||||
("before_server_stop", "before_stop", before_stop, True),
|
||||
("after_server_stop", "after_stop", after_stop, True),
|
||||
):
|
||||
listeners = []
|
||||
for blueprint in self.blueprints.values():
|
||||
listeners += blueprint.listeners[event_name]
|
||||
if args:
|
||||
if callable(args):
|
||||
args = [args]
|
||||
listeners += args
|
||||
if reverse:
|
||||
listeners.reverse()
|
||||
# Prepend sanic to the arguments when listeners are triggered
|
||||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
|
||||
# Serve
|
||||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
367
sanic/server.py
367
sanic/server.py
@@ -3,27 +3,32 @@ import os
|
||||
import traceback
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from multiprocessing import Process, Event
|
||||
from os import set_inheritable
|
||||
from signal import SIGTERM, SIGINT
|
||||
from signal import signal as signal_func
|
||||
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||
from multiprocessing import Process
|
||||
from signal import (
|
||||
SIGTERM, SIGINT,
|
||||
signal as signal_func,
|
||||
Signals
|
||||
)
|
||||
from socket import (
|
||||
socket,
|
||||
SOL_SOCKET,
|
||||
SO_REUSEADDR,
|
||||
)
|
||||
from time import time
|
||||
import warnings
|
||||
|
||||
from httptools import HttpRequestParser
|
||||
from httptools.parser.errors import HttpParserError
|
||||
|
||||
from .exceptions import ServerError
|
||||
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from .log import log
|
||||
from .request import Request
|
||||
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||
from sanic.log import log, netlog
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
||||
|
||||
current_time = None
|
||||
|
||||
@@ -33,8 +38,7 @@ class Signal:
|
||||
|
||||
|
||||
class CIDict(dict):
|
||||
"""
|
||||
Case Insensitive dict where all keys are converted to lowercase
|
||||
"""Case Insensitive dict where all keys are converted to lowercase
|
||||
This does not maintain the inputted case when calling items() or keys()
|
||||
in favor of speed, since headers are case insensitive
|
||||
"""
|
||||
@@ -60,28 +64,53 @@ class HttpProtocol(asyncio.Protocol):
|
||||
'parser', 'request', 'url', 'headers',
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'request_max_size',
|
||||
'request_class', 'is_request_stream', 'router',
|
||||
# enable or disable access log / error log purpose
|
||||
'has_log',
|
||||
# connection management
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time')
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time',
|
||||
'_is_stream_handler')
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
request_max_size=None):
|
||||
request_max_size=None, request_class=None, has_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
self.parser = None
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self.router = router
|
||||
self.signal = signal
|
||||
self.has_log = has_log
|
||||
self.connections = connections
|
||||
self.request_handler = request_handler
|
||||
self.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
self.request_max_size = request_max_size
|
||||
self.request_class = request_class or Request
|
||||
self.is_request_stream = is_request_stream
|
||||
self._is_stream_handler = False
|
||||
self._total_request_size = 0
|
||||
self._timeout_handler = None
|
||||
self._last_request_time = None
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._keep_alive = keep_alive
|
||||
self._header_fragment = b''
|
||||
self.state = state if state else {}
|
||||
if 'requests_count' not in self.state:
|
||||
self.state['requests_count'] = 0
|
||||
self._debug = debug
|
||||
|
||||
@property
|
||||
def keep_alive(self):
|
||||
return (
|
||||
self._keep_alive and
|
||||
not self.signal.stopped and
|
||||
self.parser.should_keep_alive())
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
@@ -106,10 +135,14 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self._timeout_handler = (
|
||||
self.loop.call_later(time_left, self.connection_timeout))
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
exception = RequestTimeout('Request Timeout')
|
||||
self.write_error(exception)
|
||||
try:
|
||||
raise RequestTimeout('Request Timeout')
|
||||
except RequestTimeout as exception:
|
||||
self.write_error(exception)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
@@ -129,68 +162,164 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.headers = []
|
||||
self.parser = HttpRequestParser(self)
|
||||
|
||||
# requests count
|
||||
self.state['requests_count'] = self.state['requests_count'] + 1
|
||||
|
||||
# Parse request chunk or close connection
|
||||
try:
|
||||
self.parser.feed_data(data)
|
||||
except HttpParserError:
|
||||
exception = InvalidUsage('Bad Request')
|
||||
message = 'Bad Request'
|
||||
if self._debug:
|
||||
message += '\n' + traceback.format_exc()
|
||||
exception = InvalidUsage(message)
|
||||
self.write_error(exception)
|
||||
|
||||
def on_url(self, url):
|
||||
self.url = url
|
||||
if not self.url:
|
||||
self.url = url
|
||||
else:
|
||||
self.url += url
|
||||
|
||||
def on_header(self, name, value):
|
||||
if name == b'Content-Length' and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
self._header_fragment += name
|
||||
|
||||
self.headers.append((name.decode().casefold(), value.decode()))
|
||||
if value is not None:
|
||||
if self._header_fragment == b'Content-Length' \
|
||||
and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
self.headers.append(
|
||||
(self._header_fragment.decode().casefold(),
|
||||
value.decode()))
|
||||
|
||||
self._header_fragment = b''
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = Request(
|
||||
self.request = self.request_class(
|
||||
url_bytes=self.url,
|
||||
headers=CIDict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
)
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request)
|
||||
if self._is_stream_handler:
|
||||
self.request.stream = asyncio.Queue()
|
||||
self.execute_request_handler()
|
||||
|
||||
def on_body(self, body):
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(body))
|
||||
return
|
||||
self.request.body.append(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
if self.request.body:
|
||||
self.request.body = b''.join(self.request.body)
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(None))
|
||||
return
|
||||
self.request.body = b''.join(self.request.body)
|
||||
self.execute_request_handler()
|
||||
|
||||
def execute_request_handler(self):
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(self.request, self.write_response))
|
||||
self.request_handler(
|
||||
self.request,
|
||||
self.write_response,
|
||||
self.stream_response))
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
|
||||
def write_response(self, response):
|
||||
"""
|
||||
Writes response content synchronously to the transport.
|
||||
"""
|
||||
try:
|
||||
keep_alive = (
|
||||
self.parser.should_keep_alive() and not self.signal.stopped)
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive, self.request_timeout))
|
||||
self.request.version, keep_alive,
|
||||
self.request_timeout))
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': len(response.body),
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(e))
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
async def stream_response(self, response):
|
||||
"""
|
||||
Streams a response to the client asynchronously. Attaches
|
||||
the transport to the response so the response consumer can
|
||||
write to the response as needed.
|
||||
"""
|
||||
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
response.transport = self.transport
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.request_timeout)
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': -1,
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
repr(e)))
|
||||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
else:
|
||||
# Record that we received data
|
||||
self._last_request_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
response = None
|
||||
try:
|
||||
response = self.error_handler.response(self.request, exception)
|
||||
version = self.request.version if self.request else '1.1'
|
||||
@@ -198,16 +327,34 @@ class HttpProtocol(asyncio.Protocol):
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip))
|
||||
self.request.ip if self.request else 'Unknown'))
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(e),
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True)
|
||||
finally:
|
||||
if self.has_log:
|
||||
extra = dict()
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['status'] = response.status
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['status'] = 0
|
||||
extra['byte'] = -1
|
||||
if self.request:
|
||||
extra['host'] = '%s:%d' % self.request.ip,
|
||||
extra['request'] = '%s %s' % (self.request.method,
|
||||
self.url)
|
||||
else:
|
||||
extra['host'] = 'UNKNOWN'
|
||||
extra['request'] = 'nil'
|
||||
if self.parser and not (self.keep_alive
|
||||
and extra['status'] == 408):
|
||||
netlog.info('', extra=extra)
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error and self.transport.is_closing():
|
||||
if from_error or self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
@@ -225,11 +372,13 @@ class HttpProtocol(asyncio.Protocol):
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._total_request_size = 0
|
||||
self._is_stream_handler = False
|
||||
|
||||
def close_if_idle(self):
|
||||
"""
|
||||
Close the connection if a request is not being sent or received
|
||||
"""Close the connection if a request is not being sent or received
|
||||
|
||||
:return: boolean - True if closed, false if staying open
|
||||
"""
|
||||
if not self.parser:
|
||||
@@ -237,11 +386,18 @@ class HttpProtocol(asyncio.Protocol):
|
||||
return True
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Force close the connection.
|
||||
"""
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
|
||||
def update_current_time(loop):
|
||||
"""
|
||||
Caches the current time, since it is needed
|
||||
at the end of every keep-alive request to update the request timeout time
|
||||
"""Cache the current time, since it is needed at the end of every
|
||||
keep-alive request to update the request timeout time
|
||||
|
||||
:param loop:
|
||||
:return:
|
||||
@@ -252,49 +408,54 @@ def update_current_time(loop):
|
||||
|
||||
|
||||
def trigger_events(events, loop):
|
||||
"""
|
||||
"""Trigger event callbacks (functions or async)
|
||||
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
if events:
|
||||
if not isinstance(events, list):
|
||||
events = [events]
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
|
||||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False):
|
||||
"""
|
||||
Starts asynchronous HTTP Server on an individual process.
|
||||
register_sys_signals=True, run_async=False, connections=None,
|
||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
||||
is_request_stream=False, router=None, websocket_max_size=None,
|
||||
websocket_max_queue=None, state=None,
|
||||
graceful_shutdown_timeout=15.0):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
:param host: Address to host on
|
||||
:param port: Port to host on
|
||||
:param request_handler: Sanic request handler with middleware
|
||||
:param error_handler: Sanic error handler with middleware
|
||||
:param before_start: Function to be executed before the server starts
|
||||
:param before_start: function to be executed before the server starts
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param after_start: Function to be executed after the server starts
|
||||
:param after_start: function to be executed after the server starts
|
||||
listening. Takes arguments `app` instance and `loop`
|
||||
:param before_stop: Function to be executed when a stop signal is
|
||||
:param before_stop: function to be executed when a stop signal is
|
||||
received before it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param after_stop: Function to be executed when a stop signal is
|
||||
:param after_stop: function to be executed when a stop signal is
|
||||
received after it is respected. Takes arguments
|
||||
`app` instance and `loop`
|
||||
:param debug: Enables debug output (slows server)
|
||||
`app` instance and `loop`
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
:param reuse_port: `True` for multiple workers
|
||||
:param loop: asyncio compatible event loop
|
||||
:param protocol: Subclass of asyncio protocol class
|
||||
:param protocol: subclass of asyncio protocol class
|
||||
:param request_class: Request class to use
|
||||
:param has_log: disable/enable access log and error log
|
||||
:param is_request_stream: disable/enable Request.stream
|
||||
:param router: Router object
|
||||
:return: Nothing
|
||||
"""
|
||||
if not run_async:
|
||||
@@ -304,10 +465,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
if debug:
|
||||
loop.set_debug(debug)
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
connections = set()
|
||||
signal = Signal()
|
||||
connections = connections if connections is not None else set()
|
||||
server = partial(
|
||||
protocol,
|
||||
loop=loop,
|
||||
@@ -317,6 +475,15 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
request_max_size=request_max_size,
|
||||
request_class=request_class,
|
||||
has_log=has_log,
|
||||
keep_alive=keep_alive,
|
||||
is_request_stream=is_request_stream,
|
||||
router=router,
|
||||
websocket_max_size=websocket_max_size,
|
||||
websocket_max_queue=websocket_max_queue,
|
||||
state=state,
|
||||
debug=debug,
|
||||
)
|
||||
|
||||
server_coroutine = loop.create_server(
|
||||
@@ -328,6 +495,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
sock=sock,
|
||||
backlog=backlog
|
||||
)
|
||||
|
||||
# Instead of pulling time at the end of every request,
|
||||
# pull it once per minute
|
||||
loop.call_soon(partial(update_current_time, loop))
|
||||
@@ -335,6 +503,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
if run_async:
|
||||
return server_coroutine
|
||||
|
||||
trigger_events(before_start, loop)
|
||||
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except:
|
||||
@@ -346,8 +516,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
# Register signals for graceful termination
|
||||
if register_sys_signals:
|
||||
for _signal in (SIGINT, SIGTERM):
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
|
||||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
||||
' not implemented on this platform.')
|
||||
pid = os.getpid()
|
||||
try:
|
||||
log.info('Starting worker [{}]'.format(pid))
|
||||
@@ -367,45 +540,61 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
for connection in connections:
|
||||
connection.close_if_idle()
|
||||
|
||||
while connections:
|
||||
# Gracefully shutdown timeout.
|
||||
# We should provide graceful_shutdown_timeout,
|
||||
# instead of letting connection hangs forever.
|
||||
# Let's roughly calcucate time.
|
||||
start_shutdown = 0
|
||||
while connections and (start_shutdown < graceful_shutdown_timeout):
|
||||
loop.run_until_complete(asyncio.sleep(0.1))
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
# Force close non-idle connection after waiting for
|
||||
# graceful_shutdown_timeout
|
||||
coros = []
|
||||
for conn in connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(conn.websocket.close_connection(force=True))
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
_shutdown = asyncio.gather(*coros, loop=loop)
|
||||
loop.run_until_complete(_shutdown)
|
||||
|
||||
trigger_events(after_stop, loop)
|
||||
|
||||
loop.close()
|
||||
|
||||
|
||||
def serve_multiple(server_settings, workers, stop_event=None):
|
||||
"""
|
||||
Starts multiple server processes simultaneously. Stops on interrupt
|
||||
and terminate signals, and drains connections when complete.
|
||||
def serve_multiple(server_settings, workers):
|
||||
"""Start multiple server processes simultaneously. Stop on interrupt
|
||||
and terminate signals, and drain connections when complete.
|
||||
|
||||
:param server_settings: kw arguments to be passed to the serve function
|
||||
:param workers: number of workers to launch
|
||||
:param stop_event: if provided, is used as a stop signal
|
||||
:return:
|
||||
"""
|
||||
if server_settings.get('loop', None) is not None:
|
||||
if server_settings.get('debug', False):
|
||||
warnings.simplefilter('default')
|
||||
warnings.warn("Passing a loop will be deprecated in version 0.4.0"
|
||||
" https://github.com/channelcat/sanic/pull/335"
|
||||
" has more information.", DeprecationWarning)
|
||||
server_settings['reuse_port'] = True
|
||||
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
set_inheritable(sock.fileno(), True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
# Handling when custom socket is not provided.
|
||||
if server_settings.get('sock') is None:
|
||||
sock = socket()
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.bind((server_settings['host'], server_settings['port']))
|
||||
sock.set_inheritable(True)
|
||||
server_settings['sock'] = sock
|
||||
server_settings['host'] = None
|
||||
server_settings['port'] = None
|
||||
|
||||
if stop_event is None:
|
||||
stop_event = Event()
|
||||
def sig_handler(signal, frame):
|
||||
log.info("Received signal {}. Shutting down.".format(
|
||||
Signals(signal).name))
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGINT)
|
||||
|
||||
signal_func(SIGINT, lambda s, f: stop_event.set())
|
||||
signal_func(SIGTERM, lambda s, f: stop_event.set())
|
||||
signal_func(SIGINT, lambda s, f: sig_handler(s, f))
|
||||
signal_func(SIGTERM, lambda s, f: sig_handler(s, f))
|
||||
|
||||
processes = []
|
||||
for _ in range(workers):
|
||||
@@ -420,6 +609,4 @@ def serve_multiple(server_settings, workers, stop_event=None):
|
||||
# the above processes will block this until they're stopped
|
||||
for process in processes:
|
||||
process.terminate()
|
||||
sock.close()
|
||||
|
||||
asyncio.get_event_loop().stop()
|
||||
server_settings.get('sock').close()
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
from aiofiles.os import stat
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from re import sub
|
||||
from time import strftime, gmtime
|
||||
from urllib.parse import unquote
|
||||
|
||||
from .exceptions import FileNotFound, InvalidUsage
|
||||
from .response import file, HTTPResponse
|
||||
from aiofiles.os import stat
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
FileNotFound,
|
||||
HeaderNotFound,
|
||||
InvalidUsage,
|
||||
)
|
||||
from sanic.handlers import ContentRangeHandler
|
||||
from sanic.response import file, file_stream, HTTPResponse
|
||||
|
||||
|
||||
def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
# TODO: Though sanic is not a file server, I feel like we should atleast
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
"""
|
||||
Registers a static directory handler with Sanic by adding a route to the
|
||||
Register a static directory handler with Sanic by adding a route to the
|
||||
router and registering a handler.
|
||||
|
||||
:param app: Sanic
|
||||
@@ -23,8 +33,13 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
:param use_modified_since: If true, send file modified time, and return
|
||||
not modified if the browser's matches the
|
||||
server's
|
||||
:param use_content_range: If true, process header for range requests
|
||||
and sends the file part that is requested
|
||||
:param stream_large_files: If true, use the file_stream() handler rather
|
||||
than the file() handler to send the file
|
||||
If this is an integer, this represents the
|
||||
threshold size to switch to file_stream()
|
||||
"""
|
||||
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
@@ -38,30 +53,68 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
# from herping a derp and treating the uri as an absolute path
|
||||
file_path = file_or_directory
|
||||
root_path = file_path = file_or_directory
|
||||
if file_uri:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub('^[/]*', '', file_uri))
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
# match filenames which got encoded (filenames with spaces etc)
|
||||
file_path = unquote(file_path)
|
||||
file_path = path.abspath(unquote(file_path))
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
try:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
# and it has not been modified since
|
||||
stats = None
|
||||
if use_modified_since:
|
||||
stats = await stat(file_path)
|
||||
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
|
||||
gmtime(stats.st_mtime))
|
||||
modified_since = strftime(
|
||||
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
|
||||
if request.headers.get('If-Modified-Since') == modified_since:
|
||||
return HTTPResponse(status=304)
|
||||
headers['Last-Modified'] = modified_since
|
||||
_range = None
|
||||
if use_content_range:
|
||||
_range = None
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
headers['Accept-Ranges'] = 'bytes'
|
||||
headers['Content-Length'] = str(stats.st_size)
|
||||
if request.method != 'HEAD':
|
||||
try:
|
||||
_range = ContentRangeHandler(request, stats)
|
||||
except HeaderNotFound:
|
||||
pass
|
||||
else:
|
||||
del headers['Content-Length']
|
||||
for key, value in _range.headers.items():
|
||||
headers[key] = value
|
||||
if request.method == 'HEAD':
|
||||
return HTTPResponse(
|
||||
headers=headers,
|
||||
content_type=guess_type(file_path)[0] or 'text/plain')
|
||||
else:
|
||||
if stream_large_files:
|
||||
if isinstance(stream_large_files, int):
|
||||
threshold = stream_large_files
|
||||
else:
|
||||
threshold = 1024*1000
|
||||
|
||||
return await file(file_path, headers=headers)
|
||||
except:
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
if stats.st_size >= threshold:
|
||||
return await file_stream(file_path, headers=headers,
|
||||
_range=_range)
|
||||
return await file(file_path, headers=headers, _range=_range)
|
||||
except ContentRangeError:
|
||||
raise
|
||||
except Exception:
|
||||
raise FileNotFound('File not found',
|
||||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
app.route(uri, methods=['GET'])(_handler)
|
||||
app.route(uri, methods=['GET', 'HEAD'])(_handler)
|
||||
|
||||
109
sanic/testing.py
Normal file
109
sanic/testing.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import traceback
|
||||
from json import JSONDecodeError
|
||||
|
||||
from sanic.log import log
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
class SanicTestClient:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
|
||||
log.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError as e:
|
||||
response.text = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
log.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('get', *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('post', *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('put', *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('delete', *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('patch', *args, **kwargs)
|
||||
|
||||
def options(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('options', *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._sanic_endpoint_test('head', *args, **kwargs)
|
||||
@@ -1,59 +0,0 @@
|
||||
import aiohttp
|
||||
from sanic.log import log
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||
log.info(url)
|
||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||
async with getattr(
|
||||
session, method.lower())(url, *args, **kwargs) as response:
|
||||
response.text = await response.text()
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
|
||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
async def _collect_response(sanic, loop):
|
||||
try:
|
||||
response = await local_request(method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
exceptions.append(e)
|
||||
app.stop()
|
||||
|
||||
app.run(host=HOST, debug=debug, port=PORT,
|
||||
after_start=_collect_response, **server_kwargs)
|
||||
|
||||
if exceptions:
|
||||
raise ValueError("Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
@@ -1,8 +1,9 @@
|
||||
from .exceptions import InvalidUsage
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
class HTTPMethodView:
|
||||
""" Simple class based implementation of view for the sanic.
|
||||
"""Simple class based implementation of view for the sanic.
|
||||
You should implement methods (get, post, put, patch, delete) for the class
|
||||
to every HTTP method you want to support.
|
||||
|
||||
@@ -40,17 +41,12 @@ class HTTPMethodView:
|
||||
|
||||
def dispatch_request(self, request, *args, **kwargs):
|
||||
handler = getattr(self, request.method.lower(), None)
|
||||
if handler:
|
||||
return handler(request, *args, **kwargs)
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
request.method, request.url), status_code=405)
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, *class_args, **class_kwargs):
|
||||
""" Converts the class into an actual view function that can be used
|
||||
with the routing system.
|
||||
|
||||
"""Return view function for use with the routing system, that
|
||||
dispatches request to appropriate handler method.
|
||||
"""
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
@@ -68,8 +64,13 @@ class HTTPMethodView:
|
||||
return view
|
||||
|
||||
|
||||
def stream(func):
|
||||
func.is_stream = True
|
||||
return func
|
||||
|
||||
|
||||
class CompositionView:
|
||||
""" Simple method-function mapped view for the sanic.
|
||||
"""Simple method-function mapped view for the sanic.
|
||||
You can add handler functions to methods (get, post, put, patch, delete)
|
||||
for every HTTP method you want to support.
|
||||
|
||||
@@ -87,17 +88,19 @@ class CompositionView:
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
|
||||
def add(self, methods, handler):
|
||||
def add(self, methods, handler, stream=False):
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
for method in methods:
|
||||
if method not in HTTP_METHODS:
|
||||
raise InvalidUsage(
|
||||
'{} is not a valid HTTP method.'.format(method))
|
||||
|
||||
if method in self.handlers:
|
||||
raise KeyError(
|
||||
'Method {} already is registered.'.format(method))
|
||||
raise InvalidUsage(
|
||||
'Method {} is already registered.'.format(method))
|
||||
self.handlers[method] = handler
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
handler = self.handlers.get(request.method.upper(), None)
|
||||
if handler is None:
|
||||
raise InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
request.method, request.url), status_code=405)
|
||||
handler = self.handlers[request.method.upper()]
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
73
sanic/websocket.py
Normal file
73
sanic/websocket.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.server import HttpProtocol
|
||||
from httptools import HttpParserUpgrade
|
||||
from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake
|
||||
from websockets import ConnectionClosed # noqa
|
||||
|
||||
|
||||
class WebSocketProtocol(HttpProtocol):
|
||||
def __init__(self, *args, websocket_max_size=None,
|
||||
websocket_max_queue=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.websocket = None
|
||||
self.websocket_max_size = websocket_max_size
|
||||
self.websocket_max_queue = websocket_max_queue
|
||||
|
||||
def connection_timeout(self):
|
||||
# timeouts make no sense for websocket routes
|
||||
if self.websocket is None:
|
||||
super().connection_timeout()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if self.websocket is not None:
|
||||
self.websocket.connection_lost(exc)
|
||||
super().connection_lost(exc)
|
||||
|
||||
def data_received(self, data):
|
||||
if self.websocket is not None:
|
||||
# pass the data to the websocket protocol
|
||||
self.websocket.data_received(data)
|
||||
else:
|
||||
try:
|
||||
super().data_received(data)
|
||||
except HttpParserUpgrade:
|
||||
# this is okay, it just indicates we've got an upgrade request
|
||||
pass
|
||||
|
||||
def write_response(self, response):
|
||||
if self.websocket is not None:
|
||||
# websocket requests do not write a response
|
||||
self.transport.close()
|
||||
else:
|
||||
super().write_response(response)
|
||||
|
||||
async def websocket_handshake(self, request):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
def get_header(k):
|
||||
return request.headers.get(k, '')
|
||||
|
||||
def set_header(k, v):
|
||||
headers.append((k, v))
|
||||
|
||||
try:
|
||||
key = handshake.check_request(get_header)
|
||||
handshake.build_response(set_header, key)
|
||||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
|
||||
rv += b'\r\n'
|
||||
request.transport.write(rv)
|
||||
|
||||
# hook up the websocket protocol
|
||||
self.websocket = WebSocketCommonProtocol(
|
||||
max_size=self.websocket_max_size,
|
||||
max_queue=self.websocket_max_queue
|
||||
)
|
||||
self.websocket.connection_made(request.transport)
|
||||
return self.websocket
|
||||
205
sanic/worker.py
Normal file
205
sanic/worker.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
import gunicorn.workers.base as base
|
||||
|
||||
from sanic.server import trigger_events, serve, HttpProtocol, Signal
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
|
||||
class GunicornWorker(base.Worker):
|
||||
|
||||
def __init__(self, *args, **kw): # pragma: no cover
|
||||
super().__init__(*args, **kw)
|
||||
cfg = self.cfg
|
||||
if cfg.is_ssl:
|
||||
self.ssl_context = self._create_ssl_context(cfg)
|
||||
else:
|
||||
self.ssl_context = None
|
||||
self.servers = {}
|
||||
self.connections = set()
|
||||
self.exit_code = 0
|
||||
self.signal = Signal()
|
||||
|
||||
def init_process(self):
|
||||
# create new event_loop after fork
|
||||
asyncio.get_event_loop().close()
|
||||
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
super().init_process()
|
||||
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
|
||||
else HttpProtocol)
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
protocol=protocol,
|
||||
ssl=self.ssl_context,
|
||||
run_async=True)
|
||||
self._server_settings['signal'] = self.signal
|
||||
self._server_settings.pop('sock')
|
||||
trigger_events(self._server_settings.get('before_start', []),
|
||||
self.loop)
|
||||
self._server_settings['before_start'] = ()
|
||||
|
||||
self._runner = asyncio.ensure_future(self._run(), loop=self.loop)
|
||||
try:
|
||||
self.loop.run_until_complete(self._runner)
|
||||
self.app.callable.is_running = True
|
||||
trigger_events(self._server_settings.get('after_start', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self._check_alive())
|
||||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
||||
sys.exit(self.exit_code)
|
||||
|
||||
async def close(self):
|
||||
if self.servers:
|
||||
# stop accepting connections
|
||||
self.log.info("Stopping server: %s, connections: %s",
|
||||
self.pid, len(self.connections))
|
||||
for server in self.servers:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
self.servers.clear()
|
||||
|
||||
# prepare connections for closing
|
||||
self.signal.stopped = True
|
||||
for conn in self.connections:
|
||||
conn.close_if_idle()
|
||||
|
||||
# gracefully shutdown timeout
|
||||
start_shutdown = 0
|
||||
graceful_shutdown_timeout = self.cfg.graceful_timeout
|
||||
while self.connections and \
|
||||
(start_shutdown < graceful_shutdown_timeout):
|
||||
await asyncio.sleep(0.1)
|
||||
start_shutdown = start_shutdown + 0.1
|
||||
|
||||
# Force close non-idle connection after waiting for
|
||||
# graceful_shutdown_timeout
|
||||
coros = []
|
||||
for conn in self.connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(conn.websocket.close_connection(force=True))
|
||||
else:
|
||||
conn.close()
|
||||
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||
await _shutdown
|
||||
|
||||
async def _run(self):
|
||||
for sock in self.sockets:
|
||||
state = dict(requests_count=0)
|
||||
self._server_settings["host"] = None
|
||||
self._server_settings["port"] = None
|
||||
server = await serve(
|
||||
sock=sock,
|
||||
connections=self.connections,
|
||||
state=state,
|
||||
**self._server_settings
|
||||
)
|
||||
self.servers[server] = state
|
||||
|
||||
async def _check_alive(self):
|
||||
# If our parent changed then we shut down.
|
||||
pid = os.getpid()
|
||||
try:
|
||||
while self.alive:
|
||||
self.notify()
|
||||
|
||||
req_count = sum(
|
||||
self.servers[srv]["requests_count"] for srv in self.servers
|
||||
)
|
||||
if self.max_requests and req_count > self.max_requests:
|
||||
self.alive = False
|
||||
self.log.info(
|
||||
"Max requests exceeded, shutting down: %s", self
|
||||
)
|
||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
else:
|
||||
await asyncio.sleep(1.0, loop=self.loop)
|
||||
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _create_ssl_context(cfg):
|
||||
""" Creates SSLContext instance for usage in asyncio.create_server.
|
||||
See ssl.SSLSocket.__init__ for more details.
|
||||
"""
|
||||
ctx = ssl.SSLContext(cfg.ssl_version)
|
||||
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
|
||||
ctx.verify_mode = cfg.cert_reqs
|
||||
if cfg.ca_certs:
|
||||
ctx.load_verify_locations(cfg.ca_certs)
|
||||
if cfg.ciphers:
|
||||
ctx.set_ciphers(cfg.ciphers)
|
||||
return ctx
|
||||
|
||||
def init_signals(self):
|
||||
# Set up signals through the event loop API.
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit,
|
||||
signal.SIGQUIT, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit,
|
||||
signal.SIGTERM, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGINT, self.handle_quit,
|
||||
signal.SIGINT, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch,
|
||||
signal.SIGWINCH, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1,
|
||||
signal.SIGUSR1, None)
|
||||
|
||||
self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort,
|
||||
signal.SIGABRT, None)
|
||||
|
||||
# Don't let SIGTERM and SIGUSR1 disturb active requests
|
||||
# by interrupting system calls
|
||||
signal.siginterrupt(signal.SIGTERM, False)
|
||||
signal.siginterrupt(signal.SIGUSR1, False)
|
||||
|
||||
def handle_quit(self, sig, frame):
|
||||
self.alive = False
|
||||
self.app.callable.is_running = False
|
||||
self.cfg.worker_int(self)
|
||||
|
||||
def handle_abort(self, sig, frame):
|
||||
self.alive = False
|
||||
self.exit_code = 1
|
||||
self.cfg.worker_abort(self)
|
||||
sys.exit(1)
|
||||
79
setup.py
79
setup.py
@@ -4,38 +4,77 @@ Sanic
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
from distutils.errors import DistutilsPlatformError
|
||||
from distutils.util import strtobool
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
|
||||
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
|
||||
def open_local(paths, mode='r', encoding='utf8'):
|
||||
path = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
*paths
|
||||
)
|
||||
|
||||
return codecs.open(path, mode, encoding)
|
||||
|
||||
|
||||
with open_local(['sanic', '__init__.py'], encoding='latin1') as fp:
|
||||
try:
|
||||
version = re.findall(r"^__version__ = '([^']+)'\r?$",
|
||||
fp.read(), re.M)[0]
|
||||
except IndexError:
|
||||
raise RuntimeError('Unable to determine version.')
|
||||
|
||||
setup(
|
||||
name='sanic',
|
||||
version=version,
|
||||
url='http://github.com/channelcat/sanic/',
|
||||
license='MIT',
|
||||
author='Channel Cat',
|
||||
author_email='channelcat@gmail.com',
|
||||
description='A microframework based on uvloop, httptools, and learnings of flask',
|
||||
packages=['sanic'],
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'uvloop>=0.5.3',
|
||||
'httptools>=0.0.9',
|
||||
'ujson>=1.35',
|
||||
'aiofiles>=0.3.0',
|
||||
],
|
||||
classifiers=[
|
||||
|
||||
with open_local(['README.rst']) as rm:
|
||||
long_description = rm.read()
|
||||
|
||||
setup_kwargs = {
|
||||
'name': 'sanic',
|
||||
'version': version,
|
||||
'url': 'http://github.com/channelcat/sanic/',
|
||||
'license': 'MIT',
|
||||
'author': 'Channel Cat',
|
||||
'author_email': 'channelcat@gmail.com',
|
||||
'description': (
|
||||
'A microframework based on uvloop, httptools, and learnings of flask'),
|
||||
'long_description': long_description,
|
||||
'packages': ['sanic'],
|
||||
'platforms': 'any',
|
||||
'classifiers': [
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
ujson = 'ujson>=1.35'
|
||||
uvloop = 'uvloop>=0.5.3'
|
||||
|
||||
requirements = [
|
||||
'httptools>=0.0.9',
|
||||
uvloop,
|
||||
ujson,
|
||||
'aiofiles>=0.3.0',
|
||||
'websockets>=3.2',
|
||||
]
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
print("Installing without uJSON")
|
||||
requirements.remove(ujson)
|
||||
|
||||
if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")):
|
||||
print("Installing without uvLoop")
|
||||
requirements.remove(uvloop)
|
||||
|
||||
try:
|
||||
setup_kwargs['install_requires'] = requirements
|
||||
setup(**setup_kwargs)
|
||||
except DistutilsPlatformError as exception:
|
||||
requirements.remove(ujson)
|
||||
requirements.remove(uvloop)
|
||||
print("Installing without uJSON or uvLoop")
|
||||
setup_kwargs['install_requires'] = requirements
|
||||
setup(**setup_kwargs)
|
||||
|
||||
22
tests/certs/selfsigned.cert
Normal file
22
tests/certs/selfsigned.cert
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
|
||||
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
|
||||
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
|
||||
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
|
||||
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
|
||||
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
|
||||
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
|
||||
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
|
||||
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
|
||||
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
|
||||
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
|
||||
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
|
||||
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
|
||||
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
|
||||
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
|
||||
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
|
||||
-----END CERTIFICATE-----
|
||||
27
tests/certs/selfsigned.key
Normal file
27
tests/certs/selfsigned.key
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
|
||||
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
|
||||
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
|
||||
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
|
||||
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
|
||||
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
|
||||
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
|
||||
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
|
||||
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
|
||||
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
|
||||
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
|
||||
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
|
||||
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
|
||||
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
|
||||
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
|
||||
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
|
||||
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
|
||||
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
|
||||
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
|
||||
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
|
||||
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
|
||||
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
|
||||
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
|
||||
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
|
||||
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1 +1 @@
|
||||
I need to be decoded as a uri
|
||||
I am just a regular static file that needs to have its uri decoded
|
||||
|
||||
BIN
tests/static/python.png
Normal file
BIN
tests/static/python.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -5,6 +5,7 @@ from sanic import Sanic
|
||||
def test_bad_request_response():
|
||||
app = Sanic('test_bad_request_response')
|
||||
lines = []
|
||||
@app.listener('after_server_start')
|
||||
async def _request(sanic, loop):
|
||||
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||
reader, writer = await connect
|
||||
@@ -15,6 +16,6 @@ def test_bad_request_response():
|
||||
break
|
||||
lines.append(line)
|
||||
app.stop()
|
||||
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
|
||||
app.run(host='127.0.0.1', port=42101, debug=False)
|
||||
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||
assert lines[-1] == b'Error: Bad Request'
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import NotFound, ServerError, InvalidUsage
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
method = method.lower()
|
||||
|
||||
func = getattr(bp, method)
|
||||
if callable(func):
|
||||
@func('/{}'.format(method), version=1)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
else:
|
||||
print(func)
|
||||
raise
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
client_method = getattr(app.test_client, method)
|
||||
|
||||
request, response = client_method('/v1/{}'.format(method))
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_bp():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text')
|
||||
@@ -20,10 +46,39 @@ def test_bp():
|
||||
return text('Hello')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
assert app.is_request_stream is False
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
def test_bp_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
assert response.json == None
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
bp = Blueprint('test_text', url_prefix='/test1')
|
||||
@@ -33,7 +88,7 @@ def test_bp_with_url_prefix():
|
||||
return text('Hello')
|
||||
|
||||
app.blueprint(bp)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
request, response = app.test_client.get('/test1/')
|
||||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
@@ -53,10 +108,10 @@ def test_several_bp_with_url_prefix():
|
||||
|
||||
app.blueprint(bp)
|
||||
app.blueprint(bp2)
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||
request, response = app.test_client.get('/test1/')
|
||||
assert response.text == 'Hello'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/test2/')
|
||||
request, response = app.test_client.get('/test2/')
|
||||
assert response.text == 'Hello2'
|
||||
|
||||
def test_bp_with_host():
|
||||
@@ -73,13 +128,15 @@ def test_bp_with_host():
|
||||
|
||||
app.blueprint(bp)
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello'
|
||||
|
||||
headers = {"Host": "sub.example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test1/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test1/',
|
||||
headers=headers)
|
||||
|
||||
assert response.text == 'Hello subdomain!'
|
||||
|
||||
@@ -111,18 +168,21 @@ def test_several_bp_with_host():
|
||||
|
||||
assert bp.host == "example.com"
|
||||
headers = {"Host": "example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello'
|
||||
|
||||
assert bp2.host == "sub.example.com"
|
||||
headers = {"Host": "sub.example.com"}
|
||||
request, response = sanic_endpoint_test(app, uri='/test/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test/',
|
||||
headers=headers)
|
||||
|
||||
assert response.text == 'Hello2'
|
||||
request, response = sanic_endpoint_test(app, uri='/test/other/',
|
||||
headers=headers)
|
||||
request, response = app.test_client.get(
|
||||
'/test/other/',
|
||||
headers=headers)
|
||||
assert response.text == 'Hello3'
|
||||
|
||||
def test_bp_middleware():
|
||||
@@ -139,7 +199,7 @@ def test_bp_middleware():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -166,15 +226,15 @@ def test_bp_exception_handler():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/1')
|
||||
request, response = app.test_client.get('/1')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/2')
|
||||
request, response = app.test_client.get('/2')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/3')
|
||||
request, response = app.test_client.get('/3')
|
||||
assert response.status == 200
|
||||
|
||||
def test_bp_listeners():
|
||||
@@ -209,7 +269,7 @@ def test_bp_listeners():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/')
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
@@ -225,82 +285,105 @@ def test_bp_static():
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 200
|
||||
assert response.body == current_file_contents
|
||||
|
||||
def test_bp_shorthand():
|
||||
app = Sanic('test_shorhand_routes')
|
||||
blueprint = Blueprint('test_shorhand_routes')
|
||||
ev = asyncio.Event()
|
||||
|
||||
@blueprint.get('/get')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.put('/put')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.post('/post')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.head('/head')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.options('/options')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.patch('/patch')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.delete('/delete')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
@blueprint.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
assert request.stream is None
|
||||
ev.set()
|
||||
|
||||
app.blueprint(blueprint)
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='get')
|
||||
assert app.is_request_stream is False
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/get', method='post')
|
||||
request, response = app.test_client.post('/get')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='put')
|
||||
request, response = app.test_client.put('/put')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/put', method='get')
|
||||
request, response = app.test_client.get('/post')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='post')
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/post', method='get')
|
||||
request, response = app.test_client.get('/post')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='head')
|
||||
request, response = app.test_client.head('/head')
|
||||
assert response.status == 200
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/head', method='get')
|
||||
request, response = app.test_client.get('/head')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='options')
|
||||
request, response = app.test_client.options('/options')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/options', method='get')
|
||||
request, response = app.test_client.get('/options')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='patch')
|
||||
request, response = app.test_client.patch('/patch')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/patch', method='get')
|
||||
request, response = app.test_client.get('/patch')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='delete')
|
||||
request, response = app.test_client.delete('/delete')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = sanic_endpoint_test(app, uri='/delete', method='get')
|
||||
request, response = app.test_client.get('/delete')
|
||||
assert response.status == 405
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
assert ev.is_set()
|
||||
|
||||
@@ -16,6 +16,17 @@ def test_load_from_object():
|
||||
assert app.config.CONFIG_VALUE == 'should be used'
|
||||
assert 'not_for_config' not in app.config
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == "42"
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) == None
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime, timedelta
|
||||
from http.cookies import SimpleCookie
|
||||
from sanic import Sanic
|
||||
from sanic.response import json, text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -19,7 +18,7 @@ def test_cookies():
|
||||
response.cookies['right_back'] = 'at you'
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app, cookies={"test": "working!"})
|
||||
request, response = app.test_client.get('/', cookies={"test": "working!"})
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -40,7 +39,7 @@ def test_false_cookies(httponly, expected):
|
||||
response.cookies['right_back']['httponly'] = httponly
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -55,7 +54,7 @@ def test_http2_cookies():
|
||||
return response
|
||||
|
||||
headers = {'cookie': 'test=working!'}
|
||||
request, response = sanic_endpoint_test(app, headers=headers)
|
||||
request, response = app.test_client.get('/', headers=headers)
|
||||
|
||||
assert response.text == 'Cookies are: working!'
|
||||
|
||||
@@ -70,7 +69,7 @@ def test_cookie_options():
|
||||
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
@@ -88,7 +87,7 @@ def test_cookie_deletion():
|
||||
del response.cookies['i_never_existed']
|
||||
return response
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
|
||||
30
tests/test_create_task.py
Normal file
30
tests/test_create_task.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from threading import Event
|
||||
import asyncio
|
||||
|
||||
|
||||
def test_create_task():
|
||||
e = Event()
|
||||
|
||||
async def coro():
|
||||
await asyncio.sleep(0.05)
|
||||
e.set()
|
||||
|
||||
app = Sanic('test_create_task')
|
||||
app.add_task(coro)
|
||||
|
||||
@app.route('/early')
|
||||
def not_set(request):
|
||||
return text(e.is_set())
|
||||
|
||||
@app.route('/late')
|
||||
async def set(request):
|
||||
await asyncio.sleep(0.1)
|
||||
return text(e.is_set())
|
||||
|
||||
request, response = app.test_client.get('/early')
|
||||
assert response.body == b'False'
|
||||
|
||||
request, response = app.test_client.get('/late')
|
||||
assert response.body == b'True'
|
||||
@@ -1,7 +1,6 @@
|
||||
from sanic import Sanic
|
||||
from sanic.server import HttpProtocol
|
||||
from sanic.response import text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
app = Sanic('test_custom_porotocol')
|
||||
|
||||
@@ -26,7 +25,7 @@ def test_use_custom_protocol():
|
||||
server_kwargs = {
|
||||
'protocol': CustomHttpProtocol
|
||||
}
|
||||
request, response = sanic_endpoint_test(app, uri='/1',
|
||||
server_kwargs=server_kwargs)
|
||||
request, response = app.test_client.get(
|
||||
'/1', server_kwargs=server_kwargs)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.router import RouteExists
|
||||
import pytest
|
||||
|
||||
@@ -22,8 +21,7 @@ def test_overload_dynamic_routes(method, attr, expected):
|
||||
async def handler2(request, param):
|
||||
return text('OK2 ' + param)
|
||||
|
||||
request, response = sanic_endpoint_test(
|
||||
app, method, uri='/overload/test')
|
||||
request, response = getattr(app.test_client, method)('/overload/test')
|
||||
assert getattr(response, attr) == expected
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from bs4 import BeautifulSoup
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
||||
from sanic.exceptions import Forbidden, abort
|
||||
|
||||
|
||||
class SanicExceptionTestException(Exception):
|
||||
@@ -27,10 +27,37 @@ def exception_app():
|
||||
def handler_404(request):
|
||||
raise NotFound("OK")
|
||||
|
||||
@app.route('/403')
|
||||
def handler_403(request):
|
||||
raise Forbidden("Forbidden")
|
||||
|
||||
@app.route('/401/basic')
|
||||
def handler_401_basic(request):
|
||||
raise Unauthorized("Unauthorized", "Basic", realm="Sanic")
|
||||
|
||||
@app.route('/401/digest')
|
||||
def handler_401_digest(request):
|
||||
raise Unauthorized("Unauthorized",
|
||||
"Digest",
|
||||
realm="Sanic",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
|
||||
@app.route('/401/bearer')
|
||||
def handler_401_bearer(request):
|
||||
raise Unauthorized("Unauthorized", "Bearer")
|
||||
|
||||
@app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
@app.route('/abort')
|
||||
def handler_invalid(request):
|
||||
abort(500)
|
||||
return text("OK")
|
||||
|
||||
@app.route('/divide_by_zero')
|
||||
def handle_unhandled_exception(request):
|
||||
1 / 0
|
||||
@@ -46,35 +73,81 @@ def exception_app():
|
||||
return app
|
||||
|
||||
|
||||
def test_catch_exception_list():
|
||||
app = Sanic('exception_list')
|
||||
|
||||
@app.exception([SanicExceptionTestException, NotFound])
|
||||
def exception_list(request, exception):
|
||||
return text("ok")
|
||||
|
||||
@app.route('/')
|
||||
def exception(request):
|
||||
raise SanicExceptionTestException("You won't see me")
|
||||
|
||||
request, response = app.test_client.get('/random')
|
||||
assert response.text == 'ok'
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
assert response.text == 'ok'
|
||||
|
||||
|
||||
def test_no_exception(exception_app):
|
||||
"""Test that a route works without an exception"""
|
||||
request, response = sanic_endpoint_test(exception_app)
|
||||
request, response = exception_app.test_client.get('/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_server_error_exception(exception_app):
|
||||
"""Test the built-in ServerError exception works"""
|
||||
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
||||
request, response = exception_app.test_client.get('/error')
|
||||
assert response.status == 500
|
||||
|
||||
|
||||
def test_invalid_usage_exception(exception_app):
|
||||
"""Test the built-in InvalidUsage exception works"""
|
||||
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
||||
request, response = exception_app.test_client.get('/invalid')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_not_found_exception(exception_app):
|
||||
"""Test the built-in NotFound exception works"""
|
||||
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
||||
request, response = exception_app.test_client.get('/404')
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
def test_forbidden_exception(exception_app):
|
||||
"""Test the built-in Forbidden exception"""
|
||||
request, response = exception_app.test_client.get('/403')
|
||||
assert response.status == 403
|
||||
|
||||
|
||||
def test_unauthorized_exception(exception_app):
|
||||
"""Test the built-in Unauthorized exception"""
|
||||
request, response = exception_app.test_client.get('/401/basic')
|
||||
assert response.status == 401
|
||||
assert response.headers.get('WWW-Authenticate') is not None
|
||||
assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'"
|
||||
|
||||
request, response = exception_app.test_client.get('/401/digest')
|
||||
assert response.status == 401
|
||||
|
||||
auth_header = response.headers.get('WWW-Authenticate')
|
||||
assert auth_header is not None
|
||||
assert auth_header.startswith('Digest')
|
||||
assert "qop='auth, auth-int'" in auth_header
|
||||
assert "algorithm='MD5'" in auth_header
|
||||
assert "nonce='abcdef'" in auth_header
|
||||
assert "opaque='zyxwvu'" in auth_header
|
||||
|
||||
request, response = exception_app.test_client.get('/401/bearer')
|
||||
assert response.status == 401
|
||||
assert response.headers.get('WWW-Authenticate') == "Bearer"
|
||||
|
||||
|
||||
def test_handled_unhandled_exception(exception_app):
|
||||
"""Test that an exception not built into sanic is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app, uri='/divide_by_zero')
|
||||
request, response = exception_app.test_client.get('/divide_by_zero')
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
assert soup.h1.text == 'Internal Server Error'
|
||||
@@ -84,19 +157,19 @@ def test_handled_unhandled_exception(exception_app):
|
||||
"The server encountered an internal error and "
|
||||
"cannot complete your request.")
|
||||
|
||||
|
||||
def test_exception_in_exception_handler(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app, uri='/error_in_error_handler_handler')
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler')
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=False)
|
||||
assert response.status == 500
|
||||
assert response.body == b'An error occurred while handling an error'
|
||||
@@ -104,9 +177,14 @@ def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_app,
|
||||
uri='/error_in_error_handler_handler',
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
debug=True)
|
||||
assert response.status == 500
|
||||
assert response.body.startswith(b'Exception raised in exception ')
|
||||
|
||||
|
||||
def test_abort(exception_app):
|
||||
"""Test the abort function"""
|
||||
request, response = exception_app.test_client.get('/abort')
|
||||
assert response.status == 500
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.handlers import ErrorHandler
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
exception_handler_app = Sanic('test_exception_handler')
|
||||
@@ -24,7 +24,7 @@ def handler_3(request):
|
||||
|
||||
@exception_handler_app.route('/4')
|
||||
def handler_4(request):
|
||||
foo = bar
|
||||
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
|
||||
return text(foo)
|
||||
|
||||
|
||||
@@ -35,37 +35,45 @@ def handler_5(request):
|
||||
raise CustomServerError('Custom server error')
|
||||
|
||||
|
||||
@exception_handler_app.route('/6/<arg:int>')
|
||||
def handler_6(request, arg):
|
||||
try:
|
||||
foo = 1 / arg
|
||||
except Exception as e:
|
||||
raise e from ValueError("{}".format(arg))
|
||||
return text(foo)
|
||||
|
||||
|
||||
@exception_handler_app.exception(NotFound, ServerError)
|
||||
def handler_exception(request, exception):
|
||||
return text("OK")
|
||||
|
||||
|
||||
def test_invalid_usage_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/1')
|
||||
request, response = exception_handler_app.test_client.get('/1')
|
||||
assert response.status == 400
|
||||
|
||||
|
||||
def test_server_error_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/2')
|
||||
request, response = exception_handler_app.test_client.get('/2')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_not_found_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/3')
|
||||
request, response = exception_handler_app.test_client.get('/3')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_text_exception__handler():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/random')
|
||||
request, response = exception_handler_app.test_client.get('/random')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_html_traceback_output_in_debug_mode():
|
||||
request, response = sanic_endpoint_test(
|
||||
exception_handler_app, uri='/4', debug=True)
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/4', debug=True)
|
||||
assert response.status == 500
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
@@ -77,9 +85,70 @@ def test_html_traceback_output_in_debug_mode():
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"NameError: name 'bar' "
|
||||
"is not defined while handling uri /4") == summary_text
|
||||
"is not defined while handling path /4") == summary_text
|
||||
|
||||
|
||||
def test_inherited_exception_handler():
|
||||
request, response = sanic_endpoint_test(exception_handler_app, uri='/5')
|
||||
request, response = exception_handler_app.test_client.get('/5')
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
def test_chained_exception_handler():
|
||||
request, response = exception_handler_app.test_client.get(
|
||||
'/6/0', debug=True)
|
||||
assert response.status == 500
|
||||
|
||||
soup = BeautifulSoup(response.body, 'html.parser')
|
||||
html = str(soup)
|
||||
|
||||
assert 'response = handler(request, *args, **kwargs)' in html
|
||||
assert 'handler_6' in html
|
||||
assert 'foo = 1 / arg' in html
|
||||
assert 'ValueError' in html
|
||||
assert 'The above exception was the direct cause' in html
|
||||
|
||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||
assert (
|
||||
"ZeroDivisionError: division by zero "
|
||||
"while handling path /6/0") == summary_text
|
||||
|
||||
|
||||
def test_exception_handler_lookup():
|
||||
class CustomError(Exception):
|
||||
pass
|
||||
|
||||
class CustomServerError(ServerError):
|
||||
pass
|
||||
|
||||
def custom_error_handler():
|
||||
pass
|
||||
|
||||
def server_error_handler():
|
||||
pass
|
||||
|
||||
def import_error_handler():
|
||||
pass
|
||||
|
||||
try:
|
||||
ModuleNotFoundError
|
||||
except:
|
||||
class ModuleNotFoundError(ImportError):
|
||||
pass
|
||||
|
||||
handler = ErrorHandler()
|
||||
handler.add(ImportError, import_error_handler)
|
||||
handler.add(CustomError, custom_error_handler)
|
||||
handler.add(ServerError, server_error_handler)
|
||||
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
|
||||
# once again to ensure there is no caching bug
|
||||
assert handler.lookup(ImportError()) == import_error_handler
|
||||
assert handler.lookup(ModuleNotFoundError()) == import_error_handler
|
||||
assert handler.lookup(CustomError()) == custom_error_handler
|
||||
assert handler.lookup(ServerError('Error')) == server_error_handler
|
||||
assert handler.lookup(CustomServerError('Error')) == server_error_handler
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from importlib import reload
|
||||
|
||||
from sanic.config import LOGGING
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from io import StringIO
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
import logging
|
||||
|
||||
logging_format = '''module: %(module)s; \
|
||||
@@ -11,6 +12,11 @@ function: %(funcName)s(); \
|
||||
message: %(message)s'''
|
||||
|
||||
|
||||
def reset_logging():
|
||||
logging.shutdown()
|
||||
reload(logging)
|
||||
|
||||
|
||||
def test_log():
|
||||
log_stream = StringIO()
|
||||
for handler in logging.root.handlers[:]:
|
||||
@@ -29,9 +35,23 @@ def test_log():
|
||||
log.info(rand_string)
|
||||
return text('hello')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
log_text = log_stream.getvalue()
|
||||
assert rand_string in log_text
|
||||
|
||||
|
||||
def test_default_log_fmt():
|
||||
|
||||
reset_logging()
|
||||
Sanic()
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
|
||||
assert fmt._fmt == LOGGING['formatters']['simple']['format']
|
||||
|
||||
reset_logging()
|
||||
Sanic(log_config=None)
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
|
||||
assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_log()
|
||||
|
||||
@@ -2,7 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
|
||||
from sanic import Sanic
|
||||
from sanic.request import Request
|
||||
from sanic.response import json, text, HTTPResponse
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -22,7 +22,7 @@ def test_middleware_request():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is Request
|
||||
@@ -46,7 +46,7 @@ def test_middleware_response():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == 'OK'
|
||||
assert type(results[0]) is Request
|
||||
@@ -54,6 +54,27 @@ def test_middleware_response():
|
||||
assert isinstance(results[2], HTTPResponse)
|
||||
|
||||
|
||||
def test_middleware_response_exception():
|
||||
app = Sanic('test_middleware_response_exception')
|
||||
result = {'status_code': None}
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(reqest, response):
|
||||
result['status_code'] = response.status
|
||||
return response
|
||||
|
||||
@app.exception(NotFound)
|
||||
async def error_handler(request, exception):
|
||||
return text('OK', exception.status_code)
|
||||
|
||||
@app.route('/')
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
request, response = app.test_client.get('/page_not_found')
|
||||
assert response.text == 'OK'
|
||||
assert result['status_code'] == 404
|
||||
|
||||
def test_middleware_override_request():
|
||||
app = Sanic('test_middleware_override_request')
|
||||
|
||||
@@ -65,7 +86,7 @@ def test_middleware_override_request():
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
response = sanic_endpoint_test(app, gather_request=False)
|
||||
response = app.test_client.get('/', gather_request=False)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -82,7 +103,7 @@ def test_middleware_override_response():
|
||||
async def handler(request):
|
||||
return text('FAIL')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
@@ -122,7 +143,7 @@ def test_middleware_order():
|
||||
async def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = sanic_endpoint_test(app)
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.status == 200
|
||||
assert order == [1,2,3,4,5,6]
|
||||
|
||||
@@ -3,7 +3,7 @@ import random
|
||||
import signal
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.utils import HOST, PORT
|
||||
from sanic.testing import HOST, PORT
|
||||
|
||||
|
||||
def test_multiprocessing():
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import PayloadTooLarge
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
data_received_default_app = Sanic('data_received_default')
|
||||
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||
on_header_default_app = Sanic('on_header')
|
||||
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||
|
||||
|
||||
@data_received_app.route('/1')
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@data_received_app.exception(PayloadTooLarge)
|
||||
def handler_exception(request, exception):
|
||||
return text('Payload Too Large from error_handler.', 413)
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
def test_payload_too_large_from_error_handler():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_app, uri='/1', gather_request=False)
|
||||
data_received_app = Sanic('data_received')
|
||||
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@data_received_app.route('/1')
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
@data_received_app.exception(PayloadTooLarge)
|
||||
def handler_exception(request, exception):
|
||||
return text('Payload Too Large from error_handler.', 413)
|
||||
|
||||
response = data_received_app.test_client.get('/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Payload Too Large from error_handler.'
|
||||
|
||||
|
||||
@data_received_default_app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_payload_too_large_at_data_received_default():
|
||||
response = sanic_endpoint_test(
|
||||
data_received_default_app, uri='/1', gather_request=False)
|
||||
data_received_default_app = Sanic('data_received_default')
|
||||
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||
|
||||
@data_received_default_app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
response = data_received_default_app.test_client.get(
|
||||
'/1', gather_request=False)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
|
||||
@on_header_default_app.route('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_payload_too_large_at_on_header_default():
|
||||
on_header_default_app = Sanic('on_header')
|
||||
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||
|
||||
@on_header_default_app.post('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
data = 'a' * 1000
|
||||
response = sanic_endpoint_test(
|
||||
on_header_default_app, method='post', uri='/1',
|
||||
gather_request=False, data=data)
|
||||
response = on_header_default_app.test_client.post(
|
||||
'/1', gather_request=False, data=data)
|
||||
assert response.status == 413
|
||||
assert response.text == 'Error: Payload Too Large'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user