diff --git a/src/Model/DB.py b/src/Model/DB.py new file mode 100644 index 0000000000000000000000000000000000000000..5206555a11a8c2389cee9abc67444b6ef63bb557 --- /dev/null +++ b/src/Model/DB.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +import os +import sqlite3 + +from pathlib import Path + +from tools import SQL +from Model.Except import NotImplementedMethodeError + +# Top level model class +class SQLModel(SQL): + _sub_classes = [] + + def _init_db_file(self, db, is_new = True): + exists = Path(db).exists() + + if exists and is_new: + os.remove(db) + + self._db = sqlite3.connect(db) + self._cur = self._db.cursor() + + if is_new: + self._create() # Create db + self._save() # Save + else: + self._update() # Update db scheme if necessary + self._load() # Load data + + def _create_submodel(self): + for cls in self._sub_classes: + requests = cls._sql_create( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + ) + + def _create(self): + raise NotImplementedMethodeError(self, self._create) + + def _update_submodel(self, version): + for cls in self._sub_classes: + requests = cls._sql_update( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ), + version + ) + + def _update(self): + raise NotImplementedMethodeError(self, self._update) + + def _save_submodel(self, objs): + for obj in objs: + requests = obj._sql_save( + lambda sql: self.execute( + sql, + fetch_one = False, + commit = True + ) + ) + + def _save(self): + raise NotImplementedMethodeError(self, self._save) + + @classmethod + def _load(cls, filename): + raise NotImplementedMethodeError(cls, cls._load) + +# Sub model class +class SQLSubModel(object): + _sub_classes = [] + + def _sql_format(self, value): + # Replace ''' by ''' to preserve SQL injection + if type(value) == str: + value = value.replace("'", "'") + return value + + @classmethod + def _create_submodel(cls, execute): + for sc in cls._sub_classes: + sc._sql_create(execute) + + @classmethod + def _sql_create(cls, execute): + """Create data base scheme + + Args: + execute: Function to exec SQL resquest + + Returns: + Return true, otherelse false if an issue appear + """ + raise NotImplementedMethodeError(cls, cls._sql_create) + + @classmethod + def _update_submodel(cls, execute, version): + for sc in cls._sub_classes: + sc._sql_update(execute, version) + + @classmethod + def _sql_update(cls, execute, version): + """Update data base scheme + + Args: + execute: Function to exec SQL resquest + version: Current database version + + Returns: + Return true, otherelse false if an issue appear + """ + raise NotImplementedMethodeError(cls, cls._sql_update) + + @classmethod + def _sql_load(cls, execute, data = None): + """Load instance of this class from SQL data base + + Args: + execute: Function to exec SQL request + data: Optional data for the class constructor + + Returns: + Return new instance of class + """ + raise NotImplementedMethodeError(cls, cls._sql_load) + + def _save_submodel(self, execute): + for sc in self._sub_classes: + sc._sql_update(execute) + + def _sql_save(self, execute, data = None): + """Save class data to data base + + Args: + execute: Function to exec SQL resquest + data: Optional additional information for save + + Returns: + Return true, otherelse false if an issue appear during + save + """ + raise NotImplementedMethodeError(self, self._sql_save) diff --git a/src/Model/Except.py b/src/Model/Except.py index 8480e3ee953401902b797aff59cc2a23413db94b..ad311666c4c1d4eec651ee316bce82edb4695df5 100644 --- a/src/Model/Except.py +++ b/src/Model/Except.py @@ -70,7 +70,7 @@ class NotImplementedMethodeError(ExeceptionWithMessageBox): f" '{self.func.__name__}' " + _translate("Exception", "not implemented") + _translate("Exception", "for class") + - f" '{self.obj.__class__}'" + f" '{self.obj.__class__ if self.obj.__class__ != type else self.obj}'" ) def header(self): diff --git a/src/Model/Geometry/PointXYZ.py b/src/Model/Geometry/PointXYZ.py index 5f99f67bffd276d4d1b7023e17c0010913af7460..9321d5bf98ef645fac5c2000b22dcec3f9106a21 100644 --- a/src/Model/Geometry/PointXYZ.py +++ b/src/Model/Geometry/PointXYZ.py @@ -3,9 +3,12 @@ from math import dist import numpy as np +from Model.DB import SQLSubModel from Model.Geometry.Point import Point -class PointXYZ(Point): +class PointXYZ(Point, SQLSubModel): + _sub_classes = [] + def __init__(self, x:float = 0.0, y:float = 0.0, z:float = 0.0, name:str = "", status = None): super(PointXYZ, self).__init__(name=name, status=status) @@ -14,6 +17,36 @@ class PointXYZ(Point): self._y = float(y) self._z = float(z) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE geometry_pointXYZ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT, + x INTEGER NOT NULL, + y INTEGER NOT NULL, + z INTEGER NOT NULL, + profile INTEGER NOT NULL, + FOREIGN KEY(profile) REFERENCES profileXYZ(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + + @classmethod def from_data(cls, header, data): point = None diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 08cf2cf4c7b3f4716eb8f8101d2ae248bef32ffd..2013dea31808379f40f3a82c81c3b9fad5a70166 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -5,12 +5,17 @@ from typing import List from tools import timer +from Model.DB import SQLSubModel from Model.Except import ClipboardFormatError from Model.Geometry.Profile import Profile from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.Vector_1d import Vector1d -class ProfileXYZ(Profile): +class ProfileXYZ(Profile, SQLSubModel): + _sub_classes = [ + PointXYZ, + ] + def __init__(self, name: str = "", kp: float = 0., @@ -41,6 +46,36 @@ class ProfileXYZ(Profile): status = status, ) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE geometry_profileXYZ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT, + reach INTEGER NOT NULL, + kp REAL NOT NULL, + num INTEGER NOT NULL, + code1 INTEGER NOT NULL, + code2 INTEGER NOT NULL, + FOREIGN KEY(reach) REFERENCES river_reach(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + @classmethod def from_data(cls, header, data): profile = None diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index ebff2dc71f4765a56ca408cc322c87266efa12d9..c5415c7693b7596fdfdcaaf97e534a299986bd58 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -10,12 +10,18 @@ from functools import reduce from tools import flatten, timer, trace +from Model.DB import SQLSubModel + from Model.Geometry.Profile import Profile from Model.Geometry.ProfileXYZ import ProfileXYZ from Model.Except import FileFormatError, exception_message_box -class Reach: +class Reach(SQLSubModel): + _sub_classes = [ + ProfileXYZ, + ] + def __init__(self, status=None, parent=None): self._status = status self._parent = parent @@ -24,6 +30,34 @@ class Reach: self._guidelines_is_valid = False self._guidelines = {} + @classmethod + def _sql_create(cls, execute): + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + cls._update_submodel(execute, version) + return None + + @classmethod + def _sql_load(cls, execute, data = None): + new = cls(status = data["status"], parent = data["parent"]) + + new._profiles = ProfileXYZ._sql_load( + execute, + data = { + "status": data["status"], + "reach": new, + } + ) + + return new + + def _sql_save(self, execute, data = None): + cls._save_submodel(execute, data) + return True + def profile(self, i): """Returns profile at index i diff --git a/src/Model/Network/Node.py b/src/Model/Network/Node.py index 5da21909d2aff22fa9ac7d03a34ed3fa8a9d0569..5281a23627e89d4df97f3292ccc79e4b008d5437 100644 --- a/src/Model/Network/Node.py +++ b/src/Model/Network/Node.py @@ -38,6 +38,15 @@ class Node(object): def name(self): return self._name + @property + def x(self): + return self.pos.x + + @property + def y(self): + return self.pos.y + + def setPos(self, x, y): self.pos.x = x self.pos.y = y diff --git a/src/Model/River.py b/src/Model/River.py index 580d2b8fa59999ef85e38cb29ca59aa3b0bff60e..63137639517416f6c9c27134946baa68926e36cc 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from Model.DB import SQLSubModel + from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph @@ -16,7 +18,9 @@ from Model.SolverParameters.SolverParametersList import SolverParametersList from Solver.Solvers import solver_type_list -class RiverNode(Node): +class RiverNode(Node, SQLSubModel): + _sub_classes = [] + def __init__(self, id:str, name:str, x:float, y:float, status = None): @@ -28,6 +32,31 @@ class RiverNode(Node): self._locker = None + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE river_node( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return None + + @classmethod + def _sql_load(cls, execute, data = None): + return True + + def _sql_save(self, execute, data = None): + return True + @property def locker(self): return self._locker @@ -37,7 +66,12 @@ class RiverNode(Node): self._locker = locker -class RiverReach(Edge): +class RiverReach(Edge, SQLSubModel): + _sub_classes = [ + Reach, + # SectionList, + ] + def __init__(self, id:str, name:str, node1:RiverNode = None, node2:RiverNode = None, @@ -51,6 +85,33 @@ class RiverReach(Edge): self._reach = Reach(status=self._status, parent=self) self._sections = SectionList(status=self._status) + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE river_reach( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + node1 INTEGER, + node2 INTEGER, + FOREIGN KEY(node1) REFERENCES river_node(id), + FOREIGN KEY(node2) REFERENCES river_node(id) + ) + """) + + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + @property def reach(self): return self._reach @@ -59,7 +120,17 @@ class RiverReach(Edge): def sections(self): return self._sections -class River(Graph): +class River(Graph, SQLSubModel): + _sub_classes = [ + RiverNode, + RiverReach, + # BoundaryConditionList, + # LateralContributionList, + # InitialConditionsDict, + # StricklersList, + # SolverParametersList, + ] + def __init__(self, status=None): super(River, self).__init__(status=status) @@ -74,6 +145,31 @@ class River(Graph): self._stricklers = StricklersList(status=self._status) self._parameters = {} + @classmethod + def _sql_create(cls, execute): + cls._create_submodel(execute) + return True + + @classmethod + def _sql_update(cls, execute, version): + return True + + @classmethod + def _sql_load(cls, execute, data = None): + return None + + def _sql_save(self, execute, data = None): + return True + + @property + def reach(self): + return self._reach + + @property + def sections(self): + return self._sections + + @property def boundary_condition(self): return self._boundary_condition diff --git a/src/Model/Serializable.py b/src/Model/Serializable.py index 17b12345196a0fe7244b9145f92758ea08316f50..57f2a4c16fa01424e625685756efabcb59363d11 100644 --- a/src/Model/Serializable.py +++ b/src/Model/Serializable.py @@ -3,8 +3,8 @@ import pickle class Serializable(): - def __init__(self, filename): - self.filename = filename + def __init__(self): + return @classmethod def open(cls, filename): diff --git a/src/Model/Study.py b/src/Model/Study.py index a07d32c49e8a94fc305c9db10c3ef2a2780ef2d4..238da3b482f87b40fd74e4b3f89d12fbe20772f0 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -3,18 +3,28 @@ import os from datetime import datetime +from Model.DB import SQLModel from Model.Saved import SavedStatus from Model.Serializable import Serializable - +from Model.Except import NotImplementedMethodeError from Model.River import River from Checker.Study import * -class Study(Serializable): - def __init__(self): - # Serialization information - super(Study, self).__init__("") - self.filename = "" +class Study(SQLModel): + _sub_classes = [ + River, + ] + + def __init__(self, filename = None, init_new = True): + # Metadata + self._version = "0.0.0" + self.creation_date = datetime.now() + self.last_modification_date = datetime.now() + self.last_save_date = datetime.now() + + self._filename = filename + super(Study, self).__init__(filename = filename) self.status = SavedStatus() @@ -25,12 +35,9 @@ class Study(Serializable): self._time_system = "time" self._date = datetime.fromtimestamp(0) - self.creation_date = datetime.now() - self.last_modification_date = datetime.now() - self.last_save_date = datetime.now() - - # Study data - self._river = River(status = self.status) + if init_new: + # Study data + self._river = River(status = self.status) @classmethod def checkers(cls): @@ -66,6 +73,15 @@ class Study(Serializable): self._name = str(name) self.status.modified() + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, filename): + self._filename = str(filename) + self._init_db_file(filename, is_new = True) + @property def time_system(self): return self._time_system @@ -88,9 +104,9 @@ class Study(Serializable): self._date = timestamp self.status.modified() - @classmethod - def new(cls): - return cls() + # @classmethod + # def new(cls): + # return cls() @classmethod def new(cls, name, description, date = None): @@ -101,3 +117,46 @@ class Study(Serializable): me.use_date() me.date = date return me + + @classmethod + def open(cls, filename): + me = cls._load(filename) + return me + + ####### + # SQL # + ####### + + def _create(self): + # Info (metadata) + self.execute("CREATE TABLE info(key TEXT NOT NULL UNIQUE, value TEXT NOT NULL)") + self.execute( + f"INSERT INTO info VALUES ('version', '{self._sql_format(self._version)}')", + commit = True + ) + + self._create_submodel() + self.commit() + + def _update(self): + version = self.execute(f"SELECT value FROM info WHERE key='version'") + + print(f"{version} == {self._version}") + if version == self._version: + return True + + print("TODO: update") + raise NotImplementedMethodeError(self, self._update) + + @classmethod + def _load(cls, filename): + new = cls(init_new = False) + + # Load river data + self._river = River.load() + + return new + + def _save(self): + + self.commit() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index ea23101ad97dc9ef9501642d36cfa9aef57825f8..340e3cf000fcb7236de06b12af3a7b9250bcf3e0 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -213,7 +213,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model.filename == "": + if self.model.filename is None or self.model.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", "", "Pamhyr(*.pkl)" @@ -282,9 +282,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if self.model is None: dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) - dialog.setDefaultSuffix(".pkl") + dialog.setDefaultSuffix(".pamhyr") #dialog.setFilter(dialog.filter() | QtCore.QDir.Hidden) - dialog.setNameFilters(['PamHyr (*.pkl)']) + dialog.setNameFilters(['PamHyr (*.pamhyr)']) if dialog.exec_(): file_name = dialog.selectedFiles() diff --git a/src/config.py b/src/config.py index 61c52272d0d30a773638fe4847b3eade7fc082ed..64f5664a3d92519093026085f11580cd6932ca99 100644 --- a/src/config.py +++ b/src/config.py @@ -19,7 +19,7 @@ class Config(SQL): self.filename = Config.filename() self.set_default_value() - super(Config, self).__init__(db = self.filename) + super(Config, self).__init__(filename = self.filename) def _create(self): # Info (meta data) diff --git a/src/tools.py b/src/tools.py index a3f20d049e382dd132bc2667a2dd4f22c72031ab..17d24b4e931487962013ec05d64ba054ab8fcfd0 100644 --- a/src/tools.py +++ b/src/tools.py @@ -165,7 +165,7 @@ def old_pamhyr_date_to_timestamp(date:str): # from sqlite3. class SQL(object): - def __init__(self, db = "db.sqlite3"): + def _init_db_file(self, db): exists = Path(db).exists() self._db = sqlite3.connect(db) @@ -178,6 +178,13 @@ class SQL(object): self._update() # Update db scheme if necessary self._load() # Load data + + def __init__(self, filename = None): + self._db = None + + if filename is not None: + self._init_db_file(filename) + def commit(self): self._db.commit()