├── dist └── .gitkeep ├── Pipfile ├── aio_api_ros ├── __init__.py ├── errors.py ├── creators.py ├── parser.py ├── simple_pool.py ├── unpacker.py └── connection.py ├── setup.py ├── LICENSE ├── .gitignore ├── README.md └── Pipfile.lock /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | flake8 = "*" 8 | pylint = "*" 9 | 10 | [packages] 11 | 12 | [requires] 13 | python_version = "3.7" 14 | -------------------------------------------------------------------------------- /aio_api_ros/__init__.py: -------------------------------------------------------------------------------- 1 | from aio_api_ros import errors 2 | from .connection import ApiRosConnection 3 | from .simple_pool import ApiRosSimplePool 4 | 5 | from .creators import create_rosapi_connection 6 | from .creators import create_rosapi_simple_pool 7 | 8 | 9 | version = '0.0.19' 10 | 11 | __all__ = [ 12 | 'ApiRosConnection', 13 | 'ApiRosSimplePool', 14 | 'create_rosapi_connection', 15 | 'create_rosapi_simple_pool', 16 | 'errors' 17 | ] 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import aio_api_ros 2 | from distutils.core import setup 3 | from setuptools import find_packages 4 | 5 | VERSION = aio_api_ros.version 6 | 7 | setup( 8 | name='aio_api_ros', 9 | version=VERSION, 10 | packages=find_packages(), 11 | url='https://github.com/frostspb/aio_api_ros', 12 | license='MIT', 13 | author='Frostspb', 14 | description='async implementation Mikrotik api', 15 | long_description="""async implementation Mikrotik api 16 | Only Python 3.5+""", 17 | keywords=["mikrotik", "asyncio", "apiRos"], 18 | classifiers=[ 19 | "Development Status :: 3 - Alpha", 20 | "Programming Language :: Python :: 3.5", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | ], 24 | install_requires=[ 25 | 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /aio_api_ros/errors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module contains project exceptions 3 | """ 4 | 5 | 6 | class ApiRosBaseError(Exception): 7 | """ 8 | Base exception 9 | """ 10 | def __init__(self, value=''): 11 | self.value = value 12 | 13 | def __str__(self): 14 | return repr(self.value) 15 | 16 | 17 | class LoginFailed(ApiRosBaseError): 18 | """ 19 | Login failed exception 20 | """ 21 | 22 | 23 | class UnpackerException(ApiRosBaseError): 24 | pass 25 | 26 | 27 | class BufferFull(UnpackerException): 28 | pass 29 | 30 | 31 | class OutOfData(ApiRosBaseError): 32 | pass 33 | 34 | 35 | class UnpackValueError(UnpackerException, ValueError): 36 | pass 37 | 38 | 39 | class UnknownControlByteError(UnpackValueError): 40 | pass 41 | 42 | 43 | class PackException(ApiRosBaseError): 44 | pass 45 | 46 | 47 | class ParseException(ApiRosBaseError): 48 | pass 49 | -------------------------------------------------------------------------------- /aio_api_ros/creators.py: -------------------------------------------------------------------------------- 1 | from .connection import ApiRosConnection 2 | from .simple_pool import ApiRosSimplePool 3 | 4 | 5 | async def create_rosapi_connection(mk_ip: str, mk_port: int, mk_user: str, 6 | mk_psw: str, loop=None): 7 | connection = ApiRosConnection(mk_ip=mk_ip, mk_port=mk_port, 8 | mk_user=mk_user, mk_psw=mk_psw, loop=loop) 9 | 10 | await connection.connect() 11 | await connection.login() 12 | return connection 13 | 14 | 15 | async def create_rosapi_simple_pool(mk_ip: str, mk_port: int, mk_user: str, 16 | mk_psw: str, max_size: int, loop=None): 17 | simple_pool = await ApiRosSimplePool(mk_ip=mk_ip, mk_port=mk_port, 18 | mk_user=mk_user, mk_psw=mk_psw, 19 | max_size=max_size, loop=loop) 20 | return simple_pool 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | 12 | develop-eggs/ 13 | 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | /dist/*.gz 28 | /dist/*.whl 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | /.idea/ 107 | -------------------------------------------------------------------------------- /aio_api_ros/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an adapted code from this repository 3 | https://github.com/mrin/miktapi 4 | """ 5 | 6 | from .errors import ParseException 7 | 8 | 9 | CAST_MAP = {'yes': True, 'true': True, 'no': False, 'false': False} 10 | 11 | 12 | def parse_word(word, cast_int=True, cast_bool=True): 13 | if word.startswith('!'): 14 | res = ('reply_word', word) 15 | else: 16 | parts = word.split('=') 17 | if len(parts) == 1: 18 | res = ('message', parts[0]) 19 | else: 20 | if parts[0] == '': 21 | del parts[0] 22 | len_parts = len(parts) 23 | 24 | if len_parts == 1: 25 | res = (parts[0], '') 26 | elif len_parts == 2: 27 | res = (parts[0], cast_by_map(parts[1], cast_int, cast_bool)) 28 | elif len_parts > 2: 29 | res = [ 30 | parts[0], 31 | [ 32 | cast_by_map(v, cast_int, cast_bool) for v 33 | in parts[1:len_parts] 34 | ], 35 | ] 36 | else: 37 | raise ParseException('Unexpected word format {}'.format(word)) 38 | return res 39 | 40 | 41 | def parse_sentence(sentence, cast_int=True, cast_bool=True): 42 | reply_word = sentence[0] 43 | if not reply_word.startswith('!'): 44 | raise ParseException('Unexpected reply word') 45 | if len(sentence) > 1 and sentence[1].startswith('.tag'): 46 | tag_word = parse_word(sentence[1], cast_int=False, cast_bool=False)[1] 47 | words = sentence[2:] 48 | else: 49 | tag_word = None 50 | words = sentence[1:] 51 | return ( 52 | reply_word, 53 | tag_word, 54 | dict(parse_word(w, cast_int, cast_bool) for w in words), 55 | ) 56 | 57 | 58 | def cast_by_map(v, cast_int, cast_bool): 59 | if cast_int: 60 | try: 61 | return int(v) 62 | except ValueError: 63 | pass 64 | if cast_bool: 65 | return CAST_MAP.get(v, v) 66 | return v 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aio_api_ros 2 | 3 | [![Join the chat at https://gitter.im/aio_api_ros/community](https://badges.gitter.im/aio_api_ros/community.svg)](https://gitter.im/aio_api_ros/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![PyPI version](https://badge.fury.io/py/aio-api-ros.svg)](https://badge.fury.io/py/aio-api-ros) 5 | [![Downloads](https://pepy.tech/badge/aio-api-ros)](https://pepy.tech/project/aio-api-ros) 6 | [![star this repo](http://githubbadges.com/star.svg?user=frostspb&repo=aio_api_ros&style=flat)](https://github.com/frostspb/aio_api_ros) 7 | [![fork this repo](http://githubbadges.com/fork.svg?user=frostspb&repo=aio_api_ros&style=flat)](https://github.com/frostspb/aio_api_ros/fork) 8 | 9 | async implementation Mikrotik api 10 | 11 | **Installation** 12 | 13 | ``` 14 | pip install aio_api_ros 15 | ``` 16 | 17 | **Example of usage** 18 | 19 | *Single connection* 20 | ```python 21 | import asyncio 22 | from aio_api_ros import create_rosapi_connection 23 | 24 | async def main(): 25 | 26 | mk = await create_rosapi_connection( 27 | mk_ip='127.0.0.1', 28 | mk_port=8728, 29 | mk_user='myuser', 30 | mk_psw='mypassword' 31 | ) 32 | 33 | mk.talk_word('/ip/hotspot/active/print') 34 | res = await mk.read() 35 | print(res) 36 | mk.close() 37 | 38 | 39 | if __name__ == '__main__': 40 | loop = asyncio.get_event_loop() 41 | loop.run_until_complete(main()) 42 | loop.close() 43 | 44 | ``` 45 | *Simple connections pool* 46 | ```python 47 | 48 | import asyncio 49 | from aio_api_ros import create_rosapi_simple_pool 50 | 51 | async def main(): 52 | 53 | mk = await create_rosapi_simple_pool( 54 | mk_ip='127.0.0.1', 55 | mk_port=8728, 56 | mk_user='myuser', 57 | mk_psw='mypassword', 58 | max_size=4 59 | ) 60 | 61 | await mk.talk_word('/ip/hotspot/active/print') 62 | res = await mk.read() 63 | print(res) 64 | mk.close() 65 | 66 | 67 | if __name__ == '__main__': 68 | loop = asyncio.get_event_loop() 69 | loop.run_until_complete(main()) 70 | loop.close() 71 | 72 | ``` 73 | -------------------------------------------------------------------------------- /aio_api_ros/simple_pool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aio_api_ros.connection import ApiRosConnection 4 | 5 | 6 | class ApiRosSimplePool: 7 | def __init__(self, mk_ip: str, mk_port: int, mk_user: str, mk_psw: str, 8 | max_size: int, loop=None): 9 | self.ip = mk_ip 10 | self.port = mk_port 11 | self.user = mk_user 12 | self.password = mk_psw 13 | self._max_size = max_size 14 | if loop is None: 15 | self._loop = asyncio.get_event_loop() 16 | else: 17 | self._loop = loop 18 | conns = [self.create_connection_object() for _ in range(max_size)] 19 | self._pool = conns 20 | 21 | def __await__(self): 22 | for connection in self._pool: 23 | yield from connection.connect().__await__() 24 | yield from connection.login().__await__() 25 | return self 26 | 27 | def create_connection_object(self): 28 | return ApiRosConnection( 29 | mk_user=self.user, 30 | mk_psw=self.password, 31 | mk_ip=self.ip, 32 | mk_port=self.port, 33 | loop=self._loop 34 | ) 35 | 36 | @property 37 | def max_size(self): 38 | """Maximum pool size.""" 39 | return self._max_size 40 | 41 | def get_conn(self, fut=None): 42 | if fut is None: 43 | fut = self._loop.create_future() 44 | 45 | for connection in self._pool: 46 | if not connection.used: 47 | connection.used = True 48 | fut.set_result(connection) 49 | break 50 | else: 51 | self._loop.call_soon(self.get_conn, fut) 52 | return fut 53 | 54 | def release(self, conn): 55 | if not conn.used: 56 | return 57 | for connection in self._pool: 58 | if connection.uuid != conn.uuid: 59 | continue 60 | connection.used = False 61 | break 62 | 63 | def close(self): 64 | for connection in self._pool: 65 | connection.close() 66 | 67 | async def talk_word(self, str_value: str, send_end=True): 68 | con = await self.get_conn() 69 | con.talk_word(str_value, send_end) 70 | self.release(con) 71 | 72 | async def talk_sentence(self, sentence: list): 73 | con = await self.get_conn() 74 | con.talk_sentence(sentence) 75 | self.release(con) 76 | 77 | async def read(self, length=128): 78 | con = await self.get_conn() 79 | res = await con.read(length) 80 | self.release(con) 81 | return res 82 | 83 | async def login_client(self, client_ip: str, client_login: str, 84 | client_psw: str): 85 | 86 | await self.login_client(client_ip, client_login, client_psw) 87 | result = await self.read() 88 | return result 89 | -------------------------------------------------------------------------------- /aio_api_ros/unpacker.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an adapted code from this repository 3 | https://github.com/mrin/miktapi 4 | """ 5 | import struct 6 | 7 | from .errors import BufferFull 8 | from .errors import OutOfData 9 | from .errors import UnknownControlByteError 10 | from .errors import UnpackValueError 11 | 12 | 13 | class SentenceUnpacker(object): 14 | def __init__(self, encoding='ASCII', max_buffer_size=0): 15 | self._buffer = bytearray() 16 | self._buf_o = 0 17 | self._max_buffer_size = max_buffer_size or 2**31-1 18 | self._sentence = None 19 | self._sentence_o = 0 20 | self._zero_b = b'\x00' 21 | self._encoding = encoding 22 | 23 | def feed(self, next_bytes): 24 | if isinstance(next_bytes, bytes): 25 | next_bytes = bytearray(next_bytes) 26 | if (len(self._buffer) + len(next_bytes)) > self._max_buffer_size: 27 | raise BufferFull 28 | self._buffer.extend(next_bytes) 29 | 30 | @staticmethod 31 | def _decode_word_len_num_bytes(first_byte): 32 | try: 33 | length = ord(first_byte) 34 | if length < 128: 35 | return 1 36 | elif length < 192: 37 | return 2 38 | elif length < 224: 39 | return 3 40 | elif length < 240: 41 | return 4 42 | else: 43 | raise UnknownControlByteError() 44 | except (TypeError, UnknownControlByteError): 45 | raise UnpackValueError( 46 | 'Unknown control byte {}'.format(first_byte) 47 | ) 48 | 49 | @staticmethod 50 | def _decode_word_len(length_b): 51 | nb = len(length_b) 52 | if nb < 2: 53 | offset = b'\x00\x00\x00' 54 | xor = 0 55 | elif nb < 3: 56 | offset = b'\x00\x00' 57 | xor = 0x8000 58 | elif nb < 4: 59 | offset = b'\x00' 60 | xor = 0xC00000 61 | elif nb < 5: 62 | offset = b'' 63 | 64 | xor = 0xE0000000 65 | 66 | else: 67 | raise UnpackValueError( 68 | 'Unable to decode length {}'.format(length_b) 69 | ) 70 | decoded = struct.unpack('!I', (offset + length_b))[0] 71 | decoded ^= xor 72 | return decoded 73 | 74 | def _read_cur_sentence(self, size): 75 | r = self._sentence[self._sentence_o:self._sentence_o + size] 76 | if not r: 77 | raise UnpackValueError('Unexpected byte sequence') 78 | self._sentence_o += size 79 | return r 80 | 81 | def _read_cur_sentence_word(self): 82 | fb = self._read_cur_sentence(1) 83 | if fb == self._zero_b: 84 | return fb 85 | w_len_b_cnt = self._decode_word_len_num_bytes(fb) 86 | if w_len_b_cnt > 1: 87 | w_len_b = fb + self._read_cur_sentence(w_len_b_cnt - 1) 88 | else: 89 | w_len_b = fb 90 | w_len = self._decode_word_len(w_len_b) 91 | return self._read_cur_sentence(w_len).decode( 92 | encoding=self._encoding, 93 | errors='strict' 94 | ) 95 | 96 | def _unpack(self): 97 | zero_b_pos = self._buffer.find(self._zero_b, self._buf_o) 98 | if zero_b_pos != -1: 99 | self._sentence = self._buffer[0:zero_b_pos + 1] 100 | self._sentence_o = 0 101 | del self._buffer[0:zero_b_pos + 1] 102 | self._buf_o = 0 103 | return tuple( 104 | word for word in iter( 105 | self._read_cur_sentence_word, self._zero_b 106 | ) 107 | ) 108 | else: 109 | self._buf_o = len(self._buffer) 110 | raise OutOfData 111 | 112 | def __iter__(self): 113 | return self 114 | 115 | def __next__(self): 116 | try: 117 | return self._unpack() 118 | except OutOfData: 119 | raise StopIteration 120 | -------------------------------------------------------------------------------- /aio_api_ros/connection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import binascii 4 | import uuid 5 | 6 | from .errors import LoginFailed 7 | from .errors import UnpackValueError 8 | from .unpacker import SentenceUnpacker 9 | from .parser import parse_sentence 10 | 11 | ERROR_TAG = '!trap' 12 | FATAL_ERROR_TAG = '!fatal' 13 | DEFAULT_READ_DATA_LEN = 4096 14 | LOGIN_DATA_LEN = 128 15 | 16 | 17 | class ApiRosConnection: 18 | """ 19 | Connection to Mikrotik api 20 | """ 21 | def __init__(self, mk_ip: str, mk_port: int, mk_user: str, mk_psw: str, 22 | loop=None): 23 | if not all([mk_ip, mk_port, mk_user, mk_psw]): 24 | raise RuntimeError('Wrong connection params!') 25 | self.ip = mk_ip 26 | self.port = mk_port 27 | self.user = mk_user 28 | self.password = mk_psw 29 | self.writer = None 30 | self.reader = None 31 | self._loop = loop 32 | self._uuid = uuid.uuid1() 33 | self.used = False 34 | 35 | def __del__(self): 36 | self.close() 37 | 38 | def __repr__(self): 39 | return 'Connection to %s:%s id=%s' % (self.ip, self.port, self.uuid) 40 | 41 | async def connect(self): 42 | self.reader, self.writer = await asyncio.open_connection( 43 | self.ip, self.port, loop=self._loop 44 | ) 45 | 46 | @property 47 | def uuid(self): 48 | """ 49 | uuid of connection 50 | :return: str 51 | """ 52 | return self._uuid 53 | 54 | @staticmethod 55 | def _to_bytes(str_value: str): 56 | """ 57 | Convert string to bytes 58 | :param str_value: str 59 | :return: bytes 60 | """ 61 | length = (len(str_value).bit_length() // 8) + 1 62 | res = len(str_value).to_bytes(length, byteorder='little') 63 | return res 64 | 65 | def _talk_end(self): 66 | """ 67 | Send EOC (end of command) to mikrotik api 68 | :return: 69 | """ 70 | self.writer.write(self._to_bytes('')) 71 | self.writer.write(''.encode()) 72 | 73 | def talk_word(self, str_value: str, send_end=True): 74 | """ 75 | Send word to mikrotik 76 | :param str_value: command 77 | :param send_end: bool Flag - send end after this command 78 | :return: 79 | """ 80 | self.writer.write(self._to_bytes(str_value)) 81 | self.writer.write(str_value.encode()) 82 | if send_end: 83 | self._talk_end() 84 | 85 | def talk_sentence(self, sentence: list): 86 | """ 87 | Send list of commands 88 | :param sentence: Send list of commands 89 | :return: 90 | """ 91 | for word in sentence: 92 | self.talk_word(word, False) 93 | self._talk_end() 94 | 95 | def close(self): 96 | """ 97 | Close connection 98 | :return: 99 | """ 100 | self.writer.close() 101 | 102 | def _get_login_sentence(self): 103 | """ 104 | Perform login sentence with challenge argument 105 | :param challenge_arg: 106 | :return: 107 | """ 108 | return [ 109 | "/login", 110 | "=name=" + self.user, 111 | "=password=" + self.password 112 | ] 113 | 114 | @staticmethod 115 | def _get_err_message(data): 116 | """ 117 | Parse error message from mikrotik response 118 | :param data: 119 | :return: 120 | """ 121 | return data.decode().split('=message=')[1].split('\x00')[0] 122 | 123 | @staticmethod 124 | def _get_challenge_arg(data): 125 | """ 126 | Parse from mikrotik response challenge argument 127 | :param data: 128 | :return: 129 | """ 130 | try: 131 | response_str = data.decode('UTF-8', 'replace') 132 | res_list = response_str.split('!done') 133 | str_val = res_list[1] 134 | res_list = str_val.split('%=ret=') 135 | res = str(res_list[1]) 136 | except IndexError: 137 | raise LoginFailed('Getting challenge argument failed') 138 | return res 139 | 140 | @staticmethod 141 | def _get_result_dict(code: int, message: str) -> dict: 142 | """ 143 | Return dict like {'code': 0, 'message': 'OK} 144 | :param code: 145 | :param message: 146 | :return: 147 | """ 148 | return {'code': code, 'message': message} 149 | 150 | async def read(self, parse=True, full_answer=False, 151 | length=DEFAULT_READ_DATA_LEN): 152 | """ 153 | Read response from api 154 | :param parse: 155 | :param full_answer: 156 | :param length: 157 | :return: 158 | """ 159 | byte_res = b'' 160 | list_res = [] 161 | while True: 162 | 163 | data = await self.reader.read(length) 164 | if data == b'': 165 | break 166 | if parse: 167 | try: 168 | 169 | parsed_data = self._parse_sentence(data, full_answer) 170 | list_res += parsed_data 171 | byte_res += data 172 | 173 | except UnpackValueError: 174 | parse = False 175 | else: 176 | byte_res += data 177 | 178 | if '!done' in data.decode(): 179 | res = list_res if parse else byte_res 180 | break 181 | 182 | return res 183 | 184 | @staticmethod 185 | def _parse_sentence(data, full_answer=False): 186 | unpacker = SentenceUnpacker() 187 | unpacker.feed(data) 188 | res = [ 189 | parse_sentence(sentence) if full_answer 190 | else parse_sentence(sentence)[2] for sentence in unpacker 191 | ] 192 | return res 193 | 194 | async def login(self): 195 | """ 196 | Login to api 197 | :return: 198 | """ 199 | try: 200 | login_sentence = self._get_login_sentence() 201 | self.talk_sentence(login_sentence) 202 | # await self.writer.drain() 203 | data = await self.reader.read(LOGIN_DATA_LEN) 204 | 205 | # login failed 206 | if ERROR_TAG in data.decode(): 207 | raise LoginFailed(self._get_err_message(data)) 208 | 209 | if FATAL_ERROR_TAG in data.decode(): 210 | raise LoginFailed(self._get_err_message(data)) 211 | 212 | return data 213 | 214 | except ConnectionResetError: 215 | raise LoginFailed('Connection reset by peer') 216 | 217 | async def login_client(self, client_ip: str, client_login: str, 218 | client_psw: str): 219 | """ 220 | Login client to mikrotik 221 | :param client_ip: 222 | :param client_login: 223 | :param client_psw: 224 | :return: 225 | """ 226 | sentence = [ 227 | '/ip/hotspot/active/login', 228 | '=ip={}'.format(client_ip), 229 | '=user={}'.format(client_login), 230 | '=password={}'.format(client_psw), 231 | ] 232 | self.talk_sentence(sentence) 233 | data = await self.read(parse=False) 234 | 235 | # login failed 236 | if ERROR_TAG in data.decode(): 237 | result = self._get_result_dict(-1, self._get_err_message(data)) 238 | 239 | else: 240 | result = self._get_result_dict(0, 'OK') 241 | return result 242 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4ef4d5e29cdc3630cbb8cffa63abb0e4d06e32c5d9a318460d453fa46a3fa158" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "astroid": { 21 | "hashes": [ 22 | "sha256:0ef2bf9f07c3150929b25e8e61b5198c27b0dca195e156f0e4d5bdd89185ca1a", 23 | "sha256:fc9b582dba0366e63540982c3944a9230cbc6f303641c51483fa547dcc22393a" 24 | ], 25 | "version": "==1.6.5" 26 | }, 27 | "backports.functools-lru-cache": { 28 | "hashes": [ 29 | "sha256:9d98697f088eb1b0fa451391f91afb5e3ebde16bbdb272819fd091151fda4f1a", 30 | "sha256:f0b0e4eba956de51238e17573b7087e852dfe9854afd2e9c873f73fc0ca0a6dd" 31 | ], 32 | "markers": "python_version < '3.4'", 33 | "version": "==1.5" 34 | }, 35 | "configparser": { 36 | "hashes": [ 37 | "sha256:27594cf4fc279f321974061ac69164aaebd2749af962ac8686b20503ac0bcf2d", 38 | "sha256:9d51fe0a382f05b6b117c5e601fc219fede4a8c71703324af3f7d883aef476a3" 39 | ], 40 | "markers": "python_version == '2.7'", 41 | "version": "==3.7.3" 42 | }, 43 | "entrypoints": { 44 | "hashes": [ 45 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 46 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 47 | ], 48 | "version": "==0.3" 49 | }, 50 | "enum34": { 51 | "hashes": [ 52 | "sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", 53 | "sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", 54 | "sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", 55 | "sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1" 56 | ], 57 | "markers": "python_version < '3.4'", 58 | "version": "==1.1.6" 59 | }, 60 | "flake8": { 61 | "hashes": [ 62 | "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", 63 | "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" 64 | ], 65 | "index": "pypi", 66 | "version": "==3.7.7" 67 | }, 68 | "functools32": { 69 | "hashes": [ 70 | "sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0", 71 | "sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d" 72 | ], 73 | "markers": "python_version < '3.2'", 74 | "version": "==3.2.3.post2" 75 | }, 76 | "futures": { 77 | "hashes": [ 78 | "sha256:9ec02aa7d674acb8618afb127e27fde7fc68994c0437ad759fa094a574adb265", 79 | "sha256:ec0a6cb848cc212002b9828c3e34c675e0c9ff6741dc445cab6fdd4e1085d1f1" 80 | ], 81 | "markers": "python_version < '3.2'", 82 | "version": "==3.2.0" 83 | }, 84 | "isort": { 85 | "hashes": [ 86 | "sha256:18c796c2cd35eb1a1d3f012a214a542790a1aed95e29768bdcb9f2197eccbd0b", 87 | "sha256:96151fca2c6e736503981896495d344781b60d18bfda78dc11b290c6125ebdb6" 88 | ], 89 | "version": "==4.3.15" 90 | }, 91 | "lazy-object-proxy": { 92 | "hashes": [ 93 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 94 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 95 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 96 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 97 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 98 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 99 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 100 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 101 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 102 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 103 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 104 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 105 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 106 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 107 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 108 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 109 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 110 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 111 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 112 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 113 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 114 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 115 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 116 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 117 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 118 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 119 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 120 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 121 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 122 | ], 123 | "version": "==1.3.1" 124 | }, 125 | "mccabe": { 126 | "hashes": [ 127 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 128 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 129 | ], 130 | "version": "==0.6.1" 131 | }, 132 | "pycodestyle": { 133 | "hashes": [ 134 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 135 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 136 | ], 137 | "version": "==2.5.0" 138 | }, 139 | "pyflakes": { 140 | "hashes": [ 141 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 142 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 143 | ], 144 | "version": "==2.1.1" 145 | }, 146 | "pylint": { 147 | "hashes": [ 148 | "sha256:02c2b6d268695a8b64ad61847f92e611e6afcff33fd26c3a2125370c4662905d", 149 | "sha256:ee1e85575587c5b58ddafa25e1c1b01691ef172e139fc25585e5d3f02451da93" 150 | ], 151 | "index": "pypi", 152 | "version": "==1.9.4" 153 | }, 154 | "singledispatch": { 155 | "hashes": [ 156 | "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", 157 | "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8" 158 | ], 159 | "markers": "python_version < '3.4'", 160 | "version": "==3.4.0.3" 161 | }, 162 | "six": { 163 | "hashes": [ 164 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 165 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 166 | ], 167 | "version": "==1.12.0" 168 | }, 169 | "typing": { 170 | "hashes": [ 171 | "sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", 172 | "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", 173 | "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" 174 | ], 175 | "markers": "python_version < '3.5'", 176 | "version": "==3.6.6" 177 | }, 178 | "wrapt": { 179 | "hashes": [ 180 | "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" 181 | ], 182 | "version": "==1.11.1" 183 | } 184 | } 185 | } 186 | --------------------------------------------------------------------------------