Increased flexibility and privacy.

This commit is contained in:
lwark
2024-07-25 14:42:18 -05:00
parent 4bc5e08ac6
commit 2a34f855aa
6 changed files with 172 additions and 19 deletions

View File

@@ -113,13 +113,26 @@ This is meant to import .xslx files created from the Design & Analysis Software
## SETUP: ## SETUP:
## Download: ## Download:
*Python v3.11 or greater must be installed on your system for this.*
1. Clone or download from github. 1. Clone or download from github.
2. Enter the downloaded folder. 2. Enter the downloaded folder.
3. Open a terminal in the folder with the 'src' folder.
4. Create a new virtual environment: ```python -m venv venv```
5. Activate the virtual environment: (Windows) ```venv\Scripts\activate.bat```
6. Install dependencies: ```pip install -r requirements.txt```
## Database: ## Database:
1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder. 1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder.
2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database. 2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database.
1. The path by default is sqlite based. Postgresql support is available. 1. The path by default is sqlite based. Postgresql support is available.
2. Postgres path 3. Open a terminal in the folder with the 'src' folder.
4. Run database migration: ```alembic upgrade head```
## First Run:
1. On first run, the application copies src/config.yml to C:\Users\{USERNAME}\Local\submissions\config
2. Initially, the 'directory_path' variable is set to the 'sqlalchemy.url' variable in alembic.ini
3. If this folder cannot be found, C:\Users\{USERNAME}\Documents\submissions will be used.
1. If using Postgres, the 'database_path' and other variables will have to be updated manually.

View File

@@ -55,7 +55,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db sqlalchemy.url = sqlite:///{{PUT DATABASE PATH HERE!!}}
[post_write_hooks] [post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run # post_write_hooks defines scripts or Python functions that are run

Binary file not shown.

View File

@@ -229,7 +229,7 @@ class SubmissionsSheet(QTableView):
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
@report_result @report_result
def generate_report(self): def generate_report(self, *args):
""" """
Make a report Make a report
""" """

View File

@@ -4,6 +4,8 @@ Contains miscellaenous functions used by both frontend and backend.
from __future__ import annotations from __future__ import annotations
import json import json
import pprint
import weakref
from json import JSONDecodeError from json import JSONDecodeError
import jinja2 import jinja2
import numpy as np import numpy as np
@@ -21,11 +23,14 @@ from PyQt6.QtGui import QPageSize
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from PyQt6.QtPrintSupport import QPrinter from PyQt6.QtPrintSupport import QPrinter
from __init__ import project_path
from configparser import ConfigParser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
package_dir = Path(__file__).parents[2].resolve() # package_dir = Path(__file__).parents[2].resolve()
logger.debug(f"Package dir: {package_dir}") # package_dir = project_path
logger.debug(f"Package dir: {project_path}")
if platform.system() == "Windows": if platform.system() == "Windows":
os_config_dir = "AppData/local" os_config_dir = "AppData/local"
@@ -224,7 +229,7 @@ class Settings(BaseSettings, extra="allow"):
""" """
database_schema: str database_schema: str
directory_path: Path directory_path: Path | None = None
database_user: str | None = None database_user: str | None = None
database_password: str | None = None database_password: str | None = None
database_name: str database_name: str
@@ -255,11 +260,27 @@ class Settings(BaseSettings, extra="allow"):
@field_validator('directory_path', mode="before") @field_validator('directory_path', mode="before")
@classmethod @classmethod
def ensure_directory_exists(cls, value): def ensure_directory_exists(cls, value):
if value is None:
print("No value for dir path")
if check_if_app():
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
else:
alembic_path = project_path.joinpath("alembic.ini")
print(f"Getting alembic path: {alembic_path}")
value = cls.get_alembic_db_path(alembic_path=alembic_path)
print(f"Using {value}")
if isinstance(value, str): if isinstance(value, str):
value = Path(value) value = Path(value)
if not value.exists(): try:
value = Path().home() check = value.exists()
# metadata.directory_path = value except AttributeError:
check = False
if not check:
print(f"No directory found, using Documents/submissions")
value = Path.home().joinpath("Documents", "submissions")
value.mkdir()
# metadata.directory_path = value
print(f"Final return of directory_path: {value}")
return value return value
@field_validator('database_path', mode="before") @field_validator('database_path', mode="before")
@@ -267,12 +288,14 @@ class Settings(BaseSettings, extra="allow"):
def ensure_database_exists(cls, value, values): def ensure_database_exists(cls, value, values):
# if value == ":memory:": # if value == ":memory:":
# return value # return value
if value is None:
value = values.data['directory_path']
match values.data['database_schema']: match values.data['database_schema']:
case "sqlite": case "sqlite":
value = f"/{Path(value).absolute().__str__()}/{values.data['database_name']}.db" value = Path(f"{Path(value).absolute().__str__()}/{values.data['database_name']}.db")
# db_name = f"{values.data['database_name']}.db" # db_name = f"{values.data['database_name']}.db"
case _: case _:
value = f"@{value}/{values.data['database_name']}" value = f"{value}/{values.data['database_name']}"
# db_name = values.data['database_name'] # db_name = values.data['database_name']
# match value: # match value:
# case str(): # case str():
@@ -283,7 +306,6 @@ class Settings(BaseSettings, extra="allow"):
# return value # return value
# else: # else:
# raise FileNotFoundError(f"Couldn't find database at {value}") # raise FileNotFoundError(f"Couldn't find database at {value}")
return value return value
@field_validator('database_session', mode="before") @field_validator('database_session', mode="before")
@@ -292,9 +314,15 @@ class Settings(BaseSettings, extra="allow"):
if value is not None: if value is not None:
return value return value
else: else:
match values.data['database_schema']:
case "sqlite":
value = f"/{values.data['database_path']}"
# db_name = f"{values.data['database_name']}.db"
case _:
value = f"@{values.data['database_path']}"
template = jinja_template_loading().from_string( template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ values['database_path'] }}") "{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ value }}")
database_path = template.render(values=values.data) database_path = template.render(values=values.data, value=value)
# print(f"Using {database_path} for database path") # print(f"Using {database_path} for database path")
# database_path = values.data['database_path'] # database_path = values.data['database_path']
# if database_path is None: # if database_path is None:
@@ -303,7 +331,7 @@ class Settings(BaseSettings, extra="allow"):
# database_path = Path.home().joinpath(".submissions", "submissions.db") # database_path = Path.home().joinpath(".submissions", "submissions.db")
# # NOTE: finally, look in the local dir # # NOTE: finally, look in the local dir
# else: # else:
# database_path = package_dir.joinpath("submissions.db") # database_path = project_path.joinpath("submissions.db")
# else: # else:
# if database_path == ":memory:": # if database_path == ":memory:":
# pass # pass
@@ -315,7 +343,7 @@ class Settings(BaseSettings, extra="allow"):
# database_path = database_path # database_path = database_path
# else: # else:
# raise FileNotFoundError("No database file found. Exiting program.") # raise FileNotFoundError("No database file found. Exiting program.")
logger.info(f"Using {database_path} for database file.") print(f"Using {database_path} for database file.")
# engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True) # engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
# engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions") # engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions")
engine = create_engine(database_path) engine = create_engine(database_path)
@@ -334,11 +362,13 @@ class Settings(BaseSettings, extra="allow"):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.set_from_db(db_path=kwargs['database_path']) self.set_from_db(db_path=kwargs['database_path'])
def set_from_db(self, db_path: Path): def set_from_db(self, db_path: Path):
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
output = dict(power_users=['lwark', 'styson', 'ruwang']) output = dict(power_users=['lwark', 'styson', 'ruwang'])
else: else:
# session = Session(create_engine(f"sqlite:///{db_path}")) # session = Session(create_engine(f"sqlite:///{db_path}"))
logger.debug(self.__dict__)
session = self.database_session session = self.database_session
config_items = session.execute(text("SELECT * FROM _configitem")).all() config_items = session.execute(text("SELECT * FROM _configitem")).all()
session.close() session.close()
@@ -354,6 +384,36 @@ class Settings(BaseSettings, extra="allow"):
if not hasattr(self, k): if not hasattr(self, k):
self.__setattr__(k, v) self.__setattr__(k, v)
@classmethod
def get_alembic_db_path(cls, alembic_path) -> Path:
c = ConfigParser()
c.read(alembic_path)
path = c['alembic']['sqlalchemy.url'].replace("sqlite:///", "")
return Path(path).parent
def save(self, settings_path:Path):
if not settings_path.exists():
dicto = {}
for k,v in self.__dict__.items():
if k in ['package', 'database_session']:
continue
match v:
case Path():
print("Path")
if v.is_dir():
print("dir")
v = v.absolute().__str__()
elif v.is_file():
print("file")
v = v.parent.absolute().__str__()
case _:
pass
print(f"Key: {k}, Value: {v}")
dicto[k] = v
with open(settings_path, 'w') as f:
yaml.dump(dicto, f)
# return settings
def get_config(settings_path: Path | str | None = None) -> Settings: def get_config(settings_path: Path | str | None = None) -> Settings:
""" """
@@ -400,12 +460,17 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
if check_if_app(): if check_if_app():
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml") settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
else: else:
settings_path = package_dir.joinpath('src', 'config.yml') settings_path = project_path.joinpath('src', 'config.yml')
with open(settings_path, "r") as dset: with open(settings_path, "r") as dset:
default_settings = yaml.load(dset, Loader=yaml.Loader) default_settings = yaml.load(dset, Loader=yaml.Loader)
# NOTE: Tell program we need to copy the config.yml to the user directory # NOTE: Tell program we need to copy the config.yml to the user directory
# NOTE: copy settings to config directory # NOTE: copy settings to config directory
return Settings(**copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=default_settings)) # settings = Settings(**copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=default_settings))
settings = Settings(**default_settings)
settings.save(settings_path=CONFIGDIR.joinpath("config.yml"))
print(f"Default settings: {pprint.pprint(settings.__dict__)}")
return settings
else: else:
# NOTE: check if user defined path is directory # NOTE: check if user defined path is directory
if settings_path.is_dir(): if settings_path.is_dir():
@@ -417,7 +482,9 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
logger.error("No config.yml file found. Writing to directory.") logger.error("No config.yml file found. Writing to directory.")
with open(settings_path, "r") as dset: with open(settings_path, "r") as dset:
default_settings = yaml.load(dset, Loader=yaml.Loader) default_settings = yaml.load(dset, Loader=yaml.Loader)
return Settings(**copy_settings(settings_path=settings_path, settings=default_settings)) # return Settings(**copy_settings(settings_path=settings_path, settings=default_settings))
settings = Settings(**default_settings)
settings.save(settings_path=settings_path)
# logger.debug(f"Using {settings_path} for config file.") # logger.debug(f"Using {settings_path} for config file.")
with open(settings_path, "r") as stream: with open(settings_path, "r") as stream:
settings = yaml.load(stream, Loader=yaml.Loader) settings = yaml.load(stream, Loader=yaml.Loader)

73
submissions.spec Normal file
View File

@@ -0,0 +1,73 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
#### custom for automation of documentation building ####
import sys, subprocess
from pathlib import Path
sys.path.append(Path(".").parent.joinpath('src').absolute().__str__())
from submissions import __version__, __project__, bcolors, project_path
doc_path = project_path.joinpath("docs").absolute()
build_path = project_path.joinpath(".venv", "Scripts", "sphinx-build").absolute().__str__()
print(bcolors.BOLD + "Running Sphinx subprocess to generate rst files..." + bcolors.ENDC)
api_path = project_path.joinpath(".venv", "Scripts", "sphinx-apidoc").absolute().__str__()
subprocess.run([api_path, "-o", doc_path.joinpath("source").__str__(), project_path.joinpath("src", "submissions").__str__(), "-f"])
print(bcolors.BOLD + "Running Sphinx subprocess to generate html docs..." + bcolors.ENDC)
subprocess.run([build_path, doc_path.joinpath("source").__str__(), doc_path.joinpath("build").__str__(), "-a"])
#########################################################
a = Analysis(
['src\\submissions\\__main__.py'],
pathex=[project_path.absolute().__str__()],
binaries=[],
datas=[
("src\\config.yml", "files"),
("src\\submissions\\templates\\*", "files\\templates"),
("src\\submissions\\templates\\css\\*", "files\\templates\\css"),
("docs\\build", "files\\docs"),
("src\\submissions\\resources\\*", "files\\resources"),
("alembic.ini", "files"),
],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=["*.xlsx"],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=f"{__project__}_{__version__}",
debug=True,
bootloader_ignore_signals=False,
strip=False,
upx=True,
# Change these for non-beta versions
#console=False,
#disable_windowed_traceback=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name=f"{__project__}_{__version__}",
)