k3http 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: k3http
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: HTTP/1.1 client with timeout support
5
5
  Author-email: Zhang Yanpo <drdr.xp@gmail.com>
6
6
  License: MIT
@@ -38,7 +38,6 @@ Dynamic: license-file
38
38
  # k3http
39
39
 
40
40
  [![Action-CI](https://github.com/pykit3/k3http/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3http/actions/workflows/python-package.yml)
41
- [![Build Status](https://travis-ci.com/pykit3/k3http.svg?branch=master)](https://travis-ci.com/pykit3/k3http)
42
41
  [![Documentation Status](https://readthedocs.org/projects/k3http/badge/?version=stable)](https://k3http.readthedocs.io/en/stable/?badge=stable)
43
42
  [![Package](https://img.shields.io/pypi/pyversions/k3http)](https://pypi.org/project/k3http)
44
43
 
@@ -62,23 +61,22 @@ pip install k3http
62
61
  # Synopsis
63
62
 
64
63
  ```python
64
+ import urllib
65
65
 
66
66
  import k3http
67
- import urllib
68
- import socket
69
67
 
70
68
  headers = {
71
- 'Host': '127.0.0.1',
72
- 'Accept-Language': 'en, mi',
69
+ "Host": "127.0.0.1",
70
+ "Accept-Language": "en, mi",
73
71
  }
74
72
 
75
73
  try:
76
- h = k3http.Client('127.0.0.1', 80)
74
+ h = k3http.Client("127.0.0.1", 80)
77
75
 
78
76
  # send http reqeust without body
79
77
  # read response status line
80
78
  # read response headers
81
- h.request('/test.txt', method='GET', headers=headers)
79
+ h.request("/test.txt", method="GET", headers=headers)
82
80
 
83
81
  status = h.status
84
82
  # response code return from http server, type is int
@@ -97,23 +95,22 @@ try:
97
95
 
98
96
  # get response body
99
97
  print(h.read_body(None))
100
- except (socket.error, k3http.HttpError) as e:
98
+ except (OSError, k3http.HttpError) as e:
101
99
  print(repr(e))
102
100
 
103
101
 
104
-
105
- content = urllib.urlencode({'f': 'foo', 'b': 'bar'})
102
+ content = urllib.urlencode({"f": "foo", "b": "bar"})
106
103
  headers = {
107
- 'Host': 'www.example.com',
108
- 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
109
- 'Content-Length': len(content),
104
+ "Host": "www.example.com",
105
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
106
+ "Content-Length": len(content),
110
107
  }
111
108
 
112
109
  try:
113
- h = k3http.Client('127.0.0.1', 80)
110
+ h = k3http.Client("127.0.0.1", 80)
114
111
 
115
112
  # send http reqeust
116
- h.send_request('http://www.example.com', method='POST', headers=headers)
113
+ h.send_request("http://www.example.com", method="POST", headers=headers)
117
114
 
118
115
  # send http request body
119
116
  h.send_body(content)
@@ -123,9 +120,8 @@ try:
123
120
 
124
121
  # read response body
125
122
  print(h.read_body(None))
126
- except (socket.error, k3http.HttpError) as e:
123
+ except (OSError, k3http.HttpError) as e:
127
124
  print(repr(e))
128
-
129
125
  ```
130
126
 
131
127
  # Author
@@ -1,7 +1,6 @@
1
1
  # k3http
2
2
 
3
3
  [![Action-CI](https://github.com/pykit3/k3http/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3http/actions/workflows/python-package.yml)
4
- [![Build Status](https://travis-ci.com/pykit3/k3http.svg?branch=master)](https://travis-ci.com/pykit3/k3http)
5
4
  [![Documentation Status](https://readthedocs.org/projects/k3http/badge/?version=stable)](https://k3http.readthedocs.io/en/stable/?badge=stable)
6
5
  [![Package](https://img.shields.io/pypi/pyversions/k3http)](https://pypi.org/project/k3http)
7
6
 
@@ -25,23 +24,22 @@ pip install k3http
25
24
  # Synopsis
26
25
 
27
26
  ```python
27
+ import urllib
28
28
 
29
29
  import k3http
30
- import urllib
31
- import socket
32
30
 
33
31
  headers = {
34
- 'Host': '127.0.0.1',
35
- 'Accept-Language': 'en, mi',
32
+ "Host": "127.0.0.1",
33
+ "Accept-Language": "en, mi",
36
34
  }
37
35
 
38
36
  try:
39
- h = k3http.Client('127.0.0.1', 80)
37
+ h = k3http.Client("127.0.0.1", 80)
40
38
 
41
39
  # send http reqeust without body
42
40
  # read response status line
43
41
  # read response headers
44
- h.request('/test.txt', method='GET', headers=headers)
42
+ h.request("/test.txt", method="GET", headers=headers)
45
43
 
46
44
  status = h.status
47
45
  # response code return from http server, type is int
@@ -60,23 +58,22 @@ try:
60
58
 
61
59
  # get response body
62
60
  print(h.read_body(None))
63
- except (socket.error, k3http.HttpError) as e:
61
+ except (OSError, k3http.HttpError) as e:
64
62
  print(repr(e))
65
63
 
66
64
 
67
-
68
- content = urllib.urlencode({'f': 'foo', 'b': 'bar'})
65
+ content = urllib.urlencode({"f": "foo", "b": "bar"})
69
66
  headers = {
70
- 'Host': 'www.example.com',
71
- 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
72
- 'Content-Length': len(content),
67
+ "Host": "www.example.com",
68
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
69
+ "Content-Length": len(content),
73
70
  }
74
71
 
75
72
  try:
76
- h = k3http.Client('127.0.0.1', 80)
73
+ h = k3http.Client("127.0.0.1", 80)
77
74
 
78
75
  # send http reqeust
79
- h.send_request('http://www.example.com', method='POST', headers=headers)
76
+ h.send_request("http://www.example.com", method="POST", headers=headers)
80
77
 
81
78
  # send http request body
82
79
  h.send_body(content)
@@ -86,9 +83,8 @@ try:
86
83
 
87
84
  # read response body
88
85
  print(h.read_body(None))
89
- except (socket.error, k3http.HttpError) as e:
86
+ except (OSError, k3http.HttpError) as e:
90
87
  print(repr(e))
91
-
92
88
  ```
93
89
 
94
90
  # Author
@@ -9,30 +9,29 @@ from importlib.metadata import version
9
9
  __version__ = version("k3http")
10
10
 
11
11
  from .client import (
12
+ BadStatusLineError,
13
+ ChunkedSizeError,
14
+ Client,
15
+ HeadersError,
12
16
  HttpError,
13
17
  LineTooLongError,
14
- ChunkedSizeError,
15
18
  NotConnectedError,
16
19
  ResponseNotReadyError,
17
- HeadersError,
18
- BadStatusLineError,
19
- Client,
20
20
  )
21
-
22
21
  from .util import (
23
22
  headers_add_host,
24
23
  request_add_host,
25
24
  )
26
25
 
27
26
  __all__ = [
27
+ "BadStatusLineError",
28
+ "ChunkedSizeError",
29
+ "Client",
30
+ "HeadersError",
28
31
  "HttpError",
29
32
  "LineTooLongError",
30
- "ChunkedSizeError",
31
33
  "NotConnectedError",
32
34
  "ResponseNotReadyError",
33
- "HeadersError",
34
- "BadStatusLineError",
35
- "Client",
36
35
  "headers_add_host",
37
36
  "request_add_host",
38
37
  ]
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # coding: utf-8
3
2
 
4
3
  import errno
5
4
  import logging
@@ -50,7 +49,7 @@ MAX_LINE_LENGTH = 65536
50
49
  LINE_RECV_LENGTH = 1024 * 4
51
50
 
52
51
 
53
- class Client(object):
52
+ class Client:
54
53
  """
55
54
  HTTP client class
56
55
 
@@ -125,11 +124,11 @@ class Client(object):
125
124
  sw = self.get_and_end_stopwatch()
126
125
  rst = []
127
126
  for t in sw.get_last_trace_report():
128
- ent = dict(
129
- name=t.name,
130
- time=t.end_time - t.start_time,
131
- annotation=[self._human_annotation(an) for an in t.trace_annotations],
132
- )
127
+ ent = {
128
+ "name": t.name,
129
+ "time": t.end_time - t.start_time,
130
+ "annotation": [self._human_annotation(an) for an in t.trace_annotations],
131
+ }
133
132
 
134
133
  rst.append(ent)
135
134
 
@@ -137,7 +136,7 @@ class Client(object):
137
136
 
138
137
  def _human_annotation(self, annotation):
139
138
  an = annotation
140
- return "{key}:{value}".format(key=an.key, value=an.value)
139
+ return f"{an.key}:{an.value}"
141
140
 
142
141
  def get_and_end_stopwatch(self):
143
142
  """stopwatch must be stopped before it can be read"""
@@ -198,7 +197,7 @@ class Client(object):
198
197
  self.sock = self.https_context.wrap_socket(self.sock, server_hostname=self.host)
199
198
 
200
199
  bufs = [
201
- "{method} {uri} HTTP/1.1".format(method=method, uri=uri),
200
+ f"{method} {uri} HTTP/1.1",
202
201
  ]
203
202
 
204
203
  headers = headers or {}
@@ -209,7 +208,7 @@ class Client(object):
209
208
  self.request_chunked_encoded = True
210
209
 
211
210
  for k, v in headers.items():
212
- bufs.append("%s: %s" % (k, v))
211
+ bufs.append(f"{k}: {v}")
213
212
 
214
213
  bufs.extend(["", ""])
215
214
 
@@ -218,20 +217,24 @@ class Client(object):
218
217
 
219
218
  def send_body(self, body):
220
219
  """
221
- Send http request body. It should be a 'str' of data to send after the headers are finished
220
+ Send http request body. It should be a 'bytes' of data to send after the headers are finished.
221
+ A 'str' body is encoded as UTF-8.
222
222
 
223
- :param body: a 'str' of http request body.
223
+ :param body: a 'bytes' or 'str' of http request body.
224
224
  :return: nothing
225
225
  """
226
226
 
227
227
  if self.sock is None:
228
228
  raise NotConnectedError("socket object is None")
229
229
 
230
+ if isinstance(body, str):
231
+ body = body.encode("utf-8")
232
+
230
233
  with self.stopwatch.timer("send_body"):
231
234
  if self.request_chunked_encoded:
232
- body = "{0:x}\r\n{1}\r\n".format(len(body), body)
235
+ body = b"%x\r\n%s\r\n" % (len(body), body)
233
236
 
234
- self.sock.sendall(body.encode("utf-8"))
237
+ self.sock.sendall(body)
235
238
 
236
239
  def read_status(self, skip_100=True):
237
240
  """
@@ -276,7 +279,7 @@ class Client(object):
276
279
 
277
280
  kv = line.strip().split(":", 1)
278
281
  if len(kv) < 2:
279
- raise HeadersError("invalid headers param line:%s" % (line))
282
+ raise HeadersError(f"invalid headers param line:{line}")
280
283
  self.headers[kv[0].lower()] = kv[1].strip()
281
284
 
282
285
  if self.status in (204, 304) or self.method == "HEAD":
@@ -293,7 +296,7 @@ class Client(object):
293
296
  try:
294
297
  self.content_length = int(length)
295
298
  except ValueError as e:
296
- logger.error(repr(e) + " while get content-length length:{l}".format(l=length))
299
+ logger.error(repr(e) + f" while get content-length length:{length}")
297
300
  raise HeadersError("invalid content-length")
298
301
 
299
302
  return self.headers
@@ -320,7 +323,7 @@ class Client(object):
320
323
  Read and return the response body.
321
324
 
322
325
  :param size: need read size, if greater than left size use the min one, if 'None' read left unread body.
323
- :return: the response body.
326
+ :return: the response body as 'bytes'.
324
327
  """
325
328
 
326
329
  with self.stopwatch.timer("recv_body"):
@@ -330,19 +333,19 @@ class Client(object):
330
333
  """
331
334
  Read and yield one line in response body each time.
332
335
 
333
- :param delimiter: specify the delimiter between each line, defalut supporting '\n'.
334
- :return: a generator yileding one line including delimiter in response body each time.
336
+ :param delimiter: specify the 'bytes' delimiter between each line, defalut supporting b'\n'.
337
+ :return: a generator yileding one 'bytes' line including delimiter in response body each time.
335
338
  """
336
339
 
337
340
  if delimiter is None:
338
- delimiter = "\n"
341
+ delimiter = b"\n"
339
342
 
340
- buf = ""
343
+ buf = b""
341
344
  while True:
342
345
  tmp = self._read_body(MAX_LINE_LENGTH)
343
346
 
344
- if tmp == "":
345
- if buf != "":
347
+ if tmp == b"":
348
+ if buf != b"":
346
349
  yield buf
347
350
  break
348
351
 
@@ -356,7 +359,7 @@ class Client(object):
356
359
 
357
360
  def _read_body(self, size):
358
361
  if size is not None and size <= 0:
359
- return ""
362
+ return b""
360
363
 
361
364
  if self.chunked:
362
365
  buf = self._read_chunked(size)
@@ -369,7 +372,7 @@ class Client(object):
369
372
  size = min(size, self.content_length - self.has_read)
370
373
 
371
374
  if size <= 0:
372
- return ""
375
+ return b""
373
376
 
374
377
  buf = self._read(size)
375
378
  self.has_read += size
@@ -380,16 +383,16 @@ class Client(object):
380
383
  if self.recv_iter is not None:
381
384
  try:
382
385
  self.recv_iter.close()
383
- except Exception as e:
384
- logger.exception(repr(e) + " while close recv_iter")
386
+ except Exception:
387
+ logger.exception("while close recv_iter")
385
388
 
386
389
  self.recv_iter = None
387
390
 
388
391
  if self.sock is not None:
389
392
  try:
390
393
  self.sock.close()
391
- except Exception as e:
392
- logger.exception(repr(e) + " while close sock")
394
+ except Exception:
395
+ logger.exception("while close sock")
393
396
 
394
397
  self.sock = None
395
398
 
@@ -407,7 +410,8 @@ class Client(object):
407
410
  return self.recv_iter.send(("block", size))
408
411
 
409
412
  def _readline(self):
410
- return self.recv_iter.send(("line", None))
413
+ line = self.recv_iter.send(("line", None))
414
+ return line.decode("utf-8")
411
415
 
412
416
  def _get_response_status(self):
413
417
  with self.stopwatch.timer("recv_status"):
@@ -415,7 +419,7 @@ class Client(object):
415
419
 
416
420
  vals = line.split(None, 2)
417
421
  if len(vals) < 2:
418
- raise BadStatusLineError("invalid status line:{l}".format(l=line))
422
+ raise BadStatusLineError(f"invalid status line:{line}")
419
423
 
420
424
  ver, status = vals[0], vals[1]
421
425
 
@@ -423,10 +427,10 @@ class Client(object):
423
427
  status = int(status)
424
428
  except ValueError as e:
425
429
  logger.error(repr(e) + " while get response status")
426
- raise BadStatusLineError("status is not int:{l}".format(l=line))
430
+ raise BadStatusLineError(f"status is not int:{line}")
427
431
 
428
432
  if not ver.startswith("HTTP/") or status < 100 or status > 999:
429
- raise BadStatusLineError("invalid status line:{l}".format(l=line))
433
+ raise BadStatusLineError(f"invalid status line:{line}")
430
434
 
431
435
  return status
432
436
 
@@ -441,7 +445,7 @@ class Client(object):
441
445
  try:
442
446
  chunk_size = int(line, 16)
443
447
  except ValueError as e:
444
- logger.error(repr(e) + " while get chunk size line:{l}".format(l=line))
448
+ logger.error(repr(e) + f" while get chunk size line:{line}")
445
449
  raise ChunkedSizeError("invalid chunk size")
446
450
 
447
451
  return chunk_size
@@ -450,7 +454,7 @@ class Client(object):
450
454
  buf = []
451
455
 
452
456
  if self.chunk_left == 0:
453
- return ""
457
+ return b""
454
458
 
455
459
  while size is None or size > 0:
456
460
  if self.chunk_left is None:
@@ -478,7 +482,7 @@ class Client(object):
478
482
  if line == "":
479
483
  break
480
484
 
481
- return "".join(buf)
485
+ return b"".join(buf)
482
486
 
483
487
  def set_timeout(self, timeout):
484
488
  self.timeout = timeout
@@ -488,20 +492,20 @@ class Client(object):
488
492
 
489
493
 
490
494
  def _recv_loop(sock, timeout):
491
- bufs = [""]
495
+ bufs = [b""]
492
496
  mode, size = yield
493
497
 
494
498
  while True:
495
499
  if mode == "line":
496
500
  buf = bufs[0]
497
- if "\r\n" in buf:
498
- rst, buf = buf.split("\r\n", 1)
501
+ if b"\r\n" in buf:
502
+ rst, buf = buf.split(b"\r\n", 1)
499
503
  bufs[0] = buf
500
504
  mode, size = yield rst
501
505
  continue
502
506
  else:
503
507
  if len(buf) >= MAX_LINE_LENGTH:
504
- raise LineTooLongError("line length greater than max_len:{l}".format(l=len(buf)))
508
+ raise LineTooLongError(f"line length greater than max_len:{len(buf)}")
505
509
  else:
506
510
  buf += _recv(sock, timeout, LINE_RECV_LENGTH)
507
511
  bufs[0] = buf
@@ -512,31 +516,31 @@ def _recv_loop(sock, timeout):
512
516
  bufs.append(_recv(sock, timeout, size - total))
513
517
  total += len(bufs[-1])
514
518
 
515
- rst = "".join(bufs)
519
+ rst = b"".join(bufs)
516
520
  if size < len(rst):
517
521
  bufs = [rst[size:]]
518
522
  rst = rst[:size]
519
523
  else:
520
- bufs = [""]
524
+ bufs = [b""]
521
525
  mode, size = yield rst
522
526
 
523
527
 
524
528
  def _recv(sock, timeout, size):
525
- buf = ""
529
+ buf = b""
526
530
  for _ in range(2):
527
531
  try:
528
- buf = (sock.recv(size)).decode("utf-8")
532
+ buf = sock.recv(size)
529
533
  break
530
- except socket.error as e:
534
+ except OSError as e:
531
535
  if len(e.args) <= 0 or e.args[0] != errno.EAGAIN:
532
536
  raise
533
537
 
534
- evin, evout, everr = select.select([sock.fileno()], [], [], timeout)
538
+ evin, _evout, _everr = select.select([sock.fileno()], [], [], timeout)
535
539
 
536
540
  if len(evin) <= 0:
537
- raise socket.timeout("{second}s timeout while recv".format(second=timeout))
541
+ raise socket.timeout(f"{timeout}s timeout while recv")
538
542
 
539
543
  if len(buf) <= 0:
540
- raise socket.error("got empty when recv {l} bytes".format(l=size))
544
+ raise OSError(f"got empty when recv {size} bytes")
541
545
 
542
546
  return buf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: k3http
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: HTTP/1.1 client with timeout support
5
5
  Author-email: Zhang Yanpo <drdr.xp@gmail.com>
6
6
  License: MIT
@@ -38,7 +38,6 @@ Dynamic: license-file
38
38
  # k3http
39
39
 
40
40
  [![Action-CI](https://github.com/pykit3/k3http/actions/workflows/python-package.yml/badge.svg)](https://github.com/pykit3/k3http/actions/workflows/python-package.yml)
41
- [![Build Status](https://travis-ci.com/pykit3/k3http.svg?branch=master)](https://travis-ci.com/pykit3/k3http)
42
41
  [![Documentation Status](https://readthedocs.org/projects/k3http/badge/?version=stable)](https://k3http.readthedocs.io/en/stable/?badge=stable)
43
42
  [![Package](https://img.shields.io/pypi/pyversions/k3http)](https://pypi.org/project/k3http)
44
43
 
@@ -62,23 +61,22 @@ pip install k3http
62
61
  # Synopsis
63
62
 
64
63
  ```python
64
+ import urllib
65
65
 
66
66
  import k3http
67
- import urllib
68
- import socket
69
67
 
70
68
  headers = {
71
- 'Host': '127.0.0.1',
72
- 'Accept-Language': 'en, mi',
69
+ "Host": "127.0.0.1",
70
+ "Accept-Language": "en, mi",
73
71
  }
74
72
 
75
73
  try:
76
- h = k3http.Client('127.0.0.1', 80)
74
+ h = k3http.Client("127.0.0.1", 80)
77
75
 
78
76
  # send http reqeust without body
79
77
  # read response status line
80
78
  # read response headers
81
- h.request('/test.txt', method='GET', headers=headers)
79
+ h.request("/test.txt", method="GET", headers=headers)
82
80
 
83
81
  status = h.status
84
82
  # response code return from http server, type is int
@@ -97,23 +95,22 @@ try:
97
95
 
98
96
  # get response body
99
97
  print(h.read_body(None))
100
- except (socket.error, k3http.HttpError) as e:
98
+ except (OSError, k3http.HttpError) as e:
101
99
  print(repr(e))
102
100
 
103
101
 
104
-
105
- content = urllib.urlencode({'f': 'foo', 'b': 'bar'})
102
+ content = urllib.urlencode({"f": "foo", "b": "bar"})
106
103
  headers = {
107
- 'Host': 'www.example.com',
108
- 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
109
- 'Content-Length': len(content),
104
+ "Host": "www.example.com",
105
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
106
+ "Content-Length": len(content),
110
107
  }
111
108
 
112
109
  try:
113
- h = k3http.Client('127.0.0.1', 80)
110
+ h = k3http.Client("127.0.0.1", 80)
114
111
 
115
112
  # send http reqeust
116
- h.send_request('http://www.example.com', method='POST', headers=headers)
113
+ h.send_request("http://www.example.com", method="POST", headers=headers)
117
114
 
118
115
  # send http request body
119
116
  h.send_body(content)
@@ -123,9 +120,8 @@ try:
123
120
 
124
121
  # read response body
125
122
  print(h.read_body(None))
126
- except (socket.error, k3http.HttpError) as e:
123
+ except (OSError, k3http.HttpError) as e:
127
124
  print(repr(e))
128
-
129
125
  ```
130
126
 
131
127
  # Author
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "k3http"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "HTTP/1.1 client with timeout support"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
- import k3http
2
1
  import urllib
3
- import socket
2
+
3
+ import k3http
4
4
 
5
5
  headers = {
6
6
  "Host": "127.0.0.1",
@@ -32,7 +32,7 @@ try:
32
32
 
33
33
  # get response body
34
34
  print(h.read_body(None))
35
- except (socket.error, k3http.HttpError) as e:
35
+ except (OSError, k3http.HttpError) as e:
36
36
  print(repr(e))
37
37
 
38
38
 
@@ -57,5 +57,5 @@ try:
57
57
 
58
58
  # read response body
59
59
  print(h.read_body(None))
60
- except (socket.error, k3http.HttpError) as e:
60
+ except (OSError, k3http.HttpError) as e:
61
61
  print(repr(e))
@@ -1,6 +1,3 @@
1
- #!/usr/bin/env python3
2
- # coding: utf-8
3
-
4
1
  import gc
5
2
  import os
6
3
  import socket
@@ -8,12 +5,12 @@ import ssl
8
5
  import threading
9
6
  import time
10
7
  import unittest
11
- from http.server import BaseHTTPRequestHandler
12
- from http.server import HTTPServer
8
+ from http.server import BaseHTTPRequestHandler, HTTPServer
9
+ from typing import ClassVar
13
10
 
14
- import k3http
15
11
  import k3ut
16
12
 
13
+ import k3http
17
14
 
18
15
  dd = k3ut.dd
19
16
 
@@ -26,22 +23,34 @@ HOME_PATH = os.path.dirname(os.path.abspath(__file__))
26
23
 
27
24
 
28
25
  class TestHttpClient(unittest.TestCase):
29
- special_cases = {
30
- "test_recving_server_close": (0, 1, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n"),
31
- "test_server_delay_response": (0.5, 1, "HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nabcd"),
32
- "test_raise_chunked_size_error": (0, 10, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nfoo\r\n"),
33
- "test_raise_socket_timeout": (3, 1, "H"),
34
- "test_raise_line_too_long_error": (0, KB, "a" * 65536),
26
+ special_cases: ClassVar[dict] = {
27
+ "test_recving_server_close": (0, 1, b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n"),
28
+ "test_server_delay_response": (0.5, 1, b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nabcd"),
29
+ "test_raise_chunked_size_error": (0, 10, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nfoo\r\n"),
30
+ "test_raise_socket_timeout": (3, 1, b"H"),
31
+ "test_raise_line_too_long_error": (0, KB, b"a" * 65536),
35
32
  "test_request_chunked": (),
33
+ "test_send_body_chunked_utf8": (),
36
34
  "test_readlines": (
37
35
  0,
38
36
  10,
39
- "HTTP/1.1 200 OK\r\nContent-Length: 131086\r\n\r\n" + "a" * 65540 + "\r\nbb\r\n" + "c" * 65540,
37
+ b"HTTP/1.1 200 OK\r\nContent-Length: 131086\r\n\r\n" + b"a" * 65540 + b"\r\nbb\r\n" + b"c" * 65540,
38
+ ),
39
+ "test_readlines_delimiter": (0, 10, b"HTTP/1.1 200 OK\r\nContent-Length: 15\r\n\r\nabcd\rbcde\rcdef\r"),
40
+ # The first send ends inside the 3-byte "中", the second send completes it.
41
+ "test_read_body_utf8_split_across_recv": (
42
+ 0.1,
43
+ 39,
44
+ b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\n" + "中".encode(),
45
+ ),
46
+ "test_read_body_chunked_binary": (
47
+ 0,
48
+ KB,
49
+ b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n8\r\n\x89PNG\r\n\x1a\n\r\n0\r\n\r\n",
40
50
  ),
41
- "test_readlines_delimiter": (0, 10, "HTTP/1.1 200 OK\r\nContent-Length: 15\r\n\r\nabcd\rbcde\rcdef\r"),
42
51
  }
43
- request_headers = {}
44
- request_body = {}
52
+ request_headers: ClassVar[dict] = {}
53
+ request_body = b""
45
54
 
46
55
  def test_raise_connnect_error(self):
47
56
  h = k3http.Client(HOST, PORT)
@@ -125,7 +134,7 @@ class TestHttpClient(unittest.TestCase):
125
134
  for uri, each_read_size, expected_res, content_range, chunked in cases:
126
135
  h.request(uri)
127
136
 
128
- bufs = ""
137
+ bufs = b""
129
138
  if each_read_size is None:
130
139
  bufs = h.read_body(None)
131
140
  self.assertEqual(h.has_read, len(bufs))
@@ -141,7 +150,8 @@ class TestHttpClient(unittest.TestCase):
141
150
  if len(content_range) >= 2:
142
151
  start, end = content_range[0], content_range[1] + 1
143
152
 
144
- self.assertEqual(expected_res[start:end], bufs)
153
+ expected = expected_res[start:end].encode("utf-8")
154
+ self.assertEqual(expected, bufs)
145
155
  self.assertEqual(None if chunked else len(bufs), h.content_length)
146
156
  self.assertEqual(chunked, h.chunked)
147
157
 
@@ -169,6 +179,17 @@ class TestHttpClient(unittest.TestCase):
169
179
  h.send_body(body)
170
180
  self.assertEqual(h.read_status(False), status)
171
181
 
182
+ def test_send_body_chunked_utf8(self):
183
+ # The chunk size must count the 3 UTF-8 bytes of "中", not 1 char.
184
+ h = k3http.Client(HOST, PORT)
185
+ h.send_request("", "PUT", {"Transfer-Encoding": "chunked"})
186
+ h.send_body("中")
187
+ h.send_body("")
188
+ h.read_response()
189
+
190
+ chunks = self.request_body.split(b"\r\n\r\n", 1)[1]
191
+ self.assertEqual(b"3\r\n\xe4\xb8\xad\r\n0\r\n\r\n", chunks)
192
+
172
193
  def test_request_headers(self):
173
194
  cases = (
174
195
  ("/header_1", {"host": "example.com"}),
@@ -198,9 +219,10 @@ class TestHttpClient(unittest.TestCase):
198
219
 
199
220
  def test_send_body(self):
200
221
  cases = (
201
- ("/put_1b", "a", {"Content-Length": 1}),
202
- ("/put_10k", "bc" * 5 * KB, {"Content-Length": 10 * KB}),
203
- ("/put_30m", "cde" * 10 * MB, {"Content-Length": 30 * MB}),
222
+ ("/put_1b", b"a", {"Content-Length": 1}),
223
+ ("/put_10k", b"bc" * 5 * KB, {"Content-Length": 10 * KB}),
224
+ ("/put_30m", b"cde" * 10 * MB, {"Content-Length": 30 * MB}),
225
+ ("/put_binary", b"\x89PNG\r\n\x1a\n", {"Content-Length": 8}),
204
226
  )
205
227
 
206
228
  h = k3http.Client(HOST, PORT)
@@ -216,7 +238,7 @@ class TestHttpClient(unittest.TestCase):
216
238
  h = k3http.Client(HOST, PORT)
217
239
  h.request("")
218
240
 
219
- expected_body = ("a" * 65540 + "\r\n", "bb\r\n", "c" * 65540)
241
+ expected_body = (b"a" * 65540 + b"\r\n", b"bb\r\n", b"c" * 65540)
220
242
  for idx, line in enumerate(h.readlines()):
221
243
  self.assertEqual(expected_body[idx], line)
222
244
 
@@ -224,10 +246,24 @@ class TestHttpClient(unittest.TestCase):
224
246
  h = k3http.Client(HOST, PORT)
225
247
  h.request("")
226
248
 
227
- expected_body = ("abcd\r", "bcde\r", "cdef\r")
228
- for idx, line in enumerate(h.readlines("\r")):
249
+ expected_body = (b"abcd\r", b"bcde\r", b"cdef\r")
250
+ for idx, line in enumerate(h.readlines(b"\r")):
229
251
  self.assertEqual(expected_body[idx], line)
230
252
 
253
+ def test_read_body_utf8_split_across_recv(self):
254
+ h = k3http.Client(HOST, PORT)
255
+ h.request("")
256
+
257
+ body = h.read_body(None)
258
+ self.assertEqual("中".encode(), body)
259
+
260
+ def test_read_body_chunked_binary(self):
261
+ h = k3http.Client(HOST, PORT)
262
+ h.request("")
263
+
264
+ body = h.read_body(None)
265
+ self.assertEqual(b"\x89PNG\r\n\x1a\n", body)
266
+
231
267
  def test_recving_server_close(self):
232
268
  h = k3http.Client(HOST, PORT, 3)
233
269
  succ = False
@@ -235,16 +271,14 @@ class TestHttpClient(unittest.TestCase):
235
271
  try:
236
272
  h.request("")
237
273
  h.read_body(1024)
238
- except socket.error as e:
274
+ except OSError as e:
239
275
  dd(repr(e) + " while recv server close")
240
276
  succ = True
241
- except Exception as e:
242
- dd(repr(e) + " unexpected exception")
243
277
 
244
278
  self.assertTrue(succ)
245
279
 
246
280
  def test_server_delay_response(self):
247
- case = ({"content-length": "4"}, "abcd")
281
+ case = ({"content-length": "4"}, b"abcd")
248
282
  expected_headers, expected_body = case
249
283
 
250
284
  h = k3http.Client(HOST, PORT, 1)
@@ -255,7 +289,7 @@ class TestHttpClient(unittest.TestCase):
255
289
  self.assertEqual(expected_body, body)
256
290
 
257
291
  def test_client_delay_send_data(self):
258
- case = ("/client_delay", {"Content-Length": 10}, "abcde" * 2)
292
+ case = ("/client_delay", {"Content-Length": 10}, b"abcde" * 2)
259
293
  uri, headers, body = case
260
294
 
261
295
  h = k3http.Client(HOST, PORT, 3)
@@ -290,7 +324,7 @@ class TestHttpClient(unittest.TestCase):
290
324
  try:
291
325
  with h.stopwatch.timer("exception"):
292
326
  raise FakeErrorDuringHTTP(3)
293
- except Exception:
327
+ except FakeErrorDuringHTTP:
294
328
  pass
295
329
 
296
330
  trace = h.get_trace()
@@ -306,7 +340,7 @@ class TestHttpClient(unittest.TestCase):
306
340
 
307
341
  for i, k in enumerate(ks):
308
342
  self.assertEqual(k, trace[i]["name"])
309
- self.assertEqual(type(0.1), type(trace[i]["time"]))
343
+ self.assertEqual(float, type(trace[i]["time"]))
310
344
 
311
345
  names = [x["name"] for x in trace]
312
346
  self.assertEqual(
@@ -340,9 +374,9 @@ class TestHttpClient(unittest.TestCase):
340
374
 
341
375
  def test_https(self):
342
376
  cases = (
343
- ("/get_1b", "a"),
344
- ("/get_10k", "bc" * 5 * KB),
345
- ("/get_30m", "cde" * 10 * MB),
377
+ ("/get_1b", b"a"),
378
+ ("/get_10k", b"bc" * 5 * KB),
379
+ ("/get_30m", b"cde" * 10 * MB),
346
380
  )
347
381
 
348
382
  context = ssl._create_unverified_context()
@@ -355,7 +389,7 @@ class TestHttpClient(unittest.TestCase):
355
389
  self.assertEqual(expected_res, body)
356
390
 
357
391
  def __init__(self, *args, **kwargs):
358
- super(TestHttpClient, self).__init__(*args, **kwargs)
392
+ super().__init__(*args, **kwargs)
359
393
  self.server_thread = None
360
394
  self.http_server = None
361
395
 
@@ -406,6 +440,18 @@ class TestHttpClient(unittest.TestCase):
406
440
  res = "HTTP/1.1 200 OK\r\n\r\n"
407
441
  conn.sendall(res.encode("utf-8"))
408
442
 
443
+ elif self._testMethodName == "test_send_body_chunked_utf8":
444
+ conn, _ = sock.accept()
445
+ data = b""
446
+ while not data.endswith(b"\r\n0\r\n\r\n"):
447
+ buf = conn.recv(1024)
448
+ if buf == b"":
449
+ break
450
+ data += buf
451
+
452
+ TestHttpClient.request_body = data
453
+ conn.sendall(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
454
+
409
455
  else:
410
456
  conn, _ = sock.accept()
411
457
  data = (conn.recv(1024)).decode("utf-8")
@@ -417,10 +463,10 @@ class TestHttpClient(unittest.TestCase):
417
463
  sleep_time, each_send_size, content = res
418
464
  try:
419
465
  while len(content) > 0:
420
- conn.sendall((content[:each_send_size]).encode("utf-8"))
466
+ conn.sendall(content[:each_send_size])
421
467
  content = content[each_send_size:]
422
468
  time.sleep(sleep_time)
423
- except socket.error as e:
469
+ except OSError as e:
424
470
  dd(repr(e) + " while response")
425
471
 
426
472
  time.sleep(1)
@@ -429,7 +475,7 @@ class TestHttpClient(unittest.TestCase):
429
475
 
430
476
 
431
477
  class Handle(BaseHTTPRequestHandler):
432
- all_responses = {
478
+ all_responses: ClassVar[dict] = {
433
479
  "/invalid_content_len": (200, {"content-length": "abc"}, (0, "")),
434
480
  "/invalid_header": (200, {}, (0, "")),
435
481
  "/get_1b": (200, {"content-length": 1}, (1, "a")),
@@ -438,15 +484,15 @@ class Handle(BaseHTTPRequestHandler):
438
484
  "/get_10b_chunked": (200, {"Transfer-Encoding": "chunked"}, (5, "f" * 10)),
439
485
  "/get_10k_chunked": (200, {"Transfer-Encoding": "chunked"}, (KB, "gh" * 5 * KB)),
440
486
  "/get_30m_chunked": (200, {"Transfer-Encoding": "chunked"}, (10 * MB, "ijk" * 10 * MB)),
441
- "/get_10b_range": (206, {"Content-Range": "bytes %d-%d/%d" % (2, 8, 10), "Content-Length": 7}, (5, "l" * 10)),
487
+ "/get_10b_range": (206, {"Content-Range": "bytes 2-8/10", "Content-Length": 7}, (5, "l" * 10)),
442
488
  "/get_10k_range": (
443
489
  206,
444
- {"Content-Range": "bytes %d-%d/%d" % (KB, 8 * KB, 10 * KB), "Content-Length": 7 * KB + 1},
490
+ {"Content-Range": f"bytes {KB}-{8 * KB}/{10 * KB}", "Content-Length": 7 * KB + 1},
445
491
  (KB, "mn" * 5 * KB),
446
492
  ),
447
493
  "/get_30m_range": (
448
494
  206,
449
- {"Content-Range": "bytes %d-%d/%d" % (2 * MB, 25 * MB, 30 * MB), "Content-Length": 23 * MB + 1},
495
+ {"Content-Range": f"bytes {2 * MB}-{25 * MB}/{30 * MB}", "Content-Length": 23 * MB + 1},
450
496
  (10 * MB, "opq" * 10 * MB),
451
497
  ),
452
498
  "/get_200": (200, {"content-length": 1}, (0, "")),
@@ -481,9 +527,9 @@ class Handle(BaseHTTPRequestHandler):
481
527
  return
482
528
  else:
483
529
  protocol = self.protocol_version
484
- self.wfile.write(("%s %d %s\r\n" % (protocol, code, message)).encode("utf-8"))
530
+ self.wfile.write(f"{protocol} {code} {message}\r\n".encode())
485
531
  if self.path == "/invalid_header":
486
- self.wfile.write(("foo\r\n").encode("utf-8"))
532
+ self.wfile.write(b"foo\r\n")
487
533
 
488
534
  def do_PUT(self):
489
535
  try:
@@ -493,18 +539,18 @@ class Handle(BaseHTTPRequestHandler):
493
539
  return
494
540
 
495
541
  read_bytes = 0
496
- bufs = ""
542
+ bufs = b""
497
543
 
498
544
  try:
499
545
  while read_bytes < length:
500
- bufs += (self.rfile.read(length - read_bytes)).decode("utf-8")
546
+ bufs += self.rfile.read(length - read_bytes)
501
547
  read_bytes = len(bufs)
502
548
 
503
549
  TestHttpClient.request_body = bufs
504
550
  self.send_response(200)
505
551
  self.send_header("Content-Length", 0)
506
552
  self.end_headers()
507
- except Exception as e:
553
+ except OSError as e:
508
554
  dd(repr(e) + " while parse put request")
509
555
 
510
556
  def do_GET(self):
@@ -525,7 +571,9 @@ class Handle(BaseHTTPRequestHandler):
525
571
  self.end_headers()
526
572
 
527
573
  self._send_body(headers, body)
528
- except Exception as e:
574
+ except (OSError, AttributeError) as e:
575
+ # The malformed responses, such as /invalid_line, send no header,
576
+ # so end_headers() raises AttributeError.
529
577
  dd(repr(e) + " while parse get request")
530
578
 
531
579
  def _send_body(self, headers, body):
@@ -538,13 +586,13 @@ class Handle(BaseHTTPRequestHandler):
538
586
  ext = ""
539
587
  else:
540
588
  ext = ";extname"
541
- send_buf = "%x%s\r\n%s\r\n" % (len(send_buf), ext, send_buf)
589
+ send_buf = f"{len(send_buf):x}{ext}\r\n{send_buf}\r\n"
542
590
 
543
591
  self.wfile.write(send_buf.encode("utf-8"))
544
592
  data = data[each_send_size:]
545
593
 
546
594
  if "Transfer-Encoding" in headers:
547
- self.wfile.write(("0\r\n\r\n").encode("utf-8"))
595
+ self.wfile.write(b"0\r\n\r\n")
548
596
 
549
597
  def _get_body(self, headers, body):
550
598
  each_send_size, data = body
@@ -1,6 +1,3 @@
1
- #!/usr/bin/env python3
2
- # coding: utf-8
3
-
4
1
  import unittest
5
2
 
6
3
  import k3http
@@ -1,7 +1,3 @@
1
- #!/usr/bin/env python3
2
- # coding: utf-8
3
-
4
-
5
1
  def headers_add_host(headers, address):
6
2
  """
7
3
  If there is no Host field in the headers, insert the address as a Host into the headers.
File without changes
File without changes
File without changes