Mod: global env
This commit is contained in:
L-Nafaryus 2021-12-31 01:18:39 +05:00
parent 59e44dd08d
commit 29a9a7fa7f
No known key found for this signature in database
GPG Key ID: C76D8DCD2727DBB7
14 changed files with 556 additions and 405 deletions

View File

@ -6,38 +6,37 @@
*anisotropy* is a ``Python`` package that is the result of science-research work
on the anisotropy of permeability in the periodic porous media.
A project uses own wrappers around external applications
for constructing a shapes and meshes (``Salome``) and computing a flow (``OpenFOAM``).
"""
__license__ = "GPL3"
__version__ = "1.2.0"
__author__ = __maintainer__ = "George Kusayko"
__email__ = "gkusayko@gmail.com"
__repository__ = "https://github.com/L-Nafaryus/anisotropy"
###
# Environment
##
import os
from os import path
env = dict(
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
)
env.update(
CLI = os.path.join(env["ROOT"], "anisotropy/core/cli.py")
)
env.update(
BUILD = os.path.join(env["ROOT"], "build"),
LOG = os.path.join(env["ROOT"], "logs"),
CONFIG = os.path.join(env["ROOT"], "anisotropy/config/default.toml"),
DOCS = os.path.join(env["ROOT"], "docs")
)
env.update(
logger_name = "anisotropy",
db_name = "anisotropy",
db_path = env["BUILD"],
salome_timeout = 15 * 60,
openfoam_template = os.path.join(env["ROOT"], "anisotropy/openfoam/template")
)
PROJECT = path.abspath(path.dirname(__file__))
TMP = "/tmp/anisotropy"
del os
env = {
"PROJECT": path.abspath(path.dirname(__file__)),
"TMP": TMP,
"CWD": TMP,
"BUILD_DIR": "build",
"CONF_FILE": "anisotropy.toml",
"LOG_FILE": "anisotropy.log",
"DB_FILE": "anisotropy.db"
}
def loadEnv():
prefix = "ANISOTROPY_"
for k, v in env.items():
os.environ[f"{ prefix }{ k }"] = v
del os, path, PROJECT, TMP

View File

@ -77,6 +77,7 @@ def verboseLevel(level: int):
@click.group()
@click.version_option()
def anisotropy():
pass
@ -119,19 +120,19 @@ def init(path, verbose):
@click.option(
"-N", "--nprocs", "nprocs",
type = click.INT,
default = 1,
#default = 1,
help = "Count of parallel processes"
)
@click.option(
"-s", "--stage", "stage",
type = click.Choice(["all", "shape", "mesh", "flow", "postProcess"]),
default = "all",
#default = "all",
help = "Current computation stage"
)
@click.option(
"-f", "--force", "overwrite",
is_flag = True,
default = False,
#default = False,
help = "Overwrite existing entries"
)
@click.option(
@ -149,12 +150,24 @@ def init(path, verbose):
@click.option(
"--exec-id", "execution"
)
def compute(path, configFile, nprocs, stage, overwrite, params, verbose, execution):
@click.option(
"--pid", "pid",
help = "Specify pid file path"
)
@click.option(
"--logfile", "logfile",
help = "Specify log file path"
)
def compute(path, configFile, nprocs, stage, overwrite, params, verbose, execution, pid, logfile):
from anisotropy.core.runner import UltimateRunner
from anisotropy.core.config import DefaultConfig
from anisotropy.core.utils import setupLogger
setupLogger(verboseLevel(verbose))
if path:
os.makedirs(os.path.abspath(path), exist_ok = True)
os.chdir(os.path.abspath(path))
setupLogger(verboseLevel(verbose), logfile)
logger = logging.getLogger(__name__)
config = DefaultConfig()
@ -162,34 +175,69 @@ def compute(path, configFile, nprocs, stage, overwrite, params, verbose, executi
if configFile:
filepath = os.path.abspath(configFile)
logger.info(f"Loading file from { filepath }")
config.load(configFile)
try:
config.load(configFile)
except FileNotFoundError:
config.dump(configFile)
else:
logger.info("Using default configuration")
config.update(
nprocs = nprocs,
stage = stage,
overwrite = overwrite
)
args = {
"nprocs": nprocs,
"stage": stage,
"overwrite": overwrite
}
for k, v in args.items():
if v is not None:
config.update(**{ k: v })
if pid:
pidpath = os.path.abspath(pid)
with open(pidpath, "w") as io:
io.write(str(os.getpid()))
runner = UltimateRunner(config = config, exec_id = execution)
runner.fill()
runner.start()
os.remove(pidpath)
@anisotropy.command()
@click.option(
"-P", "--path", "path",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
@click.option(
"-v", "--verbose", "verbose",
count = True,
help = "Increase verbose level"
)
def gui(verbose):
import anisotropy
from anisotropy.core.utils import setupLogger
from anisotropy.gui import app
anisotropy.loadEnv()
os.makedirs(os.path.abspath(path), exist_ok = True)
os.chdir(os.path.abspath(path))
os.environ["ANISOTROPY_CWD"] = path
setupLogger(verboseLevel(verbose))
logger = logging.getLogger(__name__)
app.run_server(debug = True)
##############
"""
def version():
msg = "Missed package anisotropy"
try:
from anisotropy.core.main import Anisotropy
msg = Anisotropy.version()
except ImportError:
pass
return msg
@click.group()
@click.version_option(version = "", message = version())
@ -197,339 +245,6 @@ def anisotropy():
pass
@anisotropy.command(
help = "Initialize new anisotropy project."
)
@click.option(
"-P", "--path", "path",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
def init(path):
from anisotropy import env
from anisotropy.core.main import Database
if not os.path.exists(path) or not os.path.isdir(path):
click.echo(f"Cannot find directory { path }")
return
wds = [ "build", "logs" ]
for wd in wds:
os.makedirs(os.path.join(path, wd), exist_ok = True)
shutil.copy(env["CONFIG"], os.path.join(path, "anisotropy.toml"), follow_symlinks = True)
db = Database(env["db_name"], path)
db.setup()
click.echo(f"Initialized anisotropy project in { path }")
@anisotropy.command(
help = "Load parameters from configuration file and update database."
)
@click.option(
"-f", "--force", "force",
is_flag = True,
default = False,
help = "Overwrite existing entries"
)
@click.option(
"-p", "--params", "params",
metavar = "key=value",
multiple = True,
cls = KeyValueOption,
help = "Specify control parameters to update (type, direction, theta)"
)
@click.option(
"-P", "--path", "path",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
def update(force, params, path):
from anisotropy import env
from anisotropy.core.main import Anisotropy, Database
env.update(
LOG = os.path.join(path, "logs"),
BUILD = os.path.join(path, "build"),
CONFIG = os.path.join(path, "anisotropy.toml"),
db_path = path
)
args = dict()
for param in params:
args.update(param)
model = Anisotropy()
database = Database(env["db_name"], env["db_path"])
click.echo("Configuring database ...")
database.setup()
if database.isempty() or update:
paramsAll = model.loadFromScratch(env["CONFIG"])
if args.get("type"):
paramsAll = [ entry for entry in paramsAll if args["type"] == entry["structure"]["type"] ]
if args.get("direction"):
paramsAll = [ entry for entry in paramsAll if args["direction"] == entry["structure"]["direction"] ]
if args.get("theta"):
paramsAll = [ entry for entry in paramsAll if args["theta"] == entry["structure"]["theta"] ]
from anisotropy.core.models import Structure, Mesh
#from numpy
for entry in paramsAll:
database.update(entry)
click.echo("{} entries was updated.".format(len(paramsAll)))
else:
click.echo("Database was not modified.")
@anisotropy.command(
# help = """#Compute cases by chain (mesh -> flow)
#
# Control parameters: type, direction, theta (each parameter affects on a queue)
"""
)
@click.option(
"-s", "--stage", "stage",
type = click.Choice(["all", "mesh", "flow", "postProcessing"]),
default = "all",
help = "Current computation stage"
)
@click.option(
"-N", "--nprocs", "nprocs",
type = click.INT,
default = 1,
help = "Count of parallel processes"
)
@click.option(
"-f", "--force", "force",
is_flag = True,
default = False,
help = "Overwrite existing entries"
)
@click.option(
"-p", "--params", "params",
metavar = "key=value",
multiple = True,
cls = KeyValueOption,
help = "Overwrite existing parameter (except control variables)"
)
@click.option(
"-P", "--path", "path",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
def compute(stage, nprocs, force, params, path):
from anisotropy import env
from anisotropy.core.main import Anisotropy, Database, logger
from anisotropy.core.utils import setupLogger, parallel
env.update(
LOG = os.path.join(path, "logs"),
BUILD = os.path.join(path, "build"),
CONFIG = os.path.join(path, "anisotropy.toml"),
db_path = path
)
setupLogger(logger, logging.INFO, env["LOG"])
args = dict()
for param in params:
args.update(param)
###
logger.info("Writing pid ...")
pidpath = os.path.join(path, "anisotropy.pid")
with open(pidpath, "w") as io:
io.write(str(os.getpid()))
###
# Preparations
##
database = Database(env["db_name"], env["db_path"])
logger.info("Loading database ...")
database.setup()
params = database.loadGeneral(
args.get("type"),
args.get("direction"),
args.get("theta")
)
queueargs = []
for p in params:
s = p["structure"]
queueargs.append((s["type"], s["direction"], s["theta"]))
###
# Wrap function
##
def computeCase(type, direction, theta):
case = Anisotropy()
case.db = database
case.load(type, direction, theta)
case.evalParams()
case.update()
logger.info(f"Case: type = { type }, direction = { direction }, theta = { theta }")
logger.info(f"Stage mode: { stage }")
if stage in ["mesh", "all"]:
case.load(type, direction, theta)
if not case.params["meshresult"]["meshStatus"] == "Done" or force:
logger.info("Current stage: mesh")
out, err, returncode = case.computeMesh(path)
if out: logger.info(out)
if err: logger.error(err)
if returncode:
logger.error("Mesh computation failed. Skipping flow computation ...")
return
else:
logger.info("Mesh exists. Skipping ...")
if stage in ["flow", "all"]:
case.load(type, direction, theta)
if not case.params["flowresult"]["flowStatus"] == "Done" or force:
logger.info("Current stage: flow")
out, err, returncode = case.computeFlow(path)
if out: logger.info(out)
if err: logger.error(err)
if returncode:
logger.error("Flow computation failed.")
return
else:
logger.info("Flow exists. Skipping ...")
if stage in ["postProcessing", "all"]:
case.load(type, direction, theta)
if case.params["meshresult"]["meshStatus"] == "Done":
logger.info("Current stage: mesh postProcessing")
case.porosity()
else:
logger.warning("Cannot compute mesh post processing values.")
if case.params["flowresult"]["flowStatus"] == "Done":
logger.info("Current stage: flow postProcessing")
case.flowRate()
else:
logger.warning("Cannot compute flow post processing values.")
###
# Run
##
if nprocs == 1:
for pos, qarg in enumerate(queueargs):
computeCase(*qarg)
else:
parallel(nprocs, queueargs, computeCase)
if os.path.exists(pidpath):
logger.info("Removing pid ...")
os.remove(pidpath)
logger.info("Computation done.")
@anisotropy.command(
help = "Kill process by pid file"
)
@click.option(
"-P", "--path", "path",
default = os.getcwd(),
help = "Specify directory to use (instead of cwd)"
)
@click.argument("pidfile")
def kill(path, pidfile):
from anisotropy.salomepl.utils import SalomeManager
try:
with open(os.path.join(path, pidfile), "r") as io:
pid = io.read()
os.kill(int(pid), 9)
except FileNotFoundError:
click.echo(f"Unknown file { pidfile }")
except ProcessLookupError:
click.echo(f"Cannot find process with pid { pid }")
# TODO: killall method kills all salome instances. Not a good way
SalomeManager().killall()
@anisotropy.command(
help = "! Internal command"
)
@click.argument("root")
@click.argument("type")
@click.argument("direction")
@click.argument("theta")
@click.argument("path")
def computemesh(root, type, direction, theta, path):
# ISSUE: can't hide command from help, 'hidden = True' doesn't work
# [Salome Environment]
###
# Args
##
direction = [ float(num) for num in direction[1:-1].split(" ") if num ]
theta = float(theta)
###
# Modules
##
import os, sys
pyversion = "{}.{}".format(3, 9) #(*sys.version_info[:2])
sys.path.extend([
root,
os.path.join(root, "env/lib/python{}/site-packages".format(pyversion)),
os.path.abspath(".local/lib/python{}/site-packages".format(pyversion))
])
from anisotropy import env
from anisotropy.core.main import Anisotropy, Database
import salome
###
model = Anisotropy()
model.db = Database(env["db_name"], path)
model.load(type, direction, theta)
salome.salome_init()
model.genmesh(path)
salome.salome_close()
@anisotropy.command()
@click.option(
"-p", "--params", "params",
@ -683,12 +398,4 @@ if __name__ == "__main__":
# click.echo("Interrupted!")
#finally:
# from anisotropy.salomepl.utils import SalomeManager
# click.echo("Exiting ...")
# if os.path.exists("anisotropy.pid"):
# os.remove("anisotropy.pid")
# SalomeManager().killall()
# sys.exit(0)

View File

@ -42,6 +42,7 @@ class Config(object):
self.content = toml.load(path)
self.options = deepcopy(self.content["options"])
self.content.pop("options")
def dump(self, filename: str):
path = os.path.abspath(filename)
@ -52,6 +53,8 @@ class Config(object):
os.makedirs(os.path.split(path)[0], exist_ok = True)
self.content.update(options = self.options)
with open(path, "w") as io:
toml.dump(self.content, io, encoder = toml.TomlNumpyEncoder())

View File

@ -13,16 +13,16 @@ from types import FunctionType
import os
class CustomFormatter(logging.Formatter):
def _getFormat(self, level: int):
def __init__(self, colors: bool = True):
self.colors = colors
def applyColors(self, level: int, info: str, msg: str):
grey = "\x1b[38;21m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
info = "[%(levelname)s %(processName)s %(asctime)s %(funcName)s]" # [ %(processName)s ]
msg = " %(message)s"
formats = {
logging.DEBUG: grey + info + reset + msg,
logging.INFO: grey + info + reset + msg,
@ -34,7 +34,15 @@ class CustomFormatter(logging.Formatter):
return formats.get(level)
def format(self, record):
log_fmt = self._getFormat(record.levelno)
info = "[%(levelname)s %(processName)s %(asctime)s %(funcName)s]" # [ %(processName)s ]
msg = " %(message)s"
if self.colors:
log_fmt = self.applyColors(record.levelno, info, msg)
else:
log_fmt = info + msg
time_fmt = "%d-%m-%y %H:%M:%S"
formatter = logging.Formatter(log_fmt, time_fmt)
@ -42,19 +50,14 @@ class CustomFormatter(logging.Formatter):
def setupLogger(level: int, filepath: str = None):
"""Applies settings to logger
"""Applies settings to root logger
:param logger:
Instance of :class:`logging.Logger`
:param level:
Logging level (logging.INFO, logging.WARNING, ..)
:param filepath:
Path to directory
"""
#logger.handlers = []
#logger.setLevel(level)
logging.addLevelName(logging.INFO, "II")
logging.addLevelName(logging.WARNING, "WW")
@ -64,20 +67,14 @@ def setupLogger(level: int, filepath: str = None):
streamhandler = logging.StreamHandler()
streamhandler.setLevel(level)
streamhandler.setFormatter(CustomFormatter())
#logger.addHandler(streamhandler)
logging.root.setLevel(level)
logging.root.addHandler(streamhandler)
logging.root.setLevel(level)
if filepath:
if not os.path.exists(filepath):
os.makedirs(filepath, exist_ok = True)
filehandler = logging.FileHandler(
os.path.join(filepath, "{}.log".format("anisotropy"))
)
filehandler = logging.FileHandler(filepath)
filehandler.setLevel(logging.INFO)
filehandler.setFormatter(CustomFormatter())
filehandler.setFormatter(CustomFormatter(colors = False))
logging.root.addHandler(filehandler)

View File

@ -0,0 +1,5 @@
from .main import app
if __name__ == "__main__":
app.run_server(debug = True)

23
anisotropy/gui/about.py Normal file
View File

@ -0,0 +1,23 @@
from dash import html
from dash import dcc
import dash_bootstrap_components as dbc
from .styles import *
import anisotropy
###
# Layout
##
layout = html.Div([
html.H1(anisotropy.__name__),
html.Hr(),
html.P([
"Author: {}".format(anisotropy.__author__),
html.Br(),
"License: {}".format(anisotropy.__license__),
html.Br(),
"Version: {}".format(anisotropy.__version__)
])
])

9
anisotropy/gui/app.py Normal file
View File

@ -0,0 +1,9 @@
import dash
import dash_bootstrap_components as dbc
app = dash.Dash(__name__, external_stylesheets = [ dbc.themes.LUX ])
app.title = "anisotropy"
app.config.update(
update_title = None
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

70
anisotropy/gui/main.py Normal file
View File

@ -0,0 +1,70 @@
from dash import html
from dash import dcc
from dash.dependencies import Input, Output, State
import dash_bootstrap_components as dbc
from . import (
runner,
settings,
about
)
from .app import app
from .styles import *
import anisotropy
###
# Layout
##
app.layout = html.Div([
# Location
dcc.Location(id = "url", refresh = False),
# Sidebar
html.Div([
# Sidebar
html.H2([html.Img(src = "/assets/simple.png", height = "150px")], style = logo),
html.Hr(style = { "color": "#ffffff" }),
dbc.Nav([
dbc.NavLink("Runner", href = "/", active = "exact", style = white),
dbc.NavLink("Settings", href = "/settings", active = "exact", style = white),
dbc.NavLink("About", href = "/about", active = "exact", style = white),
], vertical = True, pills = True),
# Misc
html.Hr(style = white),
dbc.Container([
dbc.Row([
dbc.Col("v1.2.0"),
dbc.Col(
html.A(
html.Img(src = "/assets/gh-light.png", height = "20px"),
href = anisotropy.__repository__
)
)
])
], style = misc)
], style = sidebar),
# Content
html.Div(id = "page-content", style = page),
], style = content)
###
# Callbacks
##
@app.callback(
Output("page-content", "children"),
[ Input("url", "pathname") ]
)
def displayPage(pathname):
if pathname == "/settings":
return settings.layout
elif pathname == "/about":
return about.layout
else:
return runner.layout

168
anisotropy/gui/runner.py Normal file
View File

@ -0,0 +1,168 @@
from dash.dash_table import DataTable
from dash import html
from dash import dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import os
from .app import app
from .styles import *
from .utils import getSize
###
# Layout
##
layout = html.Div([
# Messages and timer
dbc.Alert(
id = "status",
duration = 10000,
dismissable = True,
is_open = False,
style = message
),
dcc.Interval(id = "interval", interval = 1000, n_intervals = 0),
# Runner
html.H2("Runner"),
html.Hr(),
dbc.Button("Start", id = "start", color = "success", style = minWidth),
dbc.Button("Stop", id = "stop", color = "danger", disabled = True, style = minWidth),
# Monitor
html.H2("Monitor"),
html.Hr(),
html.P(id = "runner-status"),
DataTable(id = "monitor", columns = [], data = [], style_table = table),
# Log
html.H2("Log"),
html.Hr(),
dbc.Button("Delete", id = "delete", style = minWidth),
dcc.Textarea(id = "logger", disabled = True, style = bigText)
])
###
# Callbacks
##
@app.callback(
Output("start", "active"),
[ Input("start", "n_clicks") ],
prevent_initial_call = True
)
def runnerStart(clicks):
import subprocess
command = [
"anisotropy",
"compute",
"-v",
"--path", "/tmp/anisotropy",
"--conf", "anisotropy.toml",
"--pid", "anisotropy.pid",
"--logfile", "anisotropy.log"
]
subprocess.run(
command,
start_new_session = True,
)
return True
@app.callback(
Output("stop", "active"),
[ Input("stop", "n_clicks") ],
prevent_initial_call = True
)
def runnerStop(clicks):
import psutil
import signal
pidpath = "/tmp/anisotropy/anisotropy.pid"
try:
pid = int(open(pidpath, "r").read())
master = psutil.Process(pid)
except (FileNotFoundError, psutil.NoSuchProcess):
return True
else:
os.killpg(master.pid, signal.SIGTERM)
return True
@app.callback(
Output("monitor", "columns"),
Output("monitor", "data"),
Output("runner-status", "children"),
Output("start", "disabled"),
Output("stop", "disabled"),
Output("delete", "disabled"),
[ Input("interval", "n_intervals") ],
)
def monitorUpdate(intervals):
import psutil
pidpath = "/tmp/anisotropy/anisotropy.pid"
processes = []
try:
pid = int(open(pidpath, "r").read())
master = psutil.Process(pid)
except (FileNotFoundError, psutil.NoSuchProcess) as e:
return [], [], "Status: not running", False, True, False
else:
for process in [ master, *master.children() ]:
created = psutil.time.localtime(process.create_time())
processes.append({
"name": process.name(),
"pid": process.pid,
"status": process.status(),
"memory": getSize(process.memory_full_info().uss),
"threads": process.num_threads(),
"created": "{}:{}:{}".format(created.tm_hour, created.tm_min, created.tm_sec)
})
columns = [ { "name": col, "id": col } for col in processes[0].keys() ]
return columns, processes, "Status: running", True, False, True
@app.callback(
Output("logger", "value"),
[ Input("interval", "n_intervals") ]
)
def logUpdate(intervals):
logpath = "/tmp/anisotropy/anisotropy.log"
if os.path.exists(logpath):
with open(logpath, "r") as io:
log = io.read()
return log
else:
return "Not found"
@app.callback(
Output("delete", "active"),
[ Input("delete", "n_clicks") ]
)
def logDelete(clicks):
logpath = "/tmp/anisotropy/anisotropy.log"
if os.path.exists(logpath):
os.remove(logpath)
return True

107
anisotropy/gui/settings.py Normal file
View File

@ -0,0 +1,107 @@
from dash import html
from dash import dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
import os
from .app import app
from .styles import *
###
# Layout
##
layout = html.Div([
# Messages
dbc.Alert(
id = "status",
duration = 10000,
dismissable = True,
is_open = False,
style = message
),
# General
html.H2("General"),
html.Hr(),
html.P("Path: {}".format(os.environ.get("ANISOTROPY_CWD", ""))),
dbc.Button("Save", id = "submit", style = minWidth),
# Options
html.H2("Options"),
html.Hr(),
html.P("Nprocs"),
dcc.Input(id = "nprocs", type = "number", style = minWidth),
html.P("Stage"),
dcc.Dropdown(
id = "stage",
options = [ { "label": k, "value": k } for k in ["all", "shape", "mesh", "flow", "postProcess"] ],
style = minWidth
),
# Cases
html.H2("Cases"),
html.Hr(),
dcc.Textarea(id = "cases", style = bigText),
])
###
# Callbacks
##
@app.callback(
Output("nprocs", "value"),
Output("stage", "value"),
Output("cases", "value"),
[ Input("url", "pathname") ]
)
def settingsLoad(pathname):
from anisotropy.core.config import DefaultConfig
import toml
filepath = os.path.join(os.environ["ANISOTROPY_CWD"], os.environ["ANISOTROPY_CONF_FILE"])
config = DefaultConfig()
if os.path.exists(filepath):
config.load(filepath)
return config["nprocs"], config["stage"], toml.dumps(config.content)
@app.callback(
Output("status", "children"),
Output("status", "is_open"),
Output("status", "color"),
[ Input("submit", "n_clicks") ],
[
State("nprocs", "value"),
State("stage", "value"),
State("cases", "value")
],
prevent_initial_call = True
)
def settingsSave(nclick, nprocs, stage, cases):
from anisotropy.core.config import DefaultConfig
import toml
filepath = os.path.join(os.environ["ANISOTROPY_CWD"], os.environ["ANISOTROPY_CONF_FILE"])
config = DefaultConfig()
if os.path.exists(filepath):
config.load(filepath)
config.update(
nprocs = nprocs,
stage = stage
)
try:
config.content = toml.loads(cases)
config.dump(filepath)
except Exception as e:
return str(e), True, "danger"
else:
return f"Saved to { filepath }", True, "success"

56
anisotropy/gui/styles.py Normal file
View File

@ -0,0 +1,56 @@
minWidth = {
"min-width": "200px",
"max-width": "200px",
"margin-bottom": "15px"
}
bigText = {
"min-width": "100%",
"min-height": "500px"
}
message = {
"position": "fixed",
"top": "30px",
"right": "30px",
}
page = {
"padding": "30px 50px 0px 50px"
}
table = {
"margin-bottom": "15px",
"min-width": "100%"
}
sidebar = {
"position": "fixed",
"top": 0,
"left": 0,
"bottom": 0,
"width": "16rem",
"padding": "2rem 1rem",
"background-color": "#363636"
}
content = {
"margin-left": "18rem",
"margin-right": "2rem",
"padding": "2rem 1rem",
}
misc = {
"color": "#757575",
"text-align": "center"
}
logo = {
"color": "#ffffff",
"text-align": "center"
}
white = {
"color": "#ffffff"
}

7
anisotropy/gui/utils.py Normal file
View File

@ -0,0 +1,7 @@
def getSize(bytes):
for unit in ["", "K", "M", "G", "T", "P"]:
if bytes < 1024:
return f"{bytes:.2f} {unit}iB"
bytes /= 1024