Address several code style concerns

This commit is contained in:
Aaron Heise 2023-02-11 07:38:35 -06:00
parent 2789ef2624
commit 81459efcd6
10 changed files with 196 additions and 147 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ testconfig/
/rnsh.egg-info/
/build/
/dist/
.pytest_cache/

View File

@ -12,7 +12,7 @@ out.
## Quickstart
Requires Python 3.11+ on Linux or Unix. WSL probably works. Cygwin might work, too.
Requires Python 3.10+ on Linux or Unix. WSL probably works. Cygwin might work, too.
- Activate a virtualenv
- `pip3 install rnsh`

View File

@ -16,6 +16,13 @@ psutil = "^5.9.4"
rnsh = 'rnsh.rnsh:rnsh_cli'
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.1"
flake8 = "^6.0.0"
bandit = "^1.7.4"
isort = "^5.12.0"
safety = "^2.3.5"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

34
rnsh/exception.py Normal file
View File

@ -0,0 +1,34 @@
from contextlib import AbstractContextManager
class permit(AbstractContextManager):
"""Context manager to allow specified exceptions
The specified exceptions will be allowed to bubble up. Other
exceptions are suppressed.
After a non-matching exception is suppressed, execution proceeds
with the next statement following the with statement.
with allow(KeyboardInterrupt):
time.sleep(300)
# Execution still resumes here if no KeyboardInterrupt
"""
def __init__(self, *exceptions):
self._exceptions = exceptions
def __enter__(self):
pass
def __exit__(self, exctype, excinst, exctb):
# Unlike isinstance and issubclass, CPython exception handling
# currently only looks at the concrete type hierarchy (ignoring
# the instance and subclass checking hooks). While Guido considers
# that a bug rather than a feature, it's a fairly hard one to fix
# due to various internal implementation details. suppress provides
# the simpler issubclass based semantics, rather than trying to
# exactly reproduce the limitations of the CPython interpreter.
#
# See http://bugs.python.org/issue12029 for more details
return exctype is not None and issubclass(exctype, self._exceptions)

View File

@ -23,24 +23,27 @@
import asyncio
import contextlib
import errno
import fcntl
import functools
import re
import logging as __logging
import os
import pty
import select
import signal
import struct
import sys
import termios
import threading
import tty
import pty
import os
import asyncio
import sys
import fcntl
import psutil
import select
import termios
import logging as __logging
import rnsh.exception as exception
module_logger = __logging.getLogger(__name__)
def tty_add_reader_callback(fd: int, callback: callable, loop: asyncio.AbstractEventLoop | None = None):
def tty_add_reader_callback(fd: int, callback: callable, loop: asyncio.AbstractEventLoop = None):
"""
Add an async reader callback for a tty file descriptor.
@ -60,7 +63,8 @@ def tty_add_reader_callback(fd: int, callback: callable, loop: asyncio.AbstractE
loop = asyncio.get_running_loop()
loop.add_reader(fd, callback)
def tty_read(fd: int) -> bytes | None:
def tty_read(fd: int) -> bytes:
"""
Read available bytes from a tty file descriptor. When used in a callback added to a file descriptor using
tty_add_reader_callback(...), this function creates a solution for non-blocking reads from ttys.
@ -78,7 +82,7 @@ def tty_read(fd: int) -> bytes | None:
break
for f in ready:
try:
data = os.read(fd, 512)
data = os.read(f, 512)
except OSError as e:
if e.errno != errno.EIO and e.errno != errno.EWOULDBLOCK:
raise
@ -89,6 +93,7 @@ def tty_read(fd: int) -> bytes | None:
result.extend(data)
return result
def fd_is_closed(fd: int) -> bool:
"""
Check if file descriptor is closed
@ -100,20 +105,20 @@ def fd_is_closed(fd: int) -> bool:
except OSError as ose:
return ose.errno == errno.EBADF
def tty_unset_reader_callbacks(fd: int, loop: asyncio.AbstractEventLoop | None = None):
def tty_unset_reader_callbacks(fd: int, loop: asyncio.AbstractEventLoop = None):
"""
Remove async reader callbacks for file descriptor.
:param fd: file descriptor
:param loop: asyncio event loop from which to remove callbacks
"""
try:
with exception.permit(SystemExit):
if loop is None:
loop = asyncio.get_running_loop()
loop.remove_reader(fd)
except:
pass
def tty_get_winsize(fd: int) -> [int, int, int , int]:
def tty_get_winsize(fd: int) -> [int, int, int, int]:
"""
Ge the window size of a tty.
:param fd: file descriptor of tty
@ -123,6 +128,7 @@ def tty_get_winsize(fd: int) -> [int, int, int , int]:
rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
return rows, cols, h_pixels, v_pixels
def tty_set_winsize(fd: int, rows: int, cols: int, h_pixels: int, v_pixels: int):
"""
Set the window size on a tty.
@ -137,6 +143,7 @@ def tty_set_winsize(fd: int, rows: int, cols: int, h_pixels: int, v_pixels: int)
packed = struct.pack('HHHH', rows, cols, h_pixels, v_pixels)
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
def process_exists(pid) -> bool:
"""
Check For the existence of a unix pid.
@ -150,6 +157,7 @@ def process_exists(pid) -> bool:
else:
return True
class TtyRestorer:
def __init__(self, fd: int):
"""
@ -189,12 +197,12 @@ class CallbackSubprocess:
# time between checks of child process
PROCESS_POLL_TIME: float = 0.1
def __init__(self, argv: [str], env: dict | None, loop: asyncio.AbstractEventLoop, stdout_callback: callable,
def __init__(self, argv: [str], env: dict, loop: asyncio.AbstractEventLoop, stdout_callback: callable,
terminated_callback: callable):
"""
Fork a child process and generate callbacks with output from the process.
:param argv: the command line, tokenized. The first element must be the absolute path to an executable file.
:param term: the value that should be set for TERM. If None, the value from the parent process will be used
:param env: environment variables to override
:param loop: the asyncio event loop to use
:param stdout_callback: callback for data, e.g. def callback(data:bytes) -> None
:param terminated_callback: callback for termination/return code, e.g. def callback(return_code:int) -> None
@ -210,9 +218,9 @@ class CallbackSubprocess:
self._loop = loop
self._stdout_cb = stdout_callback
self._terminated_cb = terminated_callback
self._pid: int | None = None
self._child_fd: int | None = None
self._return_code: int | None = None
self._pid: int = None
self._child_fd: int = None
self._return_code: int = None
def terminate(self, kill_delay: float = 1.0):
"""
@ -223,19 +231,15 @@ class CallbackSubprocess:
if not self.running:
return
try:
with exception.permit(SystemExit):
os.kill(self._pid, signal.SIGTERM)
except:
pass
def kill():
if process_exists(self._pid):
self._log.debug("kill()")
try:
with exception.permit(SystemExit):
os.kill(self._pid, signal.SIGHUP)
os.kill(self._pid, signal.SIGKILL)
except:
pass
self._loop.call_later(kill_delay, kill)
@ -280,15 +284,15 @@ class CallbackSubprocess:
self._log.debug(f"set_winsize({r},{c},{h},{v}")
tty_set_winsize(self._child_fd, r, c, h, v)
def copy_winsize(self, fromfd:int):
def copy_winsize(self, fromfd: int):
"""
Copy window size from one tty to another.
:param fromfd: source tty file descriptor
"""
r,c,h,v = tty_get_winsize(fromfd)
self.set_winsize(r,c,h,v)
r, c, h, v = tty_get_winsize(fromfd)
self.set_winsize(r, c, h, v)
def tcsetattr(self, when: int, attr: list[int | list[int | bytes]]):
def tcsetattr(self, when: int, attr: list[any]): # actual type is list[int | list[int | bytes]]
"""
Set tty attributes.
:param when: when to apply change: termios.TCSANOW or termios.TCSADRAIN or termios.TCSAFLUSH
@ -296,7 +300,7 @@ class CallbackSubprocess:
"""
termios.tcsetattr(self._child_fd, when, attr)
def tcgetattr(self) -> list[int | list[int | bytes]]:
def tcgetattr(self) -> list[any]: # actual type is list[int | list[int | bytes]]
"""
Get tty attributes.
:return: tty attributes value
@ -328,7 +332,6 @@ class CallbackSubprocess:
# env["SHELL"] = program
# self._log.debug(f"set login shell {self._command}")
self._pid, self._child_fd = pty.fork()
if self._pid == 0:
@ -341,10 +344,8 @@ class CallbackSubprocess:
for c in p.connections(kind='all'):
if c == sys.stdin.fileno() or c == sys.stdout.fileno() or c == sys.stderr.fileno():
continue
try:
with exception.permit(SystemExit):
os.close(c.fd)
except:
pass
os.setpgrp()
os.execvpe(program, self._command, env)
except Exception as err:
@ -364,21 +365,19 @@ class CallbackSubprocess:
except Exception as e:
if not hasattr(e, "errno") or e.errno != errno.ECHILD:
self._log.debug(f"Error in process poll: {e}")
self._loop.call_later(CallbackSubprocess.PROCESS_POLL_TIME, poll)
def reader(fd: int, callback: callable):
result = bytearray()
try:
c = tty_read(fd)
if c is not None and len(c) > 0:
callback(c)
except:
pass
with exception.permit(SystemExit):
data = tty_read(fd)
if data is not None and len(data) > 0:
callback(data)
tty_add_reader_callback(self._child_fd, functools.partial(reader, self._child_fd, self._stdout_cb), self._loop)
@property
def return_code(self) -> int | None:
def return_code(self) -> int:
return self._return_code
@ -387,7 +386,6 @@ async def main():
A test driver for the CallbackProcess class.
python ./process.py /bin/zsh --login
"""
import rnsh.testlogging
log = module_logger.getChild("main")
if len(sys.argv) <= 1:
@ -413,14 +411,14 @@ async def main():
stdout_callback=stdout,
terminated_callback=terminated)
def sigint_handler(signal, frame):
def sigint_handler(sig, frame):
# log.debug("KeyboardInterrupt")
if process is None or process.started and not process.running:
raise KeyboardInterrupt
elif process.running:
process.write("\x03".encode("utf-8"))
def sigwinch_handler(signal, frame):
def sigwinch_handler(sig, frame):
# log.debug("WindowChanged")
process.copy_winsize(sys.stdin.fileno())
@ -443,6 +441,7 @@ async def main():
log.debug(f"got retcode {val}")
return val
if __name__ == "__main__":
tr = TtyRestorer(sys.stdin.fileno())
try:

View File

@ -24,6 +24,7 @@ import asyncio
import logging
import threading
import time
import rnsh.exception as exception
import logging as __logging
from typing import Callable
@ -47,7 +48,9 @@ class RetryStatus:
@property
def ready(self):
ready = time.time() > self.try_time + self.wait_delay
# self._log.debug(f"ready check {self.tag} try_time {self.try_time} wait_delay {self.wait_delay} next_try {self.try_time + self.wait_delay} now {time.time()} exceeded {time.time() - self.try_time - self.wait_delay} ready {ready}")
self._log.debug(f"ready check {self.tag} try_time {self.try_time} wait_delay {self.wait_delay} " +
f"next_try {self.try_time + self.wait_delay} now {time.time()} " +
f"exceeded {time.time() - self.try_time - self.wait_delay} ready {ready}")
return ready
@property
@ -72,11 +75,11 @@ class RetryThread:
self._tag_counter = 0
self._lock = threading.RLock()
self._run = True
self._finished: asyncio.Future | None = None
self._finished: asyncio.Future = None
self._thread = threading.Thread(name=name, target=self._thread_run)
self._thread.start()
def close(self, loop: asyncio.AbstractEventLoop | None = None) -> asyncio.Future | None:
def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future:
self._log.debug("stopping timer thread")
if loop is None:
self._run = False
@ -110,10 +113,8 @@ class RetryThread:
with self._lock:
for retry in prune:
self._log.debug(f"pruned retry {retry.tag}, retry count {retry.tries}/{retry.try_limit}")
try:
with exception.permit(SystemExit):
self._statuses.remove(retry)
except:
pass
if self._finished is not None:
self._finished.set_result(None)
@ -126,7 +127,7 @@ class RetryThread:
return next(filter(lambda s: s.tag == tag, self._statuses), None) is not None
def begin(self, try_limit: int, wait_delay: float, try_callback: Callable[[any, int], any],
timeout_callback: Callable[[any, int], None], tag: int | None = None) -> any:
timeout_callback: Callable[[any, int], None], tag: int = None) -> any:
self._log.debug(f"running first try")
tag = try_callback(tag, 1)
self._log.debug(f"first try got id {tag}")

View File

@ -23,22 +23,25 @@
# SOFTWARE.
from __future__ import annotations
import functools
from typing import Callable, TypeVar
import termios
import rnsh.rnslogging as rnslogging
import RNS
import time
import sys
import os
import base64
import rnsh.process as process
import asyncio
import threading
import signal
import rnsh.retry as retry
from rnsh.__version import __version__
import base64
import functools
import logging as __logging
import os
import signal
import sys
import termios
import threading
import time
from typing import Callable, TypeVar
import RNS
import rnsh.exception as exception
import rnsh.process as process
import rnsh.retry as retry
import rnsh.rnslogging as rnslogging
from rnsh.__version import __version__
module_logger = __logging.getLogger(__name__)
@ -60,11 +63,12 @@ _retry_timer = retry.RetryThread()
_destination: RNS.Destination | None = None
_loop: asyncio.AbstractEventLoop | None = None
async def _check_finished(timeout: float = 0):
await process.event_wait(_finished, timeout=timeout)
def _sigint_handler(signal, frame):
def _sigint_handler(sig, frame):
global _finished
log = _get_logger("_sigint_handler")
log.debug("SIGINT")
@ -91,7 +95,9 @@ def _prepare_identity(identity_path):
_identity = RNS.Identity()
_identity.to_file(identity_path)
def _print_identity(configdir, identitypath, service_name, include_destination: bool):
global _reticulum
_reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO)
_prepare_identity(identitypath)
destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, service_name)
@ -121,12 +127,13 @@ async def _listen(configdir, command, identitypath=None, service_name="default",
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
if len(a) != dest_len:
raise ValueError(
"Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(
"Allowed destination length is invalid, must be {hex} hexadecimal " +
"characters ({byte} bytes).".format(
hex=dest_len, byte=dest_len // 2))
try:
destination_hash = bytes.fromhex(a)
_allowed_identity_hashes.append(destination_hash)
except Exception as e:
except Exception:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
log.error(str(e))
@ -169,12 +176,10 @@ async def _listen(configdir, command, identitypath=None, service_name="default",
except KeyboardInterrupt:
log.warning("Shutting down")
for link in list(_destination.links):
try:
with exception.permit(SystemExit):
proc = ProcessState.get_for_tag(link.link_id)
if proc is not None and proc.process.running:
proc.process.terminate()
except:
pass
await asyncio.sleep(1)
links_still_active = list(filter(lambda l: l.status != RNS.Link.CLOSED, _destination.links))
for link in links_still_active:
@ -197,16 +202,11 @@ class ProcessState:
cls.clear_tag(tag)
cls._processes.append((tag, ps))
@classmethod
def clear_tag(cls, tag: any):
with cls._lock:
try:
with exception.permit(SystemExit):
cls._processes.remove(tag)
except:
pass
def __init__(self,
tag: any,
@ -222,7 +222,7 @@ class ProcessState:
self._mdu = mdu
self._loop = loop if loop is not None else asyncio.get_running_loop()
self._process = process.CallbackSubprocess(argv=cmd,
env={ "TERM": term or os.environ.get("TERM", None),
env={"TERM": term or os.environ.get("TERM", None),
"RNS_REMOTE_IDENTITY": remote_identity or ""},
loop=loop,
stdout_callback=self._stdout_data,
@ -302,7 +302,6 @@ class ProcessState:
except Exception as e:
self._log.debug(f"failed to update winsz: {e}")
REQUEST_IDX_STDIN = 0
REQUEST_IDX_TERM = 1
REQUEST_IDX_TIOS = 2
@ -342,7 +341,7 @@ class ProcessState:
# vpix = data[ProcessState.REQUEST_IDX_VPIX] # window vertical pixels
# term_state = data[ProcessState.REQUEST_IDX_ROWS:ProcessState.REQUEST_IDX_VPIX+1]
response = ProcessState.default_response()
term_state = data[ProcessState.REQUEST_IDX_TIOS:ProcessState.REQUEST_IDX_VPIX+1]
term_state = data[ProcessState.REQUEST_IDX_TIOS:ProcessState.REQUEST_IDX_VPIX + 1]
response[ProcessState.RESPONSE_IDX_RUNNING] = self.process.running
if self.process.running:
@ -406,7 +405,8 @@ def _subproc_data_ready(link: RNS.Link, chars_available: int):
else:
if not timeout:
log.info(
f"Notifying client try {tries} (retcode: {process_state.return_code} chars avail: {chars_available})")
f"Notifying client try {tries} (retcode: {process_state.return_code} " +
f"chars avail: {chars_available})")
packet = RNS.Packet(link, DATA_AVAIL_MSG.encode("utf-8"))
packet.send()
pr = packet.receipt
@ -431,6 +431,7 @@ def _subproc_data_ready(link: RNS.Link, chars_available: int):
else:
log.debug(f"Notification already pending for link {link}")
def _subproc_terminated(link: RNS.Link, return_code: int):
global _loop
log = _get_logger("_subproc_terminated")
@ -444,18 +445,20 @@ def _subproc_terminated(link: RNS.Link, return_code: int):
def inner():
log.debug(f"cleanup culled link {link}")
if link and link.status != RNS.Link.CLOSED:
with exception.permit(SystemExit):
try:
link.teardown()
except:
pass
finally:
ProcessState.clear_tag(link.link_id)
_loop.call_later(300, inner)
_loop.call_soon(_subproc_data_ready, link, 0)
_loop.call_soon_threadsafe(cleanup)
def _listen_start_proc(link: RNS.Link, remote_identity: str | None, term: str, loop: asyncio.AbstractEventLoop) -> ProcessState | None:
def _listen_start_proc(link: RNS.Link, remote_identity: str | None, term: str,
loop: asyncio.AbstractEventLoop) -> ProcessState | None:
global _cmd
log = _get_logger("_listen_start_proc")
try:
@ -501,7 +504,7 @@ def _initiator_identified(link, identity):
global _allow_all, _cmd, _loop
log = _get_logger("_initiator_identified")
log.info("Initiator of link " + str(link) + " identified as " + RNS.prettyhexrep(identity.hash))
if not _allow_all and not identity.hash in _allowed_identity_hashes:
if not _allow_all and identity.hash not in _allowed_identity_hashes:
log.warning("Identity " + RNS.prettyhexrep(identity.hash) + " not allowed, tearing down link", RNS.LOG_WARNING)
link.teardown()
@ -540,7 +543,7 @@ def _listen_request(path, data, request_id, link_id, remote_identity, requested_
return ProcessState.default_response()
async def _spin(until: Callable | None = None, timeout: float | None = None) -> bool:
async def _spin(until: callable = None, timeout: float | None = None) -> bool:
if timeout is not None:
timeout += time.time()
@ -644,7 +647,8 @@ async def _execute(configdir, identitypath=None, verbosity=0, quietness=0, noid=
await _spin(
until=lambda: _link.status == RNS.Link.CLOSED or (
request_receipt.status != RNS.RequestReceipt.FAILED and request_receipt.status != RNS.RequestReceipt.SENT),
request_receipt.status != RNS.RequestReceipt.FAILED and
request_receipt.status != RNS.RequestReceipt.SENT),
timeout=timeout
)
@ -758,10 +762,9 @@ async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness
if return_code is not None:
log.debug(f"received return code {return_code}, exiting")
try:
with exception.permit(SystemExit):
_link.teardown()
except:
pass
return return_code
except RemoteExecutionError as e:
print(e.msg)
@ -772,13 +775,15 @@ async def _initiate(configdir: str, identitypath: str, verbosity: int, quietness
_T = TypeVar("_T")
def _split_array_at(arr: [_T], at: _T) -> ([_T], [_T]):
try:
idx = arr.index(at)
return arr[:idx], arr[idx+1:]
return arr[:idx], arr[idx + 1:]
except ValueError:
return arr, []
async def main():
global _tr, _finished, _loop
import docopt
@ -879,9 +884,8 @@ Options:
timeout=args_timeout,
)
return return_code if args_mirror else 0
except:
finally:
_tr.restore()
raise
else:
print("")
print(args)
@ -889,17 +893,15 @@ Options:
def rnsh_cli():
return_code = 1
try:
return_code = asyncio.run(main())
finally:
try:
with exception.permit(SystemExit):
process.tty_unset_reader_callbacks(sys.stdin.fileno())
except:
pass
_tr.restore()
_retry_timer.close()
sys.exit(return_code)
sys.exit(return_code or 255)
if __name__ == "__main__":

View File

@ -20,17 +20,18 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import logging
import sys
import termios
from logging import Handler, getLevelName
from types import GenericAlias
import os
import tty
from typing import List, Any
import asyncio
import termios
import sys
from typing import Any
import RNS
import json
import rnsh.exception as exception
class RnsHandler(Handler):
"""
@ -77,29 +78,35 @@ class RnsHandler(Handler):
__class_getitem__ = classmethod(GenericAlias)
log_format = '%(name)-30s %(message)s [%(threadName)s]'
logging.basicConfig(
level=logging.DEBUG, # RNS.log will filter it, but some formatting will still be processed before it gets there
#format='%(asctime)s.%(msecs)03d %(levelname)-6s %(threadName)-15s %(name)-15s %(message)s',
# format='%(asctime)s.%(msecs)03d %(levelname)-6s %(threadName)-15s %(name)-15s %(message)s',
format=log_format,
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[RnsHandler()])
_loop: asyncio.AbstractEventLoop | None = None
_loop: asyncio.AbstractEventLoop = None
def set_main_loop(loop: asyncio.AbstractEventLoop):
global _loop
_loop = loop
#hack for temporarily overriding term settings to make debug print right
# hack for temporarily overriding term settings to make debug print right
_rns_log_orig = RNS.log
def _rns_log(msg, level=3, _override_destination = False):
def _rns_log(msg, level=3, _override_destination=False):
if not RNS.compact_log_fmt:
msg = (" " * (7 - len(RNS.loglevelname(level)))) + msg
def inner():
tattr_orig: list[Any] | None = None
try:
tattr_orig: list[Any] = None
with exception.permit(SystemExit):
tattr = termios.tcgetattr(sys.stdin.fileno())
tattr_orig = tattr.copy()
# tcflag_t c_iflag; /* input modes */
@ -109,19 +116,16 @@ def _rns_log(msg, level=3, _override_destination = False):
# cc_t c_cc[NCCS]; /* special characters */
tattr[1] = tattr[1] | termios.ONLRET | termios.ONLCR | termios.OPOST
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, tattr)
except:
pass
_rns_log_orig(msg, level, _override_destination)
if tattr_orig is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, tattr_orig)
try:
if _loop:
_loop.call_soon_threadsafe(inner)
else:
inner()
except:
inner()
RNS.log = _rns_log

View File

@ -29,7 +29,8 @@ log_format = '%(levelname)-6s %(name)-40s %(message)s [%(threadName)s]' \
__logging.basicConfig(
level=__logging.INFO,
#format='%(asctime)s.%(msecs)03d %(levelname)-6s %(threadName)-15s %(name)-15s %(message)s',
# format='%(asctime)s.%(msecs)03d %(levelname)-6s %(threadName)-15s %(name)-15s %(message)s',
format=log_format,
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[__logging.StreamHandler()])