From db8f6e35da95033ecc1fcf7435005ae4a7b41fc1 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 23 Jan 2021 10:40:47 +0300 Subject: [PATCH 01/23] freeze deps in tox.ini --- setup.py | 48 +++++++++++++++++++++++++----------------------- tox.ini | 34 +++++++++++++++++----------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/setup.py b/setup.py index f24ffc0..3073252 100644 --- a/setup.py +++ b/setup.py @@ -1,34 +1,36 @@ -import aiosnmp from pathlib import Path + from setuptools import setup -readme = Path(__file__).with_name('README.md') +import aiosnmp + +readme = Path(__file__).with_name("README.md") setup( - name='aiosnmp', + name="aiosnmp", version=aiosnmp.__version__, - packages=['aiosnmp'], - url='https://github.com/hh-h/aiosnmp', - license='MIT', - author='Valetov Konstantin', - author_email='forjob@thetrue.name', - description='asyncio SNMP client', - long_description=readme.read_text('utf-8'), - long_description_content_type='text/markdown', + packages=["aiosnmp"], + url="https://github.com/hh-h/aiosnmp", + license="MIT", + author="Valetov Konstantin", + author_email="forjob@thetrue.name", + description="asyncio SNMP client", + long_description=readme.read_text("utf-8"), + long_description_content_type="text/markdown", setup_requires=["pytest-runner"], tests_require=["pytest"], classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Development Status :: 4 - Beta', - 'Operating System :: POSIX', - 'Operating System :: MacOS :: MacOS X', - 'Framework :: AsyncIO', + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Development Status :: 4 - Beta", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Framework :: AsyncIO", ], - python_requires='>=3.6' + python_requires=">=3.6", ) diff --git a/tox.ini b/tox.ini index 49fdec4..811802b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,11 +3,11 @@ envlist = check, py{36,37,38,39}-{asyncio,uvloop} [testenv] deps = - pytest - pytest-xdist - pytest-asyncio - pytest-cov - uvloop: uvloop + pytest == 6.2.1 + pytest-xdist == 2.2.0 + pytest-asyncio == 0.14.0 + pytest-cov == 2.11.1 + uvloop: uvloop == 0.14.0 commands = asyncio: pytest -n 1 --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} uvloop: pytest -n 1 --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} @@ -16,24 +16,24 @@ docker = [testenv:check] deps = - mypy - black - flake8 - isort + flake8 == 3.8.4 + isort == 5.7.0 + black == 20.8b1 + mypy == 0.800 commands = - flake8 aiosnmp tests examples - isort -q --check --diff aiosnmp tests examples - black -q --check --diff aiosnmp tests examples - mypy aiosnmp + flake8 aiosnmp/ tests/ examples/ setup.py + isort -q --check --diff aiosnmp/ tests/ examples/ setup.py + black -l 120 -q --check --diff aiosnmp/ tests/ examples/ setup.py + mypy aiosnmp/ docker = skip_install = true [testenv:format] deps = - black - isort + isort == 5.7.0 + black == 20.8b1 commands = - isort aiosnmp tests examples - black -l 120 aiosnmp tests examples + isort aiosnmp/ tests/ examples/ setup.py + black -l 120 aiosnmp/ tests/ examples/ setup.py docker = skip_install = true From 03e3b13b8cf88ddf1adc07771a69792ca77fe1c9 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 23 Jan 2021 14:28:28 +0300 Subject: [PATCH 02/23] return transport for trap server and update example --- aiosnmp/trap.py | 7 ++++--- examples/trap.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/aiosnmp/trap.py b/aiosnmp/trap.py index 6349353..ab77898 100644 --- a/aiosnmp/trap.py +++ b/aiosnmp/trap.py @@ -1,7 +1,7 @@ __all__ = ("SnmpV2TrapServer",) import asyncio -from typing import Callable, Iterable, Optional, Set +from typing import Callable, Iterable, Optional, Set, Tuple, cast from .message import SnmpV2TrapMessage from .protocols import SnmpTrapProtocol @@ -29,10 +29,11 @@ def __init__( self.communities = set(communities) self.handler: Callable = handler - async def run(self) -> None: + async def run(self) -> Tuple[asyncio.BaseTransport, SnmpTrapProtocol]: loop = asyncio.get_event_loop() - await loop.create_datagram_endpoint( + transport, protocol = await loop.create_datagram_endpoint( lambda: SnmpTrapProtocol(self.communities, self.handler), local_addr=(self.host, self.port), ) + return transport, cast(SnmpTrapProtocol, protocol) diff --git a/examples/trap.py b/examples/trap.py index e4ce87c..181d8d7 100644 --- a/examples/trap.py +++ b/examples/trap.py @@ -9,9 +9,13 @@ async def handler(host: str, port: int, message: aiosnmp.SnmpV2TrapMessage) -> N print(f"oid: {d.oid}, value: {d.value}") -async def main(): - p = aiosnmp.SnmpV2TrapServer(host="127.0.0.1", port=162, communities=("public",), handler=handler) - await p.run() +if __name__ == "__main__": + loop = asyncio.get_event_loop() + trap_server = aiosnmp.SnmpV2TrapServer(host="127.0.0.1", port=162, communities=("public",), handler=handler) + transport, _ = loop.run_until_complete(trap_server.run()) - -asyncio.run(main()) + try: + print(f"running server on {trap_server.host}:{trap_server.port}") + loop.run_forever() + finally: + transport.close() From eec6f038865d19ef7a0dc9779b027934d04d6dd2 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 23 Jan 2021 14:29:52 +0300 Subject: [PATCH 03/23] 0.3.1 --- aiosnmp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 58c9ef4..b3877b5 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions") -__version__ = "0.3.0" +__version__ = "0.3.1" __author__ = "Valetov Konstantin" from .message import SnmpV2TrapMessage From 7671f1ab9e19f8178e548897ebfddf93101e4c51 Mon Sep 17 00:00:00 2001 From: mzsombor <38425339+mzsombor@users.noreply.github.com> Date: Sun, 21 Mar 2021 10:02:58 +0200 Subject: [PATCH 04/23] added the ability to specify local_addr (#24) Resolves #25 --- aiosnmp/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aiosnmp/connection.py b/aiosnmp/connection.py index 99cde28..8f480fa 100644 --- a/aiosnmp/connection.py +++ b/aiosnmp/connection.py @@ -1,7 +1,7 @@ __all__ = ("SnmpConnection",) import asyncio -from typing import Optional, cast +from typing import Optional, Tuple, cast from .protocols import Address, SnmpProtocol @@ -16,6 +16,7 @@ class SnmpConnection: "_peername", "host", "port", + "local_addr", "loop", "timeout", "retries", @@ -29,6 +30,7 @@ def __init__( port: int = 161, timeout: float = DEFAULT_TIMEOUT, retries: int = DEFAULT_RETRIES, + local_addr: Optional[Tuple[str, int]] = None ) -> None: self.host: str = host self.port: int = port @@ -39,11 +41,13 @@ def __init__( self.timeout: float = timeout self.retries: int = retries self._closed: bool = False + self.local_addr: Optional[Tuple[str, int]] = local_addr async def _connect(self) -> None: connect_future = self.loop.create_datagram_endpoint( lambda: SnmpProtocol(self.timeout, self.retries), remote_addr=(self.host, self.port), + local_addr=self.local_addr, ) transport, protocol = await asyncio.wait_for(connect_future, timeout=self.timeout) From 6342e29c22d02f76d0f71e073ee097748fd496a5 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 15 May 2021 20:11:58 +0300 Subject: [PATCH 05/23] added the ability to disable validation source addr --- README.md | 5 +++++ aiosnmp/connection.py | 7 +++++-- aiosnmp/protocols.py | 16 +++++++++++----- tests/test_snmp.py | 7 +++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fb1f162..9db13da 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ pip install aiosnmp Only snmp v2c supported, v3 version is not supported Oids should be like `.1.3.6...` or `1.3.6...`. `iso.3.6...` is not supported + +## Source address (host and port) validation +By default, v2c should not validate source addr, but in this library, it is enabled by default. +You can disable validation by passing `validate_source_addr=False` to `Snmp`. + ## Basic Usage ```python import asyncio diff --git a/aiosnmp/connection.py b/aiosnmp/connection.py index 8f480fa..e2d61c1 100644 --- a/aiosnmp/connection.py +++ b/aiosnmp/connection.py @@ -21,6 +21,7 @@ class SnmpConnection: "timeout", "retries", "_closed", + "validate_source_addr", ) def __init__( @@ -30,7 +31,8 @@ def __init__( port: int = 161, timeout: float = DEFAULT_TIMEOUT, retries: int = DEFAULT_RETRIES, - local_addr: Optional[Tuple[str, int]] = None + local_addr: Optional[Tuple[str, int]] = None, + validate_source_addr: bool = True, ) -> None: self.host: str = host self.port: int = port @@ -42,10 +44,11 @@ def __init__( self.retries: int = retries self._closed: bool = False self.local_addr: Optional[Tuple[str, int]] = local_addr + self.validate_source_addr: bool = validate_source_addr async def _connect(self) -> None: connect_future = self.loop.create_datagram_endpoint( - lambda: SnmpProtocol(self.timeout, self.retries), + lambda: SnmpProtocol(self.timeout, self.retries, self.validate_source_addr), remote_addr=(self.host, self.port), local_addr=self.local_addr, ) diff --git a/aiosnmp/protocols.py b/aiosnmp/protocols.py index 4d232f9..7300f8f 100644 --- a/aiosnmp/protocols.py +++ b/aiosnmp/protocols.py @@ -48,6 +48,7 @@ } Address = Union[Tuple[str, int], Tuple[str, int, int, int]] +RequestsKey = Union[Tuple[str, int, int], int] class SnmpTrapProtocol(asyncio.DatagramProtocol): @@ -80,13 +81,14 @@ def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: class SnmpProtocol(asyncio.DatagramProtocol): - __slots__ = ("loop", "transport", "requests", "timeout", "retries") + __slots__ = ("loop", "transport", "requests", "timeout", "retries", "validate_source_addr") - def __init__(self, timeout: float, retries: int) -> None: + def __init__(self, timeout: float, retries: int, validate_source_addr: bool) -> None: self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() - self.requests: Dict[Tuple[str, int, int], asyncio.Future] = {} + self.requests: Dict[RequestsKey, asyncio.Future] = {} self.timeout: float = timeout self.retries: int = retries + self.validate_source_addr: bool = validate_source_addr def connection_made(self, transport: asyncio.BaseTransport) -> None: self.transport = cast(asyncio.DatagramTransport, transport) @@ -104,7 +106,9 @@ def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: logger.warning(f"could not decode received data from {host}:{port}: {exc}") return - key = (host, port, message.data.request_id) + key: RequestsKey = ( + (host, port, message.data.request_id) if self.validate_source_addr else message.data.request_id + ) if key in self.requests: exception: Optional[Exception] = None if isinstance(message.data, PDU) and message.data.error_status != 0: @@ -126,7 +130,9 @@ def is_connected(self) -> bool: return bool(self.transport is not None and not self.transport.is_closing()) async def _send(self, message: SnmpMessage, host: str, port: int) -> List[SnmpVarbind]: - key = (host, port, message.data.request_id) + key: RequestsKey = ( + (host, port, message.data.request_id) if self.validate_source_addr else message.data.request_id + ) fut: asyncio.Future = self.loop.create_future() fut.add_done_callback(lambda fn: self.requests.pop(key) if key in self.requests else None) self.requests[key] = fut diff --git a/tests/test_snmp.py b/tests/test_snmp.py index ec0807b..ebe1692 100644 --- a/tests/test_snmp.py +++ b/tests/test_snmp.py @@ -235,3 +235,10 @@ async def test_snmp_deprecated_context(host: str, port: int) -> None: with pytest.warns(FutureWarning): with Snmp(host=host, port=port) as snmp: await snmp.get(".1.3.6.1.2.1.1.6.0") + + +@pytest.mark.asyncio +async def test_snmp_disable_validation_source_addr(host: str, port: int) -> None: + async with Snmp(host=host, port=port, validate_source_addr=False) as snmp: + results = await snmp.get(".1.3.6.1.2.1.1.6.0.12312") + assert len(results) == 1 From 41ed31b44ddae5b9ead789222b2add46c8cf295d Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 15 May 2021 20:15:02 +0300 Subject: [PATCH 06/23] bump and freeze dev and test libraries --- azure-pipelines.yml | 14 +++++++------- tox.ini | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 897f739..91d807f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,19 +23,19 @@ jobs: versionSpec: '3.9' architecture: 'x64' - - script: pip install mypy isort flake8 black + - script: pip install mypy==0.812 isort==5.8.0 flake8==3.9.2 black==21.5b1 displayName: 'Install Dependencies' - - script: flake8 aiosnmp tests examples + - script: flake8 aiosnmp/ tests/ examples/ setup.py displayName: 'Run flake8' - - script: isort -q --check --diff aiosnmp tests examples + - script: isort -q --check --diff aiosnmp/ tests/ examples/ setup.py displayName: 'Run isort' - - script: black -l 120 -q --check --diff aiosnmp tests examples + - script: black -l 120 -q --check --diff aiosnmp/ tests/ examples/ setup.py displayName: 'Run black' - - script: mypy aiosnmp + - script: mypy aiosnmp/ displayName: 'Run mypy' - job: tests @@ -82,10 +82,10 @@ jobs: - script: env displayName: 'Env' - - script: pip install pytest-xdist pytest-asyncio pytest-cov uvloop codecov + - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.15.2 codecov==2.1.11 displayName: 'Install Dependencies' - - script: pytest -n 1 --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=$(loop) + - script: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=$(loop) displayName: 'Run Tests' - script: bash <(curl -s https://codecov.io/bash) diff --git a/tox.ini b/tox.ini index 811802b..80e3069 100644 --- a/tox.ini +++ b/tox.ini @@ -3,23 +3,22 @@ envlist = check, py{36,37,38,39}-{asyncio,uvloop} [testenv] deps = - pytest == 6.2.1 - pytest-xdist == 2.2.0 - pytest-asyncio == 0.14.0 - pytest-cov == 2.11.1 - uvloop: uvloop == 0.14.0 + pytest == 6.2.4 + pytest-asyncio == 0.15.1 + pytest-cov == 2.12.0 + uvloop: uvloop == 0.15.2 commands = - asyncio: pytest -n 1 --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} - uvloop: pytest -n 1 --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} + asyncio: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} + uvloop: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} docker = koshh/aiosnmp:latest [testenv:check] deps = - flake8 == 3.8.4 - isort == 5.7.0 - black == 20.8b1 - mypy == 0.800 + flake8 == 3.9.2 + isort == 5.8.0 + black == 21.5b1 + mypy == 0.812 commands = flake8 aiosnmp/ tests/ examples/ setup.py isort -q --check --diff aiosnmp/ tests/ examples/ setup.py @@ -30,8 +29,8 @@ skip_install = true [testenv:format] deps = - isort == 5.7.0 - black == 20.8b1 + isort == 5.8.0 + black == 21.5b1 commands = isort aiosnmp/ tests/ examples/ setup.py black -l 120 aiosnmp/ tests/ examples/ setup.py From ca4b44fca2dc4332a32fc95dc55a25a064dada52 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 15 May 2021 20:44:57 +0300 Subject: [PATCH 07/23] removed python3.6 support --- README.md | 2 +- azure-pipelines.yml | 6 ------ setup.py | 3 +-- tests/__init__.py | 0 tox.ini | 2 +- 5 files changed, 3 insertions(+), 10 deletions(-) create mode 100644 tests/__init__.py diff --git a/README.md b/README.md index 9db13da..5b6499a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI version](https://badge.fury.io/py/aiosnmp.svg)](https://badge.fury.io/py/aiosnmp) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://img.shields.io/badge/license-MIT-brightgreen.svg) [![Code Style](https://img.shields.io/badge/code%20style-black-black.svg)](https://github.com/ambv/black) -[![Python version](https://img.shields.io/badge/python-3.6%2B-brightgreen.svg)](https://img.shields.io/badge/python-3.6%2B-brightgreen.svg) +[![Python version](https://img.shields.io/badge/python-3.7%2B-brightgreen.svg)](https://img.shields.io/badge/python-3.7%2B-brightgreen.svg) aiosnmp is an asynchronous SNMP client for use with asyncio. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 91d807f..e8d4c82 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -44,12 +44,6 @@ jobs: strategy: matrix: - Python36-asyncio: - python.version: '3.6' - loop: 'asyncio' - Python36-uvloop: - python.version: '3.6' - loop: 'uvloop' Python37-asyncio: python.version: '3.7' loop: 'asyncio' diff --git a/setup.py b/setup.py index 3073252..d9fd64a 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -32,5 +31,5 @@ "Operating System :: MacOS :: MacOS X", "Framework :: AsyncIO", ], - python_requires=">=3.6", + python_requires=">=3.7", ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index 80e3069..5f5fd06 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = check, py{36,37,38,39}-{asyncio,uvloop} +envlist = check, py{37,38,39}-{asyncio,uvloop} [testenv] deps = From 166a2c6a2eed2aea30eb1afe203159f820300cd7 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 15 May 2021 20:25:50 +0300 Subject: [PATCH 08/23] 0.4.0 --- aiosnmp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index b3877b5..70b717a 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions") -__version__ = "0.3.1" +__version__ = "0.4.0" __author__ = "Valetov Konstantin" from .message import SnmpV2TrapMessage From 2838f03826181c31a7a33175c971f71aecb47c5a Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 22 May 2021 13:35:24 +0300 Subject: [PATCH 09/23] do not bind in datagram endpoint --- aiosnmp/connection.py | 11 ++++++----- aiosnmp/protocols.py | 11 +++++++---- aiosnmp/snmp.py | 4 ++-- tests/test_connection.py | 8 +++----- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/aiosnmp/connection.py b/aiosnmp/connection.py index e2d61c1..77bc56d 100644 --- a/aiosnmp/connection.py +++ b/aiosnmp/connection.py @@ -13,7 +13,7 @@ class SnmpConnection: __slots__ = ( "_protocol", "_transport", - "_peername", + "_sockaddr", "host", "port", "local_addr", @@ -39,7 +39,7 @@ def __init__( self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() self._protocol: Optional[SnmpProtocol] = None self._transport: Optional[asyncio.DatagramTransport] = None - self._peername: Optional[Address] = None + self._sockaddr: Optional[Address] = None self.timeout: float = timeout self.retries: int = retries self._closed: bool = False @@ -47,16 +47,17 @@ def __init__( self.validate_source_addr: bool = validate_source_addr async def _connect(self) -> None: + gai = await self.loop.getaddrinfo(self.host, self.port) + address_family, *_, self._sockaddr = gai[0] connect_future = self.loop.create_datagram_endpoint( lambda: SnmpProtocol(self.timeout, self.retries, self.validate_source_addr), - remote_addr=(self.host, self.port), local_addr=self.local_addr, + family=address_family, ) transport, protocol = await asyncio.wait_for(connect_future, timeout=self.timeout) self._protocol = cast(SnmpProtocol, protocol) self._transport = cast(asyncio.DatagramTransport, transport) - self._peername = self._transport.get_extra_info("peername", default=(self.host, self.port)) @property def is_closed(self) -> bool: @@ -72,5 +73,5 @@ def close(self) -> None: self._protocol = None self._transport = None - self._peername = None + self._sockaddr = None self._closed = True diff --git a/aiosnmp/protocols.py b/aiosnmp/protocols.py index 7300f8f..7bf9518 100644 --- a/aiosnmp/protocols.py +++ b/aiosnmp/protocols.py @@ -63,7 +63,7 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None: self.transport = cast(asyncio.DatagramTransport, transport) def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: - host, port = addr[:2] + host, port = addr[0], addr[1] if isinstance(data, Text): logger.warning(f"received data from {host}:{port} should be bytes") @@ -94,7 +94,7 @@ def connection_made(self, transport: asyncio.BaseTransport) -> None: self.transport = cast(asyncio.DatagramTransport, transport) def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: - host, port = addr[:2] + host, port = addr[0], addr[1] if isinstance(data, Text): logger.warning(f"received data from {host}:{port} should be bytes") @@ -129,7 +129,8 @@ def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: def is_connected(self) -> bool: return bool(self.transport is not None and not self.transport.is_closing()) - async def _send(self, message: SnmpMessage, host: str, port: int) -> List[SnmpVarbind]: + async def _send(self, message: SnmpMessage, addr: Address) -> List[SnmpVarbind]: + host, port = addr[0], addr[1] key: RequestsKey = ( (host, port, message.data.request_id) if self.validate_source_addr else message.data.request_id ) @@ -137,11 +138,13 @@ async def _send(self, message: SnmpMessage, host: str, port: int) -> List[SnmpVa fut.add_done_callback(lambda fn: self.requests.pop(key) if key in self.requests else None) self.requests[key] = fut for _ in range(self.retries): - self.transport.sendto(message.encode()) + self.transport.sendto(message.encode(), addr) done, _ = await asyncio.wait({fut}, timeout=self.timeout, return_when=asyncio.ALL_COMPLETED) if not done: continue + r: List[SnmpVarbind] = fut.result() return r + fut.cancel() raise SnmpTimeoutError diff --git a/aiosnmp/snmp.py b/aiosnmp/snmp.py index bbde02b..c7747e0 100644 --- a/aiosnmp/snmp.py +++ b/aiosnmp/snmp.py @@ -63,8 +63,8 @@ async def _send(self, message: SnmpMessage) -> List[SnmpVarbind]: if self._protocol is None: raise Exception("Connection is closed") - assert self._peername - return await self._protocol._send(message, *self._peername[:2]) + assert self._sockaddr + return await self._protocol._send(message, self._sockaddr) async def get(self, oids: Union[str, List[str]]) -> List[SnmpVarbind]: if isinstance(oids, str): diff --git a/tests/test_connection.py b/tests/test_connection.py index b30a2bf..c933271 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -9,13 +9,11 @@ async def test_connection_close(host: str, port: int) -> None: await snmp.get(".1.3.6.1.2.1.1.6.0") assert snmp._transport assert snmp._protocol - assert snmp._peername + assert snmp._sockaddr assert snmp._transport is None assert snmp._protocol is None - assert snmp._peername is None + assert snmp._sockaddr is None - with pytest.raises(Exception) as exc_info: + with pytest.raises(Exception, match="Connection is closed"): await snmp.get(".1.3.6.1.2.1.1.6.0") - - assert str(exc_info.value) == "Connection is closed" From 57b42be4bb15bab9cddf6da2c9fdcec38544c355 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 22 May 2021 20:44:02 +0300 Subject: [PATCH 10/23] added documentation --- README.md | 58 ------------------------ README.rst | 103 ++++++++++++++++++++++++++++++++++++++++++ aiosnmp/__init__.py | 4 +- aiosnmp/exceptions.py | 64 +++++++++++++++++++++----- aiosnmp/message.py | 35 +++++++++++--- aiosnmp/protocols.py | 2 +- aiosnmp/snmp.py | 81 +++++++++++++++++++++++++++++++++ docs/Makefile | 20 ++++++++ docs/api.rst | 36 +++++++++++++++ docs/conf.py | 59 ++++++++++++++++++++++++ docs/index.rst | 10 ++++ docs/make.bat | 35 ++++++++++++++ requirements-docs.txt | 2 + setup.py | 4 +- 14 files changed, 433 insertions(+), 80 deletions(-) delete mode 100644 README.md create mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 requirements-docs.txt diff --git a/README.md b/README.md deleted file mode 100644 index 5b6499a..0000000 --- a/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# aiosnmp -[![Build Status](https://dev.azure.com/6660879/aiosnmp/_apis/build/status/hh-h.aiosnmp?branchName=master)](https://dev.azure.com/6660879/aiosnmp/_build/results?buildId=38&view=results) -[![Code Coverage](https://img.shields.io/codecov/c/github/hh-h/aiosnmp/master.svg?style=flat)](https://codecov.io/github/hh-h/aiosnmp?branch=master) -[![PyPI version](https://badge.fury.io/py/aiosnmp.svg)](https://badge.fury.io/py/aiosnmp) -[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://img.shields.io/badge/license-MIT-brightgreen.svg) -[![Code Style](https://img.shields.io/badge/code%20style-black-black.svg)](https://github.com/ambv/black) -[![Python version](https://img.shields.io/badge/python-3.7%2B-brightgreen.svg)](https://img.shields.io/badge/python-3.7%2B-brightgreen.svg) - -aiosnmp is an asynchronous SNMP client for use with asyncio. - -## Installation -```shell -pip install aiosnmp -``` - -## Notice -Only snmp v2c supported, v3 version is not supported -Oids should be like `.1.3.6...` or `1.3.6...`. `iso.3.6...` is not supported - - -## Source address (host and port) validation -By default, v2c should not validate source addr, but in this library, it is enabled by default. -You can disable validation by passing `validate_source_addr=False` to `Snmp`. - -## Basic Usage -```python -import asyncio -import aiosnmp - -async def main(): - async with aiosnmp.Snmp(host="127.0.0.1", port=161, community="public") as snmp: - for res in await snmp.get(".1.3.6.1.2.1.1.1.0"): - print(res.oid, res.value) - -asyncio.run(main()) -``` - -more in [**/examples**](https://github.com/hh-h/aiosnmp/tree/master/examples) - -## TODO -* documentation -* snmp v3 support -* more tests - -## License -aiosnmp is developed and distributed under the MIT license. - -## Run local tests -```shell -pip install -r requirements-dev.txt -tox -``` - -## Before submitting PR -```shell -pip install -r requirements-dev.txt -tox -e format -``` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2047f8b --- /dev/null +++ b/README.rst @@ -0,0 +1,103 @@ +aiosnmp +======= + + +.. image:: https://dev.azure.com/6660879/aiosnmp/_apis/build/status/hh-h.aiosnmp?branchName=master + :target: https://dev.azure.com/6660879/aiosnmp/_build/results?buildId=38&view=results + :alt: Build Status + + +.. image:: https://img.shields.io/codecov/c/github/hh-h/aiosnmp/master.svg?style=flat + :target: https://codecov.io/github/hh-h/aiosnmp?branch=master + :alt: Code Coverage + + +.. image:: https://badge.fury.io/py/aiosnmp.svg + :target: https://badge.fury.io/py/aiosnmp + :alt: PyPI version + + +.. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg + :target: https://img.shields.io/badge/license-MIT-brightgreen.svg + :alt: License + + +.. image:: https://img.shields.io/badge/code%20style-black-black.svg + :target: https://github.com/ambv/black + :alt: Code Style + + +.. image:: https://img.shields.io/badge/python-3.7%2B-brightgreen.svg + :target: https://img.shields.io/badge/python-3.7%2B-brightgreen.svg + :alt: Python version + + +aiosnmp is an asynchronous SNMP client for use with asyncio. + +Installation +------------ + +.. code-block:: shell + + pip install aiosnmp + +Documentation +------------- + +https://aiosnmp.readthedocs.io/en/latest/api.html + +Notice +------ + +| Only snmp v2c supported, v3 version is not supported +| Oids should be like ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + +Source address (host and port) validation +----------------------------------------- + +By default, v2c should not validate source addr, but in this library, it is enabled by default. +You can disable validation by passing ``validate_source_addr=False`` to ``Snmp``. + +Basic Usage +----------- + +.. code-block:: python + + import asyncio + import aiosnmp + + async def main(): + async with aiosnmp.Snmp(host="127.0.0.1", port=161, community="public") as snmp: + for res in await snmp.get(".1.3.6.1.2.1.1.1.0"): + print(res.oid, res.value) + + asyncio.run(main()) + +more in `/examples `_ + +TODO +---- + +* snmp v3 support +* more tests + +License +------- + +aiosnmp is developed and distributed under the MIT license. + +Run local tests +--------------- + +.. code-block:: shell + + pip install -r requirements-dev.txt + tox + +Before submitting PR +-------------------- + +.. code-block:: shell + + pip install -r requirements-dev.txt + tox -e format diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 70b717a..150d305 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,7 +1,7 @@ -__all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions") +__all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind") __version__ = "0.4.0" __author__ = "Valetov Konstantin" -from .message import SnmpV2TrapMessage +from .message import SnmpV2TrapMessage, SnmpVarbind from .snmp import Snmp from .trap import SnmpV2TrapServer diff --git a/aiosnmp/exceptions.py b/aiosnmp/exceptions.py index dca5ae3..d4e661c 100644 --- a/aiosnmp/exceptions.py +++ b/aiosnmp/exceptions.py @@ -26,18 +26,20 @@ class SnmpException(Exception): - pass + """Base class for exceptions""" class SnmpTimeoutError(SnmpException, TimeoutError): - pass + """The operation exceeded the given deadline""" class SnmpUnsupportedValueType(SnmpException): - pass + """Provided value type is unsupported""" class SnmpErrorStatus(SnmpException): + """Base class for SNMP errors""" + message = "" def __init__(self, index: int, oid: Optional[str] = None) -> None: @@ -49,20 +51,29 @@ def __init__(self, index: int, oid: Optional[str] = None) -> None: class SnmpErrorTooBig(SnmpErrorStatus): - message = "The agent could not place the results " "of the requested SNMP operation in a single SNMP message." + """The agent could not place the results of the requested SNMP operation in a single SNMP message.""" + + message = "The agent could not place the results of the requested SNMP operation in a single SNMP message." class SnmpErrorNoSuchName(SnmpErrorStatus): + """The requested SNMP operation identified an unknown variable.""" + message = "The requested SNMP operation identified an unknown variable." class SnmpErrorBadValue(SnmpErrorStatus): - message = ( - "The requested SNMP operation tried to change a variable " "but it specified either a syntax or value error." - ) + """The requested SNMP operation tried to change a variable but it specified either a syntax or value error.""" + + message = "The requested SNMP operation tried to change a variable but it specified either a syntax or value error." class SnmpErrorReadOnly(SnmpErrorStatus): + """ + The requested SNMP operation tried to change a variable that was not allowed to change, + according to the community profile of the variable. + """ + message = ( "The requested SNMP operation tried to change a variable " "that was not allowed to change, " @@ -71,22 +82,33 @@ class SnmpErrorReadOnly(SnmpErrorStatus): class SnmpErrorGenErr(SnmpErrorStatus): - message = "An error other than one of those listed here " "occurred during the requested SNMP operation." + """An error other than one of those listed here occurred during the requested SNMP operation.s""" + + message = "An error other than one of those listed here occurred during the requested SNMP operation." class SnmpErrorNoAccess(SnmpErrorStatus): + """The specified SNMP variable is not accessible.""" + message = "The specified SNMP variable is not accessible." class SnmpErrorWrongType(SnmpErrorStatus): - message = "The value specifies a type that is inconsistent " "with the type required for the variable." + """The value specifies a type that is inconsistent with the type required for the variable.""" + + message = "The value specifies a type that is inconsistent with the type required for the variable." class SnmpErrorWrongLength(SnmpErrorStatus): - message = "The value specifies a length that is inconsistent " "with the length required for the variable." + """The value specifies a length that is inconsistent with the length required for the variable.""" + + message = "The value specifies a length that is inconsistent with the length required for the variable." class SnmpErrorWrongEncoding(SnmpErrorStatus): + """The value contains an Abstract Syntax Notation One (ASN.1) encoding + that is inconsistent with the ASN.1 tag of the field.""" + message = ( "The value contains an Abstract Syntax Notation One (ASN.1) encoding " "that is inconsistent with the ASN.1 tag of the field." @@ -94,26 +116,39 @@ class SnmpErrorWrongEncoding(SnmpErrorStatus): class SnmpErrorWrongValue(SnmpErrorStatus): + """The value cannot be assigned to the variable.""" + message = "The value cannot be assigned to the variable." class SnmpErrorNoCreation(SnmpErrorStatus): + """The variable does not exist, and the agent cannot create it.""" + message = "The variable does not exist, and the agent cannot create it." class SnmpErrorInconsistentValue(SnmpErrorStatus): + """The value is inconsistent with values of other managed objects.""" + message = "The value is inconsistent with values of other managed objects." class SnmpErrorResourceUnavailable(SnmpErrorStatus): - message = "Assigning the value to the variable requires allocation of resources " "that are currently unavailable." + """Assigning the value to the variable requires allocation of resources that are currently unavailable.""" + + message = "Assigning the value to the variable requires allocation of resources that are currently unavailable." class SnmpErrorCommitFailed(SnmpErrorStatus): + """No validation errors occurred, but no variables were updated.""" + message = "No validation errors occurred, but no variables were updated." class SnmpErrorUndoFailed(SnmpErrorStatus): + """No validation errors occurred. Some variables were updated + because it was not possible to undo their assignment.""" + message = ( "No validation errors occurred. Some variables were updated " "because it was not possible to undo their assignment." @@ -121,14 +156,21 @@ class SnmpErrorUndoFailed(SnmpErrorStatus): class SnmpErrorAuthorizationError(SnmpErrorStatus): + """An authorization error occurred.""" + message = "An authorization error occurred." class SnmpErrorNotWritable(SnmpErrorStatus): + """The variable exists but the agent cannot modify it.""" + message = "The variable exists but the agent cannot modify it." class SnmpErrorInconsistentName(SnmpErrorStatus): + """The variable does not exist; the agent cannot create it because + the named object instance is inconsistent with the values of other managed objects.""" + message = ( "The variable does not exist; " "the agent cannot create it because the named object instance " diff --git a/aiosnmp/message.py b/aiosnmp/message.py index 030d941..ddef503 100644 --- a/aiosnmp/message.py +++ b/aiosnmp/message.py @@ -38,7 +38,7 @@ class PDUType(enum.IntEnum): class SnmpVarbind: - __slots__ = ("_oid", "value") + __slots__ = ("_oid", "_value") def __init__( self, @@ -46,12 +46,20 @@ def __init__( value: Union[None, str, int, bytes, ipaddress.IPv4Address] = None, ) -> None: self._oid: str = oid.lstrip(".") - self.value: Union[None, str, int, bytes, ipaddress.IPv4Address] = value + self._value: Union[None, str, int, bytes, ipaddress.IPv4Address] = value @property def oid(self) -> str: + """This property stores oid of the message""" + return f".{self._oid}" + @property + def value(self) -> Union[None, str, int, bytes, ipaddress.IPv4Address]: + """This property stores value of the message""" + + return self._value + def encode(self, encoder: Encoder) -> None: with encoder.enter(Number.Sequence): encoder.write(self._oid, Number.ObjectIdentifier) @@ -183,12 +191,27 @@ def decode(cls, data: bytes) -> "SnmpResponse": class SnmpV2TrapMessage: - __slots__ = ("version", "community", "data") + __slots__ = ("_version", "_community", "_data") def __init__(self, version: SnmpVersion, community: str, data: PDU) -> None: - self.version: SnmpVersion = version - self.community: str = community - self.data: PDU = data + self._version: SnmpVersion = version + self._community: str = community + self._data: PDU = data + + @property + def version(self) -> SnmpVersion: + """Returns version of the message""" + return self._version + + @property + def community(self) -> str: + """Returns community of the message""" + return self._community + + @property + def data(self) -> PDU: + """Returns :class:`protocol data unit ` of the message""" + return self._data @classmethod def decode(cls, data: bytes) -> Optional["SnmpV2TrapMessage"]: diff --git a/aiosnmp/protocols.py b/aiosnmp/protocols.py index 7bf9518..8f8c1f9 100644 --- a/aiosnmp/protocols.py +++ b/aiosnmp/protocols.py @@ -75,7 +75,7 @@ def datagram_received(self, data: Union[bytes, Text], addr: Address) -> None: logger.warning(f"could not decode received data from {host}:{port}: {exc}") return - if not message or (self.communities and message.community not in self.communities): + if not message or (self.communities and message._community not in self.communities): return asyncio.ensure_future(self.handler(host, port, message)) diff --git a/aiosnmp/snmp.py b/aiosnmp/snmp.py index c7747e0..dc2c6d6 100644 --- a/aiosnmp/snmp.py +++ b/aiosnmp/snmp.py @@ -11,6 +11,29 @@ class Snmp(SnmpConnection): + """This is class for initializing Snmp interface. + + :param str host: host where to connect to + :param int port: port where to connect to, default: `161` + :param SnmpVersion version: SNMP protocol version, only v2c supported now + :param str community: SNMP community, default: `public` + :param float timeout: timeout for one SNMP request/response, default `1` + :param int retries: set the number of retries to attempt, default `6` + :param int non_repeaters: sets the get_bulk max-repeaters used by bulk_walk, default `0` + :param int max_repetitions: sets the get_bulk max-repetitions used by bulk_walk, default `10` + :param Tuple[str,int] local_addr: tuple used to bind the socket locally, default getaddrinfo() + :param bool validate_source_addr: verify that the packets came from the same source they were sent to + default `True` + + Must be used with ``async with`` + + .. code-block:: python + + async with aiosnmp.Snmp(host="127.0.0.1", port=161, community="public") as snmp: + ... + + """ + __slots__ = ("version", "community", "non_repeaters", "max_repetitions") def __init__( @@ -67,12 +90,31 @@ async def _send(self, message: SnmpMessage) -> List[SnmpVarbind]: return await self._protocol._send(message, self._sockaddr) async def get(self, oids: Union[str, List[str]]) -> List[SnmpVarbind]: + """The get method is used to retrieve one or more values from SNMP agent. + + :param oids: oid or list of oids, ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + :return: list of :class:`SnmpVarbind ` + + Example + + .. code-block:: python + + async with aiosnmp.Snmp(host="127.0.0.1", port=161, community="public") as snmp: + for res in await snmp.get(".1.3.6.1.2.1.1.1.0"): + print(res.oid, res.value) + + """ if isinstance(oids, str): oids = [oids] message = SnmpMessage(self.version, self.community, GetRequest([SnmpVarbind(oid) for oid in oids])) return await self._send(message) async def get_next(self, oids: Union[str, List[str]]) -> List[SnmpVarbind]: + """The get_next method retrieves the value of the next OID in the tree. + + :param oids: oid or list of oids, ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + :return: list of :class:`SnmpVarbind ` + """ if isinstance(oids, str): oids = [oids] message = SnmpMessage( @@ -89,6 +131,15 @@ async def get_bulk( non_repeaters: Optional[int] = None, max_repetitions: Optional[int] = None, ) -> List[SnmpVarbind]: + """The get_bulk method performs a continuous get_next operation based on the max_repetitions value. + The non_repeaters value determines the number of variables in the + variable list for which a simple get_next operation has to be done. + + :param oids: oid or list of oids, ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + :param non_repeaters: overwrite non_repeaters of :class:`Snmp ` + :param max_repetitions: overwrite max_repetitions of :class:`Snmp ` + :return: list of :class:`SnmpVarbind ` + """ if isinstance(oids, str): oids = [oids] nr: int = self.non_repeaters if non_repeaters is None else non_repeaters @@ -101,6 +152,11 @@ async def get_bulk( return await self._send(message) async def walk(self, oid: str) -> List[SnmpVarbind]: + """The walk method uses get_next requests to query a network entity for a tree of information. + + :param oid: oid, ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + :return: list of :class:`SnmpVarbind ` + """ varbinds: List[SnmpVarbind] = [] message = SnmpMessage(self.version, self.community, GetNextRequest([SnmpVarbind(oid)])) base_oid = oid if oid.startswith(".") else f".{oid}" @@ -121,6 +177,23 @@ async def walk(self, oid: str) -> List[SnmpVarbind]: return varbinds async def set(self, varbinds: List[Tuple[str, Union[int, str, bytes, ipaddress.IPv4Address]]]) -> List[SnmpVarbind]: + """The set method is used to modify the value(s) of the managed object. + + :param varbinds: list of tuples[oid, int/str/bytes/ipv4] + :return: list of :class:`SnmpVarbind ` + + Example + + .. code-block:: python + + async with aiosnmp.Snmp(host="127.0.0.1", port=161, community="private") as snmp: + for res in await snmp.set([ + (".1.3.6.1.2.1.1.1.0", 10), + (".1.3.6.1.2.1.1.1.1", "hello"), + ]): + print(res.oid, res.value) + + """ for varbind in varbinds: if not isinstance(varbind[1], (int, str, bytes, ipaddress.IPv4Address)): raise SnmpUnsupportedValueType(f"Only int, str, bytes and ip address supported, got {type(varbind[1])}") @@ -138,6 +211,14 @@ async def bulk_walk( non_repeaters: Optional[int] = None, max_repetitions: Optional[int] = None, ) -> List[SnmpVarbind]: + """The bulk_walk method uses get_bulk requests to query a network entity efficiently for a tree of information. + + :param oid: oid, ``.1.3.6...`` or ``1.3.6...``. ``iso.3.6...`` is not supported + :param non_repeaters: overwrite non_repeaters of :class:`Snmp ` + :param max_repetitions: overwrite max_repetitions of :class:`Snmp ` + :return: list of :class:`SnmpVarbind ` + + """ nr: int = self.non_repeaters if non_repeaters is None else non_repeaters mr: int = self.max_repetitions if max_repetitions is None else max_repetitions base_oid: str = oid if oid.startswith(".") else f".{oid}" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..b61307a --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,36 @@ +API Reference +============= + +Classes +^^^^^^^ + +.. autoclass:: aiosnmp.snmp.Snmp + :members: get, get_next, get_bulk, walk, set, bulk_walk +.. autoclass:: aiosnmp.message.SnmpVarbind() + :members: oid, value +.. autoclass:: aiosnmp.message.SnmpV2TrapMessage() + :members: version, community, data + +Exceptions +^^^^^^^^^^ + +.. autoexception:: aiosnmp.exceptions.SnmpTimeoutError() +.. autoexception:: aiosnmp.exceptions.SnmpUnsupportedValueType() +.. autoexception:: aiosnmp.exceptions.SnmpErrorTooBig() +.. autoexception:: aiosnmp.exceptions.SnmpErrorNoSuchName() +.. autoexception:: aiosnmp.exceptions.SnmpErrorBadValue() +.. autoexception:: aiosnmp.exceptions.SnmpErrorReadOnly() +.. autoexception:: aiosnmp.exceptions.SnmpErrorGenErr() +.. autoexception:: aiosnmp.exceptions.SnmpErrorNoAccess() +.. autoexception:: aiosnmp.exceptions.SnmpErrorWrongType() +.. autoexception:: aiosnmp.exceptions.SnmpErrorWrongLength() +.. autoexception:: aiosnmp.exceptions.SnmpErrorWrongEncoding() +.. autoexception:: aiosnmp.exceptions.SnmpErrorWrongValue() +.. autoexception:: aiosnmp.exceptions.SnmpErrorNoCreation() +.. autoexception:: aiosnmp.exceptions.SnmpErrorInconsistentValue() +.. autoexception:: aiosnmp.exceptions.SnmpErrorResourceUnavailable() +.. autoexception:: aiosnmp.exceptions.SnmpErrorCommitFailed() +.. autoexception:: aiosnmp.exceptions.SnmpErrorUndoFailed() +.. autoexception:: aiosnmp.exceptions.SnmpErrorAuthorizationError() +.. autoexception:: aiosnmp.exceptions.SnmpErrorNotWritable() +.. autoexception:: aiosnmp.exceptions.SnmpErrorInconsistentName() diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..2ff2f99 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'aiosnmp' +copyright = '2021, Valetov Konstantin' +author = 'Valetov Konstantin' + +# The full version, including alpha/beta/rc tags +release = '0.4.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +add_module_names = False +autodoc_member_order = 'bysource' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c07045b --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,10 @@ +.. include:: ../README.rst + +====================== +Documentation contents +====================== + +.. toctree:: + :maxdepth: 2 + + api diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..8c7b1fd --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,2 @@ +Sphinx==4.0.2 +sphinx-rtd-theme==0.5.2 diff --git a/setup.py b/setup.py index d9fd64a..be78ae2 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import aiosnmp -readme = Path(__file__).with_name("README.md") +readme = Path(__file__).with_name("README.rst") setup( name="aiosnmp", @@ -16,7 +16,7 @@ author_email="forjob@thetrue.name", description="asyncio SNMP client", long_description=readme.read_text("utf-8"), - long_description_content_type="text/markdown", + long_description_content_type="text/x-rst", setup_requires=["pytest-runner"], tests_require=["pytest"], classifiers=[ From 1d03437c7b61be0f803bc95953529e9ebfef3602 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 22 May 2021 20:54:23 +0300 Subject: [PATCH 11/23] removed not async context manager --- aiosnmp/snmp.py | 14 -------------- tests/test_snmp.py | 7 ------- 2 files changed, 21 deletions(-) diff --git a/aiosnmp/snmp.py b/aiosnmp/snmp.py index dc2c6d6..2df8d37 100644 --- a/aiosnmp/snmp.py +++ b/aiosnmp/snmp.py @@ -1,7 +1,6 @@ __all__ = ("Snmp",) import ipaddress -import warnings from types import TracebackType from typing import Any, List, Optional, Tuple, Type, Union @@ -51,19 +50,6 @@ def __init__( self.non_repeaters: int = non_repeaters self.max_repetitions: int = max_repetitions - def __enter__(self) -> "Snmp": - warnings.warn("Use async with, this is deprecated", FutureWarning) - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self.close() - return None - async def __aenter__(self) -> "Snmp": if not self.is_connected: await self._connect() diff --git a/tests/test_snmp.py b/tests/test_snmp.py index ebe1692..cb67253 100644 --- a/tests/test_snmp.py +++ b/tests/test_snmp.py @@ -230,13 +230,6 @@ async def test_snmp_set_no_leading_dot(host: str, port: int) -> None: assert results[0].value == 42 -@pytest.mark.asyncio -async def test_snmp_deprecated_context(host: str, port: int) -> None: - with pytest.warns(FutureWarning): - with Snmp(host=host, port=port) as snmp: - await snmp.get(".1.3.6.1.2.1.1.6.0") - - @pytest.mark.asyncio async def test_snmp_disable_validation_source_addr(host: str, port: int) -> None: async with Snmp(host=host, port=port, validate_source_addr=False) as snmp: From ef5fbe9e1e8da936b4e99ae34e3f056a8cf93d90 Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sat, 22 May 2021 20:50:46 +0300 Subject: [PATCH 12/23] 0.5.0 --- aiosnmp/__init__.py | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 150d305..f1e34a0 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind") -__version__ = "0.4.0" +__version__ = "0.5.0" __author__ = "Valetov Konstantin" from .message import SnmpV2TrapMessage, SnmpVarbind diff --git a/docs/conf.py b/docs/conf.py index 2ff2f99..d2c4e4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Valetov Konstantin' # The full version, including alpha/beta/rc tags -release = '0.4.1' +release = '0.5.0' # -- General configuration --------------------------------------------------- From 6361769876e79a6ac038dbd0acc61a119231b399 Mon Sep 17 00:00:00 2001 From: hh-h Date: Mon, 31 May 2021 23:45:12 +0300 Subject: [PATCH 13/23] Refactor tests (#30) refactor tests, removed contextmanager, increased coverage --- aiosnmp/asn1.py | 128 +++--- aiosnmp/message.py | 178 ++++---- tests/test_asn1.py | 1071 ++++++++++++++++++++------------------------ 3 files changed, 638 insertions(+), 739 deletions(-) diff --git a/aiosnmp/asn1.py b/aiosnmp/asn1.py index 3369cba..5eb5520 100644 --- a/aiosnmp/asn1.py +++ b/aiosnmp/asn1.py @@ -7,8 +7,7 @@ import enum import ipaddress import re -from contextlib import contextmanager -from typing import Any, Iterator, List, NamedTuple, Optional, Tuple, Union, cast +from typing import Any, List, NamedTuple, Optional, Tuple, Union, cast class Number(enum.IntEnum): @@ -62,7 +61,7 @@ class Class(enum.IntEnum): class Tag(NamedTuple): - nr: TNumber + number: TNumber typ: TType cls: TClass @@ -77,12 +76,11 @@ class Encoder: def __init__(self) -> None: self.m_stack: List[List[bytes]] = [[]] - @contextmanager - def enter(self, nr: TNumber, cls: Optional[TClass] = None) -> Iterator[None]: + def enter(self, number: TNumber, cls: Optional[TClass] = None) -> None: """This method starts the construction of a constructed type. Args: - nr (int): The desired ASN.1 type. Use ``Number`` enumeration. + number (int): The desired ASN.1 type. Use ``Number`` enumeration. cls (int): This optional parameter specifies the class of the constructed type. The default class to use is the @@ -96,11 +94,10 @@ def enter(self, nr: TNumber, cls: Optional[TClass] = None) -> Iterator[None]: """ if cls is None: cls = Class.Universal - self._emit_tag(nr, Type.Constructed, cls) + self._emit_tag(number, Type.Constructed, cls) self.m_stack.append([]) - yield - + def exit(self) -> None: if len(self.m_stack) == 1: raise Error("Tag stack is empty.") value = b"".join(self.m_stack[-1]) @@ -111,7 +108,7 @@ def enter(self, nr: TNumber, cls: Optional[TClass] = None) -> Iterator[None]: def write( self, value: Any, - nr: Optional[TNumber] = None, + number: Optional[TNumber] = None, typ: Optional[TType] = None, cls: Optional[TClass] = None, ) -> None: @@ -128,8 +125,8 @@ def write( try to autodetect the correct ASN.1 type from the type of ``value``. - nr (int): If the desired ASN.1 type cannot be autodetected or is - autodetected wrongly, the ``nr`` parameter can be provided to + number (int): If the desired ASN.1 type cannot be autodetected or is + autodetected wrongly, the ``number`` parameter can be provided to specify the ASN.1 type to be used. Use ``Number`` enumeration. typ (int): This optional parameter can be used to write constructed @@ -150,23 +147,25 @@ def write( Raises: `Error` """ - if nr is None: - if isinstance(value, int): - nr = Number.Integer + if number is None: + if isinstance(value, bool): + number = Number.Boolean + elif isinstance(value, int): + number = Number.Integer elif isinstance(value, str) or isinstance(value, bytes): - nr = Number.OctetString + number = Number.OctetString elif value is None: - nr = Number.Null + number = Number.Null elif isinstance(value, ipaddress.IPv4Address): - nr = Number.IPAddress + number = Number.IPAddress else: raise Error(f"Cannot determine Number for value type {type(value)}") if typ is None: typ = Type.Primitive if cls is None: cls = Class.Universal - value = self._encode_value(nr, value) - self._emit_tag(nr, typ, cls) + value = self._encode_value(number, value) + self._emit_tag(number, typ, cls) self._emit_length(len(value)) self._emit(value) @@ -192,13 +191,13 @@ def output(self) -> bytes: output = b"".join(self.m_stack[0]) return output - def _emit_tag(self, nr: TNumber, typ: TType, cls: TClass) -> None: + def _emit_tag(self, number: TNumber, typ: TType, cls: TClass) -> None: """Emit a tag.""" - self._emit_tag_short(nr, typ, cls) + self._emit_tag_short(number, typ, cls) - def _emit_tag_short(self, nr: TNumber, typ: TType, cls: TClass) -> None: + def _emit_tag_short(self, number: TNumber, typ: TType, cls: TClass) -> None: """Emit a short tag.""" - self._emit(bytes([nr | typ | cls])) + self._emit(bytes([number | typ | cls])) def _emit_length(self, length: int) -> None: """Emit length octets.""" @@ -209,7 +208,6 @@ def _emit_length(self, length: int) -> None: def _emit_length_short(self, length: int) -> None: """Emit the short length form (< 128 octets).""" - assert length < 128 self._emit(bytes([length])) def _emit_length_long(self, length: int) -> None: @@ -220,7 +218,6 @@ def _emit_length_long(self, length: int) -> None: length >>= 8 values.reverse() # really for correctness as this should not happen anytime soon - assert len(values) < 127 head = bytes([0x80 | len(values)]) self._emit(head) for val in values: @@ -228,24 +225,23 @@ def _emit_length_long(self, length: int) -> None: def _emit(self, s: bytes) -> None: """Emit raw bytes.""" - assert isinstance(s, bytes) self.m_stack[-1].append(s) - def _encode_value(self, nr: TNumber, value: Any) -> bytes: + def _encode_value(self, number: TNumber, value: Any) -> bytes: """Encode a value.""" - if nr in (Number.Integer, Number.Enumerated): + if number in (Number.Integer, Number.Enumerated): return self._encode_integer(value) - elif nr in (Number.OctetString, Number.PrintableString): + elif number in (Number.OctetString, Number.PrintableString): return self._encode_octet_string(value) - elif nr == Number.Boolean: + elif number == Number.Boolean: return self._encode_boolean(value) - elif nr == Number.Null: + elif number == Number.Null: return self._encode_null() - elif nr == Number.ObjectIdentifier: + elif number == Number.ObjectIdentifier: return self._encode_object_identifier(value) - elif nr == Number.IPAddress: + elif number == Number.IPAddress: return self._encode_ipaddress(value) - raise Error(f"Unhandled Number {nr} value {value}") + raise Error(f"Unhandled Number {number} value {value}") @staticmethod def _encode_boolean(value: bool) -> bytes: @@ -275,8 +271,9 @@ def _encode_integer(value: int) -> bytes: values[i] += 1 if values[i] <= 0xFF: break - assert i != len(values) - 1 + values[i] = 0x00 + if negative and values[len(values) - 1] == 0x7F: # Two's complement corner case values.append(0xFF) values.reverse() @@ -286,7 +283,6 @@ def _encode_integer(value: int) -> bytes: def _encode_octet_string(value: Union[str, bytes]) -> bytes: """Encode an octet string.""" # Use the primitive encoding - assert isinstance(value, str) or isinstance(value, bytes) if isinstance(value, str): value = value.encode("utf-8") return value @@ -294,7 +290,7 @@ def _encode_octet_string(value: Union[str, bytes]) -> bytes: @staticmethod def _encode_null() -> bytes: """Encode a Null value.""" - return bytes(b"") + return b"" _re_oid = re.compile(r"^[0-9]+(\.[0-9]+)+$") @@ -357,9 +353,9 @@ def peek(self) -> Tag: self.m_tag = self._read_tag() return self.m_tag - def read(self, nr: Optional[TNumber] = None) -> Tuple[Tag, Any]: + def read(self, number: Optional[TNumber] = None) -> Tuple[Tag, Any]: """This method decodes one ASN.1 tag from the input and returns it as a - ``(tag, value)`` tuple. ``tag`` is a 3-tuple ``(nr, typ, cls)``, + ``(tag, value)`` tuple. ``tag`` is a 3-tuple ``(number, typ, cls)``, while ``value`` is a Python object representing the ASN.1 value. The offset in the input is increased so that the next `Decoder.read()` call will return the next tag. In case no more data is available from @@ -375,9 +371,9 @@ def read(self, nr: Optional[TNumber] = None) -> Tuple[Tag, Any]: raise Error("Input is empty.") tag = self.peek() length = self._read_length() - if nr is None: - nr = tag.nr | tag.cls - value = self._read_value(nr, length) + if number is None: + number = tag.number | tag.cls + value = self._read_value(number, length) self.m_tag = None return tag, value @@ -389,8 +385,7 @@ def eof(self) -> bool: """ return self._end_of_input() - @contextmanager - def enter(self) -> Iterator[None]: + def enter(self) -> None: """This method enters the constructed type that is at the current decoding offset. @@ -409,8 +404,7 @@ def enter(self) -> Iterator[None]: self.m_stack.append([0, bytes_data]) self.m_tag = None - yield - + def exit(self) -> None: if len(self.m_stack) == 1: raise Error("Tag stack is empty.") del self.m_stack[-1] @@ -421,15 +415,15 @@ def _read_tag(self) -> Tag: byte = self._read_byte() cls = byte & 0xC0 typ = byte & 0x20 - nr = byte & 0x1F - if nr == 0x1F: # Long form of tag encoding - nr = 0 + number = byte & 0x1F + if number == 0x1F: # Long form of tag encoding + number = 0 while True: byte = self._read_byte() - nr = (nr << 7) | (byte & 0x7F) + number = (number << 7) | (byte & 0x7F) if not byte & 0x80: break - return Tag(nr=nr, typ=typ, cls=cls) + return Tag(number=number, typ=typ, cls=cls) def _read_length(self) -> int: """Read a length from the input.""" @@ -442,20 +436,17 @@ def _read_length(self) -> int: length = 0 for byte in bytes_data: length = (length << 8) | int(byte) - try: - length = int(length) - except OverflowError: - pass else: length = byte + return length - def _read_value(self, nr: TNumber, length: int) -> Any: + def _read_value(self, number: TNumber, length: int) -> Any: """Read a value from the input.""" bytes_data = self._read_bytes(length) - if nr == Number.Boolean: + if number == Number.Boolean: return self._decode_boolean(bytes_data) - elif nr in ( + elif number in ( Number.Integer, Number.Enumerated, Number.TimeTicks, @@ -465,17 +456,17 @@ def _read_value(self, nr: TNumber, length: int) -> Any: Number.Uinteger32, ): return self._decode_integer(bytes_data) - elif nr == Number.OctetString: + elif number == Number.OctetString: return self._decode_octet_string(bytes_data) - elif nr == Number.Null: + elif number == Number.Null: return self._decode_null(bytes_data) - elif nr == Number.ObjectIdentifier: + elif number == Number.ObjectIdentifier: return self._decode_object_identifier(bytes_data) - elif nr in (Number.PrintableString, Number.IA5String, Number.UTCTime): + elif number in (Number.PrintableString, Number.IA5String, Number.UTCTime): return self._decode_printable_string(bytes_data) - elif nr in (Number.EndOfMibView, Number.NoSuchObject, Number.NoSuchInstance): + elif number in (Number.EndOfMibView, Number.NoSuchObject, Number.NoSuchInstance): return None - elif nr == Number.IPAddress: + elif number == Number.IPAddress: return self._decode_ip_address(bytes_data) return bytes_data @@ -502,7 +493,6 @@ def _read_bytes(self, count: int) -> bytes: def _end_of_input(self) -> bool: """Return True if we are at the end of input.""" index, input_data = self.m_stack[-1] - assert not index > len(input_data) return cast(int, index) == len(input_data) @staticmethod @@ -523,17 +513,13 @@ def _decode_integer(bytes_data: bytes) -> int: values[i] += 1 if values[i] <= 0xFF: break - assert i > 0 + values[i] = 0x00 value = 0 for val in values: value = (value << 8) | val if negative: value = -value - try: - value = int(value) - except OverflowError: - pass return value @staticmethod diff --git a/aiosnmp/message.py b/aiosnmp/message.py index ddef503..f9658d4 100644 --- a/aiosnmp/message.py +++ b/aiosnmp/message.py @@ -61,9 +61,10 @@ def value(self) -> Union[None, str, int, bytes, ipaddress.IPv4Address]: return self._value def encode(self, encoder: Encoder) -> None: - with encoder.enter(Number.Sequence): - encoder.write(self._oid, Number.ObjectIdentifier) - encoder.write(self.value) + encoder.enter(Number.Sequence) + encoder.write(self._oid, Number.ObjectIdentifier) + encoder.write(self.value) + encoder.exit() class PDU: @@ -78,14 +79,17 @@ def __init__(self, varbinds: List[SnmpVarbind]) -> None: self.varbinds: List[SnmpVarbind] = varbinds def encode(self, encoder: Encoder) -> None: - with encoder.enter(self._PDUType, Class.Context): - encoder.write(self.request_id, Number.Integer) - encoder.write(self.error_status, Number.Integer) - encoder.write(self.error_index, Number.Integer) + encoder.enter(self._PDUType, Class.Context) + encoder.write(self.request_id, Number.Integer) + encoder.write(self.error_status, Number.Integer) + encoder.write(self.error_index, Number.Integer) - with encoder.enter(Number.Sequence): - for varbind in self.varbinds: - varbind.encode(encoder) + encoder.enter(Number.Sequence) + for varbind in self.varbinds: + varbind.encode(encoder) + encoder.exit() + + encoder.exit() class BulkPDU: @@ -100,14 +104,17 @@ def __init__(self, varbinds: List[SnmpVarbind], non_repeaters: int, max_repetiti self.varbinds: List[SnmpVarbind] = varbinds def encode(self, encoder: Encoder) -> None: - with encoder.enter(self._PDUType, Class.Context): - encoder.write(self.request_id, Number.Integer) - encoder.write(self.non_repeaters, Number.Integer) - encoder.write(self.max_repetitions, Number.Integer) + encoder.enter(self._PDUType, Class.Context) + encoder.write(self.request_id, Number.Integer) + encoder.write(self.non_repeaters, Number.Integer) + encoder.write(self.max_repetitions, Number.Integer) + + encoder.enter(Number.Sequence) + for varbind in self.varbinds: + varbind.encode(encoder) + encoder.exit() - with encoder.enter(Number.Sequence): - for varbind in self.varbinds: - varbind.encode(encoder) + encoder.exit() class GetRequest(PDU): @@ -147,10 +154,11 @@ def __init__(self, version: SnmpVersion, community: str, data: PDUs) -> None: def encode(self) -> bytes: encoder = Encoder() - with encoder.enter(Number.Sequence): - encoder.write(self.version, Number.Integer) - encoder.write(self.community, Number.OctetString) - self.data.encode(encoder) + encoder.enter(Number.Sequence) + encoder.write(self.version, Number.Integer) + encoder.write(self.community, Number.OctetString) + self.data.encode(encoder) + encoder.exit() return encoder.output() @@ -158,31 +166,39 @@ class SnmpResponse(SnmpMessage): @classmethod def decode(cls, data: bytes) -> "SnmpResponse": decoder = Decoder(data) - with decoder.enter(): - tag, value = decoder.read() - version = SnmpVersion(value) - - tag, value = decoder.read() - community = value.decode() - - with decoder.enter(): - tag, value = decoder.read() - request_id = value - - tag, value = decoder.read() - error_status = value - - tag, value = decoder.read() - error_index = value - - with decoder.enter(): - varbinds: List[SnmpVarbind] = [] - while not decoder.eof(): - with decoder.enter(): - _, value = decoder.read() - oid = value - _, value = decoder.read() - varbinds.append(SnmpVarbind(oid, value)) + decoder.enter() # 1 + tag, value = decoder.read() + version = SnmpVersion(value) + + tag, value = decoder.read() + community = value.decode() + + decoder.enter() # 2 + tag, value = decoder.read() + request_id = value + + tag, value = decoder.read() + error_status = value + + tag, value = decoder.read() + error_index = value + + decoder.enter() # 3 + varbinds: List[SnmpVarbind] = [] + while not decoder.eof(): + decoder.enter() # 4 + _, value = decoder.read() + oid = value + _, value = decoder.read() + varbinds.append(SnmpVarbind(oid, value)) + decoder.exit() # 4 + + decoder.exit() # 3 + + decoder.exit() # 2 + + decoder.exit() # 1 + response = GetResponse(varbinds) response.request_id = request_id response.error_status = error_status @@ -216,37 +232,45 @@ def data(self) -> PDU: @classmethod def decode(cls, data: bytes) -> Optional["SnmpV2TrapMessage"]: decoder = Decoder(data) - with decoder.enter(): - tag, value = decoder.read() - version = SnmpVersion(value) - if version != SnmpVersion.v2c: - return None - - tag, value = decoder.read() - community = value.decode() - - tag = decoder.peek() - if tag.cls != Class.Context or tag.nr != PDUType.SNMPv2Trap: - return None - - with decoder.enter(): - tag, value = decoder.read() - request_id = value - - tag, value = decoder.read() - error_status = value - - tag, value = decoder.read() - error_index = value - - with decoder.enter(): - varbinds: List[SnmpVarbind] = [] - while not decoder.eof(): - with decoder.enter(): - _, value = decoder.read() - oid = value - _, value = decoder.read() - varbinds.append(SnmpVarbind(oid, value)) + decoder.enter() # 1 + tag, value = decoder.read() + version = SnmpVersion(value) + if version != SnmpVersion.v2c: + return None + + tag, value = decoder.read() + community = value.decode() + + tag = decoder.peek() + if tag.cls != Class.Context or tag.number != PDUType.SNMPv2Trap: + return None + + decoder.enter() # 2 + tag, value = decoder.read() + request_id = value + + tag, value = decoder.read() + error_status = value + + tag, value = decoder.read() + error_index = value + + decoder.enter() # 3 + varbinds: List[SnmpVarbind] = [] + while not decoder.eof(): + decoder.enter() # 4 + _, value = decoder.read() + oid = value + _, value = decoder.read() + varbinds.append(SnmpVarbind(oid, value)) + decoder.exit() # 4 + + decoder.exit() # 3 + + decoder.exit() # 2 + + decoder.exit() # 1 + response = SnmpV2Trap(varbinds) response.request_id = request_id response.error_status = error_status diff --git a/tests/test_asn1.py b/tests/test_asn1.py index de3651e..a0b39d0 100644 --- a/tests/test_asn1.py +++ b/tests/test_asn1.py @@ -1,624 +1,513 @@ -# This file is part of Python-ASN1. Python-ASN1 is free software that is -# made available under the MIT license. Consult the file "LICENSE" that -# is distributed together with this file for the exact licensing terms. -# -# Python-ASN1 is copyright (c) 2007-2016 by the Python-ASN1 authors. See the -# file "AUTHORS" for a complete overview. - import ipaddress +from typing import Any, Optional, Tuple, Type import pytest import aiosnmp.asn1 as asn1 +from aiosnmp.asn1 import TClass, TNumber, TType class TestEncoder: - def test_boolean(self) -> None: - enc = asn1.Encoder() - enc.write(True, asn1.Number.Boolean) - res = enc.output() - assert res == b"\x01\x01\xff" - - def test_integer(self) -> None: - enc = asn1.Encoder() - enc.write(1) - res = enc.output() - assert res == b"\x02\x01\x01" - - def test_long_integer(self) -> None: - enc = asn1.Encoder() - enc.write(0x0102030405060708090A0B0C0D0E0F) - res = enc.output() - assert res == b"\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" - - def test_negative_integer(self) -> None: - enc = asn1.Encoder() - enc.write(-1) - res = enc.output() - assert res == b"\x02\x01\xff" - - def test_long_negative_integer(self) -> None: - enc = asn1.Encoder() - enc.write(-0x0102030405060708090A0B0C0D0E0F) - res = enc.output() - assert res == b"\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1" + @pytest.mark.parametrize( + ("value", "number", "expected"), + [ + # boolean + (True, None, b"\x01\x01\xff"), + (True, asn1.Number.Boolean, b"\x01\x01\xff"), + (False, None, b"\x01\x01\x00"), + (False, asn1.Number.Boolean, b"\x01\x01\x00"), + # integer + (0, None, b"\x02\x01\x00"), + (1, None, b"\x02\x01\x01"), + (-0, None, b"\x02\x01\x00"), + (-1, None, b"\x02\x01\xff"), + (127, None, b"\x02\x01\x7f"), + (128, None, b"\x02\x02\x00\x80"), + (-127, None, b"\x02\x01\x81"), + (-128, None, b"\x02\x01\x80"), + (-129, None, b"\x02\x02\xff\x7f"), + (32767, None, b"\x02\x02\x7f\xff"), + (32768, None, b"\x02\x03\x00\x80\x00"), + (32769, None, b"\x02\x03\x00\x80\x01"), + (-32767, None, b"\x02\x02\x80\x01"), + (-32768, None, b"\x02\x02\x80\x00"), + (-32769, None, b"\x02\x03\xff\x7f\xff"), + ( + 5233100606242806050955395731361295, + None, + b"\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", + ), + ( + -5233100606242806050955395731361295, + None, + b"\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1", + ), + # bytes + (b"foo", None, b"\x04\x03foo"), + (b"x" * 60, None, b"\x04<" + b"x" * 60), + (b"x" * 255, None, b"\x04\x81\xff" + b"x" * 255), + (b"x" * 256, None, b"\x04\x82\x01\x00" + b"x" * 256), + (b"x" * 257, None, b"\x04\x82\x01\x01" + b"x" * 257), + (b"x" * 0xFFFF, None, b"\x04\x82\xff\xff" + b"x" * 0xFFFF), + # string + ("foo", asn1.Number.PrintableString, b"\x13\x03foo"), + ("fooé", None, b"\x04\x05\x66\x6f\x6f\xc3\xa9"), + ("fooé", asn1.Number.PrintableString, b"\x13\x05\x66\x6f\x6f\xc3\xa9"), + # null + (None, asn1.Number.Null, b"\x05\x00"), + (None, None, b"\x05\x00"), + # object identifier + ("1.2.3", asn1.Number.ObjectIdentifier, b"\x06\x02\x2a\x03"), + ("39.2.3", asn1.Number.ObjectIdentifier, b"\x06\x03\x8c\x1a\x03"), + ("1.39.3", asn1.Number.ObjectIdentifier, b"\x06\x02\x4f\x03"), + ("1.2.300000", asn1.Number.ObjectIdentifier, b"\x06\x04\x2a\x92\xa7\x60"), + ( + "1.2.840.113554.1.2.1.1", + asn1.Number.ObjectIdentifier, + b"\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01", + ), + # ip address + (ipaddress.IPv4Address("127.0.0.1"), asn1.Number.IPAddress, b"\x40\x04\x7f\x00\x00\x01"), + (ipaddress.IPv4Address("1.1.1.1"), asn1.Number.IPAddress, b"\x40\x04\x01\x01\x01\x01"), + (ipaddress.IPv4Address("255.255.255.255"), asn1.Number.IPAddress, b"\x40\x04\xff\xff\xff\xff"), + (ipaddress.IPv4Address("0.0.0.0"), asn1.Number.IPAddress, b"\x40\x04\x00\x00\x00\x00"), + # enumerated + (1, asn1.Number.Enumerated, b"\x0a\x01\x01"), + ], + ) + def test_simple_encode(self, value: Any, number: Optional[TNumber], expected: bytes) -> None: + encoder = asn1.Encoder() + encoder.write(value, number) + result = encoder.output() + assert result == expected @pytest.mark.parametrize( - ("number", "result"), - ( - (0, b"\x02\x01\x00"), - (1, b"\x02\x01\x01"), - (-0, b"\x02\x01\x00"), - (-1, b"\x02\x01\xff"), - (127, b"\x02\x01\x7f"), - (128, b"\x02\x02\x00\x80"), - (-127, b"\x02\x01\x81"), - (-128, b"\x02\x01\x80"), - (-129, b"\x02\x02\xff\x7f"), - (32767, b"\x02\x02\x7f\xff"), - (32768, b"\x02\x03\x00\x80\x00"), - (32769, b"\x02\x03\x00\x80\x01"), - (-32767, b"\x02\x02\x80\x01"), - (-32768, b"\x02\x02\x80\x00"), - (-32769, b"\x02\x03\xff\x7f\xff"), - ), + ("number", "typ", "values", "expected"), + [ + (asn1.Number.Sequence, None, (1, b"foo"), b"\x30\x08\x02\x01\x01\x04\x03foo"), + (asn1.Number.Sequence, None, (1, 2), b"\x30\x06\x02\x01\x01\x02\x01\x02"), + (asn1.Number.Set, None, (1, b"foo"), b"\x31\x08\x02\x01\x01\x04\x03foo"), + (asn1.Number.Set, None, (1, 2), b"\x31\x06\x02\x01\x01\x02\x01\x02"), + (1, asn1.Class.Context, (1,), b"\xa1\x03\x02\x01\x01"), + (1, asn1.Class.Application, (1,), b"\x61\x03\x02\x01\x01"), + (1, asn1.Class.Private, (1,), b"\xe1\x03\x02\x01\x01"), + ], ) - def test_twos_complement_boundaries(self, number: int, result: bytes) -> None: - enc = asn1.Encoder() - enc.write(number) - assert enc.output() == result - - def test_octet_string(self) -> None: - enc = asn1.Encoder() - enc.write(b"foo") - res = enc.output() - assert res == b"\x04\x03foo" - - def test_printable_string(self) -> None: - enc = asn1.Encoder() - enc.write("foo", nr=asn1.Number.PrintableString) - res = enc.output() - assert res == b"\x13\x03foo" - - def test_unicode_octet_string(self) -> None: - enc = asn1.Encoder() - enc.write("fooé") - res = enc.output() - assert res == b"\x04\x05\x66\x6f\x6f\xc3\xa9" - - def test_unicode_printable_string(self) -> None: - enc = asn1.Encoder() - enc.write("fooé", nr=asn1.Number.PrintableString) - res = enc.output() - assert res == b"\x13\x05\x66\x6f\x6f\xc3\xa9" - - def test_null(self) -> None: - enc = asn1.Encoder() - enc.write(None) - res = enc.output() - assert res == b"\x05\x00" - - def test_object_identifier(self) -> None: - enc = asn1.Encoder() - enc.write("1.2.3", asn1.Number.ObjectIdentifier) - res = enc.output() - assert res == b"\x06\x02\x2a\x03" - - def test_long_object_identifier(self) -> None: - enc = asn1.Encoder() - enc.write("39.2.3", asn1.Number.ObjectIdentifier) - res = enc.output() - assert res == b"\x06\x03\x8c\x1a\x03" - - enc = asn1.Encoder() - enc.write("1.39.3", asn1.Number.ObjectIdentifier) - res = enc.output() - assert res == b"\x06\x02\x4f\x03" - - enc = asn1.Encoder() - enc.write("1.2.300000", asn1.Number.ObjectIdentifier) - res = enc.output() - assert res == b"\x06\x04\x2a\x92\xa7\x60" - - def test_real_object_identifier(self) -> None: - enc = asn1.Encoder() - enc.write("1.2.840.113554.1.2.1.1", asn1.Number.ObjectIdentifier) - res = enc.output() - assert res == b"\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01" - - def test_ipaddress(self) -> None: - enc = asn1.Encoder() - enc.write(ipaddress.IPv4Address("127.0.0.1"), asn1.Number.IPAddress) - res = enc.output() - assert res == b"\x40\x04\x7f\x00\x00\x01" - - def test_enumerated(self) -> None: - enc = asn1.Encoder() - enc.write(1, asn1.Number.Enumerated) - res = enc.output() - assert res == b"\x0a\x01\x01" - - def test_sequence(self) -> None: - enc = asn1.Encoder() - with enc.enter(asn1.Number.Sequence): - enc.write(1) - enc.write(b"foo") - res = enc.output() - assert res == b"\x30\x08\x02\x01\x01\x04\x03foo" - - def test_sequence_of(self) -> None: - enc = asn1.Encoder() - with enc.enter(asn1.Number.Sequence): - enc.write(1) - enc.write(2) - res = enc.output() - assert res == b"\x30\x06\x02\x01\x01\x02\x01\x02" - - def test_set(self) -> None: - enc = asn1.Encoder() - with enc.enter(asn1.Number.Set): - enc.write(1) - enc.write(b"foo") - res = enc.output() - assert res == b"\x31\x08\x02\x01\x01\x04\x03foo" - - def test_set_of(self) -> None: - enc = asn1.Encoder() - with enc.enter(asn1.Number.Set): - enc.write(1) - enc.write(2) - res = enc.output() - assert res == b"\x31\x06\x02\x01\x01\x02\x01\x02" - - def test_context(self) -> None: - enc = asn1.Encoder() - with enc.enter(1, asn1.Class.Context): - enc.write(1) - res = enc.output() - assert res == b"\xa1\x03\x02\x01\x01" - - def test_application(self) -> None: - enc = asn1.Encoder() - with enc.enter(1, asn1.Class.Application): - enc.write(1) - res = enc.output() - assert res == b"\x61\x03\x02\x01\x01" - - def test_private(self) -> None: - enc = asn1.Encoder() - with enc.enter(1, asn1.Class.Private): - enc.write(1) - res = enc.output() - assert res == b"\xe1\x03\x02\x01\x01" - - def test_long_tag_length(self) -> None: - enc = asn1.Encoder() - enc.write(b"x" * 0xFFFF) - res = enc.output() - assert res == b"\x04\x82\xff\xff" + b"x" * 0xFFFF + def test_one_enter(self, number: TNumber, typ: TType, values: Tuple, expected: bytes) -> None: + encoder = asn1.Encoder() + encoder.enter(number, typ) + for value in values: + encoder.write(value) + encoder.exit() + res = encoder.output() + assert res == expected + + @pytest.mark.parametrize( + ("values", "expected"), + [ + ( + ((asn1.Number.Sequence, None, (1, b"foo")),) * 3, + b"\x30\x08\x02\x01\x01\x04\x03foo" * 3, + ), + ( + ( + (asn1.Number.Sequence, None, (1, 2)), + (asn1.Number.Sequence, None, (10, 20)), + ), + b"\x30\x06\x02\x01\x01\x02\x01\x02\x30\x06\x02\x01\n\x02\x01\x14", + ), + ( + ( + (asn1.Number.Set, None, (b"value", b"foo", ipaddress.IPv4Address("5.6.7.8"))), + (asn1.Number.Sequence, None, (1, True, False)), + ), + b"1\x12\x04\x05value\x04\x03foo@\x04\x05\x06\x07\x080\t\x02\x01\x01\x01\x01\xff\x01\x01\x00", + ), + ], + ) + def test_multiple_enter(self, values: Tuple[Tuple[TNumber, Optional[TClass], Tuple]], expected: bytes) -> None: + encoder = asn1.Encoder() + for number, typ, values_ in values: + encoder.enter(number, typ) + for value in values_: + encoder.write(value) + encoder.exit() + + res = encoder.output() + assert res == expected def test_error_stack(self) -> None: - enc = asn1.Encoder() - with enc.enter(asn1.Number.Sequence): - with pytest.raises(asn1.Error): - enc.output() + encoder = asn1.Encoder() + encoder.enter(asn1.Number.Sequence) + with pytest.raises(asn1.Error, match="Stack is not empty."): + encoder.output() - @pytest.mark.parametrize("value", ["1", "40.2.3", "1.40.3", "1.2.3.", ".1.2.3", "foo", "foo.bar"]) - def test_error_object_identifier(self, value) -> None: - enc = asn1.Encoder() - with pytest.raises(asn1.Error): - enc.write(value, asn1.Number.ObjectIdentifier) + @pytest.mark.parametrize( + "value", + ["1", "40.2.3", "1.40.3", "1.2.3.", ".1.2.3", "foo", "foo.bar"], + ) + def test_error_object_identifier(self, value: str) -> None: + encoder = asn1.Encoder() + with pytest.raises(asn1.Error, match="Illegal object identifier"): + encoder.write(value, asn1.Number.ObjectIdentifier) + + def test_exit_errors(self) -> None: + encoder = asn1.Encoder() + with pytest.raises(asn1.Error, match="Tag stack is empty."): + encoder.exit() + + def test_cannot_determine_number(self) -> None: + encoder = asn1.Encoder() + with pytest.raises(asn1.Error, match="Cannot determine Number for value type "): + encoder.write(1.21) + + def test_invalid_number(self) -> None: + encoder = asn1.Encoder() + with pytest.raises(asn1.Error, match="Unhandled Number 155 value 1"): + encoder.write(1, 155) class TestDecoder: @pytest.mark.parametrize( - ("buf", "result"), - ((b"\x01\x01\xff", 1), (b"\x01\x01\x01", 1), (b"\x01\x01\x00", 0)), + ("buffer", "instance", "number", "typ", "cls", "expected"), + [ + (b"\x01\x01\xff", bool, asn1.Number.Boolean, asn1.Type.Primitive, asn1.Class.Universal, True), + (b"\x01\x01\x01", bool, asn1.Number.Boolean, asn1.Type.Primitive, asn1.Class.Universal, True), + (b"\x01\x01\x00", bool, asn1.Number.Boolean, asn1.Type.Primitive, asn1.Class.Universal, False), + (b"\x02\x01\x01", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, 1), + (b"\x02\x04\xff\xff\xff\xff", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, -1), + (b"\x02\x01\xff", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, -1), + ( + b"\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", + int, + asn1.Number.Integer, + asn1.Type.Primitive, + asn1.Class.Universal, + 5233100606242806050955395731361295, + ), + ( + b"\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1", + int, + asn1.Number.Integer, + asn1.Type.Primitive, + asn1.Class.Universal, + -5233100606242806050955395731361295, + ), + (b"\x02\x01\x7f", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, 127), + (b"\x02\x02\x00\x80", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, 128), + (b"\x02\x01\x80", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, -128), + (b"\x02\x02\xff\x7f", int, asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal, -129), + ( + b"\x02\x10\xff\x7f\x2b\x3a\x4d\xea\x48\x1e\x1f\x37\x7b\xa8\xbd\x7f\xb0\x16", + int, + asn1.Number.Integer, + asn1.Type.Primitive, + asn1.Class.Universal, + -668929531791034950848739021124816874, + ), + (b"\x04\x03foo", bytes, asn1.Number.OctetString, asn1.Type.Primitive, asn1.Class.Universal, b"foo"), + ( + b"\x04\x82\xff\xff" + b"x" * 0xFFFF, + bytes, + asn1.Number.OctetString, + asn1.Type.Primitive, + asn1.Class.Universal, + b"x" * 0xFFFF, + ), + (b"\x13\x03foo", str, asn1.Number.PrintableString, asn1.Type.Primitive, asn1.Class.Universal, "foo"), + ( + b"\x13\x05\x66\x6f\x6f\xc3\xa9", + str, + asn1.Number.PrintableString, + asn1.Type.Primitive, + asn1.Class.Universal, + "fooé", + ), + (b"\x05\x00", type(None), asn1.Number.Null, asn1.Type.Primitive, asn1.Class.Universal, None), + ( + b"\x06\x02\x2a\x03", + str, + asn1.Number.ObjectIdentifier, + asn1.Type.Primitive, + asn1.Class.Universal, + ".1.2.3", + ), + ( + b"\x06\x03\x8c\x1a\x03", + str, + asn1.Number.ObjectIdentifier, + asn1.Type.Primitive, + asn1.Class.Universal, + ".39.2.3", + ), + ( + b"\x06\x02\x4f\x03", + str, + asn1.Number.ObjectIdentifier, + asn1.Type.Primitive, + asn1.Class.Universal, + ".1.39.3", + ), + ( + b"\x06\x04\x2a\x92\xa7\x60", + str, + asn1.Number.ObjectIdentifier, + asn1.Type.Primitive, + asn1.Class.Universal, + ".1.2.300000", + ), + ( + b"\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01", + str, + asn1.Number.ObjectIdentifier, + asn1.Type.Primitive, + asn1.Class.Universal, + ".1.2.840.113554.1.2.1.1", + ), + (b"\x0a\x01\x01", int, asn1.Number.Enumerated, asn1.Type.Primitive, asn1.Class.Universal, 1), + (b"\x80\x00", type(None), 0, asn1.Type.Primitive, asn1.Class.Context, None), + (b"\x81\x00", type(None), 1, asn1.Type.Primitive, asn1.Class.Context, None), + (b"\x82\x00", type(None), 2, asn1.Type.Primitive, asn1.Class.Context, None), + (b"\x43\x03\x54\xa5\xb0", int, 3, asn1.Type.Primitive, asn1.Class.Application, 5547440), + (b"\x42\x01\x02", int, 2, asn1.Type.Primitive, asn1.Class.Application, 2), + (b"\x41\x01\x2a", int, 1, asn1.Type.Primitive, asn1.Class.Application, 42), + ( + b"\x40\x04\x7f\x00\x00\x01", + ipaddress.IPv4Address, + 0, + asn1.Type.Primitive, + asn1.Class.Application, + ipaddress.IPv4Address("127.0.0.1"), + ), + ], ) - def test_boolean(self, buf: bytes, result: int) -> None: - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (asn1.Number.Boolean, asn1.Type.Primitive, asn1.Class.Universal) - tag, val = dec.read() - assert isinstance(val, int) - assert val == result - - @pytest.mark.parametrize(("buf", "result"), ((b"\x02\x01\x01", 1), (b"\x02\x04\xff\xff\xff\xff", -1))) - def test_integer(self, buf: bytes, result: int) -> None: - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (asn1.Number.Integer, asn1.Type.Primitive, asn1.Class.Universal) - tag, val = dec.read() - assert isinstance(val, int) - assert val == result - - def test_long_integer(self) -> None: - buf = b"\x02\x0f\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == 0x0102030405060708090A0B0C0D0E0F - - def test_negative_integer(self) -> None: - buf = b"\x02\x01\xff" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == -1 - - def test_long_negative_integer(self) -> None: - buf = b"\x02\x0f\xfe\xfd\xfc\xfb\xfa\xf9\xf8\xf7\xf6\xf5\xf4\xf3\xf2\xf1\xf1" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == -0x0102030405060708090A0B0C0D0E0F + def test_simple_decode( + self, buffer: bytes, instance: Type, number: TNumber, typ: TType, cls: TClass, expected: Any + ) -> None: + decoder = asn1.Decoder(buffer) + tag = decoder.peek() + assert tag.number == number + assert tag.typ == typ + assert tag.cls == cls + + tag, value = decoder.read() + assert tag.number == number + assert tag.typ == typ + assert tag.cls == cls + + assert isinstance(value, instance) + assert value == expected + assert decoder.eof() @pytest.mark.parametrize( - ("buf", "result"), - ( - (b"\x02\x01\x7f", 127), - (b"\x02\x02\x00\x80", 128), - (b"\x02\x01\x80", -128), - (b"\x02\x02\xff\x7f", -129), - ), + ("buffer", "number", "typ", "cls", "expected_values"), + [ + ( + b"\x30\x08\x02\x01\x01\x04\x03foo", + asn1.Number.Sequence, + asn1.Type.Constructed, + asn1.Class.Universal, + (1, b"foo"), + ), + ( + b"\x30\x06\x02\x01\x01\x02\x01\x02", + asn1.Number.Sequence, + asn1.Type.Constructed, + asn1.Class.Universal, + (1, 2), + ), + ( + b"\x31\x08\x02\x01\x01\x04\x03foo", + asn1.Number.Set, + asn1.Type.Constructed, + asn1.Class.Universal, + (1, b"foo"), + ), + ( + b"\x31\x06\x02\x01\x01\x02\x01\x02", + asn1.Number.Set, + asn1.Type.Constructed, + asn1.Class.Universal, + (1, 2), + ), + ( + b"\xa1\x03\x02\x01\x01", + 1, + asn1.Type.Constructed, + asn1.Class.Context, + (1,), + ), + ( + b"\x61\x03\x02\x01\x01", + 1, + asn1.Type.Constructed, + asn1.Class.Application, + (1,), + ), + ( + b"\xe1\x03\x02\x01\x01", + 1, + asn1.Type.Constructed, + asn1.Class.Private, + (1,), + ), + ], ) - def test_twos_complement_boundaries(self, buf: bytes, result: int) -> None: - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == result - - def test_octet_string(self) -> None: - buf = b"\x04\x03foo" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.OctetString, - asn1.Type.Primitive, - asn1.Class.Universal, - ) - tag, val = dec.read() - assert val == b"foo" - - def test_printable_string(self) -> None: - buf = b"\x13\x03foo" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.PrintableString, - asn1.Type.Primitive, - asn1.Class.Universal, - ) - tag, val = dec.read() - assert val == "foo" - - def test_unicode_printable_string(self) -> None: - buf = b"\x13\x05\x66\x6f\x6f\xc3\xa9" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.PrintableString, - asn1.Type.Primitive, - asn1.Class.Universal, - ) - tag, val = dec.read() - assert val == "fooé" - - def test_null(self) -> None: - buf = b"\x05\x00" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (asn1.Number.Null, asn1.Type.Primitive, asn1.Class.Universal) - tag, val = dec.read() - assert val is None - - def test_object_identifier(self) -> None: - buf = b"\x06\x02\x2a\x03" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.ObjectIdentifier, - asn1.Type.Primitive, - asn1.Class.Universal, - ) - tag, val = dec.read() - assert val == ".1.2.3" + def test_one_enter(self, buffer: bytes, number: TNumber, typ: TType, cls: TClass, expected_values: Tuple) -> None: + decoder = asn1.Decoder(buffer) + tag = decoder.peek() + assert tag.number == number + assert tag.typ == typ + assert tag.cls == cls + + decoder.enter() + for expected in expected_values: + _, value = decoder.read() + assert value == expected + + decoder.exit() + + assert decoder.eof() @pytest.mark.parametrize( - ("buf", "result"), - ( - (b"\x06\x03\x8c\x1a\x03", ".39.2.3"), - (b"\x06\x02\x4f\x03", ".1.39.3"), - (b"\x06\x04\x2a\x92\xa7\x60", ".1.2.300000"), - ), + ("buffer", "expected_values"), + [ + ( + b"\x30\x08\x02\x01\x01\x04\x03foo" * 3, + ((1, b"foo"),) * 3, + ), + ( + b"\x30\x06\x02\x01\x01\x02\x01\x02\x30\x06\x02\x01\n\x02\x01\x14", + ( + (1, 2), + (10, 20), + ), + ), + ( + b"1\x12\x04\x05value\x04\x03foo@\x04\x05\x06\x07\x080\t\x02\x01\x01\x01\x01\xff\x01\x01\x00", + ( + (b"value", b"foo", ipaddress.IPv4Address("5.6.7.8")), + (1, True, False), + ), + ), + ], ) - def test_long_object_identifier(self, buf: bytes, result: str) -> None: - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == result - - def test_real_object_identifier(self) -> None: - buf = b"\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x01\x01" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == ".1.2.840.113554.1.2.1.1" - - def test_enumerated(self) -> None: - buf = b"\x0a\x01\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.Enumerated, - asn1.Type.Primitive, - asn1.Class.Universal, - ) - tag, val = dec.read() - assert isinstance(val, int) - assert val == 1 - - def test_sequence(self) -> None: - buf = b"\x30\x08\x02\x01\x01\x04\x03foo" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.Sequence, - asn1.Type.Constructed, - asn1.Class.Universal, - ) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - tag, val = dec.read() - assert val == b"foo" - - def test_sequence_of(self) -> None: - buf = b"\x30\x06\x02\x01\x01\x02\x01\x02" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == ( - asn1.Number.Sequence, - asn1.Type.Constructed, - asn1.Class.Universal, - ) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - tag, val = dec.read() - assert val == 2 - - def test_set(self) -> None: - buf = b"\x31\x08\x02\x01\x01\x04\x03foo" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (asn1.Number.Set, asn1.Type.Constructed, asn1.Class.Universal) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - tag, val = dec.read() - assert val == b"foo" - - def test_set_of(self) -> None: - buf = b"\x31\x06\x02\x01\x01\x02\x01\x02" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (asn1.Number.Set, asn1.Type.Constructed, asn1.Class.Universal) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - tag, val = dec.read() - assert val == 2 - - def test_no_such_object(self) -> None: - buf = b"\x80\x00" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (0, asn1.Type.Primitive, asn1.Class.Context) - tag, val = dec.read() - assert val is None - - def test_no_such_instance(self) -> None: - buf = b"\x81\x00" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (1, asn1.Type.Primitive, asn1.Class.Context) - tag, val = dec.read() - assert val is None - - def test_end_of_mib_view(self) -> None: - buf = b"\x82\x00" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (2, asn1.Type.Primitive, asn1.Class.Context) - tag, val = dec.read() - assert val is None - - def test_time_ticks(self) -> None: - buf = b"\x43\x03\x54\xa5\xb0" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (3, asn1.Type.Primitive, asn1.Class.Application) - tag, val = dec.read() - assert val == 5547440 - - def test_gauge32(self) -> None: - buf = b"\x42\x01\x02" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (2, asn1.Type.Primitive, asn1.Class.Application) - tag, val = dec.read() - assert val == 2 - - def test_counter32(self) -> None: - buf = b"\x41\x01\x2a" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (1, asn1.Type.Primitive, asn1.Class.Application) - tag, val = dec.read() - assert val == 42 - - def test_ipaddress(self) -> None: - buf = b"\x40\x04\x7f\x00\x00\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (0, asn1.Type.Primitive, asn1.Class.Application) - tag, val = dec.read() - assert val == ipaddress.IPv4Address("127.0.0.1") - - def test_context(self) -> None: - buf = b"\xa1\x03\x02\x01\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (1, asn1.Type.Constructed, asn1.Class.Context) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - - def test_application(self) -> None: - buf = b"\x61\x03\x02\x01\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (1, asn1.Type.Constructed, asn1.Class.Application) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - - def test_private(self) -> None: - buf = b"\xe1\x03\x02\x01\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (1, asn1.Type.Constructed, asn1.Class.Private) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - - def test_long_tag_id(self) -> None: - buf = b"\x3f\x83\xff\x7f\x03\x02\x01\x01" - dec = asn1.Decoder(buf) - tag = dec.peek() - assert tag == (0xFFFF, asn1.Type.Constructed, asn1.Class.Universal) - with dec.enter(): - tag, val = dec.read() - assert val == 1 - - def test_long_tag_length(self) -> None: - buf = b"\x04\x82\xff\xff" + b"x" * 0xFFFF - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == b"x" * 0xFFFF - - def test_read_multiple(self) -> None: - buf = b"\x02\x01\x01\x02\x01\x02" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == 1 - tag, val = dec.read() - assert val == 2 - assert dec.eof() - - def test_skip_primitive(self) -> None: - buf = b"\x02\x01\x01\x02\x01\x02" - dec = asn1.Decoder(buf) - dec.read() - tag, val = dec.read() - assert val == 2 - assert dec.eof() - - def test_skip_constructed(self) -> None: - buf = b"\x30\x06\x02\x01\x01\x02\x01\x02\x02\x01\x03" - dec = asn1.Decoder(buf) - dec.read() - tag, val = dec.read() - assert val == 3 - assert dec.eof() - - def test_no_input(self) -> None: - dec = asn1.Decoder(b"") - with pytest.raises(asn1.Error): - dec.peek() - - @pytest.mark.parametrize("buf", (b"\x3f", b"\x3f\x83")) - def test_error_missing_tag_bytes(self, buf: bytes) -> None: - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.peek() - - def test_error_no_length_bytes(self) -> None: - buf = b"\x02" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_missing_length_bytes(self) -> None: - buf = b"\x04\x82\xff" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_too_many_length_bytes(self) -> None: - buf = b"\x04\xff" + b"\xff" * 0x7F - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_no_value_bytes(self) -> None: - buf = b"\x02\x01" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_missing_value_bytes(self) -> None: - buf = b"\x02\x02\x01" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_non_normalised_object_identifier(self) -> None: - buf = b"\x06\x02\x80\x01" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_error_object_identifier_with_too_large_first_component(self) -> None: - buf = b"\x06\x02\x8c\x40" - dec = asn1.Decoder(buf) - with pytest.raises(asn1.Error): - dec.read() - - def test_big_negative_integer(self) -> None: - buf = b"\x02\x10\xff\x7f\x2b\x3a\x4d\xea\x48\x1e\x1f\x37\x7b\xa8\xbd\x7f\xb0\x16" - dec = asn1.Decoder(buf) - tag, val = dec.read() - assert val == -668929531791034950848739021124816874 - assert dec.eof() + def test_multiple_enter(self, buffer: bytes, expected_values: Tuple[Tuple]) -> None: + decoder = asn1.Decoder(buffer) + for expected in expected_values: + decoder.enter() + for value_ in expected: + _, value = decoder.read() + assert value == value_ + + decoder.exit() + + assert decoder.eof() -class TestEncoderDecoder: @pytest.mark.parametrize( - "value", - ( - 668929531791034950848739021124816874, - 667441897913742713771034596334288035, - 664674827807729028941298133900846368, - 666811959353093594446621165172641478, - ), + ("buffer", "expected_values"), + [ + (b"\x02\x01\x01\x02\x01\x02", (1, 2)), + (b"\x30\x06\x02\x01\x01\x02\x01\x02\x02\x01\x03", (b"\x02\x01\x01\x02\x01\x02", 3)), + ], ) - def test_big_numbers(self, value: int) -> None: - encoder = asn1.Encoder() - encoder.write(value, asn1.Number.Integer) - encoded_bytes = encoder.output() - decoder = asn1.Decoder(encoded_bytes) - tag, val = decoder.read() - assert val == value + def test_read_multiple(self, buffer: bytes, expected_values: Tuple[Any]) -> None: + decoder = asn1.Decoder(buffer) + for expected in expected_values: + _, value = decoder.read() + assert value == expected + + assert decoder.eof() @pytest.mark.parametrize( - "value", - ( - -668929531791034950848739021124816874, - -667441897913742713771034596334288035, - -664674827807729028941298133900846368, - -666811959353093594446621165172641478, - ), + ("buffer", "error"), + [ + (b"", "Input is empty."), + (b"\x3f", "Premature end of input."), + (b"\x3f\x83", "Premature end of input."), + ], + ) + def test_peek_errors(self, buffer: bytes, error: str) -> None: + decoder = asn1.Decoder(buffer) + with pytest.raises(asn1.Error, match=error): + decoder.peek() + + @pytest.mark.parametrize( + ("buffer", "error"), + [ + (b"", "Input is empty."), + (b"\x02", "Premature end of input."), + (b"\x04\x82\xff", "Premature end of input."), + (b"\x04\xff" + b"\xff" * 0x7F, "ASN1 syntax error"), + (b"\x02\x01", "Premature end of input."), + (b"\x02\x02\x01", "Premature end of input."), + (b"\x06\x02\x80\x01", "ASN1 syntax error"), + (b"\x06\x02\x8c\x40", "ASN1 syntax error"), + ], + ) + def test_read_errors(self, buffer: bytes, error: str) -> None: + decoder = asn1.Decoder(buffer) + with pytest.raises(asn1.Error, match=error): + decoder.read() + + def test_cannot_enter(self) -> None: + decoder = asn1.Decoder(b"\x01\x01\xff") + with pytest.raises(asn1.Error, match="Cannot enter a non-constructed tag."): + decoder.enter() + + def test_premature_exit(self) -> None: + decoder = asn1.Decoder(b"\x01\x01\xff") + with pytest.raises(asn1.Error, match="Tag stack is empty."): + decoder.exit() + + def test_big_boolean(self) -> None: + decoder = asn1.Decoder(b"\x01\x02\xff\x00") + with pytest.raises(asn1.Error, match="ASN1 syntax error"): + decoder.read() + + def test_not_null_null(self) -> None: + decoder = asn1.Decoder(b"\x05\x01\x01") + with pytest.raises(asn1.Error, match="ASN1 syntax error"): + decoder.read() + + +class TestEncoderDecoder: + @pytest.mark.parametrize( + ("value", "number"), + [ + (None, None), + (True, None), + (False, None), + (0, None), + (1, None), + (-1, None), + (127, None), + (128, None), + (129, None), + (-126, None), + (-127, None), + (-128, None), + (-129, None), + (668929531791034950848739021124816874, None), + (-668929531791034950848739021124816874, None), + ("abc", asn1.Number.PrintableString), + ("abc" * 10, asn1.Number.PrintableString), + ("abc" * 100, asn1.Number.PrintableString), + (b"abc", None), + (b"abc" * 10, None), + (b"abc" * 100, None), + (ipaddress.IPv4Address("0.0.0.0"), None), + (ipaddress.IPv4Address("255.255.255.255"), None), + (ipaddress.IPv4Address("192.168.0.1"), None), + (ipaddress.IPv4Address("8.8.8.8"), None), + ], ) - def test_big_negative_numbers(self, value: int) -> None: + def test_simple(self, value: Any, number: Optional[TNumber]) -> None: encoder = asn1.Encoder() - encoder.write(value, asn1.Number.Integer) - encoded_bytes = encoder.output() - decoder = asn1.Decoder(encoded_bytes) - tag, val = decoder.read() - assert val == value + encoder.write(value, number) + data = encoder.output() + decoder = asn1.Decoder(data) + _, decoded = decoder.read() + assert decoded == value + assert decoder.eof() From df90898bf89703f77c34cbeb25a30d8998e2933a Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sun, 30 May 2021 13:34:43 +0300 Subject: [PATCH 14/23] migrated asn1 parser to rust --- .gitignore | 5 + .rustfmt.toml | 3 + Cargo.lock | 323 +++++++++++++++++++++++++++ Cargo.toml | 14 ++ MANIFEST.in | 2 + aiosnmp/asn1.py | 507 +----------------------------------------- aiosnmp/asn1_rust.pyi | 27 +++ aiosnmp/message.py | 3 +- aiosnmp/protocols.py | 2 +- azure-pipelines.yml | 163 +++++++++++++- pyproject.toml | 18 ++ requirements-dev.txt | 4 +- setup.cfg | 9 +- setup.py | 23 +- src/decoder.rs | 253 +++++++++++++++++++++ src/encoder.rs | 233 +++++++++++++++++++ src/lib.rs | 34 +++ src/tag.rs | 20 ++ tests/__init__.py | 0 tests/conftest.py | 2 +- tests/test_asn1.py | 85 +++---- tox.ini | 10 +- 22 files changed, 1159 insertions(+), 581 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 MANIFEST.in create mode 100644 aiosnmp/asn1_rust.pyi create mode 100644 pyproject.toml create mode 100644 src/decoder.rs create mode 100644 src/encoder.rs create mode 100644 src/lib.rs create mode 100644 src/tag.rs delete mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore index 216e06a..5b80687 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,8 @@ venv.bak/ # idea .idea/ + + +# Added by cargo + +/target diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..83bdbf1 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +max_width = 120 +reorder_imports = true +edition = "2018" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed377e7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,323 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "aiosnmp" +version = "0.5.0" +dependencies = [ + "lazy_static", + "pyo3", + "regex", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "inventory" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f" +dependencies = [ + "ctor", + "ghost", + "inventory-impl", +] + +[[package]] +name = "inventory-impl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" + +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "pyo3" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4837b8e8e18a102c23f79d1e9a110b597ea3b684c95e874eb1ad88f8683109c3" +dependencies = [ + "cfg-if", + "ctor", + "indoc", + "inventory", + "libc", + "parking_lot", + "paste", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-macros" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47f2c300ceec3e58064fd5f8f5b61230f2ffd64bde4970c81fdd0563a2db1bb" +dependencies = [ + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b097e5d84fcbe3e167f400fbedd657820a375b034c78bd852050749a575d66" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..86f77b1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "aiosnmp" +version = "0.5.0" +authors = ["Konstantin Valetov "] +edition = "2018" + +[lib] +name = "asn1_rust" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.13.2", features = ["extension-module"] } +regex = "1.5.4" +lazy_static = "1.4.0" diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7c68298 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include Cargo.toml +recursive-include src * diff --git a/aiosnmp/asn1.py b/aiosnmp/asn1.py index 5eb5520..a812be4 100644 --- a/aiosnmp/asn1.py +++ b/aiosnmp/asn1.py @@ -1,13 +1,5 @@ -# This file is part of Python-ASN1. Python-ASN1 is free software that is -# made available under the MIT license. Consult the file "LICENSE" that is -# distributed together with this file for the exact licensing terms. -# -# Python-ASN1 is copyright (c) 2007-2016 by the Python-ASN1 authors. - import enum -import ipaddress -import re -from typing import Any, List, NamedTuple, Optional, Tuple, Union, cast +from typing import Union class Number(enum.IntEnum): @@ -58,500 +50,3 @@ class Class(enum.IntEnum): TNumber = Union[Number, int] TType = Union[Type, int] TClass = Union[Class, int] - - -class Tag(NamedTuple): - number: TNumber - typ: TType - cls: TClass - - -class Error(Exception): - pass - - -class Encoder: - __slots__ = "m_stack" - - def __init__(self) -> None: - self.m_stack: List[List[bytes]] = [[]] - - def enter(self, number: TNumber, cls: Optional[TClass] = None) -> None: - """This method starts the construction of a constructed type. - - Args: - number (int): The desired ASN.1 type. Use ``Number`` enumeration. - - cls (int): This optional parameter specifies the class - of the constructed type. The default class to use is the - universal class. Use ``Class`` enumeration. - - Returns: - None - - Raises: - `Error` - """ - if cls is None: - cls = Class.Universal - self._emit_tag(number, Type.Constructed, cls) - self.m_stack.append([]) - - def exit(self) -> None: - if len(self.m_stack) == 1: - raise Error("Tag stack is empty.") - value = b"".join(self.m_stack[-1]) - del self.m_stack[-1] - self._emit_length(len(value)) - self._emit(value) - - def write( - self, - value: Any, - number: Optional[TNumber] = None, - typ: Optional[TType] = None, - cls: Optional[TClass] = None, - ) -> None: - """This method encodes one ASN.1 tag and writes it to the output buffer. - - Note: - Normally, ``value`` will be the only parameter to this method. - In this case Python-ASN1 will autodetect the correct ASN.1 type from - the type of ``value``, and will output the encoded value based on this - type. - - Args: - value (any): The value of the ASN.1 tag to write. Python-ASN1 will - try to autodetect the correct ASN.1 type from the type of - ``value``. - - number (int): If the desired ASN.1 type cannot be autodetected or is - autodetected wrongly, the ``number`` parameter can be provided to - specify the ASN.1 type to be used. Use ``Number`` enumeration. - - typ (int): This optional parameter can be used to write constructed - types to the output by setting it to indicate the constructed - encoding type. In this case, ``value`` must already be valid ASN.1 - encoded data as plain Python bytes. This is not normally how - constructed types should be encoded though, see `Encoder.enter()` - and `Encoder.leave()` for the recommended way of doing this. - Use ``Type`` enumeration. - - cls (int): This parameter can be used to override the class of the - ``value``. The default class is the universal class. - Use ``Class`` enumeration. - - Returns: - None - - Raises: - `Error` - """ - if number is None: - if isinstance(value, bool): - number = Number.Boolean - elif isinstance(value, int): - number = Number.Integer - elif isinstance(value, str) or isinstance(value, bytes): - number = Number.OctetString - elif value is None: - number = Number.Null - elif isinstance(value, ipaddress.IPv4Address): - number = Number.IPAddress - else: - raise Error(f"Cannot determine Number for value type {type(value)}") - if typ is None: - typ = Type.Primitive - if cls is None: - cls = Class.Universal - value = self._encode_value(number, value) - self._emit_tag(number, typ, cls) - self._emit_length(len(value)) - self._emit(value) - - def output(self) -> bytes: - """This method returns the encoded ASN.1 data as plain Python ``bytes``. - This method can be called multiple times, also during encoding. - In the latter case the data that has been encoded so far is - returned. - - Note: - It is an error to call this method if the encoder is still - constructing a constructed type, i.e. if `Encoder.enter()` has been - called more times that `Encoder.leave()`. - - Returns: - bytes: The DER encoded ASN.1 data. - - Raises: - `Error` - """ - if len(self.m_stack) != 1: - raise Error("Stack is not empty.") - output = b"".join(self.m_stack[0]) - return output - - def _emit_tag(self, number: TNumber, typ: TType, cls: TClass) -> None: - """Emit a tag.""" - self._emit_tag_short(number, typ, cls) - - def _emit_tag_short(self, number: TNumber, typ: TType, cls: TClass) -> None: - """Emit a short tag.""" - self._emit(bytes([number | typ | cls])) - - def _emit_length(self, length: int) -> None: - """Emit length octets.""" - if length < 128: - self._emit_length_short(length) - else: - self._emit_length_long(length) - - def _emit_length_short(self, length: int) -> None: - """Emit the short length form (< 128 octets).""" - self._emit(bytes([length])) - - def _emit_length_long(self, length: int) -> None: - """Emit the long length form (>= 128 octets).""" - values = [] - while length: - values.append(length & 0xFF) - length >>= 8 - values.reverse() - # really for correctness as this should not happen anytime soon - head = bytes([0x80 | len(values)]) - self._emit(head) - for val in values: - self._emit(bytes([val])) - - def _emit(self, s: bytes) -> None: - """Emit raw bytes.""" - self.m_stack[-1].append(s) - - def _encode_value(self, number: TNumber, value: Any) -> bytes: - """Encode a value.""" - if number in (Number.Integer, Number.Enumerated): - return self._encode_integer(value) - elif number in (Number.OctetString, Number.PrintableString): - return self._encode_octet_string(value) - elif number == Number.Boolean: - return self._encode_boolean(value) - elif number == Number.Null: - return self._encode_null() - elif number == Number.ObjectIdentifier: - return self._encode_object_identifier(value) - elif number == Number.IPAddress: - return self._encode_ipaddress(value) - raise Error(f"Unhandled Number {number} value {value}") - - @staticmethod - def _encode_boolean(value: bool) -> bytes: - """Encode a boolean.""" - return value and bytes(b"\xff") or bytes(b"\x00") - - @staticmethod - def _encode_integer(value: int) -> bytes: - """Encode an integer.""" - if value < 0: - value = -value - negative = True - limit = 0x80 - else: - negative = False - limit = 0x7F - values = [] - while value > limit: - values.append(value & 0xFF) - value >>= 8 - values.append(value & 0xFF) - if negative: - # create two's complement - for i in range(len(values)): # Invert bits - values[i] = 0xFF - values[i] - for i in range(len(values)): # Add 1 - values[i] += 1 - if values[i] <= 0xFF: - break - - values[i] = 0x00 - - if negative and values[len(values) - 1] == 0x7F: # Two's complement corner case - values.append(0xFF) - values.reverse() - return bytes(values) - - @staticmethod - def _encode_octet_string(value: Union[str, bytes]) -> bytes: - """Encode an octet string.""" - # Use the primitive encoding - if isinstance(value, str): - value = value.encode("utf-8") - return value - - @staticmethod - def _encode_null() -> bytes: - """Encode a Null value.""" - return b"" - - _re_oid = re.compile(r"^[0-9]+(\.[0-9]+)+$") - - def _encode_object_identifier(self, oid: str) -> bytes: - """Encode an object identifier.""" - if not self._re_oid.match(oid): - raise Error("Illegal object identifier") - cmps = list(map(int, oid.split("."))) - if cmps[0] > 39 or cmps[1] > 39: - raise Error("Illegal object identifier") - cmps = [40 * cmps[0] + cmps[1]] + cmps[2:] - cmps.reverse() - result = [] - for cmp_data in cmps: - result.append(cmp_data & 0x7F) - while cmp_data > 0x7F: - cmp_data >>= 7 - result.append(0x80 | (cmp_data & 0x7F)) - result.reverse() - return bytes(result) - - @staticmethod - def _encode_ipaddress(value: ipaddress.IPv4Address) -> bytes: - """Encode an ip address.""" - return int(value).to_bytes(4, byteorder="big") - - -class Decoder: - __slots__ = ("m_stack", "m_tag") - - def __init__(self, data: bytes) -> None: - self.m_stack: List[List] = [[0, data]] - self.m_tag: Optional[Tag] = None - - def peek(self) -> Tag: - """This method returns the current ASN.1 tag (i.e. the tag that a - subsequent `Decoder.read()` call would return) without updating the - decoding offset. In case no more data is available from the input, - this method returns ``None`` to signal end-of-file. - - This method is useful if you don't know whether the next tag will be a - primitive or a constructed tag. Depending on the return value of `peek`, - you would decide to either issue a `Decoder.read()` in case of a primitive - type, or an `Decoder.enter()` in case of a constructed type. - - Note: - Because this method does not advance the current offset in the input, - calling it multiple times in a row will return the same value for all - calls. - - Returns: - `Tag`: The current ASN.1 tag. - - Raises: - `Error` - """ - if self._end_of_input(): - raise Error("Input is empty.") - if self.m_tag is None: - self.m_tag = self._read_tag() - return self.m_tag - - def read(self, number: Optional[TNumber] = None) -> Tuple[Tag, Any]: - """This method decodes one ASN.1 tag from the input and returns it as a - ``(tag, value)`` tuple. ``tag`` is a 3-tuple ``(number, typ, cls)``, - while ``value`` is a Python object representing the ASN.1 value. - The offset in the input is increased so that the next `Decoder.read()` - call will return the next tag. In case no more data is available from - the input, this method returns ``None`` to signal end-of-file. - - Returns: - `Tag`, value: The current ASN.1 tag and its value. - - Raises: - `Error` - """ - if self._end_of_input(): - raise Error("Input is empty.") - tag = self.peek() - length = self._read_length() - if number is None: - number = tag.number | tag.cls - value = self._read_value(number, length) - self.m_tag = None - return tag, value - - def eof(self) -> bool: - """Return True if we are at the end of input. - - Returns: - bool: True if all input has been decoded, and False otherwise. - """ - return self._end_of_input() - - def enter(self) -> None: - """This method enters the constructed type that is at the current - decoding offset. - - Note: - It is an error to call `Decoder.enter()` if the to be decoded ASN.1 tag - is not of a constructed type. - - Returns: - None - """ - tag = self.peek() - if tag.typ != Type.Constructed: - raise Error("Cannot enter a non-constructed tag.") - length = self._read_length() - bytes_data = self._read_bytes(length) - self.m_stack.append([0, bytes_data]) - self.m_tag = None - - def exit(self) -> None: - if len(self.m_stack) == 1: - raise Error("Tag stack is empty.") - del self.m_stack[-1] - self.m_tag = None - - def _read_tag(self) -> Tag: - """Read a tag from the input.""" - byte = self._read_byte() - cls = byte & 0xC0 - typ = byte & 0x20 - number = byte & 0x1F - if number == 0x1F: # Long form of tag encoding - number = 0 - while True: - byte = self._read_byte() - number = (number << 7) | (byte & 0x7F) - if not byte & 0x80: - break - return Tag(number=number, typ=typ, cls=cls) - - def _read_length(self) -> int: - """Read a length from the input.""" - byte = self._read_byte() - if byte & 0x80: - count = byte & 0x7F - if count == 0x7F: - raise Error("ASN1 syntax error") - bytes_data = self._read_bytes(count) - length = 0 - for byte in bytes_data: - length = (length << 8) | int(byte) - else: - length = byte - - return length - - def _read_value(self, number: TNumber, length: int) -> Any: - """Read a value from the input.""" - bytes_data = self._read_bytes(length) - if number == Number.Boolean: - return self._decode_boolean(bytes_data) - elif number in ( - Number.Integer, - Number.Enumerated, - Number.TimeTicks, - Number.Gauge32, - Number.Counter32, - Number.Counter64, - Number.Uinteger32, - ): - return self._decode_integer(bytes_data) - elif number == Number.OctetString: - return self._decode_octet_string(bytes_data) - elif number == Number.Null: - return self._decode_null(bytes_data) - elif number == Number.ObjectIdentifier: - return self._decode_object_identifier(bytes_data) - elif number in (Number.PrintableString, Number.IA5String, Number.UTCTime): - return self._decode_printable_string(bytes_data) - elif number in (Number.EndOfMibView, Number.NoSuchObject, Number.NoSuchInstance): - return None - elif number == Number.IPAddress: - return self._decode_ip_address(bytes_data) - return bytes_data - - def _read_byte(self) -> int: - """Return the next input byte, or raise an error on end-of-input.""" - index, input_data = self.m_stack[-1] - try: - byte: int = input_data[index] - except IndexError: - raise Error("Premature end of input.") - self.m_stack[-1][0] += 1 - return byte - - def _read_bytes(self, count: int) -> bytes: - """Return the next ``count`` bytes of input. Raise error on - end-of-input.""" - index, input_data = self.m_stack[-1] - bytes_data: bytes = input_data[index : index + count] - if len(bytes_data) != count: - raise Error("Premature end of input.") - self.m_stack[-1][0] += count - return bytes_data - - def _end_of_input(self) -> bool: - """Return True if we are at the end of input.""" - index, input_data = self.m_stack[-1] - return cast(int, index) == len(input_data) - - @staticmethod - def _decode_boolean(bytes_data: bytes) -> bool: - if len(bytes_data) != 1: - raise Error("ASN1 syntax error") - return not bytes_data[0] == 0 - - @staticmethod - def _decode_integer(bytes_data: bytes) -> int: - values = [int(b) for b in bytes_data] - negative = values[0] & 0x80 - if negative: - # make positive by taking two's complement - for i in range(len(values)): - values[i] = 0xFF - values[i] - for i in range(len(values) - 1, -1, -1): - values[i] += 1 - if values[i] <= 0xFF: - break - - values[i] = 0x00 - value = 0 - for val in values: - value = (value << 8) | val - if negative: - value = -value - return value - - @staticmethod - def _decode_octet_string(bytes_data: bytes) -> bytes: - return bytes_data - - @staticmethod - def _decode_null(bytes_data: bytes) -> None: - if len(bytes_data) != 0: - raise Error("ASN1 syntax error") - - @staticmethod - def _decode_object_identifier(bytes_data: bytes) -> str: - result: List[int] = [] - value: int = 0 - for i in range(len(bytes_data)): - byte = int(bytes_data[i]) - if value == 0 and byte == 0x80: - raise Error("ASN1 syntax error") - value = (value << 7) | (byte & 0x7F) - if not byte & 0x80: - result.append(value) - value = 0 - if len(result) == 0 or result[0] > 1599: - raise Error("ASN1 syntax error") - result = [result[0] // 40, result[0] % 40] + result[1:] - return f".{'.'.join(str(x) for x in result)}" - - @staticmethod - def _decode_printable_string(bytes_data: bytes) -> str: - return bytes_data.decode("utf-8") - - @staticmethod - def _decode_ip_address(bytes_data: bytes) -> ipaddress.IPv4Address: - return ipaddress.IPv4Address(int.from_bytes(bytes_data, byteorder="big")) diff --git a/aiosnmp/asn1_rust.pyi b/aiosnmp/asn1_rust.pyi new file mode 100644 index 0000000..c5d20c5 --- /dev/null +++ b/aiosnmp/asn1_rust.pyi @@ -0,0 +1,27 @@ +from typing import Any, Optional, Tuple + +from .asn1 import TClass, TNumber, TType + +class Tag: + number: TNumber + typ: TType + cls: TClass + +class Error(Exception): ... + +class Encoder: + def __init__(self) -> None: ... + def enter(self, number: TNumber, cls: Optional[TClass] = None) -> None: ... + def exit(self) -> None: ... + def write( + self, value: Any, number: Optional[TNumber] = None, typ: Optional[TType] = None, cls: Optional[TClass] = None + ) -> None: ... + def output(self) -> bytes: ... + +class Decoder: + def __init__(self, data: bytes) -> None: ... + def peek(self) -> Tag: ... + def read(self, number: Optional[TNumber] = None) -> Tuple[Tag, Any]: ... + def eof(self) -> bool: ... + def enter(self) -> None: ... + def exit(self) -> None: ... diff --git a/aiosnmp/message.py b/aiosnmp/message.py index f9658d4..ea98892 100644 --- a/aiosnmp/message.py +++ b/aiosnmp/message.py @@ -16,7 +16,8 @@ import random from typing import List, Optional, Union -from .asn1 import Class, Decoder, Encoder, Number +from .asn1 import Class, Number +from .asn1_rust import Decoder, Encoder class SnmpVersion(enum.IntEnum): diff --git a/aiosnmp/protocols.py b/aiosnmp/protocols.py index 8f8c1f9..cbcdd67 100644 --- a/aiosnmp/protocols.py +++ b/aiosnmp/protocols.py @@ -1,7 +1,7 @@ import asyncio from typing import Callable, Dict, List, Optional, Set, Text, Tuple, Union, cast -from .asn1 import Error +from .asn1_rust import Error from .exceptions import ( SnmpErrorAuthorizationError, SnmpErrorBadValue, diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e8d4c82..8a64327 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -32,16 +32,130 @@ jobs: - script: isort -q --check --diff aiosnmp/ tests/ examples/ setup.py displayName: 'Run isort' - - script: black -l 120 -q --check --diff aiosnmp/ tests/ examples/ setup.py + - script: black -q --check --diff aiosnmp/ tests/ examples/ setup.py displayName: 'Run black' - script: mypy aiosnmp/ displayName: 'Run mypy' - - job: tests + - job: macos + dependsOn: + - check + pool: + vmImage: 'macOS-10.15' + + variables: + CIBW_BUILD: cp3[789]* + CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)/tests/test_asn1.py + + steps: + - task: UsePythonVersion@0 + + - script: | + set -o errexit + python3 -m pip install --upgrade pip + python3 -m pip install cibuildwheel==2.0.0a2 + displayName: Install dependencies + + - script: cibuildwheel --output-dir wheelhouse . + displayName: Build wheels + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: wheelhouse + artifactName: macos_wheels + parallel: true + + - job: linux + dependsOn: + - check + pool: + vmImage: 'Ubuntu-20.04' + variables: + CIBW_BUILD: cp3[789]* + CIBW_SKIP: "*_i686" + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_BEFORE_ALL: curl https://sh.rustup.rs -sSf | sh -s -- -y + CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH"' + CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_COMMAND: pytest /project/tests/test_asn1.py + + steps: + - task: UsePythonVersion@0 + + - script: | + set -o errexit + python3 -m pip install --upgrade pip + pip3 install cibuildwheel==2.0.0a2 + displayName: Install dependencies + + - script: cibuildwheel --output-dir wheelhouse . + displayName: Build wheels + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: wheelhouse + artifactName: linux_wheels + parallel: true + + - job: windows + dependsOn: + - check + pool: + vmImage: 'vs2017-win2016' + variables: + CIBW_BUILD: cp3[789]* + CIBW_SKIP: "*-win32" + CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)\\tests\\test_asn1.py + + steps: + - task: UsePythonVersion@0 + + - script: | + set -o errexit + python -m pip install --upgrade pip + pip install cibuildwheel==2.0.0a2 + displayName: Install dependencies + + - script: cibuildwheel --output-dir wheelhouse . + displayName: Build wheels + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: wheelhouse + artifactName: windows_wheels + parallel: true + + - job: sdist + dependsOn: + - check pool: vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + + - script: | + set -o errexit + python -m pip install --upgrade pip + pip install setuptools-rust==0.12.1 + displayName: Install dependencies + + - script: python setup.py sdist + displayName: Build tar.gz + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: dist + artifactName: sdist + + - job: tests + dependsOn: + - linux + pool: + vmImage: 'ubuntu-latest' strategy: matrix: Python37-asyncio: @@ -70,16 +184,21 @@ jobs: architecture: 'x64' displayName: 'Use Python $(python.version)' + - task: DownloadPipelineArtifact@2 + inputs: + artifact: linux_wheels + path: $(System.DefaultWorkingDirectory)/wheels + + - script: pip install --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp + displayName: 'Install aiosnmp' + - script: docker ps displayName: 'Docker PS' - - script: env - displayName: 'Env' - - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.15.2 codecov==2.1.11 displayName: 'Install Dependencies' - - script: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=$(loop) + - script: pytest --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=$(loop) tests/ displayName: 'Run Tests' - script: bash <(curl -s https://codecov.io/bash) @@ -91,9 +210,12 @@ jobs: - job: twine pool: vmImage: 'ubuntu-latest' - dependsOn: - check + - linux + - macos + - windows + - sdist - tests condition: and(succeeded(), startsWith(variables['build.sourceBranch'], 'refs/tags/')) @@ -103,16 +225,33 @@ jobs: versionSpec: '3.9' architecture: 'x64' - - script: pip install wheel twine - displayName: 'Install Dependencies' + - task: DownloadPipelineArtifact@2 + inputs: + artifact: macos_wheels + path: $(System.DefaultWorkingDirectory)/wheels - - script: python setup.py bdist_wheel - displayName: 'Build' + - task: DownloadPipelineArtifact@2 + inputs: + artifact: linux_wheels + path: $(System.DefaultWorkingDirectory)/wheels + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: windows_wheels + path: $(System.DefaultWorkingDirectory)/wheels + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: sdist + path: $(System.DefaultWorkingDirectory)/wheels + + - script: pip install twine + displayName: 'Install Dependencies' - task: TwineAuthenticate@1 displayName: 'Twine Authenticate' inputs: pythonUploadServiceConnection: uploadToPypi - - script: python -m twine upload -r uploadToPypi --config-file $(PYPIRC_PATH) dist/* + - script: python -m twine upload -r uploadToPypi --config-file $(PYPIRC_PATH) wheels/* displayName: 'Upload' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d41c02b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +requires-python = ">=3.7" + +[build-system] +requires = ["setuptools", "wheel", "setuptools-rust"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 +target-version = ['py39'] +include = '\.pyi?$' + +[tool.isort] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +known_first_party = ["aiosnmp"] +known_third_party = ["pytest"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 9216448..428cf8b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ -tox==3.14.3 -tox-docker==1.2.1 +tox==3.23.1 +tox-docker==3.0.0 diff --git a/setup.cfg b/setup.cfg index 392f700..ee05cac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -description-file = README.md +description_file = README.md [aliases] test = pytest @@ -10,13 +10,6 @@ max-line-length = 120 max-complexity = 18 select = B,C,E,F,W,T4,B9 -[isort] -line_length = 120 -multi_line_output = 3 -include_trailing_comma = true -known_first_party = aiosnmp -known_third_party = pytest - [mypy] incremental = true warn_redundant_casts = true diff --git a/setup.py b/setup.py index be78ae2..7bf5911 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,27 @@ -from pathlib import Path +import pathlib +import re from setuptools import setup +from setuptools_rust import Binding, RustExtension -import aiosnmp +BASE = pathlib.Path(__file__).parent -readme = Path(__file__).with_name("README.rst") +readme_file = BASE / "README.rst" +version_file = BASE / "aiosnmp" / "__init__.py" + +version = re.findall(r'^__version__ = "(.+?)"$', version_file.read_text("utf-8"), re.M)[0] setup( name="aiosnmp", - version=aiosnmp.__version__, + version=version, packages=["aiosnmp"], url="https://github.com/hh-h/aiosnmp", license="MIT", author="Valetov Konstantin", author_email="forjob@thetrue.name", description="asyncio SNMP client", - long_description=readme.read_text("utf-8"), + long_description=readme_file.read_text("utf-8"), long_description_content_type="text/x-rst", - setup_requires=["pytest-runner"], - tests_require=["pytest"], classifiers=[ "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", @@ -26,10 +29,14 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Rust", "Development Status :: 4 - Beta", - "Operating System :: POSIX", + "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", "Framework :: AsyncIO", ], python_requires=">=3.7", + rust_extensions=[RustExtension("aiosnmp.asn1_rust", binding=Binding.PyO3)], + zip_safe=False, ) diff --git a/src/decoder.rs b/src/decoder.rs new file mode 100644 index 0000000..34018e5 --- /dev/null +++ b/src/decoder.rs @@ -0,0 +1,253 @@ +use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyTuple}; + +use crate::tag::Tag; +use crate::Error; + +struct Data(usize, Vec); + +#[pyclass] +#[text_signature = "(bytes)"] +pub struct Decoder { + m_stack: Vec, + m_tag: Option, +} + +#[pymethods] +impl Decoder { + #[new] + fn new(data: Vec) -> Self { + let m_stack: Vec = vec![Data(0, data)]; + Decoder { m_stack, m_tag: None } + } + + #[text_signature = "($self)"] + fn peek(&mut self) -> PyResult { + if self.end_of_input() { + return Err(Error::new_err("Input is empty.")); + } + if self.m_tag.is_none() { + self.m_tag = Some(self.read_tag()?); + } + Ok(self.m_tag.unwrap()) + } + + #[text_signature = "($self, number)"] + #[args(number = "None")] + fn read(&mut self, py: Python, number: Option) -> PyResult<(Tag, PyObject)> { + if self.end_of_input() { + return Err(Error::new_err("Input is empty.")); + } + + let tag = self.peek()?; + let length = self.read_length()?; + let number = match number { + Some(number) => number, + None => tag.number | tag.cls, + }; + + let value = self.read_value(py, number, length)?; + self.m_tag = None; + + Ok((tag, value)) + } + + #[text_signature = "($self)"] + fn eof(&mut self) -> bool { + self.end_of_input() + } + + #[text_signature = "($self)"] + fn enter(&mut self) -> PyResult<()> { + let tag = self.peek().unwrap(); + if tag.typ != 0x20 { + return Err(Error::new_err("Cannot enter a non-constructed tag.")); + } + let length = self.read_length()?; + let data = self.read_bytes(length)?; + self.m_stack.push(Data(0, data)); + self.m_tag = None; + + Ok(()) + } + + #[text_signature = "($self)"] + fn exit(&mut self) -> PyResult<()> { + if self.m_stack.len() == 1 { + return Err(Error::new_err("Tag stack is empty.")); + } + self.m_stack.pop(); + self.m_tag = None; + + Ok(()) + } +} + +impl Decoder { + fn read_tag(&mut self) -> PyResult { + let byte = self.read_byte()?; + let cls = byte & 0xC0; + let typ = byte & 0x20; + let mut number = byte & 0x1F; + + if number == 0x1F { + number = 0; + loop { + let byte = self.read_byte()?; + number = (number << 7) | (byte & 0x7F); + if byte & 0x80 == 0 { + break; + } + } + } + + Ok(Tag { number, typ, cls }) + } + + fn read_length(&mut self) -> PyResult { + let byte = self.read_byte()?; + let mut length: u32 = 0; + if byte & 0x80 > 0 { + let count = byte & 0x7F; + if count == 0x7F { + return Err(Error::new_err("ASN1 syntax error")); + } + let data = self.read_bytes(count as u32)?; + for byte in data.into_iter() { + length = (length << 8) | byte as u32; + } + } else { + length = byte as u32; + } + + Ok(length) + } + + fn read_value(&mut self, py: Python, number: u8, length: u32) -> PyResult { + let data = self.read_bytes(length)?; + match number { + 0x01 => Ok(Decoder::decode_boolean(data)?.to_object(py)), + 0x02 | 0x0A | 0x41 | 0x42 | 0x43 | 0x46 | 0x47 => Ok(Decoder::decode_integer(data).to_object(py)), + 0x04 => Ok(PyBytes::new(py, &Decoder::decode_octet_string(data)).to_object(py)), + 0x05 => Ok(Decoder::decode_null(data)?.to_object(py)), + 0x06 => Ok(Decoder::decode_object_identifier(data)?.to_object(py)), + 0x13 | 0x16 | 0x17 => Ok(Decoder::decode_printable_string(data).to_object(py)), + 0x80 | 0x81 | 0x82 => Ok(().to_object(py)), + 0x40 => Ok(Decoder::decode_ip_address(py, data)?.to_object(py)), + _ => Ok(PyBytes::new(py, &data).to_object(py)), + } + } + + fn read_byte(&mut self) -> PyResult { + let mut data = self.m_stack.last_mut().unwrap(); + let byte = match data.1.get(data.0) { + Some(byte) => *byte, + None => return Err(Error::new_err("Premature end of input.")), + }; + data.0 += 1; + + Ok(byte) + } + + fn read_bytes(&mut self, count: u32) -> PyResult> { + let count = count as usize; + let mut data = self.m_stack.last_mut().unwrap(); + let bytes = match data.1.get(data.0..data.0 + count) { + Some(bytes) => bytes.to_vec(), + None => return Err(Error::new_err("Premature end of input.")), + }; + + data.0 += count; + + Ok(bytes) + } + + fn end_of_input(&mut self) -> bool { + let data = self.m_stack.last().unwrap(); + let eof = data.0 == data.1.len(); + eof + } + + fn decode_boolean(data: Vec) -> PyResult { + if data.len() != 1 { + return Err(Error::new_err("ASN1 syntax error")); + } + + Ok(data[0] > 0) + } + + fn decode_integer(data: Vec) -> i128 { + let mut bytes = data; + let negative = bytes[0] & 0x80 > 0; + if negative { + let last_index = bytes.len() - 1; + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = 0xFF - *byte; + if i == last_index { + *byte += 1; + } + } + } + + let mut value: i128 = 0; + for byte in bytes { + value = (value << 8) | byte as i128; + } + if negative { + value = -value; + } + value + } + + fn decode_octet_string(data: Vec) -> Vec { + data + } + + fn decode_null(data: Vec) -> PyResult<()> { + if data.len() > 0 { + return Err(Error::new_err("ASN1 syntax error")); + } + + Ok(()) + } + + fn decode_object_identifier(data: Vec) -> PyResult { + let mut result: Vec = Vec::new(); + let mut value: u32 = 0; + for byte in data.into_iter() { + if value == 0 && byte == 0x80 { + return Err(Error::new_err("ASN1 syntax error")); + } + + value = (value << 7) | (byte & 0x7F) as u32; + if byte & 0x80 == 0 { + result.push(value); + value = 0; + } + } + + if result.len() == 0 || result[0] > 1599 { + return Err(Error::new_err("ASN1 syntax error")); + } + + let mut vec = vec![result[0] / 40, result[0] % 40]; + vec.extend_from_slice(&result[1..]); + let result: Vec = vec.iter().map(|&x| x.to_string()).collect(); + Ok(format!(".{}", result.join("."))) + } + + fn decode_printable_string(data: Vec) -> String { + String::from_utf8(data).unwrap() + } + + fn decode_ip_address(py: Python, data: Vec) -> PyResult<&PyAny> { + let mut int_ip: u32 = data[0] as u32 * 256_u32.pow(3); + int_ip += data[1] as u32 * 256_u32.pow(2); + int_ip += data[2] as u32 * 256_u32; + int_ip += data[3] as u32; + let pt = PyTuple::new(py, &[int_ip]); + let ipaddress = PyModule::import(py, "ipaddress")?; + let ipv4 = ipaddress.call1("IPv4Address", pt)?; + Ok(ipv4) + } +} diff --git a/src/encoder.rs b/src/encoder.rs new file mode 100644 index 0000000..ebd387f --- /dev/null +++ b/src/encoder.rs @@ -0,0 +1,233 @@ +use lazy_static::lazy_static; +use pyo3::prelude::*; +use pyo3::types::{PyBool, PyBytes, PyInt, PyString}; +use regex::Regex; + +use crate::Error; + +lazy_static! { + static ref OID_REGEX: Regex = Regex::new(r"^\d+(?:\.\d+)+$").unwrap(); +} + +#[pyclass] +#[text_signature = "()"] +pub struct Encoder { + m_stack: Vec>, +} + +#[pymethods] +impl Encoder { + #[new] + fn new() -> Self { + let m_stack: Vec> = vec![Vec::new()]; + Encoder { m_stack } + } + + #[text_signature = "($self, number, class)"] + #[args(class = "None")] + fn enter(&mut self, number: u8, class: Option) { + let class = class.unwrap_or(0x00); + self._emit_tag(number, 0x20, class); + self.m_stack.push(Vec::new()) + } + + fn exit(&mut self) -> PyResult<()> { + if self.m_stack.len() == 1 { + return Err(Error::new_err("Tag stack is empty.")); + } + + let value = self.m_stack.pop().unwrap(); + self._emit_length(value.len()); + self._emit(value); + + Ok(()) + } + + #[args(number = "None", typ = "None", class = "None")] + fn write(&mut self, value: &PyAny, number: Option, typ: Option, class: Option) -> PyResult<()> { + let number = match number { + Some(number) => number, + None => { + if value.is_instance::().unwrap() { + 0x01 + } else if value.is_instance::().unwrap() { + 0x02 + } else if value.is_instance::().unwrap() || value.is_instance::().unwrap() { + 0x04 + } else if value.is_none() { + 0x05 + } else if value.get_type().name()? == "IPv4Address" { + 0x40 + } else { + return Err(Error::new_err("Cannot determine Number for value type")); + } + } + }; + let typ = typ.unwrap_or(0x00); + let class = class.unwrap_or(0x00); + let value = self._encode_value(number, value)?; + + self._emit_tag(number, typ, class); + self._emit_length(value.len()); + self._emit(value); + + Ok(()) + } + + fn output<'a>(&self, py: Python<'a>) -> PyResult<&'a PyBytes> { + if self.m_stack.len() != 1 { + return Err(Error::new_err("Stack is not empty.")); + } + + Ok(PyBytes::new(py, &self.m_stack[0])) + } +} + +impl Encoder { + fn _encode_value(&self, number: u8, value: &PyAny) -> PyResult> { + let value = match number { + 0x02 | 0x0A => Encoder::_encode_integer(value)?, + 0x04 | 0x13 => Encoder::_encode_octet_string(value)?, + 0x01 => Encoder::_encode_boolean(value)?, + 0x05 => Encoder::_encode_null(value), + 0x06 => Encoder::_encode_object_identifier(value)?, + 0x40 => Encoder::encode_ip_address(value)?, + _ => return Err(Error::new_err(format!("Unhandled Number {} value {}", number, value))), + }; + Ok(value) + } + + fn _encode_boolean(value: &PyAny) -> PyResult> { + let value = if value.is_true()? { 255 } else { 0 }; + Ok(vec![value]) + } + + fn _encode_integer(value: &PyAny) -> PyResult> { + let value = value.extract::()?; + let (mut value, negative, limit) = if value < 0 { + (-value as u128, true, 0x80) + } else { + (value as u128, false, 0x7F) + }; + + let mut values = Vec::new(); + while value > limit { + values.push((value & 0xFF) as u8); + value >>= 8; + } + values.push((value & 0xFF) as u8); + + if negative { + for v in values.iter_mut() { + *v = 0xFF - *v; + } + for v in values.iter_mut() { + if *v == 0xFF { + *v = 0x00; + continue; + } + *v += 1; + break; + } + } + + let len = values.len(); + if negative && values[len - 1] == 0x7F { + values.push(0xFF); + } + + values.reverse(); + Ok(values) + } + + fn _encode_octet_string(value: &PyAny) -> PyResult> { + if value.is_instance::()? { + Ok(value.extract::()?.into_bytes()) + } else { + Ok(value.downcast::()?.as_bytes().to_vec()) + } + } + + fn _encode_null(_value: &PyAny) -> Vec { + Vec::new() + } + + fn _encode_object_identifier(value: &PyAny) -> PyResult> { + let value = value.extract::<&str>().unwrap(); + if !OID_REGEX.is_match(value) { + return Err(Error::new_err("Illegal object identifier")); + } + let value: Vec = value + .split('.') + .map(|x| x.parse::()) + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .collect(); + + if value[0] > 39 || value[1] > 39 { + return Err(Error::new_err("Illegal object identifier")); + } + + let mut values = vec![40 * value[0] + value[1]]; + values.extend(&value[2..]); + values.reverse(); + let mut result: Vec = Vec::new(); + for value in values.iter() { + result.push((value & 0x7F) as u8); + let mut cmp = *value; + while cmp > 0x7F { + cmp >>= 7; + result.push((0x80 | (cmp & 0x7F)) as u8); + } + } + result.reverse(); + Ok(result) + } + fn encode_ip_address(value: &PyAny) -> PyResult> { + let value = value.call_method0("__int__")?.extract::()?; + Ok(value.to_be_bytes().to_vec()) + } + + fn _emit_tag(&mut self, number: u8, typ: u8, class: u8) { + self._emit_tag_short(number, typ, class) + } + + fn _emit_tag_short(&mut self, number: u8, typ: u8, class: u8) { + self._emit_simple(number | typ | class) + } + + fn _emit(&mut self, value: Vec) { + let len = self.m_stack.len(); + self.m_stack[len - 1].extend(value) + } + + fn _emit_simple(&mut self, value: u8) { + let len = self.m_stack.len(); + self.m_stack[len - 1].push(value) + } + + fn _emit_length(&mut self, length: usize) { + if length < 128 { + self._emit_length_short(length) + } else { + self._emit_length_long(length) + } + } + + fn _emit_length_short(&mut self, length: usize) { + self._emit_simple(length as u8) + } + + fn _emit_length_long(&mut self, length: usize) { + let mut values: Vec = Vec::new(); + let mut length = length; + while length > 0 { + values.push((length & 0xFF) as u8); + length >>= 8; + } + values.reverse(); + let head = (0x80 | values.len()) as u8; + self._emit_simple(head); + self._emit(values); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a710ed6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +use pyo3::exceptions::PyException; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::{create_exception, wrap_pymodule}; + +use crate::decoder::Decoder; +use crate::encoder::Encoder; +use crate::tag::Tag; + +mod decoder; +mod encoder; +mod tag; + +create_exception!(module, Error, PyException); + +#[pymodule] +fn asn1_rust(py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add("Error", py.get_type::())?; + + Ok(()) +} + +#[pymodule] +fn aiosnmp(py: Python, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(asn1_rust))?; + + let sys = PyModule::import(py, "sys")?; + let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + sys_modules.set_item("aiosnmp.asn1_rust", m.getattr("asn1_rust")?)?; + Ok(()) +} diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..77da781 --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; + +#[pyclass] +#[derive(Copy, Clone)] +pub struct Tag { + #[pyo3(get)] + pub number: u8, + #[pyo3(get)] + pub typ: u8, + #[pyo3(get)] + pub cls: u8, +} + +#[pymethods] +impl Tag { + #[new] + fn new(number: u8, typ: u8, cls: u8) -> Self { + Tag { number, typ, cls } + } +} diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/conftest.py b/tests/conftest.py index f159813..14454c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,4 +26,4 @@ def pytest_generate_tests(metafunc): if "host" in metafunc.fixturenames: metafunc.parametrize("host", ["127.0.0.1", "localhost", "::1"]) if "port" in metafunc.fixturenames: - metafunc.parametrize("port", [int(os.environ.get("KOSHH/AIOSNMP_161_UDP", 161))]) + metafunc.parametrize("port", [int(os.environ.get("AIOSNMP_161_UDP_PORT", 161))]) diff --git a/tests/test_asn1.py b/tests/test_asn1.py index a0b39d0..09db522 100644 --- a/tests/test_asn1.py +++ b/tests/test_asn1.py @@ -1,10 +1,11 @@ import ipaddress +import itertools from typing import Any, Optional, Tuple, Type import pytest -import aiosnmp.asn1 as asn1 -from aiosnmp.asn1 import TClass, TNumber, TType +from aiosnmp import asn1 +from aiosnmp.asn1_rust import Decoder, Encoder, Error class TestEncoder: @@ -74,9 +75,10 @@ class TestEncoder: # enumerated (1, asn1.Number.Enumerated, b"\x0a\x01\x01"), ], + ids=itertools.count(), # fix for windows ValueError: the environment variable is longer than 32767 characters ) - def test_simple_encode(self, value: Any, number: Optional[TNumber], expected: bytes) -> None: - encoder = asn1.Encoder() + def test_simple_encode(self, value: Any, number: Optional[asn1.TNumber], expected: bytes) -> None: + encoder = Encoder() encoder.write(value, number) result = encoder.output() assert result == expected @@ -93,8 +95,8 @@ def test_simple_encode(self, value: Any, number: Optional[TNumber], expected: by (1, asn1.Class.Private, (1,), b"\xe1\x03\x02\x01\x01"), ], ) - def test_one_enter(self, number: TNumber, typ: TType, values: Tuple, expected: bytes) -> None: - encoder = asn1.Encoder() + def test_one_enter(self, number: asn1.TNumber, typ: asn1.TType, values: Tuple, expected: bytes) -> None: + encoder = Encoder() encoder.enter(number, typ) for value in values: encoder.write(value) @@ -125,8 +127,10 @@ def test_one_enter(self, number: TNumber, typ: TType, values: Tuple, expected: b ), ], ) - def test_multiple_enter(self, values: Tuple[Tuple[TNumber, Optional[TClass], Tuple]], expected: bytes) -> None: - encoder = asn1.Encoder() + def test_multiple_enter( + self, values: Tuple[Tuple[asn1.TNumber, Optional[asn1.TClass], Tuple]], expected: bytes + ) -> None: + encoder = Encoder() for number, typ, values_ in values: encoder.enter(number, typ) for value in values_: @@ -137,9 +141,9 @@ def test_multiple_enter(self, values: Tuple[Tuple[TNumber, Optional[TClass], Tup assert res == expected def test_error_stack(self) -> None: - encoder = asn1.Encoder() + encoder = Encoder() encoder.enter(asn1.Number.Sequence) - with pytest.raises(asn1.Error, match="Stack is not empty."): + with pytest.raises(Error, match="Stack is not empty."): encoder.output() @pytest.mark.parametrize( @@ -147,23 +151,23 @@ def test_error_stack(self) -> None: ["1", "40.2.3", "1.40.3", "1.2.3.", ".1.2.3", "foo", "foo.bar"], ) def test_error_object_identifier(self, value: str) -> None: - encoder = asn1.Encoder() - with pytest.raises(asn1.Error, match="Illegal object identifier"): + encoder = Encoder() + with pytest.raises(Error, match="Illegal object identifier"): encoder.write(value, asn1.Number.ObjectIdentifier) def test_exit_errors(self) -> None: - encoder = asn1.Encoder() - with pytest.raises(asn1.Error, match="Tag stack is empty."): + encoder = Encoder() + with pytest.raises(Error, match="Tag stack is empty."): encoder.exit() def test_cannot_determine_number(self) -> None: - encoder = asn1.Encoder() - with pytest.raises(asn1.Error, match="Cannot determine Number for value type "): + encoder = Encoder() + with pytest.raises(Error, match="Cannot determine Number for value type"): encoder.write(1.21) def test_invalid_number(self) -> None: - encoder = asn1.Encoder() - with pytest.raises(asn1.Error, match="Unhandled Number 155 value 1"): + encoder = Encoder() + with pytest.raises(Error, match="Unhandled Number 155 value 1"): encoder.write(1, 155) @@ -280,11 +284,12 @@ class TestDecoder: ipaddress.IPv4Address("127.0.0.1"), ), ], + ids=itertools.count(), # fix for windows ValueError: the environment variable is longer than 32767 characters ) def test_simple_decode( - self, buffer: bytes, instance: Type, number: TNumber, typ: TType, cls: TClass, expected: Any + self, buffer: bytes, instance: Type, number: asn1.TNumber, typ: asn1.TType, cls: asn1.TClass, expected: Any ) -> None: - decoder = asn1.Decoder(buffer) + decoder = Decoder(buffer) tag = decoder.peek() assert tag.number == number assert tag.typ == typ @@ -353,8 +358,10 @@ def test_simple_decode( ), ], ) - def test_one_enter(self, buffer: bytes, number: TNumber, typ: TType, cls: TClass, expected_values: Tuple) -> None: - decoder = asn1.Decoder(buffer) + def test_one_enter( + self, buffer: bytes, number: asn1.TNumber, typ: asn1.TType, cls: asn1.TClass, expected_values: Tuple + ) -> None: + decoder = Decoder(buffer) tag = decoder.peek() assert tag.number == number assert tag.typ == typ @@ -393,7 +400,7 @@ def test_one_enter(self, buffer: bytes, number: TNumber, typ: TType, cls: TClass ], ) def test_multiple_enter(self, buffer: bytes, expected_values: Tuple[Tuple]) -> None: - decoder = asn1.Decoder(buffer) + decoder = Decoder(buffer) for expected in expected_values: decoder.enter() @@ -413,7 +420,7 @@ def test_multiple_enter(self, buffer: bytes, expected_values: Tuple[Tuple]) -> N ], ) def test_read_multiple(self, buffer: bytes, expected_values: Tuple[Any]) -> None: - decoder = asn1.Decoder(buffer) + decoder = Decoder(buffer) for expected in expected_values: _, value = decoder.read() assert value == expected @@ -429,8 +436,8 @@ def test_read_multiple(self, buffer: bytes, expected_values: Tuple[Any]) -> None ], ) def test_peek_errors(self, buffer: bytes, error: str) -> None: - decoder = asn1.Decoder(buffer) - with pytest.raises(asn1.Error, match=error): + decoder = Decoder(buffer) + with pytest.raises(Error, match=error): decoder.peek() @pytest.mark.parametrize( @@ -447,28 +454,28 @@ def test_peek_errors(self, buffer: bytes, error: str) -> None: ], ) def test_read_errors(self, buffer: bytes, error: str) -> None: - decoder = asn1.Decoder(buffer) - with pytest.raises(asn1.Error, match=error): + decoder = Decoder(buffer) + with pytest.raises(Error, match=error): decoder.read() def test_cannot_enter(self) -> None: - decoder = asn1.Decoder(b"\x01\x01\xff") - with pytest.raises(asn1.Error, match="Cannot enter a non-constructed tag."): + decoder = Decoder(b"\x01\x01\xff") + with pytest.raises(Error, match="Cannot enter a non-constructed tag."): decoder.enter() def test_premature_exit(self) -> None: - decoder = asn1.Decoder(b"\x01\x01\xff") - with pytest.raises(asn1.Error, match="Tag stack is empty."): + decoder = Decoder(b"\x01\x01\xff") + with pytest.raises(Error, match="Tag stack is empty."): decoder.exit() def test_big_boolean(self) -> None: - decoder = asn1.Decoder(b"\x01\x02\xff\x00") - with pytest.raises(asn1.Error, match="ASN1 syntax error"): + decoder = Decoder(b"\x01\x02\xff\x00") + with pytest.raises(Error, match="ASN1 syntax error"): decoder.read() def test_not_null_null(self) -> None: - decoder = asn1.Decoder(b"\x05\x01\x01") - with pytest.raises(asn1.Error, match="ASN1 syntax error"): + decoder = Decoder(b"\x05\x01\x01") + with pytest.raises(Error, match="ASN1 syntax error"): decoder.read() @@ -503,11 +510,11 @@ class TestEncoderDecoder: (ipaddress.IPv4Address("8.8.8.8"), None), ], ) - def test_simple(self, value: Any, number: Optional[TNumber]) -> None: - encoder = asn1.Encoder() + def test_simple(self, value: Any, number: Optional[asn1.TNumber]) -> None: + encoder = Encoder() encoder.write(value, number) data = encoder.output() - decoder = asn1.Decoder(data) + decoder = Decoder(data) _, decoded = decoder.read() assert decoded == value assert decoder.eof() diff --git a/tox.ini b/tox.ini index 5f5fd06..370b532 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = check, py{37,38,39}-{asyncio,uvloop} +isolated_build = True [testenv] deps = @@ -11,7 +12,7 @@ commands = asyncio: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} uvloop: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} docker = - koshh/aiosnmp:latest + aiosnmp [testenv:check] deps = @@ -22,7 +23,7 @@ deps = commands = flake8 aiosnmp/ tests/ examples/ setup.py isort -q --check --diff aiosnmp/ tests/ examples/ setup.py - black -l 120 -q --check --diff aiosnmp/ tests/ examples/ setup.py + black -q --check --diff aiosnmp/ tests/ examples/ setup.py mypy aiosnmp/ docker = skip_install = true @@ -33,6 +34,9 @@ deps = black == 21.5b1 commands = isort aiosnmp/ tests/ examples/ setup.py - black -l 120 aiosnmp/ tests/ examples/ setup.py + black aiosnmp/ tests/ examples/ setup.py docker = skip_install = true + +[docker:aiosnmp] +image = koshh/aiosnmp:latest From bc985888f41ddb2b797a58c1d84d3216031cbcde Mon Sep 17 00:00:00 2001 From: Konstantin Valetov Date: Sun, 6 Jun 2021 19:43:46 +0300 Subject: [PATCH 15/23] 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.rst | 7 +++++++ aiosnmp/__init__.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed377e7..5c5795a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ [[package]] name = "aiosnmp" -version = "0.5.0" +version = "0.6.0" dependencies = [ "lazy_static", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 86f77b1..476ca69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aiosnmp" -version = "0.5.0" +version = "0.6.0" authors = ["Konstantin Valetov "] edition = "2018" diff --git a/README.rst b/README.rst index 2047f8b..4928659 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,13 @@ aiosnmp aiosnmp is an asynchronous SNMP client for use with asyncio. +Notice on 0.6.0 build +--------------------- + +| If you have some problems with 0.6.0 build, please, create an issue and downgrade to 0.5.0. +| There is no difference between 0.5.0 and 0.6.0 only asn1 parser migrated to rust. + + Installation ------------ diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index f1e34a0..bd95b22 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind") -__version__ = "0.5.0" +__version__ = "0.6.0" __author__ = "Valetov Konstantin" from .message import SnmpV2TrapMessage, SnmpVarbind From cbecacdbd16ffe89005758d212fe840481c25beb Mon Sep 17 00:00:00 2001 From: hh-h Date: Mon, 7 Jun 2021 23:09:50 +0300 Subject: [PATCH 16/23] fixed coverage (#33) --- azure-pipelines.yml | 29 +++++++++++++++++++++++++---- tox.ini | 1 - 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8a64327..236f010 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -192,13 +192,34 @@ jobs: - script: pip install --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp displayName: 'Install aiosnmp' - - script: docker ps - displayName: 'Docker PS' + - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 uvloop==0.15.2 + displayName: 'Install Dependencies' + + - script: pytest --durations=5 --event-loop=$(loop) tests/ + displayName: 'Run Tests' + + services: + snmp: snmp + + - job: coverage + dependsOn: + - linux + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: UsePythonVersion@0 + + - script: pip install setuptools-rust==0.12.1 + displayName: 'Install Build Dependencies' + + - script: python setup.py develop + displayName: 'Compile Rust Module' - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.15.2 codecov==2.1.11 - displayName: 'Install Dependencies' + displayName: 'Install Test Dependencies' - - script: pytest --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=$(loop) tests/ + - script: python -m pytest --cov=aiosnmp --cov-report=term-missing tests/ displayName: 'Run Tests' - script: bash <(curl -s https://codecov.io/bash) diff --git a/tox.ini b/tox.ini index 370b532..8e7bd8a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = check, py{37,38,39}-{asyncio,uvloop} -isolated_build = True [testenv] deps = From ba70fc83bf2bdb6052e074ef08a67f9c2390a62d Mon Sep 17 00:00:00 2001 From: hh-h Date: Wed, 9 Jun 2021 10:53:13 +0300 Subject: [PATCH 17/23] added rust code checks (#34) --- azure-pipelines.yml | 6 ++++++ src/decoder.rs | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 236f010..b321e49 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,6 +38,12 @@ jobs: - script: mypy aiosnmp/ displayName: 'Run mypy' + - script: cargo fmt -- --check + displayName: 'Run rustfmt' + + - script: cargo clippy --all-targets --all-features -- -D warnings + displayName: 'Run clippy' + - job: macos dependsOn: - check diff --git a/src/decoder.rs b/src/decoder.rs index 34018e5..b5dfade 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -164,8 +164,7 @@ impl Decoder { fn end_of_input(&mut self) -> bool { let data = self.m_stack.last().unwrap(); - let eof = data.0 == data.1.len(); - eof + data.0 == data.1.len() } fn decode_boolean(data: Vec) -> PyResult { @@ -204,7 +203,7 @@ impl Decoder { } fn decode_null(data: Vec) -> PyResult<()> { - if data.len() > 0 { + if !data.is_empty() { return Err(Error::new_err("ASN1 syntax error")); } @@ -226,7 +225,7 @@ impl Decoder { } } - if result.len() == 0 || result[0] > 1599 { + if result.is_empty() || result[0] > 1599 { return Err(Error::new_err("ASN1 syntax error")); } From 568ffe1e933262062445d2c056edcc473b8e55e1 Mon Sep 17 00:00:00 2001 From: hh-h Date: Fri, 7 Jan 2022 19:12:03 +0300 Subject: [PATCH 18/23] added python 3.10 support (#37) --- azure-pipelines.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b321e49..ce10d47 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -51,7 +51,7 @@ jobs: vmImage: 'macOS-10.15' variables: - CIBW_BUILD: cp3[789]* + CIBW_BUILD: cp3{7,8,9,10}-* CIBW_TEST_REQUIRES: pytest==6.2.4 CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)/tests/test_asn1.py @@ -61,7 +61,7 @@ jobs: - script: | set -o errexit python3 -m pip install --upgrade pip - python3 -m pip install cibuildwheel==2.0.0a2 + python3 -m pip install cibuildwheel==2.3.1 displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -79,7 +79,7 @@ jobs: pool: vmImage: 'Ubuntu-20.04' variables: - CIBW_BUILD: cp3[789]* + CIBW_BUILD: cp3{7,8,9,10}-* CIBW_SKIP: "*_i686" CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_BEFORE_ALL: curl https://sh.rustup.rs -sSf | sh -s -- -y @@ -93,7 +93,7 @@ jobs: - script: | set -o errexit python3 -m pip install --upgrade pip - pip3 install cibuildwheel==2.0.0a2 + pip3 install cibuildwheel==2.3.1 displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -111,7 +111,7 @@ jobs: pool: vmImage: 'vs2017-win2016' variables: - CIBW_BUILD: cp3[789]* + CIBW_BUILD: cp3{7,8,9,10}-* CIBW_SKIP: "*-win32" CIBW_TEST_REQUIRES: pytest==6.2.4 CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)\\tests\\test_asn1.py @@ -122,7 +122,7 @@ jobs: - script: | set -o errexit python -m pip install --upgrade pip - pip install cibuildwheel==2.0.0a2 + pip install cibuildwheel==2.3.1 displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -182,6 +182,12 @@ jobs: Python39-uvloop: python.version: '3.9' loop: 'uvloop' + Python310-asyncio: + python.version: '3.10' + loop: 'asyncio' + Python310-uvloop: + python.version: '3.10' + loop: 'uvloop' steps: - task: UsePythonVersion@0 @@ -198,7 +204,7 @@ jobs: - script: pip install --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp displayName: 'Install aiosnmp' - - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 uvloop==0.15.2 + - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 uvloop==0.16.0 displayName: 'Install Dependencies' - script: pytest --durations=5 --event-loop=$(loop) tests/ @@ -222,7 +228,7 @@ jobs: - script: python setup.py develop displayName: 'Compile Rust Module' - - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.15.2 codecov==2.1.11 + - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.16.0 codecov==2.1.11 displayName: 'Install Test Dependencies' - script: python -m pytest --cov=aiosnmp --cov-report=term-missing tests/ From 369c58ed85007adbccd44a87bdf78503d23871ee Mon Sep 17 00:00:00 2001 From: hh-h Date: Fri, 7 Jan 2022 19:16:46 +0300 Subject: [PATCH 19/23] 0.7.0 (#38) --- Cargo.toml | 2 +- aiosnmp/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 476ca69..1f7aaaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aiosnmp" -version = "0.6.0" +version = "0.7.0" authors = ["Konstantin Valetov "] edition = "2018" diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index bd95b22..232fdce 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind") -__version__ = "0.6.0" +__version__ = "0.7.0" __author__ = "Valetov Konstantin" from .message import SnmpV2TrapMessage, SnmpVarbind From c670479bea275a8f1dc0496cc0a2f8b07dbcb760 Mon Sep 17 00:00:00 2001 From: hh-h Date: Tue, 23 Aug 2022 14:49:49 +0300 Subject: [PATCH 20/23] Update deps (#42) and some pipeline optimisations --- Cargo.lock | 151 ++++++++++++++---------------------------- Cargo.toml | 4 +- aiosnmp/connection.py | 2 +- aiosnmp/trap.py | 4 +- azure-pipelines.yml | 51 +++++++++----- requirements-dev.txt | 5 +- requirements-docs.txt | 4 +- setup.py | 1 + src/decoder.rs | 14 ++-- src/encoder.rs | 12 ++-- tox.ini | 22 +++--- 11 files changed, 119 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c5795a..ceea7e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.18" @@ -11,7 +13,7 @@ dependencies = [ [[package]] name = "aiosnmp" -version = "0.6.0" +version = "0.7.0" dependencies = [ "lazy_static", "pyo3", @@ -30,49 +32,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "ctor" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "ghost" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "indoc" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" -dependencies = [ - "indoc-impl", - "proc-macro-hack", -] - -[[package]] -name = "indoc-impl" -version = "0.3.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", - "unindent", -] +checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" [[package]] name = "instant" @@ -83,28 +47,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "inventory" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f" -dependencies = [ - "ctor", - "ghost", - "inventory-impl", -] - -[[package]] -name = "inventory-impl" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -132,6 +74,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + [[package]] name = "parking_lot" version = "0.11.1" @@ -157,31 +105,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "paste" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" -dependencies = [ - "paste-impl", - "proc-macro-hack", -] - -[[package]] -name = "paste-impl" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" -dependencies = [ - "proc-macro-hack", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" version = "1.0.27" @@ -193,27 +116,47 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.13.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4837b8e8e18a102c23f79d1e9a110b597ea3b684c95e874eb1ad88f8683109c3" +checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" dependencies = [ "cfg-if", - "ctor", "indoc", - "inventory", "libc", "parking_lot", - "paste", + "pyo3-build-config", + "pyo3-ffi", "pyo3-macros", "unindent", ] +[[package]] +name = "pyo3-build-config" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +dependencies = [ + "libc", + "pyo3-build-config", +] + [[package]] name = "pyo3-macros" -version = "0.13.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47f2c300ceec3e58064fd5f8f5b61230f2ffd64bde4970c81fdd0563a2db1bb" +checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" dependencies = [ + "proc-macro2", "pyo3-macros-backend", "quote", "syn", @@ -221,9 +164,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.13.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b097e5d84fcbe3e167f400fbedd657820a375b034c78bd852050749a575d66" +checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" dependencies = [ "proc-macro2", "quote", @@ -250,9 +193,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -261,9 +204,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "scopeguard" @@ -288,6 +231,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "target-lexicon" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 1f7aaaf..2e52ad3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,6 @@ name = "asn1_rust" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.13.2", features = ["extension-module"] } -regex = "1.5.4" +pyo3 = { version = "0.16.5", features = ["extension-module"] } +regex = "1.5.5" lazy_static = "1.4.0" diff --git a/aiosnmp/connection.py b/aiosnmp/connection.py index 77bc56d..a8c7cc0 100644 --- a/aiosnmp/connection.py +++ b/aiosnmp/connection.py @@ -56,7 +56,7 @@ async def _connect(self) -> None: ) transport, protocol = await asyncio.wait_for(connect_future, timeout=self.timeout) - self._protocol = cast(SnmpProtocol, protocol) + self._protocol = protocol self._transport = cast(asyncio.DatagramTransport, transport) @property diff --git a/aiosnmp/trap.py b/aiosnmp/trap.py index ab77898..b026ec0 100644 --- a/aiosnmp/trap.py +++ b/aiosnmp/trap.py @@ -1,7 +1,7 @@ __all__ = ("SnmpV2TrapServer",) import asyncio -from typing import Callable, Iterable, Optional, Set, Tuple, cast +from typing import Callable, Iterable, Optional, Set, Tuple from .message import SnmpV2TrapMessage from .protocols import SnmpTrapProtocol @@ -36,4 +36,4 @@ async def run(self) -> Tuple[asyncio.BaseTransport, SnmpTrapProtocol]: lambda: SnmpTrapProtocol(self.communities, self.handler), local_addr=(self.host, self.port), ) - return transport, cast(SnmpTrapProtocol, protocol) + return transport, protocol diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ce10d47..7fe9b30 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -5,6 +5,22 @@ trigger: pr: - master +variables: + pytest_ver: 7.1.2 + mypy_ver: 0.971 + isort_ver: 5.10.1 + flake8_ver: 5.0.4 + black_ver: 22.6.0 + cibuildwheel_ver: 2.9.0 + setuptools_rust_ver: 1.5.1 + pytest_asyncio_ver: 0.19.0 + uvloop_ver: 0.16.0 + pytest_cov_ver: 3.0.0 + codecov_ver: 2.1.12 + windows_image: 'windows-2022' + linux_image: 'ubuntu-22.04' + macos_image: 'macOS-12' + resources: containers: - container: snmp @@ -20,10 +36,10 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.10' architecture: 'x64' - - script: pip install mypy==0.812 isort==5.8.0 flake8==3.9.2 black==21.5b1 + - script: pip install mypy==$(mypy_ver) isort==$(isort_ver) flake8==$(flake8_ver) black==$(black_ver) displayName: 'Install Dependencies' - script: flake8 aiosnmp/ tests/ examples/ setup.py @@ -41,18 +57,19 @@ jobs: - script: cargo fmt -- --check displayName: 'Run rustfmt' - - script: cargo clippy --all-targets --all-features -- -D warnings + # -A clippy::borrow_deref_ref should be removed https://github.com/rust-lang/rust-clippy/issues/8971 + - script: cargo clippy --all-targets --all-features -- -D warnings -A clippy::borrow_deref_ref displayName: 'Run clippy' - job: macos dependsOn: - check pool: - vmImage: 'macOS-10.15' + vmImage: $(macos_image) variables: CIBW_BUILD: cp3{7,8,9,10}-* - CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_REQUIRES: pytest==$(pytest_ver) CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)/tests/test_asn1.py steps: @@ -61,7 +78,7 @@ jobs: - script: | set -o errexit python3 -m pip install --upgrade pip - python3 -m pip install cibuildwheel==2.3.1 + python3 -m pip install cibuildwheel==$(cibuildwheel_ver) displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -77,14 +94,14 @@ jobs: dependsOn: - check pool: - vmImage: 'Ubuntu-20.04' + vmImage: $(linux_image) variables: CIBW_BUILD: cp3{7,8,9,10}-* CIBW_SKIP: "*_i686" CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_BEFORE_ALL: curl https://sh.rustup.rs -sSf | sh -s -- -y CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH"' - CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_REQUIRES: pytest==$(pytest_ver) CIBW_TEST_COMMAND: pytest /project/tests/test_asn1.py steps: @@ -93,7 +110,7 @@ jobs: - script: | set -o errexit python3 -m pip install --upgrade pip - pip3 install cibuildwheel==2.3.1 + pip3 install cibuildwheel==$(cibuildwheel_ver) displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -109,11 +126,11 @@ jobs: dependsOn: - check pool: - vmImage: 'vs2017-win2016' + vmImage: $(windows_image) variables: CIBW_BUILD: cp3{7,8,9,10}-* CIBW_SKIP: "*-win32" - CIBW_TEST_REQUIRES: pytest==6.2.4 + CIBW_TEST_REQUIRES: pytest==$(pytest_ver) CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)\\tests\\test_asn1.py steps: @@ -122,7 +139,7 @@ jobs: - script: | set -o errexit python -m pip install --upgrade pip - pip install cibuildwheel==2.3.1 + pip install cibuildwheel==$(cibuildwheel_ver) displayName: Install dependencies - script: cibuildwheel --output-dir wheelhouse . @@ -146,7 +163,7 @@ jobs: - script: | set -o errexit python -m pip install --upgrade pip - pip install setuptools-rust==0.12.1 + pip install setuptools-rust==$(setuptools_rust_ver) displayName: Install dependencies - script: python setup.py sdist @@ -204,7 +221,7 @@ jobs: - script: pip install --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp displayName: 'Install aiosnmp' - - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 uvloop==0.16.0 + - script: pip install pytest==$(pytest_ver) pytest-asyncio==$(pytest_asyncio_ver) uvloop==$(uvloop_ver) displayName: 'Install Dependencies' - script: pytest --durations=5 --event-loop=$(loop) tests/ @@ -222,13 +239,13 @@ jobs: steps: - task: UsePythonVersion@0 - - script: pip install setuptools-rust==0.12.1 + - script: pip install setuptools-rust==$(setuptools_rust_ver) displayName: 'Install Build Dependencies' - script: python setup.py develop displayName: 'Compile Rust Module' - - script: pip install pytest==6.2.4 pytest-asyncio==0.15.1 pytest-cov==2.12.0 uvloop==0.16.0 codecov==2.1.11 + - script: pip install pytest==$(pytest_ver) pytest-asyncio==$(pytest_asyncio_ver) pytest-cov==$(pytest_cov_ver) uvloop==$(uvloop_ver) codecov==$(codecov_ver) displayName: 'Install Test Dependencies' - script: python -m pytest --cov=aiosnmp --cov-report=term-missing tests/ @@ -255,7 +272,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.9' + versionSpec: '3.10' architecture: 'x64' - task: DownloadPipelineArtifact@2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 428cf8b..1cd9249 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ -tox==3.23.1 -tox-docker==3.0.0 +tox==3.25.1 +tox-docker==3.1.0 +setuptools-rust==1.5.1 diff --git a/requirements-docs.txt b/requirements-docs.txt index 8c7b1fd..584cba1 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,2 @@ -Sphinx==4.0.2 -sphinx-rtd-theme==0.5.2 +Sphinx==5.1.1 +sphinx-rtd-theme==1.0.0 diff --git a/setup.py b/setup.py index 7bf5911..5ea12e3 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Rust", "Development Status :: 4 - Beta", "Operating System :: POSIX :: Linux", diff --git a/src/decoder.rs b/src/decoder.rs index b5dfade..1dbf5e7 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -7,7 +7,7 @@ use crate::Error; struct Data(usize, Vec); #[pyclass] -#[text_signature = "(bytes)"] +#[pyo3(text_signature = "(bytes)")] pub struct Decoder { m_stack: Vec, m_tag: Option, @@ -21,7 +21,7 @@ impl Decoder { Decoder { m_stack, m_tag: None } } - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] fn peek(&mut self) -> PyResult { if self.end_of_input() { return Err(Error::new_err("Input is empty.")); @@ -32,7 +32,7 @@ impl Decoder { Ok(self.m_tag.unwrap()) } - #[text_signature = "($self, number)"] + #[pyo3(text_signature = "($self, number)")] #[args(number = "None")] fn read(&mut self, py: Python, number: Option) -> PyResult<(Tag, PyObject)> { if self.end_of_input() { @@ -52,12 +52,12 @@ impl Decoder { Ok((tag, value)) } - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] fn eof(&mut self) -> bool { self.end_of_input() } - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] fn enter(&mut self) -> PyResult<()> { let tag = self.peek().unwrap(); if tag.typ != 0x20 { @@ -71,7 +71,7 @@ impl Decoder { Ok(()) } - #[text_signature = "($self)"] + #[pyo3(text_signature = "($self)")] fn exit(&mut self) -> PyResult<()> { if self.m_stack.len() == 1 { return Err(Error::new_err("Tag stack is empty.")); @@ -246,7 +246,7 @@ impl Decoder { int_ip += data[3] as u32; let pt = PyTuple::new(py, &[int_ip]); let ipaddress = PyModule::import(py, "ipaddress")?; - let ipv4 = ipaddress.call1("IPv4Address", pt)?; + let ipv4 = ipaddress.getattr("IPv4Address")?.call1(pt)?; Ok(ipv4) } } diff --git a/src/encoder.rs b/src/encoder.rs index ebd387f..b89d8ce 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -10,7 +10,7 @@ lazy_static! { } #[pyclass] -#[text_signature = "()"] +#[pyo3(text_signature = "()")] pub struct Encoder { m_stack: Vec>, } @@ -23,7 +23,7 @@ impl Encoder { Encoder { m_stack } } - #[text_signature = "($self, number, class)"] + #[pyo3(text_signature = "($self, number, class)")] #[args(class = "None")] fn enter(&mut self, number: u8, class: Option) { let class = class.unwrap_or(0x00); @@ -48,11 +48,11 @@ impl Encoder { let number = match number { Some(number) => number, None => { - if value.is_instance::().unwrap() { + if value.is_instance_of::().unwrap() { 0x01 - } else if value.is_instance::().unwrap() { + } else if value.is_instance_of::().unwrap() { 0x02 - } else if value.is_instance::().unwrap() || value.is_instance::().unwrap() { + } else if value.is_instance_of::().unwrap() || value.is_instance_of::().unwrap() { 0x04 } else if value.is_none() { 0x05 @@ -141,7 +141,7 @@ impl Encoder { } fn _encode_octet_string(value: &PyAny) -> PyResult> { - if value.is_instance::()? { + if value.is_instance_of::()? { Ok(value.extract::()?.into_bytes()) } else { Ok(value.downcast::()?.as_bytes().to_vec()) diff --git a/tox.ini b/tox.ini index 8e7bd8a..0699a57 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = check, py{37,38,39}-{asyncio,uvloop} +envlist = check, py{37,38,39,310}-{asyncio,uvloop} [testenv] deps = - pytest == 6.2.4 - pytest-asyncio == 0.15.1 - pytest-cov == 2.12.0 - uvloop: uvloop == 0.15.2 + pytest == 7.1.2 + pytest-asyncio == 0.19.0 + pytest-cov == 3.0.0 + uvloop: uvloop == 0.16.0 commands = asyncio: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} uvloop: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} @@ -15,10 +15,10 @@ docker = [testenv:check] deps = - flake8 == 3.9.2 - isort == 5.8.0 - black == 21.5b1 - mypy == 0.812 + flake8 == 5.0.4 + isort == 5.10.1 + black == 22.6.0 + mypy == 0.971 commands = flake8 aiosnmp/ tests/ examples/ setup.py isort -q --check --diff aiosnmp/ tests/ examples/ setup.py @@ -29,8 +29,8 @@ skip_install = true [testenv:format] deps = - isort == 5.8.0 - black == 21.5b1 + isort == 5.10.1 + black == 22.6.0 commands = isort aiosnmp/ tests/ examples/ setup.py black aiosnmp/ tests/ examples/ setup.py From 365b3c8a3973f4fdfad5dfb6330b8922b8d4cbaf Mon Sep 17 00:00:00 2001 From: hh-h Date: Sun, 4 Dec 2022 09:11:01 +0400 Subject: [PATCH 21/23] add the ability to specify number (#43) --- aiosnmp/__init__.py | 3 ++- aiosnmp/message.py | 12 ++++++++++-- aiosnmp/snmp.py | 27 ++++++++++++++++++++------- azure-pipelines.yml | 2 +- examples/snmp.py | 5 +++++ src/encoder.rs | 6 +++--- tests/test_snmp.py | 8 +++++++- 7 files changed, 48 insertions(+), 15 deletions(-) diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 232fdce..4038c21 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,7 +1,8 @@ -__all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind") +__all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind", "SnmpType") __version__ = "0.7.0" __author__ = "Valetov Konstantin" +from .asn1 import Number as SnmpType from .message import SnmpV2TrapMessage, SnmpVarbind from .snmp import Snmp from .trap import SnmpV2TrapServer diff --git a/aiosnmp/message.py b/aiosnmp/message.py index ea98892..19b4348 100644 --- a/aiosnmp/message.py +++ b/aiosnmp/message.py @@ -39,15 +39,17 @@ class PDUType(enum.IntEnum): class SnmpVarbind: - __slots__ = ("_oid", "_value") + __slots__ = ("_oid", "_value", "_number") def __init__( self, oid: str, value: Union[None, str, int, bytes, ipaddress.IPv4Address] = None, + number: Optional[Number] = None, ) -> None: self._oid: str = oid.lstrip(".") self._value: Union[None, str, int, bytes, ipaddress.IPv4Address] = value + self._number: Optional[Number] = number @property def oid(self) -> str: @@ -61,10 +63,16 @@ def value(self) -> Union[None, str, int, bytes, ipaddress.IPv4Address]: return self._value + @property + def number(self) -> Optional[Number]: + """This property stores number of the message""" + + return self._number + def encode(self, encoder: Encoder) -> None: encoder.enter(Number.Sequence) encoder.write(self._oid, Number.ObjectIdentifier) - encoder.write(self.value) + encoder.write(self.value, self.number) encoder.exit() diff --git a/aiosnmp/snmp.py b/aiosnmp/snmp.py index 2df8d37..75094ec 100644 --- a/aiosnmp/snmp.py +++ b/aiosnmp/snmp.py @@ -4,10 +4,14 @@ from types import TracebackType from typing import Any, List, Optional, Tuple, Type, Union +from .asn1 import Number from .connection import SnmpConnection from .exceptions import SnmpUnsupportedValueType from .message import GetBulkRequest, GetNextRequest, GetRequest, SetRequest, SnmpMessage, SnmpVarbind, SnmpVersion +SetParamsWithoutType = Tuple[str, Union[int, str, bytes, ipaddress.IPv4Address]] +SetParamsWithType = Tuple[str, Union[int, str, bytes, ipaddress.IPv4Address], Optional[Number]] + class Snmp(SnmpConnection): """This is class for initializing Snmp interface. @@ -162,10 +166,10 @@ async def walk(self, oid: str) -> List[SnmpVarbind]: varbinds.append(vbs[0]) return varbinds - async def set(self, varbinds: List[Tuple[str, Union[int, str, bytes, ipaddress.IPv4Address]]]) -> List[SnmpVarbind]: + async def set(self, varbinds: List[Union[SetParamsWithoutType, SetParamsWithType]]) -> List[SnmpVarbind]: """The set method is used to modify the value(s) of the managed object. - :param varbinds: list of tuples[oid, int/str/bytes/ipv4] + :param varbinds: list of tuples [oid, int/str/bytes/ipv4] or [oid, int/str/bytes/ipv4, SnmpType] :return: list of :class:`SnmpVarbind ` Example @@ -176,18 +180,27 @@ async def set(self, varbinds: List[Tuple[str, Union[int, str, bytes, ipaddress.I for res in await snmp.set([ (".1.3.6.1.2.1.1.1.0", 10), (".1.3.6.1.2.1.1.1.1", "hello"), + (".1.3.6.1.2.1.1.1.11", 10, SnmpType.Gauge32), ]): print(res.oid, res.value) """ + snmp_varbinds = [] for varbind in varbinds: if not isinstance(varbind[1], (int, str, bytes, ipaddress.IPv4Address)): raise SnmpUnsupportedValueType(f"Only int, str, bytes and ip address supported, got {type(varbind[1])}") - message = SnmpMessage( - self.version, - self.community, - SetRequest([SnmpVarbind(oid, value) for oid, value in varbinds]), - ) + + if len(varbind) == 2: + oid, value = varbind # type: ignore[misc] + number = None + elif len(varbind) == 3: + oid, value, number = varbind # type: ignore[misc] + else: + raise SnmpUnsupportedValueType(f"varbinds can consist of only two or three values, got {len(varbind)}") + + snmp_varbinds.append(SnmpVarbind(oid, value, number)) + + message = SnmpMessage(self.version, self.community, SetRequest(snmp_varbinds)) return await self._send(message) async def bulk_walk( diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7fe9b30..0a49384 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -218,7 +218,7 @@ jobs: artifact: linux_wheels path: $(System.DefaultWorkingDirectory)/wheels - - script: pip install --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp + - script: pip install --no-index --find-links $(System.DefaultWorkingDirectory)/wheels aiosnmp displayName: 'Install aiosnmp' - script: pip install pytest==$(pytest_ver) pytest-asyncio==$(pytest_asyncio_ver) uvloop==$(uvloop_ver) diff --git a/examples/snmp.py b/examples/snmp.py index 8ab7dfc..8d2d107 100644 --- a/examples/snmp.py +++ b/examples/snmp.py @@ -32,5 +32,10 @@ async def main(): for res in results: print(res.oid, res.value) + # set with custom number + results = await snmp.set([(".1.3.6.1.2.1.1.4.0", 10, aiosnmp.SnmpType.Gauge32)]) + for res in results: + print(res.oid, res.value) + asyncio.run(main()) diff --git a/src/encoder.rs b/src/encoder.rs index b89d8ce..b3bb34f 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -86,12 +86,12 @@ impl Encoder { impl Encoder { fn _encode_value(&self, number: u8, value: &PyAny) -> PyResult> { let value = match number { - 0x02 | 0x0A => Encoder::_encode_integer(value)?, + 0x02 | 0x0A | 0x41 | 0x42 | 0x43 | 0x46 | 0x47 => Encoder::_encode_integer(value)?, 0x04 | 0x13 => Encoder::_encode_octet_string(value)?, 0x01 => Encoder::_encode_boolean(value)?, 0x05 => Encoder::_encode_null(value), 0x06 => Encoder::_encode_object_identifier(value)?, - 0x40 => Encoder::encode_ip_address(value)?, + 0x40 => Encoder::_encode_ip_address(value)?, _ => return Err(Error::new_err(format!("Unhandled Number {} value {}", number, value))), }; Ok(value) @@ -183,7 +183,7 @@ impl Encoder { result.reverse(); Ok(result) } - fn encode_ip_address(value: &PyAny) -> PyResult> { + fn _encode_ip_address(value: &PyAny) -> PyResult> { let value = value.call_method0("__int__")?.extract::()?; Ok(value.to_be_bytes().to_vec()) } diff --git a/tests/test_snmp.py b/tests/test_snmp.py index cb67253..df7dc37 100644 --- a/tests/test_snmp.py +++ b/tests/test_snmp.py @@ -3,7 +3,7 @@ import pytest -from aiosnmp import Snmp +from aiosnmp import Snmp, SnmpType @pytest.mark.asyncio @@ -155,6 +155,12 @@ async def test_snmp_multiple_oids(host: str, port: int, oids: List[str], values: (".1.3.6.1.4.1.8072.2.255.1.0", b"test_bytes"), (".1.3.6.1.4.1.8072.2.255.6.0", 42), ], + [(".1.3.6.1.4.1.8072.2.255.6.0", 42, SnmpType.Gauge32)], + [ + (".1.3.6.1.4.1.8072.2.255.1.0", b"test_bytes"), + (".1.3.6.1.4.1.8072.2.255.5.0", 42), + (".1.3.6.1.4.1.8072.2.255.6.0", 42, SnmpType.Counter32), + ], ), ) async def test_snmp_set(host: str, port: int, varbinds: List[Tuple[str, Union[int, str, bytes]]]) -> None: From 113c8af6258689eeed7d120ed06db12565f2e659 Mon Sep 17 00:00:00 2001 From: hh-h Date: Mon, 5 Dec 2022 11:32:08 +0400 Subject: [PATCH 22/23] 0.7.1 (#44) --- Cargo.lock | 187 +++++++++++++++++++++++++----------------- Cargo.toml | 8 +- MANIFEST.in | 1 + README.rst | 7 -- aiosnmp/__init__.py | 2 +- aiosnmp/py.typed | 0 azure-pipelines.yml | 28 ++++--- docs/conf.py | 4 +- pyproject.toml | 5 +- requirements-dev.txt | 4 +- requirements-docs.txt | 4 +- setup.py | 1 + src/decoder.rs | 2 +- src/encoder.rs | 3 +- tox.ini | 16 ++-- 15 files changed, 155 insertions(+), 117 deletions(-) create mode 100644 aiosnmp/py.typed diff --git a/Cargo.lock b/Cargo.lock index ceea7e8..7f44e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,27 +4,33 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "aiosnmp" -version = "0.7.0" +version = "0.7.1" dependencies = [ "lazy_static", "pyo3", "regex", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cfg-if" @@ -38,15 +44,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3" -[[package]] -name = "instant" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" -dependencies = [ - "cfg-if", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -55,74 +52,83 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.94" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "memchr" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] [[package]] name = "once_cell" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-sys", ] [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "pyo3" -version = "0.16.5" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" +checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543" dependencies = [ "cfg-if", "indoc", "libc", + "memoffset", "parking_lot", "pyo3-build-config", "pyo3-ffi", @@ -132,9 +138,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.5" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8" dependencies = [ "once_cell", "target-lexicon", @@ -142,9 +148,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.5" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc" dependencies = [ "libc", "pyo3-build-config", @@ -152,9 +158,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.5" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" +checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -164,9 +170,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.5" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" +checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f" dependencies = [ "proc-macro2", "quote", @@ -175,27 +181,27 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -204,9 +210,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "scopeguard" @@ -216,57 +222,92 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "syn" -version = "1.0.72" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "target-lexicon" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" +checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unindent" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" +checksum = "58ee9362deb4a96cef4d437d1ad49cffc9b9e92d202b6995674e928ce684f112" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-sys" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 2e52ad3..4df4738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "aiosnmp" -version = "0.7.0" +version = "0.7.1" authors = ["Konstantin Valetov "] -edition = "2018" +edition = "2021" [lib] name = "asn1_rust" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.16.5", features = ["extension-module"] } -regex = "1.5.5" +pyo3 = { version = "0.17.3", features = ["extension-module"] } +regex = "1.7.0" lazy_static = "1.4.0" diff --git a/MANIFEST.in b/MANIFEST.in index 7c68298..b9c21d4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include Cargo.toml recursive-include src * +include aiosnmp/py.typed diff --git a/README.rst b/README.rst index 4928659..2047f8b 100644 --- a/README.rst +++ b/README.rst @@ -34,13 +34,6 @@ aiosnmp aiosnmp is an asynchronous SNMP client for use with asyncio. -Notice on 0.6.0 build ---------------------- - -| If you have some problems with 0.6.0 build, please, create an issue and downgrade to 0.5.0. -| There is no difference between 0.5.0 and 0.6.0 only asn1 parser migrated to rust. - - Installation ------------ diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 4038c21..238ab38 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind", "SnmpType") -__version__ = "0.7.0" +__version__ = "0.7.1" __author__ = "Valetov Konstantin" from .asn1 import Number as SnmpType diff --git a/aiosnmp/py.typed b/aiosnmp/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0a49384..a6e21d3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,16 +6,16 @@ pr: - master variables: - pytest_ver: 7.1.2 - mypy_ver: 0.971 + pytest_ver: 7.2.0 + mypy_ver: 0.991 isort_ver: 5.10.1 flake8_ver: 5.0.4 - black_ver: 22.6.0 - cibuildwheel_ver: 2.9.0 - setuptools_rust_ver: 1.5.1 - pytest_asyncio_ver: 0.19.0 - uvloop_ver: 0.16.0 - pytest_cov_ver: 3.0.0 + black_ver: 22.10.0 + cibuildwheel_ver: 2.11.2 + setuptools_rust_ver: 1.5.2 + pytest_asyncio_ver: 0.20.2 + uvloop_ver: 0.17.0 + pytest_cov_ver: 4.0.0 codecov_ver: 2.1.12 windows_image: 'windows-2022' linux_image: 'ubuntu-22.04' @@ -68,7 +68,7 @@ jobs: vmImage: $(macos_image) variables: - CIBW_BUILD: cp3{7,8,9,10}-* + CIBW_BUILD: cp3{7,8,9,10,11}-* CIBW_TEST_REQUIRES: pytest==$(pytest_ver) CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)/tests/test_asn1.py @@ -96,7 +96,7 @@ jobs: pool: vmImage: $(linux_image) variables: - CIBW_BUILD: cp3{7,8,9,10}-* + CIBW_BUILD: cp3{7,8,9,10,11}-* CIBW_SKIP: "*_i686" CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 CIBW_BEFORE_ALL: curl https://sh.rustup.rs -sSf | sh -s -- -y @@ -128,7 +128,7 @@ jobs: pool: vmImage: $(windows_image) variables: - CIBW_BUILD: cp3{7,8,9,10}-* + CIBW_BUILD: cp3{7,8,9,10,11}-* CIBW_SKIP: "*-win32" CIBW_TEST_REQUIRES: pytest==$(pytest_ver) CIBW_TEST_COMMAND: pytest $(Build.SourcesDirectory)\\tests\\test_asn1.py @@ -205,6 +205,12 @@ jobs: Python310-uvloop: python.version: '3.10' loop: 'uvloop' + Python311-asyncio: + python.version: '3.11' + loop: 'asyncio' + Python311-uvloop: + python.version: '3.11' + loop: 'uvloop' steps: - task: UsePythonVersion@0 diff --git a/docs/conf.py b/docs/conf.py index d2c4e4a..06b3b30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'aiosnmp' -copyright = '2021, Valetov Konstantin' +copyright = '2022, Valetov Konstantin' author = 'Valetov Konstantin' # The full version, including alpha/beta/rc tags -release = '0.5.0' +release = '0.7.1' # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d41c02b..1cacda9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,10 @@ -[project] -requires-python = ">=3.7" - [build-system] requires = ["setuptools", "wheel", "setuptools-rust"] build-backend = "setuptools.build_meta" [tool.black] line-length = 120 -target-version = ['py39'] +target-version = ['py310'] include = '\.pyi?$' [tool.isort] diff --git a/requirements-dev.txt b/requirements-dev.txt index 1cd9249..c50afc1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,3 @@ -tox==3.25.1 +tox==3.27.1 tox-docker==3.1.0 -setuptools-rust==1.5.1 +setuptools-rust==1.5.2 diff --git a/requirements-docs.txt b/requirements-docs.txt index 584cba1..24bbc56 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,2 +1,2 @@ -Sphinx==5.1.1 -sphinx-rtd-theme==1.0.0 +Sphinx==5.3.0 +sphinx-rtd-theme==1.1.1 diff --git a/setup.py b/setup.py index 5ea12e3..8dec593 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Rust", "Development Status :: 4 - Beta", "Operating System :: POSIX :: Linux", diff --git a/src/decoder.rs b/src/decoder.rs index 1dbf5e7..3edbc84 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -244,7 +244,7 @@ impl Decoder { int_ip += data[1] as u32 * 256_u32.pow(2); int_ip += data[2] as u32 * 256_u32; int_ip += data[3] as u32; - let pt = PyTuple::new(py, &[int_ip]); + let pt = PyTuple::new(py, [int_ip]); let ipaddress = PyModule::import(py, "ipaddress")?; let ipv4 = ipaddress.getattr("IPv4Address")?.call1(pt)?; Ok(ipv4) diff --git a/src/encoder.rs b/src/encoder.rs index b3bb34f..612755a 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -160,8 +160,7 @@ impl Encoder { let value: Vec = value .split('.') .map(|x| x.parse::()) - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) + .filter_map(|x| x.ok()) .collect(); if value[0] > 39 || value[1] > 39 { diff --git a/tox.ini b/tox.ini index 0699a57..f0a0d3b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = check, py{37,38,39,310}-{asyncio,uvloop} +envlist = check, py{37,38,39,310,311}-{asyncio,uvloop} [testenv] deps = - pytest == 7.1.2 - pytest-asyncio == 0.19.0 - pytest-cov == 3.0.0 - uvloop: uvloop == 0.16.0 + pytest == 7.2.0 + pytest-asyncio == 0.20.2 + pytest-cov == 4.0.0 + uvloop: uvloop == 0.17.0 commands = asyncio: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=asyncio {posargs} uvloop: pytest -v --durations=5 --cov=aiosnmp --cov-report=term-missing --event-loop=uvloop {posargs} @@ -17,8 +17,8 @@ docker = deps = flake8 == 5.0.4 isort == 5.10.1 - black == 22.6.0 - mypy == 0.971 + black == 22.10.0 + mypy == 0.991 commands = flake8 aiosnmp/ tests/ examples/ setup.py isort -q --check --diff aiosnmp/ tests/ examples/ setup.py @@ -30,7 +30,7 @@ skip_install = true [testenv:format] deps = isort == 5.10.1 - black == 22.6.0 + black == 22.10.0 commands = isort aiosnmp/ tests/ examples/ setup.py black aiosnmp/ tests/ examples/ setup.py From 447b8ef463c44ac957d44b7493b9ab238c6a3949 Mon Sep 17 00:00:00 2001 From: hh-h Date: Mon, 5 Dec 2022 12:48:45 +0400 Subject: [PATCH 23/23] 0.7.2 (#45) --- Cargo.toml | 2 +- aiosnmp/__init__.py | 2 +- docs/conf.py | 2 +- setup.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4df4738..47f60e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aiosnmp" -version = "0.7.1" +version = "0.7.2" authors = ["Konstantin Valetov "] edition = "2021" diff --git a/aiosnmp/__init__.py b/aiosnmp/__init__.py index 238ab38..fc25d5c 100644 --- a/aiosnmp/__init__.py +++ b/aiosnmp/__init__.py @@ -1,5 +1,5 @@ __all__ = ("Snmp", "SnmpV2TrapMessage", "SnmpV2TrapServer", "exceptions", "SnmpVarbind", "SnmpType") -__version__ = "0.7.1" +__version__ = "0.7.2" __author__ = "Valetov Konstantin" from .asn1 import Number as SnmpType diff --git a/docs/conf.py b/docs/conf.py index 06b3b30..3e6eda6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Valetov Konstantin' # The full version, including alpha/beta/rc tags -release = '0.7.1' +release = '0.7.2' # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 8dec593..8fc0e1f 100644 --- a/setup.py +++ b/setup.py @@ -41,4 +41,5 @@ python_requires=">=3.7", rust_extensions=[RustExtension("aiosnmp.asn1_rust", binding=Binding.PyO3)], zip_safe=False, + include_package_data=True, )