Skip to main content

Pure-Python client library for Mikrotik RouterOS API

Project description

routeros-py

PyPI version Python CI License: MIT

A pure-Python client library for Mikrotik RouterOS devices using the RouterOS API binary protocol.

Python port of the Go library github.com/go-routeros/routeros/v3.


Features

  • Python type casting — parse RouterOS string values to int, bool, float, timedelta, and more
  • Key/value dictionary — every sentence exposes a .map dict for fast field access
  • Source address / port binding — bind the client socket to a specific local interface
  • TLS/SSL encryption — connect via ssl.SSLContext (port 8729)
  • Logging support — integrates with Python's standard logging module
  • Synchronous mode — simple blocking run() calls
  • Asynchronous mode — tagged concurrent requests via a background thread
  • Listener / streaming — subscribe to real-time device events via a Queue
  • Context manager — use with dial(...) as c: for automatic cleanup
  • RouterOS version detection — auto-detects pre-6.43 (MD5) vs post-6.43 (cleartext) auth
  • API schema discovery — walk the full RouterOS API tree and save it as a structured JSON schema

Installation

# With uv (recommended)
uv add routeros-py

# With pip
pip install routeros-py

Requirements: Python ≥ 3.10


Quick Start

import routeros

# Connect and run a command
with routeros.dial("192.168.1.1:8728", "admin", "") as c:
    reply = c.run("/ip/address/print")
    for sentence in reply.re:
        print(sentence.map["address"], "→", sentence.map["interface"])

Usage Examples

Synchronous (default)

import routeros

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    # Print all IP addresses
    reply = c.run("/ip/address/print")
    for s in reply.re:
        print(s.map)

    # Filter by interface
    reply = c.run("/ip/address/print", "?interface=ether1")
    for s in reply.re:
        print(s.map["address"])

Asynchronous (concurrent requests)

import threading
import routeros

with routeros.dial("192.168.1.1:8728", "admin", "") as c:
    c.async_start()

    results = []

    def fetch(cmd):
        r = c.run(cmd)
        results.append(r)

    t1 = threading.Thread(target=fetch, args=("/ip/address/print",))
    t2 = threading.Thread(target=fetch, args=("/interface/print",))
    t1.start(); t2.start()
    t1.join(); t2.join()

    for r in results:
        for s in r.re:
            print(s.map)

Listener (streaming events)

import routeros

with routeros.dial("192.168.1.1:8728", "admin", "") as c:
    listener = c.listen("/ip/firewall/address-list/listen")

    for sentence in listener:          # blocks until stream ends
        print("Event:", sentence.map)

    # Or cancel after a timeout:
    # listener.cancel()

TLS / SSL

import ssl
import routeros

ctx = ssl.create_default_context()
ctx.load_verify_locations("ca.pem")       # or ctx.check_hostname = False

with routeros.dial_tls("192.168.1.1:8729", "admin", "", tls_context=ctx) as c:
    reply = c.run("/system/identity/print")
    print(reply.re[0].map["name"])

Source Address / Port Binding

import routeros

with routeros.dial(
    "192.168.1.1:8728", "admin", "",
    source_address=("10.0.0.5", 0)   # host, port (0 = ephemeral)
) as c:
    ...

Type Casting

import routeros
from routeros import cast, cast_optional, typed_map
from datetime import timedelta

with routeros.dial("192.168.1.1:8728", "admin", "") as c:
    reply = c.run("/interface/print")
    for s in reply.re:
        mtu     = cast(s.map["mtu"],     int)    # → 1500
        running = cast(s.map["running"], bool)   # → True / False

        row = typed_map(s, {
            "name":    str,
            "mtu":     int,
            "running": bool,
            "rx-byte": int,
        })
        print(row)

API Schema Discovery

Discover the full RouterOS API tree and save it as a structured JSON schema. The output format mirrors the MikroTik REST-API inspect.json — a nested tree of _type: "dir", "cmd", and "arg" nodes with parameter descriptions.

import routeros

with routeros.dial("192.168.1.1:8728", "admin", "secret") as c:
    schema = routeros.discover_api(c)

print(schema["version"])         # e.g. "7.20.8"
print(schema["endpoint_count"])  # e.g. 541

# Navigate the nested tree
ip_addr = schema["tree"]["ip"]["address"]
print(ip_addr["_type"])          # "dir"

add_cmd = ip_addr["add"]
print(add_cmd["_type"])          # "cmd"
print(add_cmd["address"])        # {"_type": "arg", "desc": "A.B.C.D ..."}

The schema is saved to data/mikrotik/api/routeros_<version>.json automatically and reloaded from disk on subsequent calls (no re-discovery needed).

# Load a previously saved schema without a live connection
schema = routeros.load_api_schema("7.20.8")
if schema:
    print(schema["tree"]["ip"]["firewall"])

Schema JSON structure

{
  "version": "7.20.8",
  "generated_at": "2026-03-21T14:00:00+00:00",
  "endpoint_count": 541,
  "tree": {
    "ip": {
      "_type": "dir",
      "address": {
        "_type": "dir",
        "add": {
          "_type": "cmd",
          "address":   {"_type": "arg", "desc": "A.B.C.D    (IP address)"},
          "interface": {"_type": "arg", "desc": "string value"},
          "disabled":  {"_type": "arg"}
        },
        "print": {
          "_type": "cmd",
          "brief":         {"_type": "arg"},
          "detail":        {"_type": "arg"},
          "count-only":    {"_type": "arg"},
          "without-paging":{"_type": "arg"}
        },
        "remove": {
          "_type": "cmd",
          "numbers": {"_type": "arg", "desc": "see documentation"}
        }
      }
    }
  }
}

Discovery strategies

  1. REST-API inspect.json (preferred) — if pre-downloaded MikroTik REST-API files exist in data/mikrotik/rest-api/<version>/, they are loaded and merged (inspect.json + extra/inspect.json) for the richest result. Covers RouterOS 7.9 – 7.22+.

  2. Binary-API BFS (fallback) — walks the live router's /console/inspect endpoint with comma-separated path components, querying request=syntax per command to obtain parameter names and descriptions.

Force re-discovery

# Ignore the cached file and re-discover from the live device
schema = routeros.discover_api(c, force=True)

Custom output directory

from pathlib import Path

schema = routeros.discover_api(c, data_dir=Path("/tmp/routeros-schemas"))

Logging

import logging
import routeros

logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger("routeros")      # library logger

with routeros.dial("192.168.1.1:8728", "admin", "", logger=log) as c:
    c.run("/system/resource/print")

API Reference

See docs/api-reference.md for the full API.

Symbol Description
dial(address, user, password, ...) Connect via plain TCP
dial_tls(address, user, password, ...) Connect via TLS/SSL
Client.run(*words) Send command, wait for reply
Client.run_args(words) Same with list argument
Client.async_start() Enable async / concurrent mode
Client.listen(*words) Start streaming listener
Reply.re List of !re sentences
Reply.done Final !done sentence
ListenReply Iterable streaming reply
Sentence.map dict[str, str] of all fields
cast(value, type) Cast a single RouterOS value
cast_optional(value, type) Cast, returning None for ""
typed_map(sentence, schema) Cast all fields from a schema dict
discover_api(client, ...) Walk & cache the full RouterOS API schema
load_api_schema(version, ...) Load a previously saved schema from disk

Development

Requirements

  • Python ≥ 3.10
  • uv

Setup

git clone https://github.com/Butterfly-Student/routeros-py
cd routeros-py/routeros

uv sync

Running Tests

# Unit tests only (no device needed)
uv run pytest tests/unit/ -v

# With coverage
uv run pytest tests/unit/ --cov=routeros --cov-report=term-missing

# Integration tests (requires .env with device credentials)
cp .env.example .env
# Edit .env with your RouterOS host/credentials
uv run pytest tests/integration/ -v -m integration

# All tests
uv run pytest

Docker (RouterOS test environment)

cd docker
cp .env.example .env        # edit if needed
docker compose up -d
# Wait ~60 s for RouterOS to boot, then run integration tests
uv run pytest tests/integration/ -v

Environment Variables

Copy .env.example to .env and fill in your values:

ROUTEROS_HOST=192.168.1.1
ROUTEROS_PORT=8728
ROUTEROS_USER=admin
ROUTEROS_PASSWORD=
ROUTEROS_TLS_PORT=8729
ROUTEROS_TLS_VERIFY=false

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

routeros_py-1.1.0.tar.gz (103.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

routeros_py-1.1.0-py3-none-any.whl (32.2 kB view details)

Uploaded Python 3

File details

Details for the file routeros_py-1.1.0.tar.gz.

File metadata

  • Download URL: routeros_py-1.1.0.tar.gz
  • Upload date:
  • Size: 103.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for routeros_py-1.1.0.tar.gz
Algorithm Hash digest
SHA256 3ee16bd297bb073bd48348b87d66df9c3d9e4988717dc5d8b1e1abc5d92cd868
MD5 7bf2ea127af0c63b171b0f5332f71c70
BLAKE2b-256 ea10068231fc2f65816756ab8769aeaf35b5625fa76c83ae8ab91e75d29b463e

See more details on using hashes here.

File details

Details for the file routeros_py-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: routeros_py-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 32.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for routeros_py-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7bcc070c2b765a032bfe52b6b9828489b0e5ec6aca356b1b100aa085b4edaf1e
MD5 26923ad2ac5300cd4be841836f1ec324
BLAKE2b-256 de075019502ba730412faf9c07c223f4a4b60820cfac3f0ec2f54325b516b821

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page