Source code for pcapkit.vendor.ngap.procedure_code

# -*- coding: utf-8 -*-
"""NGAP Elementary Procedure Codes
=====================================

.. module:: pcapkit.vendor.ngap.procedure_code

This module contains the vendor crawler for **NGAP Elementary Procedure
Codes**, which is automatically generating
:class:`pcapkit.const.ngap.procedure_code.ProcedureCode`.

GitHub issue #880's owner ruling: unlike every other crawler in this package,
the source of truth here is not a network registry :mod:`requests` can fetch
-- 3GPP publishes TS 38.413 as a PDF, with no CSV or HTML IANA-style registry
kept in step with it. |pycrate|_ already carries the up-to-date assignment as
compiled ASN.1, at ``pycrate_asn1dir/NGAP.py``'s ``NGAP_Constants`` class (one
``INT`` value object per ``id-<Name>`` constant, tagged with the ``ProcedureCode``
or ``ProtocolIE-ID`` open type it belongs to) -- and it is the very same
dependency :mod:`pcapkit.protocols.application.ngap` already needs installed to
decode NGAP at all. So this crawler reads that already-installed package
directly instead of fetching anything over the network: :attr:`~ProcedureCode.LINK`
is :obj:`None` and :meth:`ProcedureCode._request` imports ``pycrate_asn1dir.NGAP``
in place of :meth:`~pcapkit.vendor.default.Vendor._request`'s HTTP round trip.
No network access happens at generation time -- only, if at all, whenever
``pip install pypcapkit[NGAP]`` last ran.

:class:`~pcapkit.const.ngap.procedure_code.ProcedureCode` is a genuinely open
registry -- 3GPP keeps assigning new elementary procedures to TS 38.413 -- so,
unlike the closed :mod:`pcapkit.protocols.internet.mh` enums GitHub issue #877
ruled on, an in-range value this crawler has not (yet) seen is not a bug to
raise on: :meth:`~pcapkit.const.ngap.procedure_code.ProcedureCode._missing_`
answers it with a throwaway, non-registering member instead (see
:meth:`pcapkit.corekit.enum.EnumRegistry._unregistered_member`), which is what
stops two different unrecognised keys from aliasing onto the same member --
the defect GitHub issue #880 exists to fix.

.. |pycrate| replace:: ``pycrate``
.. _pycrate: https://github.com/pycrate-org/pycrate

"""
import collections
import sys
from typing import TYPE_CHECKING

from pcapkit.vendor.default import Vendor

if TYPE_CHECKING:
    from collections import Counter
    from typing import Callable

__all__ = ['ProcedureCode']

#: The ``NGAP-CommonDataTypes`` open type this crawler claims. Matched against
#: each ``NGAP_Constants`` value object's own ``_typeref.called`` -- see
#: :meth:`ProcedureCode._request` for why the private attribute is read
#: directly rather than through a public accessor.
_TYPEREF = ('NGAP-CommonDataTypes', 'ProcedureCode')

#: Default constant template of enumerate registry, sourced from pycrate
#: rather than an IANA CSV -- see the module docstring.
LINE = lambda NAME, DOCS, FLAG, 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 aenum import IntEnum

from pcapkit.corekit.enum import EnumRegistry

__all__ = ['{NAME}']


class {NAME}(EnumRegistry, IntEnum):
    """[{NAME}] {DOCS}"""

    {ENUM}

    @classmethod
    def _missing_(cls, value: 'int') -> '{NAME}':
        """Lookup function used when value is not found.

        Args:
            value: Value to get enum item.

        """
        if not ({FLAG}):
            raise ValueError('%r is not a valid %s' % (value, cls.__name__))
        return cls._unregistered_member(value, 'Unassigned')
'''.strip()  # type: Callable[[str, str, str, str, str], str]


[docs] class ProcedureCode(Vendor): """NGAP elementary procedure codes, 3GPP TS 38.413.""" #: Value limit checker, matching ``pycrate_asn1dir.NGAP``'s own #: ``ProcedureCode._const_val`` (``ASN1RangeInt(lb=0, ub=255)``): a #: ``ProcedureCode`` is encoded as one aligned-PER octet. FLAG = 'isinstance(value, int) and 0 <= value <= 255' # No network registry to link to -- see the module docstring. Left # unset rather than assigned ``None`` explicitly: ``Vendor.LINK`` already # defaults to :obj:`None`, and a fresh assignment here would need the # same ``# type: ignore[assignment]`` the base class's own declaration # carries, for narrowing a ``str``-annotated attribute to ``None`` again. def _request(self) -> 'list[tuple[str, int]]': # type: ignore[override] """Read the assignment from |pycrate|_'s compiled NGAP specification. Every ``NGAP_Constants`` value object carries a private ``_typeref`` naming which ``NGAP-CommonDataTypes`` open type it belongs to (``.called``, a ``(module, type)`` pair), a private ``_name`` holding the ASN.1 identifier verbatim (``'id-AMFConfigurationUpdate'``), and a private ``_val`` holding the assigned integer. None of the three has a public accessor that survives this far: :meth:`~pycrate_asn1rt.asnobj.ASN1Obj. get_typeref` resolves to the *referenced type object* rather than its ``(module, name)`` pair, and that object's own :meth:`get_name` reads :obj:`None` here despite :func:`repr` showing the name -- measured against pycrate 0.8.1. Reading the private attributes directly is therefore the only way to recover what this crawler needs, which is a property of parsing pycrate's own compiled representation rather than a shortcut around a public API this package should have used instead. Returns: One ``(python_identifier, value)`` pair per ``ProcedureCode`` assignment, in the compiled specification's own declaration order -- already ascending by value, measured against pycrate 0.8.1. Raises: ImportError: If the optional ``pycrate`` dependency is not installed. """ try: from pycrate_asn1dir.NGAP import NGAP_Constants except ImportError as error: raise ImportError( "pcapkit.vendor.ngap.procedure_code needs the optional 'pycrate' " "dependency installed to source NGAP's ProcedureCode assignments " "from pycrate_asn1dir.NGAP (pip install pypcapkit[NGAP]); see " 'GitHub issue #880') from error entries = [] # type: list[tuple[str, int]] for obj in NGAP_Constants._all_: # pylint: disable=protected-access typeref = obj._typeref # pylint: disable=protected-access if typeref is None or typeref.called != _TYPEREF: continue raw_name = obj._name # pylint: disable=protected-access if not raw_name.startswith('id-'): continue name = raw_name[3:].replace('-', '_') entries.append((name, obj._val)) # pylint: disable=protected-access return entries def count(self, data: 'list[tuple[str, int]]') -> 'Counter[str]': # type: ignore[override] """No duplicate-name bookkeeping needed. Unlike a hand-scraped CSV, |pycrate|_'s compiled specification names each assignment once -- measured, zero duplicate names and zero duplicate values across all 81 -- so :meth:`~pcapkit.vendor.default. Vendor.rename`'s per-name counter has nothing to disambiguate and this crawler never calls it. """ return collections.Counter() def process(self, data: 'list[tuple[str, int]]') -> 'list[str]': # type: ignore[override] """Render each assignment as a member declaration. Args: data: The ``(name, value)`` pairs :meth:`_request` read from |pycrate|_. Returns: One ``NAME = VALUE`` source line per assignment. """ return [f'{name} = {value}' for name, value in data] def context(self, data: 'list[tuple[str, int]]') -> 'str': # type: ignore[override] """Generate constant context. Args: data: The ``(name, value)`` pairs :meth:`_request` read from |pycrate|_. Returns: Constant context. """ ENUM = '\n\n '.join(self.process(data)).strip() return LINE(self.NAME, self.DOCS, self.FLAG, ENUM, self.__module__)
if __name__ == '__main__': sys.exit(ProcedureCode()) # type: ignore[arg-type]