Compare commits
1085 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d758f7c6df | ||
|
|
aa7f2759a6 | ||
|
|
9b9dd67797 | ||
|
|
3f73bc075a | ||
|
|
56989a017b | ||
|
|
ada5918bc8 | ||
|
|
4efd450b32 | ||
|
|
d2670664ba | ||
|
|
fa7405fe9c | ||
|
|
33297f48a5 | ||
|
|
06297a1918 | ||
|
|
aa0874b6d8 | ||
|
|
822ced6294 | ||
|
|
1a59614f79 | ||
|
|
f2d528e52a | ||
|
|
f7adc5f84c | ||
|
|
e955e833c4 | ||
|
|
096c44b910 | ||
|
|
efb9a42045 | ||
|
|
296cda7801 | ||
|
|
90b9d73244 | ||
|
|
c8b0e7f2a7 | ||
|
|
6ce88ab5a4 | ||
|
|
e13ab805df | ||
|
|
e58ea8c7b4 | ||
|
|
dd5bac61cb | ||
|
|
6270b27a97 | ||
|
|
f89ba1d39f | ||
|
|
8b5d137d8f | ||
|
|
2629fab649 | ||
|
|
92cd10c6a8 | ||
|
|
cc3edb90dc | ||
|
|
c60ba81984 | ||
|
|
ece3cdaa2e | ||
|
|
4cb40f2042 | ||
|
|
0e9f350982 | ||
|
|
cf439f01f8 | ||
|
|
f1f1b8a630 | ||
|
|
d4d1df03c9 | ||
|
|
92b73a6f4f | ||
|
|
b63c06c75a | ||
|
|
3e3bce422e | ||
|
|
e3a27c2cc4 | ||
|
|
f13f451084 | ||
|
|
df0e3de911 | ||
|
|
8466be8728 | ||
|
|
5cf2144b3f | ||
|
|
7c182f63c8 | ||
|
|
056180782c | ||
|
|
ff0d5870e9 | ||
|
|
b70176f8c7 | ||
|
|
e3655b525d | ||
|
|
e63d0091af | ||
|
|
7b0af2d80d | ||
|
|
7d79a86d4d | ||
|
|
ba46aff069 | ||
|
|
7a65471ba5 | ||
|
|
c7c46da975 | ||
|
|
c708e8425f | ||
|
|
905c51bef0 | ||
|
|
bd87098b7e | ||
|
|
5f486cc25f | ||
|
|
f79fb72a33 | ||
|
|
0505aa2dda | ||
|
|
485ff32e42 | ||
|
|
5ead67972f | ||
|
|
9c860dbff3 | ||
|
|
a20ad99638 | ||
|
|
8ef7bf8e7b | ||
|
|
0d5be1969a | ||
|
|
d06ea9bfc3 | ||
|
|
57e79882e1 | ||
|
|
20d1ab60c7 | ||
|
|
277c2ce2d2 | ||
|
|
34e51f01d1 | ||
|
|
f4b4e3a58c | ||
|
|
def2e033c8 | ||
|
|
dfec18278b | ||
|
|
cd5bdecda3 | ||
|
|
9b6217ba41 | ||
|
|
272f6e195d | ||
|
|
aa9bf04dfe | ||
|
|
9ae6dfb6d2 | ||
|
|
619bb79a2f | ||
|
|
0cad831eca | ||
|
|
f15a7fb588 | ||
|
|
1bdf9ca057 | ||
|
|
c8c370b784 | ||
|
|
63182f55f7 | ||
|
|
41759248e2 | ||
|
|
3149d5a66d | ||
|
|
8b13597099 | ||
|
|
36032cc26e | ||
|
|
4cb107aedc | ||
|
|
176f8d1981 | ||
|
|
9a26030bd5 | ||
|
|
6778f4d9e0 | ||
|
|
fd61b9e3e2 | ||
|
|
298d5cdf24 | ||
|
|
1bf1c9d006 | ||
|
|
7dc62be5cf | ||
|
|
be580a6a5b | ||
|
|
8ce519668b | ||
|
|
801258c46a | ||
|
|
32a1db3622 | ||
|
|
ed1f3daacc | ||
|
|
b7d74c82ba | ||
|
|
c3b31a6fb0 | ||
|
|
f4c55bbc07 | ||
|
|
a16842f7bc | ||
|
|
439a38664f | ||
|
|
5cc12fd945 | ||
|
|
fe116fff5a | ||
|
|
06aaaf4727 | ||
|
|
6deb9b49b2 | ||
|
|
d59e92d3e5 | ||
|
|
cc83c1f0cf | ||
|
|
1fe7306af8 | ||
|
|
c796d73fc3 | ||
|
|
eb93f884f3 | ||
|
|
3673feb256 | ||
|
|
7c9c783e9d | ||
|
|
74a4b9efaa | ||
|
|
4466e8cce1 | ||
|
|
b689037984 | ||
|
|
db1ba21d88 | ||
|
|
50d270ef7c | ||
|
|
d1a578b555 | ||
|
|
76e9859cf8 | ||
|
|
add9d363c5 | ||
|
|
1498baab0f | ||
|
|
df7f63d45d | ||
|
|
f7425126a1 | ||
|
|
790047e450 | ||
|
|
9198b5b0be | ||
|
|
d534acb79d | ||
|
|
d100f54551 | ||
|
|
7a9e100b0f | ||
|
|
fafe23d7c2 | ||
|
|
9a08bdae4a | ||
|
|
bcc11fa7fe | ||
|
|
7d0c0fdf7c | ||
|
|
0e33d46ead | ||
|
|
efbacc17cf | ||
|
|
bd6dbd9090 | ||
|
|
076cf51fb2 | ||
|
|
f8a6af1e28 | ||
|
|
96912f436d | ||
|
|
f0e162442f | ||
|
|
04b8dd989f | ||
|
|
5851c8bd91 | ||
|
|
78efcf93f8 | ||
|
|
bb35bc3898 | ||
|
|
f38783bdef | ||
|
|
d8f9986089 | ||
|
|
3e616b599a | ||
|
|
d38fc17191 | ||
|
|
7ae0eb0dc3 | ||
|
|
9082eb56a7 | ||
|
|
c578974246 | ||
|
|
fec81ffe73 | ||
|
|
30e6a310f1 | ||
|
|
a87934d434 | ||
|
|
b398c1fe72 | ||
|
|
6f813f940e | ||
|
|
d52498b787 | ||
|
|
79e35bbdf6 | ||
|
|
1814ff05f4 | ||
|
|
ec226e33cb | ||
|
|
6abdf9f9c1 | ||
|
|
212da1029e | ||
|
|
afea15e4a7 | ||
|
|
39ff02b6e4 | ||
|
|
b238be54a4 | ||
|
|
377c9890a3 | ||
|
|
599834b0e1 | ||
|
|
a39a7ca9d5 | ||
|
|
cd22745e6b | ||
|
|
334649dfd4 | ||
|
|
becbc5f9ef | ||
|
|
a7dd73c657 | ||
|
|
f9b29fd7e7 | ||
|
|
f770e16f6d | ||
|
|
9092ee9f0e | ||
|
|
01257f65a6 | ||
|
|
c1222175b3 | ||
|
|
5ff481952d | ||
|
|
baa689ad43 | ||
|
|
2f30f4f69f | ||
|
|
202a4c6525 | ||
|
|
7928b9b3a2 | ||
|
|
e1c9020268 | ||
|
|
04a12b436e | ||
|
|
818a8c2196 | ||
|
|
b6715464fd | ||
|
|
8f2d543d9f | ||
|
|
6cf320bedb | ||
|
|
a850ce5086 | ||
|
|
ef3bdf5408 | ||
|
|
94b9bc7950 | ||
|
|
8a07463a67 | ||
|
|
2995b23929 | ||
|
|
eb4276373b | ||
|
|
79df52e519 | ||
|
|
3dfb31b1b9 | ||
|
|
c4c4ed70d9 | ||
|
|
45422df1b7 | ||
|
|
e0b7624414 | ||
|
|
b0ecb3170f | ||
|
|
fc8b5f378a | ||
|
|
d42cb7ddb3 | ||
|
|
6454ac0944 | ||
|
|
31cf83f10b | ||
|
|
cc84005593 | ||
|
|
915d2732a1 | ||
|
|
44bc47361e | ||
|
|
3619b07843 | ||
|
|
ad3f588c79 | ||
|
|
a2fc37121b | ||
|
|
7f36d20123 | ||
|
|
d1a8e8b042 | ||
|
|
c39ddd00d3 | ||
|
|
d55e453bd5 | ||
|
|
bffed27bdb | ||
|
|
fffcb158f1 | ||
|
|
eca98a54eb | ||
|
|
46ed2c5270 | ||
|
|
23ea0b7ec9 | ||
|
|
ef26cb283b | ||
|
|
b8bb77eff6 | ||
|
|
9c75ad3de1 | ||
|
|
0b38dea613 | ||
|
|
7e4a9e3bc2 | ||
|
|
36f12c822f | ||
|
|
0cbea0f5d3 | ||
|
|
e735fe54c3 | ||
|
|
e911e2e1df | ||
|
|
1d75f6c2be | ||
|
|
ad8a168469 | ||
|
|
74fc502089 | ||
|
|
dfc2166d8b | ||
|
|
2b70346db4 | ||
|
|
090df6f224 | ||
|
|
745a1d6e94 | ||
|
|
0fe0796870 | ||
|
|
224b56bd3a | ||
|
|
571b5b544d | ||
|
|
220b40f7f4 | ||
|
|
60774c5a49 | ||
|
|
6d37ef7256 | ||
|
|
e083224df1 | ||
|
|
5ef567405f | ||
|
|
ea2521f430 | ||
|
|
82cb182fe7 | ||
|
|
3fe31ff551 | ||
|
|
48d45f1ca4 | ||
|
|
ddf2a604d1 | ||
|
|
8b920d9d56 | ||
|
|
f2c0489452 | ||
|
|
f5a2d19199 | ||
|
|
86fed12d91 | ||
|
|
7ca3ad5d4c | ||
|
|
1eecffce97 | ||
|
|
5c341a2b00 | ||
|
|
0ab64e9803 | ||
|
|
27108334f1 | ||
|
|
788253cbe8 | ||
|
|
4b6e89a526 | ||
|
|
68fd1b66b5 | ||
|
|
5806666949 | ||
|
|
a76d8108fe | ||
|
|
2135294e2e | ||
|
|
ed1c563d1f | ||
|
|
74efa3a108 | ||
|
|
49c29e6862 | ||
|
|
17d7f24825 | ||
|
|
f23c3da4ff | ||
|
|
cabcf50fbe | ||
|
|
8b23b5d389 | ||
|
|
37eb2c1db6 | ||
|
|
9751a37343 | ||
|
|
ed3bdd3443 | ||
|
|
285ad9bdc1 | ||
|
|
16f5914c90 | ||
|
|
72d56a89a2 | ||
|
|
1135c8c1b1 | ||
|
|
ec4339bd47 | ||
|
|
6c0fbef843 | ||
|
|
040c85a43b | ||
|
|
420554c737 | ||
|
|
f20b854dd2 | ||
|
|
3844cec7a4 | ||
|
|
0db49f7520 | ||
|
|
f8dedcaa1e | ||
|
|
f8b1122467 | ||
|
|
f3bf5e9a5c | ||
|
|
ba1dbacd35 | ||
|
|
4036f1c121 | ||
|
|
22ad697d1f | ||
|
|
a10d7469cd | ||
|
|
06c3153d22 | ||
|
|
9677158b75 | ||
|
|
226a73141b | ||
|
|
da3201bf35 | ||
|
|
7daebc6aea | ||
|
|
d9002769cf | ||
|
|
6d0b30953a | ||
|
|
09d6452475 | ||
|
|
6a61fce84e | ||
|
|
11017902be | ||
|
|
bd7333723e | ||
|
|
6648250fb9 | ||
|
|
a94a2d46d0 | ||
|
|
ab97018c78 | ||
|
|
be702b0924 | ||
|
|
c5c10cfb50 | ||
|
|
5682d642a6 | ||
|
|
62c6d7274c | ||
|
|
4f8633375d | ||
|
|
9f559818e5 | ||
|
|
5f329f72ee | ||
|
|
7303a06f83 | ||
|
|
e34de96b24 | ||
|
|
42cd424274 | ||
|
|
9426e94314 | ||
|
|
7a1dab3319 | ||
|
|
d63ec84745 | ||
|
|
0e7e2f4e5b | ||
|
|
46521240a9 | ||
|
|
a8827a5d95 | ||
|
|
ca0bc1cb7d | ||
|
|
8c28ce7d79 | ||
|
|
5b051f0891 | ||
|
|
c93de9450a | ||
|
|
96976fa892 | ||
|
|
c14e99cef0 | ||
|
|
c91a806774 | ||
|
|
2f0076f429 | ||
|
|
a1ffc6d55b | ||
|
|
9bdf7a9980 | ||
|
|
81494453b0 | ||
|
|
008cbe5ce7 | ||
|
|
5ee35e7eeb | ||
|
|
1a98e70281 | ||
|
|
8e3f3977bd | ||
|
|
04b04f094c | ||
|
|
9c02cdbad9 | ||
|
|
c30e805623 | ||
|
|
cd81538ce3 | ||
|
|
68cb280513 | ||
|
|
19466a15b4 | ||
|
|
d54b406cba | ||
|
|
72254a7af9 | ||
|
|
52feff266e | ||
|
|
2c3f50e34a | ||
|
|
2b0258c13a | ||
|
|
2585900692 | ||
|
|
48c2dcb110 | ||
|
|
10a378bd46 | ||
|
|
3fe3c2c79f | ||
|
|
52c2a8484e | ||
|
|
21435c1863 | ||
|
|
1ea3ab7fe8 | ||
|
|
1b0ad2c3cd | ||
|
|
aa4821864a | ||
|
|
283762224c | ||
|
|
f50a37fc88 | ||
|
|
076f0515ca | ||
|
|
049f12096d | ||
|
|
f09c0393ba | ||
|
|
472bbcf293 | ||
|
|
7a3f9daccf | ||
|
|
76511d61e0 | ||
|
|
8e7475ccf6 | ||
|
|
820d8c7bf5 | ||
|
|
cfc75b4f1a | ||
|
|
98567fe5a8 | ||
|
|
05bb812e2b | ||
|
|
c9876a6c88 | ||
|
|
979b5a52d3 | ||
|
|
e70535e8d7 | ||
|
|
ed8725bf6c | ||
|
|
098cd70e82 | ||
|
|
969dac2033 | ||
|
|
49b1d667f1 | ||
|
|
bca1e08411 | ||
|
|
bf6ed217c2 | ||
|
|
bb8e9c6438 | ||
|
|
f128ed5b1f | ||
|
|
ff5786d61b | ||
|
|
ca596c8ecd | ||
|
|
c3bcafb514 | ||
|
|
a9c7d95e9b | ||
|
|
01042c1d98 | ||
|
|
5bf722c7ae | ||
|
|
c2191153cf | ||
|
|
5bcbc5a337 | ||
|
|
f721f90add | ||
|
|
0e92d8ce2c | ||
|
|
727d6a1b61 | ||
|
|
666c0847b7 | ||
|
|
0a411f9bba | ||
|
|
49f3ba39f9 | ||
|
|
794128a053 | ||
|
|
e6be3b2313 | ||
|
|
c5cdcf0f95 | ||
|
|
ea5b07f636 | ||
|
|
477e6b8663 | ||
|
|
a0d8418b40 | ||
|
|
006fb08024 | ||
|
|
4578f6016b | ||
|
|
5b06bcc57d | ||
|
|
d4bb14a511 | ||
|
|
6d2f5da506 | ||
|
|
c96df86111 | ||
|
|
86f87cf4ac | ||
|
|
770a8fb288 | ||
|
|
c4e3a98ea7 | ||
|
|
07e95dba4f | ||
|
|
9bc1abcd00 | ||
|
|
4d515b05f3 | ||
|
|
64edf7ad9c | ||
|
|
7610c0fb2e | ||
|
|
0189e4ed59 | ||
|
|
8018c9b91d | ||
|
|
4b3920daba | ||
|
|
d876e3ed5c | ||
|
|
086b5daa53 | ||
|
|
4b877e3f6b | ||
|
|
8ce749e339 | ||
|
|
752ddfa7fc | ||
|
|
8700c96c4d | ||
|
|
e3852ceeca | ||
|
|
225ea49b6f | ||
|
|
15fd49037f | ||
|
|
2fb4697e12 | ||
|
|
1a9f770317 | ||
|
|
62871ec9b3 | ||
|
|
39c64214ee | ||
|
|
9aec5febb8 | ||
|
|
91b2167eba | ||
|
|
00d40a35cd | ||
|
|
f96ab02767 | ||
|
|
4ce699e57f | ||
|
|
4ee042c330 | ||
|
|
0b23f4ff81 | ||
|
|
5cef1634ed | ||
|
|
1b0286916e | ||
|
|
a8f764c161 | ||
|
|
1d719252cb | ||
|
|
d8cebe1188 | ||
|
|
329ebf6a5d | ||
|
|
c836441a75 | ||
|
|
074d36eeba | ||
|
|
f6eb35f67d | ||
|
|
77f70a0792 | ||
|
|
12dafd07b8 | ||
|
|
9fb8bec715 | ||
|
|
eb1146c6b6 | ||
|
|
730f7c5e41 | ||
|
|
5cabc9cff2 | ||
|
|
ddc039ed2e | ||
|
|
a146ebd856 | ||
|
|
5ee7b6caeb | ||
|
|
9c4b0f7b15 | ||
|
|
2e5d1ddff9 | ||
|
|
24bdb1ce98 | ||
|
|
8eb59ad4dc | ||
|
|
d8c8ccd180 | ||
|
|
a46e004f07 | ||
|
|
173f94216a | ||
|
|
1a74accd65 | ||
|
|
2979e03148 | ||
|
|
4bdb9a2c8e | ||
|
|
8f6fa5e9ff | ||
|
|
986135ff76 | ||
|
|
c9cbc00e36 | ||
|
|
c9a40c180a | ||
|
|
125cb17fcb | ||
|
|
53a5bd2319 | ||
|
|
4fd68f9af3 | ||
|
|
c4417b399b | ||
|
|
c2a3e42a53 | ||
|
|
73c04f5a89 | ||
|
|
195f707f14 | ||
|
|
bc20dc5c62 | ||
|
|
8b4ca51805 | ||
|
|
e2e25eb751 | ||
|
|
9572ecc5ea | ||
|
|
97d8b9e908 | ||
|
|
c59a8a60eb | ||
|
|
158da0927a | ||
|
|
78a7338346 | ||
|
|
90e5c8d39b | ||
|
|
7a6f2d8336 | ||
|
|
f49554aa57 | ||
|
|
0a72168f8f | ||
|
|
5011bfef55 | ||
|
|
6038813d03 | ||
|
|
fee9de96de | ||
|
|
35e028cd99 | ||
|
|
145cdd5c1b | ||
|
|
762b2782ee | ||
|
|
91f031b661 | ||
|
|
eab809d410 | ||
|
|
826f1b4713 | ||
|
|
fa1a95ae91 | ||
|
|
63babae63d | ||
|
|
db9924a399 | ||
|
|
5d23c7644b | ||
|
|
ef81a9f547 | ||
|
|
747c21da70 | ||
|
|
439ff11d13 | ||
|
|
947364e15f | ||
|
|
750115b727 | ||
|
|
a55efc832d | ||
|
|
c96bd21389 | ||
|
|
dd241bd6fa | ||
|
|
0dbde7400f | ||
|
|
2587f6753d | ||
|
|
4155e76a81 | ||
|
|
756bd19181 | ||
|
|
fbb2344895 | ||
|
|
bda6c85638 | ||
|
|
df4a149cd0 | ||
|
|
80f27b1db9 | ||
|
|
d5d1d3b45a | ||
|
|
c797c3f22d | ||
|
|
375ed23216 | ||
|
|
7b66a56cad | ||
|
|
7216bf7835 | ||
|
|
8b24c35ac7 | ||
|
|
f80a6ae228 | ||
|
|
9b3fbe4593 | ||
|
|
f99a723627 | ||
|
|
181ffb00a7 | ||
|
|
222eca64d7 | ||
|
|
1b687f3feb | ||
|
|
2228104bff | ||
|
|
402c3752c4 | ||
|
|
69a8bb5e1f | ||
|
|
0d76f9e030 | ||
|
|
eb8f65c58b | ||
|
|
e0e27a671e | ||
|
|
b65eb69d9f | ||
|
|
8118e542fb | ||
|
|
c866759bd4 | ||
|
|
429f7377cb | ||
|
|
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 | ||
|
|
47abf83960 | ||
|
|
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 | ||
|
|
3289e8403a | ||
|
|
104a7c7d05 | ||
|
|
7560660ec7 | ||
|
|
40ccb4a0dd | ||
|
|
f90288f5dc | ||
|
|
3bf79898d9 | ||
|
|
1d6e11ca10 | ||
|
|
6e903ee7d5 | ||
|
|
2dca53a696 | ||
|
|
d8a6d7e02f | ||
|
|
1a8961587c | ||
|
|
fa13ad8849 | ||
|
|
8b23dec322 | ||
|
|
4e8aac4b41 | ||
|
|
e6a828572a | ||
|
|
6ea43d8e6d |
32
.appveyor.yml
Normal file
32
.appveyor.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "{branch}.{build}"
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- TOXENV: py35-no-ext
|
||||
PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py36-no-ext
|
||||
PYTHON: "C:\\Python36-x64"
|
||||
PYTHON_VERSION: "3.6.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
- TOXENV: py37-no-ext
|
||||
PYTHON: "C:\\Python37-x64"
|
||||
PYTHON_VERSION: "3.7.x"
|
||||
PYTHON_ARCH: "64"
|
||||
|
||||
init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
install:
|
||||
- pip install tox
|
||||
|
||||
build: off
|
||||
|
||||
test_script: tox
|
||||
|
||||
notifications:
|
||||
- provider: Email
|
||||
on_build_success: false
|
||||
on_build_status_changed: false
|
||||
@@ -1,7 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = sanic
|
||||
omit = site-packages, sanic/utils.py
|
||||
omit = site-packages, sanic/utils.py, sanic/__main__.py
|
||||
|
||||
[html]
|
||||
directory = coverage
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks.
|
||||
|
||||
|
||||
**Code snippet**
|
||||
Relevant source code, make sure to remove what is not necessary.
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 0.8.3]
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Sanic
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context or sample code about the feature request here.
|
||||
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/help-wanted.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: Help wanted
|
||||
about: Do you need help? Try community.sanicframework.org
|
||||
|
||||
---
|
||||
|
||||
*DELETE ALL BEFORE POSTING*
|
||||
*Post your HELP WANTED questions on [the community forum](https://community.sanicframework.org/)*.
|
||||
|
||||
Checkout the community forum before posting any question here.
|
||||
We prefer if you put these kinds of questions here:
|
||||
|
||||
https://community.sanicframework.org/c/questions-and-help
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ settings.py
|
||||
docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
|
||||
39
.travis.yml
39
.travis.yml
@@ -1,14 +1,41 @@
|
||||
sudo: false
|
||||
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=py37
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_ENV=py37-no-ext
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- env: TOX_ENV=lint
|
||||
python: 3.6
|
||||
- env: TOX_ENV=check
|
||||
python: 3.6
|
||||
install:
|
||||
- pip install -U tox
|
||||
- pip install codecov
|
||||
script: travis_retry tox -e $TOX_ENV
|
||||
after_success:
|
||||
- codecov
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: channelcat
|
||||
password:
|
||||
secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k=
|
||||
secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc=
|
||||
on:
|
||||
tags: true
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@@ -1,3 +1,80 @@
|
||||
Version 0.8
|
||||
-----------
|
||||
0.8.3
|
||||
- Changes:
|
||||
- Ownership changed to org 'huge-success'
|
||||
|
||||
0.8.0
|
||||
- Changes:
|
||||
- Add Server-Sent Events extension (Innokenty Lebedev)
|
||||
- Graceful handling of request_handler_task cancellation (Ashley Sommer)
|
||||
- Sanitize URL before redirection (aveao)
|
||||
- Add url_bytes to request (johndoe46)
|
||||
- py37 support for travisci (yunstanford)
|
||||
- Auto reloader support for OSX (garyo)
|
||||
- Add UUID route support (Volodymyr Maksymiv)
|
||||
- Add pausable response streams (Ashley Sommer)
|
||||
- Add weakref to request slots (vopankov)
|
||||
- remove ubuntu 12.04 from test fixture due to deprecation (yunstanford)
|
||||
- Allow streaming handlers in add_route (kinware)
|
||||
- use travis_retry for tox (Raphael Deem)
|
||||
- update aiohttp version for test client (yunstanford)
|
||||
- add redirect import for clarity (yingshaoxo)
|
||||
- Update HTTP Entity headers (Arnulfo Solís)
|
||||
- Add register_listener method (Stephan Fitzpatrick)
|
||||
- Remove uvloop/ujson dependencies for Windows (abuckenheimer)
|
||||
- Content-length header on 204/304 responses (Arnulfo Solís)
|
||||
- Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
|
||||
- Update development status from pre-alpha to beta (Maksim Anisenkov)
|
||||
- KeepAlive Timout log level changed to debug (Arnulfo Solís)
|
||||
- Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
|
||||
- Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
|
||||
- Add support for blueprint groups and nesting (Elias Tarhini)
|
||||
- Remove uvloop for windows setup (Aleksandr Kurlov)
|
||||
- Auto Reload (Yaser Amari)
|
||||
- Documentation updates/fixups (multiple contributors)
|
||||
|
||||
- Fixes:
|
||||
- Fix: auto_reload in Linux (Ashley Sommer)
|
||||
- Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer)
|
||||
- Fix: disable auto_reload by default on windows (abuckenheimer)
|
||||
- Fix (1143): Turn off access log with gunicorn (hqy)
|
||||
- Fix (1268): Support status code for file response (Cosmo Borsky)
|
||||
- Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky)
|
||||
- Fix: subprotocols parameter missing from add_websocket_route (ciscorn)
|
||||
- Fix (1242): Responses for CI header (yunstanford)
|
||||
- Fix (1237): add version constraint for websockets (yunstanford)
|
||||
- Fix (1231): memory leak - always release resource (Phillip Xu)
|
||||
- Fix (1221): make request truthy if transport exists (Raphael Deem)
|
||||
- Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer)
|
||||
- Fix try_everything examples (PyManiacGR, kot83)
|
||||
- Fix (1158): default to auto_reload in debug mode (Raphael Deem)
|
||||
- Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux)
|
||||
- Fix: raw requires bytes-like object (cloudship)
|
||||
- Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe)
|
||||
- Fix: Bug in multipart/form-data parser (DirkGuijt)
|
||||
- Fix: Exception for missing parameter when value is null (NyanKiyoshi)
|
||||
- Fix: Parameter check (Howie Hu)
|
||||
- Fix (1089): Routing issue with named parameters and different methods (yunstanford)
|
||||
- Fix (1085): Signal handling in multi-worker mode (yunstanford)
|
||||
- Fix: single quote in readme.rst (Cosven)
|
||||
- Fix: method typos (Dmitry Dygalo)
|
||||
- Fix: log_response correct output for ip and port (Wibowo Arindrarto)
|
||||
- Fix (1042): Exception Handling (Raphael Deem)
|
||||
- Fix: Chinese URIs (Howie Hu)
|
||||
- Fix (1079): timeout bug when self.transport is None (Raphael Deem)
|
||||
- Fix (1074): fix strict_slashes when route has slash (Raphael Deem)
|
||||
- Fix (1050): add samesite cookie to cookie keys (Raphael Deem)
|
||||
- Fix (1065): allow add_task after server starts (Raphael Deem)
|
||||
- Fix (1061): double quotes in unauthorized exception (Raphael Deem)
|
||||
- Fix (1062): inject the app in add_task method (Raphael Deem)
|
||||
- Fix: update environment.yml for readthedocs (Eli Uriegas)
|
||||
- Fix: Cancel request task when response timeout is triggered (Jeong YunWon)
|
||||
- Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem)
|
||||
- Fix: IPv6 Address and Socket Data Format (Dan Palmer)
|
||||
|
||||
Note: Changelog was unmaintained between 0.1 and 0.7
|
||||
|
||||
Version 0.1
|
||||
-----------
|
||||
- 0.1.7
|
||||
@@ -5,18 +82,18 @@ Version 0.1
|
||||
- 0.1.6
|
||||
- Static files
|
||||
- Lazy Cookie Loading
|
||||
- 0.1.5
|
||||
- 0.1.5
|
||||
- Cookies
|
||||
- Blueprint listeners and ordering
|
||||
- Faster Router
|
||||
- Fix: Incomplete file reads on medium+ sized post requests
|
||||
- Breaking: after_start and before_stop now pass sanic as their first argument
|
||||
- 0.1.4
|
||||
- 0.1.4
|
||||
- Multiprocessing
|
||||
- 0.1.3
|
||||
- Blueprint support
|
||||
- Faster Response processing
|
||||
- 0.1.1 - 0.1.2
|
||||
- 0.1.1 - 0.1.2
|
||||
- Struggling to update pypi via CI
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
- 0.1.0
|
||||
- Released to public
|
||||
|
||||
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.
|
||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
Copyright (c) 2016-present Channel Cat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
@@ -0,0 +1,7 @@
|
||||
include README.rst
|
||||
include MANIFEST.in
|
||||
include LICENSE
|
||||
include setup.py
|
||||
|
||||
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 -f docker/Dockerfile .
|
||||
docker run -t sanic/test-image tox
|
||||
82
README.rst
82
README.rst
@@ -1,41 +1,15 @@
|
||||
Sanic
|
||||
=================================
|
||||
=====
|
||||
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |PyPI| |PyPI version|
|
||||
|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black|
|
||||
|
||||
Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article <https://magic.io/blog/uvloop-blazing-fast-python-networking/>`_.
|
||||
|
||||
On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy.
|
||||
|
||||
Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome!
|
||||
Sanic is developed `on GitHub <https://github.com/huge-success/sanic/>`_. We also have `a community discussion board <https://community.sanicframework.org/>`_. 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/huge-success/sanic/issues/396>`_ that we use to track those projects!
|
||||
|
||||
Hello World Example
|
||||
-------------------
|
||||
@@ -47,17 +21,24 @@ Hello World Example
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
return json({'hello': 'world'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
- ``pip install sanic``
|
||||
|
||||
To install sanic without uvloop or ujson 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 pip install sanic``
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@@ -66,22 +47,35 @@ Documentation
|
||||
|
||||
.. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg
|
||||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/channelcat/sanic
|
||||
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/huge-success/sanic
|
||||
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/huge-success/sanic
|
||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/huge-success/sanic
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||
:target: http://sanic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg
|
||||
: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/
|
||||
.. |Code style black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
Questions and Discussion
|
||||
------------------------
|
||||
|
||||
`Ask a question or join the conversation <https://community.sanicframework.org/>`_.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/huge-success/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
|
||||
`Extensions <https://github.com/huge-success/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
|
||||
`Projects <https://github.com/huge-success/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
TODO
|
||||
----
|
||||
* Streamed file processing
|
||||
* http2
|
||||
Limitations
|
||||
-----------
|
||||
* No wheels for uvloop and httptools on Windows :(
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
28
docker/Dockerfile
Normal file
28
docker/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM alpine:3.7
|
||||
|
||||
RUN apk add --no-cache --update \
|
||||
curl \
|
||||
bash \
|
||||
build-base \
|
||||
ca-certificates \
|
||||
git \
|
||||
bzip2-dev \
|
||||
linux-headers \
|
||||
ncurses-dev \
|
||||
openssl \
|
||||
openssl-dev \
|
||||
readline-dev \
|
||||
sqlite-dev
|
||||
|
||||
RUN update-ca-certificates
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
ENV PYENV_ROOT="/root/.pyenv"
|
||||
ENV PATH="$PYENV_ROOT/bin:$PATH"
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN /app/docker/bin/install_python.sh 3.5.4 3.6.4
|
||||
|
||||
ENTRYPOINT ["./docker/bin/entrypoint.sh"]
|
||||
11
docker/bin/entrypoint.sh
Executable file
11
docker/bin/entrypoint.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
source /root/.pyenv/completions/pyenv.bash
|
||||
|
||||
pip install tox
|
||||
|
||||
exec $@
|
||||
|
||||
17
docker/bin/install_python.sh
Executable file
17
docker/bin/install_python.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export CFLAGS='-O2'
|
||||
export EXTRA_CFLAGS="-DTHREAD_STACK_SIZE=0x100000"
|
||||
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
|
||||
eval "$(pyenv init -)"
|
||||
|
||||
for ver in $@
|
||||
do
|
||||
pyenv install $ver
|
||||
done
|
||||
|
||||
pyenv global $@
|
||||
pip install --upgrade pip
|
||||
pyenv rehash
|
||||
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."
|
||||
|
||||
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
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', 'sphinxcontrib.asyncio']
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Basics
|
||||
|
||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.DB_NAME = 'appdb'
|
||||
app.config.DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||
|
||||
```
|
||||
db_settings = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_NAME': 'appdb',
|
||||
'DB_USER': 'appuser'
|
||||
}
|
||||
app.config.update(db_settings)
|
||||
```
|
||||
|
||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||
|
||||
## Loading Configuration
|
||||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### 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:
|
||||
|
||||
```
|
||||
import myapp.default_settings
|
||||
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.from_envvar('MYAPP_SETTINGS')
|
||||
```
|
||||
|
||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||
|
||||
```
|
||||
$ 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:
|
||||
|
||||
```
|
||||
# config_file
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'appdb'
|
||||
DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
## Builtin Configuration Values
|
||||
|
||||
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) |
|
||||
|
||||
@@ -9,19 +9,28 @@ Guides
|
||||
sanic/getting_started
|
||||
sanic/routing
|
||||
sanic/request_data
|
||||
sanic/response
|
||||
sanic/static_files
|
||||
sanic/exceptions
|
||||
sanic/middleware
|
||||
sanic/blueprints
|
||||
sanic/websocket
|
||||
sanic/config
|
||||
sanic/cookies
|
||||
sanic/decorators
|
||||
sanic/streaming
|
||||
sanic/class_based_views
|
||||
sanic/custom_protocol
|
||||
sanic/sockets
|
||||
sanic/ssl
|
||||
sanic/logging
|
||||
sanic/versioning
|
||||
sanic/debug_mode
|
||||
sanic/testing
|
||||
sanic/deploying
|
||||
sanic/extensions
|
||||
sanic/contributing
|
||||
sanic/api_reference
|
||||
|
||||
|
||||
Module Documentation
|
||||
@@ -30,4 +39,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:
|
||||
@@ -48,20 +48,92 @@ by that blueprint. In this example, the registered routes in the `app.router`
|
||||
will look like:
|
||||
|
||||
```python
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=None, pattern=re.compile('^/$'), parameters=[])]
|
||||
[Route(handler=<function bp_root at 0x7f908382f9d8>, methods=frozenset({'GET'}), pattern=re.compile('^/$'), parameters=[], name='my_blueprint.bp_root', uri='/')]
|
||||
```
|
||||
|
||||
## Blueprint groups and nesting
|
||||
|
||||
Blueprints may also be registered as part of a list or tuple, where the registrar will recursively cycle through any sub-sequences of blueprints and register them accordingly. The `Blueprint.group` method is provided to simplify this process, allowing a 'mock' backend directory structure mimicking what's seen from the front end. Consider this (quite contrived) example:
|
||||
|
||||
```
|
||||
api/
|
||||
├──content/
|
||||
│ ├──authors.py
|
||||
│ ├──static.py
|
||||
│ └──__init__.py
|
||||
├──info.py
|
||||
└──__init__.py
|
||||
app.py
|
||||
```
|
||||
|
||||
Initialization of this app's blueprint hierarchy could go as follows:
|
||||
|
||||
```python
|
||||
# api/content/authors.py
|
||||
from sanic import Blueprint
|
||||
|
||||
authors = Blueprint('content_authors', url_prefix='/authors')
|
||||
```
|
||||
```python
|
||||
# api/content/static.py
|
||||
from sanic import Blueprint
|
||||
|
||||
static = Blueprint('content_static', url_prefix='/static')
|
||||
```
|
||||
```python
|
||||
# api/content/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .static import static
|
||||
from .authors import authors
|
||||
|
||||
content = Blueprint.group(static, authors, url_prefix='/content')
|
||||
```
|
||||
```python
|
||||
# api/info.py
|
||||
from sanic import Blueprint
|
||||
|
||||
info = Blueprint('info', url_prefix='/info')
|
||||
```
|
||||
```python
|
||||
# api/__init__.py
|
||||
from sanic import Blueprint
|
||||
|
||||
from .content import content
|
||||
from .info import info
|
||||
|
||||
api = Blueprint.group(content, info, url_prefix='/api')
|
||||
```
|
||||
|
||||
And registering these blueprints in `app.py` can now be done like so:
|
||||
|
||||
```python
|
||||
# app.py
|
||||
from sanic import Sanic
|
||||
|
||||
from .api import api
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.blueprint(api)
|
||||
```
|
||||
|
||||
## Using blueprints
|
||||
|
||||
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')
|
||||
@@ -88,7 +160,14 @@ def ignore_404s(request, exception):
|
||||
Static files can be served globally, under the blueprint prefix.
|
||||
|
||||
```python
|
||||
bp.static('/folder/to/serve', '/web/path')
|
||||
|
||||
# suppose bp.name == 'bp'
|
||||
|
||||
bp.static('/web/path', '/folder/to/serve')
|
||||
# also you can pass name parameter to it for url_for
|
||||
bp.static('/web/path', '/folder/to/server', name='uploads')
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
|
||||
|
||||
```
|
||||
|
||||
## Start and stop
|
||||
@@ -111,7 +190,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()
|
||||
@@ -137,7 +216,7 @@ 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')
|
||||
@@ -164,10 +243,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) # --> '/v1/post/5'
|
||||
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@@ -175,5 +254,3 @@ async def root(request):
|
||||
async def post_handler(request, post_id):
|
||||
return text('Post {} in Blueprint V1'.format(post_id))
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -74,10 +92,27 @@ class ViewWithDecorator(HTTPMethodView):
|
||||
def get(self, request, name):
|
||||
return text('Hello I have a decorator')
|
||||
|
||||
def post(self, request, name):
|
||||
return text("Hello I also have a decorator")
|
||||
|
||||
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||
```
|
||||
|
||||
#### URL Building
|
||||
But if you just want to decorate some functions and not all functions, you can do as follows:
|
||||
|
||||
```python
|
||||
class ViewWithSomeDecorator(HTTPMethodView):
|
||||
|
||||
@staticmethod
|
||||
@some_decorator_here
|
||||
def get(request, name):
|
||||
return text("Hello I have a decorator")
|
||||
|
||||
def post(self, request, name):
|
||||
return text("Hello I don't have any decorators")
|
||||
```
|
||||
|
||||
## URL Building
|
||||
|
||||
If you wish to build a URL for an HTTPMethodView, remember that the class name will be the endpoint
|
||||
that you will pass into `url_for`. For example:
|
||||
@@ -128,4 +163,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`.
|
||||
|
||||
121
docs/sanic/config.md
Normal file
121
docs/sanic/config.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Configuration
|
||||
|
||||
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
|
||||
|
||||
Sanic holds the configuration in the `config` attribute of the application object. The configuration object is merely an object that can be modified either using dot-notation or like a dictionary:
|
||||
|
||||
```
|
||||
app = Sanic('myapp')
|
||||
app.config.DB_NAME = 'appdb'
|
||||
app.config.DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
Since the config object actually is a dictionary, you can use its `update` method in order to set several values at once:
|
||||
|
||||
```
|
||||
db_settings = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_NAME': 'appdb',
|
||||
'DB_USER': 'appuser'
|
||||
}
|
||||
app.config.update(db_settings)
|
||||
```
|
||||
|
||||
In general the convention is to only have UPPERCASE configuration parameters. The methods described below for loading configuration only look for such uppercase parameters.
|
||||
|
||||
## Loading Configuration
|
||||
|
||||
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 and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
```
|
||||
|
||||
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
|
||||
|
||||
```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:
|
||||
|
||||
```
|
||||
import myapp.default_settings
|
||||
|
||||
app = Sanic('myapp')
|
||||
app.config.from_object(myapp.default_settings)
|
||||
```
|
||||
|
||||
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_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')
|
||||
app.config.from_envvar('MYAPP_SETTINGS')
|
||||
```
|
||||
|
||||
Then you can run your application with the `MYAPP_SETTINGS` environment variable set:
|
||||
|
||||
```
|
||||
$ 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 variables are added to the configuration. Most commonly the configuration consists of simple key value pairs:
|
||||
|
||||
```
|
||||
# config_file
|
||||
DB_HOST = 'localhost'
|
||||
DB_NAME = 'appdb'
|
||||
DB_USER = 'appuser'
|
||||
```
|
||||
|
||||
## Builtin Configuration Values
|
||||
|
||||
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 to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||
| GRACEFUL_SHUTDOWN_TIMEOUT | 15.0 | How long take to force close non-idle connection (sec) |
|
||||
| ACCESS_LOG | True | Disable or enable access log |
|
||||
|
||||
### The different Timeout variables:
|
||||
|
||||
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
|
||||
|
||||
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
|
||||
|
||||
### What is Keep Alive? And what does the Keep Alive Timeout value do?
|
||||
|
||||
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
|
||||
|
||||
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
|
||||
|
||||
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
|
||||
|
||||
For reference:
|
||||
```
|
||||
Apache httpd server default keepalive timeout = 5 seconds
|
||||
Nginx server default keepalive timeout = 75 seconds
|
||||
Nginx performance tuning guidelines uses keepalive = 15 seconds
|
||||
IE (5-9) client hard keepalive limit = 60 seconds
|
||||
Firefox client hard keepalive limit = 115 seconds
|
||||
Opera 11 client hard keepalive limit = 120 seconds
|
||||
Chrome 13+ client keepalive limit > 300+ seconds
|
||||
```
|
||||
@@ -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:
|
||||
* 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/
|
||||
53
docs/sanic/debug_mode.rst
Normal file
53
docs/sanic/debug_mode.rst
Normal file
@@ -0,0 +1,53 @@
|
||||
Debug Mode
|
||||
=============
|
||||
|
||||
When enabling Sanic's debug mode, Sanic will provide a more verbose logging output
|
||||
and by default will enable the Auto Reload feature.
|
||||
|
||||
.. warning::
|
||||
|
||||
Sanic's debug more will slow down the server's performance
|
||||
and is therefore advised to enable it only in development environments.
|
||||
|
||||
|
||||
Setting the debug mode
|
||||
----------------------
|
||||
|
||||
By setting the ``debug`` mode a more verbose output from Sanic will be outputed
|
||||
and the Automatic Reloader will be activated.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def hello_world(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
|
||||
|
||||
|
||||
Manually setting auto reload
|
||||
----------------------------
|
||||
|
||||
Sanic offers a way to enable or disable the Automatic Reloader manually,
|
||||
the ``auto_reload`` argument will activate or deactivate the Automatic Reloader.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def hello_world(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, auto_reload=True)
|
||||
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'})
|
||||
```
|
||||
|
||||
@@ -44,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()
|
||||
```
|
||||
|
||||
@@ -13,10 +13,23 @@ To throw an exception, simply `raise` the relevant exception from the
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
@app.route('/killme')
|
||||
def i_am_ready_to_die(request):
|
||||
async 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')
|
||||
async 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`
|
||||
@@ -30,10 +43,40 @@ from sanic.response import text
|
||||
from sanic.exceptions import NotFound
|
||||
|
||||
@app.exception(NotFound)
|
||||
def ignore_404s(request, exception):
|
||||
async def ignore_404s(request, exception):
|
||||
return text("Yep, I totally found the page: {}".format(request.url))
|
||||
```
|
||||
|
||||
You can also add an exception handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
async def server_error_handler(request, exception):
|
||||
return text("Oops, server error", status=500)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler.add(Exception, server_error_handler)
|
||||
```
|
||||
|
||||
In some cases, you might want want to add some more error handling
|
||||
functionality to what is provided by default. In that case, you
|
||||
can subclass Sanic's default error handler as such:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.handlers import ErrorHandler
|
||||
|
||||
class CustomErrorHandler(ErrorHandler):
|
||||
def default(self, request, exception):
|
||||
''' handles errors that have no error handlers assigned '''
|
||||
# You custom error handling logic...
|
||||
return super().default(request, exception)
|
||||
|
||||
app = Sanic()
|
||||
app.error_handler = CustomErrorHandler()
|
||||
```
|
||||
|
||||
## Useful exceptions
|
||||
|
||||
Some of the most useful exceptions are presented below:
|
||||
|
||||
@@ -1,14 +1,36 @@
|
||||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
|
||||
- [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.
|
||||
- [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic.
|
||||
- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic
|
||||
- [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-oauth](https://gitlab.com/SirEdvin/sanic-oauth): OAuth Library with many provider and OAuth1/OAuth2 support.
|
||||
- [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/))
|
||||
- [GINO](https://github.com/fantix/gino): An asyncio ORM on top of SQLAlchemy core, delivered with a Sanic extension. ([Documentation](https://python-gino.readthedocs.io/))
|
||||
- [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic.
|
||||
- [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask.
|
||||
- [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier.
|
||||
- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application.
|
||||
- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic.
|
||||
|
||||
@@ -4,24 +4,37 @@ Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and at
|
||||
least version 3.5 of Python before starting. Sanic uses the new `async`/`await`
|
||||
syntax, so earlier versions of python won't work.
|
||||
|
||||
1. Install Sanic: `python3 -m pip install sanic`
|
||||
2. Create a file called `main.py` with the following code:
|
||||
## 1. Install Sanic
|
||||
|
||||
```
|
||||
python3 -m pip install sanic
|
||||
```
|
||||
|
||||
## 2. Create a file called `main.py`
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return text('Hello world!')
|
||||
return json({"hello": "world"})
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
4. Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
## 3. Run the server
|
||||
|
||||
```
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## 4. Check your browser
|
||||
|
||||
Open the address `http://0.0.0.0:8000` in your web browser. You should see
|
||||
the message *Hello world!*.
|
||||
|
||||
You now have a working Sanic server!
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
100
docs/sanic/logging.md
Normal file
100
docs/sanic/logging.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 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.log import logger
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
@app.route('/')
|
||||
async def test(request):
|
||||
logger.info('Here is your log')
|
||||
return text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, access_log=True)
|
||||
```
|
||||
|
||||
After the server is running, you can see some messages looks like:
|
||||
```
|
||||
[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000
|
||||
[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667]
|
||||
```
|
||||
|
||||
You can send a request to server and it will print the log messages:
|
||||
```
|
||||
[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log
|
||||
[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12
|
||||
```
|
||||
|
||||
To use your own logging config, simply use `logging.config.dictConfig`, or
|
||||
pass `log_config` when you initialize `Sanic` app:
|
||||
|
||||
```python
|
||||
app = Sanic('test', log_config=LOGGING_CONFIG)
|
||||
```
|
||||
|
||||
And to close logging, simply assign access_log=False:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.run(access_log=False)
|
||||
```
|
||||
|
||||
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 debug messages
|
||||
app.run(debug=False, access_log=False)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
|
||||
|
||||
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- sanic.root:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
- sanic.error:<br>
|
||||
Used to log error logs.
|
||||
|
||||
- sanic.access:<br>
|
||||
Used to log access logs.
|
||||
|
||||
#### Log format:
|
||||
|
||||
In addition to default parameters provided by python (asctime, levelname, message),
|
||||
Sanic provides additional parameters for access logger with:
|
||||
|
||||
- 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,18 +1,23 @@
|
||||
# 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 provides 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
|
||||
receives both the request and the response as arguments.
|
||||
string representing its type: `'request'` or `'response'`.
|
||||
|
||||
* Request middleware receives only the `request` as argument.
|
||||
* Response middleware receives both the `request` and `response`.
|
||||
|
||||
The simplest middleware doesn't modify the request or response at all:
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def print_on_request(request):
|
||||
print("I print when a request is received by the server")
|
||||
@@ -28,7 +33,7 @@ Middleware can modify the request or response parameter it is given, *as long
|
||||
as it does not return it*. The following example shows a practical use-case for
|
||||
this.
|
||||
|
||||
```python
|
||||
```
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.middleware('response')
|
||||
@@ -55,7 +60,7 @@ and the response will be returned. If this occurs to a request before the
|
||||
relevant user route handler is reached, the handler will never be called.
|
||||
Returning a response will also prevent any further middleware from running.
|
||||
|
||||
```python
|
||||
```
|
||||
@app.middleware('request')
|
||||
async def halt_request(request):
|
||||
return text('I halted the request')
|
||||
@@ -64,3 +69,79 @@ 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:
|
||||
|
||||
```
|
||||
@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()
|
||||
```
|
||||
|
||||
It's also possible to register a listener using the `register_listener` method.
|
||||
This may be useful if you define your listeners in another module besides
|
||||
the one you instantiate your app in.
|
||||
|
||||
```
|
||||
app = Sanic()
|
||||
|
||||
async def setup_db(app, loop):
|
||||
app.db = await db_setup()
|
||||
|
||||
app.register_listener(setup_db, 'before_server_start')
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
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())
|
||||
```
|
||||
|
||||
Sanic will attempt to automatically inject the app, passing it as an argument to the task:
|
||||
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds)
|
||||
```
|
||||
|
||||
Or you can pass the app explicitly for the same effect:
|
||||
|
||||
```
|
||||
async def notify_server_started_after_five_seconds(app):
|
||||
await asyncio.sleep(5)
|
||||
print(app.name)
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds(app))
|
||||
`
|
||||
|
||||
@@ -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,25 @@ 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)
|
||||
```
|
||||
|
||||
|
||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||
|
||||
- `method` (str) - HTTP method of the request (ie `GET`, `POST`).
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `port` (str) - Port address of the requester.
|
||||
|
||||
- `socket` (tuple) - (IP, port) 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
|
||||
@@ -85,6 +97,14 @@ The following variables are accessible as properties on `Request` objects:
|
||||
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>/`
|
||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
||||
|
||||
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(b'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
|
||||
)
|
||||
```
|
||||
@@ -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))
|
||||
|
||||
@@ -138,13 +138,14 @@ app.add_route(person_handler2, '/person/<name:[A-z]>', methods=['GET'])
|
||||
Sanic provides a `url_for` method, to generate URLs based on the handler method name. This is useful if you want to avoid hardcoding url paths into your app; instead, you can just reference the handler name. For example:
|
||||
|
||||
```python
|
||||
from sanic.response import redirect
|
||||
|
||||
@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 redirect(url)
|
||||
|
||||
return redirect(url)
|
||||
|
||||
@app.route('/posts/<post_id>')
|
||||
async def post_handler(request, post_id):
|
||||
@@ -181,3 +182,154 @@ url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2,
|
||||
# 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.
|
||||
|
||||
|
||||
## About `strict_slashes`
|
||||
|
||||
You can make `routes` strict to trailing slash or not, it's configurable.
|
||||
|
||||
```python
|
||||
|
||||
# provide default strict_slashes value for all routes
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
# you can also overwrite strict_slashes value for specific route
|
||||
@app.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
|
||||
|
||||
@bp.get('/bp/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
```
|
||||
|
||||
## User defined route name
|
||||
|
||||
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_named_route')
|
||||
|
||||
@app.get('/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then you need use `app.url_for('get_handler')`
|
||||
# instead of # `app.url_for('handler')`
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_named_bp')
|
||||
|
||||
@bp.get('/bp/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
# then you need use `app.url_for('test_named_bp.get_handler')`
|
||||
# instead of `app.url_for('test_named_bp.handler')`
|
||||
|
||||
# different names can be used for same url with different methods
|
||||
|
||||
@app.get('/test', name='route_test')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/test', name='route_post')
|
||||
def handler2(request):
|
||||
return text('OK POST')
|
||||
|
||||
@app.put('/test', name='route_put')
|
||||
def handler3(request):
|
||||
return text('OK PUT')
|
||||
|
||||
# below url are the same, you can use any of them
|
||||
# '/test'
|
||||
app.url_for('route_test')
|
||||
# app.url_for('route_post')
|
||||
# app.url_for('route_put')
|
||||
|
||||
# for same handler name with different methods
|
||||
# you need specify the name (it's url_for issue)
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/post', name='post_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then
|
||||
# app.url_for('handler') == '/get'
|
||||
# app.url_for('post_handler') == '/post'
|
||||
```
|
||||
|
||||
## Build URL for static files
|
||||
|
||||
You can use `url_for` for static file url building now.
|
||||
If it's for file directly, `filename` can be ignored.
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/static', './static')
|
||||
app.static('/uploads', './uploads', name='uploads')
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
bp = Blueprint('bp', url_prefix='bp')
|
||||
bp.static('/static', './static')
|
||||
bp.static('/uploads', './uploads', name='uploads')
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
# then build the url
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
|
||||
# blueprint url building
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
|
||||
|
||||
```
|
||||
|
||||
66
docs/sanic/sockets.rst
Normal file
66
docs/sanic/sockets.rst
Normal file
@@ -0,0 +1,66 @@
|
||||
Sockets
|
||||
=======
|
||||
|
||||
Sanic can use the python
|
||||
`socket module <https://docs.python.org/3/library/socket.html>`_ to accommodate
|
||||
non IPv4 sockets.
|
||||
|
||||
IPv6 example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.bind(('::', 7777))
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test IPv6 ``curl -g -6 "http://[::1]:7777/"``
|
||||
|
||||
|
||||
UNIX socket example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
server_socket = '/tmp/sanic.sock'
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.bind(server_socket)
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return json({"hello": "world"})
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('Exiting')
|
||||
os.unlink(server_socket)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(sock=sock)
|
||||
|
||||
to test UNIX: ``curl -v --unix-socket /tmp/sanic.sock http://localhost/hello``
|
||||
@@ -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)
|
||||
|
||||
@@ -1,21 +1,83 @@
|
||||
# Static Files
|
||||
|
||||
Static files and directories, such as an image file, are served by Sanic when
|
||||
registered with the `app.static` method. The method takes an endpoint URL and a
|
||||
registered with the `app.static()` method. The method takes an endpoint URL and a
|
||||
filename. The file specified will then be accessible via the given endpoint.
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
app.static('/static', './static')
|
||||
# use url_for to build the url, name defaults to 'static' and can be ignored
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
|
||||
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||
# is requested
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
# you can use url_for to build the static file url
|
||||
# you can ignore name and filename parameters if you don't define it
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
|
||||
|
||||
# you need define the name for other static files
|
||||
app.static('/another.png', '/home/ubuntu/another.png', name='another')
|
||||
app.url_for('static', name='another') == '/another.png'
|
||||
app.url_for('static', name='another', filename='any') == '/another.png'
|
||||
|
||||
# also, you can use static for blueprint
|
||||
bp = Blueprint('bp', url_prefix='/bp')
|
||||
bp.static('/static', './static')
|
||||
|
||||
# servers the file directly
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
Note: currently you cannot build a URL for a static file using `url_for`.
|
||||
> **Note:** Sanic does not provide directory index when you serve a static directory.
|
||||
|
||||
## Virtual Host
|
||||
|
||||
The `app.static()` method also support **virtual host**. You can serve your static files with spefic **virtual host** with `host` argument. For example:
|
||||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/static', './static')
|
||||
app.static('/example_static', './example_static', host='www.example.com')
|
||||
```
|
||||
|
||||
## Streaming Large File
|
||||
|
||||
In some cases, you might server large file(ex: videos, images, etc.) with Sanic. You can choose to use **streaming file** rather than download directly.
|
||||
|
||||
Here is an example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=True)
|
||||
```
|
||||
|
||||
When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `file()` to serve static files. This will use **1KB** as the default chunk size. And, if needed, you can also use a custom chunk size. For example:
|
||||
```python
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||
```
|
||||
|
||||
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')
|
||||
await 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):
|
||||
await response.write('foo,')
|
||||
await 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)'):
|
||||
await response.write(record[0])
|
||||
|
||||
return stream(stream_from_db)
|
||||
```
|
||||
@@ -20,7 +20,7 @@ def test_index_put_not_allowed():
|
||||
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
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
||||
your test request is executed against your application, using `aiohttp`.
|
||||
|
||||
The `test_client` methods accept the following arguments and keyword arguments:
|
||||
@@ -59,15 +59,69 @@ the available arguments to aiohttp can be found
|
||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
|
||||
### Deprecated: `sanic_endpoint_test`
|
||||
## pytest-sanic
|
||||
|
||||
Prior to version 0.3.2, testing was provided through the `sanic_endpoint_test` method. This method will be deprecated in the next major version after 0.4.0; please use the `test_client` instead.
|
||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
|
||||
Just write tests like,
|
||||
|
||||
```
|
||||
from sanic.utils import sanic_endpoint_test
|
||||
|
||||
def test_index_returns_200():
|
||||
request, response = sanic_endpoint_test(app)
|
||||
assert response.status == 200
|
||||
```python
|
||||
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
|
||||
```
|
||||
52
docs/sanic/websocket.rst
Normal file
52
docs/sanic/websocket.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
WebSocket
|
||||
=========
|
||||
|
||||
Sanic supports websockets, to setup a WebSocket:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
from sanic.websocket import WebSocketProtocol
|
||||
|
||||
app = Sanic()
|
||||
|
||||
@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, protocol=WebSocketProtocol)
|
||||
|
||||
|
||||
Alternatively, the ``app.add_websocket_route`` method can be used instead of the
|
||||
decorator:
|
||||
|
||||
.. code:: python
|
||||
|
||||
async def feed(request, ws):
|
||||
pass
|
||||
|
||||
app.add_websocket_route(feed, '/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.
|
||||
|
||||
|
||||
You could setup your own WebSocket configuration through ``app.config``, like
|
||||
|
||||
.. code:: python
|
||||
|
||||
app.config.WEBSOCKET_MAX_SIZE = 2 ** 20
|
||||
app.config.WEBSOCKET_MAX_QUEUE = 32
|
||||
app.config.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
app.config.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
|
||||
Find more in ``Configuration`` section.
|
||||
@@ -12,7 +12,10 @@ dependencies:
|
||||
- zlib=1.2.8=0
|
||||
- pip:
|
||||
- uvloop>=0.5.3
|
||||
- httptools>=0.0.9
|
||||
- httptools>=0.0.10
|
||||
- ujson>=1.35
|
||||
- aiofiles>=0.3.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
- websockets>=6.0
|
||||
- sphinxcontrib-asyncio>=0.2.0
|
||||
- multidict>=4.0,<5.0
|
||||
- https://github.com/channelcat/docutils-fork/zipball/master
|
||||
|
||||
17
examples/add_task_sanic.py
Normal file
17
examples/add_task_sanic.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
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())
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -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)
|
||||
42
examples/authorized_sanic.py
Normal file
42
examples/authorized_sanic.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sanic import Sanic
|
||||
from functools import wraps
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
def check_request_for_authorization_status(request):
|
||||
# Note: Define your check, for instance cookie, session.
|
||||
flag = True
|
||||
return flag
|
||||
|
||||
|
||||
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'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,11 +1,10 @@
|
||||
from sanic import Sanic
|
||||
from sanic import Blueprint
|
||||
from sanic.response import json, text
|
||||
|
||||
from sanic import Blueprint, Sanic
|
||||
from sanic.response import file, 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 +17,22 @@ async def foo2(request):
|
||||
return json({'msg': 'hi from blueprint2'})
|
||||
|
||||
|
||||
app.register_blueprint(blueprint)
|
||||
app.register_blueprint(blueprint2)
|
||||
@blueprint3.route('/foo')
|
||||
async def index(request):
|
||||
return await file('websocket.html')
|
||||
|
||||
|
||||
@app.websocket('/feed')
|
||||
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,46 +0,0 @@
|
||||
"""
|
||||
Example of caching using aiocache package. To run it you will need a Redis
|
||||
instance running in localhost:6379. You can also try with SimpleMemoryCache.
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
@app.listener('before_server_start')
|
||||
def init_cache(sanic, loop):
|
||||
aiocache.settings.set_defaults(
|
||||
class_="aiocache.RedisCache",
|
||||
# class_="aiocache.SimpleMemoryCache",
|
||||
loop=loop
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
@@ -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,11 +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):
|
||||
"""
|
||||
|
||||
86
examples/log_request_id.py
Normal file
86
examples/log_request_id.py
Normal file
@@ -0,0 +1,86 @@
|
||||
'''
|
||||
Based on example from https://github.com/Skyscanner/aiotask-context
|
||||
and `examples/{override_logging,run_async}.py`.
|
||||
|
||||
Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer
|
||||
|
||||
$ pip install git+https://github.com/Skyscanner/aiotask-context.git
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import logging
|
||||
from signal import signal, SIGINT
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
import uvloop
|
||||
import aiotask_context as context
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestIdFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.request_id = context.get('X-Request-ID')
|
||||
return True
|
||||
|
||||
|
||||
LOG_SETTINGS = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'level': 'DEBUG',
|
||||
'formatter': 'default',
|
||||
'filters': ['requestid'],
|
||||
},
|
||||
},
|
||||
'filters': {
|
||||
'requestid': {
|
||||
'()': RequestIdFilter,
|
||||
},
|
||||
},
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': True
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
async def set_request_id(request):
|
||||
request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4())
|
||||
context.set("X-Request-ID", request_id)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.debug('X-Request-ID: %s', context.get('X-Request-ID'))
|
||||
log.info('Hello from test!')
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_task_factory(context.task_factory)
|
||||
task = asyncio.ensure_future(server)
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
loop.stop()
|
||||
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)
|
||||
|
||||
49
examples/pytest_xdist.py
Normal file
49
examples/pytest_xdist.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""pytest-xdist example for sanic server
|
||||
|
||||
Install testing tools:
|
||||
|
||||
$ pip install pytest pytest-xdist
|
||||
|
||||
Run with xdist params:
|
||||
|
||||
$ pytest examples/pytest_xdist.py -n 8 # 8 workers
|
||||
"""
|
||||
import re
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.testing import PORT as PORT_BASE, SanicTestClient
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_port(worker_id):
|
||||
m = re.search(r'[0-9]+', worker_id)
|
||||
if m:
|
||||
num_id = m.group(0)
|
||||
else:
|
||||
num_id = 0
|
||||
port = PORT_BASE + int(num_id)
|
||||
return port
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app():
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return text('OK')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client(app, test_port):
|
||||
return SanicTestClient(app, test_port)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('run_id', range(100))
|
||||
def test_index(client, run_id):
|
||||
request, response = client._sanic_endpoint_test('get', '/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
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')
|
||||
await 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__)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
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)
|
||||
@@ -1,67 +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__)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
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)
|
||||
@@ -1,59 +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__)
|
||||
|
||||
@app.listener('before_server_start')
|
||||
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)
|
||||
@@ -1,41 +0,0 @@
|
||||
""" sanic motor (async driver for mongodb) example
|
||||
Required packages:
|
||||
pymongo==3.4.0
|
||||
motor==1.1
|
||||
sanic==0.2.0
|
||||
"""
|
||||
from sanic import Sanic
|
||||
from sanic.response import json
|
||||
|
||||
|
||||
app = Sanic('motor_mongodb')
|
||||
|
||||
|
||||
def get_db():
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
mongo_uri = "mongodb://127.0.0.1:27017/test"
|
||||
client = AsyncIOMotorClient(mongo_uri)
|
||||
return client['test']
|
||||
|
||||
|
||||
@app.route('/objects', methods=['GET'])
|
||||
async def get(request):
|
||||
db = get_db()
|
||||
docs = await db.test_col.find().to_list(length=100)
|
||||
for doc in docs:
|
||||
doc['id'] = str(doc['_id'])
|
||||
del doc['_id']
|
||||
return json(docs)
|
||||
|
||||
|
||||
@app.route('/post', methods=['POST'])
|
||||
async def new(request):
|
||||
doc = request.json
|
||||
print(doc)
|
||||
db = get_db()
|
||||
object_id = await db.test_col.save(doc)
|
||||
return json({'object_id': str(object_id)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='127.0.0.1', port=8000)
|
||||
@@ -1,79 +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
|
||||
|
||||
## 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.listener('before_server_start')
|
||||
def setup(app, loop):
|
||||
database = PostgresqlDatabase(database='test',
|
||||
host='127.0.0.1',
|
||||
user='postgres',
|
||||
password='mysecretpassword')
|
||||
|
||||
objects = Manager(database, loop=loop)
|
||||
|
||||
@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)
|
||||
42
examples/simple_async_view.py
Normal file
42
examples/simple_async_view.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from sanic import Sanic
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.response import text
|
||||
|
||||
app = Sanic('some_name')
|
||||
|
||||
|
||||
class SimpleView(HTTPMethodView):
|
||||
|
||||
def get(self, request):
|
||||
return text('I am get method')
|
||||
|
||||
def post(self, request):
|
||||
return text('I am post method')
|
||||
|
||||
def put(self, request):
|
||||
return text('I am put method')
|
||||
|
||||
def patch(self, request):
|
||||
return text('I am patch method')
|
||||
|
||||
def delete(self, request):
|
||||
return text('I am delete method')
|
||||
|
||||
|
||||
class SimpleAsyncView(HTTPMethodView):
|
||||
|
||||
async def get(self, request):
|
||||
return text('I am async get method')
|
||||
|
||||
async def post(self, request):
|
||||
return text('I am async post method')
|
||||
|
||||
async def put(self, request):
|
||||
return text('I am async put method')
|
||||
|
||||
|
||||
app.add_route(SimpleView.as_view(), '/')
|
||||
app.add_route(SimpleAsyncView.as_view(), '/async')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
@@ -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)
|
||||
|
||||
13
examples/teapot.py
Normal file
13
examples/teapot.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sanic import Sanic
|
||||
from sanic import response as res
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(req):
|
||||
return res.text("I\'m a teapot", status=418)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
@@ -1,8 +1,8 @@
|
||||
import os
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.log import log
|
||||
from sanic.response import json, text, file
|
||||
from sanic.log import logger as log
|
||||
from sanic import response
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
app = Sanic(__name__)
|
||||
@@ -10,41 +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))
|
||||
@app.route("/dynamic/<name>/<i:int>")
|
||||
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 file(os.path.abspath("setup.py"))
|
||||
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)
|
||||
|
||||
|
||||
# ----------------------------------------------- #
|
||||
@@ -53,23 +62,29 @@ 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')})
|
||||
def post_form_json(request):
|
||||
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")
|
||||
@@ -77,7 +92,13 @@ def after_start(app, loop):
|
||||
|
||||
@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)
|
||||
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)
|
||||
|
||||
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
@@ -1,18 +1,13 @@
|
||||
aiocache
|
||||
aiofiles
|
||||
aiohttp
|
||||
aiohttp>=2.3.0,<=3.2.1
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
bottle
|
||||
coverage
|
||||
falcon
|
||||
gunicorn
|
||||
httptools
|
||||
kyoukai
|
||||
pytest
|
||||
recommonmark
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
tornado
|
||||
httptools>=0.0.10
|
||||
flake8
|
||||
pytest==3.3.2
|
||||
tox
|
||||
ujson
|
||||
uvloop
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
gunicorn
|
||||
multidict>=4.0,<5.0
|
||||
|
||||
4
requirements-docs.txt
Normal file
4
requirements-docs.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
recommonmark
|
||||
sphinxcontrib-asyncio
|
||||
@@ -1,4 +1,6 @@
|
||||
aiofiles
|
||||
httptools
|
||||
ujson
|
||||
uvloop
|
||||
httptools>=0.0.10
|
||||
ujson; sys_platform != "win32" and implementation_name == "cpython"
|
||||
uvloop; sys_platform != "win32" and implementation_name == "cpython"
|
||||
websockets>=6.0,<7.0
|
||||
multidict>=4.0,<5.0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.4.1'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
__version__ = "18.12.0"
|
||||
|
||||
__all__ = ["Sanic", "Blueprint"]
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.app import Sanic
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
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('--workers', dest='workers', type=int, default=1, )
|
||||
parser.add_argument('--debug', dest='debug', action="store_true")
|
||||
parser.add_argument('module')
|
||||
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")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
@@ -21,16 +28,29 @@ if __name__ == "__main__":
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
if not isinstance(app, Sanic):
|
||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||
"Perhaps you meant {}.app?"
|
||||
.format(type(app).__name__, args.module))
|
||||
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:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(module_name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(
|
||||
"No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app".format(e.name)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception("Failed to run app")
|
||||
|
||||
995
sanic/app.py
995
sanic/app.py
File diff suppressed because it is too large
Load Diff
@@ -3,35 +3,89 @@ from collections import defaultdict, namedtuple
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.views import CompositionView
|
||||
|
||||
FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
FutureStatic = namedtuple('Route',
|
||||
['uri', 'file_or_directory', 'args', 'kwargs'])
|
||||
|
||||
FutureRoute = namedtuple(
|
||||
"FutureRoute",
|
||||
[
|
||||
"handler",
|
||||
"uri",
|
||||
"methods",
|
||||
"host",
|
||||
"strict_slashes",
|
||||
"stream",
|
||||
"version",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
FutureListener = namedtuple(
|
||||
"FutureListener", ["handler", "uri", "methods", "host"]
|
||||
)
|
||||
FutureMiddleware = namedtuple(
|
||||
"FutureMiddleware", ["middleware", "args", "kwargs"]
|
||||
)
|
||||
FutureException = namedtuple("FutureException", ["handler", "args", "kwargs"])
|
||||
FutureStatic = namedtuple(
|
||||
"FutureStatic", ["uri", "file_or_directory", "args", "kwargs"]
|
||||
)
|
||||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None, host=None):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
url_prefix=None,
|
||||
host=None,
|
||||
version=None,
|
||||
strict_slashes=False,
|
||||
):
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param strict_slashes: strict to trailing slash
|
||||
"""
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.host = host
|
||||
|
||||
self.routes = []
|
||||
self.websocket_routes = []
|
||||
self.exceptions = []
|
||||
self.listeners = defaultdict(list)
|
||||
self.middlewares = []
|
||||
self.statics = []
|
||||
self.version = version
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
@staticmethod
|
||||
def group(*blueprints, url_prefix=""):
|
||||
"""Create a list of blueprints, optionally
|
||||
grouping them under a general URL prefix.
|
||||
|
||||
:param blueprints: blueprints to be registered as a group
|
||||
:param url_prefix: URL route to be prepended to all sub-prefixes
|
||||
"""
|
||||
|
||||
def chain(nested):
|
||||
"""itertools.chain() but leaves strings untouched"""
|
||||
for i in nested:
|
||||
if isinstance(i, (list, tuple)):
|
||||
yield from chain(i)
|
||||
else:
|
||||
yield i
|
||||
|
||||
bps = []
|
||||
for bp in chain(blueprints):
|
||||
if bp.url_prefix is None:
|
||||
bp.url_prefix = ""
|
||||
bp.url_prefix = url_prefix + bp.url_prefix
|
||||
bps.append(bp)
|
||||
return bps
|
||||
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
||||
url_prefix = options.get('url_prefix', self.url_prefix)
|
||||
url_prefix = options.get("url_prefix", self.url_prefix)
|
||||
|
||||
# Routes
|
||||
for future in self.routes:
|
||||
@@ -40,19 +94,40 @@ 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[1:] if uri.startswith('//') else uri,
|
||||
uri=uri[1:] if uri.startswith("//") else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host
|
||||
)(future.handler)
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(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,
|
||||
name=future.name,
|
||||
)(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:
|
||||
@@ -62,48 +137,126 @@ class Blueprint:
|
||||
for future in self.statics:
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.static(uri, future.file_or_directory,
|
||||
*future.args, **future.kwargs)
|
||||
app.static(
|
||||
uri, future.file_or_directory, *future.args, **future.kwargs
|
||||
)
|
||||
|
||||
# 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):
|
||||
def route(
|
||||
self,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=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.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, methods, host)
|
||||
route = FutureRoute(
|
||||
handler,
|
||||
uri,
|
||||
methods,
|
||||
host,
|
||||
strict_slashes,
|
||||
stream,
|
||||
version,
|
||||
name,
|
||||
)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
|
||||
def add_route(
|
||||
self,
|
||||
handler,
|
||||
uri,
|
||||
methods=frozenset({"GET"}),
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
version=None,
|
||||
name=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.
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
if hasattr(handler, 'view_class'):
|
||||
if hasattr(handler, "view_class"):
|
||||
methods = set()
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host)(handler)
|
||||
self.route(
|
||||
uri=uri,
|
||||
methods=methods,
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, [], host, strict_slashes, False, version, name
|
||||
)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(
|
||||
self, handler, uri, host=None, version=None, name=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, name=name)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
@@ -111,13 +264,16 @@ class Blueprint:
|
||||
|
||||
:param event: Event to listen to.
|
||||
"""
|
||||
|
||||
def decorator(listener):
|
||||
self.listeners[event].append(listener)
|
||||
return listener
|
||||
|
||||
return decorator
|
||||
|
||||
def middleware(self, *args, **kwargs):
|
||||
"""Create a blueprint middleware from a decorated function."""
|
||||
|
||||
def register_middleware(_middleware):
|
||||
future_middleware = FutureMiddleware(_middleware, args, kwargs)
|
||||
self.middlewares.append(future_middleware)
|
||||
@@ -133,10 +289,12 @@ class Blueprint:
|
||||
|
||||
def exception(self, *args, **kwargs):
|
||||
"""Create a blueprint exception from a decorated function."""
|
||||
|
||||
def decorator(handler):
|
||||
exception = FutureException(handler, args, kwargs)
|
||||
self.exceptions.append(exception)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def static(self, uri, file_or_directory, *args, **kwargs):
|
||||
@@ -145,27 +303,120 @@ class Blueprint:
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop("name", "static")
|
||||
if not name.startswith(self.name + "."):
|
||||
name = "{}.{}".format(self.name, name)
|
||||
kwargs.update(name=name)
|
||||
|
||||
strict_slashes = kwargs.get("strict_slashes")
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
kwargs.update(strict_slashes=self.strict_slashes)
|
||||
|
||||
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=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["GET"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def post(self, uri, host=None):
|
||||
return self.route(uri, methods=["POST"], host=host)
|
||||
def post(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["POST"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def put(self, uri, host=None):
|
||||
return self.route(uri, methods=["PUT"], host=host)
|
||||
def put(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PUT"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def head(self, uri, host=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host)
|
||||
def head(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["HEAD"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def options(self, uri, host=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host)
|
||||
def options(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["OPTIONS"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def patch(self, uri, host=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host)
|
||||
def patch(
|
||||
self,
|
||||
uri,
|
||||
host=None,
|
||||
strict_slashes=None,
|
||||
stream=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["PATCH"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
stream=stream,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
def delete(self, uri, host=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host)
|
||||
def delete(
|
||||
self, uri, host=None, strict_slashes=None, version=None, name=None
|
||||
):
|
||||
return self.route(
|
||||
uri,
|
||||
methods=["DELETE"],
|
||||
host=host,
|
||||
strict_slashes=strict_slashes,
|
||||
version=version,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import os
|
||||
import types
|
||||
|
||||
from sanic.exceptions import PyFileError
|
||||
|
||||
|
||||
SANIC_PREFIX = "SANIC_"
|
||||
|
||||
|
||||
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 +31,21 @@ class Config(dict):
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megababies
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.WEBSOCKET_READ_LIMIT = 2 ** 16
|
||||
self.WEBSOCKET_WRITE_LIMIT = 2 ** 16
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
self.ACCESS_LOG = True
|
||||
|
||||
if load_env:
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
self.load_environment_vars(prefix=prefix)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -47,9 +65,10 @@ class Config(dict):
|
||||
"""
|
||||
config_file = os.environ.get(variable_name)
|
||||
if not config_file:
|
||||
raise RuntimeError('The environment variable %r is not set and '
|
||||
'thus configuration could not be loaded.' %
|
||||
variable_name)
|
||||
raise RuntimeError(
|
||||
"The environment variable %r is not set and "
|
||||
"thus configuration could not be loaded." % variable_name
|
||||
)
|
||||
return self.from_pyfile(config_file)
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
@@ -58,15 +77,20 @@ class Config(dict):
|
||||
|
||||
:param filename: an absolute path to the config file
|
||||
"""
|
||||
module = types.ModuleType('config')
|
||||
module = types.ModuleType("config")
|
||||
module.__file__ = filename
|
||||
try:
|
||||
with open(filename) as config_file:
|
||||
exec(compile(config_file.read(), filename, 'exec'),
|
||||
module.__dict__)
|
||||
exec(
|
||||
compile(config_file.read(), filename, "exec"),
|
||||
module.__dict__,
|
||||
)
|
||||
except IOError as e:
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
e.strerror = "Unable to load configuration file (%s)" % e.strerror
|
||||
raise
|
||||
except Exception as e:
|
||||
raise PyFileError(filename) from e
|
||||
|
||||
self.from_object(module)
|
||||
return True
|
||||
|
||||
@@ -90,3 +114,19 @@ class Config(dict):
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
self[config_key] = int(v)
|
||||
except ValueError:
|
||||
try:
|
||||
self[config_key] = float(v)
|
||||
except ValueError:
|
||||
self[config_key] = v
|
||||
|
||||
@@ -1 +1 @@
|
||||
HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'PATCH', 'DELETE')
|
||||
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# SimpleCookie
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -8,14 +9,12 @@ import string
|
||||
# Straight up copied this section of dark magic from SimpleCookie
|
||||
|
||||
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
|
||||
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
|
||||
|
||||
_Translator = {n: '\\%03o' % n
|
||||
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
|
||||
_Translator.update({
|
||||
ord('"'): '\\"',
|
||||
ord('\\'): '\\\\',
|
||||
})
|
||||
_Translator = {
|
||||
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
|
||||
}
|
||||
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
|
||||
|
||||
|
||||
def _quote(str):
|
||||
@@ -30,7 +29,7 @@ def _quote(str):
|
||||
return '"' + str.translate(_Translator) + '"'
|
||||
|
||||
|
||||
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Custom SimpleCookie
|
||||
@@ -47,33 +46,37 @@ class CookieJar(dict):
|
||||
super().__init__()
|
||||
self.headers = headers
|
||||
self.cookie_headers = {}
|
||||
self.header_key = "Set-Cookie"
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# If this cookie doesn't exist, add it to the header keys
|
||||
cookie_header = self.cookie_headers.get(key)
|
||||
if not cookie_header:
|
||||
if not self.cookie_headers.get(key):
|
||||
cookie = Cookie(key, value)
|
||||
cookie['path'] = '/'
|
||||
cookie_header = MultiHeader("Set-Cookie")
|
||||
self.cookie_headers[key] = cookie_header
|
||||
self.headers[cookie_header] = cookie
|
||||
cookie["path"] = "/"
|
||||
self.cookie_headers[key] = self.header_key
|
||||
self.headers.add(self.header_key, cookie)
|
||||
return super().__setitem__(key, cookie)
|
||||
else:
|
||||
self[key].value = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
if key not in self.cookie_headers:
|
||||
self[key] = ''
|
||||
self[key]['max-age'] = 0
|
||||
self[key] = ""
|
||||
self[key]["max-age"] = 0
|
||||
else:
|
||||
cookie_header = self.cookie_headers[key]
|
||||
del self.headers[cookie_header]
|
||||
# remove it from header
|
||||
cookies = self.headers.popall(cookie_header)
|
||||
for cookie in cookies:
|
||||
if cookie.key != key:
|
||||
self.headers.add(cookie_header, cookie)
|
||||
del self.cookie_headers[key]
|
||||
return super().__delitem__(key)
|
||||
|
||||
|
||||
class Cookie(dict):
|
||||
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
||||
|
||||
_keys = {
|
||||
"expires": "expires",
|
||||
"path": "Path",
|
||||
@@ -83,8 +86,9 @@ class Cookie(dict):
|
||||
"secure": "Secure",
|
||||
"httponly": "HttpOnly",
|
||||
"version": "Version",
|
||||
"samesite": "SameSite",
|
||||
}
|
||||
_flags = {'secure', 'httponly'}
|
||||
_flags = {"secure", "httponly"}
|
||||
|
||||
def __init__(self, key, value):
|
||||
if key in self._keys:
|
||||
@@ -98,43 +102,31 @@ class Cookie(dict):
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
return super().__setitem__(key, value)
|
||||
if value is not False:
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
output = ["%s=%s" % (self.key, _quote(self.value))]
|
||||
for key, value in self.items():
|
||||
if key == 'max-age':
|
||||
if key == "max-age":
|
||||
try:
|
||||
output.append('%s=%d' % (self._keys[key], value))
|
||||
output.append("%s=%d" % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append('%s=%s' % (self._keys[key], value))
|
||||
elif key == 'expires':
|
||||
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")
|
||||
))
|
||||
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:
|
||||
if self[key]:
|
||||
output.append(self._keys[key])
|
||||
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))
|
||||
output.append("%s=%s" % (self._keys[key], value))
|
||||
|
||||
return "; ".join(output).encode(encoding)
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# Header Trickery
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
|
||||
class MultiHeader:
|
||||
"""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
|
||||
|
||||
def encode(self):
|
||||
return self.name.encode()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
TRACEBACK_STYLE = '''
|
||||
from sanic.helpers import STATUS_CODES
|
||||
|
||||
|
||||
TRACEBACK_STYLE = """
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
@@ -47,6 +50,10 @@ TRACEBACK_STYLE = '''
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.tb-border {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.frame-descriptor {
|
||||
background-color: #e2eafb;
|
||||
}
|
||||
@@ -55,29 +62,44 @@ TRACEBACK_STYLE = '''
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
'''
|
||||
"""
|
||||
|
||||
TRACEBACK_WRAPPER_HTML = '''
|
||||
TRACEBACK_WRAPPER_HTML = """
|
||||
<html>
|
||||
<head>
|
||||
{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_LINE_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">
|
||||
File {0.filename}, line <i>{0.lineno}</i>,
|
||||
@@ -85,19 +107,34 @@ TRACEBACK_LINE_HTML = '''
|
||||
</p>
|
||||
<p class="frame-code"><code>{0.line}</code></p>
|
||||
</div>
|
||||
'''
|
||||
"""
|
||||
|
||||
INTERNAL_SERVER_ERROR_HTML = '''
|
||||
INTERNAL_SERVER_ERROR_HTML = """
|
||||
<h1>Internal Server Error</h1>
|
||||
<p>
|
||||
The server encountered an internal error and cannot complete
|
||||
your request.
|
||||
</p>
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
_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)
|
||||
|
||||
@@ -105,53 +142,157 @@ class SanicException(Exception):
|
||||
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(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
def __init__(self, message, method, allowed_methods):
|
||||
super().__init__(message)
|
||||
self.headers = dict()
|
||||
self.headers["Allow"] = ", ".join(allowed_methods)
|
||||
if method in ["HEAD", "PATCH", "PUT", "DELETE"]:
|
||||
self.headers["Content-Length"] = 0
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
status_code = 500
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(SanicException):
|
||||
status_code = 500
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
status_code = 404
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
self.relative_url = relative_url
|
||||
|
||||
|
||||
@add_status_code(408)
|
||||
class RequestTimeout(SanicException):
|
||||
status_code = 408
|
||||
"""The Web server (running the Web site) thinks that there has been too
|
||||
long an interval of time between 1) the establishment of an IP
|
||||
connection (socket) between the client and the server and
|
||||
2) the receipt of any data on that socket, so the server has dropped
|
||||
the connection. The socket connection has actually been lost - the Web
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(413)
|
||||
class PayloadTooLarge(SanicException):
|
||||
status_code = 413
|
||||
pass
|
||||
|
||||
|
||||
class HeaderNotFound(SanicException):
|
||||
status_code = 400
|
||||
class HeaderNotFound(InvalidUsage):
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
status_code = 416
|
||||
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
"Content-Range": "bytes */%s" % (content_range.total,)
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Range": "bytes */%s" % (content_range.total,),
|
||||
}
|
||||
|
||||
|
||||
@add_status_code(403)
|
||||
class Forbidden(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRangeType(ContentRangeError):
|
||||
pass
|
||||
|
||||
|
||||
class PyFileError(Exception):
|
||||
def __init__(self, file):
|
||||
super().__init__("could not execute config file %s", file)
|
||||
|
||||
|
||||
@add_status_code(401)
|
||||
class Unauthorized(SanicException):
|
||||
"""
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
|
||||
:param message: Message describing the exception.
|
||||
:param status_code: HTTP Status code.
|
||||
: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.",
|
||||
scheme="Basic",
|
||||
realm="Restricted Area")
|
||||
|
||||
# With a Digest auth-scheme, things are a bit more complicated:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="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.", scheme="Bearer")
|
||||
|
||||
# or, if you want to specify the realm:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = ['{!s}="{!s}"'.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 = 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)
|
||||
|
||||
@@ -1,43 +1,75 @@
|
||||
import sys
|
||||
from traceback import format_exc, extract_tb
|
||||
|
||||
from traceback import extract_tb, format_exc
|
||||
|
||||
from sanic.exceptions import (
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
INTERNAL_SERVER_ERROR_HTML,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
TRACEBACK_BORDER,
|
||||
TRACEBACK_LINE_HTML,
|
||||
TRACEBACK_STYLE,
|
||||
TRACEBACK_WRAPPER_HTML)
|
||||
from sanic.log import log
|
||||
from sanic.response import text, html
|
||||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
ContentRangeError,
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
SanicException,
|
||||
)
|
||||
from sanic.log import logger
|
||||
from sanic.response import html, text
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
handlers = None
|
||||
cached_handlers = None
|
||||
_missing = object()
|
||||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
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)
|
||||
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=exc_type.__name__,
|
||||
exc_value=exc_value,
|
||||
frame_html=''.join(frame_html),
|
||||
uri=request.url)
|
||||
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[exception] = handler
|
||||
self.handlers.append((exception, handler))
|
||||
|
||||
def lookup(self, exception):
|
||||
handler = self.cached_handlers.get(type(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
|
||||
@@ -47,45 +79,54 @@ class ErrorHandler:
|
||||
:param exception: Exception to handle
|
||||
:return: Response object
|
||||
"""
|
||||
handler = self.handlers.get(type(exception), self.default)
|
||||
handler = self.lookup(exception)
|
||||
response = None
|
||||
try:
|
||||
response = handler(request=request, exception=exception)
|
||||
if handler:
|
||||
response = handler(request, exception)
|
||||
if response is None:
|
||||
response = self.default(request, exception)
|
||||
except Exception:
|
||||
self.log(format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
response_message = (
|
||||
"Exception raised in exception handler " '"%s" for uri: %s'
|
||||
)
|
||||
logger.exception(response_message, handler.__name__, url)
|
||||
|
||||
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)
|
||||
return text(response_message % (handler.__name__, url), 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return text("An error occurred while handling an error", 500)
|
||||
return response
|
||||
|
||||
def log(self, message, level='error'):
|
||||
def log(self, message, level="error"):
|
||||
"""
|
||||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
Deprecated, do not use.
|
||||
"""
|
||||
getattr(log, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
try:
|
||||
url = repr(request.url)
|
||||
except AttributeError:
|
||||
url = "unknown"
|
||||
|
||||
response_message = "Exception occurred while handling uri: %s"
|
||||
logger.exception(response_message, url)
|
||||
|
||||
if issubclass(type(exception), SanicException):
|
||||
return text(
|
||||
'Error: {}'.format(exception),
|
||||
status=getattr(exception, 'status_code', 500),
|
||||
headers=getattr(exception, 'headers', dict())
|
||||
"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)
|
||||
@@ -93,47 +134,54 @@ class ErrorHandler:
|
||||
|
||||
class ContentRangeHandler:
|
||||
"""Class responsible for parsing request header"""
|
||||
__slots__ = ('start', 'end', 'size', 'total', 'headers')
|
||||
|
||||
__slots__ = ("start", "end", "size", "total", "headers")
|
||||
|
||||
def __init__(self, request, stats):
|
||||
self.total = stats.st_size
|
||||
_range = request.headers.get('Range')
|
||||
_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 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('-')))
|
||||
"%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)
|
||||
"'%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)
|
||||
"'%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)
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
else:
|
||||
# this case represents `Content-Range: bytes 5-`
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
else:
|
||||
if self.start is None:
|
||||
# this case represents `Content-Range: bytes -5`
|
||||
self.start = self.total - self.end
|
||||
self.end = self.total
|
||||
self.end = self.total - 1
|
||||
if self.start >= self.end:
|
||||
raise ContentRangeError(
|
||||
'Invalid for Content Range parameters', self)
|
||||
self.size = self.end - self.start
|
||||
"Invalid for Content Range parameters", self
|
||||
)
|
||||
self.size = self.end - self.start + 1
|
||||
self.headers = {
|
||||
'Content-Range': "bytes %s-%s/%s" % (
|
||||
self.start, self.end, self.total)}
|
||||
"Content-Range": "bytes %s-%s/%s"
|
||||
% (self.start, self.end, self.total)
|
||||
}
|
||||
|
||||
def __bool__(self):
|
||||
return self.size > 0
|
||||
|
||||
133
sanic/helpers.py
Normal file
133
sanic/helpers.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Defines basics of HTTP standard."""
|
||||
|
||||
STATUS_CODES = {
|
||||
100: b"Continue",
|
||||
101: b"Switching Protocols",
|
||||
102: b"Processing",
|
||||
200: b"OK",
|
||||
201: b"Created",
|
||||
202: b"Accepted",
|
||||
203: b"Non-Authoritative Information",
|
||||
204: b"No Content",
|
||||
205: b"Reset Content",
|
||||
206: b"Partial Content",
|
||||
207: b"Multi-Status",
|
||||
208: b"Already Reported",
|
||||
226: b"IM Used",
|
||||
300: b"Multiple Choices",
|
||||
301: b"Moved Permanently",
|
||||
302: b"Found",
|
||||
303: b"See Other",
|
||||
304: b"Not Modified",
|
||||
305: b"Use Proxy",
|
||||
307: b"Temporary Redirect",
|
||||
308: b"Permanent Redirect",
|
||||
400: b"Bad Request",
|
||||
401: b"Unauthorized",
|
||||
402: b"Payment Required",
|
||||
403: b"Forbidden",
|
||||
404: b"Not Found",
|
||||
405: b"Method Not Allowed",
|
||||
406: b"Not Acceptable",
|
||||
407: b"Proxy Authentication Required",
|
||||
408: b"Request Timeout",
|
||||
409: b"Conflict",
|
||||
410: b"Gone",
|
||||
411: b"Length Required",
|
||||
412: b"Precondition Failed",
|
||||
413: b"Request Entity Too Large",
|
||||
414: b"Request-URI Too Long",
|
||||
415: b"Unsupported Media Type",
|
||||
416: b"Requested Range Not Satisfiable",
|
||||
417: b"Expectation Failed",
|
||||
418: b"I'm a teapot",
|
||||
422: b"Unprocessable Entity",
|
||||
423: b"Locked",
|
||||
424: b"Failed Dependency",
|
||||
426: b"Upgrade Required",
|
||||
428: b"Precondition Required",
|
||||
429: b"Too Many Requests",
|
||||
431: b"Request Header Fields Too Large",
|
||||
451: b"Unavailable For Legal Reasons",
|
||||
500: b"Internal Server Error",
|
||||
501: b"Not Implemented",
|
||||
502: b"Bad Gateway",
|
||||
503: b"Service Unavailable",
|
||||
504: b"Gateway Timeout",
|
||||
505: b"HTTP Version Not Supported",
|
||||
506: b"Variant Also Negotiates",
|
||||
507: b"Insufficient Storage",
|
||||
508: b"Loop Detected",
|
||||
510: b"Not Extended",
|
||||
511: b"Network Authentication Required",
|
||||
}
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-7.1
|
||||
_ENTITY_HEADERS = frozenset(
|
||||
[
|
||||
"allow",
|
||||
"content-encoding",
|
||||
"content-language",
|
||||
"content-length",
|
||||
"content-location",
|
||||
"content-md5",
|
||||
"content-range",
|
||||
"content-type",
|
||||
"expires",
|
||||
"last-modified",
|
||||
"extension-header",
|
||||
]
|
||||
)
|
||||
|
||||
# According to https://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
_HOP_BY_HOP_HEADERS = frozenset(
|
||||
[
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def has_message_body(status):
|
||||
"""
|
||||
According to the following RFC message body and length SHOULD NOT
|
||||
be included in responses status 1XX, 204 and 304.
|
||||
https://tools.ietf.org/html/rfc2616#section-4.4
|
||||
https://tools.ietf.org/html/rfc2616#section-4.3
|
||||
"""
|
||||
return status not in (204, 304) and not (100 <= status < 200)
|
||||
|
||||
|
||||
def is_entity_header(header):
|
||||
"""Checks if the given header is an Entity Header"""
|
||||
return header.lower() in _ENTITY_HEADERS
|
||||
|
||||
|
||||
def is_hop_by_hop_header(header):
|
||||
"""Checks if the given header is a Hop By Hop header"""
|
||||
return header.lower() in _HOP_BY_HOP_HEADERS
|
||||
|
||||
|
||||
def remove_entity_headers(headers, allowed=("content-location", "expires")):
|
||||
"""
|
||||
Removes all the entity headers present in the headers given.
|
||||
According to RFC 2616 Section 10.3.5,
|
||||
Content-Location and Expires are allowed as for the
|
||||
"strong cache validator".
|
||||
https://tools.ietf.org/html/rfc2616#section-10.3.5
|
||||
|
||||
returns the headers without the entity headers
|
||||
"""
|
||||
allowed = set([h.lower() for h in allowed])
|
||||
headers = {
|
||||
header: value
|
||||
for header, value in headers.items()
|
||||
if not is_entity_header(header) or header.lower() in allowed
|
||||
}
|
||||
return headers
|
||||
57
sanic/log.py
57
sanic/log.py
@@ -1,3 +1,58 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger('sanic')
|
||||
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
loggers={
|
||||
"sanic.root": {"level": "INFO", "handlers": ["console"]},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["error_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.error",
|
||||
},
|
||||
"sanic.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["access_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access",
|
||||
},
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stderr,
|
||||
},
|
||||
"access_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "access",
|
||||
"stream": sys.stdout,
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
"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 %z]",
|
||||
"class": "logging.Formatter",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger("sanic.root")
|
||||
error_logger = logging.getLogger("sanic.error")
|
||||
access_logger = logging.getLogger("sanic.access")
|
||||
|
||||
158
sanic/reloader_helpers.py
Normal file
158
sanic/reloader_helpers.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from multiprocessing import Process
|
||||
from time import sleep
|
||||
|
||||
|
||||
def _iter_module_files():
|
||||
"""This iterates over all relevant Python files.
|
||||
|
||||
It goes through all
|
||||
loaded files from modules, all files in folders of already loaded modules
|
||||
as well as all files reachable through a package.
|
||||
"""
|
||||
# The list call is necessary on Python 3 in case the module
|
||||
# dictionary modifies during iteration.
|
||||
for module in list(sys.modules.values()):
|
||||
if module is None:
|
||||
continue
|
||||
filename = getattr(module, "__file__", None)
|
||||
if filename:
|
||||
old = None
|
||||
while not os.path.isfile(filename):
|
||||
old = filename
|
||||
filename = os.path.dirname(filename)
|
||||
if filename == old:
|
||||
break
|
||||
else:
|
||||
if filename[-4:] in (".pyc", ".pyo"):
|
||||
filename = filename[:-1]
|
||||
yield filename
|
||||
|
||||
|
||||
def _get_args_for_reloading():
|
||||
"""Returns the executable."""
|
||||
rv = [sys.executable]
|
||||
rv.extend(sys.argv)
|
||||
return rv
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
"""Create a new process and a subprocess in it with the same arguments as
|
||||
this one.
|
||||
"""
|
||||
args = _get_args_for_reloading()
|
||||
new_environ = os.environ.copy()
|
||||
new_environ["SANIC_SERVER_RUNNING"] = "true"
|
||||
cmd = " ".join(args)
|
||||
worker_process = Process(
|
||||
target=subprocess.call,
|
||||
args=(cmd,),
|
||||
kwargs=dict(shell=True, env=new_environ),
|
||||
)
|
||||
worker_process.start()
|
||||
return worker_process
|
||||
|
||||
|
||||
def kill_process_children_unix(pid):
|
||||
"""Find and kill child processes of a process (maximum two level).
|
||||
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
root_process_path = "/proc/{pid}/task/{pid}/children".format(pid=pid)
|
||||
if not os.path.isfile(root_process_path):
|
||||
return
|
||||
with open(root_process_path) as children_list_file:
|
||||
children_list_pid = children_list_file.read().split()
|
||||
|
||||
for child_pid in children_list_pid:
|
||||
children_proc_path = "/proc/%s/task/%s/children" % (
|
||||
child_pid,
|
||||
child_pid,
|
||||
)
|
||||
if not os.path.isfile(children_proc_path):
|
||||
continue
|
||||
with open(children_proc_path) as children_list_file_2:
|
||||
children_list_pid_2 = children_list_file_2.read().split()
|
||||
for _pid in children_list_pid_2:
|
||||
try:
|
||||
os.kill(int(_pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
continue
|
||||
try:
|
||||
os.kill(int(child_pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
continue
|
||||
|
||||
|
||||
def kill_process_children_osx(pid):
|
||||
"""Find and kill child processes of a process.
|
||||
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
subprocess.run(["pkill", "-P", str(pid)])
|
||||
|
||||
|
||||
def kill_process_children(pid):
|
||||
"""Find and kill child processes of a process.
|
||||
|
||||
:param pid: PID of parent process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
kill_process_children_osx(pid)
|
||||
elif sys.platform == "linux":
|
||||
kill_process_children_unix(pid)
|
||||
else:
|
||||
pass # should signal error here
|
||||
|
||||
|
||||
def kill_program_completly(proc):
|
||||
"""Kill worker and it's child processes and exit.
|
||||
|
||||
:param proc: worker process (process ID)
|
||||
:return: Nothing
|
||||
"""
|
||||
kill_process_children(proc.pid)
|
||||
proc.terminate()
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def watchdog(sleep_interval):
|
||||
"""Watch project files, restart worker process if a change happened.
|
||||
|
||||
:param sleep_interval: interval in second.
|
||||
:return: Nothing
|
||||
"""
|
||||
mtimes = {}
|
||||
worker_process = restart_with_reloader()
|
||||
signal.signal(
|
||||
signal.SIGTERM, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
signal.signal(
|
||||
signal.SIGINT, lambda *args: kill_program_completly(worker_process)
|
||||
)
|
||||
while True:
|
||||
for filename in _iter_module_files():
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
kill_process_children(worker_process.pid)
|
||||
worker_process.terminate()
|
||||
worker_process = restart_with_reloader()
|
||||
mtimes[filename] = mtime
|
||||
break
|
||||
|
||||
sleep(sleep_interval)
|
||||
285
sanic/request.py
285
sanic/request.py
@@ -1,19 +1,33 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import parse_qs, urlunparse
|
||||
|
||||
from httptools import parse_url
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import error_logger, logger
|
||||
|
||||
|
||||
try:
|
||||
from ujson import loads as json_loads
|
||||
except ImportError:
|
||||
from json import loads as json_loads
|
||||
if sys.version_info[:2] == (3, 5):
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import log
|
||||
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
|
||||
|
||||
|
||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
|
||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||
# > as type "application/octet-stream"
|
||||
@@ -35,41 +49,86 @@ class RequestParameters(dict):
|
||||
|
||||
class Request(dict):
|
||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||
|
||||
__slots__ = (
|
||||
'app', '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",
|
||||
"_socket",
|
||||
"_port",
|
||||
"__weakref__",
|
||||
"raw_url",
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
self.raw_url = url_bytes
|
||||
# TODO: Content-Encoding detection
|
||||
url_parsed = parse_url(url_bytes)
|
||||
self._parsed_url = parse_url(url_bytes)
|
||||
self.app = None
|
||||
self.url = url_parsed.path.decode('utf-8')
|
||||
|
||||
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 = []
|
||||
self.body_init()
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = None
|
||||
self.uri_template = None
|
||||
self._cookies = None
|
||||
self.stream = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return "<{0}>".format(self.__class__.__name__)
|
||||
return "<{0}: {1} {2}>".format(
|
||||
self.__class__.__name__, self.method, self.path
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
if self.transport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def body_init(self):
|
||||
self.body = []
|
||||
|
||||
def body_push(self, data):
|
||||
self.body.append(data)
|
||||
|
||||
def body_finish(self):
|
||||
self.body = b"".join(self.body)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
self.load_json()
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
def load_json(self, loads=json_loads):
|
||||
try:
|
||||
self.parsed_json = loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
@@ -79,9 +138,14 @@ class Request(dict):
|
||||
|
||||
:return: token related to request
|
||||
"""
|
||||
auth_header = self.headers.get('Authorization')
|
||||
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
|
||||
@@ -90,19 +154,22 @@ class Request(dict):
|
||||
self.parsed_form = RequestParameters()
|
||||
self.parsed_files = RequestParameters()
|
||||
content_type = self.headers.get(
|
||||
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||
"Content-Type", DEFAULT_HTTP_CONTENT_TYPE
|
||||
)
|
||||
content_type, parameters = parse_header(content_type)
|
||||
try:
|
||||
if content_type == 'application/x-www-form-urlencoded':
|
||||
if content_type == "application/x-www-form-urlencoded":
|
||||
self.parsed_form = RequestParameters(
|
||||
parse_qs(self.body.decode('utf-8')))
|
||||
elif content_type == 'multipart/form-data':
|
||||
parse_qs(self.body.decode("utf-8"))
|
||||
)
|
||||
elif content_type == "multipart/form-data":
|
||||
# TODO: Stream this instead of reading to/from memory
|
||||
boundary = parameters['boundary'].encode('utf-8')
|
||||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
boundary = parameters["boundary"].encode("utf-8")
|
||||
self.parsed_form, self.parsed_files = parse_multipart_form(
|
||||
self.body, boundary
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Failed when parsing form")
|
||||
error_logger.exception("Failed when parsing form")
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
@@ -118,32 +185,124 @@ class Request(dict):
|
||||
if self.parsed_args is None:
|
||||
if self.query_string:
|
||||
self.parsed_args = RequestParameters(
|
||||
parse_qs(self.query_string))
|
||||
parse_qs(self.query_string)
|
||||
)
|
||||
else:
|
||||
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)
|
||||
self._cookies = {name: cookie.value
|
||||
for name, cookie in cookies.items()}
|
||||
self._cookies = {
|
||||
name: cookie.value for name, cookie in cookies.items()
|
||||
}
|
||||
else:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_ip'):
|
||||
self._ip = self.transport.get_extra_info('peername')
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._port
|
||||
|
||||
File = namedtuple('File', ['type', 'body', 'name'])
|
||||
@property
|
||||
def socket(self):
|
||||
if not hasattr(self, "_socket"):
|
||||
self._get_address()
|
||||
return self._socket
|
||||
|
||||
def _get_address(self):
|
||||
self._socket = self.transport.get_extra_info("peername") or (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
self._ip = self._socket[0]
|
||||
self._port = self._socket[1]
|
||||
|
||||
@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):
|
||||
@@ -159,42 +318,52 @@ def parse_multipart_form(body, boundary):
|
||||
form_parts = body.split(boundary)
|
||||
for form_part in form_parts[1:-1]:
|
||||
file_name = None
|
||||
file_type = None
|
||||
content_type = "text/plain"
|
||||
content_charset = "utf-8"
|
||||
field_name = None
|
||||
line_index = 2
|
||||
line_end_index = 0
|
||||
while not line_end_index == -1:
|
||||
line_end_index = form_part.find(b'\r\n', line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode('utf-8')
|
||||
line_end_index = form_part.find(b"\r\n", line_index)
|
||||
form_line = form_part[line_index:line_end_index].decode("utf-8")
|
||||
line_index = line_end_index + 2
|
||||
|
||||
if not form_line:
|
||||
break
|
||||
|
||||
colon_index = form_line.index(':')
|
||||
form_header_field = form_line[0:colon_index]
|
||||
colon_index = form_line.index(":")
|
||||
form_header_field = form_line[0:colon_index].lower()
|
||||
form_header_value, form_parameters = parse_header(
|
||||
form_line[colon_index + 2:])
|
||||
form_line[colon_index + 2 :]
|
||||
)
|
||||
|
||||
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':
|
||||
file_type = form_header_value
|
||||
if form_header_field == "content-disposition":
|
||||
file_name = form_parameters.get("filename")
|
||||
field_name = form_parameters.get("name")
|
||||
elif form_header_field == "content-type":
|
||||
content_type = form_header_value
|
||||
content_charset = form_parameters.get("charset", "utf-8")
|
||||
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name or file_type:
|
||||
file = File(type=file_type, name=file_name, body=post_data)
|
||||
if field_name in files:
|
||||
files[field_name].append(file)
|
||||
if field_name:
|
||||
post_data = form_part[line_index:-4]
|
||||
if file_name:
|
||||
form_file = File(
|
||||
type=content_type, name=file_name, body=post_data
|
||||
)
|
||||
if field_name in files:
|
||||
files[field_name].append(form_file)
|
||||
else:
|
||||
files[field_name] = [form_file]
|
||||
else:
|
||||
files[field_name] = [file]
|
||||
value = post_data.decode(content_charset)
|
||||
if field_name in fields:
|
||||
fields[field_name].append(value)
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
else:
|
||||
value = post_data.decode('utf-8')
|
||||
if field_name in fields:
|
||||
fields[field_name].append(value)
|
||||
else:
|
||||
fields[field_name] = [value]
|
||||
logger.debug(
|
||||
"Form-data field does not have a 'name' parameter "
|
||||
"in the Content-Disposition header"
|
||||
)
|
||||
|
||||
return fields, files
|
||||
|
||||
@@ -1,138 +1,196 @@
|
||||
from functools import partial
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from ujson import dumps as json_dumps
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from aiofiles import open as open_async
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
404: b'Not Found',
|
||||
500: b'Internal Server Error',
|
||||
}
|
||||
ALL_STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
200: b'OK',
|
||||
201: b'Created',
|
||||
202: b'Accepted',
|
||||
203: b'Non-Authoritative Information',
|
||||
204: b'No Content',
|
||||
205: b'Reset Content',
|
||||
206: b'Partial Content',
|
||||
207: b'Multi-Status',
|
||||
208: b'Already Reported',
|
||||
226: b'IM Used',
|
||||
300: b'Multiple Choices',
|
||||
301: b'Moved Permanently',
|
||||
302: b'Found',
|
||||
303: b'See Other',
|
||||
304: b'Not Modified',
|
||||
305: b'Use Proxy',
|
||||
307: b'Temporary Redirect',
|
||||
308: b'Permanent Redirect',
|
||||
400: b'Bad Request',
|
||||
401: b'Unauthorized',
|
||||
402: b'Payment Required',
|
||||
403: b'Forbidden',
|
||||
404: b'Not Found',
|
||||
405: b'Method Not Allowed',
|
||||
406: b'Not Acceptable',
|
||||
407: b'Proxy Authentication Required',
|
||||
408: b'Request Timeout',
|
||||
409: b'Conflict',
|
||||
410: b'Gone',
|
||||
411: b'Length Required',
|
||||
412: b'Precondition Failed',
|
||||
413: b'Request Entity Too Large',
|
||||
414: b'Request-URI Too Long',
|
||||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
426: b'Upgrade Required',
|
||||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
503: b'Service Unavailable',
|
||||
504: b'Gateway Timeout',
|
||||
505: b'HTTP Version Not Supported',
|
||||
506: b'Variant Also Negotiates',
|
||||
507: b'Insufficient Storage',
|
||||
508: b'Loop Detected',
|
||||
510: b'Not Extended',
|
||||
511: b'Network Authentication Required'
|
||||
}
|
||||
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
|
||||
|
||||
|
||||
class HTTPResponse:
|
||||
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except BaseException:
|
||||
from json import dumps
|
||||
|
||||
def __init__(self, body=None, status=200, headers=None,
|
||||
content_type='text/plain', body_bytes=b''):
|
||||
# This is done in order to ensure that the JSON response is
|
||||
# kept consistent across both ujson and inbuilt json usage.
|
||||
json_dumps = partial(dumps, separators=(",", ":"))
|
||||
|
||||
|
||||
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""
|
||||
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"),
|
||||
)
|
||||
|
||||
return headers
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
if self._cookies is None:
|
||||
self._cookies = CookieJar(self.headers)
|
||||
return self._cookies
|
||||
|
||||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
"protocol",
|
||||
"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 = CIMultiDict(headers or {})
|
||||
self._cookies = None
|
||||
|
||||
async 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.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
|
||||
await self.protocol.drain()
|
||||
|
||||
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.protocol.push_data(headers)
|
||||
await self.protocol.drain()
|
||||
await self.streaming_fn(self)
|
||||
self.protocol.push_data(b"0\r\n\r\n")
|
||||
# no need to await drain here after this write, because it is the
|
||||
# very last thing we write and nothing needs to wait for it.
|
||||
|
||||
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()
|
||||
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = 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:
|
||||
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()
|
||||
self.body = self._encode_body(body)
|
||||
else:
|
||||
self.body = body_bytes
|
||||
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.headers = CIMultiDict(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''
|
||||
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 = b''
|
||||
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')))
|
||||
timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout
|
||||
|
||||
# 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)
|
||||
body = b""
|
||||
if has_message_body(self.status):
|
||||
body = self.body
|
||||
self.headers["Content-Length"] = self.headers.get(
|
||||
"Content-Length", len(self.body)
|
||||
)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
b'%b'
|
||||
b'%b\r\n'
|
||||
b'%b') % (
|
||||
self.headers["Content-Type"] = self.headers.get(
|
||||
"Content-Type", self.content_type
|
||||
)
|
||||
|
||||
if self.status in (304, 412):
|
||||
self.headers = remove_entity_headers(self.headers)
|
||||
|
||||
headers = self._parse_headers()
|
||||
|
||||
if self.status == 200:
|
||||
status = b"OK"
|
||||
else:
|
||||
status = 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',
|
||||
b"keep-alive" if keep_alive else b"close",
|
||||
timeout_header,
|
||||
headers,
|
||||
self.body
|
||||
body,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -142,86 +200,224 @@ class HTTPResponse:
|
||||
return self._cookies
|
||||
|
||||
|
||||
def json(body, status=200, headers=None, **kwargs):
|
||||
def json(
|
||||
body,
|
||||
status=200,
|
||||
headers=None,
|
||||
content_type="application/json",
|
||||
dumps=json_dumps,
|
||||
**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.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type="application/json")
|
||||
return HTTPResponse(
|
||||
dumps(body, **kwargs),
|
||||
headers=headers,
|
||||
status=status,
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
def text(body, status=200, headers=None,
|
||||
content_type="text/plain; charset=utf-8"):
|
||||
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
|
||||
:param content_type: the content type (string) of the response
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
return HTTPResponse(
|
||||
body, status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
|
||||
def raw(body, status=200, headers=None,
|
||||
content_type="application/octet-stream"):
|
||||
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
|
||||
:param content_type: the content type (string) of the response.
|
||||
"""
|
||||
return HTTPResponse(body_bytes=body, status=status, headers=headers,
|
||||
content_type=content_type)
|
||||
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.
|
||||
"""
|
||||
return HTTPResponse(body, status=status, headers=headers,
|
||||
content_type="text/html; charset=utf-8")
|
||||
return HTTPResponse(
|
||||
body,
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None, _range=None):
|
||||
async def file(
|
||||
location,
|
||||
status=200,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=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 filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
async with open_async(location, mode="rb") as _file:
|
||||
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)
|
||||
headers["Content-Range"] = "bytes %s-%s/%s" % (
|
||||
_range.start,
|
||||
_range.end,
|
||||
_range.total,
|
||||
)
|
||||
status = 206
|
||||
else:
|
||||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
return HTTPResponse(status=200,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
mime_type = mime_type or guess_type(filename)[0] or "text/plain"
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream,
|
||||
)
|
||||
|
||||
|
||||
def redirect(to, headers=None, status=302,
|
||||
content_type="text/html; charset=utf-8"):
|
||||
async def file_stream(
|
||||
location,
|
||||
status=200,
|
||||
chunk_size=4096,
|
||||
mime_type=None,
|
||||
headers=None,
|
||||
filename=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 filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
"Content-Disposition", 'attachment; filename="{}"'.format(filename)
|
||||
)
|
||||
filename = filename or 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)
|
||||
await response.write(content)
|
||||
else:
|
||||
while True:
|
||||
content = await _file.read(chunk_size)
|
||||
if len(content) < 1:
|
||||
break
|
||||
await 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,
|
||||
)
|
||||
status = 206
|
||||
return StreamingHTTPResponse(
|
||||
streaming_fn=_streaming_fn,
|
||||
status=status,
|
||||
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"
|
||||
):
|
||||
"""Abort execution and cause a 302 redirect (by default).
|
||||
|
||||
:param to: path or fully qualified URL to redirect to
|
||||
@@ -232,10 +428,12 @@ def redirect(to, headers=None, status=302,
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# URL Quote the URL before redirecting
|
||||
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
|
||||
|
||||
# According to RFC 7231, a relative URI is now permitted.
|
||||
headers['Location'] = to
|
||||
headers["Location"] = safe_to
|
||||
|
||||
return HTTPResponse(
|
||||
status=status,
|
||||
headers=headers,
|
||||
content_type=content_type)
|
||||
status=status, headers=headers, content_type=content_type
|
||||
)
|
||||
|
||||
325
sanic/router.py
325
sanic/router.py
@@ -1,28 +1,38 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sanic.exceptions import NotFound, InvalidUsage
|
||||
from sanic.exceptions import MethodNotSupported, NotFound
|
||||
from sanic.views import CompositionView
|
||||
|
||||
|
||||
Route = namedtuple(
|
||||
'Route',
|
||||
['handler', 'methods', 'pattern', 'parameters', 'name'])
|
||||
Parameter = namedtuple('Parameter', ['name', 'cast'])
|
||||
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
|
||||
)
|
||||
Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||
|
||||
REGEX_TYPES = {
|
||||
'string': (str, r'[^/]+'),
|
||||
'int': (int, r'\d+'),
|
||||
'number': (float, r'[0-9\\.]+'),
|
||||
'alpha': (str, r'[A-Za-z]+'),
|
||||
"string": (str, r"[^/]+"),
|
||||
"int": (int, r"\d+"),
|
||||
"number": (float, r"[0-9\\.]+"),
|
||||
"alpha": (str, r"[A-Za-z]+"),
|
||||
"path": (str, r"[^/].*?"),
|
||||
"uuid": (
|
||||
uuid.UUID,
|
||||
r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-"
|
||||
r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}",
|
||||
),
|
||||
}
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
|
||||
|
||||
def url_hash(url):
|
||||
return url.count('/')
|
||||
return url.count("/")
|
||||
|
||||
|
||||
class RouteExists(Exception):
|
||||
@@ -33,6 +43,10 @@ class RouteDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParameterNameConflicts(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Router:
|
||||
"""Router supports basic routing with parameters and method checks
|
||||
|
||||
@@ -59,25 +73,30 @@ class Router:
|
||||
also be passed in as the type. The argument given to the function will
|
||||
always be a string, independent of the type.
|
||||
"""
|
||||
|
||||
routes_static = None
|
||||
routes_dynamic = None
|
||||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r'<(.+?)>')
|
||||
parameter_pattern = re.compile(r"<(.+?)>")
|
||||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
self.routes_names = {}
|
||||
self.routes_static_files = {}
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
self.routes_always_check = []
|
||||
self.hosts = set()
|
||||
|
||||
def parse_parameter_string(self, parameter_string):
|
||||
@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]')
|
||||
For example::
|
||||
|
||||
parse_parameter_string('<param_one:[A-z]>')` ->
|
||||
('param_one', str, '[A-z]')
|
||||
|
||||
:param parameter_string: String to parse
|
||||
:return: tuple containing
|
||||
@@ -85,9 +104,13 @@ class Router:
|
||||
"""
|
||||
# We could receive NAME or NAME:PATTERN
|
||||
name = parameter_string
|
||||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
pattern = "string"
|
||||
if ":" in parameter_string:
|
||||
name, pattern = parameter_string.split(":", 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
@@ -95,26 +118,16 @@ class Router:
|
||||
|
||||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None):
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
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):
|
||||
def add(
|
||||
self,
|
||||
uri,
|
||||
methods,
|
||||
handler,
|
||||
host=None,
|
||||
strict_slashes=False,
|
||||
version=None,
|
||||
name=None,
|
||||
):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
@@ -122,6 +135,61 @@ class Router:
|
||||
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 version is not None:
|
||||
version = re.escape(str(version).strip("/").lstrip("v"))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip("/")])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
if not isinstance(host, str) and host is not None:
|
||||
# we have gotten back to the top of the recursion tree where the
|
||||
# host was originally a list. By now, we've processed the strict
|
||||
# slashes logic on the leaf nodes (the individual host strings in
|
||||
# the list of host)
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slashed_methods = self.routes_all.get(uri + "/", frozenset({}))
|
||||
unslashed_methods = self.routes_all.get(uri[:-1], frozenset({}))
|
||||
if isinstance(methods, Iterable):
|
||||
_slash_is_missing = all(
|
||||
method in slashed_methods for method in methods
|
||||
)
|
||||
_without_slash_is_missing = all(
|
||||
method in unslashed_methods for method in methods
|
||||
)
|
||||
else:
|
||||
_slash_is_missing = methods in slashed_methods
|
||||
_without_slash_is_missing = methods in unslashed_methods
|
||||
|
||||
slash_is_missing = not uri[-1] == "/" and not _slash_is_missing
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == "/" and not _without_slash_is_missing and not uri == "/"
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + "/", methods, handler, host, name)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host, name)
|
||||
|
||||
def _add(self, uri, methods, handler, host=None, name=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.
|
||||
:param name: user defined route name for url_for
|
||||
:return: Nothing
|
||||
"""
|
||||
if host is not None:
|
||||
@@ -131,55 +199,62 @@ class Router:
|
||||
|
||||
else:
|
||||
if not isinstance(host, Iterable):
|
||||
raise ValueError("Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host))
|
||||
raise ValueError(
|
||||
"Expected either string or Iterable of "
|
||||
"host strings, not {!r}".format(host)
|
||||
)
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
self.add(uri, methods, handler, host_, name)
|
||||
return
|
||||
else:
|
||||
# default host
|
||||
self.hosts.add('*')
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
if methods:
|
||||
methods = frozenset(methods)
|
||||
|
||||
parameters = []
|
||||
parameter_names = set()
|
||||
properties = {"unhashable": None}
|
||||
|
||||
def add_parameter(match):
|
||||
name = match.group(1)
|
||||
name, _type, pattern = self.parse_parameter_string(name)
|
||||
|
||||
parameter = Parameter(
|
||||
name=name, cast=_type)
|
||||
if name in parameter_names:
|
||||
raise ParameterNameConflicts(
|
||||
"Multiple parameter named <{name}> "
|
||||
"in route uri {uri}".format(name=name, uri=uri)
|
||||
)
|
||||
parameter_names.add(name)
|
||||
|
||||
parameter = Parameter(name=name, cast=_type)
|
||||
parameters.append(parameter)
|
||||
|
||||
# Mark the whole route as unhashable if it has the hash key in it
|
||||
if re.search('(^|[^^]){1}/', pattern):
|
||||
properties['unhashable'] = True
|
||||
if re.search(r"(^|[^^]){1}/", pattern):
|
||||
properties["unhashable"] = True
|
||||
# Mark the route as unhashable if it matches the hash key
|
||||
elif re.search(pattern, '/'):
|
||||
properties['unhashable'] = True
|
||||
elif re.search(r"/", pattern):
|
||||
properties["unhashable"] = True
|
||||
|
||||
return '({})'.format(pattern)
|
||||
return "({})".format(pattern)
|
||||
|
||||
pattern_string = re.sub(self.parameter_pattern, add_parameter, uri)
|
||||
pattern = re.compile(r'^{}$'.format(pattern_string))
|
||||
pattern = re.compile(r"^{}$".format(pattern_string))
|
||||
|
||||
def merge_route(route, methods, handler):
|
||||
# merge to the existing route when possible.
|
||||
if not route.methods or not methods:
|
||||
# method-unspecified routes are not mergeable.
|
||||
raise RouteExists(
|
||||
"Route already registered: {}".format(uri))
|
||||
raise RouteExists("Route already registered: {}".format(uri))
|
||||
elif route.methods.intersection(methods):
|
||||
# already existing method is not overloadable.
|
||||
duplicated = methods.intersection(route.methods)
|
||||
raise RouteExists(
|
||||
"Route already registered: {} [{}]".format(
|
||||
uri, ','.join(list(duplicated))))
|
||||
uri, ",".join(list(duplicated))
|
||||
)
|
||||
)
|
||||
if isinstance(route.handler, CompositionView):
|
||||
view = route.handler
|
||||
else:
|
||||
@@ -187,42 +262,67 @@ class Router:
|
||||
view.add(route.methods, route.handler)
|
||||
view.add(methods, handler)
|
||||
route = route._replace(
|
||||
handler=view, methods=methods.union(route.methods))
|
||||
handler=view, methods=methods.union(route.methods)
|
||||
)
|
||||
return route
|
||||
|
||||
if parameters:
|
||||
# TODO: This is too complex, we need to reduce the complexity
|
||||
if properties['unhashable']:
|
||||
if properties["unhashable"]:
|
||||
routes_to_check = self.routes_always_check
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
else:
|
||||
routes_to_check = self.routes_dynamic[url_hash(uri)]
|
||||
ndx, route = self.check_dynamic_route_exists(
|
||||
pattern, routes_to_check)
|
||||
pattern, routes_to_check, parameters
|
||||
)
|
||||
if ndx != -1:
|
||||
# Pop the ndx of the route, no dups of the same route
|
||||
routes_to_check.pop(ndx)
|
||||
else:
|
||||
route = self.routes_all.get(uri)
|
||||
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
# special prefix for static files
|
||||
is_static = False
|
||||
if name and name.startswith("_static_"):
|
||||
is_static = True
|
||||
name = name.split("_static_", 1)[-1]
|
||||
|
||||
if hasattr(handler, "__blueprintname__"):
|
||||
handler_name = "{}.{}".format(
|
||||
handler.__blueprintname__, name or handler.__name__
|
||||
)
|
||||
else:
|
||||
handler_name = name or getattr(handler, "__name__", None)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, handler.__name__)
|
||||
else:
|
||||
handler_name = getattr(handler, '__name__', None)
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name)
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
pattern=pattern,
|
||||
parameters=parameters,
|
||||
name=handler_name,
|
||||
uri=uri,
|
||||
)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if properties['unhashable']:
|
||||
if is_static:
|
||||
pair = self.routes_static_files.get(handler_name)
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_static_files[handler_name] = (uri, route)
|
||||
|
||||
else:
|
||||
pair = self.routes_names.get(handler_name)
|
||||
if not (pair and (pair[0] + "/" == uri or uri + "/" == pair[0])):
|
||||
self.routes_names[handler_name] = (uri, route)
|
||||
|
||||
if properties["unhashable"]:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
self.routes_dynamic[url_hash(uri)].append(route)
|
||||
@@ -230,9 +330,9 @@ class Router:
|
||||
self.routes_static[uri] = route
|
||||
|
||||
@staticmethod
|
||||
def check_dynamic_route_exists(pattern, routes_to_check):
|
||||
def check_dynamic_route_exists(pattern, routes_to_check, parameters):
|
||||
for ndx, route in enumerate(routes_to_check):
|
||||
if route.pattern == pattern:
|
||||
if route.pattern == pattern and route.parameters == parameters:
|
||||
return ndx, route
|
||||
else:
|
||||
return -1, None
|
||||
@@ -242,13 +342,25 @@ class Router:
|
||||
uri = host + uri
|
||||
try:
|
||||
route = self.routes_all.pop(uri)
|
||||
for handler_name, pairs in self.routes_names.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_names.pop(handler_name)
|
||||
break
|
||||
|
||||
for handler_name, pairs in self.routes_static_files.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_static_files.pop(handler_name)
|
||||
break
|
||||
|
||||
except KeyError:
|
||||
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||
|
||||
if route in self.routes_always_check:
|
||||
self.routes_always_check.remove(route)
|
||||
elif url_hash(uri) in self.routes_dynamic \
|
||||
and route in self.routes_dynamic[url_hash(uri)]:
|
||||
elif (
|
||||
url_hash(uri) in self.routes_dynamic
|
||||
and route in self.routes_dynamic[url_hash(uri)]
|
||||
):
|
||||
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||
else:
|
||||
self.routes_static.pop(uri)
|
||||
@@ -257,20 +369,20 @@ class Router:
|
||||
self._get.cache_clear()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name):
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:param kwargs: additional params, usually for static files
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
for uri, route in self.routes_all.items():
|
||||
if route.name == view_name:
|
||||
return uri, route
|
||||
if view_name == "static" or view_name.endswith(".static"):
|
||||
return self.routes_static_files.get(name, (None, None))
|
||||
|
||||
return (None, None)
|
||||
return self.routes_names.get(view_name, (None, None))
|
||||
|
||||
def get(self, request):
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
@@ -281,14 +393,25 @@ class Router:
|
||||
"""
|
||||
# No virtual hosts specified; default behavior
|
||||
if not self.hosts:
|
||||
return self._get(request.url, request.method, '')
|
||||
return self._get(request.path, request.method, "")
|
||||
# virtual hosts specified; try to match route to the host header
|
||||
try:
|
||||
return self._get(request.url, request.method,
|
||||
request.headers.get("Host", ''))
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("Host", "")
|
||||
)
|
||||
# try default hosts
|
||||
except NotFound:
|
||||
return self._get(request.url, request.method, '')
|
||||
return self._get(request.path, request.method, "")
|
||||
|
||||
def get_supported_methods(self, url):
|
||||
"""Get a list of supported methods for a url and optional host.
|
||||
|
||||
:param url: URL string (including host)
|
||||
:return: frozenset of supported methods
|
||||
"""
|
||||
route = self.routes_all.get(url)
|
||||
# if methods are None then this logic will prevent an error
|
||||
return getattr(route, "methods", None) or frozenset()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
@@ -299,12 +422,14 @@ class Router:
|
||||
:param method: request method
|
||||
:return: handler, arguments, keyword arguments
|
||||
"""
|
||||
url = host + url
|
||||
url = unquote(host + url)
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
method_not_supported = InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
method, url), status_code=405)
|
||||
method_not_supported = MethodNotSupported(
|
||||
"Method {} not allowed for URL {}".format(method, url),
|
||||
method=method,
|
||||
allowed_methods=self.get_supported_methods(url),
|
||||
)
|
||||
if route:
|
||||
if route.methods and method not in route.methods:
|
||||
raise method_not_supported
|
||||
@@ -330,12 +455,28 @@ class Router:
|
||||
# Route was found but the methods didn't match
|
||||
if route_found:
|
||||
raise method_not_supported
|
||||
raise NotFound('Requested URL {} not found'.format(url))
|
||||
raise NotFound("Requested URL {} not found".format(url))
|
||||
|
||||
kwargs = {p.name: p.cast(value)
|
||||
for value, p
|
||||
in zip(match.groups(1), route.parameters)}
|
||||
kwargs = {
|
||||
p.name: p.cast(value)
|
||||
for value, p in zip(match.groups(1), route.parameters)
|
||||
}
|
||||
route_handler = route.handler
|
||||
if hasattr(route_handler, 'handlers'):
|
||||
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, MethodNotSupported):
|
||||
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")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user