# -*- coding: utf-8 -*-
"""HTTP Method
=================
.. module:: pcapkit.vendor.http.method
This module contains the vendor crawler for **HTTP Method**,
which is automatically generating :class:`pcapkit.const.http.method.Method`.
"""
import csv
import re
import sys
from typing import TYPE_CHECKING
from pcapkit.vendor.default import Vendor
if TYPE_CHECKING:
from typing import Callable
__all__ = ['Method']
#: Default constant template of enumeration registry from IANA CSV.
LINE = lambda NAME, DOCS, ENUM, MODL: f'''\
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long
"""{(name := DOCS.split(' [', maxsplit=1)[0])}
{'=' * (len(name) + 6)}
.. module:: {MODL.replace('vendor', 'const')}
This module contains the constant enumeration for **{name}**,
which is automatically generated from :class:`{MODL}.{NAME}`.
"""
from typing import TYPE_CHECKING
from aenum import StrEnum
from pcapkit.corekit.enum import EnumRegistry
from pcapkit.utilities.exceptions import EnumValueError
if TYPE_CHECKING:
from typing import Any, Optional, Type
__all__ = ['{NAME}']
class {NAME}(EnumRegistry, StrEnum):
"""[{NAME}] {DOCS}"""
if TYPE_CHECKING:
#: Safe method.
safe: 'bool'
#: Idempotent method.
idempotent: 'bool'
def __new__(cls, value: 'str', safe: 'bool' = False,
idempotent: 'bool' = False) -> 'Type[{NAME}]':
"""Create a member from a registry row.
Args:
value: Method token, which is also the member's value.
safe: Whether the method is safe.
idempotent: Whether the method is idempotent.
"""
obj = str.__new__(cls, value)
obj._value_ = value
obj.safe = safe
obj.idempotent = idempotent
return obj
def __repr__(self) -> 'str':
return f'<{{self.__class__.__name__}}.{{self._value_}}>'
{ENUM}
@classmethod
def _unregistered_member(cls, value: 'str', name: 'str') -> '{NAME}':
"""Build a member absent from this registry's own lookup tables.
Leaves :attr:`safe` and :attr:`idempotent` at the same defaults
:meth:`__new__` itself would, rather than missing entirely --
:meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`
bypasses :meth:`__new__` (it calls :class:`str`'s directly), so
those attributes would otherwise be absent. There is no more
specific value to reconstruct them from -- a bare wire method verb
carries neither, which is exactly the owner's reasoning for why
``get()``/``_missing_`` must not mint one: :meth:`register` is the
path that can actually supply them.
Args:
value: Value to get enum item -- the convention here, shared with
:class:`~pcapkit.const.ftp.command.FEATCode` and
:class:`~pcapkit.const.ftp.command.Command`, is the caller's
own casing, unchanged: an unregistered member's *value* is
exactly what was observed on the wire, matching how a
*registered* member's own value is exactly what was
declared, never reformatted. Only :attr:`name` -- the
identifier, not the value -- is canonicalised.
A *registered* member carries its value the same way:
:meth:`__new__` passes ``value`` to ``str.__new__``, as
:class:`~pcapkit.const.ftp.command.Command`'s own
``__new__`` does (:issue:`870`), so both kinds of member
carry their own value as real :class:`str` content
(``str(Method.GET) == 'GET'`` and
``str(Method('frob')) == 'frob'``).
name: Bare label for the unregistered member -- here, the
canonical upper-case form of ``value``, matching the name
every *registered* member of this class is looked up by,
since :issue:`860`'s open-vocabulary registries have no manufactured
placeholder label to fall back to.
"""
obj = super()._unregistered_member(value, name)
obj.safe = False
obj.idempotent = False
return obj
@classmethod
def get(cls, key: 'str', default: 'Optional[str]' = None) -> '{NAME}':
"""Resolve ``key`` to a member, matching the method token case-sensitively.
Delegates to :meth:`~pcapkit.corekit.enum.EnumLookup.get` for the
lookup itself, so ``key`` is checked against member values as well
as names (GitHub issue :issue:`908`). That is what resolves the two
IANA methods whose member *name* differs from their *value* --
``BASELINE_CONTROL`` / ``'BASELINE-CONTROL'`` and
``VERSION_CONTROL`` / ``'VERSION-CONTROL'``, a hyphen being unusable
in a Python identifier -- which the constructor also finds by
value, before it falls back to :meth:`_missing_`.
It is a :class:`classmethod`, as the base is, because a
zero-argument ``super()`` needs a first argument to bind
(:issue:`908`). ``{NAME}.get('X')`` binds the same as it would on a
:class:`staticmethod`.
``default`` does not follow the base's ``NO_DEFAULT`` sentinel and
value-only fallback: the base's ``default`` must name a
*registered* value (resolved through ``_value2member_map_``), while
this override's ``default`` supplies the *value* of the
unregistered member it builds on a miss.
Args:
key: Key to get enum item. Looked up case-**sensitively**,
per :rfc:`9110#section-9.1` -- the method token is
case-sensitive, unlike
:meth:`~pcapkit.const.ftp.command.Command.get`'s equivalent override, which stays
case-insensitive because :rfc:`959#section-5` says FTP
command codes are not. Checked against both member names
and values, via the base's own precedence -- name before
value -- so a value-only match such as
``'BASELINE-CONTROL'`` resolves too (:issue:`908`).
default: Value for the unregistered member built when ``key``
matches neither a name nor a value. ``None``, the
default, uses ``key`` itself.
Returns:
The member whose name or value is ``key``. On a miss, an
unregistered member named ``key`` upper-cased, with the value
``default`` (or ``key``), which is not added to the lookup tables,
so ``get`` never registers a method (:issue:`860`).
Raises:
ValueError: If ``key`` is not a :class:`str`. This reaches the
caller from the base's non-``str`` branch, where
:meth:`_validate_value` refuses it before ``cls(key)`` could
reach ``_missing_``, and is **not** caught by
the miss fallback described under *Returns* -- only a failed
lookup is. Raising is the intended behaviour for
``get(b'GET')`` as well, since no member of this registry
has a :class:`bytes` name or value.
"""
try:
return super().get(key)
except KeyError:
# NOTE: the value keeps the caller's casing (``default`` or ``key``); the name is the
# upper-case form :meth:`_missing_` uses. ``get`` does not mint (GitHub issue #860).
return cls._unregistered_member(default if default is not None else key, key.upper())
@classmethod
def _validate_value(cls, value: 'Any') -> 'None':
"""Reject ``value`` unless it is a :class:`str`, as :meth:`_missing_` does.
Overrides :meth:`~pcapkit.corekit.enum.EnumLookup._validate_value`
with the check :meth:`_missing_` makes, and the message it raises, so
:meth:`~pcapkit.corekit.enum.EnumRegistry.register` refuses exactly the
values :meth:`get` refuses, in the same words (:issue:`1816`). Any
:class:`str` passes, since :meth:`get` and :meth:`_missing_` resolve
every one.
Args:
value: Value about to be looked up or minted.
Raises:
EnumValueError: If ``value`` is not a :class:`str`. A
:exc:`ValueError`, so an ``except ValueError`` caller is
unaffected.
"""
if not isinstance(value, str):
raise EnumValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
@classmethod
def _missing_(cls, value: 'str') -> '{NAME}':
"""Lookup function used when value is not found.
Args:
value: Value to get enum item. Matched case-insensitively against
the canonical upper-case member names, then against member
values, so ``'baseline-control'`` resolves to
``BASELINE_CONTROL`` by the value match the fix for
:issue:`2284` added -- deliberately unlike :meth:`get`, which
checks names and values too (GitHub issue :issue:`908`) but
case-**sensitively**, per :rfc:`9110#section-9.1`
(:issue:`896`). The owner kept this
fold on GitHub issue :issue:`2552`, departing from that RFC on
purpose: HTTP methods are case-insensitive here, and member
names follow Python's naming conventions.
"""
if not isinstance(value, str):
raise ValueError(f'{{value!r}} is not a valid {{cls.__name__}}')
name = value.upper()
if name in cls._member_map_:
return cls._member_map_[name]
if name in cls._value2member_map_:
return cls._value2member_map_[name]
return cls._unregistered_member(value, name)
'''.strip() # type: Callable[[str, str, str, str], str]
[docs]
class Method(Vendor):
"""HTTP Method"""
#: Link to registry.
LINK = 'https://www.iana.org/assignments/http-methods/methods.csv'
def process(self, data: 'list[str]') -> 'list[str]': # type: ignore[override]
"""Process CSV data.
Args:
data: CSV data.
Returns:
Enumeration fields.
"""
reader = csv.reader(data)
next(reader) # header
enum = [] # type: list[str]
for item in reader:
meth = item[0]
if meth == '*':
continue
safe = item[1]
idem = item[2]
rfcs = item[3]
temp = [] # type: list[str]
for rfc in self.split_references(rfcs):
if 'RFC' in rfc and re.match(r'\d+', rfc[3:]):
#temp.append(f'[{rfc[:3]} {rfc[3:]}]')
temp_split = rfc[3:].split(', ', maxsplit=1)
if len(temp_split) > 1:
temp.append(f'[:rfc:`{temp_split[0]}#{temp_split[1].lower()}`]'.replace(' ', '-'))
else:
temp.append(f'[:rfc:`{temp_split[0]}`]')
else:
temp.append(f'[{rfc}]'.replace('_', ' '))
desc = self.wrap_comment(re.sub(
r'\r*\n', ' ', f'{meth} {"".join(temp) if rfcs else ""}',
flags=re.MULTILINE))
name = self.safe_name(meth).upper()
safe_flag = 'True' if safe == 'yes' else 'False'
idem_flag = 'True' if idem == 'yes' else 'False'
pres = f"{name} = {meth!r}, {safe_flag}, {idem_flag}"
sufs = f'#: {desc}'
enum.append(f'{sufs}\n {pres}')
return enum
def context(self, data: 'list[str]') -> 'str':
"""Generate constant context.
Args:
data: CSV data.
Returns:
Constant context.
"""
enum = self.process(data)
ENUM = '\n\n '.join(map(lambda s: s.rstrip(), enum)).strip()
return LINE(self.NAME, self.DOCS, ENUM, self.__module__)
if __name__ == '__main__':
sys.exit(Method()) # type: ignore[arg-type]