mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-29 04:31:55 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3338260f5d | ||
|
|
49b0df42b4 | ||
|
|
756f097626 | ||
|
|
9e0158de54 | ||
|
|
d035c9b7c7 | ||
|
|
00a6784029 | ||
|
|
56493a8a78 | ||
|
|
a7119ef1e3 | ||
|
|
bf3f6e2bfb | ||
|
|
221cd7f8bc | ||
|
|
328e93b0bc | ||
|
|
653efed311 | ||
|
|
133e5c442c | ||
|
|
28d991f765 | ||
|
|
fa6baebc66 | ||
|
|
c0e47314c2 | ||
|
|
e628a8d04a | ||
|
|
3641b44796 | ||
|
|
c56552c5e9 | ||
|
|
6cca56ec71 | ||
|
|
93ff75f51c | ||
|
|
0cce956fac | ||
|
|
9252370503 | ||
|
|
d972d9a48c | ||
|
|
7d543dc644 | ||
|
|
4948d7a63c | ||
|
|
1eab8cafda | ||
|
|
54033f9ea6 | ||
|
|
bafd1bf14c | ||
|
|
309708225c | ||
|
|
ba70aec490 | ||
|
|
fb018c81ca | ||
|
|
a6c279211d | ||
|
|
f4e1f3c95e | ||
|
|
21d10a7f3e | ||
|
|
c7d3e8dc0e | ||
|
|
0c143b6121 | ||
|
|
b806153ab6 | ||
|
|
247bddcc9f |
@@ -19,7 +19,7 @@ jobs:
|
||||
cache: 'pipenv'
|
||||
- name: "Install Dependencies"
|
||||
run: |
|
||||
python -m pip install pipenv==2026.8.0
|
||||
python -m pip install pipenv==2026.10.3
|
||||
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||
- name: "Build Documentation"
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run make -C docs html SPHINXOPTS="-W -n"
|
||||
|
||||
@@ -22,9 +22,10 @@ jobs:
|
||||
cache: 'pipenv'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install pipenv==2026.8.0
|
||||
python -m pip install pipenv==2026.10.3
|
||||
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||
- name: PyLint
|
||||
if: matrix.python-version != '3.10'
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
|
||||
- name: MyPy
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader
|
||||
|
||||
@@ -93,10 +93,6 @@ py-version=3.8
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
Generated
+178
-178
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775",
|
||||
"sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"
|
||||
"sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd",
|
||||
"sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2026.7.22"
|
||||
"version": "==2026.10.23"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -216,6 +216,14 @@
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.20"
|
||||
},
|
||||
"jeepney": {
|
||||
"hashes": [
|
||||
"sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683",
|
||||
"sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"lz4": {
|
||||
"hashes": [
|
||||
"sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc",
|
||||
@@ -281,52 +289,52 @@
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:0428f19f13452c6b89bbaf2c530f84f369873811dfa77f0cee0da4f40fb0474f",
|
||||
"sha256:0afd3f4cb2bd690cbf2b24e5d330e883cd5d8dcf3370aafc2aed16f676fff91e",
|
||||
"sha256:3b6276fe007464502df796710fb57dcfeb813b6ccac0eabaf85100be1ad358f2",
|
||||
"sha256:408fb76e0af11937381532c992d9f7c0dcf987878cbe61d772b0e788343e81c5",
|
||||
"sha256:42c8221e3a4ca04b070cb388fca004815f6544951bfb2cc6c7958e293b7caf64",
|
||||
"sha256:556e063b52f338bd924fe5c40692cc1f50cb0453bd1d6580b7ffff2eade1a894",
|
||||
"sha256:55f22d72250d7923aac3c2442ec6a1dd8774d2efa3bdcb0f68d5e137d372268e",
|
||||
"sha256:582efdf767d72c12538949b876a64b9a7af30f2cff063c06dd63ece32c4e5c1d",
|
||||
"sha256:5be79564e05ed620a63f353ea50e0d474eb598f70cda1b28c2ca8e9e2f02c139",
|
||||
"sha256:5fa46af1281be707e0ec5878aea0f77d58d8d4b465f63f7385a71ae0080bfff9",
|
||||
"sha256:6a62122fb4c85befcc01ec3a1ae5b2ba81689481aba1d383b4e21e34f18900dd",
|
||||
"sha256:72742921e388856044163e711643244b6d5c7062846dc0676b1f9f0743846ab3",
|
||||
"sha256:770db9a5b9d30d8a48486bc163870763f3fe0f983c780403cd15c332618fa51c",
|
||||
"sha256:78bb214b8016aade97a357071d753a5c11687bce2ba693da48218c3472ece287",
|
||||
"sha256:793b9f5ba7e9147a6a6d8e284854e3711e12f43d4ef57a26b3415b2455a9801a",
|
||||
"sha256:7ffcedf4210d8c206fe9852ccde9f78ec9ce35e8039f0a989900ffe710e7a5db",
|
||||
"sha256:8293a92e6f71d6de57364432ff0ba48244959a81c632574eb5586e7e67f5de59",
|
||||
"sha256:88d37b28b450c06a03bd5570467c601fe8c7dbe493ca39771716f56fdbe35d6e",
|
||||
"sha256:8a2a634c86c0a59b28b0d51bbf7e5d1feed00dc7ebdc789fe968f771f1430b47",
|
||||
"sha256:8d6c5e01e4fb1ddd20cc24d4d8706643dd78b5279453dabcf10ba4e0683bf9b2",
|
||||
"sha256:908f92fbc307b642b65580d0295698f423b057018d87ae43d441f1d1a00674a2",
|
||||
"sha256:96b60c0f36ab7c11a826c116633b08afeac58b9b9c066a5ad7cd373ed996363c",
|
||||
"sha256:99c19dcd9846d6bf9886eb758c6d8e06c5bfd8a0c83802551f1aa56e80fe13dc",
|
||||
"sha256:abf768c285d289c4a0e0678f055663162c9d6257c79f5904fbd2692ade94da1c",
|
||||
"sha256:b4f7f9c737ca1af9a79962d3289eaa6521869915ee9fbb9168e578277126691b",
|
||||
"sha256:b5d0afc29fc461d599379f814f5ba378e58c69418588034619d76747bc2b5d5d",
|
||||
"sha256:b6690bfae043c81108e28e31dc7e3afa8cb1b1eb0ba590fdbff2b66e4c6913e7",
|
||||
"sha256:bb393480b5845ef4acbc9f713e4c473ad08be81bc3cdb5ef017cd02bdafd36e1",
|
||||
"sha256:c2f42f24d7112ebb93d3aed581d011c82c20101da7d7a6248145c530e99bd2eb",
|
||||
"sha256:c55e6c42f9eb43d90c15792bf69bc09cb3e13a5e55a30eecb555695a02b0d4c0",
|
||||
"sha256:c7055400f773856cd219680d15525c6fb147237534cbe5394a5f92f57f7efaed",
|
||||
"sha256:c7b8ad51baf3505dd3d94ec974855359d15769e45c58c2b89c6d4612b1f7ed94",
|
||||
"sha256:da9996407d03684221a4e1603b70fad97d8772185e5f75addcd97fcbc4c3281f",
|
||||
"sha256:dc685ab0b420597fe9a3187555c14d5b4d87c1518a99646397018457dc2a70c8",
|
||||
"sha256:e42a2e9531c40315290278d9eee93ea5031eda3c83969f5709ada51a1c7491b4",
|
||||
"sha256:e635c7ac056f2a47d79e2bf7042fb3fc6d175f47796a954388a3cec1f2e082a4",
|
||||
"sha256:e8a7463d346015b42a0fb9c9179ebb7cccfd86bf40199c6796b546ac7be0b85c",
|
||||
"sha256:eb14a078429434b017f1fb26a75a8fcd62c12befb6b3d37ca39353dc2b5843fd",
|
||||
"sha256:ef2bd306328a8447d4d2af6037efddd67ef16630876ed3ec84babd16ca558941",
|
||||
"sha256:f6af77f15a91bb6c16607067cfd201330cf5dfefe1c715e955be1fb06d368613",
|
||||
"sha256:f96e10f0edff9d4b6b253e0a78f7c819749521d3ffe6af0139e08fcd35384405",
|
||||
"sha256:fab40542a863cc142a763aa92630d613925fe3e937bea3ba5fb735e0c42d4f96",
|
||||
"sha256:ff8c1a125b2a7a2933219e9e523aabca31c920839c3c10968b56ec9a76c81d1e"
|
||||
"sha256:03e2027f81fe6b700e7ff614d79111559bfb05ed8c4a9de9d2152d1a0a0768de",
|
||||
"sha256:09081666ffc599976c0b8b29caf2cf82212c0f05bed233c8f8ed53e4f6e1d356",
|
||||
"sha256:147c742f73bbe8791d8c454b9972330d7ea501de94e8172957af375f9bdb4a7f",
|
||||
"sha256:347c37708688414d9b3ba69e3a60df4c82f3d9f9d0fcc1d645f85252d997207b",
|
||||
"sha256:3bbcc1807502da4b5d66c94357a99589c8547aa9ad2f9ecfa537b2e9a347538b",
|
||||
"sha256:455596da1c6a1d534051c1883761f9c0122a8126375827dd7cb7f0bf83c63a3a",
|
||||
"sha256:48d9058dd4c8f3af83e048b6ff83d9c6300841c42579f77fdfab19e49156a512",
|
||||
"sha256:514d685de4b0227d35d7114047fbd575bf162bff373e07af665aa41eee089dd0",
|
||||
"sha256:569fdbeff936cbd5beb852b3969dc2f58fb255bd221b3f7a8999c035c30ec958",
|
||||
"sha256:5966f829f64c833cc72a8694cfee05ad50446a44167842c92dec4ebf6b84d72c",
|
||||
"sha256:5b37b86a3771d6aa21cccf9f8448460e4be11e68bb8e699133ce632cb986f521",
|
||||
"sha256:6159dc74824c591b4c294f8f96b74a71c3b5e9f637dab2f2da14cfa32dc24d47",
|
||||
"sha256:637b4bbc8165921b13d2b9d706e3697a9c6077d7564bfb61a3b7005182b2a7ac",
|
||||
"sha256:6773f8c65b6e3d7524f4e18542ac2aa8856979785792ca0359d69bca27a269c6",
|
||||
"sha256:77a48a855776101a61b932ddedbf522e1ead9e20b321bec0f64dd7434ac4fcc3",
|
||||
"sha256:77ef934c2b2eab3c57a431daf858fcbf272134691bcc8d729787d17ad4cbf758",
|
||||
"sha256:794f32227a480ab3b39971ac4a53dfa8ed444e28c0ddecbdc35f26d038bb46da",
|
||||
"sha256:7a81e9be7084af3591912475b22c5d0646a9160f3974b6b7300138781fffdc0d",
|
||||
"sha256:7f30261641dac5ae60e0af2fb11ad7c87200575b0ce403f7531576b0f38a52be",
|
||||
"sha256:82eb0dd8a95be97f03527b108ee49f9b86a7c534fa47fa7a510be3eab8ea9acd",
|
||||
"sha256:8321c315c4418dfe45a811b792380438ec81185deaaf202dbcd77711b4f51796",
|
||||
"sha256:8f27725ec41d6722bd4f729a403a38238d844127f39cf462991e693b9c9c2608",
|
||||
"sha256:9a732b9f5603b153dcdc071d6b342f192981907519827f2c1e4aeac41e2b34c2",
|
||||
"sha256:9b94a8c647c1f80b4c82c7ac9642dda94a71abbb4efa29ad652c410ad7a39879",
|
||||
"sha256:9bb353c764c144a9fc03302ef3763ad0c3e75bc7ca41f445cc98455f36da6c59",
|
||||
"sha256:9dd5104a1d8ccf725a3c1b40b21d5ed47530b6499abba30955c174d895c1be2f",
|
||||
"sha256:9fd3b792942e3b0937f9e39a1c56030cc41bbea058fb5a8af652ff6fd02a553e",
|
||||
"sha256:a692d2484ca8fa2c69f45c63f2b30cd00b8512834e3a522fc297d981dde5b34c",
|
||||
"sha256:b1c7a61b0b644e9780aa9f6eee301fb77c813b1e41262f6f4f387de0558e8926",
|
||||
"sha256:b299a1c10e45e1b71b34b048d555d3a8f366240942af89be5db379f88edb9542",
|
||||
"sha256:b3eda6b9416ef35b232403caa2bc6e25cc87530448d505a45d859210eab74051",
|
||||
"sha256:bd06ab1d9cf90e8b198daae3b364e9ac23a640d210bb8faddad6627cf2e33be7",
|
||||
"sha256:bf9c87112ee78e21be984c5873c17615d7b3f7305ba01f472931dc77a70ffb26",
|
||||
"sha256:c20fb5e8cf874dc182091d6df122b8b588501d23e7b500acf662c49899afdd0f",
|
||||
"sha256:cc64f4e1fc07a155ef31eb9815183a6a8eb13dfd6876ab049c01125223aa0c40",
|
||||
"sha256:cdec09935db4cbd74da5b577ea7d9959c73374060b22abf7d505429c0da7ed63",
|
||||
"sha256:e87928d8f37952ba53d215836cb3d89c63c1367f037d055c3a5b3f4403f4c6b6",
|
||||
"sha256:edc1deb28fceab6b78e12bae506eaef62ee2fc1b3cabc96f7932f0d51e368acd",
|
||||
"sha256:eeac2c9acbd2d9f0ca493fdf692ce8245ca37cbebee08c496025868d4b8ef70f",
|
||||
"sha256:f3ed97cdde1d96894057778095238bad3dd94d3e46dc07d6d618e6d4b7700cce",
|
||||
"sha256:f87d74a804ca949f46a6babe70247a789f6401f229566e90174c4b7033b87cc5",
|
||||
"sha256:fd487cc20730dc9d294e000126be86a02079485fa6cb723d8f01eddf86e96f5a",
|
||||
"sha256:fe19e03b81aefdeaa579afc6262fc03504fce93455129d6ef0da79ddb48ca47d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
|
||||
"version": "==3.24.0"
|
||||
"markers": "python_version >= '2.7' and python_version != '3.0' and python_version != '3.1' and python_version != '3.2' and python_version != '3.3' and python_version != '3.4' and python_version != '3.5' and python_version != '3.6'",
|
||||
"version": "==3.24.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
@@ -364,66 +372,66 @@
|
||||
},
|
||||
"ast-serialize": {
|
||||
"hashes": [
|
||||
"sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba",
|
||||
"sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980",
|
||||
"sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b",
|
||||
"sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb",
|
||||
"sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790",
|
||||
"sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6",
|
||||
"sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb",
|
||||
"sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72",
|
||||
"sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4",
|
||||
"sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854",
|
||||
"sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348",
|
||||
"sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76",
|
||||
"sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9",
|
||||
"sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca",
|
||||
"sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f",
|
||||
"sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976",
|
||||
"sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672",
|
||||
"sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317",
|
||||
"sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345",
|
||||
"sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90",
|
||||
"sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3",
|
||||
"sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b",
|
||||
"sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca",
|
||||
"sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3",
|
||||
"sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a",
|
||||
"sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31",
|
||||
"sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c",
|
||||
"sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a",
|
||||
"sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700",
|
||||
"sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace",
|
||||
"sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493",
|
||||
"sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515",
|
||||
"sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7",
|
||||
"sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01",
|
||||
"sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5",
|
||||
"sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303",
|
||||
"sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba",
|
||||
"sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460",
|
||||
"sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c",
|
||||
"sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f",
|
||||
"sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82",
|
||||
"sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db",
|
||||
"sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6",
|
||||
"sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14",
|
||||
"sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25",
|
||||
"sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093",
|
||||
"sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89",
|
||||
"sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085",
|
||||
"sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52",
|
||||
"sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37",
|
||||
"sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f",
|
||||
"sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e",
|
||||
"sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7",
|
||||
"sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7",
|
||||
"sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198",
|
||||
"sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d",
|
||||
"sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96"
|
||||
"sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5",
|
||||
"sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8",
|
||||
"sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd",
|
||||
"sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605",
|
||||
"sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a",
|
||||
"sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6",
|
||||
"sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5",
|
||||
"sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568",
|
||||
"sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6",
|
||||
"sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4",
|
||||
"sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47",
|
||||
"sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c",
|
||||
"sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456",
|
||||
"sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025",
|
||||
"sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769",
|
||||
"sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43",
|
||||
"sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489",
|
||||
"sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede",
|
||||
"sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a",
|
||||
"sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4",
|
||||
"sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593",
|
||||
"sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd",
|
||||
"sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad",
|
||||
"sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8",
|
||||
"sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011",
|
||||
"sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d",
|
||||
"sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a",
|
||||
"sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82",
|
||||
"sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8",
|
||||
"sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96",
|
||||
"sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117",
|
||||
"sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd",
|
||||
"sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611",
|
||||
"sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98",
|
||||
"sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b",
|
||||
"sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51",
|
||||
"sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef",
|
||||
"sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331",
|
||||
"sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8",
|
||||
"sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3",
|
||||
"sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226",
|
||||
"sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e",
|
||||
"sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6",
|
||||
"sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49",
|
||||
"sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7",
|
||||
"sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c",
|
||||
"sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe",
|
||||
"sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5",
|
||||
"sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a",
|
||||
"sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6",
|
||||
"sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9",
|
||||
"sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8",
|
||||
"sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7",
|
||||
"sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372",
|
||||
"sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037",
|
||||
"sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa",
|
||||
"sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.12.1"
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
@@ -443,11 +451,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775",
|
||||
"sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"
|
||||
"sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd",
|
||||
"sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2026.7.22"
|
||||
"version": "==2026.10.23"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -638,11 +646,11 @@
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968",
|
||||
"sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"
|
||||
"sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea",
|
||||
"sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.22.4"
|
||||
"version": "==0.23"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@@ -854,14 +862,6 @@
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"macholib": {
|
||||
"hashes": [
|
||||
"sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea",
|
||||
"sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"
|
||||
],
|
||||
"markers": "sys_platform == 'darwin'",
|
||||
"version": "==1.16.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98",
|
||||
@@ -1130,11 +1130,11 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250",
|
||||
"sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"
|
||||
"sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0",
|
||||
"sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==4.12.4"
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==4.13.5"
|
||||
},
|
||||
"prettytable": {
|
||||
"hashes": [
|
||||
@@ -1196,25 +1196,25 @@
|
||||
"sha256:f82d0fb89f21c3ed6f482ee16f341bc59e8a31dbabb9bae9ef04e53ade4dc0d9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.16' and python_version >= '3.8'",
|
||||
"markers": "python_version >= '3.8' and python_version < '3.16'",
|
||||
"version": "==6.22.3"
|
||||
},
|
||||
"pyinstaller-hooks-contrib": {
|
||||
"hashes": [
|
||||
"sha256:4d825786ad7a9b7dbcc52d612748a34562fe273461bcfd88f4d549c594bbf8f9",
|
||||
"sha256:6d21d65323ea4467753afc21f52d7d0e69416ae8101dd88269bf273ad48db084"
|
||||
"sha256:2748767901acb5cecd1304652b6c7add5bffcff1d71d01207fe24a5d42942a56",
|
||||
"sha256:fb3e67dd64122561724c48ad0366ac9e8c16e638b63bf7d85de03bf3fdbf06c3"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2026.8"
|
||||
"version": "==2026.9"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:235f13dc418c0041c649b42a5c35c99f2ffc6ca8b6a7574958eac5335906a68a",
|
||||
"sha256:e34c8636edc7d60a2eaadca4ad6c18a49e202361987a26e81b2a17fa92be28e1"
|
||||
"sha256:9928603068edfa0d1a3c167f174b099d4b97c3db75d32d0fcdd029770b4713a9",
|
||||
"sha256:a85357cae24f33ad8d86c8f3daaa92c600ae4012b54a57299cee76000e9364cf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_full_version >= '3.10.0'",
|
||||
"version": "==4.1.2"
|
||||
"version": "==4.1.3"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
@@ -1244,11 +1244,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670",
|
||||
"sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73"
|
||||
"sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc",
|
||||
"sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==84.0.0"
|
||||
"version": "==84.2.1"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
@@ -1260,21 +1260,21 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb",
|
||||
"sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"
|
||||
"sha256:4c3ec13b612712c0d3ce4cbdd9e3eb91e68577e07a8e31f32f21cf178453578c",
|
||||
"sha256:90470459f3cab15d9dfed146c71cf77694b0445df17b2e0e69059cb9f7fc9ee4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.12'",
|
||||
"version": "==9.1.0"
|
||||
"markers": "python_version >= '3.13'",
|
||||
"version": "==9.2.0"
|
||||
},
|
||||
"sphinx-autodoc-typehints": {
|
||||
"hashes": [
|
||||
"sha256:f5bac0996e76a2007fa26bc5393bb065ceb6cb4d062283fb5c69830c791c9716",
|
||||
"sha256:f5db988060f68a231dbc230df97d81421e54a0860778a63216b9f71958a9afdd"
|
||||
"sha256:b0ad1f96503d69b63c2a1c043f12e2a727c9549e6503d4adbf2125dbf5ff50ed",
|
||||
"sha256:de41ed4001f4818bb6d37c22fd10ee44a93e6c8e945c6c7e3984e28a9df87d1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.12'",
|
||||
"version": "==3.13.9"
|
||||
"version": "==3.17.2"
|
||||
},
|
||||
"sphinxcontrib-applehelp": {
|
||||
"hashes": [
|
||||
@@ -1311,11 +1311,11 @@
|
||||
},
|
||||
"sphinxcontrib-jsmath": {
|
||||
"hashes": [
|
||||
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
||||
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
||||
"sha256:15ca92fe0eec82f0baf91a7efcc51ffacdb605924be6327683a52ebdacb6744c",
|
||||
"sha256:551f780cf9687f9c3c74f0cf9f18aca06e0d6958853453f3803fe4d357c5b924"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.1"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-qthelp": {
|
||||
"hashes": [
|
||||
@@ -1407,11 +1407,11 @@
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304",
|
||||
"sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"
|
||||
"sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437",
|
||||
"sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.15.1"
|
||||
"version": "==0.15.2"
|
||||
},
|
||||
"types-requests": {
|
||||
"hashes": [
|
||||
@@ -1440,38 +1440,38 @@
|
||||
},
|
||||
"wcwidth": {
|
||||
"hashes": [
|
||||
"sha256:0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2",
|
||||
"sha256:0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b",
|
||||
"sha256:138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2",
|
||||
"sha256:196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270",
|
||||
"sha256:1bf361c8705576760623b4724ae564666d73b016f9a778bcfd1c7345378ef4ec",
|
||||
"sha256:2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec",
|
||||
"sha256:33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9",
|
||||
"sha256:42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724",
|
||||
"sha256:48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8",
|
||||
"sha256:5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c",
|
||||
"sha256:59dab4049cbd982b478bca098528df2c79a9160636a3a163ffebffcbd7d1b892",
|
||||
"sha256:674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724",
|
||||
"sha256:67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04",
|
||||
"sha256:734aa9405b321d1042301aa19c943c4731ee9e3460e4f8feea3299c064c97a14",
|
||||
"sha256:751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389",
|
||||
"sha256:7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07",
|
||||
"sha256:89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e",
|
||||
"sha256:8b4e381590b9b7390e07e22b2c0c1bb96ce50e1d2243c866d9387600362d51ed",
|
||||
"sha256:97b878d1e158da5ed9ac5aac53fa3a55e282103af6a09ec353865613d1a31a76",
|
||||
"sha256:9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79",
|
||||
"sha256:ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17",
|
||||
"sha256:ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b",
|
||||
"sha256:b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa",
|
||||
"sha256:bb08ceb501d6aaf94066c3ee122dd825b152df40ff0bd0df4dc27126233b948e",
|
||||
"sha256:c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7",
|
||||
"sha256:e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d",
|
||||
"sha256:eda88ffdc97c0fbf193d407114f2c7a54b379f67f6e52a7531ee3b9fe749eca7",
|
||||
"sha256:ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4",
|
||||
"sha256:f2f7b3bba5a5d5f31fc350fd36ce5b84b693c83b7eb95ee630b720da5a5ce06f"
|
||||
"sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef",
|
||||
"sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6",
|
||||
"sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3",
|
||||
"sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f",
|
||||
"sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d",
|
||||
"sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49",
|
||||
"sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b",
|
||||
"sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62",
|
||||
"sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95",
|
||||
"sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f",
|
||||
"sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5",
|
||||
"sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d",
|
||||
"sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633",
|
||||
"sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27",
|
||||
"sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09",
|
||||
"sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664",
|
||||
"sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7",
|
||||
"sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2",
|
||||
"sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763",
|
||||
"sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d",
|
||||
"sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967",
|
||||
"sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab",
|
||||
"sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279",
|
||||
"sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed",
|
||||
"sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17",
|
||||
"sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d",
|
||||
"sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4",
|
||||
"sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c",
|
||||
"sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.9.2"
|
||||
"version": "==0.9.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ bundled_packages = ' '.join(list(locked_packages) + ['psutil', 'pyinstaller'])
|
||||
os.makedirs('dist/licenses', exist_ok=True)
|
||||
|
||||
# install dependencies, invoke PyInstaller and collect licenses
|
||||
commands = ["pip install pipenv==2026.8.0",
|
||||
commands = ["pip install pipenv==2026.10.3",
|
||||
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec",
|
||||
f"pipenv run pip-licenses --packages {bundled_packages} --with-license-file "
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.15.5'
|
||||
__version__ = '4.16'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -16,7 +16,7 @@ from .instaloader import (get_default_session_filename, get_default_stamps_filen
|
||||
from .instaloadercontext import default_user_agent
|
||||
from .lateststamps import LatestStamps
|
||||
try:
|
||||
import browser_cookie3
|
||||
import browser_cookie3 # type: ignore[import-untyped]
|
||||
bc3_library = True
|
||||
except ImportError:
|
||||
bc3_library = False
|
||||
|
||||
+40
-23
@@ -85,31 +85,36 @@ def _requires_login(func: Callable) -> Callable:
|
||||
def _retry_on_connection_error(func: Callable) -> Callable:
|
||||
"""Decorator to retry the function max_connection_attempts number of times.
|
||||
|
||||
Herewith-decorated functions need an ``_attempt`` keyword argument.
|
||||
|
||||
This is to decorate functions that do network requests that may fail. Note that
|
||||
:meth:`.get_json`, :meth:`.get_iphone_json`, :meth:`.graphql_query` and :meth:`.graphql_node_list` already have
|
||||
their own logic for retrying, hence functions that only use these for network access must not be decorated with this
|
||||
decorator."""
|
||||
decorator.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Decorated functions no longer need to accept an ``_attempt`` keyword argument.
|
||||
"""
|
||||
@wraps(func)
|
||||
def call(instaloader, *args, **kwargs):
|
||||
try:
|
||||
return func(instaloader, *args, **kwargs)
|
||||
except (urllib3.exceptions.HTTPError, requests.exceptions.RequestException, ConnectionException) as err:
|
||||
error_string = "{}({}): {}".format(func.__name__, ', '.join([repr(arg) for arg in args]), err)
|
||||
if (kwargs.get('_attempt') or 1) == instaloader.context.max_connection_attempts:
|
||||
raise ConnectionException(error_string) from None
|
||||
instaloader.context.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||
def call(instaloader, *args, **kwargs) -> Any:
|
||||
# The failure we are currently retrying, if any. It tells us whether a ^C means "skip this
|
||||
# download" rather than "abort everything", and whether to sleep before the next attempt.
|
||||
pending_error_string: Optional[str] = None
|
||||
for attempt in range(1, instaloader.context.max_connection_attempts + 1):
|
||||
try:
|
||||
if kwargs.get('_attempt'):
|
||||
kwargs['_attempt'] += 1
|
||||
else:
|
||||
kwargs['_attempt'] = 2
|
||||
instaloader.context.do_sleep()
|
||||
return call(instaloader, *args, **kwargs)
|
||||
if pending_error_string is not None:
|
||||
instaloader.context.do_sleep()
|
||||
return func(instaloader, *args, **kwargs)
|
||||
except (urllib3.exceptions.HTTPError, requests.exceptions.RequestException, ConnectionException) as err:
|
||||
error_string = "{}({}): {}".format(func.__name__, ', '.join([repr(arg) for arg in args]), err)
|
||||
if attempt == instaloader.context.max_connection_attempts:
|
||||
raise ConnectionException(error_string) from None
|
||||
instaloader.context.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||
pending_error_string = error_string
|
||||
except KeyboardInterrupt:
|
||||
if pending_error_string is None:
|
||||
raise
|
||||
instaloader.context.error("[skipped by user]", repeat_at_end=False)
|
||||
raise ConnectionException(error_string) from None
|
||||
raise ConnectionException(pending_error_string) from None
|
||||
raise AssertionError("_retry_on_connection_error() retry loop exited without result")
|
||||
return call
|
||||
|
||||
|
||||
@@ -335,7 +340,7 @@ class Instaloader:
|
||||
|
||||
@_retry_on_connection_error
|
||||
def download_pic(self, filename: str, url: str, mtime: datetime,
|
||||
filename_suffix: Optional[str] = None, _attempt: int = 1) -> bool:
|
||||
filename_suffix: Optional[str] = None) -> bool:
|
||||
"""Downloads and saves picture with given url under given directory with given timestamp.
|
||||
Returns true, if file was actually downloaded, i.e. updated."""
|
||||
if filename_suffix is not None:
|
||||
@@ -519,8 +524,9 @@ class Instaloader:
|
||||
'{0}_{1}_{2}.{3}'.format(target, identifier, name_suffix, extension))
|
||||
|
||||
@_retry_on_connection_error
|
||||
def download_title_pic(self, url: str, target: Union[str, Path], name_suffix: str, owner_profile: Optional[Profile],
|
||||
_attempt: int = 1) -> None:
|
||||
def download_title_pic(
|
||||
self, url: str, target: Union[str, Path], name_suffix: str, owner_profile: Optional[Profile]
|
||||
) -> None:
|
||||
"""Downloads and saves a picture that does not have an association with a Post or StoryItem, such as a
|
||||
Profile picture or a Highlight cover picture. Modification time is taken from the HTTP response headers.
|
||||
|
||||
@@ -963,6 +969,11 @@ class Instaloader:
|
||||
.. versionchanged:: 4.3
|
||||
Also downloads and saves the Highlight's cover pictures.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Add progress output for highlight retrieval, displaying a counter of the current
|
||||
highlight number and the total number of highlights being processed. This provides
|
||||
feedback similar to the per-item download counter already present in this function.
|
||||
|
||||
:param user: ID or Profile of the user whose highlights should get downloaded.
|
||||
:param fast_update: If true, abort when first already-downloaded picture is encountered
|
||||
:param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
|
||||
@@ -970,14 +981,20 @@ class Instaloader:
|
||||
:param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded
|
||||
:raises LoginRequiredException: If called without being logged in.
|
||||
"""
|
||||
for user_highlight in self.get_highlights(user):
|
||||
user_highlights = list(self.get_highlights(user))
|
||||
hl_size = len(user_highlights)
|
||||
for hl_number, user_highlight in enumerate(user_highlights, start=1):
|
||||
name = user_highlight.owner_username
|
||||
highlight_target: Union[str, Path] = (filename_target
|
||||
if filename_target
|
||||
else (Path(_PostPathFormatter.sanitize_path(name, self.sanitize_paths)) /
|
||||
_PostPathFormatter.sanitize_path(user_highlight.title,
|
||||
self.sanitize_paths)))
|
||||
self.context.log("Retrieving highlights \"{}\" from profile {}".format(user_highlight.title, name))
|
||||
self.context.log(
|
||||
"[{0:{w}d}/{1:{w}d}] Retrieving highlights \"{2}\" from profile {3}".format(
|
||||
hl_number, hl_size, user_highlight.title, name, w=len(str(hl_size))
|
||||
)
|
||||
)
|
||||
self.download_highlight_cover(user_highlight, highlight_target)
|
||||
totalcount = user_highlight.itemcount
|
||||
count = 1
|
||||
|
||||
+292
-173
@@ -9,6 +9,7 @@ import textwrap
|
||||
import time
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from base64 import b64encode
|
||||
from contextlib import contextmanager, suppress
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
@@ -101,8 +102,7 @@ class InstaloaderContext:
|
||||
self.user_agent = user_agent if user_agent is not None else default_user_agent()
|
||||
self.request_timeout = request_timeout
|
||||
self._session = self.get_anonymous_session()
|
||||
self.username = None
|
||||
self.user_id = None
|
||||
self.username: Optional[str] = None
|
||||
self.sleep = sleep
|
||||
self.quiet = quiet
|
||||
self.max_connection_attempts = max_connection_attempts
|
||||
@@ -125,15 +125,16 @@ class InstaloaderContext:
|
||||
# Cache profile from id (mapping from id to Profile)
|
||||
self.profile_id_cache: Dict[int, Any] = dict()
|
||||
|
||||
# Tokens of the logged-in web page for api/graphql queries, by sessionid cookie
|
||||
self._web_api_tokens_cache: Dict[str, Dict[str, str]] = dict()
|
||||
|
||||
@contextmanager
|
||||
def anonymous_copy(self):
|
||||
session = self._session
|
||||
username = self.username
|
||||
user_id = self.user_id
|
||||
iphone_headers = self.iphone_headers
|
||||
self._session = self.get_anonymous_session()
|
||||
self.username = None
|
||||
self.user_id = None
|
||||
self.iphone_headers = default_iphone_headers()
|
||||
try:
|
||||
yield self
|
||||
@@ -141,9 +142,22 @@ class InstaloaderContext:
|
||||
self._session.close()
|
||||
self.username = username
|
||||
self._session = session
|
||||
self.user_id = user_id
|
||||
self.iphone_headers = iphone_headers
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[str]:
|
||||
"""ID of the user this session is logged in as.
|
||||
|
||||
Derived from the ``ds_user_id`` cookie of the current session, so it is set for every
|
||||
logged-in session, and ``None`` if not logged in."""
|
||||
return self._session.cookies.get_dict().get('ds_user_id') or None
|
||||
|
||||
@user_id.setter
|
||||
def user_id(self, user_id: Optional[str]) -> None:
|
||||
# The setter is only provided for backwards-compatibility with pre-4.16.
|
||||
if user_id != self.user_id:
|
||||
raise AttributeError("user_id cannot be set to a different value.")
|
||||
|
||||
@property
|
||||
def is_logged_in(self) -> bool:
|
||||
"""True, if this Instaloader instance is logged in."""
|
||||
@@ -351,7 +365,6 @@ class InstaloaderContext:
|
||||
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
|
||||
self._session = session
|
||||
self.username = user
|
||||
self.user_id = resp_json['userId']
|
||||
|
||||
def two_factor_login(self, two_factor_code):
|
||||
"""Second step of login if 2FA is enabled.
|
||||
@@ -401,8 +414,102 @@ class InstaloaderContext:
|
||||
f" when accessing {resp.url}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _rate_limit_key(path: str, params: Dict[str, Any], host: str) -> str:
|
||||
"""Determine the :class:`RateController` bucket that a request is accounted to."""
|
||||
if 'graphql/query' in path or path == 'api/graphql':
|
||||
if 'query_hash' in params:
|
||||
return params['query_hash']
|
||||
if 'doc_id' in params:
|
||||
return params['doc_id']
|
||||
if host == 'i.instagram.com':
|
||||
return 'iphone'
|
||||
return 'other'
|
||||
|
||||
def _raise_if_fatal_status(self, resp: requests.Response, host: str, path: str) -> None:
|
||||
if resp.status_code not in self.fatal_status_codes:
|
||||
return
|
||||
redirect = " redirect to {}".format(resp.headers['location']) if 'location' in resp.headers else ""
|
||||
body = ""
|
||||
if resp.headers.get('Content-Type', '').startswith('application/json'):
|
||||
body = ': ' + resp.text[:500] + ('…' if len(resp.text) > 501 else '')
|
||||
raise AbortDownloadException("Query to https://{}/{} responded with \"{} {}\"{}{}".format(
|
||||
host, path, resp.status_code, resp.reason, redirect, body
|
||||
))
|
||||
|
||||
def _raise_if_login_redirect(self, resp: requests.Response) -> None:
|
||||
"""Turn a redirect to the login page into a meaningful exception.
|
||||
|
||||
Instagram answers requests it will not serve anonymously with a redirect to the login page
|
||||
rather than with an error status, so this needs to be checked wherever we do not follow
|
||||
redirects automatically.
|
||||
"""
|
||||
if not resp.is_redirect:
|
||||
return
|
||||
redirect_url = resp.headers.get('location', '')
|
||||
if not (redirect_url.startswith('https://www.instagram.com/accounts/login') or
|
||||
redirect_url.startswith('https://i.instagram.com/accounts/login')):
|
||||
return
|
||||
if not self.is_logged_in:
|
||||
raise LoginRequiredException("Redirected to login page. Use --login or --load-cookies.")
|
||||
raise AbortDownloadException("Redirected to login page. You've been logged out, please wait " +
|
||||
"some time, recreate the session and try again")
|
||||
|
||||
def _follow_redirects(self, sess: requests.Session, resp: requests.Response,
|
||||
host: str, path: str, params: Dict[str, Any], use_post: bool) -> requests.Response:
|
||||
"""Follow redirects that stay on *host*, and reject those to the login page."""
|
||||
# Redirects are followed with GET. For a POST request, `params` is the request body and must not be
|
||||
# turned into query arguments.
|
||||
redirect_params = None if use_post else params
|
||||
while resp.is_redirect:
|
||||
redirect_url = resp.headers['location']
|
||||
self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
|
||||
self._raise_if_login_redirect(resp)
|
||||
if not redirect_url.startswith('https://{}/'.format(host)):
|
||||
break
|
||||
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
|
||||
params=redirect_params, allow_redirects=False)
|
||||
return resp
|
||||
|
||||
def _raise_for_status(self, resp: requests.Response) -> None:
|
||||
"""Translate an unsuccessful HTTP status code into the corresponding exception."""
|
||||
if resp.status_code == 400:
|
||||
with suppress(json.decoder.JSONDecodeError):
|
||||
if resp.json().get("message") in [
|
||||
"feedback_required",
|
||||
"checkpoint_required",
|
||||
"challenge_required",
|
||||
]:
|
||||
# Raise AbortDownloadException in case of substantial Instagram
|
||||
# requirements to stop producing more requests
|
||||
raise AbortDownloadException(self._response_error(resp))
|
||||
raise QueryReturnedBadRequestException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
if resp.status_code == 429:
|
||||
raise TooManyRequestsException(self._response_error(resp))
|
||||
if resp.status_code != 200:
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
|
||||
def _raise_for_download_status(self, resp: requests.Response) -> None:
|
||||
"""Translate an unsuccessful status code of an anonymous download request into an exception.
|
||||
|
||||
This is deliberately a different policy from :meth:`_raise_for_status`, which applies to the
|
||||
JSON API: a download may be answered with 403 for an invalid URL signature, and is never
|
||||
answered with the 400 or 429 that the API uses.
|
||||
"""
|
||||
if resp.status_code == 200:
|
||||
return
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
|
||||
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
||||
session: Optional[requests.Session] = None, _attempt=1,
|
||||
session: Optional[requests.Session] = None,
|
||||
response_headers: Optional[Dict[str, Any]] = None,
|
||||
use_post: bool = False) -> Dict[str, Any]:
|
||||
"""JSON request to Instagram.
|
||||
@@ -422,96 +529,112 @@ class InstaloaderContext:
|
||||
|
||||
.. versionchanged:: 4.13
|
||||
Added `use_post` parameter.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Removed the internal `_attempt` parameter; retries are no longer implemented by recursion.
|
||||
"""
|
||||
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
|
||||
is_doc_id_query = 'doc_id' in params and 'graphql/query' in path
|
||||
is_iphone_query = host == 'i.instagram.com'
|
||||
is_other_query = not is_graphql_query and not is_doc_id_query and host == "www.instagram.com"
|
||||
sess = session if session else self._session
|
||||
try:
|
||||
self.do_sleep()
|
||||
if is_graphql_query:
|
||||
self._rate_controller.wait_before_query(params['query_hash'])
|
||||
if is_doc_id_query:
|
||||
self._rate_controller.wait_before_query(params['doc_id'])
|
||||
if is_iphone_query:
|
||||
self._rate_controller.wait_before_query('iphone')
|
||||
if is_other_query:
|
||||
self._rate_controller.wait_before_query('other')
|
||||
if use_post:
|
||||
resp = sess.post('https://{0}/{1}'.format(host, path), data=params, allow_redirects=False)
|
||||
else:
|
||||
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
||||
if resp.status_code in self.fatal_status_codes:
|
||||
redirect = " redirect to {}".format(resp.headers['location']) if 'location' in resp.headers else ""
|
||||
body = ""
|
||||
if resp.headers['Content-Type'].startswith('application/json'):
|
||||
body = ': ' + resp.text[:500] + ('…' if len(resp.text) > 501 else '')
|
||||
raise AbortDownloadException("Query to https://{}/{} responded with \"{} {}\"{}{}".format(
|
||||
host, path, resp.status_code, resp.reason, redirect, body
|
||||
))
|
||||
while resp.is_redirect:
|
||||
redirect_url = resp.headers['location']
|
||||
self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
|
||||
if (redirect_url.startswith('https://www.instagram.com/accounts/login') or
|
||||
redirect_url.startswith('https://i.instagram.com/accounts/login')):
|
||||
if not self.is_logged_in:
|
||||
raise LoginRequiredException("Redirected to login page. Use --login or --load-cookies.")
|
||||
raise AbortDownloadException("Redirected to login page. You've been logged out, please wait " +
|
||||
"some time, recreate the session and try again")
|
||||
if redirect_url.startswith('https://{}/'.format(host)):
|
||||
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
|
||||
params=params, allow_redirects=False)
|
||||
else:
|
||||
break
|
||||
if response_headers is not None:
|
||||
response_headers.clear()
|
||||
response_headers.update(resp.headers)
|
||||
if resp.status_code == 400:
|
||||
with suppress(json.decoder.JSONDecodeError):
|
||||
if resp.json().get("message") in [
|
||||
"feedback_required",
|
||||
"checkpoint_required",
|
||||
"challenge_required",
|
||||
]:
|
||||
# Raise AbortDownloadException in case of substantial Instagram
|
||||
# requirements to stop producing more requests
|
||||
raise AbortDownloadException(self._response_error(resp))
|
||||
raise QueryReturnedBadRequestException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
if resp.status_code == 429:
|
||||
raise TooManyRequestsException(self._response_error(resp))
|
||||
if resp.status_code != 200:
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
else:
|
||||
resp_json = resp.json()
|
||||
if 'status' in resp_json and resp_json['status'] != "ok":
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
return resp_json
|
||||
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
|
||||
error_string = "JSON Query to {}: {}".format(path, err)
|
||||
if _attempt == self.max_connection_attempts:
|
||||
if isinstance(err, QueryReturnedNotFoundException):
|
||||
raise QueryReturnedNotFoundException(error_string) from err
|
||||
else:
|
||||
raise ConnectionException(error_string) from err
|
||||
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||
sess = session or self._session
|
||||
rate_limit_key = self._rate_limit_key(path, params, host)
|
||||
url = 'https://{0}/{1}'.format(host, path)
|
||||
|
||||
# The failure we are currently retrying, if any. It drives the 429 backoff performed before the
|
||||
# next attempt, and tells us whether a ^C means "skip this query" rather than "abort everything".
|
||||
pending_error: Optional[BaseException] = None
|
||||
pending_error_string = ""
|
||||
|
||||
for attempt in range(1, self.max_connection_attempts + 1):
|
||||
try:
|
||||
if isinstance(err, TooManyRequestsException):
|
||||
if is_graphql_query:
|
||||
self._rate_controller.handle_429(params['query_hash'])
|
||||
if is_doc_id_query:
|
||||
self._rate_controller.handle_429(params['doc_id'])
|
||||
if is_iphone_query:
|
||||
self._rate_controller.handle_429('iphone')
|
||||
if is_other_query:
|
||||
self._rate_controller.handle_429('other')
|
||||
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1,
|
||||
response_headers=response_headers)
|
||||
if isinstance(pending_error, TooManyRequestsException):
|
||||
self._rate_controller.handle_429(rate_limit_key)
|
||||
self.do_sleep()
|
||||
self._rate_controller.wait_before_query(rate_limit_key)
|
||||
|
||||
if use_post:
|
||||
resp = sess.post(url, data=params, allow_redirects=False)
|
||||
else:
|
||||
resp = sess.get(url, params=params, allow_redirects=False)
|
||||
|
||||
self._raise_if_fatal_status(resp, host, path)
|
||||
resp = self._follow_redirects(sess, resp, host, path, params, use_post)
|
||||
|
||||
if response_headers is not None:
|
||||
response_headers.clear()
|
||||
response_headers.update(resp.headers)
|
||||
|
||||
self._raise_for_status(resp)
|
||||
resp_json = resp.json()
|
||||
if 'status' in resp_json and resp_json['status'] != "ok":
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
return resp_json
|
||||
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
|
||||
error_string = "JSON Query to {}: {}".format(path, err)
|
||||
if attempt == self.max_connection_attempts:
|
||||
if isinstance(err, QueryReturnedNotFoundException):
|
||||
raise QueryReturnedNotFoundException(error_string) from err
|
||||
raise ConnectionException(error_string) from err
|
||||
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||
pending_error, pending_error_string = err, error_string
|
||||
except KeyboardInterrupt:
|
||||
if pending_error is None:
|
||||
raise
|
||||
self.error("[skipped by user]", repeat_at_end=False)
|
||||
raise ConnectionException(error_string) from err
|
||||
raise ConnectionException(pending_error_string) from pending_error
|
||||
|
||||
raise AssertionError("get_json() retry loop exited without result")
|
||||
|
||||
@contextmanager
|
||||
def _graphql_session(self, referer: Optional[str], csrf: Optional[str] = None,
|
||||
lsd: Optional[str] = None) -> Iterator[requests.Session]:
|
||||
"""Short-lived session carrying the headers that the GraphQL endpoint expects.
|
||||
|
||||
Pass the page's `lsd` token for the web app's ``api/graphql`` endpoint."""
|
||||
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
|
||||
del tmpsession.headers['Connection']
|
||||
del tmpsession.headers['Content-Length']
|
||||
tmpsession.headers['authority'] = 'www.instagram.com'
|
||||
tmpsession.headers['scheme'] = 'https'
|
||||
tmpsession.headers['accept'] = '*/*'
|
||||
if csrf is not None:
|
||||
tmpsession.headers['x-csrftoken'] = csrf
|
||||
if referer is not None:
|
||||
tmpsession.headers['referer'] = urllib.parse.quote(referer)
|
||||
if lsd is not None:
|
||||
tmpsession.headers.update({'x-fb-lsd': lsd, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin'})
|
||||
yield tmpsession
|
||||
|
||||
def _csrf_token(self) -> str:
|
||||
"""The csrftoken cookie value, requesting the start page once if we do not have one yet."""
|
||||
def token_from_cookies() -> Optional[str]:
|
||||
return next((c.value for c in self._session.cookies if c.name == 'csrftoken' and c.value), None)
|
||||
csrf = token_from_cookies()
|
||||
if csrf:
|
||||
return csrf
|
||||
self._session.get('https://www.instagram.com/', timeout=self.request_timeout)
|
||||
return token_from_cookies() or ''
|
||||
|
||||
def _web_api_tokens(self) -> Dict[str, str]:
|
||||
"""The tokens that the web app sends along with its api/graphql queries, from the start page."""
|
||||
sessionid = self._session.cookies.get_dict().get("sessionid", "")
|
||||
if sessionid not in self._web_api_tokens_cache:
|
||||
html = self._get_page(self._session, "")
|
||||
tokens = {}
|
||||
for name, pattern in [
|
||||
("fb_dtsg", r'"DTSGInitialData",\[\],\{[^{}]*"token":"([^"]+)"'),
|
||||
("lsd", r'"LSD",\[\],\{[^{}]*"token":"([^"]+)"'),
|
||||
("av", r'"IG_USER_EIMU":"?(\d+)'),
|
||||
]:
|
||||
match = re.search(pattern, html)
|
||||
if match is not None:
|
||||
tokens[name] = match.group(1)
|
||||
# Logged out, the start page has no fb_dtsg, and the web app sends its queries without.
|
||||
elif name != "fb_dtsg" or self.is_logged_in:
|
||||
raise BadResponseException(
|
||||
"The start page did not contain the token {}.".format(name)
|
||||
)
|
||||
self._web_api_tokens_cache[sessionid] = tokens
|
||||
return self._web_api_tokens_cache[sessionid]
|
||||
|
||||
def graphql_query(self, query_hash: str, variables: Dict[str, Any],
|
||||
referer: Optional[str] = None) -> Dict[str, Any]:
|
||||
@@ -526,18 +649,8 @@ class InstaloaderContext:
|
||||
.. versionchanged:: 4.13.1
|
||||
Removed the `rhx_gis` parameter.
|
||||
"""
|
||||
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
|
||||
del tmpsession.headers['Connection']
|
||||
del tmpsession.headers['Content-Length']
|
||||
tmpsession.headers['authority'] = 'www.instagram.com'
|
||||
tmpsession.headers['scheme'] = 'https'
|
||||
tmpsession.headers['accept'] = '*/*'
|
||||
if referer is not None:
|
||||
tmpsession.headers['referer'] = urllib.parse.quote(referer)
|
||||
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
with self._graphql_session(referer) as tmpsession:
|
||||
resp_json = self.get_json('graphql/query',
|
||||
params={'query_hash': query_hash,
|
||||
'variables': variables_json},
|
||||
@@ -547,7 +660,7 @@ class InstaloaderContext:
|
||||
return resp_json
|
||||
|
||||
def doc_id_graphql_query(self, doc_id: str, variables: Dict[str, Any],
|
||||
referer: Optional[str] = None) -> Dict[str, Any]:
|
||||
referer: Optional[str] = None, web_api: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Do a doc_id-based GraphQL Query using method POST.
|
||||
|
||||
@@ -556,35 +669,23 @@ class InstaloaderContext:
|
||||
:param doc_id: doc_id for the query.
|
||||
:param variables: Variables for the Query.
|
||||
:param referer: HTTP Referer, or None.
|
||||
:param web_api: Query the web app's ``api/graphql`` endpoint, as the web app does, rather than
|
||||
``graphql/query``.
|
||||
:return: The server's response dictionary.
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Added `web_api` parameter.
|
||||
"""
|
||||
csrf = next((c.value for c in self._session.cookies
|
||||
if c.name == 'csrftoken' and c.value), None)
|
||||
if not csrf:
|
||||
self._session.get('https://www.instagram.com/', timeout=self.request_timeout)
|
||||
csrf = next((c.value for c in self._session.cookies
|
||||
if c.name == 'csrftoken' and c.value), '')
|
||||
|
||||
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
|
||||
del tmpsession.headers['Connection']
|
||||
del tmpsession.headers['Content-Length']
|
||||
tmpsession.headers['authority'] = 'www.instagram.com'
|
||||
tmpsession.headers['scheme'] = 'https'
|
||||
tmpsession.headers['accept'] = '*/*'
|
||||
tmpsession.headers['x-csrftoken'] = csrf
|
||||
if referer is not None:
|
||||
tmpsession.headers['referer'] = urllib.parse.quote(referer)
|
||||
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
|
||||
resp_json = self.get_json('graphql/query',
|
||||
params={'variables': variables_json,
|
||||
'doc_id': doc_id,
|
||||
'server_timestamps': 'true'},
|
||||
session=tmpsession,
|
||||
use_post=True)
|
||||
if 'status' not in resp_json:
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
params = {'variables': variables_json,
|
||||
'doc_id': doc_id,
|
||||
'server_timestamps': 'true',
|
||||
**(self._web_api_tokens() if web_api else {})}
|
||||
with self._graphql_session(referer, csrf=self._csrf_token(), lsd=params.get('lsd')) as tmpsession:
|
||||
resp_json = self.get_json('api/graphql' if web_api else 'graphql/query', params=params,
|
||||
session=tmpsession, use_post=True)
|
||||
# api/graphql does not respond with a "status" field.
|
||||
if not web_api and 'status' not in resp_json:
|
||||
self.error("GraphQL response did not contain a \"status\" field.")
|
||||
return resp_json
|
||||
|
||||
@@ -637,12 +738,33 @@ class InstaloaderContext:
|
||||
.. versionadded:: 4.2.1"""
|
||||
with copy_session(self._session, self.request_timeout) as tempsession:
|
||||
# Set headers to simulate an API request from iPad
|
||||
tempsession.headers['ig-intended-user-id'] = str(self.user_id)
|
||||
# ig-intended-user-id should name the logged-in user; self.user_id is derived
|
||||
# from the ds_user_id cookie, so it is set for any logged-in session. Without a
|
||||
# logged-in user, omit the header rather than sending the literal string "None".
|
||||
if self.user_id is not None:
|
||||
tempsession.headers['ig-intended-user-id'] = self.user_id
|
||||
tempsession.headers['x-pigeon-rawclienttime'] = '{:.6f}'.format(time.time())
|
||||
|
||||
# Add headers obtained from previous iPad request
|
||||
tempsession.headers.update(self.iphone_headers)
|
||||
|
||||
# The mobile API requires an Authorization bearer token for most
|
||||
# endpoints (reels_media, users/info, ...); without it they answer
|
||||
# with empty data or a "fail" status. A token is handed out via
|
||||
# the ig-set-authorization response header, but only by the few
|
||||
# endpoints that still accept cookie-only requests (e.g.
|
||||
# media/<id>/info/), so relying on having seen one makes success
|
||||
# depend on request order. The token is deterministically derived
|
||||
# from the session cookies, so construct it if none is known yet.
|
||||
if 'authorization' not in tempsession.headers and self.user_id is not None:
|
||||
sessionid = tempsession.cookies.get_dict().get('sessionid')
|
||||
if sessionid:
|
||||
token = b64encode(json.dumps(
|
||||
{'ds_user_id': self.user_id,
|
||||
'sessionid': sessionid},
|
||||
separators=(',', ':')).encode()).decode()
|
||||
tempsession.headers['authorization'] = 'Bearer IGT:2:{}'.format(token)
|
||||
|
||||
# Extract key information from cookies if we haven't got it already from a previous request
|
||||
header_cookies_mapping = {'x-mid': 'mid',
|
||||
'ig-u-ds-user-id': 'ds_user_id',
|
||||
@@ -688,6 +810,25 @@ class InstaloaderContext:
|
||||
|
||||
return response
|
||||
|
||||
def _get_page(self, session: requests.Session, path: str) -> str:
|
||||
"""Load a page of ``www.instagram.com`` and return its HTML."""
|
||||
self.do_sleep()
|
||||
self._rate_controller.wait_before_query('other')
|
||||
url = 'https://www.instagram.com/{0}'.format(path)
|
||||
# Instagram only embeds the data when the request looks like a browser's page navigation.
|
||||
headers = {'Accept': 'text/html', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none'}
|
||||
try:
|
||||
resp = session.get(url, headers=headers, allow_redirects=False)
|
||||
except requests.exceptions.RequestException as err:
|
||||
raise ConnectionException("Page request to {}: {}".format(url, err)) from err
|
||||
self._raise_if_login_redirect(resp)
|
||||
if resp.status_code == 404:
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
if resp.status_code != 200:
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
return resp.text
|
||||
|
||||
def get_page_data(self, path: str) -> List[Dict[str, Any]]:
|
||||
"""Load a page of ``www.instagram.com`` as a logged-out visitor and return the data it embeds.
|
||||
|
||||
@@ -699,23 +840,10 @@ class InstaloaderContext:
|
||||
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||
:raises ConnectionException: When the request failed.
|
||||
"""
|
||||
self.do_sleep()
|
||||
self._rate_controller.wait_before_query('other')
|
||||
url = 'https://www.instagram.com/{0}'.format(path)
|
||||
# Instagram only embeds the data when the request looks like a browser's page navigation.
|
||||
headers = {'Accept': 'text/html', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none'}
|
||||
try:
|
||||
with self.get_anonymous_session() as anonymous_session:
|
||||
resp = anonymous_session.get(url, headers=headers, allow_redirects=False)
|
||||
except requests.exceptions.RequestException as err:
|
||||
raise ConnectionException("Page request to {}: {}".format(url, err)) from err
|
||||
if resp.status_code == 404:
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
if resp.status_code != 200:
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
with self.get_anonymous_session() as anonymous_session:
|
||||
html = self._get_page(anonymous_session, path)
|
||||
data: List[Dict[str, Any]] = []
|
||||
for script in re.finditer(r'<script type="application/json"[^>]*>(.*?)</script>', resp.text, re.DOTALL):
|
||||
for script in re.finditer(r'<script type="application/json"[^>]*>(.*?)</script>', html, re.DOTALL):
|
||||
with suppress(json.decoder.JSONDecodeError):
|
||||
data.extend(_embedded_query_data(json.loads(script.group(1))))
|
||||
return data
|
||||
@@ -732,27 +860,26 @@ class InstaloaderContext:
|
||||
file.write(resp)
|
||||
os.replace(filename + '.temp', filename)
|
||||
|
||||
def get_raw(self, url: str, _attempt=1) -> requests.Response:
|
||||
def get_raw(self, url: str) -> requests.Response:
|
||||
"""Downloads a file anonymously.
|
||||
|
||||
This does not retry on its own; callers that need retrying are decorated with
|
||||
``_retry_on_connection_error``.
|
||||
|
||||
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||
:raises QueryReturnedForbiddenException: When the server responds with a 403.
|
||||
:raises ConnectionException: When download failed.
|
||||
|
||||
.. versionadded:: 4.2.1"""
|
||||
.. versionadded:: 4.2.1
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Removed the `_attempt` parameter.
|
||||
"""
|
||||
with self.get_anonymous_session() as anonymous_session:
|
||||
resp = anonymous_session.get(url, stream=True)
|
||||
if resp.status_code == 200:
|
||||
resp.raw.decode_content = True
|
||||
return resp
|
||||
else:
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
self._raise_for_download_status(resp)
|
||||
resp.raw.decode_content = True
|
||||
return resp
|
||||
|
||||
def get_and_write_raw(self, url: str, filename: str) -> None:
|
||||
"""Downloads and writes anonymously-requested raw data into a file.
|
||||
@@ -773,16 +900,8 @@ class InstaloaderContext:
|
||||
"""
|
||||
with self.get_anonymous_session() as anonymous_session:
|
||||
resp = anonymous_session.head(url, allow_redirects=allow_redirects)
|
||||
if resp.status_code == 200:
|
||||
return resp
|
||||
else:
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
self._raise_for_download_status(resp)
|
||||
return resp
|
||||
|
||||
|
||||
class RateController:
|
||||
|
||||
@@ -69,6 +69,9 @@ class NodeIterator(Iterator[T]):
|
||||
|
||||
.. versionchanged: 4.13
|
||||
Included support for `doc_id`-based queries (using POST method).
|
||||
|
||||
.. versionchanged: 4.16
|
||||
Added `web_api` parameter, see :meth:`InstaloaderContext.doc_id_graphql_query`.
|
||||
"""
|
||||
|
||||
_graphql_page_length = 12
|
||||
@@ -83,10 +86,12 @@ class NodeIterator(Iterator[T]):
|
||||
query_referer: Optional[str] = None,
|
||||
first_data: Optional[Dict[str, Any]] = None,
|
||||
is_first: Optional[Callable[[T, Optional[T]], bool]] = None,
|
||||
doc_id: Optional[str] = None):
|
||||
doc_id: Optional[str] = None,
|
||||
web_api: bool = False):
|
||||
self._context = context
|
||||
self._query_hash = query_hash
|
||||
self._doc_id = doc_id
|
||||
self._web_api = web_api
|
||||
self._edge_extractor = edge_extractor
|
||||
self._node_wrapper = node_wrapper
|
||||
self._query_variables = query_variables if query_variables is not None else {}
|
||||
@@ -110,15 +115,15 @@ class NodeIterator(Iterator[T]):
|
||||
return self._query_query_hash(self._query_hash, after)
|
||||
|
||||
def _query_doc_id(self, doc_id: str, after: Optional[str] = None) -> Dict:
|
||||
pagination_variables: Dict[str, Any] = {'__relay_internal__pv__PolarisFeedShareMenurelayprovider': False}
|
||||
pagination_variables: Dict[str, Any] = {}
|
||||
if after is not None:
|
||||
pagination_variables['after'] = after
|
||||
pagination_variables['before'] = None
|
||||
pagination_variables['first'] = 12
|
||||
pagination_variables['first'] = NodeIterator._graphql_page_length
|
||||
pagination_variables['last'] = None
|
||||
data = self._edge_extractor(
|
||||
self._context.doc_id_graphql_query(
|
||||
doc_id, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||
doc_id, {**self._query_variables, **pagination_variables}, self._query_referer, self._web_api
|
||||
)
|
||||
)
|
||||
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||
|
||||
+68
-30
@@ -279,7 +279,7 @@ class Post:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_post_data(media: Dict[str, Any], context: 'InstaloaderContext') -> Dict[str, Any]:
|
||||
def _normalize_post_data(media: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Normalize a Polaris media item to a legacy-compatible node, preserving the source structure."""
|
||||
media_types = {1: "GraphImage", 2: "GraphVideo", 8: "GraphSidecar"}
|
||||
media_type = media.get("media_type")
|
||||
@@ -306,10 +306,6 @@ class Post:
|
||||
pic_json["video_duration"] = media.get("video_duration")
|
||||
pic_json["video_view_count"] = media.get("view_count")
|
||||
pic_json["video_play_count"] = media.get("play_count")
|
||||
if media_type == 2 and pic_json["video_view_count"] is None:
|
||||
pic_json["video_play_count"] = Post._fetch_play_count_from_clips(
|
||||
context, media["user"]["pk"], media["code"]
|
||||
)
|
||||
caption = media.get("caption")
|
||||
caption_text = caption.get("text") if isinstance(caption, dict) else None
|
||||
pic_json["edge_media_to_caption"] = (
|
||||
@@ -419,12 +415,14 @@ class Post:
|
||||
"shortcode": self.shortcode,
|
||||
"__relay_internal__pv__PolarisAIGMMediaWebLabelEnabledrelayprovider": False,
|
||||
},
|
||||
# As the logged-out web app does.
|
||||
web_api=not self._context.is_logged_in,
|
||||
)
|
||||
web_info = (resp.get("data") or {}).get("xdt_api__v1__media__shortcode__web_info") or {}
|
||||
items = web_info.get("items")
|
||||
if not items:
|
||||
raise BadResponseException("Fetching Post metadata failed.")
|
||||
self._full_metadata_dict = Post._normalize_post_data(items[0], self._context)
|
||||
self._full_metadata_dict = Post._normalize_post_data(items[0])
|
||||
if self.shortcode != self._full_metadata_dict['shortcode']:
|
||||
self._node.update(self._full_metadata_dict)
|
||||
raise PostChangedException
|
||||
@@ -697,7 +695,11 @@ class Post:
|
||||
|
||||
.. versionadded:: 4.14.3"""
|
||||
if self.is_video:
|
||||
return self._field('video_play_count')
|
||||
play_count = self._field('video_play_count')
|
||||
# Not logged in, the clips connection is null.
|
||||
if play_count is None and self.video_view_count is None and self._context.is_logged_in:
|
||||
play_count = Post._fetch_play_count_from_clips(self._context, str(self.owner_id), self.shortcode)
|
||||
return play_count
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -1025,16 +1027,24 @@ class Profile:
|
||||
"""
|
||||
if profile_id in context.profile_id_cache:
|
||||
return context.profile_id_cache[profile_id]
|
||||
# Resolve the current username from the user id, then load the full profile.
|
||||
# The GraphQL user query previously used here started responding with HTTP 400.
|
||||
try:
|
||||
user = context.get_json(
|
||||
"api/v1/users/{0}/info/".format(profile_id), params={}
|
||||
).get('user')
|
||||
user = context.doc_id_graphql_query(
|
||||
'27937681195819736',
|
||||
{
|
||||
"id": str(profile_id),
|
||||
"render_surface": "PROFILE",
|
||||
"__relay_internal__pv__PolarisCannesGuardianExperienceEnabledrelayprovider": True,
|
||||
"__relay_internal__pv__PolarisCASB976ProfileEnabledrelayprovider": False,
|
||||
"__relay_internal__pv__PolarisRepostsConsumptionEnabledrelayprovider": False,
|
||||
"__relay_internal__pv__PolarisWebSchoolsEnabledrelayprovider": False,
|
||||
"enable_integrity_filters": True,
|
||||
},
|
||||
web_api=True,
|
||||
).get('data', {}).get('user')
|
||||
except QueryReturnedNotFoundException:
|
||||
user = None
|
||||
if user and user.get('username'):
|
||||
profile = cls.from_username(context, user['username'])
|
||||
profile = cls(context, cls._normalize_profile_data(user))
|
||||
else:
|
||||
raise ProfileNotExistsException("No profile found, the user may have blocked you (ID: " +
|
||||
str(profile_id) + ").")
|
||||
@@ -1137,6 +1147,9 @@ class Profile:
|
||||
normalized['id'] = normalized['pk']
|
||||
if 'edge_owner_to_timeline_media' not in normalized and 'media_count' in normalized:
|
||||
normalized['edge_owner_to_timeline_media'] = {'count': normalized['media_count']}
|
||||
# The logged-out profile page has all_media_count instead.
|
||||
if 'edge_owner_to_timeline_media' not in normalized and 'all_media_count' in normalized:
|
||||
normalized['edge_owner_to_timeline_media'] = {'count': normalized['all_media_count']}
|
||||
if 'edge_felix_video_timeline' not in normalized:
|
||||
normalized['edge_felix_video_timeline'] = {'count': 0}
|
||||
if 'edge_followed_by' not in normalized and 'follower_count' in normalized:
|
||||
@@ -1222,7 +1235,11 @@ class Profile:
|
||||
return self._metadata('followed_by_viewer')
|
||||
|
||||
@property
|
||||
def mediacount(self) -> int:
|
||||
def mediacount(self) -> Optional[int]:
|
||||
"""Number of posts, or None if unavailable (e.g. when not logged in).
|
||||
|
||||
.. versionchanged:: 4.16
|
||||
Returns None if unavailable (e.g. when not logged in)."""
|
||||
return self._metadata('edge_owner_to_timeline_media', 'count')
|
||||
|
||||
@property
|
||||
@@ -1374,35 +1391,53 @@ class Profile:
|
||||
def get_posts(self) -> NodeIterator[Post]:
|
||||
"""Retrieve all posts from a profile.
|
||||
|
||||
:rtype: NodeIterator[Post]"""
|
||||
self._obtain_metadata()
|
||||
:rtype: NodeIterator[Post]
|
||||
:raises PrivateProfileNotFollowedException: When logged in and the profile is private and not followed.
|
||||
:raises LoginRequiredException: When not logged in and the profile is private."""
|
||||
if not self._context.is_logged_in:
|
||||
# As the logged-out web app, take the first posts from the profile page, and query further ones
|
||||
# (PolarisLoggedOutDesktopWWWProfilePostsTabContentQuery_connection) by the page's "id" of the
|
||||
# profile, which is not its user ID.
|
||||
page = self._context.get_page_data(f"{self.username}/")
|
||||
# The page shows no posts of a private profile, rather than an error.
|
||||
if any((data.get("xig_user_by_username") or {}).get("is_private") for data in page):
|
||||
raise LoginRequiredException(f"Login required to access the posts of private profile {self.username}.")
|
||||
user = next((data["xig_user_by_username"] for data in page
|
||||
if "polaris_ordered_timeline_connection" in (data.get("xig_user_by_username") or {})),
|
||||
None)
|
||||
if user is None:
|
||||
raise BadResponseException(f"The page of profile {self.username} did not contain its posts.")
|
||||
return NodeIterator(
|
||||
context=self._context,
|
||||
edge_extractor=lambda d: d["data"]["user"]["edge_owner_to_timeline_media"],
|
||||
node_wrapper=lambda n: Post(self._context, n, self),
|
||||
edge_extractor=lambda d: d["data"]["node"]["polaris_ordered_timeline_connection"],
|
||||
# The nodes lack most of the post's metadata, as in get_reels().
|
||||
node_wrapper=lambda n: Post.from_shortcode(self._context, n["code"]),
|
||||
query_variables={
|
||||
"data": {
|
||||
"count": 12,
|
||||
"include_relationship_info": True,
|
||||
"latest_besties_reel_media": True,
|
||||
"latest_reel_media": True,
|
||||
},
|
||||
"id": self.userid,
|
||||
"id": user["id"],
|
||||
"__relay_internal__pv__PolarisSEOIsCrawlerrelayprovider": False,
|
||||
},
|
||||
query_referer="https://www.instagram.com/{0}/".format(self.username),
|
||||
is_first=Profile._make_is_newest_checker(),
|
||||
doc_id="7950326061742207",
|
||||
doc_id="28904005159232689",
|
||||
query_hash=None,
|
||||
first_data=self._metadata("edge_owner_to_timeline_media"),
|
||||
first_data=user["polaris_ordered_timeline_connection"],
|
||||
web_api=True,
|
||||
)
|
||||
self._obtain_metadata()
|
||||
# Instagram answers with an empty timeline rather than an error in that case. Unlike followed_by_viewer,
|
||||
# which defaults to False, friendship_status tells whether the follow state is actually known.
|
||||
if (self.is_private and self.username != (self._context.username or '').lower() and
|
||||
(self._node.get('friendship_status') or {}).get('following') is False):
|
||||
raise PrivateProfileNotFollowedException("Profile {} is private but not followed.".format(self.username))
|
||||
return NodeIterator(
|
||||
context=self._context,
|
||||
edge_extractor=lambda d: d["data"]["xdt_api__v1__feed__user_timeline_graphql_connection"],
|
||||
node_wrapper=lambda n: Post.from_iphone_struct(self._context, n),
|
||||
# The query and variables that the web app sends for a profile's posts tab
|
||||
# (PolarisProfilePostsTabContentQuery_connection). Instagram answers the
|
||||
# previous query, 7898261790222653, with "execution error".
|
||||
# previous queries, 7898261790222653 ("execution error") and 28975909992013618
|
||||
# (400 "feedback_required"), with errors.
|
||||
# It must be sent to the web app's api/graphql endpoint.
|
||||
query_variables={
|
||||
"data": {
|
||||
"count": 12,
|
||||
@@ -1415,13 +1450,16 @@ class Profile:
|
||||
"include_multi_captions": True,
|
||||
"username": self.username,
|
||||
"__relay_internal__pv__PolarisMultiCaptionCarouselEnabledrelayprovider": True,
|
||||
"__relay_internal__pv__PolarisShortDramaEnabledrelayprovider": False,
|
||||
"__relay_internal__pv__PolarisShortDramaEnabledrelayprovider": True,
|
||||
"__relay_internal__pv__PolarisSEOIsCrawlerrelayprovider": False,
|
||||
"__relay_internal__pv__PolarisThreadsStyleCommentsEnabledrelayprovider": False,
|
||||
"__relay_internal__pv__PolarisReelsRecoDebugOverlayEnabledrelayprovider": False,
|
||||
},
|
||||
query_referer="https://www.instagram.com/{0}/".format(self.username),
|
||||
is_first=Profile._make_is_newest_checker(),
|
||||
doc_id="28975909992013618",
|
||||
doc_id="29104886249145907",
|
||||
query_hash=None,
|
||||
web_api=True,
|
||||
)
|
||||
|
||||
def get_saved_posts(self) -> NodeIterator[Post]:
|
||||
|
||||
Reference in New Issue
Block a user