-
Notifications
You must be signed in to change notification settings - Fork 0
/
nntp.py
122 lines (111 loc) · 3.92 KB
/
nntp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import net
from nntplib import NNTP, NNTPError
from contextlib import ExitStack, contextmanager, suppress
import time
from misc import Context
"""
TODO:
when cancelling body (e.g. file found to be complete), determine whether to
abort connection (if a lot to dl), or to do a dummy transfer (small amount
to dl)
option to reconnect after given time, to avoid server inconvenient
disconnection
"""
# Part of API
from nntplib import NNTPTemporaryError, NNTPPermanentError
failure_responses = (NNTPTemporaryError, NNTPPermanentError)
class Client(Context):
def __init__(self, log,
hostname, port=None, username=None, password=None, *,
debuglevel=None, **timeout):
self.log = log
self.hostname = hostname
self.port = port
self.username = username
self.password = password
self.debuglevel = debuglevel
self.timeout = timeout
Context.__init__(self)
self.connect()
def connect(self):
address = net.format_addr((self.hostname, self.port))
self.log.write("Connecting to {}\n".format(address))
if self.port is None:
port = ()
else:
port = (self.port,)
self.connect_time = time.monotonic()
self.nntp = NNTP(self.hostname, *port, **self.timeout)
with ExitStack() as cleanup:
cleanup.push(self)
if self.debuglevel is not None:
self.nntp.set_debuglevel(self.debuglevel)
self.log.write("{}\n".format(self.nntp.getwelcome()))
if self.username is not None:
self.log.write("Logging in as {}\n".format(self.username))
with self.handle_abort():
self.nntp.login(self.username, self.password)
self.log.write("Logged in\n")
cleanup.pop_all()
def body(self, id, *pos, **kw):
id = "<{}>".format(id)
retry = 0
while True:
try:
with self.handle_abort():
self.nntp.body(id, *pos, **kw)
break
except failure_responses as err:
[code, *msg] = err.response.split(maxsplit=1)
if code == "400":
[msg] = msg or (None,)
if not msg:
msg = "Server shut down connection"
elif code[1] == "0" and not retry:
msg = err.response
else:
raise
self.log.write(msg + "\n")
self.log_time()
if retry >= 60:
raise TimeoutError()
self.close()
time.sleep(retry)
if not retry:
start = time.monotonic()
self.connect()
if retry:
retry *= 2
else:
retry = time.monotonic() - start
if retry <= 0:
retry = 0.5
def group(self, *pos, **kw):
with self.handle_abort():
return self.nntp.group(*pos, **kw)
def over(self, *pos, **kw):
with self.handle_abort():
return self.nntp.over(*pos, **kw)
def hdr(self, *pos, **kw):
with self.handle_abort():
return self.nntp.xhdr(*pos, **kw)
@contextmanager
def handle_abort(self):
try:
yield
except failure_responses:
raise # NNTP connection still intact
except:
# Protocol is disrupted so abort the connection straight away
self.close()
raise
def close(self):
if not self.nntp:
return
# Ignore failure of inappropriate QUIT command
with suppress(NNTPError), self.nntp:
pass
self.nntp = None
def log_time(self):
# TODO: time duration formatter
self.log.write("Connection lasted {:.0f}m\n".format((time.monotonic() - self.connect_time)/60))