{"id":22220863,"url":"https://github.com/nokia/moler","last_synced_at":"2025-04-04T11:11:54.019Z","repository":{"id":38631240,"uuid":"119655506","full_name":"nokia/moler","owner":"nokia","description":"Moler – library to help build automated tests","archived":false,"fork":false,"pushed_at":"2024-10-29T09:22:09.000Z","size":6024,"stargazers_count":59,"open_issues_count":6,"forks_count":23,"subscribers_count":15,"default_branch":"main","last_synced_at":"2024-10-29T11:38:22.942Z","etag":null,"topics":["python","test-automation"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nokia.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-31T08:08:52.000Z","updated_at":"2024-10-21T19:18:32.000Z","dependencies_parsed_at":"2023-02-18T07:45:55.207Z","dependency_job_id":"e93d335d-53a5-49b1-be28-c16b46741c25","html_url":"https://github.com/nokia/moler","commit_stats":{"total_commits":3588,"total_committers":33,"mean_commits":"108.72727272727273","dds":0.6636008918617614,"last_synced_commit":"560c3e5f7994d39b650c99e7d409f547b6ac7540"},"previous_names":[],"tags_count":88,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nokia%2Fmoler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nokia%2Fmoler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nokia%2Fmoler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nokia%2Fmoler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nokia","download_url":"https://codeload.github.com/nokia/moler/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247166168,"owners_count":20894654,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["python","test-automation"],"created_at":"2024-12-02T23:10:41.117Z","updated_at":"2025-04-04T11:11:53.999Z","avatar_url":"https://github.com/nokia.png","language":"Python","funding_links":[],"categories":["Testing"],"sub_categories":["GPS, Time"],"readme":"[![image](https://img.shields.io/badge/pypi-v4.1.0-blue.svg)](https://pypi.org/project/moler/)\n[![image](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://pypi.org/project/moler/)\n[![Build Status](https://github.com/nokia/moler/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/nokia/moler/actions)\n[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](./LICENSE)\n\n# Table of Contents\n1. [Changelog](#changelog)\n2. [Moler info](#moler)\n3. [Moler usage examples](#moler-usage-examples)\n4. [API design reasoning](#api-design-reasoning)\n5. [Designed API](#designed-api)\n\n# Changelog\nView our [chronological list](https://github.com/nokia/moler/CHANGELOG.md) of user-facing changes, large and small, made to the Moler project.\n\n![moler logo](https://i.imgur.com/mkPutdC.png)\n# Moler\nMoler ([name origin](https://github.com/nokia/moler/wiki#moler-name-origin)) is a Python library\nthat provides \"bricks\" for building  automated tests.\nAll these \"bricks\" have clearly defined responsibilities, have similar API,\nfollow same construction pattern (so new ones are easy to create).\n\nHere they are:\n* Commands as self-reliant object\n  * to allow for command triggering and parsing encapsulated in single object (lower maintenance cost)\n* Event observers \u0026 callbacks (alarms are events example)\n  * to allow for online reaction (not offline postprocessing)\n* Run observers/commands in the background\n  * to allow for test logic decomposition into multiple commands running in parallel\n  * to allow for handling unexpected system behavior (reboots, alarms)\n* State machines -\u003e automatic auto-connecting after dropped connection\n  * to increase framework auto-recovery and help in troubleshooting \"what went wrong\"\n* Automatic logging of all connections towards devices used by tests\n  * to decrease investigation time by having logs focused on different parts of system under test\n\n# Moler usage examples\nLet's see Moler in action. Here is hypothetical use case: \"find PIDs of all python processes\":\n\n```python\n\nfrom moler.config import load_config\nfrom moler.device import DeviceFactory\n\nload_config(config='my_devices.yml')                    # description of available devices\nmy_unix = DeviceFactory.get_device(name='MyMachine')    # take specific device out of available ones\nps_cmd = my_unix.get_cmd(cmd_name=\"ps\",                 # take command of that device\n                         cmd_params={\"options\": \"-ef\"})\n\nprocesses_info = ps_cmd()                               # run the command, it returns result\nfor proc_info in processes_info:\n    if 'python' in proc_info['CMD']:\n        print(\"PID: {info[PID]} CMD: {info[CMD]}\".format(info=proc_info))\n```\n\n* To have command we ask device \"give me such command\".\n* To run command we just call it as function (command object is callable)\n* What command returns is usually dict or list of dicts - easy to process\n\nAbove code displays:\n\n```bash\n\n    PID: 1817 CMD: /usr/bin/python /usr/share/system-config-printer/applet.py\n    PID: 21825 CMD: /usr/bin/python /home/gl/moler/examples/command/unix_ps.py\n```\n\nHow does it know what `'MyMachine'` means? Code loads definition from `my_devices.yml` configuration file:\n\n```yaml\n\n    DEVICES:\n\n      MyMachine:\n        DEVICE_CLASS: moler.device.unixlocal.UnixLocal\n\n      RebexTestMachine:\n        DEVICE_CLASS: moler.device.unixremote.UnixRemote\n        CONNECTION_HOPS:\n          UNIX_LOCAL:                # from state\n            UNIX_REMOTE:             # to state\n              execute_command: ssh   # via command\n              command_params:        # with params\n                expected_prompt: demo@\n                host: test.rebex.net\n                login: demo\n                password: password\n                set_timeout: None   # remote doesn't support: export TMOUT\n```\n\nWe have remote machine in our config. Let's check if there is 'readme.txt' file\non that machine (and some info about the file):\n\n```python\nfrom moler.device import DeviceFactory\nremote_unix = DeviceFactory.get_device(name='RebexTestMachine')  # it starts in local shell\nremote_unix.goto_state(state=\"UNIX_REMOTE\")                      # make it go to remote shell\n\nls_cmd = remote_unix.get_cmd(cmd_name=\"ls\", cmd_params={\"options\": \"-l\"})\n\nremote_files = ls_cmd()\n\nif 'readme.txt' in remote_files['files']:\n    print(\"readme.txt file:\")\n    readme_file_info = remote_files['files']['readme.txt']\n    for attr in readme_file_info:\n        print(\"  {:\u003c18}: {}\".format(attr, readme_file_info[attr]))\n```\n\nAs you may noticed device is state machine. State transitions are defined inside\nconfiguration file under `CONNECTION_HOPS`. Please note, that it is only config file who\nknows \"I need to use ssh to be on remote\" - client code just says \"go to remote\".\nThanks to that you can exchange \"how to reach remote\" without any change in main code.\n\nAbove code displays:\n\n```bash\n\n    readme.txt file:\n      permissions       : -rw-------\n      hard_links_count  : 1\n      owner             : demo\n      group             : users\n      size_raw          : 403\n      size_bytes        : 403\n      date              : Apr 08  2014\n      name              : readme.txt\n```\n\nHow about doing multiple things in parallel. Let's ping google\nwhile asking test.rebex.net about readme.txt file:\n\n```python\nfrom moler.device import DeviceFactory\nmy_unix = DeviceFactory.get_device(name='MyMachine')\nhost = 'www.google.com'\nping_cmd = my_unix.get_cmd(cmd_name=\"ping\", cmd_params={\"destination\": host, \"options\": \"-w 6\"})\n\nremote_unix = DeviceFactory.get_device(name='RebexTestMachine')\nremote_unix.goto_state(state=\"UNIX_REMOTE\")\nls_cmd = remote_unix.get_cmd(cmd_name=\"ls\", cmd_params={\"options\": \"-l\"})\n\nprint(\"Start pinging {} ...\".format(host))\nping_cmd.start()                                # run command in background\nprint(\"Let's check readme.txt at {} while pinging {} ...\".format(remote_unix.name, host))\n\nremote_files = ls_cmd()                         # foreground \"run in the meantime\"\nfile_info = remote_files['files']['readme.txt']\nprint(\"readme.txt file: owner={fi[owner]}, size={fi[size_bytes]}\".format(fi=file_info))\n\nping_stats = ping_cmd.await_done(timeout=6)     # await background command\nprint(\"ping {}: {}={}, {}={} [{}]\".format(host,'packet_loss',\n                                          ping_stats['packet_loss'],\n                                          'time_avg',\n                                          ping_stats['time_avg'],\n                                          ping_stats['time_unit']))\n```\n\n```log\nStart pinging www.google.com ...\nLet's check readme.txt at RebexTestMachine while pinging www.google.com ...\nreadme.txt file: owner=demo, size=403\nping www.google.com: packet_loss=0, time_avg=35.251 [ms]\n```\n\nBesides being callable command-object works as \"Future\" (result promise).\nYou can start it in background and later await till it is done to grab result.\n\nIf we enhance our configuration with logging related info:\n\n```yaml\n    LOGGER:\n      PATH: ./logs\n      DATE_FORMAT: \"%H:%M:%S\"\n```\n\nthen above code will automatically create Molers' main log (`moler.log`)\nwhich shows activity on all devices:\n\n```log\n22:30:19.723 INFO       moler               |More logs in: ./logs\n22:30:19.747 INFO       MyMachine           |Connection to: 'MyMachine' has been opened.\n22:30:19.748 INFO       MyMachine           |Changed state from 'NOT_CONNECTED' into 'UNIX_LOCAL'\n22:30:19.866 INFO       MyMachine           |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('^moler_bash#')]' started.\n22:30:19.901 INFO       RebexTestMachine    |Connection to: 'RebexTestMachine' has been opened.\n22:30:19.901 INFO       RebexTestMachine    |Changed state from 'NOT_CONNECTED' into 'UNIX_LOCAL'\n22:30:19.919 INFO       RebexTestMachine    |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('demo@')]' started.\n22:30:19.920 INFO       RebexTestMachine    |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('^moler_bash#')]' started.\n22:30:19.921 INFO       RebexTestMachine    |Command 'moler.cmd.unix.ssh.Ssh':'TERM=xterm-mono ssh -l demo test.rebex.net' started.\n22:30:19.921 INFO       RebexTestMachine    |TERM=xterm-mono ssh -l demo test.rebex.net\n22:30:20.763 INFO       RebexTestMachine    |*********\n22:30:20.909 INFO       RebexTestMachine    |Changed state from 'UNIX_LOCAL' into 'UNIX_REMOTE'\n22:30:20.917 INFO       RebexTestMachine    |Command 'moler.cmd.unix.ssh.Ssh' finished.\n22:30:20.919 INFO       MyMachine           |Command 'moler.cmd.unix.ping.Ping':'ping www.google.com -w 6' started.\n22:30:20.920 INFO       MyMachine           |ping www.google.com -w 6\n22:30:20.920 INFO       RebexTestMachine    |Command 'moler.cmd.unix.ls.Ls':'ls -l' started.\n22:30:20.922 INFO       RebexTestMachine    |ls -l\n22:30:20.985 INFO       RebexTestMachine    |Command 'moler.cmd.unix.ls.Ls' finished.\n22:30:26.968 INFO       MyMachine           |Command 'moler.cmd.unix.ping.Ping' finished.\n22:30:26.992 INFO       RebexTestMachine    |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('^moler_bash#')]' finished.\n22:30:27.011 INFO       RebexTestMachine    |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('demo@')]' finished.\n22:30:27.032 INFO       MyMachine           |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('^moler_bash#')]' finished.\n\n```\n\nAs you may noticed main log shows code progress from high-level view - data\non connections are not visible, just activity of commands running on devices.\n\nIf you want to see in details what has happened on each device - you have it in device logs.\nMoler creates log per each device\n`moler.RebexTestMachine.log`:\n\n```log\n22:30:19.901  |Changed state from 'NOT_CONNECTED' into 'UNIX_LOCAL'\n22:30:19.902 \u003c|\n22:30:19.919  |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('demo@')]' started.\n22:30:19.920  |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('^moler_bash#')]' started.\n22:30:19.921  |Command 'moler.cmd.unix.ssh.Ssh':'TERM=xterm-mono ssh -l demo test.rebex.net' started.\n22:30:19.921 \u003e|TERM=xterm-mono ssh -l demo test.rebex.net\n\n22:30:19.924 \u003c|TERM=xterm-mono ssh -l demo test.rebex.net\n\n22:30:20.762 \u003c|Password:\n22:30:20.763 \u003e|*********\n22:30:20.763 \u003c|\n\n22:30:20.908 \u003c|Welcome to Rebex Virtual Shell!\n              |For a list of supported commands, type 'help'.\n              |demo@ETNA:/$\n22:30:20.909  |Changed state from 'UNIX_LOCAL' into 'UNIX_REMOTE'\n22:30:20.917  |Command 'moler.cmd.unix.ssh.Ssh' finished.\n22:30:20.920  |Command 'moler.cmd.unix.ls.Ls':'ls -l' started.\n22:30:20.922 \u003e|ls -l\n\n22:30:20.974 \u003c|ls -l\n\n22:30:20.978 \u003c|drwx------ 2 demo users          0 Jul 26  2017 .\n\n22:30:20.979 \u003c|drwx------ 2 demo users          0 Jul 26  2017 ..\n              |drwx------ 2 demo users          0 Dec 03  2015 aspnet_client\n              |drwx------ 2 demo users          0 Oct 27  2015 pub\n              |-rw------- 1 demo users        403 Apr 08  2014 readme.txt\n              |demo@ETNA:/$\n22:30:20.985  |Command 'moler.cmd.unix.ls.Ls' finished.\n22:30:26.992  |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('^moler_bash#')]' finished.\n22:30:27.011  |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('demo@')]' finished.\n```\n\nand `moler.MyMachine.log`:\n\n```log\n22:30:19.748  |Changed state from 'NOT_CONNECTED' into 'UNIX_LOCAL'\n22:30:19.748 \u003c|\n22:30:19.866  |Event 'moler.events.unix.wait4prompt.Wait4prompt':'[re.compile('^moler_bash#')]' started.\n22:30:20.919  |Command 'moler.cmd.unix.ping.Ping':'ping www.google.com -w 6' started.\n22:30:20.920 \u003e|ping www.google.com -w 6\n\n22:30:20.921 \u003c|ping www.google.com -w 6\n\n22:30:20.959 \u003c|PING www.google.com (216.58.215.68) 56(84) bytes of data.\n22:30:20.960 \u003c|\n\n22:30:21.000 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=1 ttl=51 time=40.1 ms\n22:30:21.001 \u003c|\n\n22:30:21.992 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=2 ttl=51 time=31.0 ms\n\n22:30:22.999 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=3 ttl=51 time=36.5 ms\n\n22:30:23.996 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=4 ttl=51 time=31.4 ms\n\n22:30:24.996 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=5 ttl=51 time=29.8 ms\n\n22:30:26.010 \u003c|64 bytes from waw02s16-in-f4.1e100.net (216.58.215.68): icmp_seq=6 ttl=51 time=42.4 ms\n\n22:30:26.960 \u003c|\n              |--- www.google.com ping statistics ---\n              |6 packets transmitted, 6 received, 0% packet loss, time 5007ms\n              |rtt min/avg/max/mdev = 29.888/35.251/42.405/4.786 ms\n              |moler_bash#\n22:30:26.968  |Command 'moler.cmd.unix.ping.Ping' finished.\n22:30:27.032  |Event 'moler.events.unix.wait4prompt.Wait4prompt': '[re.compile('^moler_bash#')]' finished.\n```\n\nIf the log files are too large you can split files.\n\nThe log files can be split by size. For example let's assume we want split log files by 5 MB (5242880 bytes) and we want\nto keep maximum 999 files:\n```yaml\n    LOGGER:\n      PATH: ./logs\n      DATE_FORMAT: \"%H:%M:%S\"\n      LOG_ROTATION:\n        KIND: size\n        INTERVAL: 5242880\n        BACKUP_COUNT: 999  # Default value\n\n\n```\n\nThe log files can be split by time. For example let's assume we want split log files every 30 minutes (1800 seconds)\n and we want to keep maximum 999 files (default value):\n\n```yaml\n    LOGGER:\n      PATH: ./logs\n      DATE_FORMAT: \"%H:%M:%S\"\n      LOG_ROTATION:\n        KIND: time\n        INTERVAL: 1800\n        BACKUP_COUNT: 999  # Default value\n```\n\nFor space saving Moler can compress the logs after rotation. The external tool is used. Let's use the above examples\nto show how to compress logs:\n\n```yaml\n    LOGGER:\n      PATH: ./logs\n      DATE_FORMAT: \"%H:%M:%S\"\n      LOG_ROTATION:\n        KIND: size\n        INTERVAL: 5242880\n        BACKUP_COUNT: 999  # Default value\n        COMPRESS_AFTER_ROTATION: True  # Default is False\n        COMPRESS_COMMAND: \"zip -9mq {compressed} {log_input}\"  # Default value\n        COMPRESSED_FILE_EXTENSION: \".zip\"  # Default value\n\n\n```\n\n```yaml\n    LOGGER:\n      PATH: ./logs\n      DATE_FORMAT: \"%H:%M:%S\"\n      LOG_ROTATION:\n        KIND: time\n        INTERVAL: 1800\n        BACKUP_COUNT: 999  # Default value\n        COMPRESS_COMMAND: \"zip -9mq {compressed} {log_input}\"  # Default value\n        COMPRESSED_FILE_EXTENSION: \".zip\"  # Default value\n```\n\nIn a script we can also disable logging from device. Please use it very carefully. Investigation any issue may be\n impossible if we don't have full logs.\n\n```python\nfrom moler.device import DeviceFactory\nmy_unix = DeviceFactory.get_device(name='MyMachine')\n\nmy_unix.disbale_logging()  # to disable logging on device\n\nmy_unix.enable_logging()  # to enable logging on device\n```\n\nIn a script you can add suffix to all log files or only to files for specific devices. with disable logging from device.\n\n```python\nfrom moler.device import DeviceFactory\nfrom moler.config.loggers import change_logging_suffix\nchange_logging_suffix(\".suffix1\")  # all log files with suffix\nchange_logging_suffix(None)  # all log files without suffx\n\nmy_unix = DeviceFactory.get_device(name='MyMachine')\n\nmy_unix.set_logging_suffix(\"device_suffix\")  # to add suffix to filename with logs\n\nmy_unix.set_logging_suffix(None)  # to remove suffix from filename with logs\n```\n\nPrevious examples ask device to create command. We can also create command ourselves\ngiving it connection to operate on:\n\n```python\n\nimport time\nfrom moler.cmd.unix.ping import Ping\nfrom moler.connection_factory import get_connection\n\nhost = 'www.google.com'\nterminal = get_connection(io_type='terminal', variant='threaded')  # take connection\nwith terminal.open():\n    ping_cmd = Ping(connection=terminal.moler_connection,\n                    destination=host, options=\"-w 6\")\n    print(\"Start pinging {} ...\".format(host))\n    ping_cmd.start()\n    print(\"Doing other stuff while pinging {} ...\".format(host))\n    time.sleep(3)\n    ping_stats = ping_cmd.await_done(timeout=4)\n    print(\"ping {}: {}={}, {}={} [{}]\".format(host,'packet_loss',\n                                              ping_stats['packet_loss'],\n                                              'time_avg',\n                                              ping_stats['time_avg'],\n                                              ping_stats['time_unit']))\n```\n\nPlease note also that connection is context manager doing open/close actions.\n\n\n```bash\n\n    Start pinging www.google.com ...\n    Doing other stuff while pinging www.google.com ...\n    ping www.google.com: packet_loss=0, time_avg=50.000 [ms]\n```\n\n## Reuse freedom\nLibrary gives you freedom which part you want to reuse. We are fans of \"take what you need only\".\n* You may use configuration files or configure things by Python calls.\n\n   ```python\n   from moler.config import load_config\n   load_config(config={'DEVICES': {'MyMachine': {'DEVICE_CLASS': 'moler.device.unixlocal.UnixLocal'}}})\n   ```\n* You may use devices or create commands manually\n* You can take connection or build it yourself:\n\n   ```python\n   from moler.threaded_moler_connection import ThreadedMolerConnection\n   from moler.io.raw.terminal import ThreadedTerminal\n\n   terminal_connection = ThreadedTerminal(moler_connection=ThreadedMolerConnection())\n   ```\n* You can even install your own implementation in place of default implementation per connection type\n\n# API design reasoning\nThe main goal of command is its usage simplicity: just run it and give me back its result.\n\nCommand hides from its caller:\n* a way how it realizes \"runs\"\n* how it gets data of output to be parsed\n* how it parses that data\n\nCommand shows to its caller:\n* API to start/stop it or await for its completion\n* API to query for its result or result readiness\n\nCommand works as [Futures and promises](https://en.wikipedia.org/wiki/Futures_and_promises)\n\nAfter starting, we await for its result which is parsed out command output provided usually as dict.\nRunning that command and parsing its output may take some time, so till that point result computation is yet incomplete.\n\n## Command as future\n* it starts some command on device/shell over connection\n  (as future-function starts its execution)\n* it parses data incoming over such connection\n  (as future-function does its processing)\n* it stores result of that parsing\n  (as future-function concludes in calculation result)\n* it provides means to return that result\n  (as future-function does via 'return' or 'yield' statement)\n* its result is not ready \"just-after\" calling command\n  (as it is with future in contrast to function)\n\nSo command should have future API.\n\nQuote from **_\"Professional Python\"_** by **Luke Sneeringer**:\n\u003e The Future is a standalone object. It is independent of the actual function that is running.\n\u003e It does nothing but store the state and result information.\n\nCommand differs in that it is both:\n* function-like object performing computation\n* future-like object storing result of that computation.\n\n## Command vs. Connection-observer\nCommand is just \"active version\" of connection observer.\n\nConnection observer is passive since it just observes connection for some data;\ndata that may just asynchronously appear (alarms, reboots or anything you want).\nIntention here is split of responsibility: one observer is looking for alarms,\nanother one for reboots.\n\nCommand is active since it actively triggers some output on connection\nby sending command-string over that connection. So, it activates some action\non device-behind-connection. That action is \"command\" in device terminology.\nLike `ping` on bash console/device. And it produces that \"command\" output.\nThat output is what Moler's Command as connection-observer is looking for.\n\n## Most well known Python's futures\n* [concurrent.futures.Future](https://docs.python.org/3/library/concurrent.futures.html)\n* [asyncio.Future](https://docs.python.org/3/library/asyncio-task.html#future)\n\n| API                     | concurrent.futures.Future                   | asyncio.Future                                      |\n| :---------------------- | :------------------------------------------ | :-------------------------------------------------- |\n| storing result          | :white_check_mark: `set_result()`           | :white_check_mark: `set_result()`                   |\n| result retrieval        | :white_check_mark: `result()`               | :white_check_mark: `result()`                       |\n| storing failure cause   | :white_check_mark: `set_exception()`        | :white_check_mark: `set_exception()`                |\n| failure cause retrieval | :white_check_mark: `exception()`            | :white_check_mark: `exception()`                    |\n| stopping                | :white_check_mark: `cancel()`               | :white_check_mark: `cancel()`                       |\n| check if stopped        | :white_check_mark: `cancelled()`            | :white_check_mark: `cancelled()`                    |\n| check if running        | :white_check_mark: `running()`              | :no_entry_sign: `(but AbstractEventLoop.running())` |\n| check if completed      | :white_check_mark: `done()`                 | :white_check_mark: `done()`                         |\n| subscribe completion    | :white_check_mark: `add_done_callback()`    | :white_check_mark: `add_done_callback()`            |\n| unsubscribe completion  | :no_entry_sign:                             | :white_check_mark: `remove_done_callback()`         |\n\nStarting callable to be run \"as future\" is done by entities external to future-object\n\n| API              | concurrent.futures\u003cbr\u003estart via Executor objects (thread/proc) | asyncio\u003cbr\u003estart via module-lvl functions or ev-loop |\n| ---------------- | ---------------------------------------- | ---------------------------------------------- |\n| start callable   | submit(fn, *args, **kwargs)\u003cbr\u003eSchedules callable to be executed as\u003cbr\u003efn(*args **kwargs) -\u003e Future | ensure_future(coro_or_future) -\u003e Task\u003cbr\u003efuture = run_coroutine_threadsafe(coro, loop) |\n| start same callable\u003cbr\u003eon data iterator | map(fn, *iterables, timeout) -\u003e iterator | join_future = asyncio.gather(*map(f, iterable))\u003cbr\u003eloop.run_until_complete(join_future)|\n\nAwaiting completion of future is done by entities external to future-object\n\n| API               | concurrent.futures\u003cbr\u003eawaiting by module level functions | asyncio\u003cbr\u003eawaiting by module-lvl functions or ev-loop |\n| ----------------- | ------------------------------------------ | -------------------------------------------- |\n| await completion  |  done, not_done = wait(futures, timeout) -\u003e futures | done, not_done = await wait(futures)\u003cbr\u003eresults = await gather(futures)\u003cbr\u003eresult = await future\u003cbr\u003eresult = yield from future\u003cbr\u003eresult = await coroutine\u003cbr\u003eresult = yield from coroutine\u003cbr\u003eresult = yield from wait_for(future, timeout)\u003cbr\u003eloop.run_until_complete(future) -\u003e blocking run |\n| process as they\u003cbr\u003ecomplete | for done in as_completed(futures, timeout) -\u003e futures | for done in as_completed(futures, timeout) -\u003e futures |\n\n## Fundamental difference of command\nContrary to **concurrent.futures** and **asyncio** we don't want command to be run by some external entity.\nWe want it to be self-executable for usage simplicity.\nWe want to take command and just say to it:\n* **\"run\"** or **\"run in background\"**\n* and not **\"Hi, external runner, would you run/run-background that command for me\"**\n\n# Designed API\n1. create command object\n``` python\ncommand = Command()\n```\n\n2. run it synchronously/blocking and get result in one shot behaves like function call since Command is callable.\n\nRun-as-callable gives big advantage since it fits well in python ecosystem.\n``` python\nresult = command()\n```\nfunction example:\n``` python\nmap(ping_cmd, all_machines_to_validate_reachability)\n```\n\n3. run it asynchronously/nonblocking\n``` python\ncommand_as_future = command.start()\n```\n\n4. shift from background to foreground\n\n**asyncio:** variant looks like:\n``` python\nresult = await future\ndone_futures, pending = yield from asyncio.wait(futures)\nresult = yield from asyncio.wait_for(future, 60.0)\n```\nand **concurrent.futures** variant looks like:\n``` python\ndone_futures, pending = wait(futures)\n```\nMoler's API maps to above well-known API\n``` python\nresult = command.await_done(timeout)\n```\n* it is \"internal\" to command \"Hi command, that is what I want from you\" (above APIs say \"Hi you there, that is what I want you to do with command\")\n* it directly (Zen of Python) shows what we are awaiting for\n* timeout is required parameter (not as in concurrent.futures) since we don't expect endless execution of command (user must know what is worst case timeout to await command completion)\n\n# Video introduction\nYou can [watch videos how to use Moler on YouTube](https://www.youtube.com/channel/UCgToo2qq8kLMyEgzd4btM9g).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnokia%2Fmoler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnokia%2Fmoler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnokia%2Fmoler/lists"}