diff --git a/README.md b/README.md index c32d940..974ea78 100644 --- a/README.md +++ b/README.md @@ -112,14 +112,14 @@ This is meant to import .xslx files created from the Design & Analysis Software ## SETUP: -## Download: +## Download and Setup: *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``` +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: @@ -133,6 +133,14 @@ This is meant to import .xslx files created from the Design & Analysis Software ## 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 +2. 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. +3. Initially, the config variables are set parsing the 'sqlalchemy.url' variable in alembic.ini + +## Building Portable Application: +*Download and Setup must have been performed beforehand* + +1. Using pyinstaller, an exe can be created. +2. Open a terminal in the folder with the 'src' folder. +3. Activate the virtual environment: (Windows) ```.venv\Scripts\activate.bat``` +4. Enter the following command: ```pyinstaller .\submissions.spec --noconfirm``` \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..bbc9ccb --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,47 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import sys +from pathlib import Path +sys.path.append(Path(__file__).parents[2].joinpath('src').absolute().__str__()) +from submissions import __version__, __copyright__, __author__ + +project = 'RSL Submissions' +copyright = __copyright__ +author = f"{__author__['name']} - {__author__['email']}" +release = __version__ + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +master_doc = "index" + +extensions = [ + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx_markdown_builder', + 'sphinx_mdinclude', + ] + +templates_path = ['_templates'] +exclude_patterns = [] + +sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src").__str__()) +sys.path.insert(0, Path(__file__).absolute().resolve().parents[2].joinpath("src/submissions").__str__()) + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +# html_style = 'custom.css' +html_static_path = ['_static'] + + +# autodoc_mock_imports = ["backend.db.models.submissions"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c28e8be..946da5f 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/submissions/__main__.py b/src/submissions/__main__.py index 375f8ad..47c02c9 100644 --- a/src/submissions/__main__.py +++ b/src/submissions/__main__.py @@ -1,5 +1,6 @@ import sys, os from tools import ctx, setup_logger, check_if_app + # environment variable must be set to enable qtwebengine in network path if check_if_app(): os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1" diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 45bd7b1..02e7d38 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -188,7 +188,10 @@ class App(QMainWindow): current_month_bak = current_month_bak.with_suffix(".db") if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__(): logger.info("No backup found for this month, backing up database.") - shutil.copyfile(self.ctx.database_path, current_month_bak) + try: + shutil.copyfile(self.ctx.backup_path, current_month_bak) + except PermissionError as e: + logger.error(f"Couldn't backup database due to: {e}") case "postgresql+psycopg2": logger.warning(f"Backup function not yet implemented for psql") current_month_bak = current_month_bak.with_suffix(".psql") diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index 11d29b4..f79a527 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -85,7 +85,10 @@ class ControlsViewer(QWidget): self.mode = self.mode_typer.currentText() self.sub_typer.clear() # NOTE: lookup subtypes - sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode) + try: + sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode) + except AttributeError: + sub_types = [] if sub_types != []: # NOTE: block signal that will rerun controls getter and update sub_typer with QSignalBlocker(self.sub_typer) as blocker: diff --git a/src/submissions/tools.py b/src/submissions/tools/__init__.py similarity index 95% rename from src/submissions/tools.py rename to src/submissions/tools/__init__.py index 625f7aa..b33ea6e 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools/__init__.py @@ -5,9 +5,7 @@ from __future__ import annotations import json import pprint -import weakref from json import JSONDecodeError -import jinja2 import numpy as np import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv import pandas as pd @@ -15,7 +13,7 @@ from jinja2 import Environment, FileSystemLoader from logging import handlers from pathlib import Path from sqlalchemy.orm import Session -from sqlalchemy import create_engine, text +from sqlalchemy import create_engine, text, MetaData from pydantic import field_validator, BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Any, Tuple, Literal, List @@ -53,7 +51,7 @@ main_form_style = ''' QComboBox:!editable, QDateEdit { background-color:light gray; } - + ''' @@ -302,11 +300,11 @@ class Settings(BaseSettings, extra="allow"): check = value.exists() except AttributeError: check = False - if not check: #and values.data['database_schema'] == "sqlite": + if not check: # and values.data['database_schema'] == "sqlite": # print(f"No directory found, using Documents/submissions") # value = Path.home().joinpath("Documents", "submissions") value.mkdir(exist_ok=True) - print(f"Final return of directory_path: {value}") + # print(f"Final return of directory_path: {value}") return value @field_validator('database_path', mode="before") @@ -314,8 +312,8 @@ class Settings(BaseSettings, extra="allow"): def ensure_database_exists(cls, value, values): # if value == ":memory:": # return value - # and values.data['database_schema'] == "sqlite": - # value = values.data['directory_path'] + # and values.data['database_schema'] == "sqlite": + # value = values.data['directory_path'] match values.data['database_schema']: case "sqlite": if value is None: @@ -366,7 +364,7 @@ class Settings(BaseSettings, extra="allow"): alembic_path = project_path.joinpath("alembic.ini") # print(f"Getting alembic path: {alembic_path}") value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='user') - print(f"Got {value} for user") + # print(f"Got {value} for user") return value @field_validator("database_password", mode='before') @@ -379,10 +377,9 @@ class Settings(BaseSettings, extra="allow"): alembic_path = project_path.joinpath("alembic.ini") # print(f"Getting alembic path: {alembic_path}") value = cls.get_alembic_db_path(alembic_path=alembic_path, mode='pass') - print(f"Got {value} for pass") + # print(f"Got {value} for pass") return value - @field_validator('database_session', mode="before") @classmethod def create_database_session(cls, value, values): @@ -395,8 +392,9 @@ class Settings(BaseSettings, extra="allow"): value = f"/{values.data['database_path']}" db_name = f"{values.data['database_name']}.db" case _: - print(pprint.pprint(values.data)) - tmp = jinja_template_loading().from_string("{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}") + # print(pprint.pprint(values.data)) + tmp = jinja_template_loading().from_string( + "{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}") value = tmp.render(values=values.data) db_name = values.data['database_name'] template = jinja_template_loading().from_string( @@ -422,7 +420,7 @@ class Settings(BaseSettings, extra="allow"): # database_path = database_path # else: # raise FileNotFoundError("No database file found. Exiting program.") - print(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) @@ -439,15 +437,22 @@ class Settings(BaseSettings, extra="allow"): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_from_db(db_path=kwargs['database_path']) + self.set_from_db() - def set_from_db(self, db_path: Path): + def set_from_db(self): 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 + metadata = MetaData() + try: + tables = metadata.reflect(bind=session.get_bind()).tables.keys() + except AttributeError: + return + if "_configitem" not in tables: + return config_items = session.execute(text("SELECT * FROM _configitem")).all() session.close() # print(config_items) @@ -480,14 +485,14 @@ class Settings(BaseSettings, extra="allow"): try: return url[:url.index("@")].split(":")[0] except (IndexError, ValueError) as e: - print(f"Error on user: {e}") + # print(f"Error on user: {e}") return None case "pass": url = re.sub(r"^.*//", "", url) try: return url[:url.index("@")].split(":")[1] except (IndexError, ValueError) as e: - print(f"Error on user: {e}") + # print(f"Error on user: {e}") return None def save(self, settings_path: Path): @@ -498,18 +503,18 @@ class Settings(BaseSettings, extra="allow"): continue match v: case Path(): - print("Path") + # print("Path") if v.is_dir(): - print("dir") + # print("dir") v = v.absolute().__str__() elif v.is_file(): - print("file") + # print("file") v = v.parent.absolute().__str__() else: v = v.__str__() case _: pass - print(f"Key: {k}, Value: {v}") + # print(f"Key: {k}, Value: {v}") dicto[k] = v with open(settings_path, 'w') as f: yaml.dump(dicto, f) @@ -570,7 +575,7 @@ def get_config(settings_path: Path | str | None = None) -> 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__)}") + # print(f"Default settings: {pprint.pprint(settings.__dict__)}") return settings else: # NOTE: check if user defined path is directory @@ -758,7 +763,7 @@ def jinja_template_loading() -> Environment: if check_if_app(): loader_path = Path(sys._MEIPASS).joinpath("files", "templates") else: - loader_path = Path(__file__).parent.joinpath('templates').absolute() #.__str__() + loader_path = Path(__file__).parents[1].joinpath('templates').absolute() # .__str__() # NOTE: jinja template loading loader = FileSystemLoader(loader_path) env = Environment(loader=loader) diff --git a/submissions.spec b/submissions.spec index 033725a..a6ddd67 100644 --- a/submissions.spec +++ b/submissions.spec @@ -7,14 +7,16 @@ import sys, subprocess from pathlib import Path sys.path.append(Path(".").parent.joinpath('src').absolute().__str__()) from submissions import __version__, __project__, bcolors, project_path - +print(f"Using {project_path} as 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"]) +docs_build = doc_path.joinpath("build") +#docs_build.mkdir(exist_ok=True, parents=True) +subprocess.run([build_path, doc_path.joinpath("source").__str__(), docs_build.__str__(), "-a"]) ######################################################### a = Analysis(