mirror of
https://github.com/moraroy/NonSteamLaunchers-On-Steam-Deck.git
synced 2026-06-13 12:15:04 +03:00
Compare commits
1044 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 215ed34388 | |||
| e5bfb5f501 | |||
| de1ae75c16 | |||
| b6aaff2959 | |||
| 07c1dc88e6 | |||
| bb73939339 | |||
| 34e0bf1f87 | |||
| 5504eda0f6 | |||
| 445cfafb32 | |||
| 3fd414caec | |||
| c0f0fcc68a | |||
| 067b9f861f | |||
| a558488b0d | |||
| 6399dff8cc | |||
| 79a7d2a44e | |||
| edfcb32c14 | |||
| 6e0c22411b | |||
| 4cf91b1320 | |||
| 809bd92694 | |||
| 09ce5ce133 | |||
| 08c46611d7 | |||
| 73511e820c | |||
| f62ad4a8cf | |||
| 4f552c5b3c | |||
| 579cdfdef0 | |||
| d9afa910c0 | |||
| 426fb47c24 | |||
| 6497af39aa | |||
| c1731b8765 | |||
| 03a5065f63 | |||
| cafbc388dc | |||
| 5043e420b6 | |||
| a6708f3b61 | |||
| 4c6338ff63 | |||
| bdf4f57b13 | |||
| 1345e75e35 | |||
| 3b9228c7e2 | |||
| 7d5aad2b32 | |||
| e9c657edf1 | |||
| ff32ed3ef9 | |||
| cd124132cc | |||
| be8bd97c55 | |||
| e9d635cf07 | |||
| c8a11ebd84 | |||
| 4fede75a6e | |||
| 65fb8debb8 | |||
| ee46328a73 | |||
| 800b6dbd04 | |||
| e64c6abdc5 | |||
| d2b4778919 | |||
| 66f0a99630 | |||
| 983ac18514 | |||
| dff6045347 | |||
| 3bab8fc87a | |||
| 0b0ff25a6b | |||
| bb993af3c2 | |||
| d6340ec9bc | |||
| 9f9e3bfc98 | |||
| 034def7b27 | |||
| e873366177 | |||
| 6601ab0b7f | |||
| 11a6bdab61 | |||
| 9ee61c9013 | |||
| 880b1e92eb | |||
| 524100bc02 | |||
| 5cf6e879a0 | |||
| 9fdc622f55 | |||
| 60613b3e92 | |||
| 56b91d49e6 | |||
| 2efe4f3843 | |||
| 007c54446f | |||
| 8cd30e72f3 | |||
| c6866c4a94 | |||
| 1570e1ae5d | |||
| 14ceba5d45 | |||
| 2249350107 | |||
| 34378716a9 | |||
| e7a918d62d | |||
| dcfa6cef44 | |||
| d1a371494a | |||
| dce8cb8b2b | |||
| c71d8f718c | |||
| 3a0f4c9c50 | |||
| 28916c9d68 | |||
| adee0aa156 | |||
| 80415a9009 | |||
| a876023ca9 | |||
| bdc94863cc | |||
| 2b760d765b | |||
| 55a4875450 | |||
| f113b0039c | |||
| d7cc6cee26 | |||
| 460002a926 | |||
| 4bdbf14adf | |||
| d3d4c763dc | |||
| ba8c868a90 | |||
| 56c47fd9a9 | |||
| de234c0a26 | |||
| ee7898cbd3 | |||
| 1636eceb49 | |||
| 8a54ca33e3 | |||
| cec4e691bb | |||
| fff5f08473 | |||
| 398614fe00 | |||
| ed4a00e6c4 | |||
| b93068acd4 | |||
| 3c2c7ce602 | |||
| 3a6450a9a3 | |||
| 784eac1d2a | |||
| 74ec8c9224 | |||
| 33c06c6947 | |||
| 7a5fb3a386 | |||
| 74b9305f91 | |||
| 7c436eeb1d | |||
| ee8b8abec9 | |||
| 1f5fc50062 | |||
| fadab483d3 | |||
| b12c04c94a | |||
| 0393f36329 | |||
| 04c11ecc9e | |||
| ec7325cb9f | |||
| dde79a4e4a | |||
| 2bd695a5ed | |||
| 1e18ae1647 | |||
| 5e9d1ccda2 | |||
| 8f8e9c3194 | |||
| 472b64c0cd | |||
| 88bc974128 | |||
| 04bea8d4bd | |||
| 6f70afe3f7 | |||
| 324b39e874 | |||
| d29ef42985 | |||
| 746dffbf65 | |||
| c5279623f0 | |||
| 1454fddef3 | |||
| 849380e3d4 | |||
| a443427e6b | |||
| bde517ca40 | |||
| b077d8f9c8 | |||
| 0767edd423 | |||
| 57f92375fa | |||
| 310c750071 | |||
| 920793c5b4 | |||
| 617476347a | |||
| a245b0305d | |||
| 723e12cf2c | |||
| dc8c2cb71c | |||
| bfe0217e1d | |||
| 0ea9002c70 | |||
| bce80a0739 | |||
| 32de4174bf | |||
| 321d030dee | |||
| c58077380b | |||
| f938ca1f99 | |||
| b975f5e6a7 | |||
| 2ae2fdb5ef | |||
| 0f3d2bb259 | |||
| 513a5ef0c5 | |||
| 0f6f8e9996 | |||
| a9f987ba69 | |||
| 20c0b0c484 | |||
| cf09aa6a1f | |||
| 594c7437bd | |||
| 041c6ea467 | |||
| dadceeb94e | |||
| 4c6e14eca1 | |||
| 813c89b983 | |||
| 09f56e83a0 | |||
| 526d4e534c | |||
| 2da400dea4 | |||
| f18c3114a4 | |||
| a9bdce3979 | |||
| 5dd1d89e9d | |||
| 904cca8d53 | |||
| d9a18a7509 | |||
| 22e823bba2 | |||
| 5cde12fa02 | |||
| 83b474a73d | |||
| 733fa0c9c9 | |||
| b929fb6f69 | |||
| 142ad46dff | |||
| 70024df474 | |||
| f3310223d0 | |||
| 71cc38ba14 | |||
| edfe68ce7e | |||
| 7b09d8ca3b | |||
| 6b9dd71206 | |||
| db9060c5a5 | |||
| 091cb02109 | |||
| 9826855e76 | |||
| 3a5fdc6461 | |||
| 68d2b9592c | |||
| 618eb573a8 | |||
| be6d19a1b3 | |||
| eac7cd4bd3 | |||
| a7faeda64f | |||
| 6edc8d9cf0 | |||
| e402a60d8b | |||
| 727a58fa50 | |||
| 31af2fc92e | |||
| baae26fc6e | |||
| 93c18eff2d | |||
| 0dce30534c | |||
| 318ef5a4f3 | |||
| 497a3960e7 | |||
| 59d1267758 | |||
| c313f63ecf | |||
| 5592775e19 | |||
| df68044842 | |||
| 3590ae73b9 | |||
| 3caf4370b7 | |||
| 015f3947c1 | |||
| 7243d1c45f | |||
| ead6d04dc7 | |||
| ec8f17770a | |||
| 5a4df4b833 | |||
| 5f1e4674d3 | |||
| 1879a642c1 | |||
| 1f6e10d4a4 | |||
| dd17e70668 | |||
| 072fbf46f5 | |||
| 8b14452f02 | |||
| a178fec3d2 | |||
| b0e6dcf36c | |||
| e06ff6048c | |||
| 7ad0b0efb6 | |||
| 04254d5917 | |||
| 0ce4c44063 | |||
| 26e6920d17 | |||
| 0bdeb11e2d | |||
| 0fd2efdd34 | |||
| 932bfd7b08 | |||
| e0a7affd35 | |||
| 42cd21014b | |||
| f4bd2509e2 | |||
| 87c437aeed | |||
| 16d31018e1 | |||
| 3e98bdbc04 | |||
| 6c604a564a | |||
| b20fa8244b | |||
| a4143d140a | |||
| 6090a8e97e | |||
| 643ca62f78 | |||
| cb766e43ca | |||
| 399b9e93c5 | |||
| 830c42e359 | |||
| 6f295a4b3a | |||
| 482982a51b | |||
| a61f71b675 | |||
| c2c056cb89 | |||
| bc0695bef5 | |||
| 1ffa3e6ccd | |||
| 25ef61fb6a | |||
| 3513ae5c60 | |||
| 70ca10ae44 | |||
| f28a1f19f2 | |||
| 6e9e634c19 | |||
| 2e6fbe73f7 | |||
| bfeac8e152 | |||
| 15f834627a | |||
| 6d4c078c63 | |||
| 36f84dfb81 | |||
| 101fb40aba | |||
| cfdf0c1990 | |||
| 745b015cac | |||
| 435fdfc77f | |||
| b35e0ef25a | |||
| 3ed3bf726f | |||
| 3ae952d59a | |||
| 68a4c2112e | |||
| e34ff1331b | |||
| 476ee78711 | |||
| ed57754ec5 | |||
| 9d1b119542 | |||
| f3b100679d | |||
| 305e2ffae2 | |||
| dd1b970cad | |||
| 688ac8e4d6 | |||
| 2e2d4eb25e | |||
| e1eeae1b2e | |||
| 9d8075027a | |||
| ea7c7f8f55 | |||
| 47fa3f1216 | |||
| d9e0f60517 | |||
| bd7da61b51 | |||
| 53dde899f8 | |||
| 638e9316a8 | |||
| a6b4144e9c | |||
| 64b78a471a | |||
| e62f1fdbb9 | |||
| 64fcafdcbc | |||
| b5027d6490 | |||
| bc7b8f01a4 | |||
| a038483e58 | |||
| 2b367835b3 | |||
| 01b2e5b47d | |||
| 0dc79e2a61 | |||
| e04590d936 | |||
| 79d295b59b | |||
| 4cbfac4ed8 | |||
| b8394b7158 | |||
| 7e54381769 | |||
| e40beda9b6 | |||
| 0f1f380e3f | |||
| 2f76a8bde1 | |||
| bf2aade65f | |||
| 2b1587cdf7 | |||
| 415af41d9f | |||
| 95651b33a8 | |||
| a2ea90fe12 | |||
| bd82999815 | |||
| 8ea4657bcf | |||
| 2f21c7524d | |||
| ff5502054d | |||
| 80e36750cd | |||
| 68d373eec3 | |||
| 3e0f134e0a | |||
| ece2714274 | |||
| 75679a28e0 | |||
| 27cc63875b | |||
| dc29a24c4f | |||
| cd41a4455c | |||
| 3e5e436f19 | |||
| ba5533f993 | |||
| fdb5b0165e | |||
| 8c4da6e024 | |||
| 831773b85b | |||
| dc36c54c39 | |||
| 5fd0565f43 | |||
| 53d4a90385 | |||
| 7ffa435ef2 | |||
| bc8be2c3be | |||
| 283f86900e | |||
| b52f4f7be4 | |||
| 1be8219893 | |||
| 7942ba5242 | |||
| 275ce3d65e | |||
| f0247dbb41 | |||
| aa36b2080d | |||
| c62a4c9742 | |||
| f9a23b1ddf | |||
| 4bb8308330 | |||
| 9d073b05ff | |||
| f5bf34cfbc | |||
| fa2481294e | |||
| 3e8ad27a1e | |||
| f35217c968 | |||
| c379365715 | |||
| 7f9d8d5407 | |||
| 9d2090922e | |||
| 7987a0378d | |||
| 960db23687 | |||
| be6d8755f1 | |||
| 077ffc423b | |||
| 23e1e48d3e | |||
| 9febdee1d0 | |||
| 749f6a01d7 | |||
| ecd098d0c0 | |||
| f736b777b6 | |||
| cf1d5c53f8 | |||
| 8e0f6711a9 | |||
| 4db0a8af71 | |||
| 23385ef6fe | |||
| 215f09bf23 | |||
| b1aac895d4 | |||
| 96a5e8310a | |||
| 85c25da2d3 | |||
| 883ef90dcf | |||
| 867edfb387 | |||
| 4ae0f085a6 | |||
| b633822d5d | |||
| 066aa8d400 | |||
| 64cb0d2294 | |||
| 153bac5f82 | |||
| 92a864213a | |||
| 0c05842304 | |||
| 698cb22da2 | |||
| f1e8202937 | |||
| 678ee9a205 | |||
| caefbbe064 | |||
| 4f35ac7c7b | |||
| 7ebf49a3a3 | |||
| 91fc7b0710 | |||
| 924cb804c3 | |||
| c8ccce9013 | |||
| bc806f8649 | |||
| cb50c0d7fe | |||
| f8b58e9cda | |||
| 0721fc2263 | |||
| d816d9974e | |||
| beb149eb2e | |||
| c3b2aea605 | |||
| a7a9c5ad71 | |||
| 583ba810da | |||
| 7cbb25084c | |||
| 95339cd5dc | |||
| 155a6057c8 | |||
| 6dadded06c | |||
| 9608c46d50 | |||
| 40c0877b3f | |||
| acbd715467 | |||
| a28d27f611 | |||
| 24e6baeaed | |||
| 548068382b | |||
| b83850c3fb | |||
| 71ebd08b9a | |||
| 9a406b683a | |||
| 1dd127b158 | |||
| 17abc1b527 | |||
| a906cb07c2 | |||
| adeb402b74 | |||
| 467174f53f | |||
| 272ffb1eaf | |||
| e7b816f215 | |||
| 20aa5d106c | |||
| 2a88bb41f8 | |||
| 8fbec24b61 | |||
| 70529ea0f8 | |||
| 26c7aa20af | |||
| e549013dbc | |||
| bd77c670ab | |||
| a6e5149dfd | |||
| c2f96faf39 | |||
| 3a123ad010 | |||
| 9dbeaf85f4 | |||
| 90bdd699e3 | |||
| 317d43bb12 | |||
| 27b15d4148 | |||
| 3a1377fd93 | |||
| 1d4a0d100f | |||
| 5bcea859ba | |||
| 91bb3499e4 | |||
| 2962a58ee5 | |||
| bbd700f1a6 | |||
| 3a70945d29 | |||
| e21a9c15b1 | |||
| a0de1440d3 | |||
| dfb7a6f0ca | |||
| 2ca8ac1d31 | |||
| 4a7f58e668 | |||
| 7d218e4f5e | |||
| 48b35856ee | |||
| 798361f7ab | |||
| 1218901515 | |||
| 194e4ef7b4 | |||
| 9da623d196 | |||
| 2b856c8b2b | |||
| 3e1bf13141 | |||
| 49ddec9c1f | |||
| 72eb7d9f46 | |||
| 45d783cbb8 | |||
| c9e788106c | |||
| a77e4c856e | |||
| 4f97d8c602 | |||
| 4ca0106498 | |||
| 7f0d156629 | |||
| 99ad4ace84 | |||
| 14e8f0d154 | |||
| 2821760b03 | |||
| b5ed726271 | |||
| 00400a5c97 | |||
| 443e54c6a9 | |||
| 18d84a4f58 | |||
| b03e422f29 | |||
| dc0ab11331 | |||
| 05008f1d92 | |||
| 21e3c1febf | |||
| 52e6be5730 | |||
| f42dd57cad | |||
| 48bca90858 | |||
| ab9a9af95a | |||
| ceca7df06a | |||
| cd339a8ed7 | |||
| d089c0cc11 | |||
| c5e3331f72 | |||
| fc5f4f61f5 | |||
| ce8eff25bf | |||
| dc39166f29 | |||
| 733e9df756 | |||
| 00700d41c4 | |||
| 7a38a909ef | |||
| 57155f687f | |||
| bcdfbef520 | |||
| 799b2fed89 | |||
| 99bab1b76e | |||
| 4bdf56eeb6 | |||
| cda76d423f | |||
| 43ebf5ac49 | |||
| 4aa5f63620 | |||
| 1566c21e9d | |||
| 56b0592832 | |||
| b2af72a7a5 | |||
| 9467e52d94 | |||
| acb366912c | |||
| 037b229b8e | |||
| 0b104541cd | |||
| c2aa08b62a | |||
| 505c72202c | |||
| 4f0a9238c8 | |||
| cdbb05a5e2 | |||
| 2dbf10f093 | |||
| 8978b5c075 | |||
| a3a06ddee1 | |||
| d13d11cc8d | |||
| 15f42879a7 | |||
| 796f19e7c7 | |||
| 1b37bc4618 | |||
| 4e3439a3c9 | |||
| 7a9ef1f953 | |||
| de2495be6b | |||
| eca61b0003 | |||
| de59763a38 | |||
| e68b0721fe | |||
| 9b6c11611a | |||
| 4e644a992d | |||
| 513aaafe0b | |||
| 497e94d004 | |||
| 1e66fed3c1 | |||
| 61df808a1e | |||
| 670a821e49 | |||
| 97c7c75a7d | |||
| 570ad10490 | |||
| 3e6f3726ce | |||
| da707265a4 | |||
| 61cdfb9e14 | |||
| 1abe5db209 | |||
| 328fac7f13 | |||
| 44b63f78d2 | |||
| 18bc0d433e | |||
| d143c32a65 | |||
| ba7435b955 | |||
| d43dab695b | |||
| 53c9a35177 | |||
| 1a148ebe3a | |||
| 43e4176103 | |||
| 1eccc366d7 | |||
| ab5fe48411 | |||
| 8a63e0e6a6 | |||
| 80ba5aa925 | |||
| e85d2aa3e8 | |||
| e0265375f4 | |||
| 37465abef9 | |||
| 12900d6292 | |||
| ac18f130b4 | |||
| 1ddbe4f140 | |||
| 6dad3e7c24 | |||
| 8976e3125b | |||
| 50a6599ce2 | |||
| 5c147c53dd | |||
| 51200565ba | |||
| 26a15b89ff | |||
| f409b434f7 | |||
| 7d691c9d2c | |||
| 81f59e5721 | |||
| 3f94878d42 | |||
| d1d971f63b | |||
| 81b74e2a5e | |||
| 8ca27302d7 | |||
| 4fcd082b32 | |||
| 023d49d107 | |||
| 042aefa69c | |||
| 2e8b92c8c9 | |||
| 310ee129b1 | |||
| 76cc80118f | |||
| 10068bb4a1 | |||
| e67a535df8 | |||
| a3e8e43335 | |||
| 1fedecba0c | |||
| 1e60db366c | |||
| 585c7ad7f1 | |||
| 63a4da7e41 | |||
| ae9af85f71 | |||
| 93b8b1b039 | |||
| 626cf7858a | |||
| 2f978dc3f5 | |||
| 0abea29415 | |||
| a5fe05ba8e | |||
| 67e6d9f177 | |||
| 6c219e64bd | |||
| b8b40f9baa | |||
| 83bb83e74b | |||
| 484def960e | |||
| bf04b72b6a | |||
| 8869f36706 | |||
| 8380bd6ea4 | |||
| 8bb8ffb403 | |||
| 2259c59a03 | |||
| a0d09eb796 | |||
| a9b75d4126 | |||
| ad833b1ff8 | |||
| 7f7203a2ed | |||
| c6c8690283 | |||
| 6b88ed7149 | |||
| e8bb84f32a | |||
| 73e0cd3bcc | |||
| f76ff722c4 | |||
| 55fda6814a | |||
| aac0081fbb | |||
| e78fb345aa | |||
| 030f2ad5d2 | |||
| 8dda0c3f5b | |||
| f5c3b65423 | |||
| d9eba32cd6 | |||
| 1bccb2425d | |||
| de7098be29 | |||
| b2fd40cf3e | |||
| 1e860a27b5 | |||
| 78e97db5e5 | |||
| 5112a76dfd | |||
| aa07f7e060 | |||
| bdd4186c54 | |||
| 90dc8c5bdd | |||
| f7bdca5540 | |||
| e47dc9060c | |||
| a30190562a | |||
| 828e24b776 | |||
| 4f319c3972 | |||
| 5b4f07be6b | |||
| b2b11abc9e | |||
| c2e766607b | |||
| b2ad6c3f5b | |||
| 10e9412112 | |||
| 9ee7a12dc3 | |||
| 7ad591c2b4 | |||
| 70e941dcf8 | |||
| 3cb58c3121 | |||
| e14cf4e12b | |||
| 0a55968fb8 | |||
| 422dd824b9 | |||
| 026106274b | |||
| e8eb30bdc8 | |||
| 84ea22bd02 | |||
| 84f9b5cd56 | |||
| ba187f86e6 | |||
| 79984e85b9 | |||
| fe6214941b | |||
| 6ede5a24b8 | |||
| 0f422a9d2a | |||
| 18c244a817 | |||
| fbf8b13f85 | |||
| b95c45a942 | |||
| ff3c7e8936 | |||
| 53699425f0 | |||
| 376b292942 | |||
| 975d05c9e6 | |||
| d23be1dca0 | |||
| 916272bfd0 | |||
| c0b01995b8 | |||
| a185521560 | |||
| 72ef421c49 | |||
| 086a01ac20 | |||
| b48a3ab64a | |||
| 212abb22c7 | |||
| c88d7dea0d | |||
| 9688709a3d | |||
| 9fe131a93f | |||
| ac16720b8b | |||
| 5f1c97013d | |||
| 37ecab3575 | |||
| 3f3e0571eb | |||
| ed0dca546f | |||
| d2b884b6c7 | |||
| e0473f06d5 | |||
| 2b87e118fe | |||
| 0570ba9ca8 | |||
| 7bf2296eb7 | |||
| d8d1249546 | |||
| 8ec9b03a77 | |||
| 4f53508667 | |||
| dd604eab73 | |||
| 6a7c31719e | |||
| 19e0b802a2 | |||
| 61fc835c8f | |||
| 361c7c35ce | |||
| 5d5e712a8f | |||
| 065c662216 | |||
| ed13418124 | |||
| 9c00b2a22c | |||
| 087a8296d8 | |||
| 57f6d436fa | |||
| 3251636369 | |||
| 43200bc737 | |||
| 2f1e19affb | |||
| 5f219dc33c | |||
| 21fe359961 | |||
| 6245118cf2 | |||
| 92990ff86a | |||
| a033100a47 | |||
| 60a28268ae | |||
| 15109e93d1 | |||
| 6b92f20a40 | |||
| 39195008a9 | |||
| 93572c981f | |||
| b202a81346 | |||
| 741e3951d0 | |||
| f51d3fbbab | |||
| 1085afbd12 | |||
| 6f74d9451b | |||
| 1cf770c09e | |||
| 5ae944be49 | |||
| bb8ed151d6 | |||
| 10db206c24 | |||
| 18123b0cf7 | |||
| 0653263313 | |||
| 6d6a263735 | |||
| 54988b1b41 | |||
| d53a238583 | |||
| f75dbf490d | |||
| 087adfe509 | |||
| cfe94755ee | |||
| 327b7ab87d | |||
| 841ba44a09 | |||
| 0b40ebf3c0 | |||
| ad93f52988 | |||
| b8555e6efe | |||
| 504032e41d | |||
| 45b822b565 | |||
| 4d7dcbcb5a | |||
| fbf068a79f | |||
| aa8ca2470d | |||
| 7e498278b2 | |||
| aab3f6c4ab | |||
| 9325e1afe7 | |||
| 50393c8eda | |||
| 325125852c | |||
| e867837705 | |||
| e49c0dff60 | |||
| 2843306833 | |||
| 9535b6f11f | |||
| 08512e9541 | |||
| 047144e4bf | |||
| 48ca52cd31 | |||
| ffd8eeecfe | |||
| bbd1ca0f11 | |||
| 3d815e165e | |||
| 840b0977c4 | |||
| 1d0a2505af | |||
| 5b26e17d30 | |||
| dde0afcfe2 | |||
| 6cc43cbed5 | |||
| 225962e8ab | |||
| c43fdeaf8c | |||
| 2de5de559d | |||
| b2c6812e23 | |||
| d9e341c998 | |||
| 6fb31a4226 | |||
| a1d28f8598 | |||
| 2421500709 | |||
| be6d41420d | |||
| cee929d953 | |||
| 7e7b92fa84 | |||
| 718442653a | |||
| f2f53a933b | |||
| c1cccbd2f7 | |||
| 82c2f0fdac | |||
| 586a5a9467 | |||
| 658d6d76a3 | |||
| 289f2a7fd6 | |||
| e7cf3adf7f | |||
| 001d645c69 | |||
| 17b9d816fb | |||
| 959613e408 | |||
| cfcbfc22df | |||
| a8dcf988df | |||
| 2fe292083b | |||
| fad93ae52a | |||
| c171463254 | |||
| eef4695d8d | |||
| 65c16c6582 | |||
| 9535568470 | |||
| a94429b1bd | |||
| e288756ead | |||
| bb04d1098c | |||
| d120f7cb23 | |||
| 22fde21e9c | |||
| 55e40bfff8 | |||
| 7941fc5729 | |||
| 4b2acb2fd0 | |||
| 998e5af150 | |||
| f68a2d5893 | |||
| cff2d6c7c9 | |||
| bd9b330b1f | |||
| 1107eadf0e | |||
| a41562fb7a | |||
| 6c677f6512 | |||
| 16996b7a89 | |||
| 888e7fc84d | |||
| 89003132f6 | |||
| 6de1d90b28 | |||
| f86e78d920 | |||
| a79d778786 | |||
| 47f4e19513 | |||
| 0969a6441c | |||
| 8313381770 | |||
| e04115fc7d | |||
| c287de25fc | |||
| 2505804d2b | |||
| 30310735c1 | |||
| 4172e7a107 | |||
| 080f35a269 | |||
| cf10965f91 | |||
| 6dfcef1ebc | |||
| b667219adc | |||
| 8f198f616b | |||
| 68e34cfde8 | |||
| bb38286145 | |||
| c352008260 | |||
| af6f46e046 | |||
| 56b4243b03 | |||
| c241fa2401 | |||
| 3f7576cfda | |||
| 9663e83ade | |||
| c07decc9ea | |||
| 7a66485ccf | |||
| 51592d6b12 | |||
| eae8523fa0 | |||
| 4b17e0e484 | |||
| 1c0a2225a1 | |||
| 123bc80b2f | |||
| fcdb93cd7b | |||
| 01ef7a4d26 | |||
| 97f49575cc | |||
| 1efb184d39 | |||
| adf4579cc9 | |||
| a9852026af | |||
| 7f419ab0b0 | |||
| 7402a158c2 | |||
| 45e38d36af | |||
| b421643531 | |||
| a88b3a0b90 | |||
| 2bfe14a6a7 | |||
| f8f91258e9 | |||
| 2c579109e7 | |||
| a7de732b02 | |||
| 41dee335a8 | |||
| f2c1545c9e | |||
| 5822cf0705 | |||
| 1d905ea1af | |||
| 8181303f61 | |||
| 3e86a6539d | |||
| a0a2a699c8 | |||
| 105855ffda | |||
| df4168a87e | |||
| 878d716b20 | |||
| a739b000bb | |||
| 414c618318 | |||
| 871bce2c17 | |||
| 40310df41a | |||
| cd518c3b1b | |||
| 5c12bb3814 | |||
| 21bd1394da | |||
| f5e835175a | |||
| 8689eaba68 | |||
| 41bd6242a2 | |||
| b9110ae1d1 | |||
| 510eff83b6 | |||
| b0e1a78860 | |||
| 5df303cc33 | |||
| 1de33397b7 | |||
| c4d9add919 | |||
| 4d55342209 | |||
| fb198431f5 | |||
| c63f536032 | |||
| 60a42ea2e9 | |||
| 4ff1d27e48 | |||
| 8a75c42100 | |||
| 2f82847f74 | |||
| 00f85efde4 | |||
| 97ce677083 | |||
| ce67401e34 | |||
| 521bb3a916 | |||
| 31ec5936a0 | |||
| 53a803b447 | |||
| b52847c786 | |||
| 4bc8441ab1 | |||
| 9cdb5a4828 | |||
| 5663a0b950 | |||
| 29c846cb76 | |||
| 414e92d162 | |||
| 3c65cbfd15 | |||
| 00bcb32904 | |||
| dab631a126 | |||
| 934e719a23 | |||
| c346dc35d6 | |||
| ee8a1d6f68 | |||
| 7069c41855 | |||
| a8c13d41ad | |||
| 9a02c133c1 | |||
| 83c3a3bfa1 | |||
| 0918b5fe94 | |||
| b107c5b5fe | |||
| 49cd0788bc | |||
| 2f200aefb6 | |||
| 148e0cb9bb | |||
| 0e0034ebdf | |||
| 3456c6c8cd | |||
| 4db4e2ecdf | |||
| 4b860d14c4 | |||
| 67ff873969 | |||
| 0557aee20a | |||
| 8bb2e01b2a | |||
| 903fec3834 | |||
| c435c1aa15 | |||
| de4ebc1bac | |||
| 7efb2d9da3 | |||
| 3dfa8e7637 | |||
| 2d0705f940 | |||
| 3676f9a37c | |||
| 50ba58bd3b | |||
| 576ed66de0 | |||
| 2e5e8673e3 | |||
| 1f4510608f | |||
| fcde7d9c0d | |||
| c33fbb36f2 | |||
| f820d1dd58 | |||
| 95132cf63b | |||
| c92defb786 | |||
| 516992e797 | |||
| a2a6687758 | |||
| f1f1c61f63 | |||
| a879402dde | |||
| a25c6bcc70 | |||
| ce48851169 | |||
| 8ba5bb7f94 | |||
| ad248dd3be | |||
| 3839e79934 | |||
| b4ec47c819 | |||
| 838c763b95 | |||
| 2d75e133a6 | |||
| 1aeb618247 | |||
| a19525f7bf | |||
| 01902298ef | |||
| 38517effeb | |||
| bb2315fe31 | |||
| 010a965266 | |||
| 4ed5f64886 | |||
| fcd0d8a1ae | |||
| 71bf91080b | |||
| 2c279024d2 | |||
| f86356c7b6 | |||
| fc9f546505 | |||
| d4afc8c3d6 | |||
| 6cf309b8d2 | |||
| 236fff0ed8 | |||
| 9740bbe4c4 | |||
| 9668299bd8 | |||
| fdc3014541 | |||
| 2c68256e85 | |||
| 2875707e92 | |||
| 89498f0ac2 | |||
| 654d6bdee9 | |||
| 16267ee886 | |||
| 0ddeebc98f | |||
| aceb9a03ea | |||
| 32a89c0fb8 | |||
| 76b691e8c2 | |||
| f8453c66f1 | |||
| e6a70de97f | |||
| 00bb19d9db | |||
| e7abfe56f9 | |||
| 74cf362e4e | |||
| 3847a6756f | |||
| ea871c22e8 | |||
| 69d0941269 | |||
| d25a38ecd3 | |||
| b2ab96975d | |||
| 5c17c086c0 | |||
| a93336eb85 | |||
| 462462e196 | |||
| a975ce512f | |||
| 39cdfc0d4b | |||
| b828b4a9e9 | |||
| cea403eb89 | |||
| ca03c797fa | |||
| 33f9e62b0b | |||
| a8f9c19cfb | |||
| 73d386d0ff | |||
| 18e84770a9 | |||
| f79b4efc8e | |||
| c29162231d | |||
| 7a1b562fde | |||
| 5203bd123d | |||
| 4b82ce2e8b | |||
| 7c770dbdba | |||
| 2cf3f336db | |||
| 59eaa5e32d | |||
| 3bcf1eb0bd | |||
| aa4d4d29fd | |||
| 124873d1c5 | |||
| bc60bad31d | |||
| 46b535e243 | |||
| a3bee506ca | |||
| 1aa602cff7 | |||
| 26176376e3 | |||
| 0e08f2009f | |||
| 41565a298b | |||
| 4e608c7d77 | |||
| ee3bfdca22 | |||
| d772d8e524 | |||
| 306e59d6f7 | |||
| 60cbfe85a9 | |||
| 25d59a8237 | |||
| f26978609a | |||
| 135cd26d04 | |||
| e98f46502d | |||
| 4288e7d8d7 | |||
| 5f8f9438ee | |||
| 1f628b2c54 | |||
| b3333347af | |||
| d4c725e87e | |||
| 3bf35a1d0a | |||
| e5b1a18367 | |||
| 21f79556ac | |||
| c32545fbdc | |||
| bb0bdfed79 | |||
| 3422b13fdb | |||
| 43034167e8 | |||
| 9d78ebc396 | |||
| af2d7bad52 | |||
| 8d8be0e2bf | |||
| ed5c145014 | |||
| b228384352 | |||
| 3d7f5698ab | |||
| 69b9030179 | |||
| cf55a61421 | |||
| b1173b2b9a | |||
| 52609692d6 | |||
| 53103f8aac | |||
| 6f0f66a8da | |||
| 78938fe1d2 | |||
| 81939aedfe | |||
| c4f1397629 | |||
| 64ae4b05e7 | |||
| 2948ba9c65 | |||
| 7ba8903554 | |||
| 81a863b968 | |||
| ec492481f7 | |||
| 6dae54a948 | |||
| c529e86384 | |||
| d2add81605 | |||
| dd98acc46b | |||
| 9bc0fa1d3e | |||
| 2246954058 | |||
| 2b68cd0477 |
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(backlog:*)",
|
||||
"Bash(ruff:*)",
|
||||
"Bash(pytest:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(pre-commit:*)",
|
||||
"Bash(uv:*)",
|
||||
"mcp__context7__get-library-docs",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__serena__activate_project",
|
||||
"mcp__serena__check_onboarding_performed",
|
||||
"mcp__serena__find_symbol",
|
||||
"mcp__serena__get_symbols_overview",
|
||||
"mcp__serena__insert_after_symbol",
|
||||
"mcp__serena__insert_before_symbol",
|
||||
"mcp__serena__list_dir",
|
||||
"mcp__serena__onboarding",
|
||||
"mcp__serena__replace_symbol_body",
|
||||
"mcp__serena__search_for_pattern",
|
||||
"mcp__serena__think_about_collected_information",
|
||||
"mcp__serena__think_about_whether_you_are_done",
|
||||
"mcp__serena__write_memory"
|
||||
],
|
||||
"deny": [ ]
|
||||
}
|
||||
// TODO: instate at a later time
|
||||
// "hooks": {
|
||||
// "PostToolUse": [
|
||||
// {
|
||||
// "matcher": "Edit|MultiEdit|Write",
|
||||
// "hooks": [
|
||||
// {
|
||||
// "type": "command",
|
||||
// "command": "git diff --name-only HEAD | xargs -r pre-commit run --files"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ github: moraroy
|
||||
patreon: moraroy
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: moraroy
|
||||
buy_me_a_coffee: moraroy
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: moraroy
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: "\U0001F41E Bug Report (Linux / SteamOS)"
|
||||
about: Report a bug or problem that you have experienced with NonSteamLaunchers
|
||||
title: bug
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Describe the Bug
|
||||
A clear and concise description of the issue you’re experiencing with **NonSteamLaunchers (NSL)**.
|
||||
Include any error messages, crashes, or unexpected behavior.
|
||||
|
||||
**Which version are you using?**
|
||||
- [ ] NonSteamLaunchers.desktop (Desktop Version)
|
||||
- [ ] Decky Plugin (`NonSteamLaunchersDecky`)
|
||||
|
||||
---
|
||||
|
||||
## 🔁 To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
1. (Optional) Switch to Desktop Mode.
|
||||
2. Launch NSL using your selected method above.
|
||||
3. Select and install one or more launchers (Epic, EA, GOG, etc.)
|
||||
4. Wait for the installation to complete or for any errors to appear.
|
||||
5. (If applicable) Return to Gaming Mode and attempt to launch the installed launcher(s).
|
||||
6. Observe the issue.
|
||||
|
||||
*Example:*
|
||||
> Epic Games Launcher installs successfully but fails to open in Gaming Mode (Proton GE 9-5).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Expected Behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
Example:
|
||||
> The Epic Games Launcher should open in Gaming Mode and allow login.
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Screenshots / Logs
|
||||
If applicable, add screenshots or logs to help explain your problem.
|
||||
|
||||
**Log locations:**
|
||||
- Script/Desktop version:
|
||||
`/home/deck/Downloads/NonSteamLaunchers-install.log`
|
||||
- Decky Plugin version:
|
||||
`/home/deck/homebrew/logs/NonSteamLaunchers/`
|
||||
|
||||
Please attach relevant sections or errors from the logs if possible.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 System Information
|
||||
Please complete the following details:
|
||||
|
||||
**Device Type**
|
||||
- [ ] Steam Deck
|
||||
- [ ] Desktop PC
|
||||
- [ ] Laptop
|
||||
- [ ] Other: (please specify)
|
||||
|
||||
**Operating System**
|
||||
- [ ] SteamOS (Arch-based)
|
||||
- [ ] Other Linux Distro (please specify)
|
||||
|
||||
**Details**
|
||||
- **SteamOS Version:** e.g. 3.6.5
|
||||
- **Kernel Version:** (run `uname -r` in Konsole)
|
||||
- **Proton Version Used:** e.g. Proton GE 9-5 / Proton Experimental
|
||||
- **Launchers Affected:** Epic / GOG / EA / Battle.net / etc.
|
||||
- **NSL Version:** (from GitHub release tag, Decky Plugin version, or script header)
|
||||
- **Install Method:** Script (.sh) / Desktop Launcher (.desktop) / Decky Plugin
|
||||
|
||||
---
|
||||
|
||||
## 💬 Additional Context
|
||||
Add any other context about the problem here, such as:
|
||||
- [ ] Occurs only in Gaming Mode
|
||||
- [ ] Occurs only in Desktop Mode
|
||||
- [ ] Reinstalling NSL did not fix the issue
|
||||
- [ ] Clearing compatibility data did not help
|
||||
- [ ] I am using a custom Proton build
|
||||
|
||||
Provide any temporary fixes or additional details below:
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: "\U0001F4A1 Feature Request (Linux / SteamOS)"
|
||||
about: Whats the big idea?
|
||||
title: enhancement
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## 💭 Is your feature request related to a problem?
|
||||
A clear and concise description of what the problem is.
|
||||
Example:
|
||||
> I'm always frustrated when NonSteamLaunchers creates duplicates in the scanner!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Describe the Solution You’d Like
|
||||
A clear and concise description of what you’d like to see added or improved.
|
||||
Example:
|
||||
> Add a booster seat feature so NSL can launch into space.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Describe Alternatives You’ve Considered
|
||||
List any alternative approaches, workarounds, or existing tools you’ve tried.
|
||||
Example:
|
||||
> I currently use the Decky Plugin version to manage launchers, but it could be better!
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Scope of Feature
|
||||
Please indicate what area of NSL your request affects:
|
||||
|
||||
- [ ] Desktop (`NonSteamLaunchers.desktop`)
|
||||
- [ ] Decky Plugin (`NonSteamLaunchersDecky`)
|
||||
- [ ] Proton / Compatibility Integration
|
||||
- [ ] UI / User Experience
|
||||
- [ ] Other (please specify below)
|
||||
|
||||
---
|
||||
|
||||
## 🧰 System Context
|
||||
(Optional, but helpful for context)
|
||||
- **Device:** Steam Deck / Desktop PC / Laptop / Other
|
||||
- **OS / Distro:** SteamOS (Arch-based) / Other Linux Distro (specify)
|
||||
- **Current NSL Version:** (from GitHub release tag, Decky Plugin version, or script header)
|
||||
|
||||
---
|
||||
|
||||
## 💬 Additional Context
|
||||
Add any other context, mockups, or screenshots related to your suggestion here.
|
||||
You can also include:
|
||||
- Why this feature would improve the Linux/Steam Deck experience
|
||||
- Whether it should be optional or enabled by default
|
||||
- If it relates to an existing issue or pull request
|
||||
@@ -12,14 +12,18 @@ permissions:
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Release with release-please
|
||||
uses: google-github-actions/release-please-action@v4
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
# PAT with write access to the repository
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
# optional. customize path to release-please-config.json
|
||||
config-file: release-please-config.json
|
||||
# optional. customize path to .release-please-manifest.json
|
||||
manifest-file: .release-please-manifest.json
|
||||
repo-url: moraroy/NonSteamLaunchers-On-Steam-Deck
|
||||
fork: false
|
||||
include-component-in-tag: false
|
||||
skip-github-release: false
|
||||
skip-github-pull-request: false
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# ETC
|
||||
*.out
|
||||
.serena/
|
||||
CLAUDE.md
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -292,3 +294,8 @@ dist
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# INCLUDE
|
||||
!**/.gitkeep
|
||||
!**/*.example
|
||||
!**/AGENTS.md
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.8.2"
|
||||
".": "4.2.3"
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
python 3.11.6
|
||||
poetry 1.7.1
|
||||
nodejs 21.4.0
|
||||
python 3.11.13
|
||||
uv 0.8.8
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to LLMs when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
NonSteamLaunchers is a tool that installs various game launchers (Epic Games, EA App, GOG Galaxy, etc.) under a single Proton prefix on Steam Deck and Linux systems. It automatically adds these launchers to the Steam Library and can scan for installed games in real-time.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Python Environment Setup
|
||||
|
||||
```bash
|
||||
# Create and activate virtual environment using UV
|
||||
uv venv --python ">=3.11,<3.13"
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
uv pip install -r pyproject.toml --all-extras
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Format Python code
|
||||
ruff format .
|
||||
|
||||
# Check Python linting
|
||||
ruff check .
|
||||
|
||||
# Fix linting issues automatically
|
||||
ruff check . --fix
|
||||
|
||||
# Run pre-commit hooks manually
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
### Running the Project
|
||||
|
||||
```bash
|
||||
# Main installer script
|
||||
./NonSteamLaunchers.sh
|
||||
|
||||
# Game scanner service
|
||||
python NSLGameScanner.py
|
||||
|
||||
# Install specific launcher via command line
|
||||
/bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Epic Games"'
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **NonSteamLaunchers.sh**: Main Bash script that handles launcher installation, Steam integration, and UI
|
||||
- **NSLGameScanner.py**: Python service that monitors installed launchers and automatically adds games to Steam library
|
||||
- **config.py**: Configuration management for paths and settings
|
||||
- **Modules/**: Vendored Python dependencies (steamgrid, requests, vdf, etc.)
|
||||
|
||||
### Key Paths
|
||||
|
||||
- Compatdata directory: `~/.local/share/Steam/steamapps/compatdata/`
|
||||
- NonSteamLaunchers prefix: `~/.local/share/Steam/steamapps/compatdata/NonSteamLaunchers/`
|
||||
- Logs: `/home/deck/Downloads/NonSteamLaunchers-install.log`
|
||||
- Game saves backup: `/home/deck/NSLGameSaves`
|
||||
|
||||
### Integration Points
|
||||
|
||||
- **Steam**: Adds shortcuts via Steam's shortcuts.vdf file
|
||||
- **Proton**: Uses UMU Launcher and GE-Proton for Windows compatibility
|
||||
- **Decky Loader**: Plugin available for Steam Deck Game Mode
|
||||
- **Ludusavi**: Pre-configured for game save backups
|
||||
|
||||
## Code Style
|
||||
|
||||
### Python
|
||||
|
||||
- Formatter: Ruff (Black-compatible, 130 char line length)
|
||||
- Python version: 3.11-3.12
|
||||
- Indentation: 4 spaces
|
||||
- Follow PEP 8 with exceptions defined in ruff.toml
|
||||
|
||||
### Shell Scripts
|
||||
|
||||
- Indentation: Tabs
|
||||
- Follow shellcheck rules (see .shellcheckrc)
|
||||
- Use bash shebang: `#!/bin/bash`
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
Pre-commit is configured with:
|
||||
|
||||
- Ruff for Python formatting and linting
|
||||
- File checks (YAML, JSON, large files, private keys)
|
||||
- End-of-file fixing and line ending normalization
|
||||
|
||||
## Testing
|
||||
|
||||
Currently, there is no formal test suite. When implementing new features:
|
||||
|
||||
- Test installation of launchers manually
|
||||
- Verify Steam library integration
|
||||
- Check game scanner functionality
|
||||
- Test on both Desktop and Game Mode
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
The main script supports:
|
||||
|
||||
- Launcher names: `"Epic Games"`, `"EA App"`, `"GOG Galaxy"`, etc.
|
||||
- Uninstall: `"Uninstall Epic Games"`
|
||||
- Utilities: `"Start Fresh"`, `"Update Proton-GE"`, `"Stop NSLGameScanner"`
|
||||
- SD Card: `"Move to SD Card" "LauncherName"`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The script modifies Steam configuration files - handle with care
|
||||
- Game scanner runs as a systemd service
|
||||
- Community notes feature uses `#nsl` hashtag
|
||||
- MicroSD card support requires proper mount points
|
||||
- Cloud saves via Ludusavi are backed up to `/home/deck/NSLGameSaves`
|
||||
@@ -0,0 +1 @@
|
||||
Oh wow. It seems like you forced yourself to read this by your own free will. Thats known as a choice. A decision. You must be greatly offended with yourself by now. By the end of this sentence, your consent will be taken away from you. Maybe go read the README file instead?
|
||||
@@ -1,180 +0,0 @@
|
||||
# __
|
||||
# /__) _ _ _ _ _/ _
|
||||
# / ( (- (/ (/ (- _) / _)
|
||||
# /
|
||||
|
||||
"""
|
||||
Requests HTTP Library
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Requests is an HTTP library, written in Python, for human beings.
|
||||
Basic GET usage:
|
||||
|
||||
>>> import requests
|
||||
>>> r = requests.get('https://www.python.org')
|
||||
>>> r.status_code
|
||||
200
|
||||
>>> b'Python is a programming language' in r.content
|
||||
True
|
||||
|
||||
... or POST:
|
||||
|
||||
>>> payload = dict(key1='value1', key2='value2')
|
||||
>>> r = requests.post('https://httpbin.org/post', data=payload)
|
||||
>>> print(r.text)
|
||||
{
|
||||
...
|
||||
"form": {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
The other HTTP methods are supported - see `requests.api`. Full documentation
|
||||
is at <https://requests.readthedocs.io>.
|
||||
|
||||
:copyright: (c) 2017 by Kenneth Reitz.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import urllib3
|
||||
|
||||
from .exceptions import RequestsDependencyWarning
|
||||
|
||||
try:
|
||||
from charset_normalizer import __version__ as charset_normalizer_version
|
||||
except ImportError:
|
||||
charset_normalizer_version = None
|
||||
|
||||
try:
|
||||
from chardet import __version__ as chardet_version
|
||||
except ImportError:
|
||||
chardet_version = None
|
||||
|
||||
|
||||
def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):
|
||||
urllib3_version = urllib3_version.split(".")
|
||||
assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git.
|
||||
|
||||
# Sometimes, urllib3 only reports its version as 16.1.
|
||||
if len(urllib3_version) == 2:
|
||||
urllib3_version.append("0")
|
||||
|
||||
# Check urllib3 for compatibility.
|
||||
major, minor, patch = urllib3_version # noqa: F811
|
||||
major, minor, patch = int(major), int(minor), int(patch)
|
||||
# urllib3 >= 1.21.1
|
||||
assert major >= 1
|
||||
if major == 1:
|
||||
assert minor >= 21
|
||||
|
||||
# Check charset_normalizer for compatibility.
|
||||
if chardet_version:
|
||||
major, minor, patch = chardet_version.split(".")[:3]
|
||||
major, minor, patch = int(major), int(minor), int(patch)
|
||||
# chardet_version >= 3.0.2, < 6.0.0
|
||||
assert (3, 0, 2) <= (major, minor, patch) < (6, 0, 0)
|
||||
elif charset_normalizer_version:
|
||||
major, minor, patch = charset_normalizer_version.split(".")[:3]
|
||||
major, minor, patch = int(major), int(minor), int(patch)
|
||||
# charset_normalizer >= 2.0.0 < 4.0.0
|
||||
assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0)
|
||||
else:
|
||||
raise Exception("You need either charset_normalizer or chardet installed")
|
||||
|
||||
|
||||
def _check_cryptography(cryptography_version):
|
||||
# cryptography < 1.3.4
|
||||
try:
|
||||
cryptography_version = list(map(int, cryptography_version.split(".")))
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if cryptography_version < [1, 3, 4]:
|
||||
warning = "Old version of cryptography ({}) may cause slowdown.".format(
|
||||
cryptography_version
|
||||
)
|
||||
warnings.warn(warning, RequestsDependencyWarning)
|
||||
|
||||
|
||||
# Check imported dependencies for compatibility.
|
||||
try:
|
||||
check_compatibility(
|
||||
urllib3.__version__, chardet_version, charset_normalizer_version
|
||||
)
|
||||
except (AssertionError, ValueError):
|
||||
warnings.warn(
|
||||
"urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported "
|
||||
"version!".format(
|
||||
urllib3.__version__, chardet_version, charset_normalizer_version
|
||||
),
|
||||
RequestsDependencyWarning,
|
||||
)
|
||||
|
||||
# Attempt to enable urllib3's fallback for SNI support
|
||||
# if the standard library doesn't support SNI or the
|
||||
# 'ssl' library isn't available.
|
||||
try:
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
if not getattr(ssl, "HAS_SNI", False):
|
||||
from urllib3.contrib import pyopenssl
|
||||
|
||||
pyopenssl.inject_into_urllib3()
|
||||
|
||||
# Check cryptography version
|
||||
from cryptography import __version__ as cryptography_version
|
||||
|
||||
_check_cryptography(cryptography_version)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# urllib3's DependencyWarnings should be silenced.
|
||||
from urllib3.exceptions import DependencyWarning
|
||||
|
||||
warnings.simplefilter("ignore", DependencyWarning)
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
|
||||
from . import packages, utils
|
||||
from .__version__ import (
|
||||
__author__,
|
||||
__author_email__,
|
||||
__build__,
|
||||
__cake__,
|
||||
__copyright__,
|
||||
__description__,
|
||||
__license__,
|
||||
__title__,
|
||||
__url__,
|
||||
__version__,
|
||||
)
|
||||
from .api import delete, get, head, options, patch, post, put, request
|
||||
from .exceptions import (
|
||||
ConnectionError,
|
||||
ConnectTimeout,
|
||||
FileModeWarning,
|
||||
HTTPError,
|
||||
JSONDecodeError,
|
||||
ReadTimeout,
|
||||
RequestException,
|
||||
Timeout,
|
||||
TooManyRedirects,
|
||||
URLRequired,
|
||||
)
|
||||
from .models import PreparedRequest, Request, Response
|
||||
from .sessions import Session, session
|
||||
from .status_codes import codes
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
# FileModeWarnings go off per the default.
|
||||
warnings.simplefilter("default", FileModeWarning, append=True)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
# .-. .-. .-. . . .-. .-. .-. .-.
|
||||
# |( |- |.| | | |- `-. | `-.
|
||||
# ' ' `-' `-`.`-' `-' `-' ' `-'
|
||||
|
||||
__title__ = "requests"
|
||||
__description__ = "Python HTTP for Humans."
|
||||
__url__ = "https://requests.readthedocs.io"
|
||||
__version__ = "2.31.0"
|
||||
__build__ = 0x023100
|
||||
__author__ = "Kenneth Reitz"
|
||||
__author_email__ = "me@kennethreitz.org"
|
||||
__license__ = "Apache 2.0"
|
||||
__copyright__ = "Copyright Kenneth Reitz"
|
||||
__cake__ = "\u2728 \U0001f370 \u2728"
|
||||
@@ -1,50 +0,0 @@
|
||||
"""
|
||||
requests._internal_utils
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Provides utility functions that are consumed internally by Requests
|
||||
which depend on extremely few external helpers (such as compat)
|
||||
"""
|
||||
import re
|
||||
|
||||
from .compat import builtin_str
|
||||
|
||||
_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*$")
|
||||
_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$")
|
||||
_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$")
|
||||
_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$")
|
||||
|
||||
_HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR)
|
||||
_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE)
|
||||
HEADER_VALIDATORS = {
|
||||
bytes: _HEADER_VALIDATORS_BYTE,
|
||||
str: _HEADER_VALIDATORS_STR,
|
||||
}
|
||||
|
||||
|
||||
def to_native_string(string, encoding="ascii"):
|
||||
"""Given a string object, regardless of type, returns a representation of
|
||||
that string in the native string type, encoding and decoding where
|
||||
necessary. This assumes ASCII unless told otherwise.
|
||||
"""
|
||||
if isinstance(string, builtin_str):
|
||||
out = string
|
||||
else:
|
||||
out = string.decode(encoding)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def unicode_is_ascii(u_string):
|
||||
"""Determine if unicode string only contains ASCII characters.
|
||||
|
||||
:param str u_string: unicode string to check. Must be unicode
|
||||
and not Python 2 `str`.
|
||||
:rtype: bool
|
||||
"""
|
||||
assert isinstance(u_string, str)
|
||||
try:
|
||||
u_string.encode("ascii")
|
||||
return True
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
@@ -1,538 +0,0 @@
|
||||
"""
|
||||
requests.adapters
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains the transport adapters that Requests uses to define
|
||||
and maintain connections.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import socket # noqa: F401
|
||||
|
||||
from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
|
||||
from urllib3.exceptions import HTTPError as _HTTPError
|
||||
from urllib3.exceptions import InvalidHeader as _InvalidHeader
|
||||
from urllib3.exceptions import (
|
||||
LocationValueError,
|
||||
MaxRetryError,
|
||||
NewConnectionError,
|
||||
ProtocolError,
|
||||
)
|
||||
from urllib3.exceptions import ProxyError as _ProxyError
|
||||
from urllib3.exceptions import ReadTimeoutError, ResponseError
|
||||
from urllib3.exceptions import SSLError as _SSLError
|
||||
from urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from urllib3.util import Timeout as TimeoutSauce
|
||||
from urllib3.util import parse_url
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from .auth import _basic_auth_str
|
||||
from .compat import basestring, urlparse
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .exceptions import (
|
||||
ConnectionError,
|
||||
ConnectTimeout,
|
||||
InvalidHeader,
|
||||
InvalidProxyURL,
|
||||
InvalidSchema,
|
||||
InvalidURL,
|
||||
ProxyError,
|
||||
ReadTimeout,
|
||||
RetryError,
|
||||
SSLError,
|
||||
)
|
||||
from .models import Response
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .utils import (
|
||||
DEFAULT_CA_BUNDLE_PATH,
|
||||
extract_zipped_paths,
|
||||
get_auth_from_url,
|
||||
get_encoding_from_headers,
|
||||
prepend_scheme_if_needed,
|
||||
select_proxy,
|
||||
urldefragauth,
|
||||
)
|
||||
|
||||
try:
|
||||
from urllib3.contrib.socks import SOCKSProxyManager
|
||||
except ImportError:
|
||||
|
||||
def SOCKSProxyManager(*args, **kwargs):
|
||||
raise InvalidSchema("Missing dependencies for SOCKS support.")
|
||||
|
||||
|
||||
DEFAULT_POOLBLOCK = False
|
||||
DEFAULT_POOLSIZE = 10
|
||||
DEFAULT_RETRIES = 0
|
||||
DEFAULT_POOL_TIMEOUT = None
|
||||
|
||||
|
||||
class BaseAdapter:
|
||||
"""The Base Transport Adapter"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def send(
|
||||
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
|
||||
):
|
||||
"""Sends PreparedRequest object. Returns Response object.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param stream: (optional) Whether to stream the request content.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a :ref:`(connect timeout,
|
||||
read timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
||||
the server's TLS certificate, or a string, in which case it must be a path
|
||||
to a CA bundle to use
|
||||
:param cert: (optional) Any user-provided SSL certificate to be trusted.
|
||||
:param proxies: (optional) The proxies dictionary to apply to the request.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
"""Cleans up adapter specific items."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HTTPAdapter(BaseAdapter):
|
||||
"""The built-in HTTP Adapter for urllib3.
|
||||
|
||||
Provides a general-case interface for Requests sessions to contact HTTP and
|
||||
HTTPS urls by implementing the Transport Adapter interface. This class will
|
||||
usually be created by the :class:`Session <Session>` class under the
|
||||
covers.
|
||||
|
||||
:param pool_connections: The number of urllib3 connection pools to cache.
|
||||
:param pool_maxsize: The maximum number of connections to save in the pool.
|
||||
:param max_retries: The maximum number of retries each connection
|
||||
should attempt. Note, this applies only to failed DNS lookups, socket
|
||||
connections and connection timeouts, never to requests where data has
|
||||
made it to the server. By default, Requests does not retry failed
|
||||
connections. If you need granular control over the conditions under
|
||||
which we retry a request, import urllib3's ``Retry`` class and pass
|
||||
that instead.
|
||||
:param pool_block: Whether the connection pool should block for connections.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> s = requests.Session()
|
||||
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
|
||||
>>> s.mount('http://', a)
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
"max_retries",
|
||||
"config",
|
||||
"_pool_connections",
|
||||
"_pool_maxsize",
|
||||
"_pool_block",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pool_connections=DEFAULT_POOLSIZE,
|
||||
pool_maxsize=DEFAULT_POOLSIZE,
|
||||
max_retries=DEFAULT_RETRIES,
|
||||
pool_block=DEFAULT_POOLBLOCK,
|
||||
):
|
||||
if max_retries == DEFAULT_RETRIES:
|
||||
self.max_retries = Retry(0, read=False)
|
||||
else:
|
||||
self.max_retries = Retry.from_int(max_retries)
|
||||
self.config = {}
|
||||
self.proxy_manager = {}
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._pool_connections = pool_connections
|
||||
self._pool_maxsize = pool_maxsize
|
||||
self._pool_block = pool_block
|
||||
|
||||
self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
|
||||
|
||||
def __getstate__(self):
|
||||
return {attr: getattr(self, attr, None) for attr in self.__attrs__}
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Can't handle by adding 'proxy_manager' to self.__attrs__ because
|
||||
# self.poolmanager uses a lambda function, which isn't pickleable.
|
||||
self.proxy_manager = {}
|
||||
self.config = {}
|
||||
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
self.init_poolmanager(
|
||||
self._pool_connections, self._pool_maxsize, block=self._pool_block
|
||||
)
|
||||
|
||||
def init_poolmanager(
|
||||
self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs
|
||||
):
|
||||
"""Initializes a urllib3 PoolManager.
|
||||
|
||||
This method should not be called from user code, and is only
|
||||
exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param connections: The number of urllib3 connection pools to cache.
|
||||
:param maxsize: The maximum number of connections to save in the pool.
|
||||
:param block: Block when no free connections are available.
|
||||
:param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.
|
||||
"""
|
||||
# save these values for pickling
|
||||
self._pool_connections = connections
|
||||
self._pool_maxsize = maxsize
|
||||
self._pool_block = block
|
||||
|
||||
self.poolmanager = PoolManager(
|
||||
num_pools=connections,
|
||||
maxsize=maxsize,
|
||||
block=block,
|
||||
**pool_kwargs,
|
||||
)
|
||||
|
||||
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
||||
"""Return urllib3 ProxyManager for the given proxy.
|
||||
|
||||
This method should not be called from user code, and is only
|
||||
exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param proxy: The proxy to return a urllib3 ProxyManager for.
|
||||
:param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager.
|
||||
:returns: ProxyManager
|
||||
:rtype: urllib3.ProxyManager
|
||||
"""
|
||||
if proxy in self.proxy_manager:
|
||||
manager = self.proxy_manager[proxy]
|
||||
elif proxy.lower().startswith("socks"):
|
||||
username, password = get_auth_from_url(proxy)
|
||||
manager = self.proxy_manager[proxy] = SOCKSProxyManager(
|
||||
proxy,
|
||||
username=username,
|
||||
password=password,
|
||||
num_pools=self._pool_connections,
|
||||
maxsize=self._pool_maxsize,
|
||||
block=self._pool_block,
|
||||
**proxy_kwargs,
|
||||
)
|
||||
else:
|
||||
proxy_headers = self.proxy_headers(proxy)
|
||||
manager = self.proxy_manager[proxy] = proxy_from_url(
|
||||
proxy,
|
||||
proxy_headers=proxy_headers,
|
||||
num_pools=self._pool_connections,
|
||||
maxsize=self._pool_maxsize,
|
||||
block=self._pool_block,
|
||||
**proxy_kwargs,
|
||||
)
|
||||
|
||||
return manager
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
"""Verify a SSL certificate. This method should not be called from user
|
||||
code, and is only exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param conn: The urllib3 connection object associated with the cert.
|
||||
:param url: The requested URL.
|
||||
:param verify: Either a boolean, in which case it controls whether we verify
|
||||
the server's TLS certificate, or a string, in which case it must be a path
|
||||
to a CA bundle to use
|
||||
:param cert: The SSL certificate to verify.
|
||||
"""
|
||||
if url.lower().startswith("https") and verify:
|
||||
|
||||
cert_loc = None
|
||||
|
||||
# Allow self-specified cert location.
|
||||
if verify is not True:
|
||||
cert_loc = verify
|
||||
|
||||
if not cert_loc:
|
||||
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
|
||||
if not cert_loc or not os.path.exists(cert_loc):
|
||||
raise OSError(
|
||||
f"Could not find a suitable TLS CA certificate bundle, "
|
||||
f"invalid path: {cert_loc}"
|
||||
)
|
||||
|
||||
conn.cert_reqs = "CERT_REQUIRED"
|
||||
|
||||
if not os.path.isdir(cert_loc):
|
||||
conn.ca_certs = cert_loc
|
||||
else:
|
||||
conn.ca_cert_dir = cert_loc
|
||||
else:
|
||||
conn.cert_reqs = "CERT_NONE"
|
||||
conn.ca_certs = None
|
||||
conn.ca_cert_dir = None
|
||||
|
||||
if cert:
|
||||
if not isinstance(cert, basestring):
|
||||
conn.cert_file = cert[0]
|
||||
conn.key_file = cert[1]
|
||||
else:
|
||||
conn.cert_file = cert
|
||||
conn.key_file = None
|
||||
if conn.cert_file and not os.path.exists(conn.cert_file):
|
||||
raise OSError(
|
||||
f"Could not find the TLS certificate file, "
|
||||
f"invalid path: {conn.cert_file}"
|
||||
)
|
||||
if conn.key_file and not os.path.exists(conn.key_file):
|
||||
raise OSError(
|
||||
f"Could not find the TLS key file, invalid path: {conn.key_file}"
|
||||
)
|
||||
|
||||
def build_response(self, req, resp):
|
||||
"""Builds a :class:`Response <requests.Response>` object from a urllib3
|
||||
response. This should not be called from user code, and is only exposed
|
||||
for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`
|
||||
|
||||
:param req: The :class:`PreparedRequest <PreparedRequest>` used to generate the response.
|
||||
:param resp: The urllib3 response object.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
response = Response()
|
||||
|
||||
# Fallback to None if there's no status_code, for whatever reason.
|
||||
response.status_code = getattr(resp, "status", None)
|
||||
|
||||
# Make headers case-insensitive.
|
||||
response.headers = CaseInsensitiveDict(getattr(resp, "headers", {}))
|
||||
|
||||
# Set encoding.
|
||||
response.encoding = get_encoding_from_headers(response.headers)
|
||||
response.raw = resp
|
||||
response.reason = response.raw.reason
|
||||
|
||||
if isinstance(req.url, bytes):
|
||||
response.url = req.url.decode("utf-8")
|
||||
else:
|
||||
response.url = req.url
|
||||
|
||||
# Add new cookies from the server.
|
||||
extract_cookies_to_jar(response.cookies, req, resp)
|
||||
|
||||
# Give the Response some context.
|
||||
response.request = req
|
||||
response.connection = self
|
||||
|
||||
return response
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
"""Returns a urllib3 connection for the given URL. This should not be
|
||||
called from user code, and is only exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param url: The URL to connect to.
|
||||
:param proxies: (optional) A Requests-style dictionary of proxies used on this request.
|
||||
:rtype: urllib3.ConnectionPool
|
||||
"""
|
||||
proxy = select_proxy(url, proxies)
|
||||
|
||||
if proxy:
|
||||
proxy = prepend_scheme_if_needed(proxy, "http")
|
||||
proxy_url = parse_url(proxy)
|
||||
if not proxy_url.host:
|
||||
raise InvalidProxyURL(
|
||||
"Please check proxy URL. It is malformed "
|
||||
"and could be missing the host."
|
||||
)
|
||||
proxy_manager = self.proxy_manager_for(proxy)
|
||||
conn = proxy_manager.connection_from_url(url)
|
||||
else:
|
||||
# Only scheme should be lower case
|
||||
parsed = urlparse(url)
|
||||
url = parsed.geturl()
|
||||
conn = self.poolmanager.connection_from_url(url)
|
||||
|
||||
return conn
|
||||
|
||||
def close(self):
|
||||
"""Disposes of any internal state.
|
||||
|
||||
Currently, this closes the PoolManager and any active ProxyManager,
|
||||
which closes any pooled connections.
|
||||
"""
|
||||
self.poolmanager.clear()
|
||||
for proxy in self.proxy_manager.values():
|
||||
proxy.clear()
|
||||
|
||||
def request_url(self, request, proxies):
|
||||
"""Obtain the url to use when making the final request.
|
||||
|
||||
If the message is being sent through a HTTP proxy, the full URL has to
|
||||
be used. Otherwise, we should only use the path portion of the URL.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param proxies: A dictionary of schemes or schemes and hosts to proxy URLs.
|
||||
:rtype: str
|
||||
"""
|
||||
proxy = select_proxy(request.url, proxies)
|
||||
scheme = urlparse(request.url).scheme
|
||||
|
||||
is_proxied_http_request = proxy and scheme != "https"
|
||||
using_socks_proxy = False
|
||||
if proxy:
|
||||
proxy_scheme = urlparse(proxy).scheme.lower()
|
||||
using_socks_proxy = proxy_scheme.startswith("socks")
|
||||
|
||||
url = request.path_url
|
||||
if is_proxied_http_request and not using_socks_proxy:
|
||||
url = urldefragauth(request.url)
|
||||
|
||||
return url
|
||||
|
||||
def add_headers(self, request, **kwargs):
|
||||
"""Add any headers needed by the connection. As of v2.0 this does
|
||||
nothing by default, but is left for overriding by users that subclass
|
||||
the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` to add headers to.
|
||||
:param kwargs: The keyword arguments from the call to send().
|
||||
"""
|
||||
pass
|
||||
|
||||
def proxy_headers(self, proxy):
|
||||
"""Returns a dictionary of the headers to add to any request sent
|
||||
through a proxy. This works with urllib3 magic to ensure that they are
|
||||
correctly sent to the proxy, rather than in a tunnelled request if
|
||||
CONNECT is being used.
|
||||
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param proxy: The url of the proxy being used for this request.
|
||||
:rtype: dict
|
||||
"""
|
||||
headers = {}
|
||||
username, password = get_auth_from_url(proxy)
|
||||
|
||||
if username:
|
||||
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
|
||||
|
||||
return headers
|
||||
|
||||
def send(
|
||||
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
|
||||
):
|
||||
"""Sends PreparedRequest object. Returns Response object.
|
||||
|
||||
:param request: The :class:`PreparedRequest <PreparedRequest>` being sent.
|
||||
:param stream: (optional) Whether to stream the request content.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a :ref:`(connect timeout,
|
||||
read timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple or urllib3 Timeout object
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether
|
||||
we verify the server's TLS certificate, or a string, in which case it
|
||||
must be a path to a CA bundle to use
|
||||
:param cert: (optional) Any user-provided SSL certificate to be trusted.
|
||||
:param proxies: (optional) The proxies dictionary to apply to the request.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
try:
|
||||
conn = self.get_connection(request.url, proxies)
|
||||
except LocationValueError as e:
|
||||
raise InvalidURL(e, request=request)
|
||||
|
||||
self.cert_verify(conn, request.url, verify, cert)
|
||||
url = self.request_url(request, proxies)
|
||||
self.add_headers(
|
||||
request,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
verify=verify,
|
||||
cert=cert,
|
||||
proxies=proxies,
|
||||
)
|
||||
|
||||
chunked = not (request.body is None or "Content-Length" in request.headers)
|
||||
|
||||
if isinstance(timeout, tuple):
|
||||
try:
|
||||
connect, read = timeout
|
||||
timeout = TimeoutSauce(connect=connect, read=read)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
|
||||
f"or a single float to set both timeouts to the same value."
|
||||
)
|
||||
elif isinstance(timeout, TimeoutSauce):
|
||||
pass
|
||||
else:
|
||||
timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
|
||||
try:
|
||||
resp = conn.urlopen(
|
||||
method=request.method,
|
||||
url=url,
|
||||
body=request.body,
|
||||
headers=request.headers,
|
||||
redirect=False,
|
||||
assert_same_host=False,
|
||||
preload_content=False,
|
||||
decode_content=False,
|
||||
retries=self.max_retries,
|
||||
timeout=timeout,
|
||||
chunked=chunked,
|
||||
)
|
||||
|
||||
except (ProtocolError, OSError) as err:
|
||||
raise ConnectionError(err, request=request)
|
||||
|
||||
except MaxRetryError as e:
|
||||
if isinstance(e.reason, ConnectTimeoutError):
|
||||
# TODO: Remove this in 3.0.0: see #2811
|
||||
if not isinstance(e.reason, NewConnectionError):
|
||||
raise ConnectTimeout(e, request=request)
|
||||
|
||||
if isinstance(e.reason, ResponseError):
|
||||
raise RetryError(e, request=request)
|
||||
|
||||
if isinstance(e.reason, _ProxyError):
|
||||
raise ProxyError(e, request=request)
|
||||
|
||||
if isinstance(e.reason, _SSLError):
|
||||
# This branch is for urllib3 v1.22 and later.
|
||||
raise SSLError(e, request=request)
|
||||
|
||||
raise ConnectionError(e, request=request)
|
||||
|
||||
except ClosedPoolError as e:
|
||||
raise ConnectionError(e, request=request)
|
||||
|
||||
except _ProxyError as e:
|
||||
raise ProxyError(e)
|
||||
|
||||
except (_SSLError, _HTTPError) as e:
|
||||
if isinstance(e, _SSLError):
|
||||
# This branch is for urllib3 versions earlier than v1.22
|
||||
raise SSLError(e, request=request)
|
||||
elif isinstance(e, ReadTimeoutError):
|
||||
raise ReadTimeout(e, request=request)
|
||||
elif isinstance(e, _InvalidHeader):
|
||||
raise InvalidHeader(e, request=request)
|
||||
else:
|
||||
raise
|
||||
|
||||
return self.build_response(request, resp)
|
||||
@@ -1,157 +0,0 @@
|
||||
"""
|
||||
requests.api
|
||||
~~~~~~~~~~~~
|
||||
|
||||
This module implements the Requests API.
|
||||
|
||||
:copyright: (c) 2012 by Kenneth Reitz.
|
||||
:license: Apache2, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from . import sessions
|
||||
|
||||
|
||||
def request(method, url, **kwargs):
|
||||
"""Constructs and sends a :class:`Request <Request>`.
|
||||
|
||||
:param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary, list of tuples or bytes to send
|
||||
in the query string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
|
||||
``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
|
||||
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
|
||||
defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
|
||||
to add for the file.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) How many seconds to wait for the server to send data
|
||||
before giving up, as a float, or a :ref:`(connect timeout, read
|
||||
timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
||||
the server's TLS certificate, or a string, in which case it must be a path
|
||||
to a CA bundle to use. Defaults to ``True``.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> req = requests.request('GET', 'https://httpbin.org/get')
|
||||
>>> req
|
||||
<Response [200]>
|
||||
"""
|
||||
|
||||
# By using the 'with' statement we are sure the session is closed, thus we
|
||||
# avoid leaving sockets open which can trigger a ResourceWarning in some
|
||||
# cases, and look like a memory leak in others.
|
||||
with sessions.Session() as session:
|
||||
return session.request(method=method, url=url, **kwargs)
|
||||
|
||||
|
||||
def get(url, params=None, **kwargs):
|
||||
r"""Sends a GET request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary, list of tuples or bytes to send
|
||||
in the query string for the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("get", url, params=params, **kwargs)
|
||||
|
||||
|
||||
def options(url, **kwargs):
|
||||
r"""Sends an OPTIONS request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("options", url, **kwargs)
|
||||
|
||||
|
||||
def head(url, **kwargs):
|
||||
r"""Sends a HEAD request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes. If
|
||||
`allow_redirects` is not provided, it will be set to `False` (as
|
||||
opposed to the default :meth:`request` behavior).
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault("allow_redirects", False)
|
||||
return request("head", url, **kwargs)
|
||||
|
||||
|
||||
def post(url, data=None, json=None, **kwargs):
|
||||
r"""Sends a POST request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("post", url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
r"""Sends a PUT request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("put", url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **kwargs):
|
||||
r"""Sends a PATCH request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("patch", url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **kwargs):
|
||||
r"""Sends a DELETE request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request("delete", url, **kwargs)
|
||||
@@ -1,315 +0,0 @@
|
||||
"""
|
||||
requests.auth
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This module contains the authentication handlers for Requests.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import warnings
|
||||
from base64 import b64encode
|
||||
|
||||
from ._internal_utils import to_native_string
|
||||
from .compat import basestring, str, urlparse
|
||||
from .cookies import extract_cookies_to_jar
|
||||
from .utils import parse_dict_header
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
|
||||
CONTENT_TYPE_MULTI_PART = "multipart/form-data"
|
||||
|
||||
|
||||
def _basic_auth_str(username, password):
|
||||
"""Returns a Basic Auth string."""
|
||||
|
||||
# "I want us to put a big-ol' comment on top of it that
|
||||
# says that this behaviour is dumb but we need to preserve
|
||||
# it because people are relying on it."
|
||||
# - Lukasa
|
||||
#
|
||||
# These are here solely to maintain backwards compatibility
|
||||
# for things like ints. This will be removed in 3.0.0.
|
||||
if not isinstance(username, basestring):
|
||||
warnings.warn(
|
||||
"Non-string usernames will no longer be supported in Requests "
|
||||
"3.0.0. Please convert the object you've passed in ({!r}) to "
|
||||
"a string or bytes object in the near future to avoid "
|
||||
"problems.".format(username),
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
username = str(username)
|
||||
|
||||
if not isinstance(password, basestring):
|
||||
warnings.warn(
|
||||
"Non-string passwords will no longer be supported in Requests "
|
||||
"3.0.0. Please convert the object you've passed in ({!r}) to "
|
||||
"a string or bytes object in the near future to avoid "
|
||||
"problems.".format(type(password)),
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
password = str(password)
|
||||
# -- End Removal --
|
||||
|
||||
if isinstance(username, str):
|
||||
username = username.encode("latin1")
|
||||
|
||||
if isinstance(password, str):
|
||||
password = password.encode("latin1")
|
||||
|
||||
authstr = "Basic " + to_native_string(
|
||||
b64encode(b":".join((username, password))).strip()
|
||||
)
|
||||
|
||||
return authstr
|
||||
|
||||
|
||||
class AuthBase:
|
||||
"""Base class that all auth implementations derive from"""
|
||||
|
||||
def __call__(self, r):
|
||||
raise NotImplementedError("Auth hooks must be callable.")
|
||||
|
||||
|
||||
class HTTPBasicAuth(AuthBase):
|
||||
"""Attaches HTTP Basic Authentication to the given Request object."""
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def __eq__(self, other):
|
||||
return all(
|
||||
[
|
||||
self.username == getattr(other, "username", None),
|
||||
self.password == getattr(other, "password", None),
|
||||
]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __call__(self, r):
|
||||
r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
|
||||
class HTTPProxyAuth(HTTPBasicAuth):
|
||||
"""Attaches HTTP Proxy Authentication to a given Request object."""
|
||||
|
||||
def __call__(self, r):
|
||||
r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
|
||||
class HTTPDigestAuth(AuthBase):
|
||||
"""Attaches HTTP Digest Authentication to the given Request object."""
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
# Keep state in per-thread local storage
|
||||
self._thread_local = threading.local()
|
||||
|
||||
def init_per_thread_state(self):
|
||||
# Ensure state is initialized just once per-thread
|
||||
if not hasattr(self._thread_local, "init"):
|
||||
self._thread_local.init = True
|
||||
self._thread_local.last_nonce = ""
|
||||
self._thread_local.nonce_count = 0
|
||||
self._thread_local.chal = {}
|
||||
self._thread_local.pos = None
|
||||
self._thread_local.num_401_calls = None
|
||||
|
||||
def build_digest_header(self, method, url):
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
realm = self._thread_local.chal["realm"]
|
||||
nonce = self._thread_local.chal["nonce"]
|
||||
qop = self._thread_local.chal.get("qop")
|
||||
algorithm = self._thread_local.chal.get("algorithm")
|
||||
opaque = self._thread_local.chal.get("opaque")
|
||||
hash_utf8 = None
|
||||
|
||||
if algorithm is None:
|
||||
_algorithm = "MD5"
|
||||
else:
|
||||
_algorithm = algorithm.upper()
|
||||
# lambdas assume digest modules are imported at the top level
|
||||
if _algorithm == "MD5" or _algorithm == "MD5-SESS":
|
||||
|
||||
def md5_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.md5(x).hexdigest()
|
||||
|
||||
hash_utf8 = md5_utf8
|
||||
elif _algorithm == "SHA":
|
||||
|
||||
def sha_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha1(x).hexdigest()
|
||||
|
||||
hash_utf8 = sha_utf8
|
||||
elif _algorithm == "SHA-256":
|
||||
|
||||
def sha256_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha256(x).hexdigest()
|
||||
|
||||
hash_utf8 = sha256_utf8
|
||||
elif _algorithm == "SHA-512":
|
||||
|
||||
def sha512_utf8(x):
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha512(x).hexdigest()
|
||||
|
||||
hash_utf8 = sha512_utf8
|
||||
|
||||
KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731
|
||||
|
||||
if hash_utf8 is None:
|
||||
return None
|
||||
|
||||
# XXX not implemented yet
|
||||
entdig = None
|
||||
p_parsed = urlparse(url)
|
||||
#: path is request-uri defined in RFC 2616 which should not be empty
|
||||
path = p_parsed.path or "/"
|
||||
if p_parsed.query:
|
||||
path += f"?{p_parsed.query}"
|
||||
|
||||
A1 = f"{self.username}:{realm}:{self.password}"
|
||||
A2 = f"{method}:{path}"
|
||||
|
||||
HA1 = hash_utf8(A1)
|
||||
HA2 = hash_utf8(A2)
|
||||
|
||||
if nonce == self._thread_local.last_nonce:
|
||||
self._thread_local.nonce_count += 1
|
||||
else:
|
||||
self._thread_local.nonce_count = 1
|
||||
ncvalue = f"{self._thread_local.nonce_count:08x}"
|
||||
s = str(self._thread_local.nonce_count).encode("utf-8")
|
||||
s += nonce.encode("utf-8")
|
||||
s += time.ctime().encode("utf-8")
|
||||
s += os.urandom(8)
|
||||
|
||||
cnonce = hashlib.sha1(s).hexdigest()[:16]
|
||||
if _algorithm == "MD5-SESS":
|
||||
HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}")
|
||||
|
||||
if not qop:
|
||||
respdig = KD(HA1, f"{nonce}:{HA2}")
|
||||
elif qop == "auth" or "auth" in qop.split(","):
|
||||
noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}"
|
||||
respdig = KD(HA1, noncebit)
|
||||
else:
|
||||
# XXX handle auth-int.
|
||||
return None
|
||||
|
||||
self._thread_local.last_nonce = nonce
|
||||
|
||||
# XXX should the partial digests be encoded too?
|
||||
base = (
|
||||
f'username="{self.username}", realm="{realm}", nonce="{nonce}", '
|
||||
f'uri="{path}", response="{respdig}"'
|
||||
)
|
||||
if opaque:
|
||||
base += f', opaque="{opaque}"'
|
||||
if algorithm:
|
||||
base += f', algorithm="{algorithm}"'
|
||||
if entdig:
|
||||
base += f', digest="{entdig}"'
|
||||
if qop:
|
||||
base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
|
||||
|
||||
return f"Digest {base}"
|
||||
|
||||
def handle_redirect(self, r, **kwargs):
|
||||
"""Reset num_401_calls counter on redirects."""
|
||||
if r.is_redirect:
|
||||
self._thread_local.num_401_calls = 1
|
||||
|
||||
def handle_401(self, r, **kwargs):
|
||||
"""
|
||||
Takes the given response and tries digest-auth, if needed.
|
||||
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
# If response is not 4xx, do not auth
|
||||
# See https://github.com/psf/requests/issues/3772
|
||||
if not 400 <= r.status_code < 500:
|
||||
self._thread_local.num_401_calls = 1
|
||||
return r
|
||||
|
||||
if self._thread_local.pos is not None:
|
||||
# Rewind the file position indicator of the body to where
|
||||
# it was to resend the request.
|
||||
r.request.body.seek(self._thread_local.pos)
|
||||
s_auth = r.headers.get("www-authenticate", "")
|
||||
|
||||
if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
|
||||
|
||||
self._thread_local.num_401_calls += 1
|
||||
pat = re.compile(r"digest ", flags=re.IGNORECASE)
|
||||
self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
|
||||
|
||||
# Consume content and release the original connection
|
||||
# to allow our new request to reuse the same one.
|
||||
r.content
|
||||
r.close()
|
||||
prep = r.request.copy()
|
||||
extract_cookies_to_jar(prep._cookies, r.request, r.raw)
|
||||
prep.prepare_cookies(prep._cookies)
|
||||
|
||||
prep.headers["Authorization"] = self.build_digest_header(
|
||||
prep.method, prep.url
|
||||
)
|
||||
_r = r.connection.send(prep, **kwargs)
|
||||
_r.history.append(r)
|
||||
_r.request = prep
|
||||
|
||||
return _r
|
||||
|
||||
self._thread_local.num_401_calls = 1
|
||||
return r
|
||||
|
||||
def __call__(self, r):
|
||||
# Initialize per-thread state, if needed
|
||||
self.init_per_thread_state()
|
||||
# If we have a saved nonce, skip the 401
|
||||
if self._thread_local.last_nonce:
|
||||
r.headers["Authorization"] = self.build_digest_header(r.method, r.url)
|
||||
try:
|
||||
self._thread_local.pos = r.body.tell()
|
||||
except AttributeError:
|
||||
# In the case of HTTPDigestAuth being reused and the body of
|
||||
# the previous request was a file-like object, pos has the
|
||||
# file position of the previous body. Ensure it's set to
|
||||
# None.
|
||||
self._thread_local.pos = None
|
||||
r.register_hook("response", self.handle_401)
|
||||
r.register_hook("response", self.handle_redirect)
|
||||
self._thread_local.num_401_calls = 1
|
||||
|
||||
return r
|
||||
|
||||
def __eq__(self, other):
|
||||
return all(
|
||||
[
|
||||
self.username == getattr(other, "username", None),
|
||||
self.password == getattr(other, "password", None),
|
||||
]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
requests.certs
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module returns the preferred default CA certificate bundle. There is
|
||||
only one — the one from the certifi package.
|
||||
|
||||
If you are packaging Requests, e.g., for a Linux distribution or a managed
|
||||
environment, you can change the definition of where() to return a separately
|
||||
packaged CA bundle.
|
||||
"""
|
||||
from certifi import where
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(where())
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
requests.compat
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This module previously handled import compatibility issues
|
||||
between Python 2 and Python 3. It remains for backwards
|
||||
compatibility until the next major version.
|
||||
"""
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
import charset_normalizer as chardet
|
||||
|
||||
import sys
|
||||
|
||||
# -------
|
||||
# Pythons
|
||||
# -------
|
||||
|
||||
# Syntax sugar.
|
||||
_ver = sys.version_info
|
||||
|
||||
#: Python 2.x?
|
||||
is_py2 = _ver[0] == 2
|
||||
|
||||
#: Python 3.x?
|
||||
is_py3 = _ver[0] == 3
|
||||
|
||||
# json/simplejson module import resolution
|
||||
has_simplejson = False
|
||||
try:
|
||||
import simplejson as json
|
||||
|
||||
has_simplejson = True
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
if has_simplejson:
|
||||
from simplejson import JSONDecodeError
|
||||
else:
|
||||
from json import JSONDecodeError
|
||||
|
||||
# Keep OrderedDict for backwards compatibility.
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Mapping, MutableMapping
|
||||
from http import cookiejar as cookielib
|
||||
from http.cookies import Morsel
|
||||
from io import StringIO
|
||||
|
||||
# --------------
|
||||
# Legacy Imports
|
||||
# --------------
|
||||
from urllib.parse import (
|
||||
quote,
|
||||
quote_plus,
|
||||
unquote,
|
||||
unquote_plus,
|
||||
urldefrag,
|
||||
urlencode,
|
||||
urljoin,
|
||||
urlparse,
|
||||
urlsplit,
|
||||
urlunparse,
|
||||
)
|
||||
from urllib.request import (
|
||||
getproxies,
|
||||
getproxies_environment,
|
||||
parse_http_list,
|
||||
proxy_bypass,
|
||||
proxy_bypass_environment,
|
||||
)
|
||||
|
||||
builtin_str = str
|
||||
str = str
|
||||
bytes = bytes
|
||||
basestring = (str, bytes)
|
||||
numeric_types = (int, float)
|
||||
integer_types = (int,)
|
||||
@@ -1,561 +0,0 @@
|
||||
"""
|
||||
requests.cookies
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Compatibility code to be able to use `cookielib.CookieJar` with requests.
|
||||
|
||||
requests.utils imports from here, so be careful with imports.
|
||||
"""
|
||||
|
||||
import calendar
|
||||
import copy
|
||||
import time
|
||||
|
||||
from ._internal_utils import to_native_string
|
||||
from .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse
|
||||
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
import dummy_threading as threading
|
||||
|
||||
|
||||
class MockRequest:
|
||||
"""Wraps a `requests.Request` to mimic a `urllib2.Request`.
|
||||
|
||||
The code in `cookielib.CookieJar` expects this interface in order to correctly
|
||||
manage cookie policies, i.e., determine whether a cookie can be set, given the
|
||||
domains of the request and the cookie.
|
||||
|
||||
The original request object is read-only. The client is responsible for collecting
|
||||
the new headers via `get_new_headers()` and interpreting them appropriately. You
|
||||
probably want `get_cookie_header`, defined below.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
self._r = request
|
||||
self._new_headers = {}
|
||||
self.type = urlparse(self._r.url).scheme
|
||||
|
||||
def get_type(self):
|
||||
return self.type
|
||||
|
||||
def get_host(self):
|
||||
return urlparse(self._r.url).netloc
|
||||
|
||||
def get_origin_req_host(self):
|
||||
return self.get_host()
|
||||
|
||||
def get_full_url(self):
|
||||
# Only return the response's URL if the user hadn't set the Host
|
||||
# header
|
||||
if not self._r.headers.get("Host"):
|
||||
return self._r.url
|
||||
# If they did set it, retrieve it and reconstruct the expected domain
|
||||
host = to_native_string(self._r.headers["Host"], encoding="utf-8")
|
||||
parsed = urlparse(self._r.url)
|
||||
# Reconstruct the URL as we expect it
|
||||
return urlunparse(
|
||||
[
|
||||
parsed.scheme,
|
||||
host,
|
||||
parsed.path,
|
||||
parsed.params,
|
||||
parsed.query,
|
||||
parsed.fragment,
|
||||
]
|
||||
)
|
||||
|
||||
def is_unverifiable(self):
|
||||
return True
|
||||
|
||||
def has_header(self, name):
|
||||
return name in self._r.headers or name in self._new_headers
|
||||
|
||||
def get_header(self, name, default=None):
|
||||
return self._r.headers.get(name, self._new_headers.get(name, default))
|
||||
|
||||
def add_header(self, key, val):
|
||||
"""cookielib has no legitimate use for this method; add it back if you find one."""
|
||||
raise NotImplementedError(
|
||||
"Cookie headers should be added with add_unredirected_header()"
|
||||
)
|
||||
|
||||
def add_unredirected_header(self, name, value):
|
||||
self._new_headers[name] = value
|
||||
|
||||
def get_new_headers(self):
|
||||
return self._new_headers
|
||||
|
||||
@property
|
||||
def unverifiable(self):
|
||||
return self.is_unverifiable()
|
||||
|
||||
@property
|
||||
def origin_req_host(self):
|
||||
return self.get_origin_req_host()
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.get_host()
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
|
||||
|
||||
...what? Basically, expose the parsed HTTP headers from the server response
|
||||
the way `cookielib` expects to see them.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
"""Make a MockResponse for `cookielib` to read.
|
||||
|
||||
:param headers: a httplib.HTTPMessage or analogous carrying the headers
|
||||
"""
|
||||
self._headers = headers
|
||||
|
||||
def info(self):
|
||||
return self._headers
|
||||
|
||||
def getheaders(self, name):
|
||||
self._headers.getheaders(name)
|
||||
|
||||
|
||||
def extract_cookies_to_jar(jar, request, response):
|
||||
"""Extract the cookies from the response into a CookieJar.
|
||||
|
||||
:param jar: cookielib.CookieJar (not necessarily a RequestsCookieJar)
|
||||
:param request: our own requests.Request object
|
||||
:param response: urllib3.HTTPResponse object
|
||||
"""
|
||||
if not (hasattr(response, "_original_response") and response._original_response):
|
||||
return
|
||||
# the _original_response field is the wrapped httplib.HTTPResponse object,
|
||||
req = MockRequest(request)
|
||||
# pull out the HTTPMessage with the headers and put it in the mock:
|
||||
res = MockResponse(response._original_response.msg)
|
||||
jar.extract_cookies(res, req)
|
||||
|
||||
|
||||
def get_cookie_header(jar, request):
|
||||
"""
|
||||
Produce an appropriate Cookie header string to be sent with `request`, or None.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
r = MockRequest(request)
|
||||
jar.add_cookie_header(r)
|
||||
return r.get_new_headers().get("Cookie")
|
||||
|
||||
|
||||
def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
|
||||
"""Unsets a cookie by name, by default over all domains and paths.
|
||||
|
||||
Wraps CookieJar.clear(), is O(n).
|
||||
"""
|
||||
clearables = []
|
||||
for cookie in cookiejar:
|
||||
if cookie.name != name:
|
||||
continue
|
||||
if domain is not None and domain != cookie.domain:
|
||||
continue
|
||||
if path is not None and path != cookie.path:
|
||||
continue
|
||||
clearables.append((cookie.domain, cookie.path, cookie.name))
|
||||
|
||||
for domain, path, name in clearables:
|
||||
cookiejar.clear(domain, path, name)
|
||||
|
||||
|
||||
class CookieConflictError(RuntimeError):
|
||||
"""There are two cookies that meet the criteria specified in the cookie jar.
|
||||
Use .get and .set and include domain and path args in order to be more specific.
|
||||
"""
|
||||
|
||||
|
||||
class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict
|
||||
interface.
|
||||
|
||||
This is the CookieJar we create by default for requests and sessions that
|
||||
don't specify one, since some clients may expect response.cookies and
|
||||
session.cookies to support dict operations.
|
||||
|
||||
Requests does not use the dict interface internally; it's just for
|
||||
compatibility with external client code. All requests code should work
|
||||
out of the box with externally provided instances of ``CookieJar``, e.g.
|
||||
``LWPCookieJar`` and ``FileCookieJar``.
|
||||
|
||||
Unlike a regular CookieJar, this class is pickleable.
|
||||
|
||||
.. warning:: dictionary operations that are normally O(1) may be O(n).
|
||||
"""
|
||||
|
||||
def get(self, name, default=None, domain=None, path=None):
|
||||
"""Dict-like get() that also supports optional domain and path args in
|
||||
order to resolve naming collisions from using one cookie jar over
|
||||
multiple domains.
|
||||
|
||||
.. warning:: operation is O(n), not O(1).
|
||||
"""
|
||||
try:
|
||||
return self._find_no_duplicates(name, domain, path)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def set(self, name, value, **kwargs):
|
||||
"""Dict-like set() that also supports optional domain and path args in
|
||||
order to resolve naming collisions from using one cookie jar over
|
||||
multiple domains.
|
||||
"""
|
||||
# support client code that unsets cookies by assignment of a None value:
|
||||
if value is None:
|
||||
remove_cookie_by_name(
|
||||
self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(value, Morsel):
|
||||
c = morsel_to_cookie(value)
|
||||
else:
|
||||
c = create_cookie(name, value, **kwargs)
|
||||
self.set_cookie(c)
|
||||
return c
|
||||
|
||||
def iterkeys(self):
|
||||
"""Dict-like iterkeys() that returns an iterator of names of cookies
|
||||
from the jar.
|
||||
|
||||
.. seealso:: itervalues() and iteritems().
|
||||
"""
|
||||
for cookie in iter(self):
|
||||
yield cookie.name
|
||||
|
||||
def keys(self):
|
||||
"""Dict-like keys() that returns a list of names of cookies from the
|
||||
jar.
|
||||
|
||||
.. seealso:: values() and items().
|
||||
"""
|
||||
return list(self.iterkeys())
|
||||
|
||||
def itervalues(self):
|
||||
"""Dict-like itervalues() that returns an iterator of values of cookies
|
||||
from the jar.
|
||||
|
||||
.. seealso:: iterkeys() and iteritems().
|
||||
"""
|
||||
for cookie in iter(self):
|
||||
yield cookie.value
|
||||
|
||||
def values(self):
|
||||
"""Dict-like values() that returns a list of values of cookies from the
|
||||
jar.
|
||||
|
||||
.. seealso:: keys() and items().
|
||||
"""
|
||||
return list(self.itervalues())
|
||||
|
||||
def iteritems(self):
|
||||
"""Dict-like iteritems() that returns an iterator of name-value tuples
|
||||
from the jar.
|
||||
|
||||
.. seealso:: iterkeys() and itervalues().
|
||||
"""
|
||||
for cookie in iter(self):
|
||||
yield cookie.name, cookie.value
|
||||
|
||||
def items(self):
|
||||
"""Dict-like items() that returns a list of name-value tuples from the
|
||||
jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a
|
||||
vanilla python dict of key value pairs.
|
||||
|
||||
.. seealso:: keys() and values().
|
||||
"""
|
||||
return list(self.iteritems())
|
||||
|
||||
def list_domains(self):
|
||||
"""Utility method to list all the domains in the jar."""
|
||||
domains = []
|
||||
for cookie in iter(self):
|
||||
if cookie.domain not in domains:
|
||||
domains.append(cookie.domain)
|
||||
return domains
|
||||
|
||||
def list_paths(self):
|
||||
"""Utility method to list all the paths in the jar."""
|
||||
paths = []
|
||||
for cookie in iter(self):
|
||||
if cookie.path not in paths:
|
||||
paths.append(cookie.path)
|
||||
return paths
|
||||
|
||||
def multiple_domains(self):
|
||||
"""Returns True if there are multiple domains in the jar.
|
||||
Returns False otherwise.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
domains = []
|
||||
for cookie in iter(self):
|
||||
if cookie.domain is not None and cookie.domain in domains:
|
||||
return True
|
||||
domains.append(cookie.domain)
|
||||
return False # there is only one domain in jar
|
||||
|
||||
def get_dict(self, domain=None, path=None):
|
||||
"""Takes as an argument an optional domain and path and returns a plain
|
||||
old Python dict of name-value pairs of cookies that meet the
|
||||
requirements.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
dictionary = {}
|
||||
for cookie in iter(self):
|
||||
if (domain is None or cookie.domain == domain) and (
|
||||
path is None or cookie.path == path
|
||||
):
|
||||
dictionary[cookie.name] = cookie.value
|
||||
return dictionary
|
||||
|
||||
def __contains__(self, name):
|
||||
try:
|
||||
return super().__contains__(name)
|
||||
except CookieConflictError:
|
||||
return True
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Dict-like __getitem__() for compatibility with client code. Throws
|
||||
exception if there are more than one cookie with name. In that case,
|
||||
use the more explicit get() method instead.
|
||||
|
||||
.. warning:: operation is O(n), not O(1).
|
||||
"""
|
||||
return self._find_no_duplicates(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
"""Dict-like __setitem__ for compatibility with client code. Throws
|
||||
exception if there is already a cookie of that name in the jar. In that
|
||||
case, use the more explicit set() method instead.
|
||||
"""
|
||||
self.set(name, value)
|
||||
|
||||
def __delitem__(self, name):
|
||||
"""Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
|
||||
``remove_cookie_by_name()``.
|
||||
"""
|
||||
remove_cookie_by_name(self, name)
|
||||
|
||||
def set_cookie(self, cookie, *args, **kwargs):
|
||||
if (
|
||||
hasattr(cookie.value, "startswith")
|
||||
and cookie.value.startswith('"')
|
||||
and cookie.value.endswith('"')
|
||||
):
|
||||
cookie.value = cookie.value.replace('\\"', "")
|
||||
return super().set_cookie(cookie, *args, **kwargs)
|
||||
|
||||
def update(self, other):
|
||||
"""Updates this jar with cookies from another CookieJar or dict-like"""
|
||||
if isinstance(other, cookielib.CookieJar):
|
||||
for cookie in other:
|
||||
self.set_cookie(copy.copy(cookie))
|
||||
else:
|
||||
super().update(other)
|
||||
|
||||
def _find(self, name, domain=None, path=None):
|
||||
"""Requests uses this method internally to get cookie values.
|
||||
|
||||
If there are conflicting cookies, _find arbitrarily chooses one.
|
||||
See _find_no_duplicates if you want an exception thrown if there are
|
||||
conflicting cookies.
|
||||
|
||||
:param name: a string containing name of cookie
|
||||
:param domain: (optional) string containing domain of cookie
|
||||
:param path: (optional) string containing path of cookie
|
||||
:return: cookie.value
|
||||
"""
|
||||
for cookie in iter(self):
|
||||
if cookie.name == name:
|
||||
if domain is None or cookie.domain == domain:
|
||||
if path is None or cookie.path == path:
|
||||
return cookie.value
|
||||
|
||||
raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
|
||||
|
||||
def _find_no_duplicates(self, name, domain=None, path=None):
|
||||
"""Both ``__get_item__`` and ``get`` call this function: it's never
|
||||
used elsewhere in Requests.
|
||||
|
||||
:param name: a string containing name of cookie
|
||||
:param domain: (optional) string containing domain of cookie
|
||||
:param path: (optional) string containing path of cookie
|
||||
:raises KeyError: if cookie is not found
|
||||
:raises CookieConflictError: if there are multiple cookies
|
||||
that match name and optionally domain and path
|
||||
:return: cookie.value
|
||||
"""
|
||||
toReturn = None
|
||||
for cookie in iter(self):
|
||||
if cookie.name == name:
|
||||
if domain is None or cookie.domain == domain:
|
||||
if path is None or cookie.path == path:
|
||||
if toReturn is not None:
|
||||
# if there are multiple cookies that meet passed in criteria
|
||||
raise CookieConflictError(
|
||||
f"There are multiple cookies with name, {name!r}"
|
||||
)
|
||||
# we will eventually return this as long as no cookie conflict
|
||||
toReturn = cookie.value
|
||||
|
||||
if toReturn:
|
||||
return toReturn
|
||||
raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
|
||||
|
||||
def __getstate__(self):
|
||||
"""Unlike a normal CookieJar, this class is pickleable."""
|
||||
state = self.__dict__.copy()
|
||||
# remove the unpickleable RLock object
|
||||
state.pop("_cookies_lock")
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""Unlike a normal CookieJar, this class is pickleable."""
|
||||
self.__dict__.update(state)
|
||||
if "_cookies_lock" not in self.__dict__:
|
||||
self._cookies_lock = threading.RLock()
|
||||
|
||||
def copy(self):
|
||||
"""Return a copy of this RequestsCookieJar."""
|
||||
new_cj = RequestsCookieJar()
|
||||
new_cj.set_policy(self.get_policy())
|
||||
new_cj.update(self)
|
||||
return new_cj
|
||||
|
||||
def get_policy(self):
|
||||
"""Return the CookiePolicy instance used."""
|
||||
return self._policy
|
||||
|
||||
|
||||
def _copy_cookie_jar(jar):
|
||||
if jar is None:
|
||||
return None
|
||||
|
||||
if hasattr(jar, "copy"):
|
||||
# We're dealing with an instance of RequestsCookieJar
|
||||
return jar.copy()
|
||||
# We're dealing with a generic CookieJar instance
|
||||
new_jar = copy.copy(jar)
|
||||
new_jar.clear()
|
||||
for cookie in jar:
|
||||
new_jar.set_cookie(copy.copy(cookie))
|
||||
return new_jar
|
||||
|
||||
|
||||
def create_cookie(name, value, **kwargs):
|
||||
"""Make a cookie from underspecified parameters.
|
||||
|
||||
By default, the pair of `name` and `value` will be set for the domain ''
|
||||
and sent on every request (this is sometimes called a "supercookie").
|
||||
"""
|
||||
result = {
|
||||
"version": 0,
|
||||
"name": name,
|
||||
"value": value,
|
||||
"port": None,
|
||||
"domain": "",
|
||||
"path": "/",
|
||||
"secure": False,
|
||||
"expires": None,
|
||||
"discard": True,
|
||||
"comment": None,
|
||||
"comment_url": None,
|
||||
"rest": {"HttpOnly": None},
|
||||
"rfc2109": False,
|
||||
}
|
||||
|
||||
badargs = set(kwargs) - set(result)
|
||||
if badargs:
|
||||
raise TypeError(
|
||||
f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
|
||||
)
|
||||
|
||||
result.update(kwargs)
|
||||
result["port_specified"] = bool(result["port"])
|
||||
result["domain_specified"] = bool(result["domain"])
|
||||
result["domain_initial_dot"] = result["domain"].startswith(".")
|
||||
result["path_specified"] = bool(result["path"])
|
||||
|
||||
return cookielib.Cookie(**result)
|
||||
|
||||
|
||||
def morsel_to_cookie(morsel):
|
||||
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
|
||||
|
||||
expires = None
|
||||
if morsel["max-age"]:
|
||||
try:
|
||||
expires = int(time.time() + int(morsel["max-age"]))
|
||||
except ValueError:
|
||||
raise TypeError(f"max-age: {morsel['max-age']} must be integer")
|
||||
elif morsel["expires"]:
|
||||
time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
|
||||
expires = calendar.timegm(time.strptime(morsel["expires"], time_template))
|
||||
return create_cookie(
|
||||
comment=morsel["comment"],
|
||||
comment_url=bool(morsel["comment"]),
|
||||
discard=False,
|
||||
domain=morsel["domain"],
|
||||
expires=expires,
|
||||
name=morsel.key,
|
||||
path=morsel["path"],
|
||||
port=None,
|
||||
rest={"HttpOnly": morsel["httponly"]},
|
||||
rfc2109=False,
|
||||
secure=bool(morsel["secure"]),
|
||||
value=morsel.value,
|
||||
version=morsel["version"] or 0,
|
||||
)
|
||||
|
||||
|
||||
def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
|
||||
"""Returns a CookieJar from a key/value dictionary.
|
||||
|
||||
:param cookie_dict: Dict of key/values to insert into CookieJar.
|
||||
:param cookiejar: (optional) A cookiejar to add the cookies to.
|
||||
:param overwrite: (optional) If False, will not replace cookies
|
||||
already in the jar with new ones.
|
||||
:rtype: CookieJar
|
||||
"""
|
||||
if cookiejar is None:
|
||||
cookiejar = RequestsCookieJar()
|
||||
|
||||
if cookie_dict is not None:
|
||||
names_from_jar = [cookie.name for cookie in cookiejar]
|
||||
for name in cookie_dict:
|
||||
if overwrite or (name not in names_from_jar):
|
||||
cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
|
||||
|
||||
return cookiejar
|
||||
|
||||
|
||||
def merge_cookies(cookiejar, cookies):
|
||||
"""Add cookies to cookiejar and returns a merged CookieJar.
|
||||
|
||||
:param cookiejar: CookieJar object to add the cookies to.
|
||||
:param cookies: Dictionary or CookieJar object to be added.
|
||||
:rtype: CookieJar
|
||||
"""
|
||||
if not isinstance(cookiejar, cookielib.CookieJar):
|
||||
raise ValueError("You can only merge into CookieJar")
|
||||
|
||||
if isinstance(cookies, dict):
|
||||
cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)
|
||||
elif isinstance(cookies, cookielib.CookieJar):
|
||||
try:
|
||||
cookiejar.update(cookies)
|
||||
except AttributeError:
|
||||
for cookie_in_jar in cookies:
|
||||
cookiejar.set_cookie(cookie_in_jar)
|
||||
|
||||
return cookiejar
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
requests.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains the set of Requests' exceptions.
|
||||
"""
|
||||
from urllib3.exceptions import HTTPError as BaseHTTPError
|
||||
|
||||
from .compat import JSONDecodeError as CompatJSONDecodeError
|
||||
|
||||
|
||||
class RequestException(IOError):
|
||||
"""There was an ambiguous exception that occurred while handling your
|
||||
request.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize RequestException with `request` and `response` objects."""
|
||||
response = kwargs.pop("response", None)
|
||||
self.response = response
|
||||
self.request = kwargs.pop("request", None)
|
||||
if response is not None and not self.request and hasattr(response, "request"):
|
||||
self.request = self.response.request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class InvalidJSONError(RequestException):
|
||||
"""A JSON error occurred."""
|
||||
|
||||
|
||||
class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
|
||||
"""Couldn't decode the text into json"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Construct the JSONDecodeError instance first with all
|
||||
args. Then use it's args to construct the IOError so that
|
||||
the json specific args aren't used as IOError specific args
|
||||
and the error message from JSONDecodeError is preserved.
|
||||
"""
|
||||
CompatJSONDecodeError.__init__(self, *args)
|
||||
InvalidJSONError.__init__(self, *self.args, **kwargs)
|
||||
|
||||
|
||||
class HTTPError(RequestException):
|
||||
"""An HTTP error occurred."""
|
||||
|
||||
|
||||
class ConnectionError(RequestException):
|
||||
"""A Connection error occurred."""
|
||||
|
||||
|
||||
class ProxyError(ConnectionError):
|
||||
"""A proxy error occurred."""
|
||||
|
||||
|
||||
class SSLError(ConnectionError):
|
||||
"""An SSL error occurred."""
|
||||
|
||||
|
||||
class Timeout(RequestException):
|
||||
"""The request timed out.
|
||||
|
||||
Catching this error will catch both
|
||||
:exc:`~requests.exceptions.ConnectTimeout` and
|
||||
:exc:`~requests.exceptions.ReadTimeout` errors.
|
||||
"""
|
||||
|
||||
|
||||
class ConnectTimeout(ConnectionError, Timeout):
|
||||
"""The request timed out while trying to connect to the remote server.
|
||||
|
||||
Requests that produced this error are safe to retry.
|
||||
"""
|
||||
|
||||
|
||||
class ReadTimeout(Timeout):
|
||||
"""The server did not send any data in the allotted amount of time."""
|
||||
|
||||
|
||||
class URLRequired(RequestException):
|
||||
"""A valid URL is required to make a request."""
|
||||
|
||||
|
||||
class TooManyRedirects(RequestException):
|
||||
"""Too many redirects."""
|
||||
|
||||
|
||||
class MissingSchema(RequestException, ValueError):
|
||||
"""The URL scheme (e.g. http or https) is missing."""
|
||||
|
||||
|
||||
class InvalidSchema(RequestException, ValueError):
|
||||
"""The URL scheme provided is either invalid or unsupported."""
|
||||
|
||||
|
||||
class InvalidURL(RequestException, ValueError):
|
||||
"""The URL provided was somehow invalid."""
|
||||
|
||||
|
||||
class InvalidHeader(RequestException, ValueError):
|
||||
"""The header value provided was somehow invalid."""
|
||||
|
||||
|
||||
class InvalidProxyURL(InvalidURL):
|
||||
"""The proxy URL provided is invalid."""
|
||||
|
||||
|
||||
class ChunkedEncodingError(RequestException):
|
||||
"""The server declared chunked encoding but sent an invalid chunk."""
|
||||
|
||||
|
||||
class ContentDecodingError(RequestException, BaseHTTPError):
|
||||
"""Failed to decode response content."""
|
||||
|
||||
|
||||
class StreamConsumedError(RequestException, TypeError):
|
||||
"""The content for this response was already consumed."""
|
||||
|
||||
|
||||
class RetryError(RequestException):
|
||||
"""Custom retries logic failed"""
|
||||
|
||||
|
||||
class UnrewindableBodyError(RequestException):
|
||||
"""Requests encountered an error when trying to rewind a body."""
|
||||
|
||||
|
||||
# Warnings
|
||||
|
||||
|
||||
class RequestsWarning(Warning):
|
||||
"""Base warning for Requests."""
|
||||
|
||||
|
||||
class FileModeWarning(RequestsWarning, DeprecationWarning):
|
||||
"""A file was opened in text mode, but Requests determined its binary length."""
|
||||
|
||||
|
||||
class RequestsDependencyWarning(RequestsWarning):
|
||||
"""An imported dependency doesn't match the expected version range."""
|
||||
@@ -1,134 +0,0 @@
|
||||
"""Module containing bug report helper(s)."""
|
||||
|
||||
import json
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import idna
|
||||
import urllib3
|
||||
|
||||
from . import __version__ as requests_version
|
||||
|
||||
try:
|
||||
import charset_normalizer
|
||||
except ImportError:
|
||||
charset_normalizer = None
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
chardet = None
|
||||
|
||||
try:
|
||||
from urllib3.contrib import pyopenssl
|
||||
except ImportError:
|
||||
pyopenssl = None
|
||||
OpenSSL = None
|
||||
cryptography = None
|
||||
else:
|
||||
import cryptography
|
||||
import OpenSSL
|
||||
|
||||
|
||||
def _implementation():
|
||||
"""Return a dict with the Python implementation and version.
|
||||
|
||||
Provide both the name and the version of the Python implementation
|
||||
currently running. For example, on CPython 3.10.3 it will return
|
||||
{'name': 'CPython', 'version': '3.10.3'}.
|
||||
|
||||
This function works best on CPython and PyPy: in particular, it probably
|
||||
doesn't work for Jython or IronPython. Future investigation should be done
|
||||
to work out the correct shape of the code for those platforms.
|
||||
"""
|
||||
implementation = platform.python_implementation()
|
||||
|
||||
if implementation == "CPython":
|
||||
implementation_version = platform.python_version()
|
||||
elif implementation == "PyPy":
|
||||
implementation_version = "{}.{}.{}".format(
|
||||
sys.pypy_version_info.major,
|
||||
sys.pypy_version_info.minor,
|
||||
sys.pypy_version_info.micro,
|
||||
)
|
||||
if sys.pypy_version_info.releaselevel != "final":
|
||||
implementation_version = "".join(
|
||||
[implementation_version, sys.pypy_version_info.releaselevel]
|
||||
)
|
||||
elif implementation == "Jython":
|
||||
implementation_version = platform.python_version() # Complete Guess
|
||||
elif implementation == "IronPython":
|
||||
implementation_version = platform.python_version() # Complete Guess
|
||||
else:
|
||||
implementation_version = "Unknown"
|
||||
|
||||
return {"name": implementation, "version": implementation_version}
|
||||
|
||||
|
||||
def info():
|
||||
"""Generate information for a bug report."""
|
||||
try:
|
||||
platform_info = {
|
||||
"system": platform.system(),
|
||||
"release": platform.release(),
|
||||
}
|
||||
except OSError:
|
||||
platform_info = {
|
||||
"system": "Unknown",
|
||||
"release": "Unknown",
|
||||
}
|
||||
|
||||
implementation_info = _implementation()
|
||||
urllib3_info = {"version": urllib3.__version__}
|
||||
charset_normalizer_info = {"version": None}
|
||||
chardet_info = {"version": None}
|
||||
if charset_normalizer:
|
||||
charset_normalizer_info = {"version": charset_normalizer.__version__}
|
||||
if chardet:
|
||||
chardet_info = {"version": chardet.__version__}
|
||||
|
||||
pyopenssl_info = {
|
||||
"version": None,
|
||||
"openssl_version": "",
|
||||
}
|
||||
if OpenSSL:
|
||||
pyopenssl_info = {
|
||||
"version": OpenSSL.__version__,
|
||||
"openssl_version": f"{OpenSSL.SSL.OPENSSL_VERSION_NUMBER:x}",
|
||||
}
|
||||
cryptography_info = {
|
||||
"version": getattr(cryptography, "__version__", ""),
|
||||
}
|
||||
idna_info = {
|
||||
"version": getattr(idna, "__version__", ""),
|
||||
}
|
||||
|
||||
system_ssl = ssl.OPENSSL_VERSION_NUMBER
|
||||
system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""}
|
||||
|
||||
return {
|
||||
"platform": platform_info,
|
||||
"implementation": implementation_info,
|
||||
"system_ssl": system_ssl_info,
|
||||
"using_pyopenssl": pyopenssl is not None,
|
||||
"using_charset_normalizer": chardet is None,
|
||||
"pyOpenSSL": pyopenssl_info,
|
||||
"urllib3": urllib3_info,
|
||||
"chardet": chardet_info,
|
||||
"charset_normalizer": charset_normalizer_info,
|
||||
"cryptography": cryptography_info,
|
||||
"idna": idna_info,
|
||||
"requests": {
|
||||
"version": requests_version,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Pretty-print the bug information as JSON."""
|
||||
print(json.dumps(info(), sort_keys=True, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,33 +0,0 @@
|
||||
"""
|
||||
requests.hooks
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module provides the capabilities for the Requests hooks system.
|
||||
|
||||
Available hooks:
|
||||
|
||||
``response``:
|
||||
The response generated from a Request.
|
||||
"""
|
||||
HOOKS = ["response"]
|
||||
|
||||
|
||||
def default_hooks():
|
||||
return {event: [] for event in HOOKS}
|
||||
|
||||
|
||||
# TODO: response is the only one
|
||||
|
||||
|
||||
def dispatch_hook(key, hooks, hook_data, **kwargs):
|
||||
"""Dispatches a hook dictionary on a given piece of data."""
|
||||
hooks = hooks or {}
|
||||
hooks = hooks.get(key)
|
||||
if hooks:
|
||||
if hasattr(hooks, "__call__"):
|
||||
hooks = [hooks]
|
||||
for hook in hooks:
|
||||
_hook_data = hook(hook_data, **kwargs)
|
||||
if _hook_data is not None:
|
||||
hook_data = _hook_data
|
||||
return hook_data
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
import sys
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
import warnings
|
||||
|
||||
import charset_normalizer as chardet
|
||||
|
||||
warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer")
|
||||
|
||||
# This code exists for backwards compatibility reasons.
|
||||
# I don't like it either. Just look the other way. :)
|
||||
|
||||
for package in ("urllib3", "idna"):
|
||||
locals()[package] = __import__(package)
|
||||
# This traversal is apparently necessary such that the identities are
|
||||
# preserved (requests.packages.urllib3.* is urllib3.*)
|
||||
for mod in list(sys.modules):
|
||||
if mod == package or mod.startswith(f"{package}."):
|
||||
sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
|
||||
|
||||
target = chardet.__name__
|
||||
for mod in list(sys.modules):
|
||||
if mod == target or mod.startswith(f"{target}."):
|
||||
target = target.replace(target, "chardet")
|
||||
sys.modules[f"requests.packages.{target}"] = sys.modules[mod]
|
||||
# Kinda cool, though, right?
|
||||
@@ -1,833 +0,0 @@
|
||||
"""
|
||||
requests.sessions
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides a Session object to manage and persist settings across
|
||||
requests (cookies, auth, proxies).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from ._internal_utils import to_native_string
|
||||
from .adapters import HTTPAdapter
|
||||
from .auth import _basic_auth_str
|
||||
from .compat import Mapping, cookielib, urljoin, urlparse
|
||||
from .cookies import (
|
||||
RequestsCookieJar,
|
||||
cookiejar_from_dict,
|
||||
extract_cookies_to_jar,
|
||||
merge_cookies,
|
||||
)
|
||||
from .exceptions import (
|
||||
ChunkedEncodingError,
|
||||
ContentDecodingError,
|
||||
InvalidSchema,
|
||||
TooManyRedirects,
|
||||
)
|
||||
from .hooks import default_hooks, dispatch_hook
|
||||
|
||||
# formerly defined here, reexposed here for backward compatibility
|
||||
from .models import ( # noqa: F401
|
||||
DEFAULT_REDIRECT_LIMIT,
|
||||
REDIRECT_STATI,
|
||||
PreparedRequest,
|
||||
Request,
|
||||
)
|
||||
from .status_codes import codes
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .utils import ( # noqa: F401
|
||||
DEFAULT_PORTS,
|
||||
default_headers,
|
||||
get_auth_from_url,
|
||||
get_environ_proxies,
|
||||
get_netrc_auth,
|
||||
requote_uri,
|
||||
resolve_proxies,
|
||||
rewind_body,
|
||||
should_bypass_proxies,
|
||||
to_key_val_list,
|
||||
)
|
||||
|
||||
# Preferred clock, based on which one is more accurate on a given system.
|
||||
if sys.platform == "win32":
|
||||
preferred_clock = time.perf_counter
|
||||
else:
|
||||
preferred_clock = time.time
|
||||
|
||||
|
||||
def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
"""Determines appropriate setting for a given request, taking into account
|
||||
the explicit setting on that request, and the setting in the session. If a
|
||||
setting is a dictionary, they will be merged together using `dict_class`
|
||||
"""
|
||||
|
||||
if session_setting is None:
|
||||
return request_setting
|
||||
|
||||
if request_setting is None:
|
||||
return session_setting
|
||||
|
||||
# Bypass if not a dictionary (e.g. verify)
|
||||
if not (
|
||||
isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping)
|
||||
):
|
||||
return request_setting
|
||||
|
||||
merged_setting = dict_class(to_key_val_list(session_setting))
|
||||
merged_setting.update(to_key_val_list(request_setting))
|
||||
|
||||
# Remove keys that are set to None. Extract keys first to avoid altering
|
||||
# the dictionary during iteration.
|
||||
none_keys = [k for (k, v) in merged_setting.items() if v is None]
|
||||
for key in none_keys:
|
||||
del merged_setting[key]
|
||||
|
||||
return merged_setting
|
||||
|
||||
|
||||
def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
|
||||
"""Properly merges both requests and session hooks.
|
||||
|
||||
This is necessary because when request_hooks == {'response': []}, the
|
||||
merge breaks Session hooks entirely.
|
||||
"""
|
||||
if session_hooks is None or session_hooks.get("response") == []:
|
||||
return request_hooks
|
||||
|
||||
if request_hooks is None or request_hooks.get("response") == []:
|
||||
return session_hooks
|
||||
|
||||
return merge_setting(request_hooks, session_hooks, dict_class)
|
||||
|
||||
|
||||
class SessionRedirectMixin:
|
||||
def get_redirect_target(self, resp):
|
||||
"""Receives a Response. Returns a redirect URI or ``None``"""
|
||||
# Due to the nature of how requests processes redirects this method will
|
||||
# be called at least once upon the original response and at least twice
|
||||
# on each subsequent redirect response (if any).
|
||||
# If a custom mixin is used to handle this logic, it may be advantageous
|
||||
# to cache the redirect location onto the response object as a private
|
||||
# attribute.
|
||||
if resp.is_redirect:
|
||||
location = resp.headers["location"]
|
||||
# Currently the underlying http module on py3 decode headers
|
||||
# in latin1, but empirical evidence suggests that latin1 is very
|
||||
# rarely used with non-ASCII characters in HTTP headers.
|
||||
# It is more likely to get UTF8 header rather than latin1.
|
||||
# This causes incorrect handling of UTF8 encoded location headers.
|
||||
# To solve this, we re-encode the location in latin1.
|
||||
location = location.encode("latin1")
|
||||
return to_native_string(location, "utf8")
|
||||
return None
|
||||
|
||||
def should_strip_auth(self, old_url, new_url):
|
||||
"""Decide whether Authorization header should be removed when redirecting"""
|
||||
old_parsed = urlparse(old_url)
|
||||
new_parsed = urlparse(new_url)
|
||||
if old_parsed.hostname != new_parsed.hostname:
|
||||
return True
|
||||
# Special case: allow http -> https redirect when using the standard
|
||||
# ports. This isn't specified by RFC 7235, but is kept to avoid
|
||||
# breaking backwards compatibility with older versions of requests
|
||||
# that allowed any redirects on the same host.
|
||||
if (
|
||||
old_parsed.scheme == "http"
|
||||
and old_parsed.port in (80, None)
|
||||
and new_parsed.scheme == "https"
|
||||
and new_parsed.port in (443, None)
|
||||
):
|
||||
return False
|
||||
|
||||
# Handle default port usage corresponding to scheme.
|
||||
changed_port = old_parsed.port != new_parsed.port
|
||||
changed_scheme = old_parsed.scheme != new_parsed.scheme
|
||||
default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None)
|
||||
if (
|
||||
not changed_scheme
|
||||
and old_parsed.port in default_port
|
||||
and new_parsed.port in default_port
|
||||
):
|
||||
return False
|
||||
|
||||
# Standard case: root URI must match
|
||||
return changed_port or changed_scheme
|
||||
|
||||
def resolve_redirects(
|
||||
self,
|
||||
resp,
|
||||
req,
|
||||
stream=False,
|
||||
timeout=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
proxies=None,
|
||||
yield_requests=False,
|
||||
**adapter_kwargs,
|
||||
):
|
||||
"""Receives a Response. Returns a generator of Responses or Requests."""
|
||||
|
||||
hist = [] # keep track of history
|
||||
|
||||
url = self.get_redirect_target(resp)
|
||||
previous_fragment = urlparse(req.url).fragment
|
||||
while url:
|
||||
prepared_request = req.copy()
|
||||
|
||||
# Update history and keep track of redirects.
|
||||
# resp.history must ignore the original request in this loop
|
||||
hist.append(resp)
|
||||
resp.history = hist[1:]
|
||||
|
||||
try:
|
||||
resp.content # Consume socket so it can be released
|
||||
except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
|
||||
resp.raw.read(decode_content=False)
|
||||
|
||||
if len(resp.history) >= self.max_redirects:
|
||||
raise TooManyRedirects(
|
||||
f"Exceeded {self.max_redirects} redirects.", response=resp
|
||||
)
|
||||
|
||||
# Release the connection back into the pool.
|
||||
resp.close()
|
||||
|
||||
# Handle redirection without scheme (see: RFC 1808 Section 4)
|
||||
if url.startswith("//"):
|
||||
parsed_rurl = urlparse(resp.url)
|
||||
url = ":".join([to_native_string(parsed_rurl.scheme), url])
|
||||
|
||||
# Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2)
|
||||
parsed = urlparse(url)
|
||||
if parsed.fragment == "" and previous_fragment:
|
||||
parsed = parsed._replace(fragment=previous_fragment)
|
||||
elif parsed.fragment:
|
||||
previous_fragment = parsed.fragment
|
||||
url = parsed.geturl()
|
||||
|
||||
# Facilitate relative 'location' headers, as allowed by RFC 7231.
|
||||
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
|
||||
# Compliant with RFC3986, we percent encode the url.
|
||||
if not parsed.netloc:
|
||||
url = urljoin(resp.url, requote_uri(url))
|
||||
else:
|
||||
url = requote_uri(url)
|
||||
|
||||
prepared_request.url = to_native_string(url)
|
||||
|
||||
self.rebuild_method(prepared_request, resp)
|
||||
|
||||
# https://github.com/psf/requests/issues/1084
|
||||
if resp.status_code not in (
|
||||
codes.temporary_redirect,
|
||||
codes.permanent_redirect,
|
||||
):
|
||||
# https://github.com/psf/requests/issues/3490
|
||||
purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding")
|
||||
for header in purged_headers:
|
||||
prepared_request.headers.pop(header, None)
|
||||
prepared_request.body = None
|
||||
|
||||
headers = prepared_request.headers
|
||||
headers.pop("Cookie", None)
|
||||
|
||||
# Extract any cookies sent on the response to the cookiejar
|
||||
# in the new request. Because we've mutated our copied prepared
|
||||
# request, use the old one that we haven't yet touched.
|
||||
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
|
||||
merge_cookies(prepared_request._cookies, self.cookies)
|
||||
prepared_request.prepare_cookies(prepared_request._cookies)
|
||||
|
||||
# Rebuild auth and proxy information.
|
||||
proxies = self.rebuild_proxies(prepared_request, proxies)
|
||||
self.rebuild_auth(prepared_request, resp)
|
||||
|
||||
# A failed tell() sets `_body_position` to `object()`. This non-None
|
||||
# value ensures `rewindable` will be True, allowing us to raise an
|
||||
# UnrewindableBodyError, instead of hanging the connection.
|
||||
rewindable = prepared_request._body_position is not None and (
|
||||
"Content-Length" in headers or "Transfer-Encoding" in headers
|
||||
)
|
||||
|
||||
# Attempt to rewind consumed file-like object.
|
||||
if rewindable:
|
||||
rewind_body(prepared_request)
|
||||
|
||||
# Override the original request.
|
||||
req = prepared_request
|
||||
|
||||
if yield_requests:
|
||||
yield req
|
||||
else:
|
||||
|
||||
resp = self.send(
|
||||
req,
|
||||
stream=stream,
|
||||
timeout=timeout,
|
||||
verify=verify,
|
||||
cert=cert,
|
||||
proxies=proxies,
|
||||
allow_redirects=False,
|
||||
**adapter_kwargs,
|
||||
)
|
||||
|
||||
extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
|
||||
|
||||
# extract redirect url, if any, for the next loop
|
||||
url = self.get_redirect_target(resp)
|
||||
yield resp
|
||||
|
||||
def rebuild_auth(self, prepared_request, response):
|
||||
"""When being redirected we may want to strip authentication from the
|
||||
request to avoid leaking credentials. This method intelligently removes
|
||||
and reapplies authentication where possible to avoid credential loss.
|
||||
"""
|
||||
headers = prepared_request.headers
|
||||
url = prepared_request.url
|
||||
|
||||
if "Authorization" in headers and self.should_strip_auth(
|
||||
response.request.url, url
|
||||
):
|
||||
# If we get redirected to a new host, we should strip out any
|
||||
# authentication headers.
|
||||
del headers["Authorization"]
|
||||
|
||||
# .netrc might have more auth for us on our new host.
|
||||
new_auth = get_netrc_auth(url) if self.trust_env else None
|
||||
if new_auth is not None:
|
||||
prepared_request.prepare_auth(new_auth)
|
||||
|
||||
def rebuild_proxies(self, prepared_request, proxies):
|
||||
"""This method re-evaluates the proxy configuration by considering the
|
||||
environment variables. If we are redirected to a URL covered by
|
||||
NO_PROXY, we strip the proxy configuration. Otherwise, we set missing
|
||||
proxy keys for this URL (in case they were stripped by a previous
|
||||
redirect).
|
||||
|
||||
This method also replaces the Proxy-Authorization header where
|
||||
necessary.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
headers = prepared_request.headers
|
||||
scheme = urlparse(prepared_request.url).scheme
|
||||
new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env)
|
||||
|
||||
if "Proxy-Authorization" in headers:
|
||||
del headers["Proxy-Authorization"]
|
||||
|
||||
try:
|
||||
username, password = get_auth_from_url(new_proxies[scheme])
|
||||
except KeyError:
|
||||
username, password = None, None
|
||||
|
||||
# urllib3 handles proxy authorization for us in the standard adapter.
|
||||
# Avoid appending this to TLS tunneled requests where it may be leaked.
|
||||
if not scheme.startswith('https') and username and password:
|
||||
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
|
||||
|
||||
return new_proxies
|
||||
|
||||
def rebuild_method(self, prepared_request, response):
|
||||
"""When being redirected we may want to change the method of the request
|
||||
based on certain specs or browser behavior.
|
||||
"""
|
||||
method = prepared_request.method
|
||||
|
||||
# https://tools.ietf.org/html/rfc7231#section-6.4.4
|
||||
if response.status_code == codes.see_other and method != "HEAD":
|
||||
method = "GET"
|
||||
|
||||
# Do what the browsers do, despite standards...
|
||||
# First, turn 302s into GETs.
|
||||
if response.status_code == codes.found and method != "HEAD":
|
||||
method = "GET"
|
||||
|
||||
# Second, if a POST is responded to with a 301, turn it into a GET.
|
||||
# This bizarre behaviour is explained in Issue 1704.
|
||||
if response.status_code == codes.moved and method == "POST":
|
||||
method = "GET"
|
||||
|
||||
prepared_request.method = method
|
||||
|
||||
|
||||
class Session(SessionRedirectMixin):
|
||||
"""A Requests session.
|
||||
|
||||
Provides cookie persistence, connection-pooling, and configuration.
|
||||
|
||||
Basic Usage::
|
||||
|
||||
>>> import requests
|
||||
>>> s = requests.Session()
|
||||
>>> s.get('https://httpbin.org/get')
|
||||
<Response [200]>
|
||||
|
||||
Or as a context manager::
|
||||
|
||||
>>> with requests.Session() as s:
|
||||
... s.get('https://httpbin.org/get')
|
||||
<Response [200]>
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
"headers",
|
||||
"cookies",
|
||||
"auth",
|
||||
"proxies",
|
||||
"hooks",
|
||||
"params",
|
||||
"verify",
|
||||
"cert",
|
||||
"adapters",
|
||||
"stream",
|
||||
"trust_env",
|
||||
"max_redirects",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
#: A case-insensitive dictionary of headers to be sent on each
|
||||
#: :class:`Request <Request>` sent from this
|
||||
#: :class:`Session <Session>`.
|
||||
self.headers = default_headers()
|
||||
|
||||
#: Default Authentication tuple or object to attach to
|
||||
#: :class:`Request <Request>`.
|
||||
self.auth = None
|
||||
|
||||
#: Dictionary mapping protocol or protocol and host to the URL of the proxy
|
||||
#: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
|
||||
#: be used on each :class:`Request <Request>`.
|
||||
self.proxies = {}
|
||||
|
||||
#: Event-handling hooks.
|
||||
self.hooks = default_hooks()
|
||||
|
||||
#: Dictionary of querystring data to attach to each
|
||||
#: :class:`Request <Request>`. The dictionary values may be lists for
|
||||
#: representing multivalued query parameters.
|
||||
self.params = {}
|
||||
|
||||
#: Stream response content default.
|
||||
self.stream = False
|
||||
|
||||
#: SSL Verification default.
|
||||
#: Defaults to `True`, requiring requests to verify the TLS certificate at the
|
||||
#: remote end.
|
||||
#: If verify is set to `False`, requests will accept any TLS certificate
|
||||
#: presented by the server, and will ignore hostname mismatches and/or
|
||||
#: expired certificates, which will make your application vulnerable to
|
||||
#: man-in-the-middle (MitM) attacks.
|
||||
#: Only set this to `False` for testing.
|
||||
self.verify = True
|
||||
|
||||
#: SSL client certificate default, if String, path to ssl client
|
||||
#: cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
self.cert = None
|
||||
|
||||
#: Maximum number of redirects allowed. If the request exceeds this
|
||||
#: limit, a :class:`TooManyRedirects` exception is raised.
|
||||
#: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
|
||||
#: 30.
|
||||
self.max_redirects = DEFAULT_REDIRECT_LIMIT
|
||||
|
||||
#: Trust environment settings for proxy configuration, default
|
||||
#: authentication and similar.
|
||||
self.trust_env = True
|
||||
|
||||
#: A CookieJar containing all currently outstanding cookies set on this
|
||||
#: session. By default it is a
|
||||
#: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
|
||||
#: may be any other ``cookielib.CookieJar`` compatible object.
|
||||
self.cookies = cookiejar_from_dict({})
|
||||
|
||||
# Default connection adapters.
|
||||
self.adapters = OrderedDict()
|
||||
self.mount("https://", HTTPAdapter())
|
||||
self.mount("http://", HTTPAdapter())
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def prepare_request(self, request):
|
||||
"""Constructs a :class:`PreparedRequest <PreparedRequest>` for
|
||||
transmission and returns it. The :class:`PreparedRequest` has settings
|
||||
merged from the :class:`Request <Request>` instance and those of the
|
||||
:class:`Session`.
|
||||
|
||||
:param request: :class:`Request` instance to prepare with this
|
||||
session's settings.
|
||||
:rtype: requests.PreparedRequest
|
||||
"""
|
||||
cookies = request.cookies or {}
|
||||
|
||||
# Bootstrap CookieJar.
|
||||
if not isinstance(cookies, cookielib.CookieJar):
|
||||
cookies = cookiejar_from_dict(cookies)
|
||||
|
||||
# Merge with session cookies
|
||||
merged_cookies = merge_cookies(
|
||||
merge_cookies(RequestsCookieJar(), self.cookies), cookies
|
||||
)
|
||||
|
||||
# Set environment's basic authentication if not explicitly set.
|
||||
auth = request.auth
|
||||
if self.trust_env and not auth and not self.auth:
|
||||
auth = get_netrc_auth(request.url)
|
||||
|
||||
p = PreparedRequest()
|
||||
p.prepare(
|
||||
method=request.method.upper(),
|
||||
url=request.url,
|
||||
files=request.files,
|
||||
data=request.data,
|
||||
json=request.json,
|
||||
headers=merge_setting(
|
||||
request.headers, self.headers, dict_class=CaseInsensitiveDict
|
||||
),
|
||||
params=merge_setting(request.params, self.params),
|
||||
auth=merge_setting(auth, self.auth),
|
||||
cookies=merged_cookies,
|
||||
hooks=merge_hooks(request.hooks, self.hooks),
|
||||
)
|
||||
return p
|
||||
|
||||
def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
params=None,
|
||||
data=None,
|
||||
headers=None,
|
||||
cookies=None,
|
||||
files=None,
|
||||
auth=None,
|
||||
timeout=None,
|
||||
allow_redirects=True,
|
||||
proxies=None,
|
||||
hooks=None,
|
||||
stream=None,
|
||||
verify=None,
|
||||
cert=None,
|
||||
json=None,
|
||||
):
|
||||
"""Constructs a :class:`Request <Request>`, prepares it and sends it.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
:param method: method for the new :class:`Request` object.
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param params: (optional) Dictionary or bytes to be sent in the query
|
||||
string for the :class:`Request`.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json to send in the body of the
|
||||
:class:`Request`.
|
||||
:param headers: (optional) Dictionary of HTTP Headers to send with the
|
||||
:class:`Request`.
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the
|
||||
:class:`Request`.
|
||||
:param files: (optional) Dictionary of ``'filename': file-like-objects``
|
||||
for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple or callable to enable
|
||||
Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float, or a :ref:`(connect timeout,
|
||||
read timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple
|
||||
:param allow_redirects: (optional) Set to True by default.
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol or protocol and
|
||||
hostname to the URL of the proxy.
|
||||
:param stream: (optional) whether to immediately download the response
|
||||
content. Defaults to ``False``.
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
||||
the server's TLS certificate, or a string, in which case it must be a path
|
||||
to a CA bundle to use. Defaults to ``True``. When set to
|
||||
``False``, requests will accept any TLS certificate presented by
|
||||
the server, and will ignore hostname mismatches and/or expired
|
||||
certificates, which will make your application vulnerable to
|
||||
man-in-the-middle (MitM) attacks. Setting verify to ``False``
|
||||
may be useful during local development or testing.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem).
|
||||
If Tuple, ('cert', 'key') pair.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
# Create the Request.
|
||||
req = Request(
|
||||
method=method.upper(),
|
||||
url=url,
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data or {},
|
||||
json=json,
|
||||
params=params or {},
|
||||
auth=auth,
|
||||
cookies=cookies,
|
||||
hooks=hooks,
|
||||
)
|
||||
prep = self.prepare_request(req)
|
||||
|
||||
proxies = proxies or {}
|
||||
|
||||
settings = self.merge_environment_settings(
|
||||
prep.url, proxies, stream, verify, cert
|
||||
)
|
||||
|
||||
# Send the request.
|
||||
send_kwargs = {
|
||||
"timeout": timeout,
|
||||
"allow_redirects": allow_redirects,
|
||||
}
|
||||
send_kwargs.update(settings)
|
||||
resp = self.send(prep, **send_kwargs)
|
||||
|
||||
return resp
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
r"""Sends a GET request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault("allow_redirects", True)
|
||||
return self.request("GET", url, **kwargs)
|
||||
|
||||
def options(self, url, **kwargs):
|
||||
r"""Sends a OPTIONS request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault("allow_redirects", True)
|
||||
return self.request("OPTIONS", url, **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
r"""Sends a HEAD request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault("allow_redirects", False)
|
||||
return self.request("HEAD", url, **kwargs)
|
||||
|
||||
def post(self, url, data=None, json=None, **kwargs):
|
||||
r"""Sends a POST request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return self.request("POST", url, data=data, json=json, **kwargs)
|
||||
|
||||
def put(self, url, data=None, **kwargs):
|
||||
r"""Sends a PUT request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return self.request("PUT", url, data=data, **kwargs)
|
||||
|
||||
def patch(self, url, data=None, **kwargs):
|
||||
r"""Sends a PATCH request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
|
||||
object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return self.request("PATCH", url, data=data, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
r"""Sends a DELETE request. Returns :class:`Response` object.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return self.request("DELETE", url, **kwargs)
|
||||
|
||||
def send(self, request, **kwargs):
|
||||
"""Send a given PreparedRequest.
|
||||
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
# Set defaults that the hooks can utilize to ensure they always have
|
||||
# the correct parameters to reproduce the previous request.
|
||||
kwargs.setdefault("stream", self.stream)
|
||||
kwargs.setdefault("verify", self.verify)
|
||||
kwargs.setdefault("cert", self.cert)
|
||||
if "proxies" not in kwargs:
|
||||
kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env)
|
||||
|
||||
# It's possible that users might accidentally send a Request object.
|
||||
# Guard against that specific failure case.
|
||||
if isinstance(request, Request):
|
||||
raise ValueError("You can only send PreparedRequests.")
|
||||
|
||||
# Set up variables needed for resolve_redirects and dispatching of hooks
|
||||
allow_redirects = kwargs.pop("allow_redirects", True)
|
||||
stream = kwargs.get("stream")
|
||||
hooks = request.hooks
|
||||
|
||||
# Get the appropriate adapter to use
|
||||
adapter = self.get_adapter(url=request.url)
|
||||
|
||||
# Start time (approximately) of the request
|
||||
start = preferred_clock()
|
||||
|
||||
# Send the request
|
||||
r = adapter.send(request, **kwargs)
|
||||
|
||||
# Total elapsed time of the request (approximately)
|
||||
elapsed = preferred_clock() - start
|
||||
r.elapsed = timedelta(seconds=elapsed)
|
||||
|
||||
# Response manipulation hooks
|
||||
r = dispatch_hook("response", hooks, r, **kwargs)
|
||||
|
||||
# Persist cookies
|
||||
if r.history:
|
||||
|
||||
# If the hooks create history then we want those cookies too
|
||||
for resp in r.history:
|
||||
extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
|
||||
|
||||
extract_cookies_to_jar(self.cookies, request, r.raw)
|
||||
|
||||
# Resolve redirects if allowed.
|
||||
if allow_redirects:
|
||||
# Redirect resolving generator.
|
||||
gen = self.resolve_redirects(r, request, **kwargs)
|
||||
history = [resp for resp in gen]
|
||||
else:
|
||||
history = []
|
||||
|
||||
# Shuffle things around if there's history.
|
||||
if history:
|
||||
# Insert the first (original) request at the start
|
||||
history.insert(0, r)
|
||||
# Get the last request made
|
||||
r = history.pop()
|
||||
r.history = history
|
||||
|
||||
# If redirects aren't being followed, store the response on the Request for Response.next().
|
||||
if not allow_redirects:
|
||||
try:
|
||||
r._next = next(
|
||||
self.resolve_redirects(r, request, yield_requests=True, **kwargs)
|
||||
)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
if not stream:
|
||||
r.content
|
||||
|
||||
return r
|
||||
|
||||
def merge_environment_settings(self, url, proxies, stream, verify, cert):
|
||||
"""
|
||||
Check the environment and merge it with some settings.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
# Gather clues from the surrounding environment.
|
||||
if self.trust_env:
|
||||
# Set environment's proxies.
|
||||
no_proxy = proxies.get("no_proxy") if proxies is not None else None
|
||||
env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
|
||||
for (k, v) in env_proxies.items():
|
||||
proxies.setdefault(k, v)
|
||||
|
||||
# Look for requests environment configuration
|
||||
# and be compatible with cURL.
|
||||
if verify is True or verify is None:
|
||||
verify = (
|
||||
os.environ.get("REQUESTS_CA_BUNDLE")
|
||||
or os.environ.get("CURL_CA_BUNDLE")
|
||||
or verify
|
||||
)
|
||||
|
||||
# Merge all the kwargs.
|
||||
proxies = merge_setting(proxies, self.proxies)
|
||||
stream = merge_setting(stream, self.stream)
|
||||
verify = merge_setting(verify, self.verify)
|
||||
cert = merge_setting(cert, self.cert)
|
||||
|
||||
return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
|
||||
|
||||
def get_adapter(self, url):
|
||||
"""
|
||||
Returns the appropriate connection adapter for the given URL.
|
||||
|
||||
:rtype: requests.adapters.BaseAdapter
|
||||
"""
|
||||
for (prefix, adapter) in self.adapters.items():
|
||||
|
||||
if url.lower().startswith(prefix.lower()):
|
||||
return adapter
|
||||
|
||||
# Nothing matches :-/
|
||||
raise InvalidSchema(f"No connection adapters were found for {url!r}")
|
||||
|
||||
def close(self):
|
||||
"""Closes all adapters and as such the session"""
|
||||
for v in self.adapters.values():
|
||||
v.close()
|
||||
|
||||
def mount(self, prefix, adapter):
|
||||
"""Registers a connection adapter to a prefix.
|
||||
|
||||
Adapters are sorted in descending order by prefix length.
|
||||
"""
|
||||
self.adapters[prefix] = adapter
|
||||
keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
|
||||
|
||||
for key in keys_to_move:
|
||||
self.adapters[key] = self.adapters.pop(key)
|
||||
|
||||
def __getstate__(self):
|
||||
state = {attr: getattr(self, attr, None) for attr in self.__attrs__}
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
|
||||
def session():
|
||||
"""
|
||||
Returns a :class:`Session` for context-management.
|
||||
|
||||
.. deprecated:: 1.0.0
|
||||
|
||||
This method has been deprecated since version 1.0.0 and is only kept for
|
||||
backwards compatibility. New code should use :class:`~requests.sessions.Session`
|
||||
to create a session. This may be removed at a future date.
|
||||
|
||||
:rtype: Session
|
||||
"""
|
||||
return Session()
|
||||
@@ -1,128 +0,0 @@
|
||||
r"""
|
||||
The ``codes`` object defines a mapping from common names for HTTP statuses
|
||||
to their numerical codes, accessible either as attributes or as dictionary
|
||||
items.
|
||||
|
||||
Example::
|
||||
|
||||
>>> import requests
|
||||
>>> requests.codes['temporary_redirect']
|
||||
307
|
||||
>>> requests.codes.teapot
|
||||
418
|
||||
>>> requests.codes['\o/']
|
||||
200
|
||||
|
||||
Some codes have multiple names, and both upper- and lower-case versions of
|
||||
the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
|
||||
``codes.okay`` all correspond to the HTTP status code 200.
|
||||
"""
|
||||
|
||||
from .structures import LookupDict
|
||||
|
||||
_codes = {
|
||||
# Informational.
|
||||
100: ("continue",),
|
||||
101: ("switching_protocols",),
|
||||
102: ("processing",),
|
||||
103: ("checkpoint",),
|
||||
122: ("uri_too_long", "request_uri_too_long"),
|
||||
200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
|
||||
201: ("created",),
|
||||
202: ("accepted",),
|
||||
203: ("non_authoritative_info", "non_authoritative_information"),
|
||||
204: ("no_content",),
|
||||
205: ("reset_content", "reset"),
|
||||
206: ("partial_content", "partial"),
|
||||
207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"),
|
||||
208: ("already_reported",),
|
||||
226: ("im_used",),
|
||||
# Redirection.
|
||||
300: ("multiple_choices",),
|
||||
301: ("moved_permanently", "moved", "\\o-"),
|
||||
302: ("found",),
|
||||
303: ("see_other", "other"),
|
||||
304: ("not_modified",),
|
||||
305: ("use_proxy",),
|
||||
306: ("switch_proxy",),
|
||||
307: ("temporary_redirect", "temporary_moved", "temporary"),
|
||||
308: (
|
||||
"permanent_redirect",
|
||||
"resume_incomplete",
|
||||
"resume",
|
||||
), # "resume" and "resume_incomplete" to be removed in 3.0
|
||||
# Client Error.
|
||||
400: ("bad_request", "bad"),
|
||||
401: ("unauthorized",),
|
||||
402: ("payment_required", "payment"),
|
||||
403: ("forbidden",),
|
||||
404: ("not_found", "-o-"),
|
||||
405: ("method_not_allowed", "not_allowed"),
|
||||
406: ("not_acceptable",),
|
||||
407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"),
|
||||
408: ("request_timeout", "timeout"),
|
||||
409: ("conflict",),
|
||||
410: ("gone",),
|
||||
411: ("length_required",),
|
||||
412: ("precondition_failed", "precondition"),
|
||||
413: ("request_entity_too_large",),
|
||||
414: ("request_uri_too_large",),
|
||||
415: ("unsupported_media_type", "unsupported_media", "media_type"),
|
||||
416: (
|
||||
"requested_range_not_satisfiable",
|
||||
"requested_range",
|
||||
"range_not_satisfiable",
|
||||
),
|
||||
417: ("expectation_failed",),
|
||||
418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
|
||||
421: ("misdirected_request",),
|
||||
422: ("unprocessable_entity", "unprocessable"),
|
||||
423: ("locked",),
|
||||
424: ("failed_dependency", "dependency"),
|
||||
425: ("unordered_collection", "unordered"),
|
||||
426: ("upgrade_required", "upgrade"),
|
||||
428: ("precondition_required", "precondition"),
|
||||
429: ("too_many_requests", "too_many"),
|
||||
431: ("header_fields_too_large", "fields_too_large"),
|
||||
444: ("no_response", "none"),
|
||||
449: ("retry_with", "retry"),
|
||||
450: ("blocked_by_windows_parental_controls", "parental_controls"),
|
||||
451: ("unavailable_for_legal_reasons", "legal_reasons"),
|
||||
499: ("client_closed_request",),
|
||||
# Server Error.
|
||||
500: ("internal_server_error", "server_error", "/o\\", "✗"),
|
||||
501: ("not_implemented",),
|
||||
502: ("bad_gateway",),
|
||||
503: ("service_unavailable", "unavailable"),
|
||||
504: ("gateway_timeout",),
|
||||
505: ("http_version_not_supported", "http_version"),
|
||||
506: ("variant_also_negotiates",),
|
||||
507: ("insufficient_storage",),
|
||||
509: ("bandwidth_limit_exceeded", "bandwidth"),
|
||||
510: ("not_extended",),
|
||||
511: ("network_authentication_required", "network_auth", "network_authentication"),
|
||||
}
|
||||
|
||||
codes = LookupDict(name="status_codes")
|
||||
|
||||
|
||||
def _init():
|
||||
for code, titles in _codes.items():
|
||||
for title in titles:
|
||||
setattr(codes, title, code)
|
||||
if not title.startswith(("\\", "/")):
|
||||
setattr(codes, title.upper(), code)
|
||||
|
||||
def doc(code):
|
||||
names = ", ".join(f"``{n}``" for n in _codes[code])
|
||||
return "* %d: %s" % (code, names)
|
||||
|
||||
global __doc__
|
||||
__doc__ = (
|
||||
__doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes))
|
||||
if __doc__ is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
_init()
|
||||
@@ -1,99 +0,0 @@
|
||||
"""
|
||||
requests.structures
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Data structures that power Requests.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from .compat import Mapping, MutableMapping
|
||||
|
||||
|
||||
class CaseInsensitiveDict(MutableMapping):
|
||||
"""A case-insensitive ``dict``-like object.
|
||||
|
||||
Implements all methods and operations of
|
||||
``MutableMapping`` as well as dict's ``copy``. Also
|
||||
provides ``lower_items``.
|
||||
|
||||
All keys are expected to be strings. The structure remembers the
|
||||
case of the last key to be set, and ``iter(instance)``,
|
||||
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
|
||||
will contain case-sensitive keys. However, querying and contains
|
||||
testing is case insensitive::
|
||||
|
||||
cid = CaseInsensitiveDict()
|
||||
cid['Accept'] = 'application/json'
|
||||
cid['aCCEPT'] == 'application/json' # True
|
||||
list(cid) == ['Accept'] # True
|
||||
|
||||
For example, ``headers['content-encoding']`` will return the
|
||||
value of a ``'Content-Encoding'`` response header, regardless
|
||||
of how the header name was originally stored.
|
||||
|
||||
If the constructor, ``.update``, or equality comparison
|
||||
operations are given keys that have equal ``.lower()``s, the
|
||||
behavior is undefined.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, **kwargs):
|
||||
self._store = OrderedDict()
|
||||
if data is None:
|
||||
data = {}
|
||||
self.update(data, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# Use the lowercased key for lookups, but store the actual
|
||||
# key alongside the value.
|
||||
self._store[key.lower()] = (key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._store[key.lower()]
|
||||
|
||||
def __iter__(self):
|
||||
return (casedkey for casedkey, mappedvalue in self._store.values())
|
||||
|
||||
def __len__(self):
|
||||
return len(self._store)
|
||||
|
||||
def lower_items(self):
|
||||
"""Like iteritems(), but with all lowercase keys."""
|
||||
return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Mapping):
|
||||
other = CaseInsensitiveDict(other)
|
||||
else:
|
||||
return NotImplemented
|
||||
# Compare insensitively
|
||||
return dict(self.lower_items()) == dict(other.lower_items())
|
||||
|
||||
# Copy is required
|
||||
def copy(self):
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self.items()))
|
||||
|
||||
|
||||
class LookupDict(dict):
|
||||
"""Dictionary lookup object."""
|
||||
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
super().__init__()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<lookup '{self.name}'>"
|
||||
|
||||
def __getitem__(self, key):
|
||||
# We allow fall-through here, so values default to None
|
||||
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.__dict__.get(key, default)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from .steamgrid import *
|
||||
from .enums import *
|
||||
from .asset import *
|
||||
from .game import *
|
||||
from .author import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,220 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from typing import Tuple, Iterator, Any
|
||||
|
||||
from .http import HTTPClient
|
||||
from .author import Author
|
||||
from .enums import AssetType
|
||||
|
||||
__all__ = (
|
||||
'Grid',
|
||||
'Hero',
|
||||
'Logo',
|
||||
'Icon',
|
||||
)
|
||||
|
||||
class Asset:
|
||||
"""Base class for all assets.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
.. describe:: x == y
|
||||
Checks if two asset are the same.
|
||||
.. describe:: x != y
|
||||
Checks if two asset are not the same.
|
||||
.. describe:: iter(x)
|
||||
Returns an iterator of ``(field, value)`` pairs. This allows this class
|
||||
to be used as an iterable in list/dict/etc constructions.
|
||||
.. describe:: str(x)
|
||||
Returns a string representation of the asset.
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
id: :class:`str`
|
||||
The asset's ID.
|
||||
author: :class:`Author`
|
||||
The author of the asset.
|
||||
score: :class:`int`
|
||||
The asset's score.
|
||||
width: :class:`int`
|
||||
The asset's width.
|
||||
height: :class:`int`
|
||||
The asset's width.
|
||||
style: :class:`str`
|
||||
The style of the asset.
|
||||
notes: Optional[:class:`str`]
|
||||
Notes about the asset.
|
||||
mime: :class:`str`
|
||||
The MIME type of the asset.
|
||||
language: :class:`str`
|
||||
The language of the asset.
|
||||
url: :class:`str`
|
||||
The URL of the asset.
|
||||
thumbnail: :class:`str`
|
||||
The URL of the asset's thumbnail.
|
||||
type: :class:`AssetType`
|
||||
The type of the asset.
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
'_payload',
|
||||
'_http',
|
||||
'id',
|
||||
'score',
|
||||
'width',
|
||||
'height',
|
||||
'style',
|
||||
'_nsfw',
|
||||
'_humor',
|
||||
'notes',
|
||||
'mime',
|
||||
'language',
|
||||
'url',
|
||||
'thumbnail',
|
||||
'_lock',
|
||||
'_epilepsy',
|
||||
'type',
|
||||
'author'
|
||||
)
|
||||
|
||||
def __init__(self, payload: dict, type: AssetType, http: HTTPClient) -> None:
|
||||
self._payload = payload
|
||||
self._http = http
|
||||
self._from_data(payload)
|
||||
self.type = type
|
||||
|
||||
def _from_data(self, asset: dict):
|
||||
self.id = asset.get('id')
|
||||
self.author: Author = Author(asset['author'])
|
||||
self.score = asset.get('score')
|
||||
self.width = asset.get('width')
|
||||
self.height = asset.get('height')
|
||||
self.style = asset.get('style')
|
||||
self._nsfw = asset.get('nsfw')
|
||||
self._humor = asset.get('humor')
|
||||
self.notes = asset.get('notes', None)
|
||||
self.mime = asset.get('mime')
|
||||
self.language = asset.get('language')
|
||||
self.url = asset.get('url')
|
||||
self.thumbnail = asset.get('thumb')
|
||||
self._lock = asset.get('lock')
|
||||
self._epilepsy = asset.get('epilepsy')
|
||||
self.upvotes = asset.get('upvotes')
|
||||
self.downvotes = asset.get('downvotes')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.url
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == other.id
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return self.id != other.id
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
for attr in self.__slots__:
|
||||
if attr[0] != '_':
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None:
|
||||
yield (attr, value)
|
||||
|
||||
def to_json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
def is_lock(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is locked."""
|
||||
return self.lock
|
||||
|
||||
def is_humor(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is a humor asset."""
|
||||
return self.humor
|
||||
|
||||
def is_nsfw(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is NSFW."""
|
||||
return self.nsfw
|
||||
|
||||
def is_epilepsy(self) -> bool:
|
||||
""":class:`bool`: Returns whether the asset is epilepsy-inducing."""
|
||||
return self.is_epilepsy
|
||||
|
||||
|
||||
class Grid(Asset):
|
||||
def __init__(self, payload: dict, http: HTTPClient) -> None:
|
||||
super().__init__(
|
||||
payload,
|
||||
AssetType.Grid,
|
||||
http
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Grid id={self.id} height={self.height} width={self.width} author={self.author.name}>'
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the grid."""
|
||||
self._http.delete_grid([self.id])
|
||||
|
||||
class Hero(Asset):
|
||||
def __init__(self, payload: dict, http: HTTPClient) -> None:
|
||||
super().__init__(
|
||||
payload,
|
||||
AssetType.Hero,
|
||||
http
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Hero id={self.id} height={self.height} width={self.width} author={self.author.name}>'
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the hero."""
|
||||
self._http.delete_hero([self.id])
|
||||
|
||||
class Logo(Asset):
|
||||
def __init__(self, payload: dict, http: HTTPClient) -> None:
|
||||
super().__init__(
|
||||
payload,
|
||||
AssetType.Logo,
|
||||
http
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Logo id={self.id} height={self.height} width={self.width} author={self.author.name}>'
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the logo."""
|
||||
self._http.delete_logo([self.id])
|
||||
|
||||
class Icon(Asset):
|
||||
def __init__(self, payload: dict, http: HTTPClient) -> None:
|
||||
super().__init__(
|
||||
payload,
|
||||
AssetType.Icon,
|
||||
http
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Icon id={self.id} height={self.height} width={self.width} author={self.author.name}>'
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Delete the icon."""
|
||||
self._http.delete_icon([self.id])
|
||||
@@ -1,93 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from typing import Iterator, Tuple, Any
|
||||
|
||||
__all__ = (
|
||||
'Author',
|
||||
)
|
||||
|
||||
class Author:
|
||||
"""Represents a custom author.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
.. describe:: x == y
|
||||
Checks if two author are the same.
|
||||
.. describe:: x != y
|
||||
Checks if two author are not the same.
|
||||
.. describe:: iter(x)
|
||||
Returns an iterator of ``(field, value)`` pairs. This allows this class
|
||||
to be used as an iterable in list/dict/etc constructions.
|
||||
.. describe:: str(x)
|
||||
Returns a string representation of the author.
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the author.
|
||||
steam64: :class:`str`
|
||||
The author's steam64 ID.
|
||||
avatar: :class:`str`
|
||||
The author's avatar URL.
|
||||
|
||||
"""
|
||||
|
||||
__slots__: Tuple[str, ...] = (
|
||||
'_payload',
|
||||
'name',
|
||||
'steam64',
|
||||
'avatar',
|
||||
)
|
||||
|
||||
def __init__(self, payload: dict) -> None:
|
||||
self._payload = payload
|
||||
self._from_data(payload)
|
||||
|
||||
def _from_data(self, author: dict):
|
||||
self.name = author.get('name')
|
||||
self.steam64 = author.get('steam64')
|
||||
self.avatar = author.get('avatar')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Author name={self.name} steam64={self.steam64}>'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.name == other.name
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return self.name != other.name
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
for attr in self.__slots__:
|
||||
if attr[0] != '_':
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None:
|
||||
yield (attr, value)
|
||||
|
||||
def to_json(self) -> dict:
|
||||
""":class:`dict`: Returns a JSON-compatible representation of the author."""
|
||||
return self._payload
|
||||
@@ -1,75 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
__all__ = [
|
||||
'PlatformType',
|
||||
'StyleType',
|
||||
'MimeType',
|
||||
'ImageType',
|
||||
]
|
||||
|
||||
class PlatformType(Enum):
|
||||
Steam = 'steam'
|
||||
Origin = 'origin'
|
||||
Egs = 'egs'
|
||||
Bnet = 'bnet'
|
||||
Uplay = 'uplay'
|
||||
Flashpoint = 'flashpoint'
|
||||
Eshop = 'eshop'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class StyleType(Enum):
|
||||
Alternate = 'alternate'
|
||||
Blurred = 'blurred'
|
||||
White_logo = 'white_logo'
|
||||
Material = 'material'
|
||||
No_logo = 'no_logo'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class MimeType(Enum):
|
||||
PNG = 'image/png'
|
||||
JPEG = 'image/jpeg'
|
||||
WEBP = 'image/webp'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class ImageType(Enum):
|
||||
Static = 'static '
|
||||
Animated = 'animated'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
class AssetType(Enum):
|
||||
Grid = 'grids'
|
||||
Hero = 'heroes'
|
||||
Logo = 'logoes'
|
||||
Icon = 'icons'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Iterator, Tuple, Any
|
||||
|
||||
__all__ = (
|
||||
'Game',
|
||||
)
|
||||
|
||||
class Game:
|
||||
"""Represents a custom game.
|
||||
|
||||
Depending on the way this object was created, some of the attributes can
|
||||
have a value of ``None``.
|
||||
|
||||
.. container:: operations
|
||||
.. describe:: x == y
|
||||
Checks if two game are the same.
|
||||
.. describe:: x != y
|
||||
Checks if two game are not the same.
|
||||
.. describe:: iter(x)
|
||||
Returns an iterator of ``(field, value)`` pairs. This allows this class
|
||||
to be used as an iterable in list/dict/etc constructions.
|
||||
.. describe:: str(x)
|
||||
Returns a string representation of the game.
|
||||
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
name: :class:`str`
|
||||
The name of the game.
|
||||
id: :class:`int`
|
||||
The game's ID.
|
||||
types: List[:class:`str`]
|
||||
List of game types.
|
||||
verified: :class:`bool`
|
||||
Whether an game is verified or not.
|
||||
release_date: Optional[:class:`datetime`]
|
||||
The release date of the game.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_payload',
|
||||
'id',
|
||||
'name',
|
||||
'types',
|
||||
'verified',
|
||||
'release_date',
|
||||
'_release_date'
|
||||
)
|
||||
|
||||
def __init__(self, payload: dict) -> None:
|
||||
self._payload = payload
|
||||
self._from_data(payload)
|
||||
|
||||
def _from_data(self, game: dict):
|
||||
self.name = game.get('name')
|
||||
self.id = game.get('id')
|
||||
self.types = game.get('types')
|
||||
self.verified = game.get('verified')
|
||||
self._release_date = game.get('release_date', None)
|
||||
self.release_date = datetime.fromtimestamp(game['release_date']) if self._release_date else None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<Game id={self.id} name={self.name}>'
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self.id == other.id
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return self.id != other.id
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, Any]]:
|
||||
for attr in self.__slots__:
|
||||
if attr[0] != '_':
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None:
|
||||
yield (attr, value)
|
||||
|
||||
def to_json(self) -> dict:
|
||||
""":class:`dict`: Returns a JSON-compatible representation of the author."""
|
||||
return self._payload
|
||||
@@ -1,158 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import List
|
||||
|
||||
class HTTPException(Exception):
|
||||
"""Exception raised when the HTTP request fails."""
|
||||
pass
|
||||
|
||||
class HTTPClient:
|
||||
|
||||
BASE_URL = 'https://www.steamgriddb.com/api/v2'
|
||||
|
||||
def __init__(self, auth_key: str):
|
||||
self.session = requests.Session()
|
||||
self.auth_key = auth_key
|
||||
self.session.headers.update({'Authorization': 'Bearer ' + self.auth_key})
|
||||
|
||||
def get(self, endpoint: str, queries: dict = None) -> dict:
|
||||
if queries:
|
||||
responce = self.session.get(endpoint, params=queries)
|
||||
else:
|
||||
responce = self.session.get(endpoint)
|
||||
|
||||
try:
|
||||
payload = responce.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
raise Exception('Responce JSON Decode Error')
|
||||
|
||||
if not payload['success']:
|
||||
error_context = payload['errors'][0]
|
||||
raise HTTPException(f'API Error: ({responce.status_code}) {error_context}')
|
||||
|
||||
return payload['data'] if payload else None
|
||||
|
||||
def post(self, endpoint: str, body: dict = None) -> dict:
|
||||
responce = self.session.post(endpoint, data=body)
|
||||
try:
|
||||
payload = responce.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
raise Exception('Responce JSON Decode Error')
|
||||
|
||||
is_success = payload.get('success', None)
|
||||
if not is_success:
|
||||
error_context = payload['errors'][0] if payload else ''
|
||||
raise HTTPException(f'API Error: ({responce.status_code}) {error_context}')
|
||||
|
||||
return payload['data'] if payload else None
|
||||
|
||||
def delete(self, endpoint: str) -> dict:
|
||||
responce = self.session.delete(endpoint)
|
||||
try:
|
||||
payload = responce.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
raise Exception('Responce JSON Decode Error')
|
||||
|
||||
if not payload['success']:
|
||||
error_context = payload['errors'][0]
|
||||
raise HTTPException(f'API Error: ({responce.status_code}) {error_context}')
|
||||
|
||||
return payload['data'] if payload else None
|
||||
|
||||
def get_game(self, game_id: int, request_type: str) -> dict:
|
||||
if request_type == 'steam':
|
||||
url = self.BASE_URL + '/games/steam/' + str(game_id)
|
||||
elif request_type == 'game':
|
||||
url = self.BASE_URL + '/games/id/' + str(game_id)
|
||||
|
||||
return self.get(url)
|
||||
|
||||
def get_grid(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
request_type: str,
|
||||
platform: str = None,
|
||||
queries: dict = None
|
||||
) -> List[dict]:
|
||||
if request_type == 'game':
|
||||
url = self.BASE_URL + '/grids/game/' + str(game_ids[0])
|
||||
elif request_type == 'platform':
|
||||
url = self.BASE_URL + '/grids/' + platform + '/' + ','.join(str(i) for i in game_ids)
|
||||
|
||||
return self.get(url, queries)
|
||||
|
||||
def delete_grid(self, grid_ids: List[int]):
|
||||
url = self.BASE_URL + '/grids/' + ','.join(str(i) for i in grid_ids)
|
||||
self.delete(url)
|
||||
|
||||
def get_hero(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
request_type: str,
|
||||
platform: str = None,
|
||||
queries: dict = None
|
||||
) -> List[dict]:
|
||||
if request_type == 'game':
|
||||
url = self.BASE_URL + '/heroes/game/' + str(game_ids[0])
|
||||
elif request_type == 'platform':
|
||||
url = self.BASE_URL + '/heroes/' + platform + '/' + ','.join(str(i) for i in game_ids)
|
||||
|
||||
return self.get(url, queries)
|
||||
|
||||
def delete_hero(self, hero_ids: List[int]):
|
||||
url = self.BASE_URL + '/heroes/' + ','.join(str(i) for i in hero_ids)
|
||||
self.delete(url)
|
||||
|
||||
def get_logo(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
request_type: str,
|
||||
platform: str = None,
|
||||
queries: dict = None
|
||||
) -> List[dict]:
|
||||
if request_type == 'game':
|
||||
url = self.BASE_URL + '/logos/game/' + str(game_ids[0])
|
||||
elif request_type == 'platform':
|
||||
url = self.BASE_URL + '/logos/' + platform + '/' + ','.join(str(i) for i in game_ids)
|
||||
|
||||
return self.get(url, queries)
|
||||
|
||||
def delete_logo(self, logo_ids: List[int]):
|
||||
url = self.BASE_URL + '/logos/' + ','.join(str(i) for i in logo_ids)
|
||||
self.delete(url)
|
||||
|
||||
def get_icon(self, game_ids: List[int], request_type: str, platform: str = None, queries: dict = None) -> List[dict]:
|
||||
if request_type == 'game':
|
||||
url = self.BASE_URL + '/icons/game/' + str(game_ids[0])
|
||||
elif request_type == 'platform':
|
||||
url = self.BASE_URL + '/icons/' + platform + '/' + ','.join(str(i) for i in game_ids)
|
||||
|
||||
return self.get(url, queries)
|
||||
|
||||
def delete_icon(self, logo_ids: List[int]):
|
||||
url = self.BASE_URL + '/icons/' + ','.join(str(i) for i in logo_ids)
|
||||
self.delete(url)
|
||||
|
||||
def search_games(self, term: str) -> List[dict]:
|
||||
url = self.BASE_URL + '/search/autocomplete/' + term
|
||||
|
||||
return self.get(url)
|
||||
@@ -1,802 +0,0 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015-present Rapptz
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from .http import HTTPClient
|
||||
from .game import Game
|
||||
from .enums import (
|
||||
StyleType,
|
||||
MimeType,
|
||||
ImageType,
|
||||
PlatformType
|
||||
)
|
||||
from .asset import *
|
||||
|
||||
__all__ = (
|
||||
'SteamGridDB',
|
||||
)
|
||||
|
||||
class SteamGridDB:
|
||||
"""Represents a custom author.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
auth_key: :class:`str`
|
||||
The auth key of the steamgriddb for authorization.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('_http')
|
||||
|
||||
def __init__(self, auth_key: str) -> None:
|
||||
self._http = HTTPClient(auth_key)
|
||||
|
||||
def auth_key(self) -> str:
|
||||
""":class:`str`: Returns the auth key of the steamgriddb.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`str`
|
||||
The auth key of the steamgriddb.
|
||||
"""
|
||||
return self._http.auth_key
|
||||
|
||||
def get_game_by_gameid(
|
||||
self,
|
||||
game_id: int,
|
||||
) -> Optional[Game]:
|
||||
""":class:`Game`: Returns a game by game id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_id: :class:`int`
|
||||
The game id of the game.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If the game_id is not an integer.
|
||||
HTTPException
|
||||
If the game_id is not found.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Game`
|
||||
The game that was fetched.
|
||||
"""
|
||||
if not isinstance(game_id, int):
|
||||
raise TypeError('\'game_id\' must be an integer.')
|
||||
|
||||
payload = self._http.get_game(game_id, 'game')
|
||||
return Game(payload) if payload != [] else None
|
||||
|
||||
def get_game_by_steam_appid(
|
||||
self,
|
||||
app_id: int,
|
||||
) -> Optional[Game]:
|
||||
""":class:`Game`: Returns a game by steam app id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
app_id: :class:`int`
|
||||
The steam app id of the game.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If the app_id is not an integer.
|
||||
HTTPException
|
||||
If the app_id is not found.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`Game`
|
||||
The game that was fetched.
|
||||
"""
|
||||
if not isinstance(app_id, int):
|
||||
raise TypeError('\'app_id\' must be an integer.')
|
||||
|
||||
payload = self._http.get_game(app_id, 'steam')
|
||||
return Game(payload) if payload != [] else None
|
||||
|
||||
def get_grids_by_gameid(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Grid]]:
|
||||
"""Optional[List[:class:`Grid`]] Returns a list of grids by game id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the grids. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the grids. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the grids. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the grids are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the grids are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If the game_id is not found.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Grid`]]
|
||||
The grids that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(i.value for i in styles),
|
||||
'mimes': ','.join(i.value for i in mimes),
|
||||
'types': ','.join(i.value for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
payloads = self._http.get_grid(game_ids, 'game', queries=queries)
|
||||
if payloads != []:
|
||||
return [Grid(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_grids_by_platform(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
platform: PlatformType,
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Grid]]:
|
||||
|
||||
"""Optional[List[:class:`Grid`]] Returns a list of grids by platform.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
platform: :class:`PlatformType`
|
||||
The platform type of the grids.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the grids. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the grids. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the grids. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the grids are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the grids are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Grid`]]
|
||||
The grids that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(platform, PlatformType):
|
||||
raise TypeError('\'platform\' must be a PlatformType.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(str(i) for i in styles),
|
||||
'mimes': ','.join(str(i) for i in mimes),
|
||||
'types': ','.join(str(i) for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_grid(
|
||||
game_ids,
|
||||
'platform',
|
||||
platform=platform.value,
|
||||
queries=queries
|
||||
)
|
||||
if payloads != []:
|
||||
return [Grid(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_heroes_by_gameid(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Hero]]:
|
||||
"""Optional[List[:class:`Hero`]] Returns a list of heroes by game id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the heroes. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the heroes. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the heroes. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the heroes are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the heroes are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Hero`]]
|
||||
The heroes that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(i.value for i in styles),
|
||||
'mimes': ','.join(i.value for i in mimes),
|
||||
'types': ','.join(i.value for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_hero(game_ids, 'game', queries=queries)
|
||||
if payloads != []:
|
||||
return [Hero(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_heroes_by_platform(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
platform: PlatformType,
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Hero]]:
|
||||
"""Optional[List[:class:`Hero`]] Returns a list of heroes by platform.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
platform: :class:`PlatformType`
|
||||
The platform type of the heroes.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the heroes. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the heroes. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the heroes. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the heroes are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the heroes are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Hero`]]
|
||||
The heroes that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(platform, PlatformType):
|
||||
raise TypeError('\'platform\' must be a PlatformType.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(str(i) for i in styles),
|
||||
'mimes': ','.join(str(i) for i in mimes),
|
||||
'types': ','.join(str(i) for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_hero(
|
||||
game_ids,
|
||||
'platform',
|
||||
platform=platform.value,
|
||||
queries=queries
|
||||
)
|
||||
if payloads != []:
|
||||
return [Grid(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_logos_by_gameid(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Logo]]:
|
||||
"""Optional[List[:class:`Logo`]] Returns a list of logos by game id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the logos. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the logos. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the logos. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the logos are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the logos are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Logo`]]
|
||||
The logos that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(i.value for i in styles),
|
||||
'mimes': ','.join(i.value for i in mimes),
|
||||
'types': ','.join(i.value for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_logo(game_ids, 'game', queries=queries)
|
||||
if payloads != []:
|
||||
return [Logo(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_logos_by_platform(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
platform: PlatformType,
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Logo]]:
|
||||
"""Optional[List[:class:`Logo`]] Returns a list of logos by platform.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
platform: :class:`PlatformType`
|
||||
The platform type of the logos.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the logos. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the logos. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the logos. Defaults to all types.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(platform, PlatformType):
|
||||
raise TypeError('\'platform\' must be a PlatformType.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(str(i) for i in styles),
|
||||
'mimes': ','.join(str(i) for i in mimes),
|
||||
'types': ','.join(str(i) for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_logo(
|
||||
game_ids,
|
||||
'platform',
|
||||
platform=platform.value,
|
||||
queries=queries
|
||||
)
|
||||
if payloads != []:
|
||||
return [Logo(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_icons_by_gameid(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Icon]]:
|
||||
"""Optional[List[:class:`Icon`]] Returns a list of icons by game id.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the icons. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the icons. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the icons. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the icons are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the icons are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Icon`]]
|
||||
The icons that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(i.value for i in styles),
|
||||
'mimes': ','.join(i.value for i in mimes),
|
||||
'types': ','.join(i.value for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_icon(game_ids, 'game', queries=queries)
|
||||
if payloads != []:
|
||||
return [Icon(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def get_icons_by_platform(
|
||||
self,
|
||||
game_ids: List[int],
|
||||
platform: PlatformType,
|
||||
styles: List[StyleType] = [],
|
||||
mimes: List[MimeType] = [],
|
||||
types: List[ImageType] = [],
|
||||
is_nsfw: bool = False,
|
||||
is_humor: bool = False,
|
||||
) -> Optional[List[Icon]]:
|
||||
"""Optional[List[:class:`Icon`]] Returns a list of icons by platform.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
game_ids: List[:class:`int`]
|
||||
The game ids of the games.
|
||||
platform: :class:`PlatformType`
|
||||
The platform type of the icons.
|
||||
styles: List[:class:`StyleType`]
|
||||
The styles of the icons. Defaults to all styles.
|
||||
mimes: List[:class:`MimeType`]
|
||||
The mimes of the icons. Defaults to all mimes.
|
||||
types: List[:class:`ImageType`]
|
||||
The types of the icons. Defaults to all types.
|
||||
is_nsfw: :class:`bool`
|
||||
Whether or not the icons are NSFW. Defaults to False.
|
||||
is_humor: :class:`bool`
|
||||
Whether or not the icons are humor. Defaults to False.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Icon`]]
|
||||
The icons that were fetched.
|
||||
"""
|
||||
if not isinstance(game_ids, List):
|
||||
raise TypeError('\'game_ids\' must be a list of integers.')
|
||||
if not isinstance(platform, PlatformType):
|
||||
raise TypeError('\'platform\' must be a PlatformType.')
|
||||
if not isinstance(styles, List):
|
||||
raise TypeError('\'styles\' must be a list of StyleType.')
|
||||
if not isinstance(mimes, List):
|
||||
raise TypeError('\'mimes\' must be a list of MimeType.')
|
||||
if not isinstance(types, List):
|
||||
raise TypeError('\'types\' must be a list of ImageType.')
|
||||
if not isinstance(is_nsfw, bool):
|
||||
raise TypeError('\'is_nsfw\' must be a boolean.')
|
||||
if not isinstance(is_humor, bool):
|
||||
raise TypeError('\'is_humor\' must be a boolean.')
|
||||
|
||||
queries = {
|
||||
'styles': ','.join(str(i) for i in styles),
|
||||
'mimes': ','.join(str(i) for i in mimes),
|
||||
'types': ','.join(str(i) for i in types),
|
||||
'nsfw': str(is_nsfw).lower(),
|
||||
'humor': str(is_humor).lower(),
|
||||
}
|
||||
|
||||
payloads = self._http.get_icon(
|
||||
game_ids,
|
||||
'platform',
|
||||
platform=platform.value,
|
||||
queries=queries
|
||||
)
|
||||
if payloads != []:
|
||||
return [Icon(payload, self._http) for payload in payloads]
|
||||
return None
|
||||
|
||||
def delete_grid(
|
||||
self,
|
||||
grid_ids: List[int],
|
||||
) -> None:
|
||||
"""Deletes list of grid images from the website.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
grid_ids: List[:class:`int`]
|
||||
The grid ids to delete.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
"""
|
||||
if not isinstance(grid_ids, List):
|
||||
raise TypeError('\'grid_ids\' must be a list of integers.')
|
||||
|
||||
self._http.delete_grid(grid_ids)
|
||||
|
||||
def delete_hero(
|
||||
self,
|
||||
hero_ids: List[int],
|
||||
) -> None:
|
||||
"""Deletes list of hero images from the website.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
hero_ids: List[:class:`int`]
|
||||
The hero ids to delete.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
"""
|
||||
if not isinstance(hero_ids, List):
|
||||
raise TypeError('\'hero_ids\' must be a list of integers.')
|
||||
|
||||
self._http.delete_hero(hero_ids)
|
||||
|
||||
def delete_logo(
|
||||
self,
|
||||
logo_ids: List[int],
|
||||
) -> None:
|
||||
"""Deletes list of logo images from the website.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
logo_ids: List[:class:`int`]
|
||||
The logo ids to delete.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
"""
|
||||
if not isinstance(logo_ids, List):
|
||||
raise TypeError('\'logo_ids\' must be a list of integers.')
|
||||
|
||||
self._http.delete_logo(logo_ids)
|
||||
|
||||
def delete_icon(
|
||||
self,
|
||||
icon_ids: List[int],
|
||||
) -> None:
|
||||
"""Deletes list of icon images from the website.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
icon_ids: List[:class:`int`]
|
||||
The icon ids to delete.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
"""
|
||||
if not isinstance(icon_ids, List):
|
||||
raise TypeError('\'icon_ids\' must be a list of integers.')
|
||||
|
||||
self._http.delete_icon(icon_ids)
|
||||
|
||||
def search_game(
|
||||
self,
|
||||
term: str
|
||||
) -> Optional[List[Game]]:
|
||||
"""Searches for games on the website.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
term: :class:`str`
|
||||
The term to search for.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Optional[List[:class:`Game`]]
|
||||
The list of games that match the search term.
|
||||
"""
|
||||
if not isinstance(term, str):
|
||||
raise TypeError('\'term\' must be a string.')
|
||||
|
||||
payloads = self._http.search_games(term)
|
||||
return [Game(payload) for payload in payloads]
|
||||
|
||||
def set_auth_key(
|
||||
self,
|
||||
auth_key: str
|
||||
) -> None:
|
||||
"""Sets the new auth key for the API.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
auth_key: :class:`str`
|
||||
The new auth key to set.
|
||||
|
||||
Raises
|
||||
--------
|
||||
TypeError
|
||||
If one of the parameters is not of the correct type.
|
||||
HTTPException
|
||||
If there is an error with the request.
|
||||
ValueError
|
||||
If the auth key is not valid format.
|
||||
"""
|
||||
if not isinstance(auth_key, str):
|
||||
raise TypeError('\'auth_key\' must be a string.')
|
||||
if len(auth_key) != 32:
|
||||
raise ValueError('\'auth_key\' must be a 32-character string.')
|
||||
|
||||
self._http.session.headers['Authorization'] = f'Bearer {auth_key}'
|
||||
+6075
-564
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
[Desktop Entry]
|
||||
Comment[en_US]=
|
||||
Comment=
|
||||
Exec=/bin/bash -c 'export logged_in_home=$(eval echo "~$(whoami)"); curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/refs/heads/main/NSLPluginInstaller.sh | nohup /bin/bash'
|
||||
GenericName[en_US]=
|
||||
GenericName=
|
||||
Icon=uav
|
||||
MimeType=
|
||||
Name[en_US]=NSL Decky Plugin
|
||||
Name=NSL Decky Plugin
|
||||
Path=
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
TerminalOptions=
|
||||
Type=Application
|
||||
X-DBUS-ServiceName=
|
||||
X-DBUS-StartupType=
|
||||
X-KDE-SubstituteUID=false
|
||||
X-KDE-Username=
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ENVIRONMENT VARIABLES
|
||||
logged_in_user=$(logname 2>/dev/null || whoami)
|
||||
logged_in_home=$(eval echo "~${logged_in_user}")
|
||||
|
||||
# Function to prompt for sudo password
|
||||
prompt_for_sudo() {
|
||||
password=$(zenity --password --title="Authentication Required" --text="Please enter your password to proceed with installation/update.")
|
||||
|
||||
# Validate password
|
||||
echo "$password" | sudo -S -v >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
zenity --error --text="Incorrect password or sudo failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to switch to Game Mode
|
||||
switch_to_game_mode() {
|
||||
echo "Switching to Game Mode..."
|
||||
rm -rf "${logged_in_home}/.config/systemd/user/nslgamescanner.service"
|
||||
unlink "${logged_in_home}/.config/systemd/user/default.target.wants/nslgamescanner.service"
|
||||
systemctl --user daemon-reload
|
||||
qdbus org.kde.Shutdown /Shutdown org.kde.Shutdown.logout
|
||||
}
|
||||
|
||||
# Function to display Zenity messages
|
||||
show_message() {
|
||||
zenity --notification --text="$1" --timeout=1
|
||||
}
|
||||
|
||||
show_update_message() {
|
||||
zenity --notification --text="Updating from $1 to $2..." --timeout=5
|
||||
}
|
||||
|
||||
# Set URLs and paths
|
||||
REPO_URL="https://github.com/moraroy/NonSteamLaunchersDecky/archive/refs/heads/main.zip"
|
||||
GITHUB_URL="https://raw.githubusercontent.com/moraroy/NonSteamLaunchersDecky/main/package.json"
|
||||
LOCAL_DIR="${logged_in_home}/homebrew/plugins/NonSteamLaunchers"
|
||||
|
||||
# Ask the user
|
||||
zenity --question --text="Would you like to install or update the NonSteamLaunchers Decky Plugin?" --title="Install/Update Plugin" --ok-label="Yes" --cancel-label="No"
|
||||
if [ $? -eq 1 ]; then
|
||||
echo "User canceled the installation/update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prompt for sudo once
|
||||
prompt_for_sudo
|
||||
|
||||
# Check for existing directories
|
||||
DECKY_LOADER_EXISTS=false
|
||||
NSL_PLUGIN_EXISTS=false
|
||||
|
||||
if [ -d "${logged_in_home}/homebrew/plugins" ]; then
|
||||
DECKY_LOADER_EXISTS=true
|
||||
fi
|
||||
|
||||
if [ -d "$LOCAL_DIR" ] && [ -n "$(ls -A "$LOCAL_DIR")" ]; then
|
||||
NSL_PLUGIN_EXISTS=true
|
||||
fi
|
||||
|
||||
# Version extraction from JSON (no jq)
|
||||
extract_version() {
|
||||
grep -o '"version": *"[^"]*"' "$1" | sed 's/.*"version": *"\([^"]*\)".*/\1/'
|
||||
}
|
||||
|
||||
fetch_github_version() {
|
||||
version=$(curl -s "$GITHUB_URL" | grep -o '"version": *"[^"]*"' | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
fetch_local_version() {
|
||||
if [ -f "$LOCAL_DIR/package.json" ]; then
|
||||
version=$(extract_version "$LOCAL_DIR/package.json")
|
||||
echo "$version"
|
||||
fi
|
||||
}
|
||||
|
||||
compare_versions() {
|
||||
if [ ! -f "$LOCAL_DIR/package.json" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local_version=$(fetch_local_version)
|
||||
github_version=$(fetch_github_version)
|
||||
|
||||
if [ "$local_version" == "$github_version" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
set +x
|
||||
|
||||
# Sanity checks
|
||||
if $DECKY_LOADER_EXISTS; then
|
||||
if ! $NSL_PLUGIN_EXISTS; then
|
||||
zenity --info --text="Decky Loader is detected but no NSL plugin found. It will now be injected into Game Mode."
|
||||
fi
|
||||
else
|
||||
zenity --error --text="Decky Loader not found. Please install it and re-run the script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Version check
|
||||
compare_versions
|
||||
if [ $? -eq 0 ]; then
|
||||
show_message "No update needed. The plugin is already up-to-date."
|
||||
else
|
||||
local_version=$(fetch_local_version)
|
||||
github_version=$(fetch_github_version)
|
||||
show_update_message "$local_version" "$github_version"
|
||||
|
||||
if $NSL_PLUGIN_EXISTS; then
|
||||
show_message "NSL Plugin detected. Deleting and updating..."
|
||||
echo "$password" | sudo -S rm -rf "$LOCAL_DIR"
|
||||
fi
|
||||
|
||||
show_message "Creating base directory and setting permissions..."
|
||||
|
||||
echo "$password" | sudo -S mkdir -p "$LOCAL_DIR"
|
||||
echo "$password" | sudo -S chmod -R u+rw "$LOCAL_DIR"
|
||||
echo "$password" | sudo -S chown -R "$logged_in_user:$logged_in_user" "$LOCAL_DIR"
|
||||
|
||||
curl -L "$REPO_URL" -o /tmp/NonSteamLaunchersDecky.zip
|
||||
unzip -o /tmp/NonSteamLaunchersDecky.zip -d /tmp/
|
||||
cp -r /tmp/NonSteamLaunchersDecky-main/* "$LOCAL_DIR"
|
||||
|
||||
rm -rf /tmp/NonSteamLaunchersDecky*
|
||||
fi
|
||||
|
||||
set -x
|
||||
cd "$LOCAL_DIR"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Ask to switch to Game Mode
|
||||
zenity --question --text="Plugin installed or updated. Do you want to switch to Game Mode now?" --title="Switch to Game Mode?" --ok-label="Yes" --cancel-label="No"
|
||||
if [ $? -eq 0 ]; then
|
||||
switch_to_game_mode
|
||||
else
|
||||
show_message "Remaining in Desktop Mode."
|
||||
fi
|
||||
+2863
-689
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,25 @@
|
||||
<p align="center"><em>“Who hath ascended up into heaven, or descended? who hath gathered the wind in his fists? who hath bound the waters in a garment? who hath established all the ends of the earth? what is his name, and what is his son's name, if thou canst tell?”</em><br><em>- Proverbs 30:4 (KJV)</em></p>
|
||||
|
||||
<p align="center"><em>“For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.”</em><br><em>“For God sent not his Son into the world to condemn the world; but that the world through him might be saved.”</em><br><em>- John 3:16-17 (KJV)</em></p>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img
|
||||
src="https://github-stats-extended.vercel.app/api?username=moraroy&theme=transparent&show_icons=true&hide_border=true&count_private=true"
|
||||
width="48%"
|
||||
/>
|
||||
<img
|
||||
src="https://streak-stats.demolab.com?user=moraroy&theme=transparent&hide_border=true"
|
||||
width="48%"
|
||||
/>
|
||||
<img
|
||||
src="https://github-stats-extended.vercel.app/api/top-langs/?username=moraroy&theme=transparent&hide_border=true&layout=compact"
|
||||
width="48%"
|
||||
/>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/cchrkk/NSLOSD-DL/raw/main/logo.svg" width=40% height=auto
|
||||
@@ -7,15 +29,16 @@
|
||||
NonSteamLaunchers 🚀
|
||||
</h1>
|
||||
|
||||
This script installs the latest GE-Proton, installs NonSteamLaunchers under one unique Proton prefix folder in your compatdata folder path called "NonSteamLaunchers" and adds them to your Steam Library. It will also add the games automatically on every steam restart.
|
||||
So you can use them on Desktop or in Game Mode.
|
||||
Local Saves and Cloud saves are supported, as well as multiplayer/online support (because youre using the launchers). Obviously, certain anticheat games will not work on linux enviroments; this is on a game to game basis.
|
||||
This script installs the latest UMU & GE-Proton and installs NonSteamLaunchers under one unique Proton prefix folder in your compatdata folder path called "NonSteamLaunchers" and adds them to your Steam Library. It will also add the games automatically in real time and will attempt to remove the games from your library in real time when you uninstall a game from a launcher. Collections for your games, launchers and web shortcuts will also be created/removed per launcher. Play time is tracked for all non steam games as well as boot videos downloaded per shortcut if any exist. Non-Downloadable Game theme music is also applied to your library. Metadata cards are also applied to your library automatically to give your non steam pages some life including Player Count if there are any on Steam!
|
||||
Special ".desktop" files will be created per shortcut allowing you to run the games and launchers outside of Steam as well.
|
||||
NSL can be used on Desktop or in Game Mode, and don't you worry,
|
||||
Local Saves and Cloud saves are supported, as well as multiplayer/online support (because you're using the launchers). Obviously, certain anticheat games will not work on linux enviroments; this is on a game to game basis.
|
||||
|
||||
<h1 align="center">
|
||||
Features ✅
|
||||
</h1>
|
||||
|
||||
- Automatic installation of the most popular launchers in your Steam Deck 🎮
|
||||
- Automatic installation of the most popular launchers for your Steam Deck and Steam Machine on SteamOS 🎮
|
||||
|
||||
- Handle automatically the download and installation of your chosen launchers and the games, artwork included! ⌚️
|
||||
|
||||
@@ -27,82 +50,105 @@ Features ✅
|
||||
|
||||
- Command Line Ready, you can call it from online, heres an example of installing a launcher ``` /bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Epic Games"' ```
|
||||
|
||||
- NSL can in fact be installed on many linux distros, feel free to try, here are some examples of some... ChimeraOS, Nobara and Arch Linux as well as any KDE Environments such as this opensuse - tumbleweed - wayland , if for any reason you find that NonSteamLaunchers installs perfectly or not, let me know!
|
||||
- NSL can in fact be installed on many linux distros, feel free to try, here are some examples of some... Ubuntu LTS, ChimeraOS, Nobara and Arch Linux as well as any KDE Environments such as this opensuse - tumbleweed - wayland , if for any reason you find that NonSteamLaunchers installs perfectly or not, let me know!
|
||||
|
||||
- RemotePlayWhatever is also bundled with NSL to allow for local and co-op play between non steam games, this is created by m4Engi, here is the repo [here](https://github.com/m4dEngi/RemotePlayWhatever)
|
||||
|
||||
- Ludusavi is also pre-installed and setup for NSL for your games save backups. Not all games will work with this yet so bare this in mind when deleted or uninstalling games that are arent backed up yet, here is the repo [here](https://github.com/mtkennerly/ludusavi)
|
||||
- Ludusavi is also pre-installed and setup for NSL for your games save backups. Not all games will work with this yet so bear this in mind when deleted or uninstalling games that are arent backed up yet, here is the repo [here](https://github.com/mtkennerly/ludusavi)
|
||||
|
||||
In both versions of NonSteamLaunchers, Desktop or Decky, NSL will back up your games saves here automatically ```/home/deck/NSLGameSaves``` The Desktop Version only does this once, at the start of when the script is opened and you see the main options list. The decky plugin version does this on every manual scan that you do.
|
||||
|
||||
|
||||
-Pressing "Update Proton GE" in both the Desktop version or the Decky Plugin version, will give you the latest version of Proton GE and UMU. A patch will also be applied to allow Game Streaming from Discord in Game Mode.
|
||||
|
||||
- Pressing the "Music Button" on the top left of your game page will enable/disble the Game Theme Feature. This simply attempts to play your games theme music in the client! You can even change the music if you dont like it with the paste button!. Clicking the button will hide and disable the feature.
|
||||
|
||||
- [UMU Launcher](https://github.com/Open-Wine-Components/umu-launcher) is automatically used and is processed for each game and Launcher. Proton GE will be used where necessary.
|
||||
|
||||
### Notes
|
||||
- With NSL youre able to send notes to each other and communicate to other NSL users via a hashtag in your note at the beginning, write #nsl and leave a space, and then type your actual note. The script will then look for that note and send it through the api and spit it back out for that non-steam game. Everyone who uses NSL will then receive it and it will be added to the "NSL Community Note". This is to allow people to have first hand information about their games right in front of them from others! Currently you can participate only if you send a note! Once you created a note, open up NonSteamLaunchers and press the ❤️. This is an expiremental feature so keep that in mind!
|
||||
|
||||
### Logs
|
||||
- The logs for NSL are located here ``` /home/deck/Downloads/NonSteamLaunchers-install.log ``` and here ``` /home/deck/homebrew/logs/NonSteamLaunchers/ ``` if using the Decky Plugin Version.
|
||||
|
||||
|
||||
# As Seen on
|
||||
just to name a few!...there are much more videos and articles out there just wanted to share some resources on how to install and how the program works.
|
||||
## Videos
|
||||
- [Linus Tech Tips](https://www.youtube.com/watch?v=tdR-bxvQKN8&t=885s) (starting at 14:45)
|
||||
- [GameTechPlanet](https://www.youtube.com/watch?v=jE1qD3yzrks)
|
||||
- [NerdZap](https://www.youtube.com/watch?v=t2EzbKkbS1Q)
|
||||
- [Joserra y sus cosicas](https://www.youtube.com/watch?v=6ETxmbzRODQ)
|
||||
- [Steam Deck In Hand](https://www.youtube.com/watch?v=_j3HV6yyGjI)
|
||||
- [Steam Deck Gaming](https://www.youtube.com/watch?v=svOj4MTEAVc)
|
||||
- [BakaKuma](https://www.youtube.com/watch?v=QluZ3UGYoKo)
|
||||
- [SteamFlow](https://www.youtube.com/watch?v=aud5F6iwA0s)
|
||||
- [Hooandee - 6 Hour Video](https://www.youtube.com/watch?v=OGmwtSS-zoE&t=7023s) (starting at 1:57:23)
|
||||
- [Central Deck](https://www.youtube.com/watch?v=oIKL1JRn4cw)
|
||||
- [Goldenoptic Gaming](https://www.youtube.com/watch?v=dMnUn3U0dPE)
|
||||
- [Deck Ready](https://www.youtube.com/watch?v=9Ap_suofBV8&t=196s) (starting at 3:16)
|
||||
- [Steam Deck Checker](https://www.youtube.com/watch?v=vFRllG15jjs)
|
||||
- [SteamDeckHQ](https://www.tiktok.com/@steamdeckhq/video/7579970230265384223)
|
||||
- [ChoiTech](https://www.youtube.com/watch?v=ucrVWJNQ2rc)
|
||||
|
||||
## Articles
|
||||
- [Gaming On Linux - Non-Steam Launchers Tool for Installing Popular Game Stores](https://www.gamingonlinux.com/2025/01/nonsteamlaunchers-tool-for-installing-popular-game-stores-working-on-better-desktop-linux-support/)
|
||||
- [Steam Deck HQ - Non-Steam Launchers New Update Community Notes](https://steamdeckhq.com/news/nonsteamlaunchers-new-update-community-notes/)
|
||||
- [Windows Central - How to Install Decky Loader on Steam Deck](https://www.windowscentral.com/gaming/how-to-install-decky-loader-on-steam-deck)
|
||||
- [Dexerto - Non-Steam Launchers on Steam Deck](https://www.dexerto.com/tech/nonsteamlaunchers-steam-deck-2808063/)
|
||||
- [MSN - Steam Deck: How to Install Epic Games Launcher with Decky Loader](https://www.msn.com/en-ca/news/technology/steam-deck-how-to-install-epic-games-launcher-with-decky-loader/ar-BB1pW1Ht)
|
||||
- [PCMAG - How to Install Third-Party Game Launchers on Steam Deck](https://www.pcmag.com/how-to/steam-deck-install-third-party-game-launchers)
|
||||
- [dadwithadeck - How to install Non-Steam Game Launchers on Steam Deck with NonSteamLaunchers](https://dadwithadeck.com/2025/11/24/how-to-install-non-steam-game-launchers-on-steam-deck-with-nonsteamlaunchers/)
|
||||
|
||||
<p align="center">
|
||||
▶️ <b>YouTube Tutorials</b> 🡺🡺🡺
|
||||
<a href="https://youtu.be/sxMmI8I9G_g">Decky Plugin</a> |
|
||||
<a href="https://www.youtube.com/watch?v=HHdw4u5enrc">GamingOnSteam</a>
|
||||
🡸🡸🡸 ▶️
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
📖 <b>Step-by-step Articles</b> 🡺🡺🡺
|
||||
<a href="https://steamdeckhq.com/news/nonsteamlaunchers-adds-scan-support-launchers">SteamDeckHQ Guide</a> |
|
||||
<a href="https://gamingonsteam.com/2026/04/30/the-ultimate-guide-to-installing-any-launcher-on-your-steam-deck-nonsteamlaunchers/">GamingOnSteam Guide</a>
|
||||
🡸🡸🡸 📖
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 align="center">
|
||||
Currently Working On 👷♂️
|
||||
</h1>
|
||||
|
||||
* Decky Loader Plugin is available [here](https://github.com/moraroy/NonSteamLaunchersDecky) and the pull request for it [here](https://github.com/SteamDeckHomebrew/decky-plugin-database/pull/677)
|
||||
* Decky Loader Plugin is available [here](https://github.com/moraroy/NonSteamLaunchersDecky) and the pull request for it [here](https://github.com/SteamDeckHomebrew/decky-plugin-database/pull/677) and can be installed with this big button, only press this button if you have Decky Loader installed already
|
||||
* Working on Flatpak version
|
||||
|
||||
<p align="center">
|
||||
<a name="download button" href="https://github.com/moraroy/NonSteamLaunchers-On-Steam-Deck/releases/download/v3.9.6/NSLPlugin.desktop"><img src="https://user-images.githubusercontent.com/98482469/242361563-33f31d3d-9a69-4fca-a928-207a5d17a98f.png" alt="Download NSL Decky Plugin" width="350px" style="padding-top: 15px;"></a>
|
||||
|
||||
---
|
||||
|
||||
**Windows Installation Steps**:
|
||||
|
||||
1. Download the current Windows version of Decky Loader here [link](https://nightly.link/SteamDeckHomebrew/decky-loader/workflows/build-win/main/PluginLoader%20Win.zip)
|
||||
|
||||
2. Download **NSLPluginWindows.exe** from [here](https://github.com/moraroy/NonSteamLaunchersDecky/releases).
|
||||
|
||||
3. Run **NSLPluginWindows.exe** first. This will also create the necessary cef debugging file for Decky Loader.
|
||||
|
||||
4. Run either No_console.exe or Plugin Loader.exe. You can also press Win + R, type shell:startup, and press Enter, then place the .exe there to launch Decky Loader on boot.
|
||||
5. Go into **Game Mode** or **Big Picture Mode** to see the Decky Loader plugin and NonSteamLaunchers.
|
||||
|
||||
|
||||
<p align="center">
|
||||
▶️ YouTube Tutorial 🡺🡺🡺 https://youtu.be/sxMmI8I9G_g 🡸🡸🡸 ▶️
|
||||
</p>
|
||||
<p align="center">
|
||||
📖 Step-by-step Article 🡺🡺🡺 <a href="https://steamdeckhq.com/news/nonsteamlaunchers-adds-scan-support-launchers">here</a> 🡸🡸🡸 📖
|
||||
</p>
|
||||
|
||||
This setup will automatically add all your non-Steam games with artwork, correctly formatted for Windows. Only scanning will work; nothing else will function, so you can either auto-scan or manually scan your games.
|
||||
|
||||
<h1 align="center">
|
||||
Supported Stores 🛍
|
||||
</h1>
|
||||
|
||||
- Amazon Games Launcher ✔️
|
||||
- Battle.net ✔️
|
||||
- EA App ✔️
|
||||
- Epic Games ✔️
|
||||
- GOG Galaxy ✔️
|
||||
- Humble Games Collection ✔️
|
||||
- IndieGala ✔️
|
||||
- Itch.io ✔️
|
||||
- Legacy Games ✔️
|
||||
- Rockstar Games Launcher ✔️
|
||||
- Ubisoft Connect ✔️
|
||||
- Glyph ✔️
|
||||
- Playstation Plus ✔️
|
||||
- VK Play ✔️
|
||||
- HoYoPlay ✔️
|
||||
- Nexon Launcher ✔️
|
||||
|
||||
<h1 align="center">
|
||||
Supported Streaming Sites for games and as well as any website. 🌐
|
||||
</h1>
|
||||
|
||||
- Website Shortcut Creator ✔️
|
||||
- Fortnite ✔️
|
||||
- Xbox Game Pass ✔️
|
||||
- GeForce Now ✔️
|
||||
- Amazon Luna ✔️
|
||||
- Netflix ✔️
|
||||
- Amazon Prime Video ✔️
|
||||
- Disney+ ✔️
|
||||
- Hulu ✔️
|
||||
- Youtube ✔️
|
||||
- Twitch ✔️
|
||||
|
||||
<h1 align="left">
|
||||
Finds Games Automatically
|
||||
</h1>
|
||||
|
||||
"NSLGameScanner.service" is also live when you use this script and continues after the script is closed and even works after your Steam Deck has restarted. This works in the background as a service file to automatically add your games to your library on every Steam restart. Currently adds:
|
||||
- Epic Games 🎮 💾 Full SD Card Support
|
||||
- Ubisoft Connect 🎮
|
||||
- EA App 🎮
|
||||
- Gog Galaxy 🎮 💾 Full SD Card Support
|
||||
- Battle.net 🎮
|
||||
- Amazon Games 🎮 💾 Full SD Card Support
|
||||
- Itch.io 🎮
|
||||
- Legacy Games 🎮
|
||||
|
||||
To stop the NSLGameScanner.service, open up NSL and hit "Stop NSLGameScanner" it will then ask you if you want to restart it, click no, and that's it.
|
||||
|
||||
<h1 align="center">
|
||||
How to Install 🔧
|
||||
How to Install the Desktop Version 🔧
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -113,18 +159,29 @@ How to Install 🔧
|
||||
* Go to desktop mode, click the download button above and it should download the .desktop file in your Downloads folder.
|
||||
* Go to your downloads folder, click the NonSteamLaunchers icon, it will download and run the latest NonSteamLaunchers.sh from this repository and run it.
|
||||
* You will simply have to choose which launcher to install and let the script handle the rest. 💻 No files are left in your "Downloads" they are deleted after installation.
|
||||
* After running the script, launch Steam on your Steam Deck. You'll find the new launchers in your library under the non-steam tab. Click a launcher to see your installed games from that store, and launch them directly from Steam! If you have downloaded a game inside of your launcher, restart your Deck or quit and reopen Steam and the NSLGameScanner.service should add it to your library, even in gamemode! 🥳
|
||||
|
||||
<!--- TODO: handful of broken icons (cf. 🡺🡺🡺 ); probably should remove or replace them with more common font to handle unicode-->
|
||||
|
||||
* After running the script, you'll find the new launchers in your library under the non-steam tab. Click a launcher to see your installed games from that store, and launch them directly from Steam! If you have downloaded a game inside of your launcher, the scanner should have added it to our library, if not simply run a scan. Restart your Deck or quit and reopen Steam and the NSLGameScanner.service should add it to your library, even in gamemode! 🥳
|
||||
|
||||
<p align="center">
|
||||
|
||||
<h1 align="center">
|
||||
How to Uninstall 🗑
|
||||
How to Run 🏃♂️
|
||||
</h1>
|
||||
|
||||
+ Just run the script, and hit "Uninstall". This will uninstall the launcher and its games. Alternatively, if you want to totally wipe everything from NonSteamLaunchers, click "Start Fresh".
|
||||
+ That's it.
|
||||
+ Select your launchers and hit OK. This will install your selected launchers. (Optionally, check "separate app ids", to install each launcher in its own prefix)
|
||||
+ ❤️ = this will send and recieves any notes you have created to the community using the ```#nsl``` tag at the beginning of your note.
|
||||
+ Uninstall = uninstalls the specific launchers and possibly its games too, each launcher uninstallation is different.
|
||||
+ 🔍 = Pressing the magnifying glass will stop the NSL Scanner and prompt you to restart it if needed. When you open NSL the scanner will update from online then auto scan. So restarting it activates the real time service file for active scanning.
|
||||
+ Start Fresh = Wipes all of NSL, all the prefixes, launchers, games, etc. Shortcuts will remain, and your game save backups, if any, at ```/home/deck/NSLGameSaves``` will not be deleted. This essentially "Uninstalls" NonSteamLaunchers.
|
||||
+ Move to SD Card = moves each prefix to your SD Card, this is legacy code and probably still needs work.
|
||||
+ Update Proton GE = this will update and install Proton GE and UMU if you dont have it already, the script attempts to do this on each launcher install but you can do it manually and help the script before hand if you want.
|
||||
+ 🖥️ Off = this simply turns off your screen, useful if your doing long downloads to save battery.
|
||||
+ NSLGameSaves = this will inject your game saves from ```/home/deck/NSLGameSaves``` into its correct locations using ludusavi into your launchers. Use this if you pressed "Start Fresh" and have downloaded your launchers again, dont download your games until you have pressed this button.
|
||||
+ README = opens up this read me file.
|
||||
|
||||
|
||||
|
||||
To stop the NSLGameScanner.service, open up NSL and hit "🔍" it will then ask you if you want to restart it, click no, and that's it.
|
||||
|
||||
|
||||
<h1 align="center">
|
||||
Command Lines 🫡
|
||||
@@ -142,11 +199,126 @@ The NSL script can be called from online via bash, heres an example of it instal
|
||||
|
||||
- The "Move to SD Card" function can only be called in this format
|
||||
|
||||
```/bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Move to SD Card" "EpicGamesLauncher"```
|
||||
```/bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Move to SD Card" "EpicGamesLauncher"'```
|
||||
|
||||
- The format of "EpicGamesLauncher" comes from the user choosing to either "Separate App ID's" or use the default installation prefix "NonSteamLaunchers" in the compatdata folder. This would be named differently for each launcher. Otherwise the command line would then only be
|
||||
|
||||
```/bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Move to SD Card" "NonSteamLaunchers"```
|
||||
```/bin/bash -c 'curl -Ls https://raw.githubusercontent.com/moraroy/NonSteamLaunchers-On-Steam-Deck/main/NonSteamLaunchers.sh | nohup /bin/bash -s -- "Move to SD Card" "NonSteamLaunchers"'```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 align="center">
|
||||
Supported Stores 🛍
|
||||
</h1>
|
||||
|
||||
- Unreal Engine (via Epic Games) ✔️
|
||||
- Amazon Games Launcher ✔️
|
||||
- Battle.net ✔️
|
||||
- EA App ✔️
|
||||
- Epic Games ✔️
|
||||
- GOG Galaxy ✔️
|
||||
- Humble Games Collection ✔️
|
||||
- IndieGala ✔️
|
||||
- itch.io ✔️
|
||||
- Legacy Games ✔️
|
||||
- Rockstar Games Launcher ✔️
|
||||
- Ubisoft Connect ✔️
|
||||
- Glyph ✔️
|
||||
- Playstation Plus ✔️
|
||||
- VK Play ✔️
|
||||
- HoYoPlay ✔️
|
||||
- Nexon Launcher ✔️
|
||||
- Game Jolt Client ✔️
|
||||
- Artix Game Launcher ✔️
|
||||
- ARC Launcher ✔️
|
||||
- Pokémon Trading Card Game Live ✔️
|
||||
- Minecraft Launcher(Legacy) (Java Edition doesnt work but its for Dungeons) ✔️
|
||||
- PURPLE Launcher ✔️
|
||||
- Plarium Play ✔️
|
||||
- VFUN Launcher ✔️
|
||||
- Tempo Launcher ✔️
|
||||
- Antstream Arcade ✔️
|
||||
- Hytale ✔️
|
||||
- Big Fish Games Manager ✔️
|
||||
- Gryphlink ✔️
|
||||
- RemotePlayWhatever ✔️
|
||||
- NVIDIA GeForce NOW (Native Linux) ✔️
|
||||
- STOVE Client ✔️
|
||||
- Moonlight Game Streaming ✔️
|
||||
|
||||
<h1 align="center">
|
||||
Supported Streaming Sites for games and as well as any website. 🌐
|
||||
</h1>
|
||||
|
||||
- Website Shortcut Creator ✔️
|
||||
- Fortnite ✔️
|
||||
- Venge ✔️
|
||||
- PokéRogue ✔️
|
||||
- Xbox Game Pass ✔️
|
||||
- Better xCloud ✔️
|
||||
- GeForce Now ✔️
|
||||
- Amazon Luna ✔️
|
||||
- Boosteroid Cloud Gaming ✔️
|
||||
- Stim.io ✔️
|
||||
- WebRcade ✔️
|
||||
- WebRcade Editor ✔️
|
||||
- Afterplay.io ✔️
|
||||
- OnePlay ✔️
|
||||
- AirGPU ✔️
|
||||
- CloudDeck ✔️
|
||||
- JioGamesCloud ✔️
|
||||
- WatchParty ✔️
|
||||
- Rocketcrab ✔️
|
||||
- Netflix ✔️
|
||||
- Amazon Prime Video ✔️
|
||||
- Disney+ ✔️
|
||||
- Hulu ✔️
|
||||
- Tubi ✔️
|
||||
- Youtube ✔️
|
||||
- Twitch ✔️
|
||||
- Plex ✔️
|
||||
- Apple TV+ ✔️
|
||||
- Crunchyroll ✔️
|
||||
- Super Monkey Ball Online ✔️
|
||||
|
||||
|
||||
<h1 align="left">
|
||||
Finds Games Automatically
|
||||
</h1>
|
||||
|
||||
"NSLGameScanner.service" is also live when you use this script and continues after the script is closed and even works after your Steam Deck has restarted. This works in the background as a service file to automatically add your games to your library on every Steam restart. Currently adds:
|
||||
- Epic Games 🎮 💾 Full SD Card Support
|
||||
- Ubisoft Connect 🎮 💾 Full SD Card Support
|
||||
- EA App 🎮 💾 Full SD Card Support not sure
|
||||
- Gog Galaxy 🎮 💾 Full SD Card Support
|
||||
- Battle.net 🎮
|
||||
- Amazon Games 🎮 💾 Full SD Card Support
|
||||
- Itch.io 🎮
|
||||
- Legacy Games 🎮
|
||||
- VK Play 🎮 💾 Full SD Card Support
|
||||
- HoYoPlay 🎮 💾 Full SD Card Support
|
||||
- Game Jolt Client 🎮 💾 Full SD Card Support
|
||||
- Minecraft Launcher 🎮
|
||||
- Waydroid Apps 🎮 📜 Your own script Support
|
||||
- Humble Games Collection 🎮 💾 Full SD Card Support
|
||||
- NVIDIA GeForce NOW (Native Linux App) - You must "Favorite" the game with the heart. Old favorites will not be picked up, you need to re-favorite.
|
||||
|
||||
## Chrome Bookmarks
|
||||
The scanner will pick these up automatically. But in Chrome for Geforce Now and Boosteroid Cloud Gaming you need to change the name of the bookmark to your actual game name. Or you can press "Play" then Use "Ctrl + D" to edit the bookmarks for the Chrome Browser. As long as the game name is in the Bookmark Name.
|
||||
- Xbox Game Pass
|
||||
- GeForce Now
|
||||
- Amazon Luna
|
||||
- Boosteroid Cloud Gaming
|
||||
|
||||
## Waydroid Detection
|
||||
If you're not in Official SteamOS or not using [this script](https://github.com/ryanrudolfoba/SteamOS-Waydroid-Installer) to install your Waydroid, you can put your own script for the App launch with the name "waydroid-cage.sh" in your own home directory and the scanner will do the rest. A good references for writing your own Script is in [here](https://github.com/SwallowKOR/cachyos-waydroid-gamemode).
|
||||
|
||||
|
||||
|
||||
<!--- TODO: handful of broken icons (cf. 🡺🡺🡺 ); probably should remove or replace them with more common font to handle unicode-->
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -244,38 +416,9 @@ Most importantly, `ruff` is used to lint all python code.
|
||||
|
||||
While not currently enforced, by using [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), it's possible to automatically generate changelogs and version numbers via [release-please](https://github.com/googleapis/release-please).
|
||||
|
||||
To help with that, the [commitizen](https://commitizen-tools.github.io/commitizen/) tool can be installed.
|
||||
To help with that, the [commitizen](https://commitizen-tools.github.io/commitizen/) tool can be installed.
|
||||
|
||||
#### Usage
|
||||
|
||||
```bash
|
||||
# install cz
|
||||
npm install -g commitizen cz-conventional-changelog
|
||||
|
||||
# make repo cz friendly
|
||||
commitizen init cz-conventional-changelog --save-dev --save-exact
|
||||
npm install
|
||||
|
||||
# add file to commit
|
||||
git add .gitignore
|
||||
|
||||
# run cz
|
||||
λ git cz
|
||||
cz-cli@4.3.0, cz-conventional-changelog@3.3.0
|
||||
|
||||
? Select the type of change that you're committing: chore: Other changes that don't modify src or test files
|
||||
? What is the scope of this change (e.g. component or file name): (press enter to skip) .gitignore
|
||||
? Write a short, imperative tense description of the change (max 81 chars):
|
||||
(17) update .gitignore
|
||||
? Provide a longer description of the change: (press enter to skip)
|
||||
|
||||
? Are there any breaking changes? No
|
||||
? Does this change affect any open issues? No
|
||||
[main 0a9920d] chore(.gitignore): update .gitignore
|
||||
1 file changed, 131 insertions(+)
|
||||
|
||||
λ git push
|
||||
```
|
||||
To reduce some of the non-essential tooling, it's been removed from the repo, but feel free to install globally without checking into source control.
|
||||
|
||||
### Formatting
|
||||
|
||||
@@ -292,32 +435,72 @@ cz-cli@4.3.0, cz-conventional-changelog@3.3.0
|
||||
|
||||
### Additional tooling
|
||||
|
||||
#### TODO
|
||||
#### mise
|
||||
|
||||
* Add [devbox](https://www.jetpack.io/devbox/) 👌
|
||||
|
||||
#### asdf
|
||||
|
||||
* Install [asdf](https://asdf-vm.com/guide/getting-started.html#_2-download-asdf)
|
||||
* Add plugins
|
||||
```bash
|
||||
asdf plugin-add python
|
||||
asdf plugin-add poetry https://github.com/asdf-community/asdf-poetry.git
|
||||
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
|
||||
```
|
||||
* Install [mise](https://mise.jdx.dev/getting-started.html)
|
||||
* `curl https://mise.run | sh`
|
||||
* Usage
|
||||
* Install local plugins in repo
|
||||
```bash
|
||||
asdf install
|
||||
```
|
||||
* Install specific plugins
|
||||
```bash
|
||||
# install stable python
|
||||
asdf install python <latest|3.11.4>
|
||||
|
||||
# set stable to system python
|
||||
asdf global python latest
|
||||
```
|
||||
```bash
|
||||
# Install tools from repo
|
||||
mise install
|
||||
|
||||
# List tools
|
||||
mise list uv
|
||||
|
||||
# Install specific plugins
|
||||
mise use uv 0.8.8
|
||||
|
||||
# set stable to system python
|
||||
mise use -g python
|
||||
```
|
||||
|
||||
#### uv
|
||||
|
||||
[uv](https://docs.astral.sh/uv/) is a faster drop-in replacement for `poetry` among other dependency and virtual environment managers written in rust.
|
||||
|
||||
Common operations after [installing](https://docs.astral.sh/uv/getting-started/installation/):
|
||||
|
||||
```bash
|
||||
# create venv w/pinned python version
|
||||
uv venv --python ">=3.11,<3.13"
|
||||
|
||||
# activate venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# add deps
|
||||
uv add redis
|
||||
uv add --optional dev ruff
|
||||
|
||||
# install optional (extra) deps
|
||||
uv pip install -r pyproject.toml --all-extras
|
||||
|
||||
# pip freeze
|
||||
uv pip freeze > requirements.txt
|
||||
```
|
||||
|
||||
#### Ruff
|
||||
|
||||
[Ruff](https://docs.astral.sh/ruff/) is a linter and formatter compatible with various other tools like black, flake8, isort, etc. Like [uv](#uv), it's by Astral and written in rust.
|
||||
|
||||
Install is handled by `uv`. These are a few of my favorite things 🎵:
|
||||
|
||||
```bash
|
||||
# run linter
|
||||
ruff check .
|
||||
|
||||
# formatter
|
||||
ruff format .
|
||||
|
||||
# run tests
|
||||
ruff
|
||||
|
||||
# run tests with coverage
|
||||
ruff --coverage
|
||||
|
||||
# run tests with coverage and open in browser
|
||||
ruff --coverage --open
|
||||
```
|
||||
|
||||
#### shellcheck
|
||||
|
||||
|
||||
@@ -128,8 +128,10 @@ flavor_mapping = {
|
||||
"StarCraft": "S1",
|
||||
"Warcraft Arclight Rumble": "GRY",
|
||||
"Warcraft II: Battle.net Edition": "W2",
|
||||
"Warcraft II: Remastered": "W2R",
|
||||
"Warcraft III: Reforged": "W3",
|
||||
"Warcraft: Orcs & Humans": "W1",
|
||||
"Warcraft I: Remastered": "W1R",
|
||||
"World of Warcraft Classic": "WoWC",
|
||||
"World of Warcraft": "WoW",
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 15 KiB |
Generated
-2009
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"cz-conventional-changelog": "^3.3.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-1813
File diff suppressed because it is too large
Load Diff
+27
-76
@@ -1,86 +1,37 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "non-steam-launchers"
|
||||
version = "3.8.2"
|
||||
version = "4.2.3"
|
||||
description = ""
|
||||
authors = [
|
||||
"moraroy <88516395+moraroy@users.noreply.github.com>",
|
||||
"pythoninthegrass <4097471+pythoninthegrass@users.noreply.github.com>"
|
||||
{ name = "moraroy", email = "88516395+moraroy@users.noreply.github.com" },
|
||||
{ name = "pythoninthegrass", email = "4097471+pythoninthegrass@users.noreply.github.com" },
|
||||
]
|
||||
license = "MIT"
|
||||
requires-python = ">=3.11,<3.13"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.11,<3.13"
|
||||
python-decouple = "^3.8"
|
||||
python-steamgriddb = "^1.0.5"
|
||||
requests = "^2.31.0"
|
||||
vdf = "^3.4"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
coverage = "^7.3.2"
|
||||
hypothesis = {extras = ["cli"], version = "^6.88.4"}
|
||||
icecream = "^2.1.3"
|
||||
ipython = "^8.17.2"
|
||||
poetry-plugin-export = "^1.6.0"
|
||||
pytest = "^8.0.2"
|
||||
pytest-asyncio = "^0.24.0"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-datafiles = "^3.0.0"
|
||||
pytest-xdist = "^3.4.0"
|
||||
rich = "^13.6.0"
|
||||
ruff = "^0.6.1"
|
||||
|
||||
[tool.ruff]
|
||||
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
|
||||
select = ["E", "F"]
|
||||
ignore = []
|
||||
# Skip unused variable rules
|
||||
extend-ignore = ["D203", "E203", "E251", "E266", "E401", "E402", "E501", "F401", "F403"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
|
||||
unfixable = []
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
"dist",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
"__pycache__",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
dependencies = [
|
||||
"python-decouple~=3.8",
|
||||
"python-steamgriddb>=1.0.5,<2",
|
||||
"requests>=2.31.0,<3",
|
||||
"vdf~=3.4",
|
||||
]
|
||||
|
||||
# Black (default: 88)
|
||||
line-length = 130
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# Assume Python 3.11.
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
# Unlike Flake8, default to a complexity level of 10.
|
||||
max-complexity = 10
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"coverage>=7.3.2,<8",
|
||||
"hypothesis[cli]>=6.88.4,<7",
|
||||
"icecream>=2.1.3,<3",
|
||||
"ipython>=9.0.2,<10",
|
||||
"pytest>=8.0.2,<10",
|
||||
"pytest-asyncio>=1.0.0,<2",
|
||||
"pytest-cov>=6.0.0,<8",
|
||||
"pytest-datafiles>=3.0.0,<4",
|
||||
"pytest-xdist>=3.4.0,<4",
|
||||
"rich>=14.0.0,<16",
|
||||
"ruff>=0.12.1,<0.16",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
+8
-8
@@ -1,8 +1,8 @@
|
||||
certifi==2024.2.2 ; python_version >= "3.11" and python_version < "3.13"
|
||||
charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "3.13"
|
||||
idna==3.6 ; python_version >= "3.11" and python_version < "3.13"
|
||||
python-decouple==3.8 ; python_version >= "3.11" and python_version < "3.13"
|
||||
python-steamgriddb==1.0.5 ; python_version >= "3.11" and python_version < "3.13"
|
||||
requests==2.31.0 ; python_version >= "3.11" and python_version < "3.13"
|
||||
urllib3==2.2.1 ; python_version >= "3.11" and python_version < "3.13"
|
||||
vdf==3.4 ; python_version >= "3.11" and python_version < "3.13"
|
||||
certifi==2026.5.20
|
||||
charset-normalizer==3.4.7
|
||||
idna==3.18
|
||||
python-decouple==3.8
|
||||
python-steamgriddb==1.0.5
|
||||
requests==2.34.2
|
||||
urllib3==2.7.0
|
||||
vdf==3.4
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Fix without reporting on leftover violations
|
||||
fix-only = true
|
||||
|
||||
# Enumerate all fixed violations
|
||||
show-fixes = true
|
||||
|
||||
# Indent width (default: 4)
|
||||
indent-width = 4
|
||||
|
||||
# Black (default: 88)
|
||||
line-length = 130
|
||||
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
"dist",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
"__pycache__",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Assume Python 3.12
|
||||
target-version = "py312"
|
||||
|
||||
[format]
|
||||
# Use spaces instead of tabs
|
||||
indent-style = "space"
|
||||
|
||||
# Use `\n` line endings for all files
|
||||
line-ending = "lf"
|
||||
|
||||
# Set quote style for strings
|
||||
quote-style = "preserve"
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# flake8-simplify
|
||||
"SIM",
|
||||
# isort
|
||||
"I",
|
||||
]
|
||||
ignore = ["D203", "E203", "E251", "E266", "E401", "E402", "E501", "F401", "F403", "F841"]
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TID", "TRY", "UP", "YTT"]
|
||||
|
||||
[lint.isort]
|
||||
combine-as-imports = true
|
||||
from-first = false
|
||||
no-sections = true
|
||||
order-by-type = true
|
||||
|
||||
[lint.flake8-quotes]
|
||||
docstring-quotes = "double"
|
||||
|
||||
[lint.mccabe]
|
||||
# Unlike Flake8, default to a complexity level of 10.
|
||||
max-complexity = 10
|
||||
Reference in New Issue
Block a user