diff --git a/README.md b/README.md index 3677b0d..c32d940 100644 --- a/README.md +++ b/README.md @@ -113,13 +113,26 @@ This is meant to import .xslx files created from the Design & Analysis Software ## SETUP: ## Download: +*Python v3.11 or greater must be installed on your system for this.* 1. Clone or download from github. 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: 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. 1. The path by default is sqlite based. Postgresql support is available. - 2. Postgres path \ No newline at end of file +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. \ No newline at end of file diff --git a/alembic_default.ini b/alembic_default.ini index 8446f8e..54b313b 100644 --- a/alembic_default.ini +++ b/alembic_default.ini @@ -55,7 +55,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # 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 defines scripts or Python functions that are run diff --git a/requirements.txt b/requirements.txt index bcdafd9..c28e8be 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 170ffbd..9f31caf 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -229,7 +229,7 @@ class SubmissionsSheet(QTableView): self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) @report_result - def generate_report(self): + def generate_report(self, *args): """ Make a report """ diff --git a/src/submissions/tools.py b/src/submissions/tools.py index 9de01be..c83a3ae 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools.py @@ -4,6 +4,8 @@ Contains miscellaenous functions used by both frontend and backend. from __future__ import annotations import json +import pprint +import weakref from json import JSONDecodeError import jinja2 import numpy as np @@ -21,11 +23,14 @@ from PyQt6.QtGui import QPageSize from PyQt6.QtWebEngineWidgets import QWebEngineView from openpyxl.worksheet.worksheet import Worksheet from PyQt6.QtPrintSupport import QPrinter +from __init__ import project_path +from configparser import ConfigParser logger = logging.getLogger(f"submissions.{__name__}") -package_dir = Path(__file__).parents[2].resolve() -logger.debug(f"Package dir: {package_dir}") +# package_dir = Path(__file__).parents[2].resolve() +# package_dir = project_path +logger.debug(f"Package dir: {project_path}") if platform.system() == "Windows": os_config_dir = "AppData/local" @@ -224,7 +229,7 @@ class Settings(BaseSettings, extra="allow"): """ database_schema: str - directory_path: Path + directory_path: Path | None = None database_user: str | None = None database_password: str | None = None database_name: str @@ -255,11 +260,27 @@ class Settings(BaseSettings, extra="allow"): @field_validator('directory_path', mode="before") @classmethod 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): value = Path(value) - if not value.exists(): - value = Path().home() - # metadata.directory_path = value + try: + check = value.exists() + 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 @field_validator('database_path', mode="before") @@ -267,12 +288,14 @@ class Settings(BaseSettings, extra="allow"): def ensure_database_exists(cls, value, values): # if value == ":memory:": # return value + if value is None: + value = values.data['directory_path'] match values.data['database_schema']: 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" case _: - value = f"@{value}/{values.data['database_name']}" + value = f"{value}/{values.data['database_name']}" # db_name = values.data['database_name'] # match value: # case str(): @@ -283,7 +306,6 @@ class Settings(BaseSettings, extra="allow"): # return value # else: # raise FileNotFoundError(f"Couldn't find database at {value}") - return value @field_validator('database_session', mode="before") @@ -292,9 +314,15 @@ class Settings(BaseSettings, extra="allow"): if value is not None: return value 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( - "{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ values['database_path'] }}") - database_path = template.render(values=values.data) + "{{ 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, value=value) # print(f"Using {database_path} for database path") # database_path = values.data['database_path'] # if database_path is None: @@ -303,7 +331,7 @@ class Settings(BaseSettings, extra="allow"): # database_path = Path.home().joinpath(".submissions", "submissions.db") # # NOTE: finally, look in the local dir # else: - # database_path = package_dir.joinpath("submissions.db") + # database_path = project_path.joinpath("submissions.db") # else: # if database_path == ":memory:": # pass @@ -315,7 +343,7 @@ class Settings(BaseSettings, extra="allow"): # database_path = database_path # else: # 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("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions") engine = create_engine(database_path) @@ -334,11 +362,13 @@ class Settings(BaseSettings, extra="allow"): super().__init__(*args, **kwargs) self.set_from_db(db_path=kwargs['database_path']) + def set_from_db(self, db_path: Path): if 'pytest' in sys.modules: output = dict(power_users=['lwark', 'styson', 'ruwang']) else: # session = Session(create_engine(f"sqlite:///{db_path}")) + logger.debug(self.__dict__) session = self.database_session config_items = session.execute(text("SELECT * FROM _configitem")).all() session.close() @@ -354,6 +384,36 @@ class Settings(BaseSettings, extra="allow"): if not hasattr(self, k): 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: """ @@ -400,12 +460,17 @@ def get_config(settings_path: Path | str | None = None) -> Settings: if check_if_app(): settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml") else: - settings_path = package_dir.joinpath('src', 'config.yml') + settings_path = project_path.joinpath('src', 'config.yml') with open(settings_path, "r") as dset: default_settings = yaml.load(dset, Loader=yaml.Loader) + # NOTE: Tell program we need to copy the config.yml to the user 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: # NOTE: check if user defined path is directory 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.") with open(settings_path, "r") as dset: 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.") with open(settings_path, "r") as stream: settings = yaml.load(stream, Loader=yaml.Loader) diff --git a/submissions.spec b/submissions.spec new file mode 100644 index 0000000..033725a --- /dev/null +++ b/submissions.spec @@ -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__}", +)