Skip to content

Commit

Permalink
ver 0.1.5-alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
blademd committed Oct 19, 2023
1 parent c514a87 commit d101186
Show file tree
Hide file tree
Showing 35 changed files with 866 additions and 1,235 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ Thymus supports:

</details>

## Requirements

Python **3.8.1**. Please, see also the `requirements.txt`.

## Modes

Thymus operates in two modes:
Expand Down
227 changes: 115 additions & 112 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
[tool.poetry]
name = "thymus"
version = "0.1.3"
version = "0.1.5"
description = "A browser for network configuration files"
authors = ["Igor Malyushkin <gmalyushkin@gmail.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.8.1"
textual = "0.36.0"
textual = "0.40.0"
netmiko = "^4.2.0"
thymus-ast = "^0.1.2"


[tool.poetry.group.dev.dependencies]
Expand Down
Binary file modified requirements.txt
Binary file not shown.
12 changes: 6 additions & 6 deletions thymus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__version__ = '0.1.4-alpha'
__version__ = '0.1.5-alpha'

CONFIG_PATH = 'thymus/settings/'
CONFIG_PATH = 'settings/'
CONFIG_NAME = 'thymus.json'
WELCOME_TEXT = '''\
▄▄▄█████▓ ██░ ██▓██ ██▓ ███▄ ▄███▓ █ ██ ██████
Expand All @@ -17,13 +17,13 @@
'''
WELCOME_TEXT_LEN = 55
SAVES_DIR = 'thymus/saves/'
SCREENS_SAVES_DIR = 'thymus/saves/screenshots/'
LOGGING_CONF_DIR = 'thymus/settings/'
SAVES_DIR = 'saves/'
SCREENS_SAVES_DIR = 'saves/screenshots/'
LOGGING_CONF_DIR = 'settings/'
LOGGING_CONF = LOGGING_CONF_DIR + 'logging.conf'
LOGGING_CONF_ENCODING = 'utf-8'
LOGGING_LEVEL = 'INFO'
LOGGING_FILE_DIR = 'thymus/log/'
LOGGING_FILE_DIR = 'log/'
LOGGING_FILE = LOGGING_FILE_DIR + 'thymus.log'
LOGGING_FILE_ENCODING = 'utf-8'
LOGGING_FILE_MAX_SIZE_BYTES = 5000000
Expand Down
91 changes: 55 additions & 36 deletions thymus/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@

from __future__ import annotations

from typing import TYPE_CHECKING
import os
import sys
import json
import logging

from typing import Any
from copy import copy
from logging.handlers import RotatingFileHandler, BufferingHandler
from logging.config import fileConfig

from pygments.styles import get_all_styles
if sys.version_info.major == 3 and sys.version_info.minor >= 9:
from collections.abc import Callable
else:
from typing import Callable

from pygments.styles import get_all_styles # type: ignore

from . import __version__ as app_ver
from . import (
Expand All @@ -37,57 +47,45 @@
SAVES_DIR,
SCREENS_SAVES_DIR,
)
from .responses import SettingsResponse

import os
import sys
import json
import logging


if TYPE_CHECKING:
from typing import Any
if sys.version_info.major == 3 and sys.version_info.minor >= 9:
from collections.abc import Callable
else:
from typing import Callable
from .responses import Response, SettingsResponse


DEFAULT_GLOBALS = {
DEFAULT_GLOBALS: dict[str, str | int] = {
'theme': 'monokai',
'night_mode': 'off',
'filename_len': 256,
'sidebar_limit': 64,
'sidebar_strict_on_tab': 'on',
'open_dialog_platform': 'junos',
}
DEFAULT_JUNOS = {
DEFAULT_JUNOS: dict[str, str | int] = {
'spaces': 2,
}
DEFAULT_IOS = {
DEFAULT_IOS: dict[str, str | int] = {
'spaces': 1,
'heuristics': 'off',
'base_heuristics': 'on',
'crop': 'off',
'promisc': 'off',
}
DEFAULT_EOS = {
DEFAULT_EOS: dict[str, str | int] = {
'spaces': 2,
'heuristics': 'off',
'base_heuristics': 'on',
'crop': 'off',
'promisc': 'off',
}
DEFAULT_NXOS = {
DEFAULT_NXOS: dict[str, str | int] = {
'spaces': 2,
'heuristics': 'off',
'base_heuristics': 'on',
'crop': 'off',
}
PLATFORMS = {
PLATFORMS: dict[str, dict[str, str | int]] = {
'junos': DEFAULT_JUNOS,
'ios': DEFAULT_IOS,
'eos': DEFAULT_EOS,
'nxos': DEFAULT_NXOS,
'eos': DEFAULT_EOS,
}


Expand Down Expand Up @@ -128,8 +126,11 @@ def styles(self) -> list[str]:
def logger(self) -> logging.Logger:
return self.__logger

@property
def platforms(self) -> list[str]:
return list(self.__platforms.keys())

def __init__(self) -> None:
self.__logger: logging.Logger = None
self.__globals: dict[str, str | int] = {}
self.__platforms: dict[str, dict[str, str | int]] = {}
for platform in PLATFORMS:
Expand Down Expand Up @@ -256,7 +257,9 @@ def __save_config(self) -> None:
if not self.__is_dir:
return
self.__logger.debug(f'Saving a configuration into the file: {CONFIG_PATH}{CONFIG_NAME}.')
data = self.globals
data: dict[str, str | int | dict[str, str | int]] = {}
for k, v in self.globals.items():
data[k] = v
for platform, platform_data in PLATFORMS.items():
data.update(
{
Expand Down Expand Up @@ -301,7 +304,6 @@ def validate_keys(
errors.append(err_msg)
except Exception:
# makes it default
err_msg: str = ''
if platform:
err_msg = f'Incorrect value for the global attribute "{key}": {value}.'
self.__platforms[platform][key] = store[key]
Expand All @@ -325,6 +327,9 @@ def __validate_globals(self, key: str, value: str | int) -> str | int:
value = int(value)
if value <= 0 or value > N_VALUE_LIMIT:
raise Exception
elif key == 'open_dialog_platform':
if value not in PLATFORMS:
raise Exception
elif key in ('sidebar_strict_on_tab', 'night_mode'):
if value not in ('0', '1', 'on', 'off', 0, 1):
raise Exception
Expand Down Expand Up @@ -389,7 +394,7 @@ def is_bool_set(self, key: str, *, attr_name: str = 'globals') -> bool:
return False
return attr[key] in (1, '1', 'on')

def process_command(self, command: str) -> SettingsResponse:
def process_command(self, command: str) -> Response:
if not command.startswith('global '):
return SettingsResponse.error('Unknown global command.')
parts = command.split()
Expand All @@ -403,21 +408,26 @@ def process_command(self, command: str) -> SettingsResponse:
if arg == 'themes':
if len(parts) == 4:
return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.')
result: list[str] = []
result.append('* -- current theme')
themes_result = []
themes_result.append('* -- current theme')
for theme in self.styles:
result.append(f'{theme}*' if theme == self.globals['theme'] else theme)
return SettingsResponse.success(result)
themes_result.append(f'{theme}*' if theme == self.globals['theme'] else theme)
return SettingsResponse.success(themes_result)
elif arg == 'open_dialog_platform':
if len(parts) == 4:
return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.')
platf_result = self.globals[arg]
return SettingsResponse.success(str(platf_result))
elif arg in ('filename_len', 'sidebar_limit'):
if len(parts) == 4:
return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.')
result: int = self.globals[arg]
return SettingsResponse.success(str(result))
num_result = self.globals[arg]
return SettingsResponse.success(str(num_result))
elif arg in ('sidebar_strict_on_tab', 'night_mode'):
if len(parts) == 4:
return SettingsResponse.error(f'Too many arguments for "global show {arg}" command.')
result: bool = self.is_bool_set(arg)
return SettingsResponse.success(str(result))
bool_result = self.is_bool_set(arg)
return SettingsResponse.success(str(bool_result))
elif arg in PLATFORMS:
if len(parts) == 4:
subarg = parts[3]
Expand Down Expand Up @@ -450,6 +460,15 @@ def process_command(self, command: str) -> SettingsResponse:
self.__globals[arg] = value
self.__save_config()
return SettingsResponse.success(f'The "{arg}" was changed to: {value}.')
elif arg == 'open_dialog_platform':
if len(parts) > 4:
return SettingsResponse.error('Too many arguments for "global set" command.')
value = parts[3]
if value not in PLATFORMS:
return SettingsResponse.error(f'Unsupported platform: {value}.')
self.__globals[arg] = value
self.__save_config()
return SettingsResponse.success(f'The "{arg}" was changed to: {value}.')
elif arg in ('filename_len', 'sidebar_limit'):
if len(parts) > 4:
return SettingsResponse.error(f'Too many arguments for "global set {arg}" command.')
Expand All @@ -464,7 +483,7 @@ def process_command(self, command: str) -> SettingsResponse:
return SettingsResponse.error(f'Too many arguments for "global set {arg}" command.')
value = parts[3]
if value not in ('0', '1', 'on', 'off', 0, 1):
raise SettingsResponse.error('Value must be in (0, 1, on, off).')
return SettingsResponse.error('Value must be in (0, 1, on, off).')
self.__globals[arg] = value
self.__save_config()
return SettingsResponse.success(f'The "{arg}" was changed to: {value}.')
Expand Down
48 changes: 30 additions & 18 deletions thymus/clier.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
from __future__ import annotations

from typing import TYPE_CHECKING
import sys
import time

from typing import Optional
from logging import Logger, getLogger

from .contexts import (
Context,
JunOSContext,
IOSContext,
EOSContext
)

import sys
import time


if TYPE_CHECKING:
from .contexts import Context

NOS_LIST = {
ENCODING = 'utf-8'
NOS_LIST: dict[str, type[Context]] = {
'junos': JunOSContext,
'ios': IOSContext,
'eos': EOSContext,
Expand All @@ -34,48 +33,58 @@ class SystemWrapper:
'__contexts',
'__current',
'__number',
'__logger',
)

def __init__(self) -> None:
self.__base_prompt: str = 'thymus> '
self.__contexts: dict[str, Context] = {}
self.__current: Context = {}
self.__current: Optional[Context] = None
self.__number: int = 0
self.__logger: Logger = getLogger('clier_logger')

def __open_config(self, args: list[str]) -> None:
if len(args) != 2:
err_print('Incorrect arguments for "open". Usage: "open nos_type file".')
return
return None
nos = args[0]
config_path = args[1]
nos = nos.lower()
if nos not in NOS_LIST:
err_print(f'Unknown network OS: {nos}. Use:')
for key in NOS_LIST:
err_print(f'\t{key}')
return
return None
config: list[str] = []
try:
with open(config_path, encoding='utf-8-sig', errors='replace') as f:
config = f.readlines()
except FileNotFoundError:
err_print(f'Cannot open the file: {config_path}.')
return
return None
context_name = f'vty{self.__number}'
self.__contexts[context_name] = NOS_LIST[nos](context_name, config)
self.__contexts[context_name] = NOS_LIST[nos](
context_name,
config,
encoding=ENCODING,
settings={},
logger=self.__logger
)
self.__current = self.__contexts[context_name]
self.__number += 1
print(f'[{nos}] "{config_path}" successfully opened!')

def __switch_context(self, args: list[str]) -> None:
if len(args) != 1:
err_print('Incorrect arguments for "switch".')
return
return None
context_name = args[0]
if context_name not in self.__contexts or not self.__contexts[context_name]:
err_print(f'No such a context: {context_name}.')
return
return None
self.__current = self.__contexts[context_name]
print(f'Context is switched to: {context_name}.')
return None

def __new_prompt(self) -> str:
if not self.__current:
Expand Down Expand Up @@ -104,10 +113,12 @@ def process_command(self, value: str) -> str:
err_print('Unknown command or no valid context.')
return self.__base_prompt
result = self.__current.on_enter(value)
out = print if result.is_ok else err_print
for line in result.value:
if line:
out(line)
if result.is_ok:
print(line)
else:
err_print(line)
return self.__new_prompt()
return self.__base_prompt

Expand All @@ -129,3 +140,4 @@ def main() -> None:
t = time.time()
prompt = cli.process_command(user_input)
print(f'\nCommand execution time is {time.time() - t} secs.')
return None
Loading

0 comments on commit d101186

Please sign in to comment.