Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
985e45c
ci(flake8): remove ignoring unnecessary codes
spaceone Apr 27, 2026
536a00f
style: fix regex-flag-alias (FURB167)
spaceone Apr 27, 2026
23e413d
style: fix single-item-membership-test (FURB171)
spaceone Apr 27, 2026
50d6527
style: fix implicit-namespace-package (INP001)
spaceone Apr 27, 2026
704320b
perf(messages.body): fix try-except-in-loop (PERF203)
spaceone Apr 27, 2026
7d29646
perf(header.element): fix try-except-in-loop (PERF203)
spaceone Apr 27, 2026
b4e6776
style: fix blanket-noqa (PGH004)
spaceone Apr 27, 2026
7957d79
style: fix unnecessary-range-start (PIE808)
spaceone Apr 27, 2026
18d1841
style: fix pytest (PT)
spaceone Apr 27, 2026
3d0e8b6
style: fix os-path-abspath (PTH100)
spaceone Apr 27, 2026
981deb2
style: fix any-eq-ne-annotation (PYI032)
spaceone Apr 27, 2026
390e492
style: fix implicit-return-value (RET502)
spaceone Apr 27, 2026
5eaba74
ci(ruff): ignore unnecessary-paren-on-raise-exception (RSE102)
spaceone Apr 27, 2026
03b77da
style: fix typing-only-first-party-import (TC001)
spaceone Apr 27, 2026
2f271ad
style: fix unnecessary-dunder-call (PLC2801)
spaceone Apr 27, 2026
c937054
style: fix collapsible-else-if (PLR5501)
spaceone Apr 27, 2026
b65c60f
style: fix yoda-conditions (SIM300)
spaceone Apr 27, 2026
6cca7f4
style: pyupgrade
spaceone Apr 27, 2026
ecf11c2
style: noqa SIM
spaceone Apr 27, 2026
041f0d7
style: fix unraw-re-pattern (RUF039)
spaceone Apr 27, 2026
1138c46
style: fix unsorted-dunder-all (RUF022)
spaceone Apr 27, 2026
09ff5df
style: fix unused-unpacked-variable (RUF059)
spaceone Apr 27, 2026
cb53673
style: drop six
spaceone Apr 28, 2026
c1c99ff
style: fix literal-membership (PLR6201)
spaceone Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[flake8]
ignore = E501,W191,C901,E265,E402,E117
ignore = E501
29 changes: 10 additions & 19 deletions httoop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,14 @@


__all__ = [
'Status', 'Body', 'Headers', 'URI', 'Method',
'Request', 'Response', 'Protocol', 'Date', 'ServerStateMachine', 'ClientStateMachine',
'ProxyStateMachine', 'InvalidLine', 'InvalidHeader', 'InvalidURI', 'InvalidBody', 'InvalidDate',
'CONTINUE', 'SWITCHING_PROTOCOLS', 'OK', 'CREATED', 'ACCEPTED',
'NON_AUTHORITATIVE_INFORMATION', 'NO_CONTENT', 'RESET_CONTENT',
'PARTIAL_CONTENT', 'MULTIPLE_CHOICES', 'MOVED_PERMANENTLY', 'FOUND',
'SEE_OTHER', 'NOT_MODIFIED', 'USE_PROXY', 'TEMPORARY_REDIRECT',
'BAD_REQUEST', 'UNAUTHORIZED', 'PAYMENT_REQUIRED', 'FORBIDDEN',
'NOT_FOUND', 'METHOD_NOT_ALLOWED', 'NOT_ACCEPTABLE', 'GONE',
'PROXY_AUTHENTICATION_REQUIRED', 'REQUEST_TIMEOUT', 'CONFLICT',
'LENGTH_REQUIRED', 'PRECONDITION_FAILED', 'PAYLOAD_TOO_LARGE',
'URI_TOO_LONG', 'UNSUPPORTED_MEDIA_TYPE', 'BAD_GATEWAY',
'RANGE_NOT_SATISFIABLE', 'EXPECTATION_FAILED',
'I_AM_A_TEAPOT', 'INTERNAL_SERVER_ERROR', 'NOT_IMPLEMENTED',
'SERVICE_UNAVAILABLE', 'GATEWAY_TIMEOUT', 'UNPROCESSABLE_ENTITY',
'HTTP_VERSION_NOT_SUPPORTED', 'StatusException',
'DecodeError', 'EncodeError',
'__version__', 'ServerProtocol', 'UserAgentHeader', 'ServerHeader',
'cache', 'ComposedRequest', 'ComposedResponse',
'ACCEPTED', 'BAD_GATEWAY', 'BAD_REQUEST', 'CONFLICT', 'CONTINUE', 'CREATED', 'EXPECTATION_FAILED', 'FORBIDDEN', 'FOUND', 'GATEWAY_TIMEOUT',
'GONE', 'HTTP_VERSION_NOT_SUPPORTED', 'INTERNAL_SERVER_ERROR', 'I_AM_A_TEAPOT', 'LENGTH_REQUIRED', 'METHOD_NOT_ALLOWED', 'MOVED_PERMANENTLY',
'MULTIPLE_CHOICES', 'NON_AUTHORITATIVE_INFORMATION', 'NOT_ACCEPTABLE', 'NOT_FOUND', 'NOT_IMPLEMENTED', 'NOT_MODIFIED', 'NO_CONTENT', 'OK',
'PARTIAL_CONTENT', 'PAYLOAD_TOO_LARGE', 'PAYMENT_REQUIRED', 'PRECONDITION_FAILED', 'PROXY_AUTHENTICATION_REQUIRED', 'RANGE_NOT_SATISFIABLE',
'REQUEST_TIMEOUT', 'RESET_CONTENT', 'SEE_OTHER', 'SERVICE_UNAVAILABLE', 'SWITCHING_PROTOCOLS', 'TEMPORARY_REDIRECT', 'UNAUTHORIZED',
'UNPROCESSABLE_ENTITY', 'UNSUPPORTED_MEDIA_TYPE', 'URI', 'URI_TOO_LONG', 'USE_PROXY',
'Body', 'ClientStateMachine', 'ComposedRequest', 'ComposedResponse', 'Date', 'DecodeError', 'EncodeError', 'Headers',
'InvalidBody', 'InvalidDate', 'InvalidHeader', 'InvalidLine', 'InvalidURI',
'Method', 'Protocol', 'ProxyStateMachine', 'Request', 'Response', 'ServerHeader', 'ServerProtocol', 'ServerStateMachine', 'Status',
'StatusException', 'UserAgentHeader', '__version__', 'cache',
]
5 changes: 3 additions & 2 deletions httoop/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
python3 -m httoop compose response | python3 -m httoop parse response
"""

import pathlib
import sys
from argparse import ArgumentParser, FileType

Expand All @@ -22,7 +23,7 @@ def __init__(self) -> None:
self.message = None
self.parser = ArgumentParser(name, description=self.__doc__, epilog='https://github.com/spaceone/httoop/')
self.parent_parser = ArgumentParser(add_help=False)
self.parser.add_argument('-v', '--version', action='version', version='%%(prog)s %s' % (version,))
self.parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {version}')
self.add_subparsers()
self.parse_arguments()

Expand Down Expand Up @@ -132,7 +133,7 @@ def common(self) -> None:
if body == '-':
body = sys.stdin.read()
elif body.startswith('@'):
body = open(body[1:], 'rb')
body = pathlib.Path(body[1:]).open('rb')
self.message.body = body

sys.stdout.write(self.decode(bytes(self.message)))
Expand Down
3 changes: 2 additions & 1 deletion httoop/authentication/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def compose(self) -> bytes:
def split(cls, value: bytes) -> list[bytes | Any]:
value = cls.RE_SPACE_SPLIT.split(value)
indexes = [i for i, val in enumerate(value) if val != b',' and b'=' not in val]
return [b' '.join(value[a:b]) for a, b in zip(indexes, indexes[1:] + [None])]
return [b' '.join(value[a:b]) for a, b in zip(indexes, [*indexes[1:], None])]


class AuthRequestElement(AuthElement):
Expand Down Expand Up @@ -89,6 +89,7 @@ def username(self, username) -> None:
def password(self):
if self.scheme == 'basic':
return self.params.get('password').decode(self.encoding)
return None

@password.setter
def password(self, password) -> None:
Expand Down
3 changes: 1 addition & 2 deletions httoop/authentication/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ def parse(authinfo: bytes) -> dict[str, bytes]:
except ValueError:
raise InvalidHeader(_('No username:password provided'))

authinfo = {
return {
# 'username': username.decode('ISO8859-1'),
# 'password': password.decode('ISO8859-1')
'username': username,
'password': password,
}
return authinfo

@staticmethod
def compose(authinfo: ByteUnicodeDict) -> bytes:
Expand Down
16 changes: 7 additions & 9 deletions httoop/authentication/digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,19 +175,19 @@ def calculate_request_digest(cls, authinfo: ByteUnicodeDict) -> bytes:
algorithm = authinfo.get('algorithm', b'MD5').decode('ASCII', 'replace')
H = cls.get_algorithm(algorithm)

if algorithm == 'MD5-sess' and authinfo.get('A1'):
if algorithm == 'MD5-sess' and authinfo.get('A1'): # noqa: SIM108
secret = H(authinfo['A1'])
else:
secret = H(cls.A1(authinfo))

qop = authinfo.get('qop')
hash_a2 = H(cls.A2(authinfo))
if qop in (b'auth', b'auth-int'):
if qop in {b'auth', b'auth-int'}:
data = b'%s:%s:%s:%s:%s' % (authinfo['nonce'], authinfo['nc'], authinfo['cnonce'], authinfo['qop'], hash_a2)
elif qop is None:
data = b'%s:%s' % (authinfo['nonce'], hash_a2)
else: # pragma: no cover
raise NotImplementedError('Unknown quality of protection: %r' % (qop,))
raise NotImplementedError(f'Unknown quality of protection: {qop!r}')

return H(b'%s:%s' % (secret, data))

Expand All @@ -196,21 +196,19 @@ def A2(cls, params: ByteUnicodeDict) -> bytes:
qop = params.get('qop', b'')
if not qop or qop == b'auth':
return b'%s:%s' % (params['method'], params['uri'])
elif qop == b'auth-int':
if qop == b'auth-int':
H = cls.get_algorithm(params['algorithm'])
return b'%s:%s:%s' % (params['method'], params['uri'], H(params['entity_body']))
else: # pragma: no cover
raise NotImplementedError('Unknown quality of protection: %r' % (qop,))
raise NotImplementedError(f'Unknown quality of protection: {qop!r}') # pragma: no cover

@classmethod
def A1(cls, params: ByteUnicodeDict) -> bytes:
algorithm = params.get('algorithm', b'')

if not algorithm or algorithm == b'MD5':
return b'%s:%s:%s' % (params['username'], params['realm'], params['password'])
elif algorithm == b'MD5-sess':
if algorithm == b'MD5-sess':
H = cls.get_algorithm(algorithm)
s = b'%s:%s:%s' % (params['username'], params['realm'], params['password'])
return b'%s:%s:%s' % (H(s), params['nonce'], params['cnonce'])
else: # pragma: no cover
raise NotImplementedError('Unknown algorithm: %s' % (algorithm,))
raise NotImplementedError(f'Unknown algorithm: {algorithm}') # pragma: no cover
2 changes: 1 addition & 1 deletion httoop/codecs/application/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
from httoop.codecs.application.zlib import Deflate


__all__ = ['FormURLEncoded', 'JSON', 'GZip', 'Deflate', 'XML', 'HAL']
__all__ = ['HAL', 'JSON', 'XML', 'Deflate', 'FormURLEncoded', 'GZip']
4 changes: 2 additions & 2 deletions httoop/codecs/application/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def decode(cls, data: bytes, charset: None = None, mimetype: None = None) -> str
try:
with gzip.GzipFile(fileobj=io.BytesIO(data)) as fd:
data = fd.read()
except (zlib.error, IOError, EOFError):
except (zlib.error, OSError, EOFError):
raise DecodeError(_('Invalid gzip data.'))
return Codec.decode(data, charset)

Expand Down Expand Up @@ -61,5 +61,5 @@ def iterdecode(cls, data, charset=None, mimetype=None):
fd.write(part)
fd.seek(0)
yield Codec.decode(gzfd.read(), charset)
except (zlib.error, IOError, EOFError):
except (zlib.error, OSError, EOFError):
raise DecodeError(_('Invalid gzip data.'))
9 changes: 4 additions & 5 deletions httoop/codecs/application/hal_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@
# TODO: emit a warning
def expand(href: str, templates: dict[str, str]) -> str:
for templ, value in templates.items():
href = href.replace('{%s}' % (templ,), value)
href = href.replace(f'{{{templ}}}', value)
return href


from httoop.codecs.application.json import JSON
from httoop.exceptions import DecodeError, EncodeError
from httoop.six import string_types


if TYPE_CHECKING:
Expand All @@ -40,7 +39,7 @@ def __init__(self, *args, **kwargs) -> None:
def self(self) -> str:
return self.expand(self.get_link('self')['href'])

def get_links(self, relation: str, name: None = None) -> Iterator[dict[str, None | bool | str]]:
def get_links(self, relation: str, name: None = None) -> Iterator[dict[str, bool | str | None]]:
links = self['_links'].get(relation)
if links is None:
return
Expand All @@ -51,7 +50,7 @@ def get_links(self, relation: str, name: None = None) -> Iterator[dict[str, None
for link in links:
if not isinstance(link, dict):
raise DecodeError('HAL link must be object')
if not isinstance(link.get('href'), string_types):
if not isinstance(link.get('href'), str):
raise DecodeError('HAL links must contain href')
if name is not None and link.get('name') != name:
continue
Expand All @@ -62,7 +61,7 @@ def get_links(self, relation: str, name: None = None) -> Iterator[dict[str, None
link.setdefault('profile', None)
link.setdefault('hreflang', None)
for key in ('name', 'type', 'profile', 'hreflang'):
if not isinstance(link[key], string_types):
if not isinstance(link[key], str):
link[key] = None
if not isinstance(link['templated'], bool):
link['templated'] = False
Expand Down
2 changes: 1 addition & 1 deletion httoop/codecs/application/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType |
try:
return fromstring(data)
except ParseError as exc:
raise DecodeError('Could not decode as %s: %s' % (mimetype, exc,))
raise DecodeError(f'Could not decode as {mimetype}: {exc}')

@classmethod
def encode(cls, root: Element, charset: str | None = None, mimetype: ContentType | None = None) -> bytes:
Expand Down
2 changes: 1 addition & 1 deletion httoop/codecs/multipart/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from httoop.codecs.multipart.multipart import Multipart


__all__ = ['MultipartMixed', 'MultipartMixedReplace', 'MultipartFormData', 'MultipartAlternative', 'MultipartDigest', 'MultipartParallel', 'MultipartByteranges']
__all__ = ['MultipartAlternative', 'MultipartByteranges', 'MultipartDigest', 'MultipartFormData', 'MultipartMixed', 'MultipartMixedReplace', 'MultipartParallel']


class MultipartMixed(Multipart):
Expand Down
2 changes: 1 addition & 1 deletion httoop/codecs/multipart/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def decode(cls, data: bytes, charset: str | None = None, mimetype: ContentType |
if part:
raise DecodeError(_('Data before boundary: %r'), part.decode('ISO8859-1'))
part = parts.pop()
if part not in (b'--', b'--\r\n'):
if part not in {b'--', b'--\r\n'}:
raise DecodeError(_('Invalid multipart end: %r'), part.decode('ISO8859-1'))

from httoop.messages.body import Body
Expand Down
2 changes: 1 addition & 1 deletion httoop/codecs/text/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from httoop.codecs.text.plain import PlainText


__all__ = ['PlainText', 'HTML']
__all__ = ['HTML', 'PlainText']
7 changes: 3 additions & 4 deletions httoop/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@

from httoop.exceptions import InvalidDate
from httoop.meta import HTTPSemantic
from httoop.six import with_metaclass
from httoop.util import _


__all__ = ['Date']


class Date(with_metaclass(HTTPSemantic)):
class Date(metaclass=HTTPSemantic):
"""
A HTTP Date string.

Expand All @@ -41,7 +40,7 @@ class Date(with_metaclass(HTTPSemantic)):
Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
"""

__slots__ = ('__composed', '__timestamp', '__datetime', '__time_struct')
__slots__ = ('__composed', '__datetime', '__time_struct', '__timestamp')

def __init__(self, timeval: Any | None = None) -> None:
"""
Expand Down Expand Up @@ -153,7 +152,7 @@ def __int__(self) -> int:
def __float__(self) -> float:
return float(self.__timestamp)

def __eq__(self, other: Any) -> bool:
def __eq__(self, other: object) -> bool:
try:
return int(self) == int(self.__other(other))
except NotImplementedError: # pragma: no cover
Expand Down
12 changes: 5 additions & 7 deletions httoop/gateway/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from typing import Any, Callable, Iterator

from httoop.messages import Body
from httoop.six import reraise


__all__ = ('WSGI',)
Expand Down Expand Up @@ -66,15 +65,15 @@ def __call__(self, application: Callable) -> Iterator[Any]:
def write(data):
if not self.headers_set:
raise RuntimeError('write() before start_response()')
elif not self.headers_sent:
if not self.headers_sent:
self.start_response()
self.headers_sent = True
return self.response.body.write(data)

def start_response(status, response_headers, exc_info=None):
if exc_info and self.headers_sent:
try:
reraise(exc_info[0], exc_info[1], exc_info[2])
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None # avoid dangling circular ref
elif self.headers_set:
Expand Down Expand Up @@ -105,7 +104,7 @@ def start_response(status, response_headers, exc_info=None):
break
else:
write(b'') # send headers now if body was empty
return
return None

def buffered(data):
try:
Expand All @@ -126,7 +125,7 @@ def get_environ(self) -> dict[str, str | tuple[int, int] | WSGIBody | bool | Non
environ.update({
'HTTP_%s' % name.upper().replace('-', '_'): value
for name, value in self.request.headers.items()
if name.lower() not in ('content-type', 'content-length')
if name.lower() not in {'content-type', 'content-length'}
})
environ.update({
'REQUEST_METHOD': str(self.request.method),
Expand All @@ -147,8 +146,7 @@ def get_environ(self) -> dict[str, str | tuple[int, int] | WSGIBody | bool | Non
'wsgi.multiprocess': self.multiprocess,
'wsgi.run_once': self.run_once,
})
environ = {key: value.decode('ISO8859-1') if isinstance(value, bytes) else value for key, value in environ.items()}
return environ
return {key: value.decode('ISO8859-1') if isinstance(value, bytes) else value for key, value in environ.items()}

def set_environ(self, environ: dict[str, str]) -> None:
environ = environ.copy()
Expand Down
12 changes: 5 additions & 7 deletions httoop/header/conditional.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Any

from httoop.date import Date
from httoop.exceptions import InvalidDate
from httoop.header.element import HeaderElement
Expand All @@ -13,7 +11,7 @@ def sanitize(self) -> None:
super().sanitize()
self.value = self.Date.parse(self.value.encode('ASCII', 'replace'))

def __eq__(self, other: Any) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Date):
if isinstance(other, _DateComparable):
other = int(other)
Expand All @@ -29,8 +27,8 @@ def __int__(self) -> int:

class _MatchElement:

def __eq__(self, other: Any) -> bool:
return self.value == other or self.value == '*'
def __eq__(self, other: object) -> bool:
return self.value in {other, '*'}

def matches(self, etag):
return self == etag
Expand All @@ -57,10 +55,10 @@ class ETag(HeaderElement):

is_response_header = True

def __eq__(self, other: Any) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, ETag):
other = self.__class__(other)
return other.value == self.value or other.value == '*'
return other.value in {self.value, '*'}


class LastModified(_DateComparable, HeaderElement):
Expand Down
Loading
Loading