Compare commits

..

4 Commits

34 changed files with 1050 additions and 147 deletions

View File

@ -12,7 +12,6 @@ omit =
**/__init__.py
# ignore main.py
main.py
# TODO: increase code-coverage:
precision = 0
exclude_lines =
pragma: no cover

View File

@ -6,10 +6,13 @@ info:
ADS2 an der Universität Leipzig (Sommersemester 2022)
implementiert.
options:
# log-level: DEBUG
log-level: INFO
verbose: &ref_verbose true
tarjan:
verbose: true
verbose: *ref_verbose
tsp:
verbose: true
verbose: *ref_verbose
hirschberg:
# standardwerte sind (1, 1) und (2, 1):
penality-gap: 1

View File

@ -11,51 +11,29 @@ import sys
os.chdir(os.path.join(os.path.dirname(__file__)));
sys.path.insert(0, os.getcwd());
from src.thirdparty.maths import *;
from models.generated.config import *;
from models.generated.commands import *;
from src.core.log import *;
from src.setup.config import *;
from src.models.graphs import *;
from src.algorithms.tarjan import *;
from src.algorithms.tsp import *;
from src.algorithms.hirschberg import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# GLOBAL CONSTANTS/VARIABLES
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
from src.models.config import *;
from src.core import log;
from src.setup import config;
from src import api;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# MAIN METHOD
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def enter():
for command in COMMANDS:
if isinstance(command, CommandTarjan):
tarjan_algorithm(
G = Graph(
nodes=command.nodes,
edges=list(map(tuple, command.edges)),
),
verbose = OPTIONS.tarjan.verbose
);
if isinstance(command, CommandTsp):
tsp_algorithm(
dist = np.asarray(command.dist, dtype=float),
optimise = min if command.optimise == EnumTSPOptimise.min else max,
verbose = OPTIONS.tsp.verbose,
);
elif isinstance(command, CommandHirschberg):
hirschberg_algorithm(
X = command.word1,
Y = command.word2,
once = command.once,
verbose = OPTIONS.hirschberg.verbose,
show = OPTIONS.hirschberg.show,
);
def enter(*args: str):
# set logging level:
log.configure_logging(config.LOG_LEVEL);
# process inputs:
if len(args) == 0:
# Führe befehle in Assets aus:
for command in config.COMMANDS:
result = api.run_command(command);
log.log_result(result, debug=True); # ignored if log-level >> DEBUG
else:
# Führe CLI-Befehl aus:
result = api.run_command_from_json(args[0]);
log.log_result(result, debug=True); # ignored if log-level >> DEBUG
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -63,4 +41,5 @@ def enter():
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
if __name__ == '__main__':
enter();
sys.tracebacklimit = 0;
enter(*sys.argv[1:]);

View File

@ -48,9 +48,17 @@ components:
Options pertaining to the rudimentary setup of the app.
type: object
required:
- log-level
- tsp
- tarjan
- hirschberg
properties:
log-level:
$ref: '#/components/schemas/EnumLogLevel'
verbose:
description: Global setting for verbosity.
type: boolean
default: false
tarjan:
type: object
required:
@ -106,6 +114,16 @@ components:
$ref: '#/components/schemas/EnumHirschbergShow'
default: []
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Enum LogLevel
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
EnumLogLevel:
description: |-
Enumeration of settings for log level.
type: string
enum:
- INFO
- DEBUG
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Enum Hirschberg - Verbosity options
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
EnumHirschbergVerbosity:

View File

@ -14,5 +14,6 @@ from src.algorithms.hirschberg.display import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'simple_algorithm',
'hirschberg_algorithm',
];

View File

@ -32,7 +32,6 @@ def simple_algorithm(
X: str,
Y: str,
verbose: List[EnumHirschbergVerbosity] = [],
show: List[EnumHirschbergShow] = [],
) -> Tuple[str, str]:
'''
Dieser Algorithmus berechnet die Edit-Distanzen + optimale Richtungen ein Mal.
@ -50,7 +49,6 @@ def simple_algorithm(
def hirschberg_algorithm(
X: str,
Y: str,
once: bool = False,
verbose: List[EnumHirschbergVerbosity] = [],
show: List[EnumHirschbergShow] = [],
) -> Tuple[str, str]:
@ -64,10 +62,6 @@ def hirschberg_algorithm(
Daraus wird unmittelbar ein optimales Alignment bestimmt.
Des Weiteren werden Zeitkosten durch Divide-and-Conquer klein gehalten.
'''
# ggf. nur den simplen Algorithmus ausführen:
if once:
return simple_algorithm(X=X, Y=Y, verbose=verbose, show=show);
align = hirschberg_algorithm_step(X=X, Y=Y, depth=1, verbose=verbose, show=show);
word_x = align.as_string1();
word_y = align.as_string2();

41
code/python/src/api.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.code import *;
from models.generated.commands import *;
from src.models.config import *
from src.endpoints import *;
from src.core.calls import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'run_command',
'run_command_from_json',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# API METHODS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@run_safely(tag='api-from-json')
def run_command_from_json(command_json: str) -> Result[CallResult, CallError]:
command = command_from_json(command_json);
return run_command(command);
@run_safely(tag='api-from-command')
def run_command(command: Command) -> Result[CallResult, CallError]:
if isinstance(command, CommandTarjan):
return endpoint_tarjan(command);
if isinstance(command, CommandTsp):
return endpoint_tsp(command);
elif isinstance(command, CommandHirschberg):
return endpoint_hirschberg(command);
raise Exception(f'No endpoint set for `{command.name.value}`-command type.');

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from __future__ import annotations;
from src.thirdparty.code import *;
from src.thirdparty.misc import *;
from src.thirdparty.run import *;
from src.thirdparty.types import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'CallResult',
'CallError',
'run_safely',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# CONSTANTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# local usage only
T = TypeVar('T');
V = TypeVar('V');
E = TypeVar('E', bound=list);
ARGS = ParamSpec('ARGS');
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# CLASS Trace for debugging only!
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@dataclass
class CallResult(): # pragma: no cover
'''
An auxiliary class which keeps track of the latest return value during calls.
'''
action_taken: bool = field(default=False);
message: Optional[Any] = field(default=None);
@dataclass
class CallErrorRaw(): # pragma: no cover
timestamp: str = field();
tag: str = field();
errors: List[str] = field(default_factory=list);
class CallError(CallErrorRaw):
'''
An auxiliary class which keeps track of potentially multiple errors during calls.
'''
timestamp: str;
tag: str;
errors: List[str];
def __init__(self, tag: str, err: Any = Nothing()):
self.timestamp = str(datetime.now());
self.tag = tag;
self.errors = [];
if isinstance(err, list):
for e in err:
self.append(e);
else:
self.append(err);
def __len__(self) -> int:
return len(self.errors);
def append(self, e: Any):
if isinstance(e, Nothing):
return;
if isinstance(e, Some):
e = e.unwrap();
self.errors.append(str(e));
def extend(self, E: CallError):
self.errors.extend(E.errors);
def __repr__(self) -> str:
return f'CallError(tag=\'{self.tag}\', errors={self.errors})';
def __str__(self) -> str:
return self.__repr__();
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DECORATOR - forces methods to run safely
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def run_safely(tag: Union[str, None] = None, error_message: Union[str, None] = None):
'''
Creates a decorator for an action to perform it safely.
@inputs (parameters)
- `tag` - optional string to aid error tracking.
- `error_message` - optional string for an error message.
### Example usage ###
```py
@run_safely(tag='recognise int', error_message='unrecognise string')
def action1(x: str) -> Result[int, CallError]:
return Ok(int(x));
assert action1('5') == Ok(5);
result = action1('not a number');
assert isinstance(result, Err);
err = result.unwrap_err();
assert isinstance(err, CallError);
assert err.tag == 'recognise int';
assert err.errors == ['unrecognise string'];
@run_safely('recognise int')
def action2(x: str) -> Result[int, CallError]:
return Ok(int(x));
assert action2('5') == Ok(5);
result = action2('not a number');
assert isinstance(result, Err);
err = result.unwrap_err();
assert isinstance(err, CallError);
assert err.tag == 'recognise int';
assert len(err.errors) == 1;
```
NOTE: in the second example, err.errors is a list containing
the stringified Exception generated when calling `int('not a number')`.
'''
def dec(action: Callable[ARGS, Result[V, CallError]]) -> Callable[ARGS, Result[V, CallError]]:
'''
Wraps action with return type Result[..., CallError],
so that it is performed safely a promise,
catching any internal exceptions as an Err(...)-component of the Result.
'''
@wraps(action)
def wrapped_action(*_, **__) -> Result[V, CallError]:
# NOTE: intercept Exceptions first, then flatten:
return Result.of(lambda: action(*_, **__)) \
.or_else(
lambda err: Err(CallError(
tag = tag or action.__name__,
err = error_message or err
))
) \
.and_then(lambda V: V);
return wrapped_action;
return dec;

View File

@ -5,60 +5,93 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
from src.thirdparty.code import *;
from src.thirdparty.log import *;
from src.thirdparty.types import *;
from src.core.calls import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'LOG_LEVELS',
'configure_logging',
'log_info',
'log_debug',
'log_warn',
'log_error',
'log_fatal',
'log_result',
'log_dev',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHODS logging
# CONSTANTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def log_info(*text: str):
'''
Prints an info message
'''
for line in text:
print("[\x1b[94;1mINFO\x1b[0m] {}".format(line));
_LOGGING_DEBUG_FILE: str = 'logs/debug.log';
class LOG_LEVELS(Enum): # pragma: no cover
INFO = logging.INFO;
DEBUG = logging.DEBUG;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHODS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def configure_logging(level: LOG_LEVELS): # pragma: no cover
logging.basicConfig(
format = '[\x1b[1m%(levelname)s\x1b[0m] %(message)s',
level = level.value,
);
return;
def log_debug(*text: str):
'''
Prints a debug message
'''
for line in text:
print("[\x1b[96;1mDEBUG\x1b[95;0m] {}".format(line));
return;
def log_debug(*messages: Any):
logging.debug(*messages);
def log_warn(*text: str):
'''
Prints a warning message
'''
for line in text:
print("[\x1b[93;1mWARNING\x1b[0m] {}".format(line));
return;
def log_info(*messages: Any):
logging.info(*messages);
def log_error(*text: str):
'''
Prints an error message
'''
for line in text:
print("[\x1b[91;1mERROR\x1b[0m] {}".format(line));
return;
def log_warn(*messages: Any):
logging.warning(*messages);
def log_fatal(*text: str):
'''
Prints a fatal error message + crashes
'''
for line in text:
print("[\x1b[91;1mFATAL\x1b[0m] {}".format(line));
def log_error(*messages: Any):
logging.error(*messages);
def log_fatal(*messages: Any):
logging.fatal(*messages);
exit(1);
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Special Methods
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def log_result(result: Result[CallResult, CallError], debug: bool = False):
'''
Logs safely encapsulated result of call as either debug/info or error.
@inputs
- `result` - the result of the call.
- `debug = False` (default) - if the result is okay, will be logged as an INFO message.
- `debug = True` - if the result is okay, will be logged as a DEBUG message.
'''
if isinstance(result, Ok):
value = result.unwrap();
if debug:
log_debug(asdict(value));
else:
log_info(asdict(value));
else:
err = result.unwrap_err();
log_error(asdict(err));
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# DEBUG LOGGING FOR DEVELOPMENT
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def log_dev(*messages: Any): # pragma: no cover
with open(_LOGGING_DEBUG_FILE, 'a') as fp:
print(*messages, file=fp);

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.endpoints.ep_algorithm_hirschberg import *;
from src.endpoints.ep_algorithm_tarjan import *;
from src.endpoints.ep_algorithm_tsp import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'endpoint_hirschberg',
'endpoint_tarjan',
'endpoint_tsp',
];

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.code import *;
from models.generated.commands import *;
from src.core.calls import *;
from src.setup import config;
from src.algorithms.hirschberg import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'endpoint_hirschberg',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ENDPOINT
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@run_safely()
def endpoint_hirschberg(command: CommandHirschberg) -> Result[CallResult, CallError]:
if command.once:
result = simple_algorithm(
X = command.word1,
Y = command.word2,
verbose = config.OPTIONS.hirschberg.verbose,
);
else:
result = hirschberg_algorithm(
X = command.word1,
Y = command.word2,
verbose = config.OPTIONS.hirschberg.verbose,
show = config.OPTIONS.hirschberg.show,
);
return Ok(CallResult(action_taken=True, message=result));

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.code import *;
from models.generated.commands import *;
from src.core.calls import *;
from src.setup import config;
from src.models.graphs import *;
from src.algorithms.tarjan import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'endpoint_tarjan',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ENDPOINT
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@run_safely()
def endpoint_tarjan(command: CommandTarjan) -> Result[CallResult, CallError]:
result = tarjan_algorithm(
G = Graph(
nodes=command.nodes,
edges=list(map(tuple, command.edges)),
),
verbose = config.OPTIONS.tarjan.verbose
);
return Ok(CallResult(action_taken=True, message=result));

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.code import *;
from src.thirdparty.maths import *;
from models.generated.commands import *;
from src.core.calls import *;
from src.setup import config;
from src.algorithms.tsp import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'endpoint_tsp',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ENDPOINT
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@run_safely()
def endpoint_tsp(command: CommandTsp) -> Result[CallResult, CallError]:
result = tsp_algorithm(
dist = np.asarray(command.dist, dtype=float),
optimise = min if command.optimise == EnumTSPOptimise.min else max,
verbose = config.OPTIONS.tsp.verbose,
);
return Ok(CallResult(action_taken=True, message=result));

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.models.config.app import *;
from src.models.config.commands import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'log_level',
'command_from_json',
'interpret_command',
];

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from models.generated.config import AppOptions;
from models.generated.config import EnumLogLevel;
from src.core.log import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'log_level',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHODS log level
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def log_level(options: AppOptions) -> LOG_LEVELS:
match options.log_level:
case EnumLogLevel.debug:
return LOG_LEVELS.DEBUG;
case EnumLogLevel.info:
return LOG_LEVELS.INFO;
case _:
return LOG_LEVELS.INFO;

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.config import *;
from models.generated.commands import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'command_from_json',
'interpret_command',
];
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# METHODS Convert to appropriate command type
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def command_from_json(command_json: str) -> Command:
try:
instructions = json.loads(command_json);
except:
raise Exception('Invalid json!');
try:
command = Command(**instructions);
except:
raise Exception('Invalid instruction format - consult schema!');
command = interpret_command(command);
return command;
def interpret_command(command: Command) -> Command:
match command.name:
case EnumAlgorithmNames.tarjan:
return CommandTarjan(**command.dict());
case EnumAlgorithmNames.tsp:
return CommandTsp(**command.dict());
case EnumAlgorithmNames.hirschberg:
return CommandHirschberg(**command.dict());
raise command;

View File

@ -6,7 +6,7 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.types import *;
from src.setup.config import *;
from src.setup import config;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
@ -23,6 +23,6 @@ __all__ = [
class Directions(Enum):
UNSET = -1;
# Prioritäten hier setzen
DIAGONAL = OPTIONS.hirschberg.move_priorities.diagonal;
HORIZONTAL = OPTIONS.hirschberg.move_priorities.horizontal;
VERTICAL = OPTIONS.hirschberg.move_priorities.vertical;
DIAGONAL = config.OPTIONS.hirschberg.move_priorities.diagonal;
HORIZONTAL = config.OPTIONS.hirschberg.move_priorities.horizontal;
VERTICAL = config.OPTIONS.hirschberg.move_priorities.vertical;

View File

@ -6,7 +6,7 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.types import *;
from src.setup.config import *;
from src.setup import config;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
@ -22,7 +22,7 @@ __all__ = [
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def gap_penalty(x: str):
return OPTIONS.hirschberg.penality_gap;
return config.OPTIONS.hirschberg.penality_gap;
def missmatch_penalty(x: str, y: str):
return 0 if x == y else OPTIONS.hirschberg.penality_mismatch;
return 0 if x == y else config.OPTIONS.hirschberg.penality_mismatch;

View File

@ -12,6 +12,8 @@ from src.thirdparty.types import *;
from models.generated.config import *;
from models.generated.commands import *;
from src.core.log import *;
from src.models.config import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
@ -41,21 +43,16 @@ def load_assets_config(path: str) -> Config: # pragma: no cover
return Config(**assets);
def create_commands(path: str) -> List[Command]: # pragma: no cover
commands = [];
with open(path, 'r') as fp:
assets = yaml_load(fp, Loader=yaml_FullLoader);
for command in assets:
match Command(**command).name:
case EnumAlgorithmNames.tarjan:
commands.append(CommandTarjan(**command));
case EnumAlgorithmNames.tsp:
commands.append(CommandTsp(**command));
case EnumAlgorithmNames.hirschberg:
commands.append(CommandHirschberg(**command));
return commands;
return [
interpret_command(Command(**instruction))
for instruction in assets or []
];
# use lazy loaing to ensure that values only loaded (once) when used
CONFIG: Config = lazy(load_assets_config, path=PATH_ASSETS_CONFIG);
INFO: Info = lazy(lambda x: x.info, CONFIG);
OPTIONS: AppOptions = lazy(lambda x: x.options, CONFIG);
COMMANDS: List[Command] = lazy(create_commands, path=PATH_ASSETS_COMMANDS);
CONFIG: Config = lazy(load_assets_config, path=PATH_ASSETS_CONFIG);
INFO: Info = lazy(lambda x: x.info, CONFIG);
OPTIONS: AppOptions = lazy(lambda x: x.options, CONFIG);
LOG_LEVEL: LOG_LEVELS = lazy(log_level, OPTIONS);
COMMANDS: List[Command] = lazy(create_commands, path=PATH_ASSETS_COMMANDS);

18
code/python/src/thirdparty/log.py vendored Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import logging;
from logging import LogRecord;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'logging',
'LogRecord',
];

View File

@ -5,6 +5,9 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from datetime import datetime;
from datetime import timedelta;
import lorem;
import re;
from textwrap import dedent;
@ -13,6 +16,9 @@ from textwrap import dedent;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'lorem',
'datetime',
'timedelta',
're',
'dedent',
];

41
code/python/src/thirdparty/run.py vendored Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import aiohttp;
from asyncio import Future;
from asyncio import gather as asyncio_gather;
from asyncio import get_event_loop as asyncio_get_event_loop;
from asyncio import new_event_loop as asyncio_new_event_loop;
from asyncio import set_event_loop as asyncio_set_event_loop;
from asyncio import ensure_future as asyncio_ensure_future;
from asyncio import sleep as asyncio_sleep;
from asyncio import AbstractEventLoop;
from codetiming import Timer;
from signal import Signals;
from signal import SIGINT;
from signal import SIGTERM;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'aiohttp',
'Future',
'asyncio_gather',
'asyncio_ensure_future',
'asyncio_get_event_loop',
'asyncio_new_event_loop',
'asyncio_set_event_loop',
'asyncio_sleep',
'AbstractEventLoop',
'Timer',
'Signals',
'SIGINT',
'SIGTERM',
];

View File

@ -16,6 +16,7 @@ from typing import Generator;
from typing import Generic;
from typing import List;
from typing import Optional;
from typing import ParamSpec;
from typing import Tuple;
from typing import Type;
from typing import TypeVar;
@ -38,6 +39,7 @@ __all__ = [
'Generic',
'List',
'Optional',
'ParamSpec',
'Tuple',
'Type',
'TypeVar',

View File

@ -5,21 +5,42 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from unittest import TestCase;
from pytest import fixture;
from src.thirdparty.run import *;
from src.thirdparty.types import *;
from tests.thirdparty.unit import *;
from src.core.log import log_dev;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# CONSTANTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
LOOP: Optional[AbstractEventLoop] = None;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# FIXTURES
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@fixture(scope='module')
def test():
@fixture(scope='module', autouse=True)
def test() -> TestCase:
return TestCase();
@fixture(scope='module')
def debug():
def log(*lines: str):
with open('logs/debug.log', 'a') as fp:
for line in lines:
print(line, end='\n', file=fp);
return log;
@fixture(scope='module', autouse=True)
def debug() -> Callable[..., None]:
'''
Fixture for development purposes only.
Logs to file 'logs/debug.log'.
'''
return log_dev;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# FIXTURES for async
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# NOTE: !!! Must be called 'event_loop' !!!
@fixture(scope='session', autouse=True)
def event_loop() -> AbstractEventLoop:
global LOOP;
if LOOP is None or LOOP.is_closed():
LOOP = asyncio_new_event_loop();
asyncio_set_event_loop(loop=LOOP);
return LOOP;

View File

@ -5,8 +5,7 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from pytest import mark;
from pytest import fixture;
from tests.thirdparty.unit import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# CONSTANTS

View File

@ -5,17 +5,230 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from pytest import mark;
from pytest import fixture;
from src.thirdparty.log import *;
from src.thirdparty.misc import *;
from tests.thirdparty.unit import *;
from src.core.log import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# FIXTURES
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
@fixture(scope='module')
def message_1():
return lorem.sentence();
@fixture(scope='module')
def message_2():
return lorem.sentence();
@fixture(scope='module')
def message_3():
return lorem.sentence();
@fixture(scope='module')
def message_4():
return lorem.sentence();
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test logging
# Test log debug
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_debug_at_debug_level(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
):
caplog.records.clear();
caplog.set_level(logging.DEBUG);
test.assertEqual(len(caplog.records), 0);
log_debug(message_1);
log_debug(message_2);
log_debug(message_3);
log_debug(message_4);
# check that none of the 4 debug messages were suppressed:
test.assertEqual(len(caplog.records), 4);
test.assertEqual(caplog.records[0].levelname, 'DEBUG');
test.assertEqual(caplog.records[0].message, message_1);
test.assertEqual(caplog.records[1].levelname, 'DEBUG');
test.assertEqual(caplog.records[1].message, message_2);
test.assertEqual(caplog.records[2].levelname, 'DEBUG');
test.assertEqual(caplog.records[2].message, message_3);
test.assertEqual(caplog.records[3].levelname, 'DEBUG');
test.assertEqual(caplog.records[3].message, message_4);
return;
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_debug_at_info_level(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
):
caplog.records.clear();
caplog.set_level(logging.INFO);
test.assertEqual(len(caplog.records), 0);
log_debug(message_1);
log_info(message_2);
log_error(message_3);
log_debug(message_4);
# check that debug messages suppressed:
test.assertNotEqual(len(caplog.records), 4);
test.assertEqual(len(caplog.records), 2);
test.assertEqual(caplog.records[0].levelname, 'INFO');
test.assertEqual(caplog.records[1].levelname, 'ERROR');
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test log info
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@mark.parametrize(('level',), [(logging.DEBUG,), (logging.INFO,)])
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_info(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
level: int,
):
caplog.records.clear();
caplog.set_level(level);
test.assertEqual(len(caplog.records), 0);
log_info(message_1);
log_info(message_2);
log_info(message_3);
log_info(message_4);
# check that 4 info messages printed:
test.assertEqual(len(caplog.records), 4);
test.assertEqual(caplog.records[0].levelname, 'INFO');
test.assertEqual(caplog.records[0].message, message_1);
test.assertEqual(caplog.records[1].levelname, 'INFO');
test.assertEqual(caplog.records[1].message, message_2);
test.assertEqual(caplog.records[2].levelname, 'INFO');
test.assertEqual(caplog.records[2].message, message_3);
test.assertEqual(caplog.records[3].levelname, 'INFO');
test.assertEqual(caplog.records[3].message, message_4);
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test log warn
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@mark.parametrize(('level',), [(logging.DEBUG,), (logging.INFO,)])
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_warn(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
level: int,
):
caplog.records.clear();
caplog.set_level(level);
test.assertEqual(len(caplog.records), 0);
log_warn(message_1);
log_warn(message_2);
log_warn(message_3);
log_warn(message_4);
# check that 4 warning messages printed:
test.assertEqual(len(caplog.records), 4);
test.assertEqual(caplog.records[0].levelname, 'WARNING');
test.assertEqual(caplog.records[0].message, message_1);
test.assertEqual(caplog.records[1].levelname, 'WARNING');
test.assertEqual(caplog.records[1].message, message_2);
test.assertEqual(caplog.records[2].levelname, 'WARNING');
test.assertEqual(caplog.records[2].message, message_3);
test.assertEqual(caplog.records[3].levelname, 'WARNING');
test.assertEqual(caplog.records[3].message, message_4);
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test log error
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@mark.parametrize(('level',), [(logging.DEBUG,), (logging.INFO,)])
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_error(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
level: int,
):
caplog.records.clear();
caplog.set_level(level);
test.assertEqual(len(caplog.records), 0);
# assert that no sys-exit raised:
with does_not_raise():
log_error(message_1);
with does_not_raise():
log_error(message_2);
with does_not_raise():
log_error(message_3);
with does_not_raise():
log_error(message_4);
# check that 4 error messages printed:
test.assertEqual(len(caplog.records), 4);
test.assertEqual(caplog.records[0].levelname, 'ERROR');
test.assertEqual(caplog.records[0].message, message_1);
test.assertEqual(caplog.records[1].levelname, 'ERROR');
test.assertEqual(caplog.records[1].message, message_2);
test.assertEqual(caplog.records[2].levelname, 'ERROR');
test.assertEqual(caplog.records[2].message, message_3);
test.assertEqual(caplog.records[3].levelname, 'ERROR');
test.assertEqual(caplog.records[3].message, message_4);
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test log fatal
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@mark.parametrize(('level',), [(logging.DEBUG,), (logging.INFO,)])
@mark.usefixtures('message_1', 'message_2', 'message_3', 'message_4')
def test_log_fatal(
test: TestCase,
caplog: LogCaptureFixture,
message_1: str,
message_2: str,
message_3: str,
message_4: str,
level: int,
):
caplog.records.clear();
caplog.set_level(level);
test.assertEqual(len(caplog.records), 0);
# intercept sys-exist 1:
with assert_raises(SystemExit) as caught:
log_fatal(message_1);
test.assertEqual(caught.value.code, 1);
with assert_raises(SystemExit):
log_fatal(message_2);
with assert_raises(SystemExit):
log_fatal(message_3);
with assert_raises(SystemExit):
log_fatal(message_4);
# check that 4 critical messages printed:
test.assertEqual(len(caplog.records), 4);
test.assertEqual(caplog.records[0].levelname, 'CRITICAL');
test.assertEqual(caplog.records[0].message, message_1);
test.assertEqual(caplog.records[1].levelname, 'CRITICAL');
test.assertEqual(caplog.records[1].message, message_2);
test.assertEqual(caplog.records[2].levelname, 'CRITICAL');
test.assertEqual(caplog.records[2].message, message_3);
test.assertEqual(caplog.records[3].levelname, 'CRITICAL');
test.assertEqual(caplog.records[3].message, message_4);
return;

View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from src.thirdparty.run import *;
from src.thirdparty.code import *;
from src.thirdparty.misc import *;
from src.thirdparty.types import *;
from tests.thirdparty.unit import *;
from src.core.calls import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# FIXTURES
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@fixture(scope='function')
def func_success_1() -> Callable[..., Result[int, list]]:
return MagicMock(spec=Callable[..., Result[int, list]], return_value=Ok(41));
@fixture(scope='function')
def func_success_2() -> Callable[..., Result[int, list]]:
return MagicMock(spec=Callable[..., Result[int, list]], return_value=Ok(42));
@fixture(scope='function')
def func_error_1() -> Callable[..., Result[int, list]]:
return MagicMock(spec=Callable[..., Result[int, list]], return_value=Err(['RESULT ERROR 1']));
@fixture(scope='function')
def func_error_2() -> Callable[..., Result[int, list]]:
return MagicMock(spec=Callable[..., Result[int, list]], return_value=Err(['RESULT ERROR 2']));
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# MOCKS - for inspection
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def func_do_something1():
pass;
def func_do_something2():
pass;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test CallResult class
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def test_callResult(test: TestCase):
with does_not_raise():
x = CallResult();
x = CallResult(message='hello');
test.assertEqual(x.action_taken, False);
test.assertIsNotNone(x.message);
x = CallResult(action_taken=True);
test.assertEqual(x.action_taken, True);
test.assertIsNone(x.message);
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test CallError class
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def test_callerror(test: TestCase):
with does_not_raise():
e = CallError(tag='some-method');
e = CallError(tag='something');
test.assertEqual(e.errors, []);
e = CallError(tag='something', err=[]);
test.assertEqual(e.errors, []);
e = CallError(tag='something', err=Nothing());
test.assertEqual(e.errors, []);
e = CallError(tag='something', err='oh no!');
test.assertEqual(e.errors, ['oh no!']);
e = CallError(tag='something', err=Exception('oh no!'));
test.assertEqual(e.errors, ['oh no!']);
e = CallError(tag='something', err=['oh no!']);
test.assertEqual(e.errors, ['oh no!']);
e = CallError(tag='something', err=Exception('oh no!'));
test.assertEqual(len(e), 1);
e.append(Nothing());
test.assertEqual(len(e), 1);
e.append(Some('another error'));
test.assertEqual(len(e), 2);
test.assertEqual(e.errors, ['oh no!', 'another error']);
test.assertRegexpMatches(str(e), r"^CallError\(\s*tag\s*=\s*'something',\s*errors\s*=\s*\[.*\]\)$");
e2 = CallError(tag='something-else');
e2.append('yet another fail');
e.extend(e2);
test.assertEqual(len(e), 3);
test.assertEqual(e.errors, ['oh no!', 'another error', 'yet another fail']);
test.assertRegexpMatches(str(e), r"^CallError\(\s*tag\s*=\s*'something',\s*errors\s*=\s*\[.*\]\)$");
return;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test run_safely
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def test_run_safely(test: TestCase):
@run_safely(tag='recognise int', error_message='unrecognise string')
def action1(x: str) -> Result[int, CallError]:
return Ok(int(x));
assert action1('5') == Ok(5);
result = action1('not a number');
assert isinstance(result, Err);
err = result.unwrap_err();
assert isinstance(err, CallError);
assert err.tag == 'recognise int';
assert err.errors == ['unrecognise string'];
def test_run_safely_no_error_message(test: TestCase):
@run_safely('recognise int')
def action2(x: str) -> Result[int, CallError]:
return Ok(int(x));
assert action2('5') == Ok(5);
result = action2('not a number');
assert isinstance(result, Err);
err = result.unwrap_err();
assert isinstance(err, CallError);
assert err.tag == 'recognise int';
assert len(err.errors) == 1;

View File

@ -5,9 +5,7 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from unittest import TestCase;
from pytest import mark;
from pytest import fixture;
from tests.thirdparty.unit import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# CONSTANTS

View File

@ -5,11 +5,7 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from contextlib import nullcontext as does_not_raise
from collections import Counter;
from pytest import mark;
from pytest import fixture;
from unittest import TestCase;
from tests.thirdparty.unit import *;
from src.models.graphs import *;

View File

@ -5,14 +5,9 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import pytest;
from pytest import mark;
from pytest import fixture;
from pytest import lazy_fixture;
from unittest import TestCase;
from unittest.mock import patch;
from tests.thirdparty.unit import *;
from src.thirdparty.types import *;
from src.models.graphs import *;
from src.algorithms.tarjan import *;
@ -77,7 +72,7 @@ def test_tarjan(test, G, expected):
@mark.parametrize(('G', 'expected'), [ (lazy_fixture('graph1'), [[1], [3], [2,4]])])
@mark.usefixtures('test')
def test_failable_tarjan(test, G, expected):
with pytest.raises(AssertionError):
with assert_raises(AssertionError):
test_tarjan(test, G, expected);
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -5,8 +5,7 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from pytest import mark;
from pytest import fixture;
from tests.thirdparty.unit import *;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# FIXTURES

View File

@ -5,10 +5,7 @@
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
from contextlib import nullcontext as does_not_raise
import pytest;
from pytest import mark;
from unittest import TestCase;
from tests.thirdparty.unit import *;
from src.models.stacks import Stack;
@ -56,7 +53,7 @@ def test_stack_error_po(test: TestCase):
stack.push('hallo');
stack.push('welt');
stack.push('!');
with pytest.raises(Exception):
with assert_raises(Exception):
stack.pop();
stack.pop();
stack.pop();

View File

39
code/python/tests/thirdparty/unit.py vendored Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# IMPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# for unit tests:
import anyio;
from contextlib import nullcontext as does_not_raise;
from pytest import fixture;
from pytest_lazyfixture import lazy_fixture;
from pytest import LogCaptureFixture;
from pytest import mark;
from pytest import raises as assert_raises;
from testfixtures import LogCapture;
from unittest import TestCase;
from unittest.mock import patch;
from unittest.mock import MagicMock;
from unittest.mock import PropertyMock;
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# EXPORTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__all__ = [
'anyio',
'does_not_raise',
'fixture',
'lazy_fixture',
'LogCaptureFixture',
'mark',
'assert_raises',
'LogCapture',
'TestCase',
'patch',
'MagicMock',
'PropertyMock',
];