Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7dc03d06 | ||
|
|
f511049eaa | ||
|
|
76118ba196 | ||
|
|
83b10ab2f3 | ||
|
|
604c52e608 | ||
|
|
8949599232 | ||
|
|
c9c235061c | ||
|
|
328889689f | ||
|
|
e865c25a4e | ||
|
|
6d581c22f9 | ||
|
|
87aadeeb58 | ||
|
|
d331131cfe | ||
|
|
0dce1b009a | ||
|
|
bd77cd2058 | ||
|
|
8e47e8d5db | ||
|
|
946dd155d8 | ||
|
|
3565153597 | ||
|
|
f6c6ec5603 | ||
|
|
1cd0bab60b | ||
|
|
1c246eea6d | ||
|
|
3a365243d9 | ||
|
|
7f8882c9e9 | ||
|
|
bc401ac80f | ||
|
|
0a9837adca | ||
|
|
f4bf30c0e9 | ||
|
|
acfba74821 | ||
|
|
f46845c777 | ||
|
|
74f1536553 | ||
|
|
af4c2fe4b9 | ||
|
|
b7c656536d | ||
|
|
faf875c0f5 | ||
|
|
b3ebe3879d | ||
|
|
da79895e55 | ||
|
|
aaa7421fdf | ||
|
|
b9f213f047 | ||
|
|
fee070b299 | ||
|
|
275e28b635 | ||
|
|
808e599be6 | ||
|
|
5cb6f7f123 | ||
|
|
a2f1c658f0 | ||
|
|
05de644d77 | ||
|
|
b908855566 | ||
|
|
8d93bfcb95 | ||
|
|
bf68859f38 | ||
|
|
78fbe97b66 | ||
|
|
166a256c1c |
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
|
||||
#logs
|
||||
log.txt
|
||||
*.log
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# develop
|
||||
*.egg-info/
|
||||
|
||||
# Pypi dist
|
||||
dist/
|
||||
README.rst
|
||||
temporary/
|
||||
# Pypi wheel
|
||||
build/
|
||||
exclude/
|
||||
2
Pipfile
2
Pipfile
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
httpx = {extras = ["http2"], version = "0.16.1"}
|
||||
httpx = {extras = ["http2"]}
|
||||
|
||||
[dev-packages]
|
||||
pytest-mock = "*"
|
||||
|
||||
235
Pipfile.lock
generated
235
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e1eb34f14c75998519a90838b283ccd23bd168afa8e4837f956c5c4df66376f9"
|
||||
"sha256": "74b83f2e50bc16f8d90c06ddc775d24ee427f8481a2501f62170bf5b76a2f1bd"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -14,19 +14,28 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
|
||||
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
|
||||
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
|
||||
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||
],
|
||||
"version": "==2020.11.8"
|
||||
"version": "==2021.5.30"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
|
||||
"sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
|
||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"version": "==0.11.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"h2": {
|
||||
"hashes": [
|
||||
@@ -44,22 +53,22 @@
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:420700af11db658c782f7e8fda34f9dcd95e3ee93944dd97d78cb70247e0cd06",
|
||||
"sha256:dd1d762d4f7c2702149d06be2597c35fb154c5eff9789a8c5823fbcf4d2978d6"
|
||||
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
|
||||
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.2"
|
||||
"version": "==0.13.6"
|
||||
},
|
||||
"httpx": {
|
||||
"extras": [
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
|
||||
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
|
||||
"sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c",
|
||||
"sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
@@ -70,20 +79,20 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
],
|
||||
"version": "==2.10"
|
||||
"version": "==3.2"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
@@ -95,6 +104,14 @@
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0",
|
||||
"sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"atomicwrites": {
|
||||
"hashes": [
|
||||
"sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
|
||||
@@ -105,82 +122,92 @@
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.2.0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080",
|
||||
"sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd"
|
||||
"sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa",
|
||||
"sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.2.1"
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
|
||||
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
|
||||
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||
],
|
||||
"version": "==2020.11.8"
|
||||
"version": "==2021.5.30"
|
||||
},
|
||||
"chardet": {
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1",
|
||||
"sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"markers": "platform_system == 'Windows' and sys_platform == 'win32'",
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
||||
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
||||
"sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
|
||||
"sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.16"
|
||||
"version": "==0.17.1"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab",
|
||||
"sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87"
|
||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"version": "==0.11.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:420700af11db658c782f7e8fda34f9dcd95e3ee93944dd97d78cb70247e0cd06",
|
||||
"sha256:dd1d762d4f7c2702149d06be2597c35fb154c5eff9789a8c5823fbcf4d2978d6"
|
||||
"sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e",
|
||||
"sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.2"
|
||||
"version": "==0.13.6"
|
||||
},
|
||||
"httpx": {
|
||||
"extras": [
|
||||
"http2"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
|
||||
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
|
||||
"sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c",
|
||||
"sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.1"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
],
|
||||
"version": "==2.10"
|
||||
"version": "==3.2"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac",
|
||||
"sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.6.1"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
@@ -191,26 +218,26 @@
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:12de23258a95f3b13e5b167f7a641a878e91eab8ef16fafc077720a95e6115bb",
|
||||
"sha256:207bd66f2a9881c835dad653da04e196c678bf104f8252141d2d3c4f31051579"
|
||||
"sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8",
|
||||
"sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.5.0"
|
||||
"version": "==23.0.1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.7"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.0"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193",
|
||||
"sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9"
|
||||
"sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779",
|
||||
"sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"
|
||||
],
|
||||
"version": "==1.6.1"
|
||||
"version": "==1.7.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -222,19 +249,19 @@
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
|
||||
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
|
||||
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
|
||||
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.7.2"
|
||||
"version": "==2.9.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
@@ -246,27 +273,27 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
|
||||
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
|
||||
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
||||
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==6.1.2"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==6.2.4"
|
||||
},
|
||||
"pytest-httpx": {
|
||||
"hashes": [
|
||||
"sha256:0a7c56e559b23efbf857054cd74de60a7c540694a162423f89c70da6ad358d8e",
|
||||
"sha256:d32e8f6fb7e028f0313f5f5a2d463c8673eb43fd11a9bfe8527299717a7764c4"
|
||||
"sha256:1e135b8779060091fa1c87d8aff7904921e8bea95fce5e971a0262764d064b12",
|
||||
"sha256:e262932f2d3ce380da8273c7bacbcfdc2c94e167fa94da29571caaf1f4d3ba27"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.1"
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:024e405ad382646318c4281948aadf6fe1135632bea9cc67366ea0c4098ef5f2",
|
||||
"sha256:a4d6d37329e4a893e77d9ffa89e838dd2b45d5dc099984cf03c703ac8411bb82"
|
||||
"sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3",
|
||||
"sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.1"
|
||||
"version": "==3.6.1"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
@@ -278,18 +305,18 @@
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d",
|
||||
"sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a"
|
||||
"sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
|
||||
"sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
|
||||
],
|
||||
"version": "==28.0"
|
||||
"version": "==29.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.0"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
@@ -303,18 +330,18 @@
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
|
||||
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
@@ -334,27 +361,27 @@
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:5c0d04e06ccc0da1bd3fa5ae4550effcce42fcad947b4a6cafa77bdc9b09ff22",
|
||||
"sha256:9e7b8ab0ecbdbf0595adadd5f0ebbb9e69010e0bd48bbb0c15e550bf2a5292df"
|
||||
"sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64",
|
||||
"sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.54.0"
|
||||
"version": "==4.61.2"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab",
|
||||
"sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472"
|
||||
"sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218",
|
||||
"sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.0"
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
|
||||
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
|
||||
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
||||
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.2"
|
||||
"version": "==1.26.6"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
@@ -365,11 +392,19 @@
|
||||
},
|
||||
"wheel": {
|
||||
"hashes": [
|
||||
"sha256:906864fb722c0ab5f2f9c35b2c65e3af3c009402c108a709c0aca27bc2c9187b",
|
||||
"sha256:aaef9b8c36db72f8bf7f1e54f85f875c4d466819940863ca0b3f3f77f0a1646f"
|
||||
"sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e",
|
||||
"sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.36.1"
|
||||
"version": "==0.36.2"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3",
|
||||
"sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
|
||||
"""
|
||||
__copyright__ = 'Copyright (C) 2019, 2020 taizan-hokuto'
|
||||
__version__ = '0.5.0'
|
||||
__copyright__ = 'Copyright (C) 2019, 2020, 2021 taizan-hokuto'
|
||||
__version__ = '0.5.5'
|
||||
__license__ = 'MIT'
|
||||
__author__ = 'taizan-hokuto'
|
||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||
|
||||
@@ -4,7 +4,9 @@ from base64 import a85decode as dc
|
||||
headers = {
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 Edg/86.0.622.63,gzip(gfe)',
|
||||
}
|
||||
|
||||
m_headers = {
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Mobile Safari/537.36 Edg/91.0.864.59',
|
||||
}
|
||||
_sml = dc(b"BQS?8F#ks-GB\\6`H#IhIF^eo7@rH3;H#IhIF^eor06T''Ch\\'(?XmbXF>%9<FC/iuG%G#jBOQ!ICLqcS5tQB2;gCZ)?UdXC;f$GR3)MM2<(0>O7mh!,G@+K5?SO9T@okV").decode()
|
||||
_smr = dc(b"BQS?8F#ks-GB\\6`H#IhIF^eo7@rH3;H#IhIF^eor06T''Ch\\'(?XmbXF>%9<FC/iuG%G#jBOQ!iEb03+@<k(QAU-F)8U=fDGsP557S5F7CiNH7;)D3N77^*B6YU@\\?WfBr0emZX=#^").decode()
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from .. import util
|
||||
headers = config.headers
|
||||
MAX_RETRY = 10
|
||||
|
||||
|
||||
class PytchatCore:
|
||||
'''
|
||||
|
||||
@@ -30,9 +29,13 @@ class PytchatCore:
|
||||
|
||||
processor : ChatProcessor
|
||||
|
||||
client : httpx.Client
|
||||
The client for connecting youtube.
|
||||
You can specify any customized httpx client (e.g. coolies, user agent).
|
||||
|
||||
interruptable : bool
|
||||
Allows keyboard interrupts.
|
||||
Set this parameter to False if your own threading program causes
|
||||
Set this parameter to False if your own multi-threading program causes
|
||||
the problem.
|
||||
|
||||
force_replay : bool
|
||||
@@ -45,6 +48,10 @@ class PytchatCore:
|
||||
If True, when exceptions occur, the exception is held internally,
|
||||
and can be raised by raise_for_status().
|
||||
|
||||
replay_continuation : str
|
||||
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||
This parameter is only allowed in archived mode.
|
||||
|
||||
Attributes
|
||||
---------
|
||||
_is_alive : bool
|
||||
@@ -54,12 +61,15 @@ class PytchatCore:
|
||||
def __init__(self, video_id,
|
||||
seektime=-1,
|
||||
processor=DefaultProcessor(),
|
||||
client = httpx.Client(http2=True),
|
||||
interruptable=True,
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
hold_exception=True,
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
self._client = client
|
||||
self._video_id = util.extract_video_id(video_id)
|
||||
self.seektime = seektime
|
||||
if isinstance(processor, tuple):
|
||||
@@ -67,32 +77,36 @@ class PytchatCore:
|
||||
else:
|
||||
self.processor = processor
|
||||
self._is_alive = True
|
||||
self._is_replay = force_replay
|
||||
self._is_replay = force_replay or (replay_continuation is not None)
|
||||
self._hold_exception = hold_exception
|
||||
self._exception_holder = None
|
||||
self._parser = Parser(
|
||||
is_replay=self._is_replay,
|
||||
exception_holder=self._exception_holder
|
||||
)
|
||||
self._first_fetch = True
|
||||
self._fetch_url = config._sml
|
||||
self._first_fetch = replay_continuation is None
|
||||
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||
self._topchat_only = topchat_only
|
||||
self._dat = ''
|
||||
self._last_offset_ms = 0
|
||||
self._logger = logger
|
||||
self.continuation = replay_continuation
|
||||
if interruptable:
|
||||
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
time.sleep(0.1) # sleep shortly to prohibit skipping fetching data
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
self.continuation = liveparam.getparam(self._video_id, 3)
|
||||
|
||||
if not self.continuation:
|
||||
time.sleep(0.1) # sleep shortly to prohibit skipping fetching data
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
self.continuation = liveparam.getparam(
|
||||
self._video_id,
|
||||
channel_id=util.get_channelid(self._client, self._video_id),
|
||||
past_sec=3)
|
||||
|
||||
def _get_chat_component(self):
|
||||
|
||||
''' Fetch chat data and store them into buffer,
|
||||
get next continuaiton parameter and loop.
|
||||
|
||||
@@ -102,19 +116,18 @@ class PytchatCore:
|
||||
parameter for next chat data
|
||||
'''
|
||||
try:
|
||||
with httpx.Client(http2=True) as client:
|
||||
if self.continuation and self._is_alive:
|
||||
contents = self._get_contents(self.continuation, client, headers)
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
"timeout": timeout,
|
||||
"chatdata": chatdata
|
||||
}
|
||||
self.continuation = metadata.get('continuation')
|
||||
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||
return chat_component
|
||||
if self.continuation and self._is_alive:
|
||||
contents = self._get_contents(self.continuation, self._client, headers)
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
"timeout": timeout,
|
||||
"chatdata": chatdata
|
||||
}
|
||||
self.continuation = metadata.get('continuation')
|
||||
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||
return chat_component
|
||||
except exceptions.ChatParseException as e:
|
||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||
self._raise_exception(e)
|
||||
@@ -131,9 +144,8 @@ class PytchatCore:
|
||||
-------
|
||||
'continuationContents' which includes metadata & chat data.
|
||||
'''
|
||||
livechat_json = (
|
||||
self._get_livechat_json(continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||
)
|
||||
livechat_json = self._get_livechat_json(
|
||||
continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||
contents, dat = self._parser.get_contents(livechat_json)
|
||||
if self._dat == '' and dat:
|
||||
self._dat = dat
|
||||
@@ -143,8 +155,9 @@ class PytchatCore:
|
||||
self._parser.is_replay = True
|
||||
self._fetch_url = config._smr
|
||||
continuation = arcparam.getparam(
|
||||
self._video_id, self.seektime, self._topchat_only)
|
||||
livechat_json = (self._get_livechat_json(continuation, client, replay=True, offset_ms=self.seektime * 1000))
|
||||
self._video_id, self.seektime, self._topchat_only, util.get_channelid(client, self._video_id))
|
||||
livechat_json = self._get_livechat_json(
|
||||
continuation, client, replay=True, offset_ms=self.seektime * 1000)
|
||||
reload_continuation = self._parser.reload_continuation(
|
||||
self._parser.get_contents(livechat_json)[0])
|
||||
if reload_continuation:
|
||||
@@ -165,21 +178,20 @@ class PytchatCore:
|
||||
offset_ms = 0
|
||||
param = util.get_param(continuation, dat=self._dat, replay=replay, offsetms=offset_ms)
|
||||
for _ in range(MAX_RETRY + 1):
|
||||
with httpx.Client(http2=True) as client:
|
||||
try:
|
||||
response = client.post(self._fetch_url, json=param)
|
||||
livechat_json = json.loads(response.text)
|
||||
break
|
||||
except (json.JSONDecodeError, httpx.ConnectTimeout, httpx.ReadTimeout, httpx.ConnectError) as e:
|
||||
err = e
|
||||
time.sleep(2)
|
||||
continue
|
||||
try:
|
||||
response = client.post(self._fetch_url, json=param)
|
||||
livechat_json = response.json()
|
||||
break
|
||||
except (json.JSONDecodeError, httpx.ConnectTimeout, httpx.ReadTimeout, httpx.ConnectError) as e:
|
||||
err = e
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
self._logger.error(f"[{self._video_id}]"
|
||||
f"Exceeded retry count. Last error: {str(err)}")
|
||||
self._raise_exception(exceptions.RetryExceedMaxCount())
|
||||
return livechat_json
|
||||
|
||||
|
||||
def get(self):
|
||||
if self.is_alive():
|
||||
chat_component = self._get_chat_component()
|
||||
@@ -194,6 +206,8 @@ class PytchatCore:
|
||||
return self._is_alive
|
||||
|
||||
def terminate(self):
|
||||
if not self.is_alive():
|
||||
return
|
||||
self._is_alive = False
|
||||
self.processor.finalize()
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ class LiveChatAsync:
|
||||
topchat_only : bool
|
||||
If True, get only top chat.
|
||||
|
||||
replay_continuation : str
|
||||
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||
This parameter is only allowed in archived mode.
|
||||
|
||||
Attributes
|
||||
---------
|
||||
_is_alive : bool
|
||||
@@ -74,6 +78,7 @@ class LiveChatAsync:
|
||||
seektime=-1,
|
||||
processor=DefaultProcessor(),
|
||||
buffer=None,
|
||||
client = httpx.AsyncClient(http2=True),
|
||||
interruptable=True,
|
||||
callback=None,
|
||||
done_callback=None,
|
||||
@@ -82,7 +87,9 @@ class LiveChatAsync:
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
self._client:httpx.AsyncClient = client
|
||||
self._video_id = util.extract_video_id(video_id)
|
||||
self.seektime = seektime
|
||||
if isinstance(processor, tuple):
|
||||
@@ -95,17 +102,18 @@ class LiveChatAsync:
|
||||
self._exception_handler = exception_handler
|
||||
self._direct_mode = direct_mode
|
||||
self._is_alive = True
|
||||
self._is_replay = force_replay
|
||||
self._is_replay = force_replay or (replay_continuation is not None)
|
||||
self._parser = Parser(is_replay=self._is_replay)
|
||||
self._pauser = Queue()
|
||||
self._pauser.put_nowait(None)
|
||||
self._first_fetch = True
|
||||
self._fetch_url = config._sml
|
||||
self._first_fetch = replay_continuation is None
|
||||
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||
self._topchat_only = topchat_only
|
||||
self._dat = ''
|
||||
self._last_offset_ms = 0
|
||||
self._logger = logger
|
||||
self.exception = None
|
||||
self.continuation = replay_continuation
|
||||
LiveChatAsync._logger = logger
|
||||
|
||||
if exception_handler:
|
||||
@@ -145,8 +153,14 @@ class LiveChatAsync:
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
||||
await self._listen(initial_continuation)
|
||||
if not self.continuation:
|
||||
channel_id = await util.get_channelid_async(self._client, self._video_id)
|
||||
self.continuation = liveparam.getparam(
|
||||
self._video_id,
|
||||
channel_id,
|
||||
past_sec=3)
|
||||
|
||||
await self._listen(self.continuation)
|
||||
|
||||
async def _listen(self, continuation):
|
||||
''' Fetch chat data and store them into buffer,
|
||||
@@ -158,11 +172,14 @@ class LiveChatAsync:
|
||||
parameter for next chat data
|
||||
'''
|
||||
try:
|
||||
async with httpx.AsyncClient(http2=True) as client:
|
||||
async with self._client as client:
|
||||
while(continuation and self._is_alive):
|
||||
continuation = await self._check_pause(continuation)
|
||||
contents = await self._get_contents(continuation, client, headers)
|
||||
contents = await self._get_contents(continuation, client, headers) #Q#
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
continuation = metadata.get('continuation')
|
||||
if continuation:
|
||||
self.continuation = continuation
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
@@ -181,7 +198,6 @@ class LiveChatAsync:
|
||||
await self._buffer.put(chat_component)
|
||||
diff_time = timeout - (time.time() - time_mark)
|
||||
await asyncio.sleep(diff_time)
|
||||
continuation = metadata.get('continuation')
|
||||
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||
except exceptions.ChatParseException as e:
|
||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||
@@ -201,8 +217,12 @@ class LiveChatAsync:
|
||||
'''
|
||||
self._pauser.put_nowait(None)
|
||||
if not self._is_replay:
|
||||
continuation = liveparam.getparam(
|
||||
self._video_id, 3, self._topchat_only)
|
||||
async with self._client as client:
|
||||
channel_id = await util.get_channelid_async(client, self.video_id)
|
||||
continuation = liveparam.getparam(self._video_id,
|
||||
channel_id,
|
||||
past_sec=3)
|
||||
|
||||
return continuation
|
||||
|
||||
async def _get_contents(self, continuation, client, headers):
|
||||
@@ -223,8 +243,9 @@ class LiveChatAsync:
|
||||
'''Try to fetch archive chat data.'''
|
||||
self._parser.is_replay = True
|
||||
self._fetch_url = config._smr
|
||||
channelid = await util.get_channelid_async(client, self._video_id)
|
||||
continuation = arcparam.getparam(
|
||||
self._video_id, self.seektime, self._topchat_only)
|
||||
self._video_id, self.seektime, self._topchat_only, channelid)
|
||||
livechat_json = (await self._get_livechat_json(
|
||||
continuation, client, replay=True, offset_ms=self.seektime * 1000))
|
||||
reload_continuation = self._parser.reload_continuation(
|
||||
@@ -241,7 +262,6 @@ class LiveChatAsync:
|
||||
'''
|
||||
Get json which includes chat data.
|
||||
'''
|
||||
# continuation = urllib.parse.quote(continuation)
|
||||
livechat_json = None
|
||||
if offset_ms < 0:
|
||||
offset_ms = 0
|
||||
@@ -322,12 +342,14 @@ class LiveChatAsync:
|
||||
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||
|
||||
def terminate(self):
|
||||
if not self.is_alive():
|
||||
return
|
||||
if self._pauser.empty():
|
||||
self._pauser.put_nowait(None)
|
||||
self._is_alive = False
|
||||
self._buffer.put_nowait({})
|
||||
self.processor.finalize()
|
||||
|
||||
|
||||
def _keyboard_interrupt(self):
|
||||
self.exception = exceptions.ChatDataFinished()
|
||||
self.terminate()
|
||||
|
||||
@@ -60,6 +60,10 @@ class LiveChat:
|
||||
topchat_only : bool
|
||||
If True, get only top chat.
|
||||
|
||||
replay_continuation : str
|
||||
If this parameter is not None, the processor will attempt to get chat data from continuation.
|
||||
This parameter is only allowed in archived mode.
|
||||
|
||||
Attributes
|
||||
---------
|
||||
_executor : ThreadPoolExecutor
|
||||
@@ -74,6 +78,7 @@ class LiveChat:
|
||||
def __init__(self, video_id,
|
||||
seektime=-1,
|
||||
processor=DefaultProcessor(),
|
||||
client = httpx.Client(http2=True),
|
||||
buffer=None,
|
||||
interruptable=True,
|
||||
callback=None,
|
||||
@@ -81,8 +86,10 @@ class LiveChat:
|
||||
direct_mode=False,
|
||||
force_replay=False,
|
||||
topchat_only=False,
|
||||
logger=config.logger(__name__)
|
||||
logger=config.logger(__name__),
|
||||
replay_continuation=None
|
||||
):
|
||||
self._client = client
|
||||
self._video_id = util.extract_video_id(video_id)
|
||||
self.seektime = seektime
|
||||
if isinstance(processor, tuple):
|
||||
@@ -95,17 +102,19 @@ class LiveChat:
|
||||
self._executor = ThreadPoolExecutor(max_workers=2)
|
||||
self._direct_mode = direct_mode
|
||||
self._is_alive = True
|
||||
self._is_replay = force_replay
|
||||
self._is_replay = force_replay or (replay_continuation is not None)
|
||||
self._parser = Parser(is_replay=self._is_replay)
|
||||
self._pauser = Queue()
|
||||
self._pauser.put_nowait(None)
|
||||
self._first_fetch = True
|
||||
self._fetch_url = config._sml
|
||||
self._first_fetch = replay_continuation is None
|
||||
self._fetch_url = config._sml if replay_continuation is None else config._smr
|
||||
self._topchat_only = topchat_only
|
||||
self._dat = ''
|
||||
self._last_offset_ms = 0
|
||||
self._event = Event()
|
||||
self._logger = logger
|
||||
self._event = Event()
|
||||
self.continuation = replay_continuation
|
||||
|
||||
self.exception = None
|
||||
if interruptable:
|
||||
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
|
||||
@@ -140,8 +149,12 @@ class LiveChat:
|
||||
"""Fetch first continuation parameter,
|
||||
create and start _listen loop.
|
||||
"""
|
||||
initial_continuation = liveparam.getparam(self._video_id, 3)
|
||||
self._listen(initial_continuation)
|
||||
if not self.continuation:
|
||||
self.continuation = liveparam.getparam(
|
||||
self._video_id,
|
||||
channel_id=util.get_channelid(self._client, self._video_id),
|
||||
past_sec=3)
|
||||
self._listen(self.continuation)
|
||||
|
||||
def _listen(self, continuation):
|
||||
''' Fetch chat data and store them into buffer,
|
||||
@@ -153,11 +166,14 @@ class LiveChat:
|
||||
parameter for next chat data
|
||||
'''
|
||||
try:
|
||||
with httpx.Client(http2=True) as client:
|
||||
with self._client as client:
|
||||
while(continuation and self._is_alive):
|
||||
continuation = self._check_pause(continuation)
|
||||
contents = self._get_contents(continuation, client, headers)
|
||||
metadata, chatdata = self._parser.parse(contents)
|
||||
continuation = metadata.get('continuation')
|
||||
if continuation:
|
||||
self.continuation = continuation
|
||||
timeout = metadata['timeoutMs'] / 1000
|
||||
chat_component = {
|
||||
"video_id": self._video_id,
|
||||
@@ -176,7 +192,6 @@ class LiveChat:
|
||||
self._buffer.put(chat_component)
|
||||
diff_time = timeout - (time.time() - time_mark)
|
||||
self._event.wait(diff_time if diff_time > 0 else 0)
|
||||
continuation = metadata.get('continuation')
|
||||
self._last_offset_ms = metadata.get('last_offset_ms', 0)
|
||||
except exceptions.ChatParseException as e:
|
||||
self._logger.debug(f"[{self._video_id}]{str(e)}")
|
||||
@@ -196,7 +211,10 @@ class LiveChat:
|
||||
'''
|
||||
self._pauser.put_nowait(None)
|
||||
if not self._is_replay:
|
||||
continuation = liveparam.getparam(self._video_id, 3)
|
||||
continuation = liveparam.getparam(
|
||||
self._video_id, channel_id=util.get_channelid(httpx.Client(http2=True), self._video_id),
|
||||
past_sec=3, topchat_only=self._topchat_only)
|
||||
|
||||
return continuation
|
||||
|
||||
def _get_contents(self, continuation, client, headers):
|
||||
@@ -208,7 +226,8 @@ class LiveChat:
|
||||
-------
|
||||
'continuationContents' which includes metadata & chat data.
|
||||
'''
|
||||
livechat_json = self._get_livechat_json(continuation, client, headers)
|
||||
livechat_json = self._get_livechat_json(
|
||||
continuation, client, replay=self._is_replay, offset_ms=self._last_offset_ms)
|
||||
contents, dat = self._parser.get_contents(livechat_json)
|
||||
if self._dat == '' and dat:
|
||||
self._dat = dat
|
||||
@@ -218,9 +237,9 @@ class LiveChat:
|
||||
self._parser.is_replay = True
|
||||
self._fetch_url = config._smr
|
||||
continuation = arcparam.getparam(
|
||||
self._video_id, self.seektime, self._topchat_only)
|
||||
livechat_json = (self._get_livechat_json(
|
||||
continuation, client, replay=True, offset_ms=self.seektime * 1000))
|
||||
self._video_id, self.seektime, self._topchat_only, util.get_channelid(client, self._video_id))
|
||||
livechat_json = self._get_livechat_json(
|
||||
continuation, client, replay=True, offset_ms=self.seektime * 1000)
|
||||
reload_continuation = self._parser.reload_continuation(
|
||||
self._parser.get_contents(livechat_json)[0])
|
||||
if reload_continuation:
|
||||
@@ -235,15 +254,14 @@ class LiveChat:
|
||||
'''
|
||||
Get json which includes chat data.
|
||||
'''
|
||||
# continuation = urllib.parse.quote(continuation)
|
||||
livechat_json = None
|
||||
if offset_ms < 0:
|
||||
offset_ms = 0
|
||||
param = util.get_param(continuation, dat=self._dat, replay=replay, offsetms=offset_ms)
|
||||
for _ in range(MAX_RETRY + 1):
|
||||
try:
|
||||
resp = client.post(self._fetch_url, json=param)
|
||||
livechat_json = resp.json()
|
||||
response = client.post(self._fetch_url, json=param)
|
||||
livechat_json = response.json()
|
||||
break
|
||||
except (json.JSONDecodeError, httpx.HTTPError):
|
||||
time.sleep(2)
|
||||
@@ -316,6 +334,8 @@ class LiveChat:
|
||||
self._logger.debug(f'[{self._video_id}] cancelled:{sender}')
|
||||
|
||||
def terminate(self):
|
||||
if not self.is_alive():
|
||||
return
|
||||
if self._pauser.empty():
|
||||
self._pauser.put_nowait(None)
|
||||
self._is_alive = False
|
||||
|
||||
@@ -3,8 +3,7 @@ from base64 import urlsafe_b64encode as b64enc
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _header(video_id) -> str:
|
||||
channel_id = '_' * 24
|
||||
def _header(video_id, channel_id) -> str:
|
||||
S1_3 = enc.rs(1, video_id)
|
||||
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||
@@ -13,31 +12,26 @@ def _header(video_id) -> str:
|
||||
return b64enc(header_replay)
|
||||
|
||||
|
||||
def _build(video_id, seektime, topchat_only) -> str:
|
||||
def _build(video_id, seektime, topchat_only, channel_id) -> str:
|
||||
chattype = 4 if topchat_only else 1
|
||||
fetch_before_start = 3
|
||||
timestamp = 1000
|
||||
if seektime < 0:
|
||||
fetch_before_start = 4
|
||||
elif seektime == 0:
|
||||
timestamp = 1000
|
||||
else:
|
||||
timestamp = int(seektime * 1000000)
|
||||
header = enc.rs(3, _header(video_id))
|
||||
seektime = 0
|
||||
timestamp = int(seektime * 1000000)
|
||||
header = enc.rs(3, _header(video_id, channel_id))
|
||||
timestamp = enc.nm(5, timestamp)
|
||||
s6 = enc.nm(6, 0)
|
||||
s7 = enc.nm(7, 0)
|
||||
s8 = enc.nm(8, 0)
|
||||
s9 = enc.nm(9, fetch_before_start)
|
||||
s9 = enc.nm(9, 4)
|
||||
s10 = enc.rs(10, enc.nm(4, 0))
|
||||
chattype = enc.rs(14, enc.nm(1, chattype))
|
||||
chattype = enc.rs(14, enc.nm(1, 4))
|
||||
s15 = enc.nm(15, 0)
|
||||
entity = b''.join((header, timestamp, s6, s7, s8, s9, s10, chattype, s15))
|
||||
continuation = enc.rs(156074452, entity)
|
||||
return quote(b64enc(continuation).decode())
|
||||
|
||||
|
||||
def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
||||
def getparam(video_id, seektime=0, topchat_only=False, channel_id='') -> str:
|
||||
'''
|
||||
Parameter
|
||||
---------
|
||||
@@ -47,4 +41,4 @@ def getparam(video_id, seektime=-1, topchat_only=False) -> str:
|
||||
topchat_only : bool
|
||||
if True, fetch only 'top chat'
|
||||
'''
|
||||
return _build(video_id, seektime, topchat_only)
|
||||
return _build(video_id, seektime, topchat_only, channel_id)
|
||||
|
||||
@@ -5,11 +5,16 @@ from base64 import urlsafe_b64encode as b64enc
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def _header(video_id) -> str:
|
||||
return b64enc(enc.rs(1, enc.rs(1, enc.rs(1, video_id))) + enc.nm(4, 1))
|
||||
def _header(video_id, channel_id) -> str:
|
||||
S1_3 = enc.rs(1, video_id)
|
||||
S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id)
|
||||
S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5)
|
||||
S3 = enc.rs(48687757, enc.rs(1, video_id))
|
||||
header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1)
|
||||
return b64enc(header_replay)
|
||||
|
||||
|
||||
def _build(video_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||
def _build(video_id, channel_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||
chattype = 4 if topchat_only else 1
|
||||
|
||||
b1 = enc.nm(1, 0)
|
||||
@@ -23,7 +28,7 @@ def _build(video_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
|
||||
b11 = enc.nm(11, 3)
|
||||
b15 = enc.nm(15, 0)
|
||||
|
||||
header = enc.rs(3, _header(video_id))
|
||||
header = enc.rs(3, _header(video_id, channel_id))
|
||||
timestamp1 = enc.nm(5, ts1)
|
||||
s6 = enc.nm(6, 0)
|
||||
s7 = enc.nm(7, 0)
|
||||
@@ -53,7 +58,7 @@ def _times(past_sec):
|
||||
return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5]))
|
||||
|
||||
|
||||
def getparam(video_id, past_sec=0, topchat_only=False) -> str:
|
||||
def getparam(video_id, channel_id, past_sec=0, topchat_only=False) -> str:
|
||||
'''
|
||||
Parameter
|
||||
---------
|
||||
@@ -62,4 +67,4 @@ def getparam(video_id, past_sec=0, topchat_only=False) -> str:
|
||||
topchat_only : bool
|
||||
if True, fetch only 'top chat'
|
||||
'''
|
||||
return _build(video_id, *_times(past_sec), topchat_only)
|
||||
return _build(video_id, channel_id, *_times(past_sec), topchat_only)
|
||||
@@ -28,7 +28,7 @@ class Parser:
|
||||
def get_contents(self, jsn):
|
||||
if jsn is None:
|
||||
self.raise_exception(exceptions.IllegalFunctionCall('Called with none JSON object.'))
|
||||
if jsn.get("error") or jsn.get("responseContext", {}).get("errors"):
|
||||
if jsn.get("responseContext", {}).get("errors"):
|
||||
raise exceptions.ResponseContextError(
|
||||
'The video_id would be wrong, or video is deleted or private.')
|
||||
contents = jsn.get('continuationContents')
|
||||
|
||||
@@ -7,6 +7,7 @@ from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
||||
from .renderer.membership import LiveChatMembershipItemRenderer
|
||||
from .renderer.donation import LiveChatDonationAnnouncementRenderer
|
||||
from .. chat_processor import ChatProcessor
|
||||
from ... import config
|
||||
|
||||
@@ -124,7 +125,8 @@ class DefaultProcessor(ChatProcessor):
|
||||
"liveChatPaidMessageRenderer": LiveChatPaidMessageRenderer(),
|
||||
"liveChatPaidStickerRenderer": LiveChatPaidStickerRenderer(),
|
||||
"liveChatLegacyPaidMessageRenderer": LiveChatLegacyPaidMessageRenderer(),
|
||||
"liveChatMembershipItemRenderer": LiveChatMembershipItemRenderer()
|
||||
"liveChatMembershipItemRenderer": LiveChatMembershipItemRenderer(),
|
||||
"liveChatDonationAnnouncementRenderer": LiveChatDonationAnnouncementRenderer(),
|
||||
}
|
||||
|
||||
def process(self, chat_components: list):
|
||||
|
||||
6
pytchat/processors/default/renderer/donation.py
Normal file
6
pytchat/processors/default/renderer/donation.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .base import BaseRenderer
|
||||
|
||||
|
||||
class LiveChatDonationAnnouncementRenderer(BaseRenderer):
|
||||
def settype(self):
|
||||
self.chat.type = "donation"
|
||||
@@ -82,16 +82,17 @@ class RingQueue:
|
||||
|
||||
class SpeedCalculator(ChatProcessor, RingQueue):
|
||||
"""
|
||||
チャットの勢いを計算する。
|
||||
Calculate the momentum of the chat.
|
||||
|
||||
一定期間のチャットデータのうち、最初のチャットの投稿時刻と
|
||||
最後のチャットの投稿時刻の差を、チャット数で割り返し
|
||||
1分あたりの速度に換算する。
|
||||
Divide the difference between the time of the first chat and
|
||||
the time of the last chat in the chat data over a period of
|
||||
time by the number of chats and convert it to speed per minute.
|
||||
|
||||
Parameter
|
||||
----------
|
||||
capacity : int
|
||||
RingQueueに格納するチャット勢い算出用データの最大数
|
||||
Maximum number of data for calculating chat momentum
|
||||
to be stored in RingQueue.
|
||||
"""
|
||||
|
||||
def __init__(self, capacity=10):
|
||||
@@ -111,17 +112,17 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
||||
|
||||
def _calc_speed(self):
|
||||
"""
|
||||
RingQueue内のチャット勢い算出用データリストを元に、
|
||||
チャット速度を計算して返す
|
||||
Calculates the chat speed based on the data list for calculating
|
||||
the chat momentum in RingQueue.
|
||||
|
||||
Return
|
||||
---------------------------
|
||||
チャット速度(1分間で換算したチャット数)
|
||||
Chat speed (number of chats converted per minute)
|
||||
"""
|
||||
try:
|
||||
# キュー内の総チャット数
|
||||
# Total number of chats in the queue
|
||||
total = sum(item['chat_count'] for item in self.items)
|
||||
# キュー内の最初と最後のチャットの時間差
|
||||
# Interval between the first and last chats in the queue
|
||||
duration = (self.items[self.last_pos]['endtime'] - self.items[self.first_pos]['starttime'])
|
||||
if duration != 0:
|
||||
return int(total * 60 / duration)
|
||||
@@ -131,19 +132,12 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
||||
|
||||
def _put_chatdata(self, actions):
|
||||
"""
|
||||
チャットデータからタイムスタンプを読み取り、勢い測定用のデータを組み立て、
|
||||
RingQueueに投入する。
|
||||
200円以上のスパチャはtickerとmessageの2つのデータが生成されるが、
|
||||
tickerの方は時刻データの場所が異なることを利用し、勢いの集計から除外している。
|
||||
Parameter
|
||||
---------
|
||||
actions : List[dict]
|
||||
チャットデータ(addChatItemAction) のリスト
|
||||
List of addChatItemActions
|
||||
"""
|
||||
def _put_emptydata():
|
||||
'''
|
||||
チャットデータがない場合に空のデータをキューに投入する。
|
||||
'''
|
||||
timestamp_now = int(time.time())
|
||||
self.put({
|
||||
'chat_count': 0,
|
||||
@@ -152,9 +146,6 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
||||
})
|
||||
|
||||
def _get_timestamp(action: dict):
|
||||
"""
|
||||
チャットデータから時刻データを取り出す。
|
||||
"""
|
||||
try:
|
||||
item = action['addChatItemAction']['item']
|
||||
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
|
||||
@@ -166,32 +157,24 @@ class SpeedCalculator(ChatProcessor, RingQueue):
|
||||
_put_emptydata()
|
||||
return
|
||||
|
||||
# actions内の時刻データを持つチャットデータの数
|
||||
counter = 0
|
||||
# actions内の最初のチャットデータの時刻
|
||||
starttime = None
|
||||
# actions内の最後のチャットデータの時刻
|
||||
endtime = None
|
||||
|
||||
for action in actions:
|
||||
# チャットデータからtimestampUsecを読み取る
|
||||
# Get timestampUsec from chatdata
|
||||
gettime = _get_timestamp(action)
|
||||
|
||||
# 時刻のないデータだった場合は次の行のデータで読み取り試行
|
||||
if gettime is None:
|
||||
continue
|
||||
|
||||
# 最初に有効な時刻を持つデータのtimestampをstarttimeに設定
|
||||
if starttime is None:
|
||||
starttime = gettime
|
||||
|
||||
# 最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく)
|
||||
endtime = gettime
|
||||
|
||||
# チャットの数をインクリメント
|
||||
counter += 1
|
||||
|
||||
# チャット速度用のデータをRingQueueに送る
|
||||
if starttime is None or endtime is None:
|
||||
_put_emptydata()
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ import httpx
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
from .. import config
|
||||
from .. exceptions import InvalidVideoIdException
|
||||
|
||||
@@ -10,6 +11,10 @@ PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
|
||||
|
||||
PATTERN_YTURL = re.compile(r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)")
|
||||
|
||||
PATTERN_CHANNEL = re.compile(r"\\\"channelId\\\":\\\"(.{24})\\\"")
|
||||
|
||||
PATTERN_M_CHANNEL = re.compile(r"\"channelId\":\"(.{24})\"")
|
||||
|
||||
YT_VIDEO_ID_LENGTH = 11
|
||||
|
||||
CLIENT_VERSION = ''.join(("2.", (datetime.datetime.today() - datetime.timedelta(days=1)).strftime("%Y%m%d"), ".01.00"))
|
||||
@@ -92,3 +97,51 @@ def extract_video_id(url_or_id: str) -> str:
|
||||
if ret is None or len(ret) != YT_VIDEO_ID_LENGTH:
|
||||
raise InvalidVideoIdException(f"Invalid video id: {url_or_id}")
|
||||
return ret
|
||||
|
||||
|
||||
def get_channelid(client, video_id):
|
||||
resp = client.get("https://www.youtube.com/embed/{}".format(quote(video_id)), headers=config.headers)
|
||||
match = re.search(PATTERN_CHANNEL, resp.text)
|
||||
try:
|
||||
if match is None:
|
||||
raise IndexError
|
||||
ret = match.group(1)
|
||||
except IndexError:
|
||||
ret = get_channelid_2nd(client, video_id)
|
||||
return ret
|
||||
|
||||
|
||||
def get_channelid_2nd(client, video_id):
|
||||
resp = client.get("https://m.youtube.com/watch?v={}".format(quote(video_id)), headers=config.m_headers)
|
||||
|
||||
match = re.search(PATTERN_M_CHANNEL, resp.text)
|
||||
if match is None:
|
||||
raise InvalidVideoIdException(f"Cannot find channel id for video id:{video_id}. This video id seems to be invalid.")
|
||||
try:
|
||||
ret = match.group(1)
|
||||
except IndexError:
|
||||
raise InvalidVideoIdException(f"Invalid video id: {video_id}")
|
||||
return ret
|
||||
|
||||
|
||||
async def get_channelid_async(client, video_id):
|
||||
resp = await client.get("https://www.youtube.com/embed/{}".format(quote(video_id)), headers=config.headers)
|
||||
match = re.search(PATTERN_CHANNEL, resp.text)
|
||||
try:
|
||||
if match is None:
|
||||
raise IndexError
|
||||
ret = match.group(1)
|
||||
except IndexError:
|
||||
ret = await get_channelid_async_2nd(client, video_id)
|
||||
return ret
|
||||
|
||||
async def get_channelid_async_2nd(client, video_id):
|
||||
resp = await client.get("https://m.youtube.com/watch?v={}".format(quote(video_id)), headers=config.m_headers)
|
||||
match = re.search(PATTERN_M_CHANNEL, resp.text)
|
||||
if match is None:
|
||||
raise InvalidVideoIdException(f"Cannot find channel id for video id:{video_id}. This video id seems to be invalid.")
|
||||
try:
|
||||
ret = match.group(1)
|
||||
except IndexError:
|
||||
raise InvalidVideoIdException(f"Invalid video id: {video_id}")
|
||||
return ret
|
||||
@@ -1 +1 @@
|
||||
httpx[http2]==0.16.1
|
||||
httpx[http2]
|
||||
5
setup.py
5
setup.py
@@ -50,8 +50,9 @@ setup(
|
||||
'Natural Language :: Japanese',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
description="a python library for fetching youtube live chat.",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import json
|
||||
import httpx
|
||||
import pytchat.config as config
|
||||
from pytchat.paramgen import arcparam
|
||||
from pytchat.parser.live import Parser
|
||||
|
||||
|
||||
def test_arcparam_0(mocker):
|
||||
param = arcparam.getparam("01234567890", -1)
|
||||
assert param == "op2w0wSDARpsQ2pnYURRb0xNREV5TXpRMU5qYzRPVEFxSndvWVgxOWZYMTlmWDE5ZlgxOWZYMTlmWDE5ZlgxOWZYMTlmRWdzd01USXpORFUyTnpnNU1Cb1Q2cWpkdVFFTkNnc3dNVEl6TkRVMk56ZzVNQ0FCKOgHMAA4AEAASARSAiAAcgIIAXgA"
|
||||
|
||||
|
||||
def test_arcparam_1(mocker):
|
||||
param = arcparam.getparam("01234567890", seektime=100000)
|
||||
assert param == "op2w0wSHARpsQ2pnYURRb0xNREV5TXpRMU5qYzRPVEFxSndvWVgxOWZYMTlmWDE5ZlgxOWZYMTlmWDE5ZlgxOWZYMTlmRWdzd01USXpORFUyTnpnNU1Cb1Q2cWpkdVFFTkNnc3dNVEl6TkRVMk56ZzVNQ0FCKIDQ28P0AjAAOABAAEgDUgIgAHICCAF4AA%3D%3D"
|
||||
|
||||
def test_arcparam_3(mocker):
|
||||
param = arcparam.getparam("01234567890")
|
||||
assert param == "op2w0wSDARpsQ2pnYURRb0xNREV5TXpRMU5qYzRPVEFxSndvWVgxOWZYMTlmWDE5ZlgxOWZYMTlmWDE5ZlgxOWZYMTlmRWdzd01USXpORFUyTnpnNU1Cb1Q2cWpkdVFFTkNnc3dNVEl6TkRVMk56ZzVNQ0FCKOgHMAA4AEAASARSAiAAcgIIAXgA"
|
||||
@@ -1,140 +0,0 @@
|
||||
from pytchat.processors.superchat.calculator import SuperchatCalculator
|
||||
|
||||
get_item = SuperchatCalculator()._get_item
|
||||
|
||||
dict_test = {
|
||||
'root':{
|
||||
'node0' : 'value0',
|
||||
'node1' : 'value1',
|
||||
'node2' : {
|
||||
'node2-0' : 'value2-0'
|
||||
},
|
||||
|
||||
'node3' : [
|
||||
{'node3-0' : 'value3-0'},
|
||||
{'node3-1' :
|
||||
{'node3-1-0' : 'value3-1-0'}
|
||||
}
|
||||
],
|
||||
'node4' : [],
|
||||
'node5' : [
|
||||
[
|
||||
{'node5-1-0' : 'value5-1-0'},
|
||||
{'node5-1-1' : 'value5-1-1'},
|
||||
],
|
||||
{'node5-0' : 'value5-0'},
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
items_test0 = [
|
||||
'root',
|
||||
'node1'
|
||||
]
|
||||
|
||||
|
||||
items_test_not_found0 = [
|
||||
'root',
|
||||
'other_data'
|
||||
]
|
||||
|
||||
|
||||
items_test_nest = [
|
||||
'root',
|
||||
'node2',
|
||||
'node2-0'
|
||||
]
|
||||
|
||||
items_test_list0 = [
|
||||
'root',
|
||||
'node3',
|
||||
1,
|
||||
'node3-1'
|
||||
]
|
||||
|
||||
items_test_list1 = [
|
||||
'root',
|
||||
'node3',
|
||||
1,
|
||||
'node3-1',
|
||||
'node3-1-0'
|
||||
]
|
||||
|
||||
items_test_list2 = [
|
||||
'root',
|
||||
'node4',
|
||||
None
|
||||
]
|
||||
|
||||
items_test_list3 = [
|
||||
'root',
|
||||
'node4'
|
||||
]
|
||||
|
||||
items_test_list_nest = [
|
||||
'root',
|
||||
'node5',
|
||||
0,
|
||||
1,
|
||||
'node5-1-1'
|
||||
]
|
||||
|
||||
items_test_list_nest_not_found1 = [
|
||||
'root',
|
||||
'node5',
|
||||
0,
|
||||
1,
|
||||
'node5-1-1',
|
||||
'nodez'
|
||||
]
|
||||
|
||||
items_test_not_found1 = [
|
||||
'root',
|
||||
'node3',
|
||||
2,
|
||||
'node3-1',
|
||||
'node3-1-0'
|
||||
]
|
||||
|
||||
items_test_not_found2 = [
|
||||
'root',
|
||||
'node3',
|
||||
2,
|
||||
'node3-1',
|
||||
'node3-1-0',
|
||||
'nodex'
|
||||
]
|
||||
def test_get_items_0():
|
||||
assert get_item(dict_test, items_test0) == 'value1'
|
||||
|
||||
def test_get_items_1():
|
||||
assert get_item(dict_test, items_test_not_found0) is None
|
||||
|
||||
def test_get_items_2():
|
||||
assert get_item(dict_test, items_test_nest) == 'value2-0'
|
||||
|
||||
def test_get_items_3():
|
||||
assert get_item(
|
||||
dict_test, items_test_list0) == {'node3-1-0' : 'value3-1-0'}
|
||||
|
||||
def test_get_items_4():
|
||||
assert get_item(dict_test, items_test_list1) == 'value3-1-0'
|
||||
|
||||
def test_get_items_5():
|
||||
assert get_item(dict_test, items_test_not_found1) == None
|
||||
|
||||
def test_get_items_6():
|
||||
assert get_item(dict_test, items_test_not_found2) == None
|
||||
|
||||
def test_get_items_7():
|
||||
assert get_item(dict_test, items_test_list2) == None
|
||||
|
||||
def test_get_items_8():
|
||||
assert get_item(dict_test, items_test_list_nest) == 'value5-1-1'
|
||||
|
||||
def test_get_items_9():
|
||||
assert get_item(dict_test, items_test_list_nest_not_found1) == None
|
||||
|
||||
def test_get_items_10():
|
||||
assert get_item(dict_test, items_test_list3) == []
|
||||
@@ -67,6 +67,5 @@ def test_process_2():
|
||||
'chatdata': load_chatdata(r"tests/testdata/calculator/replay_end.json")
|
||||
}
|
||||
assert False
|
||||
SuperchatCalculator().process([chat_component])
|
||||
except ChatParseException:
|
||||
assert True
|
||||
|
||||
24
tests/test_compatible_processor2.py
Normal file
24
tests/test_compatible_processor2.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytchat
|
||||
from pytchat.processors.compatible.processor import CompatibleProcessor
|
||||
|
||||
|
||||
root_keys = ('kind', 'etag', 'nextPageToken', 'pollingIntervalMillis', 'pageInfo', 'items')
|
||||
item_keys = ('kind', 'etag', 'id', 'snippet', 'authorDetails')
|
||||
snippet_keys = ('type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage', 'textMessageDetails')
|
||||
author_details_keys = ('channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator')
|
||||
|
||||
def test_compatible_processor():
|
||||
stream = pytchat.create("Hj-wnLIYKjw", seektime = 6000, processor=CompatibleProcessor())
|
||||
while stream.is_alive():
|
||||
chat = stream.get()
|
||||
for key in chat.keys():
|
||||
assert key in root_keys
|
||||
for key in chat["items"][0].keys():
|
||||
assert key in item_keys
|
||||
for key in chat["items"][0]["snippet"].keys():
|
||||
assert key in snippet_keys
|
||||
for key in chat["items"][0]["authorDetails"].keys():
|
||||
assert key in author_details_keys
|
||||
break
|
||||
|
||||
|
||||
94
tests/test_default_processor2.py
Normal file
94
tests/test_default_processor2.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import asyncio
|
||||
import pytchat
|
||||
from concurrent.futures import CancelledError
|
||||
from pytchat.core_multithread.livechat import LiveChat
|
||||
from pytchat.core_async.livechat import LiveChatAsync
|
||||
|
||||
cases = [
|
||||
{
|
||||
"video_id":"1X7oL0hDnMg", "seektime":1620,
|
||||
"result1":{'textMessage': 84},
|
||||
"result2":{'': 83, 'MODERATOR': 1}
|
||||
},
|
||||
{
|
||||
"video_id":"Hj-wnLIYKjw", "seektime":420,
|
||||
"result1":{'superChat': 1, 'newSponsor': 6, 'textMessage': 63, 'donation': 1},
|
||||
"result2":{'': 59, 'MEMBER': 12}
|
||||
},
|
||||
{
|
||||
"video_id":"S8dmq5YIUoc", "seektime":3,
|
||||
"result1":{'textMessage': 86},
|
||||
"result2":{'': 62, 'MEMBER': 21, 'OWNER': 2, 'VERIFIED': 1}
|
||||
},{
|
||||
"video_id":"yLrstz80MKs", "seektime":30,
|
||||
"result1":{'superSticker': 8, 'superChat': 2, 'textMessage': 67},
|
||||
"result2":{'': 73, 'MEMBER': 4}
|
||||
}
|
||||
]
|
||||
|
||||
def test_archived_stream():
|
||||
for case in cases:
|
||||
stream = pytchat.create(video_id=case["video_id"], seektime=case["seektime"])
|
||||
while stream.is_alive():
|
||||
chat = stream.get()
|
||||
agg1 = {}
|
||||
agg2 = {}
|
||||
for c in chat.items:
|
||||
if c.type in agg1:
|
||||
agg1[c.type] += 1
|
||||
else:
|
||||
agg1[c.type] = 1
|
||||
if c.author.type in agg2:
|
||||
agg2[c.author.type] += 1
|
||||
else:
|
||||
agg2[c.author.type] = 1
|
||||
break
|
||||
assert agg1 == case["result1"]
|
||||
assert agg2 == case["result2"]
|
||||
|
||||
|
||||
def test_archived_stream_multithread():
|
||||
for case in cases:
|
||||
stream = LiveChat(video_id=case["video_id"], seektime=case["seektime"])
|
||||
while stream.is_alive():
|
||||
chat = stream.get()
|
||||
agg1 = {}
|
||||
agg2 = {}
|
||||
for c in chat.items:
|
||||
if c.type in agg1:
|
||||
agg1[c.type] += 1
|
||||
else:
|
||||
agg1[c.type] = 1
|
||||
if c.author.type in agg2:
|
||||
agg2[c.author.type] += 1
|
||||
else:
|
||||
agg2[c.author.type] = 1
|
||||
break
|
||||
assert agg1 == case["result1"]
|
||||
assert agg2 == case["result2"]
|
||||
|
||||
def test_async_live_stream():
|
||||
async def test_loop():
|
||||
for case in cases:
|
||||
stream = LiveChatAsync(video_id=case["video_id"], seektime=case["seektime"])
|
||||
while stream.is_alive():
|
||||
chat = await stream.get()
|
||||
agg1 = {}
|
||||
agg2 = {}
|
||||
for c in chat.items:
|
||||
if c.type in agg1:
|
||||
agg1[c.type] += 1
|
||||
else:
|
||||
agg1[c.type] = 1
|
||||
if c.author.type in agg2:
|
||||
agg2[c.author.type] += 1
|
||||
else:
|
||||
agg2[c.author.type] = 1
|
||||
break
|
||||
assert agg1 == case["result1"]
|
||||
assert agg2 == case["result2"]
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop())
|
||||
except CancelledError:
|
||||
assert True
|
||||
@@ -1,48 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pytest_httpx import HTTPXMock
|
||||
from concurrent.futures import CancelledError
|
||||
from pytchat.core_multithread.livechat import LiveChat
|
||||
from pytchat.core_async.livechat import LiveChatAsync
|
||||
from pytchat.exceptions import ResponseContextError
|
||||
|
||||
|
||||
def _open_file(path):
|
||||
with open(path, mode='r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def add_response_file(httpx_mock: HTTPXMock, jsonfile_path: str):
|
||||
testdata = json.loads(_open_file(jsonfile_path))
|
||||
httpx_mock.add_response(json=testdata)
|
||||
|
||||
|
||||
def test_async(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/paramgen_firstread.json')
|
||||
|
||||
async def test_loop():
|
||||
try:
|
||||
chat = LiveChatAsync(video_id='__test_id__')
|
||||
_ = await chat.get()
|
||||
assert chat.is_alive()
|
||||
chat.terminate()
|
||||
assert not chat.is_alive()
|
||||
except ResponseContextError:
|
||||
assert False
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop())
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
def test_multithread(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/paramgen_firstread.json')
|
||||
try:
|
||||
chat = LiveChat(video_id='__test_id__')
|
||||
_ = chat.get()
|
||||
assert chat.is_alive()
|
||||
chat.terminate()
|
||||
assert not chat.is_alive()
|
||||
except ResponseContextError:
|
||||
assert False
|
||||
@@ -1,113 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
from pytest_httpx import HTTPXMock
|
||||
from concurrent.futures import CancelledError
|
||||
from pytchat.core_multithread.livechat import LiveChat
|
||||
from pytchat.core_async.livechat import LiveChatAsync
|
||||
from pytchat.processors.dummy_processor import DummyProcessor
|
||||
|
||||
|
||||
def _open_file(path):
|
||||
with open(path, mode='r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def add_response_file(httpx_mock: HTTPXMock, jsonfile_path: str):
|
||||
testdata = json.loads(_open_file(jsonfile_path))
|
||||
httpx_mock.add_response(json=testdata)
|
||||
|
||||
|
||||
def test_async_live_stream(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
||||
|
||||
async def test_loop():
|
||||
chat = LiveChatAsync(video_id='__test_id__', processor=DummyProcessor())
|
||||
chats = await chat.get()
|
||||
rawdata = chats[0]["chatdata"]
|
||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatTextMessageRenderer"
|
||||
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatTextMessageRenderer"
|
||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPlaceholderItemRenderer"
|
||||
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[
|
||||
0] == "liveChatTickerPaidMessageItemRenderer"
|
||||
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidMessageRenderer"
|
||||
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidStickerRenderer"
|
||||
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[
|
||||
0] == "liveChatTickerSponsorItemRenderer"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop())
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
def test_async_replay_stream(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/finished_live.json')
|
||||
add_response_file(httpx_mock, 'tests/testdata/chatreplay.json')
|
||||
|
||||
async def test_loop():
|
||||
chat = LiveChatAsync(video_id='__test_id__', processor=DummyProcessor())
|
||||
chats = await chat.get()
|
||||
rawdata = chats[0]["chatdata"]
|
||||
# assert fetching replaychat data
|
||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatTextMessageRenderer"
|
||||
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidMessageRenderer"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop())
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
def test_async_force_replay(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
||||
add_response_file(httpx_mock, 'tests/testdata/chatreplay.json')
|
||||
|
||||
async def test_loop():
|
||||
chat = LiveChatAsync(
|
||||
video_id='__test_id__', processor=DummyProcessor(), force_replay=True)
|
||||
chats = await chat.get()
|
||||
rawdata = chats[0]["chatdata"]
|
||||
# assert fetching replaychat data
|
||||
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidMessageRenderer"
|
||||
# assert not mix livechat data
|
||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
||||
0] != "liveChatPlaceholderItemRenderer"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(test_loop())
|
||||
except CancelledError:
|
||||
assert True
|
||||
|
||||
|
||||
def test_multithread_live_stream(httpx_mock: HTTPXMock):
|
||||
add_response_file(httpx_mock, 'tests/testdata/test_stream.json')
|
||||
chat = LiveChat(video_id='__test_id__', processor=DummyProcessor())
|
||||
chats = chat.get()
|
||||
rawdata = chats[0]["chatdata"]
|
||||
# assert fetching livachat data
|
||||
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatTextMessageRenderer"
|
||||
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatTextMessageRenderer"
|
||||
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPlaceholderItemRenderer"
|
||||
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[
|
||||
0] == "liveChatTickerPaidMessageItemRenderer"
|
||||
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidMessageRenderer"
|
||||
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[
|
||||
0] == "liveChatPaidStickerRenderer"
|
||||
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[
|
||||
0] == "liveChatTickerSponsorItemRenderer"
|
||||
chat.terminate()
|
||||
@@ -1,9 +0,0 @@
|
||||
import pytest
|
||||
from pytchat.paramgen import liveparam
|
||||
|
||||
def test_liveparam_0(mocker):
|
||||
_ts1= 1546268400
|
||||
param = liveparam._build("01234567890",
|
||||
*([_ts1*1000000 for i in range(5)]), topchat_only=False)
|
||||
test_param="0ofMyAN1GhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09KIC41tWqyt8CMAA4AEABShsIABAAGAAgADoAQABKAFCAuNbVqsrfAlgDeABQgLjW1arK3wJYgLjW1arK3wJoAYIBAggBiAEAmgECCACgAYC41tWqyt8C"
|
||||
assert test_param == param
|
||||
16
tests/test_speed_calculator2.py
Normal file
16
tests/test_speed_calculator2.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import json
|
||||
import pytchat
|
||||
from pytchat.parser.live import Parser
|
||||
from pytchat.processors.speed.calculator import SpeedCalculator
|
||||
|
||||
parser = Parser(is_replay=False)
|
||||
|
||||
|
||||
def test_speed_1():
|
||||
stream = pytchat.create("Hj-wnLIYKjw", seektime = 6000,processor=SpeedCalculator())
|
||||
while stream.is_alive():
|
||||
speed = stream.get()
|
||||
assert speed > 100
|
||||
break
|
||||
test_speed_1()
|
||||
|
||||
Reference in New Issue
Block a user