Skip to content

Commit

Permalink
ESBMC-AI Config Tool initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Yiannis128 committed Feb 8, 2024
1 parent 1710e10 commit fcf5168
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 0 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ libclang = "*"
clang = "*"
langchain = "*"
langchain-openai = "*"
urwid = "*"

[dev-packages]
pylint = "*"
Expand Down
4 changes: 4 additions & 0 deletions esbmc-ai-config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh

python -m esbmc_ai_config $@

1 change: 1 addition & 0 deletions esbmc_ai_config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Author: Yiannis Charalambous
29 changes: 29 additions & 0 deletions esbmc_ai_config/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Author: Yiannis Charalambous

from urwid import (
MainLoop,
)

from esbmc_ai_config.context_manager import ContextManager
from esbmc_ai_config.contexts.main_menu import MainMenu


palette = [
("banner", "", "", "", "#ffa", "#60d"),
("streak", "", "", "", "g50", "#60a"),
("inside", "", "", "", "g38", "#808"),
("outside", "", "", "", "g27", "#a06"),
("bg", "", "", "", "g7", "#d06"),
]


def main() -> None:
top_ctx = MainMenu()

app: MainLoop = MainLoop(top_ctx.widget, palette=[("reversed", "standout", "")])
ContextManager.init(app, top_ctx)
app.run()


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions esbmc_ai_config/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Author: Yiannis Charalambous

import urwid


class Context(object):
def __init__(self, widget: urwid.Widget) -> None:
super().__init__()

self.widget: urwid.Widget = widget
33 changes: 33 additions & 0 deletions esbmc_ai_config/context_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Author: Yiannis Charalambous

from urwid import MainLoop

from esbmc_ai_config.context import Context


class ContextManager(object):
app: MainLoop
view_stack: list[Context] = []

def __init__(self) -> None:
raise Exception("Static class cannot be instantiated...")

@classmethod
def init(cls, app: MainLoop, ctx: Context) -> None:
cls.app = app
cls.view_stack.append(ctx)
cls.app.widget = ctx.widget

@classmethod
def push_context(cls, ctx: Context) -> None:
cls.view_stack.append(ctx)
cls.app.widget = ctx.widget

@classmethod
def pop_context(cls) -> Context:
cls.app.widget = cls.view_stack[-2].widget
return cls.view_stack.pop()

@classmethod
def get_context(cls) -> Context:
return cls.view_stack[-1]
56 changes: 56 additions & 0 deletions esbmc_ai_config/contexts/base_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Author: Yiannis Charalambous

import urwid
from urwid import Button, Text, connect_signal, AttrMap

from esbmc_ai_config.context import Context
from esbmc_ai_config.context_manager import ContextManager


class BaseMenu(Context):
def __init__(
self,
title: str,
choices: list[str | urwid.Widget],
back_choice: bool = True,
) -> None:
"""Creates a Menu context and displays it on the screen."""

_choices: list[str | urwid.Widget] = choices.copy()
if back_choice:
_choices.append(urwid.Divider())
_choices.append("Back")

menu: urwid.ListBox = self.create_menu(title=title, choices=_choices)

menu_box: urwid.WidgetDecoration = urwid.Padding(
menu,
left=2,
right=2,
)

super().__init__(menu_box)

def item_chosen(self, button, choice) -> None:
if choice == "Back":
ContextManager.pop_context()

def create_menu(self, title, choices: list[str | urwid.Widget]) -> urwid.ListBox:
body: list[urwid.Widget] = [Text(title), urwid.Divider()]
for c in choices:
if isinstance(c, str):
button = Button(c)
connect_signal(
obj=button,
name="click",
callback=self.item_chosen,
user_arg=c,
)
# Reverse attributes when focused
body.append(AttrMap(button, None, focus_map="reversed"))
elif isinstance(c, urwid.Widget):
body.append(c)
else:
raise ValueError(f"create_menu: {c} is not a str or urwid.Widget")

return urwid.ListBox(urwid.SimpleFocusListWalker(body))
26 changes: 26 additions & 0 deletions esbmc_ai_config/contexts/env_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Author: Yiannis Charalambous

import urwid

from esbmc_ai_config.models.env_config_loader import EnvConfigLoader
from esbmc_ai_config.contexts.base_menu import BaseMenu


class EnvMenu(BaseMenu):
def __init__(self) -> None:
self.config: EnvConfigLoader = EnvConfigLoader(create_missing_fields=True)
choices: list = [field.name for field in self.config.fields]
super().__init__(title="Setup Environment", choices=choices)

top: urwid.Widget = urwid.Overlay(
urwid.LineBox(self.widget),
urwid.SolidFill("\N{MEDIUM SHADE}"),
align="center",
valign="middle",
width=("relative", 60),
height=("relative", 15),
min_width=20,
min_height=10,
)

self.widget = top
72 changes: 72 additions & 0 deletions esbmc_ai_config/contexts/main_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Author: Yiannis Charalambous


from typing_extensions import override
import urwid
from urwid import Text, Widget

from esbmc_ai_config.contexts.base_menu import BaseMenu
from esbmc_ai_config.context_manager import ContextManager
from esbmc_ai_config.contexts.env_menu import EnvMenu


class MainMenu(BaseMenu):
def __init__(self) -> None:
super().__init__(
title="Main Menu",
choices=[
"Setup Environment",
"ESBMC Settings",
"AI Configuration",
"Save",
"Save As",
urwid.Divider(),
"Exit",
],
back_choice=False,
)

# Add additional content to widget.
text = urwid.ListBox(
[
Text(
"ESBMC-AI CLI Configuration Tool\n"
"Made by Yiannis Charalambous\n\n"
"This tool configures ESBMC-AI through CLI menus using the ncurses library."
"Not all options may be available, so it is worth checking the documentation:\n\n"
"https://github.com/Yiannis128/esbmc-ai/wiki/Configuration\n\n"
"Env File: ~/.config/esbmc-ai.env\n"
"Config File: ~/.config/esbmc-ai.json\n\n"
"Control Keys:\n"
"- Up/Down: Navigation\n"
"- Enter: Select Option\n"
)
]
)

top: Widget = urwid.Overlay(
urwid.LineBox(self.widget),
text,
align="center",
valign="middle",
width=("relative", 60),
height=("relative", 15),
min_width=20,
min_height=12,
)

self.widget = top

@override
def item_chosen(self, button, choice) -> None:
super().item_chosen(button, choice)

match choice:
case "Exit":
self.exit_program(button)
case "Setup Environment":
ContextManager.push_context(EnvMenu())
return

def exit_program(self, _):
raise urwid.ExitMainLoop()
30 changes: 30 additions & 0 deletions esbmc_ai_config/models/config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Author: Yiannis Charalambous


from abc import abstractmethod
import os


class ConfigLoader(object):
"""Class responsible for loading the ESBMC-AI config. Is generic so contains
methods for EnvConfigLoader and JsonConfigLoader"""

def __init__(self, file_path: str, create_missing_fields: bool = False) -> None:
self.file_path: str = os.path.expanduser(os.path.expandvars(file_path))
if os.path.exists(self.file_path) and os.path.isfile(self.file_path):
with open(self.file_path, "r") as file:
self.content: str = file.read()
else:
# Create default file.
self._create_default_file()

# Read fields.
self._read_fields(create_missing_fields=create_missing_fields)

@abstractmethod
def _create_default_file(self) -> None:
raise NotImplementedError()

@abstractmethod
def _read_fields(self, create_missing_fields: bool = False) -> None:
raise NotImplementedError()
114 changes: 114 additions & 0 deletions esbmc_ai_config/models/env_config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Author: Yiannis Charalambous

import os
import sys
from dotenv import load_dotenv, find_dotenv
from pathlib import Path
from dataclasses import dataclass
from typing_extensions import override, Optional
from platform import system as system_name
from esbmc_ai_config.models.config_loader import ConfigLoader

__ALLOWED_ENV_TYPES = bool | float | int | str


@dataclass
class EnvConfigField:
name: str
default_value: "__ALLOWED_ENV_TYPES" = ""
is_optional: bool = False
"""Will not load the config if the value is not specified by the user. If
true will assign default_value."""


class EnvConfigLoader(ConfigLoader):
def __init__(
self,
file_path: str = "~/.config/esbmc-ai.env",
fields: list[EnvConfigField] = [
EnvConfigField("ESBMC_AI_CFG_PATH", "", is_optional=False),
EnvConfigField("OPENAI_API_KEY", "", is_optional=True),
EnvConfigField("HUGGINGFACE_API_KEY", "", is_optional=True),
],
create_missing_fields: bool = False,
) -> None:
assert file_path.endswith(".env"), f"{self.file_path} is not a valid env file."

self.fields: list[EnvConfigField] = fields

self.values: dict[str, "__ALLOWED_ENV_TYPES"] = {}

super().__init__(
file_path=file_path,
create_missing_fields=create_missing_fields,
)

@override
def _create_default_file(self) -> None:
with open(self.file_path, "w") as file:
file.write("Generated by ESBMC-AI config tool.")

@override
def _read_fields(self, create_missing_fields: bool = False) -> None:
"""Environment variables are loaded in the following order:
1. Environment variables already loaded. Any variable not present will be looked for in
.env files in the following locations.
2. .env file in the current directory, moving upwards in the directory tree.
3. esbmc-ai.env file in the current directory, moving upwards in the directory tree.
4. esbmc-ai.env file in $HOME/.config/ for Linux/macOS and %userprofile% for Windows.
Note: ESBMC_AI_CFG_PATH undergoes tilde user expansion and also environment
variable expansion.
"""

values: dict[str, "__ALLOWED_ENV_TYPES"] = {}

def get_env_vars() -> None:
"""Gets all the system environment variables that are currently loaded. Will not
load values that are not following the EnvConfigField specification."""
for field in self.fields:
value: Optional[str] = os.getenv(field.name)
# Check if value is not None
if value != None:
values[field.name] = value

# Read from system environment.
get_env_vars()

# Search for .env or esbmc-ai.env in cwd and go up.
# Find .env in file path and load it else find esbmc-ai.env in current working directory and load it.
dotenv_file_path: str = find_dotenv(usecwd=True)
if dotenv_file_path != "":
load_dotenv(dotenv_path=dotenv_file_path, override=False, verbose=True)
else:
dotenv_file_path: str = find_dotenv(filename="esbmc-ai.env", usecwd=True)
if dotenv_file_path != "":
load_dotenv(dotenv_path=dotenv_file_path, override=False, verbose=True)

get_env_vars()

# Look for .env in home folder.
home_path: Path = Path.home()
match system_name():
case "Linux" | "Darwin":
home_path /= ".config/esbmc-ai.env"
case "Windows":
home_path /= "esbmc-ai.env"
case _:
raise ValueError(f"Unknown OS type: {system_name()}")

load_dotenv(home_path, override=False, verbose=True)
get_env_vars()

# Check if all the values are set, else create them with defaults.
for field in self.fields:
if field.name not in values:
if create_missing_fields or field.is_optional:
# Create new field with default value.
values[field.name] = field.default_value
else:
print(f"Error: No ${field.name} in environment.")
sys.exit(1)

self.values: dict[str, "__ALLOWED_ENV_TYPES"] = values
1 change: 1 addition & 0 deletions esbmc_ai_config/models/json_config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Author: Yiannis Charalambous

0 comments on commit fcf5168

Please sign in to comment.