mirror of
https://github.com/lilydjwg/nvchecker.git
synced 2025-03-10 06:14:02 +00:00
parent
254a229401
commit
4f3a900505
13 changed files with 163 additions and 77 deletions
|
@ -52,8 +52,8 @@ If you want to send an HTTP request, it's preferred to use :meth:
|
||||||
`nvchecker.api.session` object. It will use the auto-selected HTTP backend and
|
`nvchecker.api.session` object. It will use the auto-selected HTTP backend and
|
||||||
handle the ``proxy`` option automatically.
|
handle the ``proxy`` option automatically.
|
||||||
|
|
||||||
For details about these objects, see :mod:`the API documentation <nvchecker.api>
|
For details about these objects, see :mod:`the API documentation <nvchecker.api>`,
|
||||||
`, or take existing source plugins as examples.
|
or take existing source plugins as examples.
|
||||||
|
|
||||||
How to write a more powerful plugin
|
How to write a more powerful plugin
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
|
@ -134,6 +134,9 @@ proxy
|
||||||
max_concurrency
|
max_concurrency
|
||||||
Max number of concurrent jobs. Default: 20.
|
Max number of concurrent jobs. Default: 20.
|
||||||
|
|
||||||
|
http_timeout
|
||||||
|
Time in seconds to wait for HTTP requests. Default: 20.
|
||||||
|
|
||||||
keyfile
|
keyfile
|
||||||
Specify an ini config file containing key (token) information. This file
|
Specify an ini config file containing key (token) information. This file
|
||||||
should contain a ``keys`` table, mapping key names to key values. See
|
should contain a ``keys`` table, mapping key names to key values. See
|
||||||
|
|
|
@ -46,8 +46,13 @@ def main() -> None:
|
||||||
|
|
||||||
task_sem = asyncio.Semaphore(options.max_concurrency)
|
task_sem = asyncio.Semaphore(options.max_concurrency)
|
||||||
result_q: asyncio.Queue[RawResult] = asyncio.Queue()
|
result_q: asyncio.Queue[RawResult] = asyncio.Queue()
|
||||||
|
dispatcher = core.setup_httpclient(
|
||||||
|
options.max_concurrency,
|
||||||
|
options.httplib,
|
||||||
|
options.http_timeout,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
futures = core.dispatch(
|
futures = dispatcher.dispatch(
|
||||||
entries, task_sem, result_q,
|
entries, task_sem, result_q,
|
||||||
keymanager, args.tries,
|
keymanager, args.tries,
|
||||||
options.source_configs,
|
options.source_configs,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# MIT licensed
|
# MIT licensed
|
||||||
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
# Copyright (c) 2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
from .httpclient import session, TemporaryError # type: ignore
|
from .httpclient import session, TemporaryError
|
||||||
from .util import (
|
from .util import (
|
||||||
Entry, BaseWorker, RawResult, VersionResult,
|
Entry, BaseWorker, RawResult, VersionResult,
|
||||||
AsyncCache, KeyManager, GetVersionError,
|
AsyncCache, KeyManager, GetVersionError,
|
||||||
|
|
|
@ -33,6 +33,7 @@ from .util import (
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .sortversion import sort_version_keys
|
from .sortversion import sort_version_keys
|
||||||
from .ctxvars import tries as ctx_tries
|
from .ctxvars import tries as ctx_tries
|
||||||
|
from . import httpclient
|
||||||
|
|
||||||
logger = structlog.get_logger(logger_name=__name__)
|
logger = structlog.get_logger(logger_name=__name__)
|
||||||
|
|
||||||
|
@ -140,6 +141,8 @@ class Options(NamedTuple):
|
||||||
proxy: Optional[str]
|
proxy: Optional[str]
|
||||||
keymanager: KeyManager
|
keymanager: KeyManager
|
||||||
source_configs: Dict[str, Dict[str, Any]]
|
source_configs: Dict[str, Dict[str, Any]]
|
||||||
|
httplib: Optional[str]
|
||||||
|
http_timeout: int
|
||||||
|
|
||||||
class FileLoadError(Exception):
|
class FileLoadError(Exception):
|
||||||
def __init__(self, kind, exc):
|
def __init__(self, kind, exc):
|
||||||
|
@ -191,59 +194,75 @@ def load_file(
|
||||||
|
|
||||||
max_concurrency = c.get('max_concurrency', 20)
|
max_concurrency = c.get('max_concurrency', 20)
|
||||||
proxy = c.get('proxy')
|
proxy = c.get('proxy')
|
||||||
|
httplib = c.get('httplib', None)
|
||||||
|
http_timeout = c.get('http_timeout', 20)
|
||||||
else:
|
else:
|
||||||
max_concurrency = 20
|
max_concurrency = 20
|
||||||
proxy = None
|
proxy = None
|
||||||
|
httplib = None
|
||||||
|
http_timeout = 20
|
||||||
|
|
||||||
return cast(Entries, config), Options(
|
return cast(Entries, config), Options(
|
||||||
ver_files, max_concurrency, proxy, keymanager,
|
ver_files, max_concurrency, proxy, keymanager,
|
||||||
source_configs,
|
source_configs, httplib, http_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def dispatch(
|
def setup_httpclient(
|
||||||
entries: Entries,
|
max_concurrency: int = 20,
|
||||||
task_sem: asyncio.Semaphore,
|
httplib: Optional[str] = None,
|
||||||
result_q: Queue[RawResult],
|
http_timeout: int = 20,
|
||||||
keymanager: KeyManager,
|
) -> Dispatcher:
|
||||||
tries: int,
|
httplib_ = httplib or httpclient.find_best_httplib()
|
||||||
source_configs: Dict[str, Dict[str, Any]],
|
httpclient.setup(
|
||||||
) -> List[asyncio.Future]:
|
httplib_, max_concurrency, http_timeout)
|
||||||
mods: Dict[str, Tuple[types.ModuleType, List]] = {}
|
return Dispatcher()
|
||||||
ctx_tries.set(tries)
|
|
||||||
root_ctx = contextvars.copy_context()
|
|
||||||
|
|
||||||
for name, entry in entries.items():
|
class Dispatcher:
|
||||||
source = entry.get('source', 'none')
|
def dispatch(
|
||||||
if source not in mods:
|
self,
|
||||||
mod = import_module('nvchecker_source.' + source)
|
entries: Entries,
|
||||||
tasks: List[Tuple[str, Entry]] = []
|
task_sem: asyncio.Semaphore,
|
||||||
mods[source] = mod, tasks
|
result_q: Queue[RawResult],
|
||||||
config = source_configs.get(source)
|
keymanager: KeyManager,
|
||||||
if config and getattr(mod, 'configure'):
|
tries: int,
|
||||||
mod.configure(config) # type: ignore
|
source_configs: Dict[str, Dict[str, Any]],
|
||||||
else:
|
) -> List[asyncio.Future]:
|
||||||
tasks = mods[source][1]
|
mods: Dict[str, Tuple[types.ModuleType, List]] = {}
|
||||||
tasks.append((name, entry))
|
ctx_tries.set(tries)
|
||||||
|
root_ctx = contextvars.copy_context()
|
||||||
|
|
||||||
ret = []
|
for name, entry in entries.items():
|
||||||
for mod, tasks in mods.values():
|
source = entry.get('source', 'none')
|
||||||
if hasattr(mod, 'Worker'):
|
if source not in mods:
|
||||||
worker_cls = mod.Worker # type: ignore
|
mod = import_module('nvchecker_source.' + source)
|
||||||
else:
|
tasks: List[Tuple[str, Entry]] = []
|
||||||
worker_cls = FunctionWorker
|
mods[source] = mod, tasks
|
||||||
|
config = source_configs.get(source)
|
||||||
|
if config and getattr(mod, 'configure'):
|
||||||
|
mod.configure(config) # type: ignore
|
||||||
|
else:
|
||||||
|
tasks = mods[source][1]
|
||||||
|
tasks.append((name, entry))
|
||||||
|
|
||||||
ctx = root_ctx.copy()
|
ret = []
|
||||||
worker = ctx.run(
|
for mod, tasks in mods.values():
|
||||||
worker_cls,
|
if hasattr(mod, 'Worker'):
|
||||||
task_sem, result_q, tasks, keymanager,
|
worker_cls = mod.Worker # type: ignore
|
||||||
)
|
else:
|
||||||
if worker_cls is FunctionWorker:
|
worker_cls = FunctionWorker
|
||||||
func = mod.get_version # type: ignore
|
|
||||||
ctx.run(worker.initialize, func)
|
|
||||||
|
|
||||||
ret.append(ctx.run(worker.run))
|
ctx = root_ctx.copy()
|
||||||
|
worker = ctx.run(
|
||||||
|
worker_cls,
|
||||||
|
task_sem, result_q, tasks, keymanager,
|
||||||
|
)
|
||||||
|
if worker_cls is FunctionWorker:
|
||||||
|
func = mod.get_version # type: ignore
|
||||||
|
ctx.run(worker.initialize, func)
|
||||||
|
|
||||||
return ret
|
ret.append(ctx.run(worker.run))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def substitute_version(
|
def substitute_version(
|
||||||
version: str, conf: Entry,
|
version: str, conf: Entry,
|
||||||
|
|
|
@ -1,27 +1,55 @@
|
||||||
# MIT licensed
|
# MIT licensed
|
||||||
# Copyright (c) 2013-2017 lilydjwg <lilydjwg@gmail.com>, et al.
|
# Copyright (c) 2013-2020 lilydjwg <lilydjwg@gmail.com>, et al.
|
||||||
|
|
||||||
try:
|
from typing import Optional
|
||||||
import tornado, pycurl
|
|
||||||
# connection reuse, http/2
|
|
||||||
which = 'tornado'
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import aiohttp
|
|
||||||
which = 'aiohttp'
|
|
||||||
# connection reuse
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import httpx
|
|
||||||
which = 'httpx'
|
|
||||||
except ImportError:
|
|
||||||
import tornado
|
|
||||||
which = 'tornado'
|
|
||||||
# fallback
|
|
||||||
|
|
||||||
m = __import__('%s_httpclient' % which, globals(), locals(), level=1)
|
|
||||||
__all__ = m.__all__
|
|
||||||
for x in __all__:
|
|
||||||
globals()[x] = getattr(m, x)
|
|
||||||
|
|
||||||
from .base import TemporaryError
|
from .base import TemporaryError
|
||||||
|
|
||||||
|
class Proxy:
|
||||||
|
_obj = None
|
||||||
|
|
||||||
|
def set_obj(self, obj):
|
||||||
|
super().__setattr__('_obj', obj)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._obj, name)
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
return setattr(self._obj, name, value)
|
||||||
|
|
||||||
|
session = Proxy()
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
which: Optional[str] = None,
|
||||||
|
concurreny: int = 20,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> None:
|
||||||
|
if which is None:
|
||||||
|
which = find_best_httplib()
|
||||||
|
|
||||||
|
m = __import__(
|
||||||
|
'%s_httpclient' % which, globals(), locals(), level=1)
|
||||||
|
|
||||||
|
session.set_obj(m.session)
|
||||||
|
session.setup(concurreny, timeout)
|
||||||
|
|
||||||
|
def find_best_httplib() -> str:
|
||||||
|
try:
|
||||||
|
import tornado, pycurl
|
||||||
|
# connection reuse, http/2
|
||||||
|
which = 'tornado'
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import aiohttp
|
||||||
|
which = 'aiohttp'
|
||||||
|
# connection reuse
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
which = 'httpx'
|
||||||
|
except ImportError:
|
||||||
|
import tornado
|
||||||
|
which = 'tornado'
|
||||||
|
# fallback
|
||||||
|
|
||||||
|
return which
|
||||||
|
|
|
@ -16,10 +16,14 @@ logger = structlog.get_logger(logger_name=__name__)
|
||||||
connector = aiohttp.TCPConnector(limit=20)
|
connector = aiohttp.TCPConnector(limit=20)
|
||||||
|
|
||||||
class AiohttpSession(BaseSession):
|
class AiohttpSession(BaseSession):
|
||||||
def __init__(self):
|
def setup(
|
||||||
|
self,
|
||||||
|
concurreny: int = 20,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> None:
|
||||||
self.session = aiohttp.ClientSession(
|
self.session = aiohttp.ClientSession(
|
||||||
connector = aiohttp.TCPConnector(limit=20),
|
connector = aiohttp.TCPConnector(limit=concurreny),
|
||||||
timeout = aiohttp.ClientTimeout(total=20),
|
timeout = aiohttp.ClientTimeout(total=timeout),
|
||||||
trust_env = True,
|
trust_env = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,13 @@ class Response:
|
||||||
|
|
||||||
class BaseSession:
|
class BaseSession:
|
||||||
'''The base class for different HTTP backend.'''
|
'''The base class for different HTTP backend.'''
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
concurreny: int = 20,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
async def get(self, *args, **kwargs):
|
async def get(self, *args, **kwargs):
|
||||||
'''Shortcut for ``GET`` request.'''
|
'''Shortcut for ``GET`` request.'''
|
||||||
return await self.request(
|
return await self.request(
|
||||||
|
|
|
@ -11,8 +11,13 @@ from .base import BaseSession, TemporaryError, Response
|
||||||
__all__ = ['session']
|
__all__ = ['session']
|
||||||
|
|
||||||
class HttpxSession(BaseSession):
|
class HttpxSession(BaseSession):
|
||||||
def __init__(self):
|
def setup(
|
||||||
self.clients = {}
|
self,
|
||||||
|
concurreny: int = 20,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> None:
|
||||||
|
self.clients: Dict[Optional[str], httpx.AsyncClient] = {}
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
async def request_impl(
|
async def request_impl(
|
||||||
self, url: str, *,
|
self, url: str, *,
|
||||||
|
@ -25,7 +30,7 @@ class HttpxSession(BaseSession):
|
||||||
client = self.clients.get(proxy)
|
client = self.clients.get(proxy)
|
||||||
if not client:
|
if not client:
|
||||||
client = httpx.AsyncClient(
|
client = httpx.AsyncClient(
|
||||||
timeout = httpx.Timeout(20, pool=None),
|
timeout = httpx.Timeout(self.timeout, pool=None),
|
||||||
http2 = True,
|
http2 = True,
|
||||||
proxies = {'all://': proxy},
|
proxies = {'all://': proxy},
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,6 @@ from tornado.httpclient import AsyncHTTPClient, HTTPRequest
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pycurl
|
import pycurl
|
||||||
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient", max_clients=20)
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pycurl = None # type: ignore
|
pycurl = None # type: ignore
|
||||||
|
|
||||||
|
@ -31,6 +30,20 @@ def try_use_http2(curl):
|
||||||
curl.setopt(pycurl.HTTP_VERSION, 4)
|
curl.setopt(pycurl.HTTP_VERSION, 4)
|
||||||
|
|
||||||
class TornadoSession(BaseSession):
|
class TornadoSession(BaseSession):
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
concurreny: int = 20,
|
||||||
|
timeout: int = 20,
|
||||||
|
) -> None:
|
||||||
|
impl: Optional[str]
|
||||||
|
if pycurl:
|
||||||
|
impl = "tornado.curl_httpclient.CurlAsyncHTTPClient"
|
||||||
|
else:
|
||||||
|
impl = None
|
||||||
|
AsyncHTTPClient.configure(
|
||||||
|
impl, max_clients = concurreny)
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
async def request_impl(
|
async def request_impl(
|
||||||
self, url: str, *,
|
self, url: str, *,
|
||||||
method: str,
|
method: str,
|
||||||
|
@ -42,6 +55,7 @@ class TornadoSession(BaseSession):
|
||||||
kwargs: Dict[str, Any] = {
|
kwargs: Dict[str, Any] = {
|
||||||
'method': method,
|
'method': method,
|
||||||
'headers': headers,
|
'headers': headers,
|
||||||
|
'request_timeout': self.timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
if json:
|
if json:
|
||||||
|
|
|
@ -10,7 +10,7 @@ import sys
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from .httpclient.base import TemporaryError
|
from .httpclient import TemporaryError
|
||||||
|
|
||||||
def _console_msg(event):
|
def _console_msg(event):
|
||||||
evt = event['event']
|
evt = event['event']
|
||||||
|
|
|
@ -17,7 +17,7 @@ import abc
|
||||||
import toml
|
import toml
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from .httpclient import session # type: ignore
|
from .httpclient import session
|
||||||
from .ctxvars import tries as ctx_tries
|
from .ctxvars import tries as ctx_tries
|
||||||
from .ctxvars import proxy as ctx_proxy
|
from .ctxvars import proxy as ctx_proxy
|
||||||
from .ctxvars import user_agent as ctx_ua
|
from .ctxvars import user_agent as ctx_ua
|
||||||
|
|
|
@ -27,7 +27,8 @@ async def run(
|
||||||
else:
|
else:
|
||||||
keymanager = core.KeyManager(None)
|
keymanager = core.KeyManager(None)
|
||||||
|
|
||||||
futures = core.dispatch(
|
dispatcher = core.setup_httpclient()
|
||||||
|
futures = dispatcher.dispatch(
|
||||||
entries, task_sem, result_q,
|
entries, task_sem, result_q,
|
||||||
keymanager, 1, {},
|
keymanager, 1, {},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue