From 5a09bc3af1fc0a80d65bcce12aca78893ea90be6 Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Thu, 30 Dec 2021 13:19:41 -0500 Subject: [PATCH 1/7] version --- CHANGELOG.md | 3 +++ dse_do_utils/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cddcca0..dd345d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased]## [0.5.3.2b] +### Changed + ## [0.5.3.1] - 2021-12-30 ### Changed - (critical) ScenarioDbManager - Replaced OrderedDict with Dict as type. Was causing a syntax error. diff --git a/dse_do_utils/version.py b/dse_do_utils/version.py index c1a7924..d379142 100644 --- a/dse_do_utils/version.py +++ b/dse_do_utils/version.py @@ -9,4 +9,4 @@ See https://stackoverflow.com/questions/458550/standard-way-to-embed-version-into-python-package """ -__version__ = "0.5.3.1" +__version__ = "0.5.3.2b" From 485a8875f6f859543787e88825ec16d8e58f0764 Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Sun, 9 Jan 2022 14:04:07 -0500 Subject: [PATCH 2/7] Bumping-up beta version to v0.5.4.0b --- VersioningReadMe.md | 2 +- dse_do_utils/scenariodbmanager.py | 51 +++++++++++++++++++++++++++++-- dse_do_utils/version.py | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/VersioningReadMe.md b/VersioningReadMe.md index 248f238..5b7e88f 100644 --- a/VersioningReadMe.md +++ b/VersioningReadMe.md @@ -20,7 +20,7 @@ Note that if you added/removed modules, you first need to re-run the sphinx comm `python setup.py sdist bdist_wheel` 5. Upload to PyPI (from PyCharm terminal run):
-`twine upload dist/* --verbose` +`make clean` Enter username and password when prompted. (For TestPyPI use: `twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose`)
Before the twine upload, you can check the distribution with:
diff --git a/dse_do_utils/scenariodbmanager.py b/dse_do_utils/scenariodbmanager.py index c59beee..560d16e 100644 --- a/dse_do_utils/scenariodbmanager.py +++ b/dse_do_utils/scenariodbmanager.py @@ -22,7 +22,7 @@ import sqlalchemy import pandas as pd -from typing import Dict, List +from typing import Dict, List, NamedTuple, Any from collections import OrderedDict import re from sqlalchemy import exc @@ -201,6 +201,16 @@ def insert_table_in_db_bulk(self, df, mgr, connection=None): print(e) +class DbCellUpdate(NamedTuple): + scenario_name: str + table_name: str + row_index: List[Dict[str, Any]] # e.g. [{'column': 'col1', 'value': 1}, {'column': 'col2', 'value': 'pear'}] + column_name: str + current_value: Any + previous_value: Any # Not used for DB operation + row_idx: int # Not used for DB operation + + ######################################################################### # ScenarioDbManager ######################################################################### @@ -527,6 +537,8 @@ def _replace_scenario_in_db_transaction(self, scenario_name: str, inputs: Inputs Will set/overwrite the scenario_name in all dfs, so no need to add in advance. Assumes schema has been created. Note: there is no difference between dfs in inputs or outputs, i.e. they are inserted the same way. + + TODO: break-out in a delete and an insert. Then we can re-use the insert for the duplicate API """ # Step 1: delete scenario if exists self._delete_scenario_from_db(scenario_name, connection=connection) @@ -551,7 +563,6 @@ def _delete_scenario_from_db(self, scenario_name: str, connection=None): Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. Also deletes entry in scenario table - TODO: do within one session/cursor, so we don't have to worry about the order of the delete? """ insp = sqlalchemy.inspect(self.engine) for scenario_table_name, db_table in reversed(self.db_tables.items()): @@ -573,7 +584,7 @@ def _insert_single_scenario_tables_in_db(self, inputs: Inputs = {}, outputs: Out bulk: bool = True, connection=None) -> int: """Specifically for single scenario replace/insert. Does NOT insert into the `scenario` table. - No `auto_insert`, i.e. only df matching db_tables. + No `auto_insert`, i.e. only df matching db_tables. TODO: verify if doesn't work with AutoScenarioDbTable """ num_caught_exceptions = 0 dfs = {**inputs, **outputs} # Combine all dfs in one dict @@ -721,6 +732,40 @@ def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: Scenario return df + ############################################################################################ + # Update scenario + ############################################################################################ + def update_cell_changes_in_db(self, db_cell_updates: List[DbCellUpdate]): + """Update a set of cells in the DB. + + :param db_cell_updates: + :return: + """ + if self.enable_transactions: + print("Update cells with transaction") + with self.engine.begin() as connection: + self._update_cell_changes_in_db(db_cell_updates, connection=connection) + else: + self._update_cell_changes_in_db(db_cell_updates) + + def _update_cell_changes_in_db(self, db_cell_updates: List[DbCellUpdate], connection=None): + """Update an ordered list of single value changes (cell) in the DB.""" + for db_cell_change in db_cell_updates: + self._update_cell_change_in_db(db_cell_change, connection) + + def _update_cell_change_in_db(self, db_cell_update: DbCellUpdate, connection=None): + """Update a single value (cell) change in the DB.""" + db_table_name = self.db_tables[db_cell_update.table_name].db_table_name + column_change = f"{db_cell_update.column_name} = '{db_cell_update.current_value}'" + scenario_condition = f"scenario_name = '{db_cell_update.scenario_name}'" + pk_conditions = ' AND '.join([f"{pk['column']} = '{pk['value']}'" for pk in db_cell_update.row_index]) + sql = f"UPDATE {db_table_name} SET {column_change} WHERE {pk_conditions} AND {scenario_condition};" + # print(f"_update_cell_change_in_db = {sql}") + if connection is None: + self.engine.execute(sql) + else: + connection.execute(sql) + ############################################################################################ # Old Read scenario APIs diff --git a/dse_do_utils/version.py b/dse_do_utils/version.py index d379142..9b9a90d 100644 --- a/dse_do_utils/version.py +++ b/dse_do_utils/version.py @@ -9,4 +9,4 @@ See https://stackoverflow.com/questions/458550/standard-way-to-embed-version-into-python-package """ -__version__ = "0.5.3.2b" +__version__ = "0.5.4.0b" From d1963a0f3c8aef512909300da5f8377fe876b2f6 Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Mon, 10 Jan 2022 20:01:23 -0500 Subject: [PATCH 3/7] Migration of duplicate, rename and delete scenario APIs --- CHANGELOG.md | 6 +- dse_do_utils/scenariodbmanager.py | 379 ++++++++++++++++++++++++++++-- 2 files changed, 360 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd345d6..3679a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased]## [0.5.3.2b] +## [Unreleased]## [0.5.4.0b] ### Changed +- ScenarioDbManager - Converted text SQL operations to SQLAlchemy operations to support any column-name (i.e. lower, upper, mixed, reserved words) +### Added +- ScenarioDbManager - Edit cells in tables +- ScenarioDbManager - Duplicate, Rename and Delete scenario ## [0.5.3.1] - 2021-12-30 ### Changed diff --git a/dse_do_utils/scenariodbmanager.py b/dse_do_utils/scenariodbmanager.py index 560d16e..ab0e30f 100644 --- a/dse_do_utils/scenariodbmanager.py +++ b/dse_do_utils/scenariodbmanager.py @@ -19,10 +19,11 @@ # - Make 'multi_scenario' the default option # ----------------------------------------------------------------------------------- from abc import ABC +from multiprocessing.pool import ThreadPool import sqlalchemy import pandas as pd -from typing import Dict, List, NamedTuple, Any +from typing import Dict, List, NamedTuple, Any, Optional from collections import OrderedDict import re from sqlalchemy import exc @@ -62,6 +63,7 @@ def __init__(self, db_table_name: str, columns_metadata: List[sqlalchemy.Column] reserved_table_names = ['order', 'parameter'] # TODO: add more reserved words for table names if db_table_name in reserved_table_names: print(f"Warning: the db_table_name '{db_table_name}' is a reserved word. Do not use as table name.") + self._sa_column_by_name = None # Dict[str, sqlalchemy.Column] Will be generated dynamically first time it is needed. def get_db_table_name(self) -> str: return self.db_table_name @@ -74,7 +76,21 @@ def get_df_column_names(self) -> List[str]: column_names.append(c.name) return column_names - def create_table_metadata(self, metadata, multi_scenario: bool = False): + def get_sa_table(self) -> sqlalchemy.Table: + """Returns the SQLAlchemy Table""" + return self.table_metadata + + def get_sa_column(self, db_column_name) -> Optional[sqlalchemy.Column]: + """Returns the SQLAlchemy column with the specified name. + Dynamically creates a dict/hashtable for more efficient access.""" + # for c in self.columns_metadata: + # if isinstance(c, sqlalchemy.Column) and c.name == db_column_name: + # return c + if self._sa_column_by_name is None: + self._sa_column_by_name = {c.name: c for c in self.columns_metadata if isinstance(c, sqlalchemy.Column)} + return self._sa_column_by_name.get(db_column_name) # returns None if npt found (?) + + def create_table_metadata(self, metadata, multi_scenario: bool = False) -> sqlalchemy.Table: """If multi_scenario, then add a primary key 'scenario_name'.""" columns_metadata = self.columns_metadata constraints_metadata = self.constraints_metadata @@ -266,6 +282,11 @@ def _add_scenario_db_table(self, input_db_tables: Dict[str, ScenarioDbTable]) -> print("Warning: the `Scenario` table should be the first in the input tables") return input_db_tables + def get_scenario_db_table(self) -> ScenarioDbTable: + """Scenario table must be the first in self.input_db_tables""" + db_table: ScenarioTable = list(self.input_db_tables.values())[0] + return db_table + def _create_database_engine(self, credentials=None, schema: str = None, echo: bool = False): """Creates a SQLAlchemy engine at initialization. If no credentials, creates an in-memory SQLite DB. Which can be used for schema validation of the data. @@ -677,13 +698,19 @@ def insert_tables_in_db(self, inputs: Inputs = {}, outputs: Outputs = {}, ############################################################################################ # Read scenario ############################################################################################ - def get_scenarios_df(self): + def get_scenarios_df(self) -> pd.DataFrame: """Return all scenarios in df. Result is indexed by `scenario_name`. Main API to get all scenarios. The API called by a cached procedure in the dse_do_dashboard.DoDashApp. """ - sql = f"SELECT * FROM SCENARIO" - df = pd.read_sql(sql, con=self.engine).set_index(['scenario_name']) + # sql = f"SELECT * FROM SCENARIO" + sa_scenario_table = list(self.input_db_tables.values())[0].table_metadata + sql = sa_scenario_table.select() + if self.enable_transactions: + with self.engine.begin() as connection: + df = pd.read_sql(sql, con=connection).set_index(['scenario_name']) + else: + df = pd.read_sql(sql, con=self.engine).set_index(['scenario_name']) return df def read_scenario_table_from_db(self, scenario_name: str, scenario_table_name: str) -> pd.DataFrame: @@ -702,31 +729,138 @@ def read_scenario_table_from_db(self, scenario_name: str, scenario_table_name: s # error! raise ValueError(f"Scenario table name '{scenario_table_name}' unknown. Cannot load data from DB.") - df = self._read_scenario_db_table_from_db(scenario_name, db_table) + if self.enable_transactions: + with self.engine.begin() as connection: + df = self._read_scenario_db_table_from_db(scenario_name, db_table, connection) + else: + df = self._read_scenario_db_table_from_db(scenario_name, db_table, self.engine) return df - def read_scenario_from_db(self, scenario_name: str) -> (Inputs, Outputs): + # def read_scenario_from_db(self, scenario_name: str) -> (Inputs, Outputs): + # """Single scenario load. + # Main API to read a complete scenario. + # Reads all tables for a single scenario. + # Returns all tables in one dict""" + # inputs = {} + # for scenario_table_name, db_table in self.input_db_tables.items(): + # inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # + # outputs = {} + # for scenario_table_name, db_table in self.output_db_tables.items(): + # outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # + # return inputs, outputs + + + # def _read_scenario_from_db(self, scenario_name: str, connection) -> (Inputs, Outputs): + # """Single scenario load. + # Main API to read a complete scenario. + # Reads all tables for a single scenario. + # Returns all tables in one dict + # """ + # inputs = {} + # for scenario_table_name, db_table in self.input_db_tables.items(): + # # print(f"scenario_table_name = {scenario_table_name}") + # if scenario_table_name != 'Scenario': # Skip the Scenario table as an input + # inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + # + # outputs = {} + # for scenario_table_name, db_table in self.output_db_tables.items(): + # outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + # # if scenario_table_name == 'kpis': + # # # print(f"kpis table columns = {outputs[scenario_table_name].columns}") + # # outputs[scenario_table_name] = outputs[scenario_table_name].rename(columns={'name': 'NAME'}) #HACK!!!!! + # return inputs, outputs + def read_scenario_from_db(self, scenario_name: str, multi_threaded: bool = False) -> (Inputs, Outputs): + """Single scenario load. + Main API to read a complete scenario. + Reads all tables for a single scenario. + Returns all tables in one dict + + Note: multi_threaded doesn't seem to lead to performance improvement. + Fixed: omit reading scenario table as an input. + """ + # print(f"read_scenario_from_db.multi_threaded = {multi_threaded}") + if multi_threaded: + inputs, outputs = self._read_scenario_from_db_multi_threaded(scenario_name) + else: + if self.enable_transactions: + with self.engine.begin() as connection: + inputs, outputs = self._read_scenario_from_db(scenario_name, connection) + else: + inputs, outputs = self._read_scenario_from_db(scenario_name, self.engine) + return inputs, outputs + + def _read_scenario_from_db(self, scenario_name: str, connection) -> (Inputs, Outputs): """Single scenario load. Main API to read a complete scenario. Reads all tables for a single scenario. - Returns all tables in one dict""" + Returns all tables in one dict + """ inputs = {} for scenario_table_name, db_table in self.input_db_tables.items(): - inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # print(f"scenario_table_name = {scenario_table_name}") + if scenario_table_name != 'Scenario': # Skip the Scenario table as an input + inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) outputs = {} for scenario_table_name, db_table in self.output_db_tables.items(): - outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + + return inputs, outputs + + def _read_scenario_from_db_multi_threaded(self, scenario_name) -> (Inputs, Outputs): + """Reads all tables from a scenario using multi-threading. + Does NOT seem to result in performance improvement!""" + class ReadTableFunction(object): + def __init__(self, dbm): + self.dbm = dbm + def __call__(self, scenario_table_name, db_table): + return self._read_scenario_db_table_from_db_thread(scenario_table_name, db_table) + def _read_scenario_db_table_from_db_thread(self, scenario_table_name, db_table): + with self.dbm.engine.begin() as connection: + df = self.dbm._read_scenario_db_table_from_db(scenario_name, db_table, connection) + dict = {scenario_table_name: df} + return dict + + thread_number = 8 + pool = ThreadPool(thread_number) + thread_worker = ReadTableFunction(self) + # print("ThreadPool created") + all_tables = [(scenario_table_name, db_table) for scenario_table_name, db_table in self.db_tables.items() if scenario_table_name != 'Scenario'] + # print(all_tables) + all_results = pool.starmap(thread_worker, all_tables) + inputs = {k:v for element in all_results for k,v in element.items() if k in self.input_db_tables.keys()} + outputs = {k:v for element in all_results for k,v in element.items() if k in self.output_db_tables.keys()} + # print("All tables loaded") return inputs, outputs - def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable) -> pd.DataFrame: + # def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable) -> pd.DataFrame: + # """Read one table from the DB. + # Removes the `scenario_name` column.""" + # db_table_name = db_table.db_table_name + # sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" + # df = pd.read_sql(sql, con=self.engine) + # if db_table_name != 'scenario': + # df = df.drop(columns=['scenario_name']) + # + # return df + def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable, connection) -> pd.DataFrame: """Read one table from the DB. - Removes the `scenario_name` column.""" + Removes the `scenario_name` column. + + Modification: based on SQLAlchemy syntax. If doing the plain text SQL, then some column names not properly extracted + """ db_table_name = db_table.db_table_name - sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" - df = pd.read_sql(sql, con=self.engine) + # sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + # db_table.table_metadata is a Table() + t = db_table.table_metadata + sql = t.select().where(t.c.scenario_name == scenario_name) # This is NOT a simple string! + # print(f"_read_scenario_db_table_from_db SQL = {sql}") + # df = pd.read_sql(sql, con=self.engine) + df = pd.read_sql(sql, con=connection) if db_table_name != 'scenario': df = df.drop(columns=['scenario_name']) @@ -753,19 +887,214 @@ def _update_cell_changes_in_db(self, db_cell_updates: List[DbCellUpdate], connec for db_cell_change in db_cell_updates: self._update_cell_change_in_db(db_cell_change, connection) - def _update_cell_change_in_db(self, db_cell_update: DbCellUpdate, connection=None): + def _update_cell_change_in_db(self, db_cell_update: DbCellUpdate, connection): """Update a single value (cell) change in the DB.""" - db_table_name = self.db_tables[db_cell_update.table_name].db_table_name - column_change = f"{db_cell_update.column_name} = '{db_cell_update.current_value}'" - scenario_condition = f"scenario_name = '{db_cell_update.scenario_name}'" - pk_conditions = ' AND '.join([f"{pk['column']} = '{pk['value']}'" for pk in db_cell_update.row_index]) - sql = f"UPDATE {db_table_name} SET {column_change} WHERE {pk_conditions} AND {scenario_condition};" + # db_table_name = self.db_tables[db_cell_update.table_name].db_table_name + # column_change = f"{db_cell_update.column_name} = '{db_cell_update.current_value}'" + # scenario_condition = f"scenario_name = '{db_cell_update.scenario_name}'" + # pk_conditions = ' AND '.join([f"{pk['column']} = '{pk['value']}'" for pk in db_cell_update.row_index]) + # old_sql = f"UPDATE {db_table_name} SET {column_change} WHERE {pk_conditions} AND {scenario_condition};" + + db_table: ScenarioDbTable = self.db_tables[db_cell_update.table_name] + t: sqlalchemy.Table = db_table.get_sa_table() + pk_conditions = [(db_table.get_sa_column(pk['column']) == pk['value']) for pk in db_cell_update.row_index] + target_col: sqlalchemy.Column = db_table.get_sa_column(db_cell_update.column_name) + sql = t.update().where(sqlalchemy.and_((t.c.scenario_name == db_cell_update.scenario_name), *pk_conditions)).values({target_col:db_cell_update.current_value}) # print(f"_update_cell_change_in_db = {sql}") - if connection is None: - self.engine.execute(sql) + + connection.execute(sql) + + ############################################################################################ + # CRUD operations on scenarios in DB: + # - Delete scenario + # - Duplicate scenario + # - Rename scenario + ############################################################################################ + def delete_scenario_from_db(self, scenario_name: str): + """Delete a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Delete scenario within a transaction") + with self.engine.begin() as connection: + self._delete_scenario_from_db(scenario_name=scenario_name, connection=connection) else: - connection.execute(sql) + self._delete_scenario_from_db(scenario_name=scenario_name, connection=self.engine) + + ########################################################## + def duplicate_scenario_in_db(self, source_scenario_name: str, target_scenario_name: str): + """Duplicate a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Duplicate scenario within a transaction") + with self.engine.begin() as connection: + self._duplicate_scenario_in_db(connection, source_scenario_name, target_scenario_name) + else: + self._duplicate_scenario_in_db(self.engine, source_scenario_name, target_scenario_name) + + def _duplicate_scenario_in_db(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """Is fully done in DB using SQL in one SQL execute statement + :param source_scenario_name: + :param target_scenario_name: + :param connection: + :return: + """ + if target_scenario_name is None: + new_scenario_name = self._find_free_duplicate_scenario_name(source_scenario_name) + elif self._check_free_scenario_name(target_scenario_name): + new_scenario_name = target_scenario_name + else: + raise ValueError(f"Target name for duplicate scenario '{target_scenario_name}' already exists.") + + # inputs, outputs = self.read_scenario_from_db(source_scenario_name) + # self._replace_scenario_in_db_transaction(scenario_name=new_scenario_name, inputs=inputs, outputs=outputs, + # bulk=True, connection=connection) + self._duplicate_scenario_in_db_sql(connection, source_scenario_name, new_scenario_name) + + def _duplicate_scenario_in_db_sql(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """ + :param source_scenario_name: + :param target_scenario_name: + :param connection: + :return: + + See https://stackoverflow.com/questions/9879830/select-modify-and-insert-into-the-same-table + + Problem: the table Parameter/parameters has a column 'value' (lower-case). + Almost all of the column names in the DFs are lower-case, as are the column names in the ScenarioDbTable. + Typically, the DB schema converts that the upper-case column names in the DB. + But probably because 'VALUE' is a reserved word, it does NOT do this for 'value'. But that means in order to refer to this column in SQL, + one needs to put "value" between double quotes. + Problem is that you CANNOT do that for other columns, since these are in upper-case in the DB. + Note that the kpis table uses upper case 'VALUE' and that seems to work fine + + Resolution: use SQLAlchemy to construct the SQL. Do NOT create SQL expressions by text manipulation. + SQLAlchemy has the smarts to properly deal with these complex names. + """ + if target_scenario_name is None: + new_scenario_name = self._find_free_duplicate_scenario_name(source_scenario_name) + elif self._check_free_scenario_name(target_scenario_name): + new_scenario_name = target_scenario_name + else: + raise ValueError(f"Target name for duplicate scenario '{target_scenario_name}' already exists.") + + batch_sql=False # BEWARE: batch = True does NOT work! + sql_statements = [] + + # 1. Insert scenario in scenario table + # sql_insert = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{new_scenario_name}')" # Old SQL + # sa_scenario_table = list(self.input_db_tables.values())[0].get_sa_table() # Scenario table must be the first + sa_scenario_table = self.get_scenario_db_table().get_sa_table() + sql_insert = sa_scenario_table.insert().values(scenario_name = new_scenario_name) + # print(f"_duplicate_scenario_in_db_sql - Insert SQL = {sql_insert}") + if batch_sql: + sql_statements.append(sql_insert) + else: + connection.execute(sql_insert) + + # 2. Do 'insert into select' to duplicate rows in each table + for scenario_table_name, db_table in self.db_tables.items(): + if scenario_table_name == 'Scenario': + continue + + t: sqlalchemy.table = db_table.table_metadata # The table at hand + s: sqlalchemy.table = sa_scenario_table # The scenario table + # print("+++++++++++SQLAlchemy insert-select") + select_columns = [s.c.scenario_name if c.name == 'scenario_name' else c for c in t.columns] # Replace the t.c.scenario_name with s.c.scenario_name, so we get the new value + # print(f"select columns = {select_columns}") + select_sql = (sqlalchemy.select(select_columns) + .where(sqlalchemy.and_(t.c.scenario_name == source_scenario_name, s.c.scenario_name == target_scenario_name))) + target_columns = [c for c in t.columns] + sql_insert = t.insert().from_select(target_columns, select_sql) + # print(f"sql_insert = {sql_insert}") + + # sql_insert = f"INSERT INTO {db_table.db_table_name} ({target_columns_txt}) SELECT '{target_scenario_name}',{other_source_columns_txt} FROM {db_table.db_table_name} WHERE scenario_name = '{source_scenario_name}'" + if batch_sql: + sql_statements.append(sql_insert) + else: + connection.execute(sql_insert) + if batch_sql: + batch_sql = ";\n".join(sql_statements) + print(batch_sql) + connection.execute(batch_sql) + + def _find_free_duplicate_scenario_name(self, scenario_name: str, scenarios_df=None) -> Optional[str]: + """Finds next free scenario name based on pattern '{scenario_name}_copy_n'. + Will try at maximum 20 attempts. + """ + max_num_attempts = 20 + for i in range(1, max_num_attempts + 1): + new_name = f"{scenario_name}({i})" + free = self._check_free_scenario_name(new_name, scenarios_df) + if free: + return new_name + raise ValueError(f"Cannot find free name for duplicate scenario. Tried {max_num_attempts}. Last attempt = {new_name}. Rename scenarios.") + return None + + def _check_free_scenario_name(self, scenario_name, scenarios_df=None) -> bool: + if scenarios_df is None: + scenarios_df = self.get_scenarios_df() + free = (False if scenario_name in scenarios_df.index else True) + return free + + ############################################## + def rename_scenario_in_db(self, source_scenario_name: str, target_scenario_name: str): + """Rename a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Rename scenario within a transaction") + with self.engine.begin() as connection: + # self._rename_scenario_in_db(source_scenario_name, target_scenario_name, connection=connection) + self._rename_scenario_in_db_sql(connection, source_scenario_name, target_scenario_name) + else: + # self._rename_scenario_in_db(source_scenario_name, target_scenario_name) + self._rename_scenario_in_db_sql(self.engine, source_scenario_name, target_scenario_name) + + def _rename_scenario_in_db_sql(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """Rename scenario. + Uses 2 steps: + 1. Duplicate scenario + 2. Delete source scenario. + + Problem is that we use scenario_name as a primary key. You should not change the value of primary keys in a DB. + Instead, first copy the data using a new scenario_name, i.e. duplicate a scenario. Next, delete the original scenario. + Long-term solution: use a scenario_seq sequence key as the PK. With scenario_name as a ordinary column in the scenario table. + + Use of 'insert into select': https://stackoverflow.com/questions/9879830/select-modify-and-insert-into-the-same-table + """ + # 1. Duplicate scenario + self._duplicate_scenario_in_db_sql(connection, source_scenario_name, target_scenario_name) + # 2. Delete scenario + self._delete_scenario_from_db(source_scenario_name, connection=connection) + + def _delete_scenario_from_db(self, scenario_name: str, connection): + """Deletes all rows associated with a given scenario. + Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! + Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. + Also deletes entry in scenario table + Uses SQLAlchemy syntax to generate SQL + TODO: check with 'auto-inserted' tables + TODO: batch all sql statements in single execute. Faster? And will that do the defer integrity checks? + """ + batch_sql=False # Batch=True does NOT work! + insp = sqlalchemy.inspect(connection) + tables_in_db = insp.get_table_names(schema=self.schema) + sql_statements = [] + for scenario_table_name, db_table in reversed(self.db_tables.items()): # Note this INCLUDES the SCENARIO table! + if db_table.db_table_name in tables_in_db: + # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + t = db_table.table_metadata # A Table() + sql = t.delete().where(t.c.scenario_name == scenario_name) + if batch_sql: + sql_statements.append(sql) + else: + connection.execute(sql) + + # Because the scenario table has already been included in above loop, no need to do separately + # Delete scenario entry in scenario table: + # sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" + # sql_statements.append(sql) + if batch_sql: + batch_sql = ";\n".join(sql_statements) + # print(batch_sql) + connection.execute(batch_sql) ############################################################################################ # Old Read scenario APIs @@ -933,7 +1262,9 @@ def read_scenario_tables_from_db(self, scenario_name: str, def read_scenarios_from_db(self, scenario_names: List[str] = []) -> (Inputs, Outputs): """Multi scenario load. - Reads all tables from set of scenarios""" + Reads all tables from set of scenarios + TODO: avoid use of text SQL. Use SQLAlchemy sql generation. + """ where_scenarios = ','.join([f"'{n}'" for n in scenario_names]) inputs = {} From 3749e4caeb7db2b77755d64923324b3ecf5db331 Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Tue, 11 Jan 2022 00:19:15 -0500 Subject: [PATCH 4/7] read_scenario_tables_from_db Connection handling cleanup More sql via SQLAlchemy --- CHANGELOG.md | 1 + dse_do_utils/scenariodbmanager.py | 152 +++++++++++++++++------------- 2 files changed, 90 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3679a24..d3b5c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased]## [0.5.4.0b] ### Changed - ScenarioDbManager - Converted text SQL operations to SQLAlchemy operations to support any column-name (i.e. lower, upper, mixed, reserved words) +- Updated ScenarioDbManager.read_scenario_tables_from_db to selectively read tables from a scenario ### Added - ScenarioDbManager - Edit cells in tables - ScenarioDbManager - Duplicate, Rename and Delete scenario diff --git a/dse_do_utils/scenariodbmanager.py b/dse_do_utils/scenariodbmanager.py index ab0e30f..adc33f2 100644 --- a/dse_do_utils/scenariodbmanager.py +++ b/dse_do_utils/scenariodbmanager.py @@ -443,9 +443,9 @@ def create_schema(self): with self.engine.begin() as connection: self._create_schema_transaction(connection=connection) else: - self._create_schema_transaction() + self._create_schema_transaction(self.engine) - def _create_schema_transaction(self, connection=None): + def _create_schema_transaction(self, connection): """(Re)creates a schema, optionally using a transaction Drops all tables and re-creates the schema in the DB.""" # if self.schema is None: @@ -454,10 +454,7 @@ def _create_schema_transaction(self, connection=None): # self.drop_schema_transaction(self.schema) # DROP SCHEMA isn't working properly, so back to dropping all tables self._drop_all_tables_transaction(connection=connection) - if connection is None: - self.metadata.create_all(self.engine, checkfirst=True) - else: - self.metadata.create_all(connection, checkfirst=True) + self.metadata.create_all(connection, checkfirst=True) def drop_all_tables(self): """Drops all tables in the current schema.""" @@ -465,9 +462,9 @@ def drop_all_tables(self): with self.engine.begin() as connection: self._drop_all_tables_transaction(connection=connection) else: - self._drop_all_tables_transaction() + self._drop_all_tables_transaction(self.engine) - def _drop_all_tables_transaction(self, connection=None): + def _drop_all_tables_transaction(self, connection): """Drops all tables as defined in db_tables (if exists) TODO: loop over tables as they exist in the DB. This will make sure that however the schema definition has changed, all tables will be cleared. @@ -478,15 +475,18 @@ def _drop_all_tables_transaction(self, connection=None): However, the order is alphabetically, which causes FK constraint violation Weirdly, this happens in SQLite, not in DB2! With or without transactions + + TODO: + 1. Use SQLAlchemy to drop table, avoid text SQL + 2. Drop all tables without having to loop and know all tables + See: https://stackoverflow.com/questions/35918605/how-to-delete-a-table-in-sqlalchemy) + See https://docs.sqlalchemy.org/en/14/core/metadata.html#sqlalchemy.schema.MetaData.drop_all """ for scenario_table_name, db_table in reversed(self.db_tables.items()): db_table_name = db_table.db_table_name sql = f"DROP TABLE IF EXISTS {db_table_name}" # print(f"Dropping table {db_table_name}") - if connection is None: - r = self.engine.execute(sql) - else: - r = connection.execute(sql) + connection.execute(sql) def _drop_schema_transaction(self, schema: str, connection=None): """NOT USED. Not working in DB2 Cloud. @@ -494,6 +494,7 @@ def _drop_schema_transaction(self, schema: str, connection=None): See: https://www.ibm.com/docs/en/db2/11.5?topic=procedure-admin-drop-schema-drop-schema However, this doesn't work on DB2 cloud. TODO: find out if and how we can get this to work. + See https://docs.sqlalchemy.org/en/14/core/metadata.html#sqlalchemy.schema.MetaData.drop_all """ # sql = f"DROP SCHEMA {schema} CASCADE" # Not allowed in DB2! sql = f"CALL SYSPROC.ADMIN_DROP_SCHEMA('{schema}', NULL, 'ERRORSCHEMA', 'ERRORTABLE')" @@ -546,12 +547,12 @@ def replace_scenario_in_db(self, scenario_name: str, inputs: Inputs = {}, output if self.enable_transactions: print("Replacing scenario within transaction") with self.engine.begin() as connection: - self._replace_scenario_in_db_transaction(scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk, connection=connection) + self._replace_scenario_in_db_transaction(connection, scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk) else: - self._replace_scenario_in_db_transaction(scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk) + self._replace_scenario_in_db_transaction(self.engine, scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk) - def _replace_scenario_in_db_transaction(self, scenario_name: str, inputs: Inputs = {}, outputs: Outputs = {}, - bulk: bool = True, connection=None): + def _replace_scenario_in_db_transaction(self, connection, scenario_name: str, inputs: Inputs = {}, outputs: Outputs = {}, + bulk: bool = True): """Replace a single full scenario in the DB. If doesn't exist, will insert. Only inserts tables with an entry defined in self.db_tables (i.e. no `auto_insert`). Will first delete all rows associated with a scenario_name. @@ -567,11 +568,10 @@ def _replace_scenario_in_db_transaction(self, scenario_name: str, inputs: Inputs inputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, inputs) outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) # Step 3: insert scenario_name in scenario table - sql = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{scenario_name}')" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) + # sql = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{scenario_name}')" + sa_scenario_table = self.get_scenario_db_table().get_sa_table() + sql_insert = sa_scenario_table.insert().values(scenario_name = scenario_name) + connection.execute(sql_insert) # Step 4: (bulk) insert scenario num_caught_exceptions = self._insert_single_scenario_tables_in_db(inputs=inputs, outputs=outputs, bulk=bulk, connection=connection) # Throw exception if any exceptions caught in 'non-bulk' mode @@ -579,27 +579,32 @@ def _replace_scenario_in_db_transaction(self, scenario_name: str, inputs: Inputs if num_caught_exceptions > 0: raise RuntimeError(f"Multiple ({num_caught_exceptions}) Integrity and/or Statement errors caught. See log. Raising exception to allow for rollback.") - def _delete_scenario_from_db(self, scenario_name: str, connection=None): - """Deletes all rows associated with a given scenario. - Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! - Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. - Also deletes entry in scenario table - """ - insp = sqlalchemy.inspect(self.engine) - for scenario_table_name, db_table in reversed(self.db_tables.items()): - if insp.has_table(db_table.db_table_name, schema=self.schema): - sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) - - # Delete scenario entry in scenario table: - sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) + # def _delete_scenario_from_db(self, scenario_name: str, connection=None): + # """Deletes all rows associated with a given scenario. + # Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! + # Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. + # Also deletes entry in scenario table + # """ + # insp = sqlalchemy.inspect(self.engine) + # for scenario_table_name, db_table in reversed(self.db_tables.items()): + # if insp.has_table(db_table.db_table_name, schema=self.schema): + # + # # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" + # t: sqlalchemy.Table = db_table.get_sa_table() # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # if connection is None: + # self.engine.execute(sql) + # else: + # connection.execute(sql) + # + # # Delete scenario entry in scenario table: + # # sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" + # t: sqlalchemy.Table = self.get_scenario_db_table().get_sa_table() # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # if connection is None: + # self.engine.execute(sql) + # else: + # connection.execute(sql) def _insert_single_scenario_tables_in_db(self, inputs: Inputs = {}, outputs: Outputs = {}, bulk: bool = True, connection=None) -> int: @@ -837,6 +842,47 @@ def _read_scenario_db_table_from_db_thread(self, scenario_table_name, db_table): return inputs, outputs + def read_scenario_tables_from_db(self, scenario_name: str, + input_table_names: List[str] = None, + output_table_names: List[str] = None) -> (Inputs, Outputs): + """Read selected set input and output tables from scenario. + If input_table_names/output_table_names contains a '*', then all input/output tables will be read. + If empty list or None, then no tables will be read. + """ + if self.enable_transactions: + with self.engine.begin() as connection: + inputs, outputs = self._read_scenario_tables_from_db(connection, scenario_name, input_table_names, output_table_names) + else: + inputs, outputs = self._read_scenario_tables_from_db(self.engine, scenario_name, input_table_names, output_table_names) + return inputs, outputs + + def _read_scenario_tables_from_db(self, connection, scenario_name: str, + input_table_names: List[str] = None, + output_table_names: List[str] = None) -> (Inputs, Outputs): + """Loads data for selected input and output tables. + If either list is names is ['*'], will load all tables as defined in db_tables configuration. + """ + if input_table_names is None: # load no tables by default + input_table_names = [] + elif '*' in input_table_names: + input_table_names = list(self.input_db_tables.keys()) + if 'Scenario' in input_table_names: input_table_names.remove('Scenario') # Remove the scenario table + + if output_table_names is None: # load no tables by default + output_table_names = [] + elif '*' in output_table_names: + output_table_names = self.output_db_tables.keys() + + inputs = {} + for scenario_table_name, db_table in self.input_db_tables.items(): + if scenario_table_name in input_table_names: + inputs[scenario_table_name] = self._read_scenario_table_from_db(scenario_name, db_table, connection=connection) + outputs = {} + for scenario_table_name, db_table in self.output_db_tables.items(): + if scenario_table_name in output_table_names: + outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + return inputs, outputs + # def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable) -> pd.DataFrame: # """Read one table from the DB. # Removes the `scenario_name` column.""" @@ -856,10 +902,8 @@ def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: Scenario db_table_name = db_table.db_table_name # sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" # Old # db_table.table_metadata is a Table() - t = db_table.table_metadata + t: sqlalchemy.Table = db_table.get_sa_table() #table_metadata sql = t.select().where(t.c.scenario_name == scenario_name) # This is NOT a simple string! - # print(f"_read_scenario_db_table_from_db SQL = {sql}") - # df = pd.read_sql(sql, con=self.engine) df = pd.read_sql(sql, con=connection) if db_table_name != 'scenario': df = df.drop(columns=['scenario_name']) @@ -1240,25 +1284,7 @@ def read_scenario_tables_from_db_cached(self, scenario_name: str, ####################################################################################################### # Review ####################################################################################################### - def read_scenario_tables_from_db(self, scenario_name: str, - input_table_names: List[str] = None, - output_table_names: List[str] = None) -> (Inputs, Outputs): - """Loads data for selected input and output tables. - If either list is names is None, will load all tables as defined in db_tables configuration. - """ - if input_table_names is None: # load all tables by default - input_table_names = list(self.input_db_tables.keys()) - if 'Scenario' in input_table_names: input_table_names.remove('Scenario') # Remove the scenario table - if output_table_names is None: # load all tables by default - output_table_names = self.output_db_tables.keys() - inputs = {} - for scenario_table_name in input_table_names: - inputs[scenario_table_name] = self.read_scenario_table_from_db(scenario_name, scenario_table_name) - outputs = {} - for scenario_table_name in output_table_names: - outputs[scenario_table_name] = self.read_scenario_table_from_db(scenario_name, scenario_table_name) - return inputs, outputs def read_scenarios_from_db(self, scenario_names: List[str] = []) -> (Inputs, Outputs): """Multi scenario load. From 1edd7386a875ca684d9c83cac1c37f08c9800b8a Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Tue, 11 Jan 2022 16:38:23 -0500 Subject: [PATCH 5/7] read-input-tables and replace-output-tables APIs --- CHANGELOG.md | 2 + dse_do_utils/scenariodbmanager.py | 98 ++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b5c3a..82b89b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - ScenarioDbManager - Edit cells in tables - ScenarioDbManager - Duplicate, Rename and Delete scenario +- ScenarioDbManager.read_scenario_input_tables_from_db main API to read input for solve +- ScenarioDbManager.update_scenario_output_tables_in_db main API to store solve output ## [0.5.3.1] - 2021-12-30 ### Changed diff --git a/dse_do_utils/scenariodbmanager.py b/dse_do_utils/scenariodbmanager.py index adc33f2..79d89f4 100644 --- a/dse_do_utils/scenariodbmanager.py +++ b/dse_do_utils/scenariodbmanager.py @@ -147,6 +147,15 @@ def insert_table_in_db_bulk(self, df: pd.DataFrame, mgr, connection=None): print(f"DataFrame insert/append of table '{table_name}'") print(e) + def _delete_scenario_table_from_db(self, scenario_name, connection): + """Delete all rows associated with the scenario in the DB table. + Beware: make sure this is done in the right 'inverse cascading' order to avoid FK violations. + """ + # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + t = self.get_sa_table() # A Table() + sql = t.delete().where(t.c.scenario_name == scenario_name) + connection.execute(sql) + @staticmethod def sqlcol(df: pd.DataFrame) -> Dict: dtypedict = {} @@ -842,9 +851,14 @@ def _read_scenario_db_table_from_db_thread(self, scenario_table_name, db_table): return inputs, outputs + def read_scenario_input_tables_from_db(self, scenario_name: str): + """Convenience method to load all input tables. + Typically used at start if optimization model.""" + return self.read_scenario_tables_from_db(scenario_name, input_table_names=['*']) + def read_scenario_tables_from_db(self, scenario_name: str, - input_table_names: List[str] = None, - output_table_names: List[str] = None) -> (Inputs, Outputs): + input_table_names: Optional[List[str]] = None, + output_table_names: Optional[List[str]] = None) -> (Inputs, Outputs): """Read selected set input and output tables from scenario. If input_table_names/output_table_names contains a '*', then all input/output tables will be read. If empty list or None, then no tables will be read. @@ -948,6 +962,61 @@ def _update_cell_change_in_db(self, db_cell_update: DbCellUpdate, connection): connection.execute(sql) + ############################################################################################ + # Update/Replace tables in scenario + ############################################################################################ + def update_scenario_output_tables_in_db(self, scenario_name, outputs: Outputs): + """Main API to update output from a DO solve in the scenario. + Deletes ALL output tables. Then inserts the given set of tables. + Since this only touches the output tables, more efficient than replacing the whole scenario.""" + if self.enable_transactions: + with self.engine.begin() as connection: + self._update_scenario_output_tables_in_db(scenario_name, outputs, connection) + else: + self._update_scenario_output_tables_in_db(scenario_name, outputs, self.engine) + + def _update_scenario_output_tables_in_db(self, scenario_name, outputs: Outputs, connection): + """Deletes ALL output tables. Then inserts the given set of tables. + Note that if a defined output table is not included in the outputs, it will still be deleted from the scenario data.""" + # 1. Add scenario name to dfs: + outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) + # 2. Delete all output tables + for scenario_table_name, db_table in reversed(self.output_db_tables.items()): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario'): + db_table._delete_scenario_table_from_db() + # 3. Insert new data + for scenario_table_name, db_table in self.output_db_tables.items(): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in outputs.keys(): # If in given set of tables to replace + df = outputs[scenario_table_name] + db_table.insert_table_in_db_bulk(df=df, mgr=self, connection=connection) # The scenario_name is a column in the df + + def replace_scenario_tables_in_db(self, scenario_name, inputs={}, outputs={}): + """Untested""" + if self.enable_transactions: + with self.engine.begin() as connection: + self._replace_scenario_tables_in_db(connection, scenario_name, inputs, outputs) + else: + self._replace_scenario_tables_in_db(self.engine, scenario_name, inputs, outputs) + + def _replace_scenario_tables_in_db(self, connection, scenario_name, inputs={}, outputs={}): + """Untested + Replace only the tables listed in the inputs and outputs. But leave all other tables untouched. + Will first delete all given tables (in reverse cascading order), then insert the new ones (in cascading order)""" + + # Add scenario name to dfs: + inputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, inputs) + outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) + dfs = {**inputs, **outputs} + # 1. Delete tables + for scenario_table_name, db_table in reversed(self.db_tables.items()): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in dfs.keys(): # If in given set of tables to replace + db_table._delete_scenario_table_from_db() + # 2. Insert new data + for scenario_table_name, db_table in self.db_tables.items(): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in dfs.keys(): # If in given set of tables to replace + df = dfs[scenario_table_name] + db_table.insert_table_in_db_bulk(df=df, mgr=self, connection=connection) # The scenario_name is a column in the df + ############################################################################################ # CRUD operations on scenarios in DB: # - Delete scenario @@ -1117,28 +1186,17 @@ def _delete_scenario_from_db(self, scenario_name: str, connection): TODO: check with 'auto-inserted' tables TODO: batch all sql statements in single execute. Faster? And will that do the defer integrity checks? """ - batch_sql=False # Batch=True does NOT work! + # batch_sql=False # Batch=True does NOT work! insp = sqlalchemy.inspect(connection) tables_in_db = insp.get_table_names(schema=self.schema) - sql_statements = [] + # sql_statements = [] for scenario_table_name, db_table in reversed(self.db_tables.items()): # Note this INCLUDES the SCENARIO table! if db_table.db_table_name in tables_in_db: - # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old - t = db_table.table_metadata # A Table() - sql = t.delete().where(t.c.scenario_name == scenario_name) - if batch_sql: - sql_statements.append(sql) - else: - connection.execute(sql) - - # Because the scenario table has already been included in above loop, no need to do separately - # Delete scenario entry in scenario table: - # sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" - # sql_statements.append(sql) - if batch_sql: - batch_sql = ";\n".join(sql_statements) - # print(batch_sql) - connection.execute(batch_sql) + # # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + # t = db_table.table_metadata # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # connection.execute(sql) + db_table._delete_scenario_table_from_db(scenario_name, connection) ############################################################################################ # Old Read scenario APIs From aac854779fe4ae05b83c9a3f34160a94df858f3f Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Tue, 11 Jan 2022 16:52:59 -0500 Subject: [PATCH 6/7] Fixed bug --- dse_do_utils/scenariodbmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dse_do_utils/scenariodbmanager.py b/dse_do_utils/scenariodbmanager.py index 79d89f4..472ed3c 100644 --- a/dse_do_utils/scenariodbmanager.py +++ b/dse_do_utils/scenariodbmanager.py @@ -983,7 +983,7 @@ def _update_scenario_output_tables_in_db(self, scenario_name, outputs: Outputs, # 2. Delete all output tables for scenario_table_name, db_table in reversed(self.output_db_tables.items()): # Note this INCLUDES the SCENARIO table! if (scenario_table_name != 'Scenario'): - db_table._delete_scenario_table_from_db() + db_table._delete_scenario_table_from_db(scenario_name, connection) # 3. Insert new data for scenario_table_name, db_table in self.output_db_tables.items(): # Note this INCLUDES the SCENARIO table! if (scenario_table_name != 'Scenario') and db_table.db_table_name in outputs.keys(): # If in given set of tables to replace From bfb26d6fcf0061b8124690498a6b25da57996323 Mon Sep 17 00:00:00 2001 From: vjterpstra Date: Tue, 11 Jan 2022 16:59:31 -0500 Subject: [PATCH 7/7] Release v0.5.4.0 --- CHANGELOG.md | 4 +- VersioningReadMe.md | 2 +- docs/doc_build/doctrees/dse_do_utils.doctree | Bin 924728 -> 970775 bytes docs/doc_build/doctrees/environment.pickle | Bin 341922 -> 375680 bytes docs/doc_build/html/.buildinfo | 2 +- .../doc_build/html/_modules/dse_do_utils.html | 6 +- .../_modules/dse_do_utils/cpd25utilities.html | 6 +- .../_modules/dse_do_utils/datamanager.html | 6 +- .../dse_do_utils/deployeddomodel.html | 6 +- .../dse_do_utils/deployeddomodelcpd21.html | 6 +- .../dse_do_utils/domodelexporter.html | 6 +- .../_modules/dse_do_utils/mapmanager.html | 6 +- .../dse_do_utils/multiscenariomanager.html | 6 +- .../dse_do_utils/optimizationengine.html | 6 +- .../_modules/dse_do_utils/plotlymanager.html | 6 +- .../dse_do_utils/scenariodbmanager.html | 620 +++++++++++++++--- .../dse_do_utils/scenariomanager.html | 6 +- .../_modules/dse_do_utils/scenariopicker.html | 6 +- .../html/_modules/dse_do_utils/utilities.html | 6 +- docs/doc_build/html/_modules/index.html | 6 +- docs/doc_build/html/_static/bizstyle.js | 2 +- .../html/_static/documentation_options.js | 2 +- docs/doc_build/html/dse_do_utils.html | 142 +++- docs/doc_build/html/genindex.html | 63 +- docs/doc_build/html/index.html | 6 +- docs/doc_build/html/modules.html | 6 +- docs/doc_build/html/objects.inv | Bin 2175 -> 2305 bytes docs/doc_build/html/py-modindex.html | 6 +- docs/doc_build/html/readme_link.html | 6 +- docs/doc_build/html/search.html | 6 +- docs/doc_build/html/searchindex.js | 2 +- dse_do_utils/version.py | 2 +- 32 files changed, 793 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b89b3..1c7e835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased]## [0.5.4.0b] +## [Unreleased]## [0.5.4.1b] + +## [0.5.4.0] - 2022-01-11 ### Changed - ScenarioDbManager - Converted text SQL operations to SQLAlchemy operations to support any column-name (i.e. lower, upper, mixed, reserved words) - Updated ScenarioDbManager.read_scenario_tables_from_db to selectively read tables from a scenario diff --git a/VersioningReadMe.md b/VersioningReadMe.md index 5b7e88f..248f238 100644 --- a/VersioningReadMe.md +++ b/VersioningReadMe.md @@ -20,7 +20,7 @@ Note that if you added/removed modules, you first need to re-run the sphinx comm `python setup.py sdist bdist_wheel` 5. Upload to PyPI (from PyCharm terminal run):
-`make clean` +`twine upload dist/* --verbose` Enter username and password when prompted. (For TestPyPI use: `twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose`)
Before the twine upload, you can check the distribution with:
diff --git a/docs/doc_build/doctrees/dse_do_utils.doctree b/docs/doc_build/doctrees/dse_do_utils.doctree index 5d4dc5e1673f92a7089e58092d56074e58b6fb9a..f87f1ddfde8cfe63d17cbb3c743f706b22f9ee24 100644 GIT binary patch delta 84240 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 V2xD3b=D0icpOo;x=d5eZ{|^z|Y1RM$ delta 27468 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 Cp=J;O diff --git a/docs/doc_build/html/.buildinfo b/docs/doc_build/html/.buildinfo index 7ff3c5f..e47c0e5 100644 --- a/docs/doc_build/html/.buildinfo +++ b/docs/doc_build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: effd2c6945fa34906f8d2c06e4196c89 +config: bd2b9a92071f1c40e294c4bb1fb76baa tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/doc_build/html/_modules/dse_do_utils.html b/docs/doc_build/html/_modules/dse_do_utils.html index e8eb574..ccdd09f 100644 --- a/docs/doc_build/html/_modules/dse_do_utils.html +++ b/docs/doc_build/html/_modules/dse_do_utils.html @@ -6,7 +6,7 @@ - dse_do_utils — DSE DO Utils 0.5.2.0 documentation + dse_do_utils — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

Navigation

  • modules |
  • - + @@ -169,7 +169,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/cpd25utilities.html b/docs/doc_build/html/_modules/dse_do_utils/cpd25utilities.html index d25f756..957b80f 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/cpd25utilities.html +++ b/docs/doc_build/html/_modules/dse_do_utils/cpd25utilities.html @@ -6,7 +6,7 @@ - dse_do_utils.cpd25utilities — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.cpd25utilities — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -211,7 +211,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/datamanager.html b/docs/doc_build/html/_modules/dse_do_utils/datamanager.html index dd24b20..88a952f 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/datamanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/datamanager.html @@ -6,7 +6,7 @@ - dse_do_utils.datamanager — DSE DO Utils 0.5.3.0 documentation + dse_do_utils.datamanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -437,7 +437,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/deployeddomodel.html b/docs/doc_build/html/_modules/dse_do_utils/deployeddomodel.html index 50eae7c..2adf821 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/deployeddomodel.html +++ b/docs/doc_build/html/_modules/dse_do_utils/deployeddomodel.html @@ -6,7 +6,7 @@ - dse_do_utils.deployeddomodel — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.deployeddomodel — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -325,7 +325,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/deployeddomodelcpd21.html b/docs/doc_build/html/_modules/dse_do_utils/deployeddomodelcpd21.html index 9c80a91..b67f48c 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/deployeddomodelcpd21.html +++ b/docs/doc_build/html/_modules/dse_do_utils/deployeddomodelcpd21.html @@ -6,7 +6,7 @@ - dse_do_utils.deployeddomodelcpd21 — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.deployeddomodelcpd21 — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -737,7 +737,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/domodelexporter.html b/docs/doc_build/html/_modules/dse_do_utils/domodelexporter.html index 86bca86..bb5d464 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/domodelexporter.html +++ b/docs/doc_build/html/_modules/dse_do_utils/domodelexporter.html @@ -6,7 +6,7 @@ - dse_do_utils.domodelexporter — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.domodelexporter — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -342,7 +342,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/mapmanager.html b/docs/doc_build/html/_modules/dse_do_utils/mapmanager.html index 300fd9a..1505952 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/mapmanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/mapmanager.html @@ -6,7 +6,7 @@ - dse_do_utils.mapmanager — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.mapmanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -333,7 +333,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/multiscenariomanager.html b/docs/doc_build/html/_modules/dse_do_utils/multiscenariomanager.html index 64c5ba5..1c7157f 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/multiscenariomanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/multiscenariomanager.html @@ -6,7 +6,7 @@ - dse_do_utils.multiscenariomanager — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.multiscenariomanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -331,7 +331,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/optimizationengine.html b/docs/doc_build/html/_modules/dse_do_utils/optimizationengine.html index 75dbc48..426a476 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/optimizationengine.html +++ b/docs/doc_build/html/_modules/dse_do_utils/optimizationengine.html @@ -6,7 +6,7 @@ - dse_do_utils.optimizationengine — DSE DO Utils 0.5.3.0 documentation + dse_do_utils.optimizationengine — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -350,7 +350,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/plotlymanager.html b/docs/doc_build/html/_modules/dse_do_utils/plotlymanager.html index 462b2b4..a30a29e 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/plotlymanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/plotlymanager.html @@ -6,7 +6,7 @@ - dse_do_utils.plotlymanager — DSE DO Utils 0.5.3.1 documentation + dse_do_utils.plotlymanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -118,7 +118,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/scenariodbmanager.html b/docs/doc_build/html/_modules/dse_do_utils/scenariodbmanager.html index 90cc008..e55ae79 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/scenariodbmanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/scenariodbmanager.html @@ -6,7 +6,7 @@ - dse_do_utils.scenariodbmanager — DSE DO Utils 0.5.3.1 documentation + dse_do_utils.scenariodbmanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -65,10 +65,11 @@

    Source code for dse_do_utils.scenariodbmanager

    # - Make 'multi_scenario' the default option # ----------------------------------------------------------------------------------- from abc import ABC +from multiprocessing.pool import ThreadPool import sqlalchemy import pandas as pd -from typing import Dict, List +from typing import Dict, List, NamedTuple, Any, Optional from collections import OrderedDict import re from sqlalchemy import exc @@ -108,6 +109,7 @@

    Source code for dse_do_utils.scenariodbmanager

    reserved_table_names = ['order', 'parameter'] # TODO: add more reserved words for table names if db_table_name in reserved_table_names: print(f"Warning: the db_table_name '{db_table_name}' is a reserved word. Do not use as table name.") + self._sa_column_by_name = None # Dict[str, sqlalchemy.Column] Will be generated dynamically first time it is needed.

    [docs] def get_db_table_name(self) -> str: return self.db_table_name
    @@ -120,7 +122,21 @@

    Source code for dse_do_utils.scenariodbmanager

    column_names.append(c.name) return column_names

    -
    [docs] def create_table_metadata(self, metadata, multi_scenario: bool = False): +
    [docs] def get_sa_table(self) -> sqlalchemy.Table: + """Returns the SQLAlchemy Table""" + return self.table_metadata
    + +
    [docs] def get_sa_column(self, db_column_name) -> Optional[sqlalchemy.Column]: + """Returns the SQLAlchemy column with the specified name. + Dynamically creates a dict/hashtable for more efficient access.""" + # for c in self.columns_metadata: + # if isinstance(c, sqlalchemy.Column) and c.name == db_column_name: + # return c + if self._sa_column_by_name is None: + self._sa_column_by_name = {c.name: c for c in self.columns_metadata if isinstance(c, sqlalchemy.Column)} + return self._sa_column_by_name.get(db_column_name) # returns None if npt found (?)
    + +
    [docs] def create_table_metadata(self, metadata, multi_scenario: bool = False) -> sqlalchemy.Table: """If multi_scenario, then add a primary key 'scenario_name'.""" columns_metadata = self.columns_metadata constraints_metadata = self.constraints_metadata @@ -177,6 +193,15 @@

    Source code for dse_do_utils.scenariodbmanager

    print(f"DataFrame insert/append of table '{table_name}'") print(e)

    + def _delete_scenario_table_from_db(self, scenario_name, connection): + """Delete all rows associated with the scenario in the DB table. + Beware: make sure this is done in the right 'inverse cascading' order to avoid FK violations. + """ + # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + t = self.get_sa_table() # A Table() + sql = t.delete().where(t.c.scenario_name == scenario_name) + connection.execute(sql) +
    [docs] @staticmethod def sqlcol(df: pd.DataFrame) -> Dict: dtypedict = {} @@ -247,6 +272,16 @@

    Source code for dse_do_utils.scenariodbmanager

    print(e)

    +
    [docs]class DbCellUpdate(NamedTuple): + scenario_name: str + table_name: str + row_index: List[Dict[str, Any]] # e.g. [{'column': 'col1', 'value': 1}, {'column': 'col2', 'value': 'pear'}] + column_name: str + current_value: Any + previous_value: Any # Not used for DB operation + row_idx: int # Not used for DB operation
    + + ######################################################################### # ScenarioDbManager ######################################################################### @@ -302,6 +337,11 @@

    Source code for dse_do_utils.scenariodbmanager

    print("Warning: the `Scenario` table should be the first in the input tables") return input_db_tables +

    [docs] def get_scenario_db_table(self) -> ScenarioDbTable: + """Scenario table must be the first in self.input_db_tables""" + db_table: ScenarioTable = list(self.input_db_tables.values())[0] + return db_table
    + def _create_database_engine(self, credentials=None, schema: str = None, echo: bool = False): """Creates a SQLAlchemy engine at initialization. If no credentials, creates an in-memory SQLite DB. Which can be used for schema validation of the data. @@ -458,9 +498,9 @@

    Source code for dse_do_utils.scenariodbmanager

    with self.engine.begin() as connection: self._create_schema_transaction(connection=connection) else: - self._create_schema_transaction()

    + self._create_schema_transaction(self.engine)
    - def _create_schema_transaction(self, connection=None): + def _create_schema_transaction(self, connection): """(Re)creates a schema, optionally using a transaction Drops all tables and re-creates the schema in the DB.""" # if self.schema is None: @@ -469,10 +509,7 @@

    Source code for dse_do_utils.scenariodbmanager

    # self.drop_schema_transaction(self.schema) # DROP SCHEMA isn't working properly, so back to dropping all tables self._drop_all_tables_transaction(connection=connection) - if connection is None: - self.metadata.create_all(self.engine, checkfirst=True) - else: - self.metadata.create_all(connection, checkfirst=True) + self.metadata.create_all(connection, checkfirst=True)

    [docs] def drop_all_tables(self): """Drops all tables in the current schema.""" @@ -480,9 +517,9 @@

    Source code for dse_do_utils.scenariodbmanager

    with self.engine.begin() as connection: self._drop_all_tables_transaction(connection=connection) else: - self._drop_all_tables_transaction()

    + self._drop_all_tables_transaction(self.engine)
    - def _drop_all_tables_transaction(self, connection=None): + def _drop_all_tables_transaction(self, connection): """Drops all tables as defined in db_tables (if exists) TODO: loop over tables as they exist in the DB. This will make sure that however the schema definition has changed, all tables will be cleared. @@ -493,15 +530,18 @@

    Source code for dse_do_utils.scenariodbmanager

    However, the order is alphabetically, which causes FK constraint violation Weirdly, this happens in SQLite, not in DB2! With or without transactions + + TODO: + 1. Use SQLAlchemy to drop table, avoid text SQL + 2. Drop all tables without having to loop and know all tables + See: https://stackoverflow.com/questions/35918605/how-to-delete-a-table-in-sqlalchemy) + See https://docs.sqlalchemy.org/en/14/core/metadata.html#sqlalchemy.schema.MetaData.drop_all """ for scenario_table_name, db_table in reversed(self.db_tables.items()): db_table_name = db_table.db_table_name sql = f"DROP TABLE IF EXISTS {db_table_name}" # print(f"Dropping table {db_table_name}") - if connection is None: - r = self.engine.execute(sql) - else: - r = connection.execute(sql) + connection.execute(sql) def _drop_schema_transaction(self, schema: str, connection=None): """NOT USED. Not working in DB2 Cloud. @@ -509,6 +549,7 @@

    Source code for dse_do_utils.scenariodbmanager

    See: https://www.ibm.com/docs/en/db2/11.5?topic=procedure-admin-drop-schema-drop-schema However, this doesn't work on DB2 cloud. TODO: find out if and how we can get this to work. + See https://docs.sqlalchemy.org/en/14/core/metadata.html#sqlalchemy.schema.MetaData.drop_all """ # sql = f"DROP SCHEMA {schema} CASCADE" # Not allowed in DB2! sql = f"CALL SYSPROC.ADMIN_DROP_SCHEMA('{schema}', NULL, 'ERRORSCHEMA', 'ERRORTABLE')" @@ -561,18 +602,20 @@

    Source code for dse_do_utils.scenariodbmanager

    if self.enable_transactions: print("Replacing scenario within transaction") with self.engine.begin() as connection: - self._replace_scenario_in_db_transaction(scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk, connection=connection) + self._replace_scenario_in_db_transaction(connection, scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk) else: - self._replace_scenario_in_db_transaction(scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk)

    + self._replace_scenario_in_db_transaction(self.engine, scenario_name=scenario_name, inputs=inputs, outputs=outputs, bulk=bulk)
    - def _replace_scenario_in_db_transaction(self, scenario_name: str, inputs: Inputs = {}, outputs: Outputs = {}, - bulk: bool = True, connection=None): + def _replace_scenario_in_db_transaction(self, connection, scenario_name: str, inputs: Inputs = {}, outputs: Outputs = {}, + bulk: bool = True): """Replace a single full scenario in the DB. If doesn't exist, will insert. Only inserts tables with an entry defined in self.db_tables (i.e. no `auto_insert`). Will first delete all rows associated with a scenario_name. Will set/overwrite the scenario_name in all dfs, so no need to add in advance. Assumes schema has been created. Note: there is no difference between dfs in inputs or outputs, i.e. they are inserted the same way. + + TODO: break-out in a delete and an insert. Then we can re-use the insert for the duplicate API """ # Step 1: delete scenario if exists self._delete_scenario_from_db(scenario_name, connection=connection) @@ -580,11 +623,10 @@

    Source code for dse_do_utils.scenariodbmanager

    inputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, inputs) outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) # Step 3: insert scenario_name in scenario table - sql = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{scenario_name}')" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) + # sql = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{scenario_name}')" + sa_scenario_table = self.get_scenario_db_table().get_sa_table() + sql_insert = sa_scenario_table.insert().values(scenario_name = scenario_name) + connection.execute(sql_insert) # Step 4: (bulk) insert scenario num_caught_exceptions = self._insert_single_scenario_tables_in_db(inputs=inputs, outputs=outputs, bulk=bulk, connection=connection) # Throw exception if any exceptions caught in 'non-bulk' mode @@ -592,34 +634,38 @@

    Source code for dse_do_utils.scenariodbmanager

    if num_caught_exceptions > 0: raise RuntimeError(f"Multiple ({num_caught_exceptions}) Integrity and/or Statement errors caught. See log. Raising exception to allow for rollback.") - def _delete_scenario_from_db(self, scenario_name: str, connection=None): - """Deletes all rows associated with a given scenario. - Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! - Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. - Also deletes entry in scenario table - TODO: do within one session/cursor, so we don't have to worry about the order of the delete? - """ - insp = sqlalchemy.inspect(self.engine) - for scenario_table_name, db_table in reversed(self.db_tables.items()): - if insp.has_table(db_table.db_table_name, schema=self.schema): - sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) - - # Delete scenario entry in scenario table: - sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" - if connection is None: - self.engine.execute(sql) - else: - connection.execute(sql) + # def _delete_scenario_from_db(self, scenario_name: str, connection=None): + # """Deletes all rows associated with a given scenario. + # Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! + # Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. + # Also deletes entry in scenario table + # """ + # insp = sqlalchemy.inspect(self.engine) + # for scenario_table_name, db_table in reversed(self.db_tables.items()): + # if insp.has_table(db_table.db_table_name, schema=self.schema): + # + # # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" + # t: sqlalchemy.Table = db_table.get_sa_table() # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # if connection is None: + # self.engine.execute(sql) + # else: + # connection.execute(sql) + # + # # Delete scenario entry in scenario table: + # # sql = f"DELETE FROM SCENARIO WHERE scenario_name = '{scenario_name}'" + # t: sqlalchemy.Table = self.get_scenario_db_table().get_sa_table() # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # if connection is None: + # self.engine.execute(sql) + # else: + # connection.execute(sql) def _insert_single_scenario_tables_in_db(self, inputs: Inputs = {}, outputs: Outputs = {}, bulk: bool = True, connection=None) -> int: """Specifically for single scenario replace/insert. Does NOT insert into the `scenario` table. - No `auto_insert`, i.e. only df matching db_tables. + No `auto_insert`, i.e. only df matching db_tables. TODO: verify if doesn't work with AutoScenarioDbTable """ num_caught_exceptions = 0 dfs = {**inputs, **outputs} # Combine all dfs in one dict @@ -712,13 +758,19 @@

    Source code for dse_do_utils.scenariodbmanager

    ############################################################################################ # Read scenario ############################################################################################ -

    [docs] def get_scenarios_df(self): +
    [docs] def get_scenarios_df(self) -> pd.DataFrame: """Return all scenarios in df. Result is indexed by `scenario_name`. Main API to get all scenarios. The API called by a cached procedure in the dse_do_dashboard.DoDashApp. """ - sql = f"SELECT * FROM SCENARIO" - df = pd.read_sql(sql, con=self.engine).set_index(['scenario_name']) + # sql = f"SELECT * FROM SCENARIO" + sa_scenario_table = list(self.input_db_tables.values())[0].table_metadata + sql = sa_scenario_table.select() + if self.enable_transactions: + with self.engine.begin() as connection: + df = pd.read_sql(sql, con=connection).set_index(['scenario_name']) + else: + df = pd.read_sql(sql, con=self.engine).set_index(['scenario_name']) return df
    [docs] def read_scenario_table_from_db(self, scenario_name: str, scenario_table_name: str) -> pd.DataFrame: @@ -737,36 +789,460 @@

    Source code for dse_do_utils.scenariodbmanager

    # error! raise ValueError(f"Scenario table name '{scenario_table_name}' unknown. Cannot load data from DB.") - df = self._read_scenario_db_table_from_db(scenario_name, db_table) + if self.enable_transactions: + with self.engine.begin() as connection: + df = self._read_scenario_db_table_from_db(scenario_name, db_table, connection) + else: + df = self._read_scenario_db_table_from_db(scenario_name, db_table, self.engine) return df

    -
    [docs] def read_scenario_from_db(self, scenario_name: str) -> (Inputs, Outputs): + # def read_scenario_from_db(self, scenario_name: str) -> (Inputs, Outputs): + # """Single scenario load. + # Main API to read a complete scenario. + # Reads all tables for a single scenario. + # Returns all tables in one dict""" + # inputs = {} + # for scenario_table_name, db_table in self.input_db_tables.items(): + # inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # + # outputs = {} + # for scenario_table_name, db_table in self.output_db_tables.items(): + # outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # + # return inputs, outputs + + + # def _read_scenario_from_db(self, scenario_name: str, connection) -> (Inputs, Outputs): + # """Single scenario load. + # Main API to read a complete scenario. + # Reads all tables for a single scenario. + # Returns all tables in one dict + # """ + # inputs = {} + # for scenario_table_name, db_table in self.input_db_tables.items(): + # # print(f"scenario_table_name = {scenario_table_name}") + # if scenario_table_name != 'Scenario': # Skip the Scenario table as an input + # inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + # + # outputs = {} + # for scenario_table_name, db_table in self.output_db_tables.items(): + # outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + # # if scenario_table_name == 'kpis': + # # # print(f"kpis table columns = {outputs[scenario_table_name].columns}") + # # outputs[scenario_table_name] = outputs[scenario_table_name].rename(columns={'name': 'NAME'}) #HACK!!!!! + # return inputs, outputs +
    [docs] def read_scenario_from_db(self, scenario_name: str, multi_threaded: bool = False) -> (Inputs, Outputs): """Single scenario load. Main API to read a complete scenario. Reads all tables for a single scenario. - Returns all tables in one dict""" + Returns all tables in one dict + + Note: multi_threaded doesn't seem to lead to performance improvement. + Fixed: omit reading scenario table as an input. + """ + # print(f"read_scenario_from_db.multi_threaded = {multi_threaded}") + if multi_threaded: + inputs, outputs = self._read_scenario_from_db_multi_threaded(scenario_name) + else: + if self.enable_transactions: + with self.engine.begin() as connection: + inputs, outputs = self._read_scenario_from_db(scenario_name, connection) + else: + inputs, outputs = self._read_scenario_from_db(scenario_name, self.engine) + return inputs, outputs
    + + def _read_scenario_from_db(self, scenario_name: str, connection) -> (Inputs, Outputs): + """Single scenario load. + Main API to read a complete scenario. + Reads all tables for a single scenario. + Returns all tables in one dict + """ inputs = {} for scenario_table_name, db_table in self.input_db_tables.items(): - inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + # print(f"scenario_table_name = {scenario_table_name}") + if scenario_table_name != 'Scenario': # Skip the Scenario table as an input + inputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) outputs = {} for scenario_table_name, db_table in self.output_db_tables.items(): - outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table) + outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + + return inputs, outputs + + def _read_scenario_from_db_multi_threaded(self, scenario_name) -> (Inputs, Outputs): + """Reads all tables from a scenario using multi-threading. + Does NOT seem to result in performance improvement!""" + class ReadTableFunction(object): + def __init__(self, dbm): + self.dbm = dbm + def __call__(self, scenario_table_name, db_table): + return self._read_scenario_db_table_from_db_thread(scenario_table_name, db_table) + def _read_scenario_db_table_from_db_thread(self, scenario_table_name, db_table): + with self.dbm.engine.begin() as connection: + df = self.dbm._read_scenario_db_table_from_db(scenario_name, db_table, connection) + dict = {scenario_table_name: df} + return dict + + thread_number = 8 + pool = ThreadPool(thread_number) + thread_worker = ReadTableFunction(self) + # print("ThreadPool created") + all_tables = [(scenario_table_name, db_table) for scenario_table_name, db_table in self.db_tables.items() if scenario_table_name != 'Scenario'] + # print(all_tables) + all_results = pool.starmap(thread_worker, all_tables) + inputs = {k:v for element in all_results for k,v in element.items() if k in self.input_db_tables.keys()} + outputs = {k:v for element in all_results for k,v in element.items() if k in self.output_db_tables.keys()} + # print("All tables loaded") + + return inputs, outputs + +
    [docs] def read_scenario_input_tables_from_db(self, scenario_name: str): + """Convenience method to load all input tables. + Typically used at start if optimization model.""" + return self.read_scenario_tables_from_db(scenario_name, input_table_names=['*'])
    +
    [docs] def read_scenario_tables_from_db(self, scenario_name: str, + input_table_names: Optional[List[str]] = None, + output_table_names: Optional[List[str]] = None) -> (Inputs, Outputs): + """Read selected set input and output tables from scenario. + If input_table_names/output_table_names contains a '*', then all input/output tables will be read. + If empty list or None, then no tables will be read. + """ + if self.enable_transactions: + with self.engine.begin() as connection: + inputs, outputs = self._read_scenario_tables_from_db(connection, scenario_name, input_table_names, output_table_names) + else: + inputs, outputs = self._read_scenario_tables_from_db(self.engine, scenario_name, input_table_names, output_table_names) return inputs, outputs
    - def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable) -> pd.DataFrame: + def _read_scenario_tables_from_db(self, connection, scenario_name: str, + input_table_names: List[str] = None, + output_table_names: List[str] = None) -> (Inputs, Outputs): + """Loads data for selected input and output tables. + If either list is names is ['*'], will load all tables as defined in db_tables configuration. + """ + if input_table_names is None: # load no tables by default + input_table_names = [] + elif '*' in input_table_names: + input_table_names = list(self.input_db_tables.keys()) + if 'Scenario' in input_table_names: input_table_names.remove('Scenario') # Remove the scenario table + + if output_table_names is None: # load no tables by default + output_table_names = [] + elif '*' in output_table_names: + output_table_names = self.output_db_tables.keys() + + inputs = {} + for scenario_table_name, db_table in self.input_db_tables.items(): + if scenario_table_name in input_table_names: + inputs[scenario_table_name] = self._read_scenario_table_from_db(scenario_name, db_table, connection=connection) + outputs = {} + for scenario_table_name, db_table in self.output_db_tables.items(): + if scenario_table_name in output_table_names: + outputs[scenario_table_name] = self._read_scenario_db_table_from_db(scenario_name, db_table, connection=connection) + return inputs, outputs + + # def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable) -> pd.DataFrame: + # """Read one table from the DB. + # Removes the `scenario_name` column.""" + # db_table_name = db_table.db_table_name + # sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" + # df = pd.read_sql(sql, con=self.engine) + # if db_table_name != 'scenario': + # df = df.drop(columns=['scenario_name']) + # + # return df + def _read_scenario_db_table_from_db(self, scenario_name: str, db_table: ScenarioDbTable, connection) -> pd.DataFrame: """Read one table from the DB. - Removes the `scenario_name` column.""" + Removes the `scenario_name` column. + + Modification: based on SQLAlchemy syntax. If doing the plain text SQL, then some column names not properly extracted + """ db_table_name = db_table.db_table_name - sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" - df = pd.read_sql(sql, con=self.engine) + # sql = f"SELECT * FROM {db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + # db_table.table_metadata is a Table() + t: sqlalchemy.Table = db_table.get_sa_table() #table_metadata + sql = t.select().where(t.c.scenario_name == scenario_name) # This is NOT a simple string! + df = pd.read_sql(sql, con=connection) if db_table_name != 'scenario': df = df.drop(columns=['scenario_name']) return df + ############################################################################################ + # Update scenario + ############################################################################################ +
    [docs] def update_cell_changes_in_db(self, db_cell_updates: List[DbCellUpdate]): + """Update a set of cells in the DB. + + :param db_cell_updates: + :return: + """ + if self.enable_transactions: + print("Update cells with transaction") + with self.engine.begin() as connection: + self._update_cell_changes_in_db(db_cell_updates, connection=connection) + else: + self._update_cell_changes_in_db(db_cell_updates)
    + + def _update_cell_changes_in_db(self, db_cell_updates: List[DbCellUpdate], connection=None): + """Update an ordered list of single value changes (cell) in the DB.""" + for db_cell_change in db_cell_updates: + self._update_cell_change_in_db(db_cell_change, connection) + + def _update_cell_change_in_db(self, db_cell_update: DbCellUpdate, connection): + """Update a single value (cell) change in the DB.""" + # db_table_name = self.db_tables[db_cell_update.table_name].db_table_name + # column_change = f"{db_cell_update.column_name} = '{db_cell_update.current_value}'" + # scenario_condition = f"scenario_name = '{db_cell_update.scenario_name}'" + # pk_conditions = ' AND '.join([f"{pk['column']} = '{pk['value']}'" for pk in db_cell_update.row_index]) + # old_sql = f"UPDATE {db_table_name} SET {column_change} WHERE {pk_conditions} AND {scenario_condition};" + + db_table: ScenarioDbTable = self.db_tables[db_cell_update.table_name] + t: sqlalchemy.Table = db_table.get_sa_table() + pk_conditions = [(db_table.get_sa_column(pk['column']) == pk['value']) for pk in db_cell_update.row_index] + target_col: sqlalchemy.Column = db_table.get_sa_column(db_cell_update.column_name) + sql = t.update().where(sqlalchemy.and_((t.c.scenario_name == db_cell_update.scenario_name), *pk_conditions)).values({target_col:db_cell_update.current_value}) + # print(f"_update_cell_change_in_db = {sql}") + + connection.execute(sql) + + ############################################################################################ + # Update/Replace tables in scenario + ############################################################################################ +
    [docs] def update_scenario_output_tables_in_db(self, scenario_name, outputs: Outputs): + """Main API to update output from a DO solve in the scenario. + Deletes ALL output tables. Then inserts the given set of tables. + Since this only touches the output tables, more efficient than replacing the whole scenario.""" + if self.enable_transactions: + with self.engine.begin() as connection: + self._update_scenario_output_tables_in_db(scenario_name, outputs, connection) + else: + self._update_scenario_output_tables_in_db(scenario_name, outputs, self.engine)
    + + def _update_scenario_output_tables_in_db(self, scenario_name, outputs: Outputs, connection): + """Deletes ALL output tables. Then inserts the given set of tables. + Note that if a defined output table is not included in the outputs, it will still be deleted from the scenario data.""" + # 1. Add scenario name to dfs: + outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) + # 2. Delete all output tables + for scenario_table_name, db_table in reversed(self.output_db_tables.items()): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario'): + db_table._delete_scenario_table_from_db(scenario_name, connection) + # 3. Insert new data + for scenario_table_name, db_table in self.output_db_tables.items(): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in outputs.keys(): # If in given set of tables to replace + df = outputs[scenario_table_name] + db_table.insert_table_in_db_bulk(df=df, mgr=self, connection=connection) # The scenario_name is a column in the df + +
    [docs] def replace_scenario_tables_in_db(self, scenario_name, inputs={}, outputs={}): + """Untested""" + if self.enable_transactions: + with self.engine.begin() as connection: + self._replace_scenario_tables_in_db(connection, scenario_name, inputs, outputs) + else: + self._replace_scenario_tables_in_db(self.engine, scenario_name, inputs, outputs)
    + + def _replace_scenario_tables_in_db(self, connection, scenario_name, inputs={}, outputs={}): + """Untested + Replace only the tables listed in the inputs and outputs. But leave all other tables untouched. + Will first delete all given tables (in reverse cascading order), then insert the new ones (in cascading order)""" + + # Add scenario name to dfs: + inputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, inputs) + outputs = ScenarioDbManager.add_scenario_name_to_dfs(scenario_name, outputs) + dfs = {**inputs, **outputs} + # 1. Delete tables + for scenario_table_name, db_table in reversed(self.db_tables.items()): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in dfs.keys(): # If in given set of tables to replace + db_table._delete_scenario_table_from_db() + # 2. Insert new data + for scenario_table_name, db_table in self.db_tables.items(): # Note this INCLUDES the SCENARIO table! + if (scenario_table_name != 'Scenario') and db_table.db_table_name in dfs.keys(): # If in given set of tables to replace + df = dfs[scenario_table_name] + db_table.insert_table_in_db_bulk(df=df, mgr=self, connection=connection) # The scenario_name is a column in the df + + ############################################################################################ + # CRUD operations on scenarios in DB: + # - Delete scenario + # - Duplicate scenario + # - Rename scenario + ############################################################################################ +
    [docs] def delete_scenario_from_db(self, scenario_name: str): + """Delete a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Delete scenario within a transaction") + with self.engine.begin() as connection: + self._delete_scenario_from_db(scenario_name=scenario_name, connection=connection) + else: + self._delete_scenario_from_db(scenario_name=scenario_name, connection=self.engine)
    + + ########################################################## +
    [docs] def duplicate_scenario_in_db(self, source_scenario_name: str, target_scenario_name: str): + """Duplicate a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Duplicate scenario within a transaction") + with self.engine.begin() as connection: + self._duplicate_scenario_in_db(connection, source_scenario_name, target_scenario_name) + else: + self._duplicate_scenario_in_db(self.engine, source_scenario_name, target_scenario_name)
    + + def _duplicate_scenario_in_db(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """Is fully done in DB using SQL in one SQL execute statement + :param source_scenario_name: + :param target_scenario_name: + :param connection: + :return: + """ + if target_scenario_name is None: + new_scenario_name = self._find_free_duplicate_scenario_name(source_scenario_name) + elif self._check_free_scenario_name(target_scenario_name): + new_scenario_name = target_scenario_name + else: + raise ValueError(f"Target name for duplicate scenario '{target_scenario_name}' already exists.") + + # inputs, outputs = self.read_scenario_from_db(source_scenario_name) + # self._replace_scenario_in_db_transaction(scenario_name=new_scenario_name, inputs=inputs, outputs=outputs, + # bulk=True, connection=connection) + self._duplicate_scenario_in_db_sql(connection, source_scenario_name, new_scenario_name) + + def _duplicate_scenario_in_db_sql(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """ + :param source_scenario_name: + :param target_scenario_name: + :param connection: + :return: + + See https://stackoverflow.com/questions/9879830/select-modify-and-insert-into-the-same-table + + Problem: the table Parameter/parameters has a column 'value' (lower-case). + Almost all of the column names in the DFs are lower-case, as are the column names in the ScenarioDbTable. + Typically, the DB schema converts that the upper-case column names in the DB. + But probably because 'VALUE' is a reserved word, it does NOT do this for 'value'. But that means in order to refer to this column in SQL, + one needs to put "value" between double quotes. + Problem is that you CANNOT do that for other columns, since these are in upper-case in the DB. + Note that the kpis table uses upper case 'VALUE' and that seems to work fine + + Resolution: use SQLAlchemy to construct the SQL. Do NOT create SQL expressions by text manipulation. + SQLAlchemy has the smarts to properly deal with these complex names. + """ + if target_scenario_name is None: + new_scenario_name = self._find_free_duplicate_scenario_name(source_scenario_name) + elif self._check_free_scenario_name(target_scenario_name): + new_scenario_name = target_scenario_name + else: + raise ValueError(f"Target name for duplicate scenario '{target_scenario_name}' already exists.") + + batch_sql=False # BEWARE: batch = True does NOT work! + sql_statements = [] + + # 1. Insert scenario in scenario table + # sql_insert = f"INSERT INTO SCENARIO (scenario_name) VALUES ('{new_scenario_name}')" # Old SQL + # sa_scenario_table = list(self.input_db_tables.values())[0].get_sa_table() # Scenario table must be the first + sa_scenario_table = self.get_scenario_db_table().get_sa_table() + sql_insert = sa_scenario_table.insert().values(scenario_name = new_scenario_name) + # print(f"_duplicate_scenario_in_db_sql - Insert SQL = {sql_insert}") + if batch_sql: + sql_statements.append(sql_insert) + else: + connection.execute(sql_insert) + + # 2. Do 'insert into select' to duplicate rows in each table + for scenario_table_name, db_table in self.db_tables.items(): + if scenario_table_name == 'Scenario': + continue + + t: sqlalchemy.table = db_table.table_metadata # The table at hand + s: sqlalchemy.table = sa_scenario_table # The scenario table + # print("+++++++++++SQLAlchemy insert-select") + select_columns = [s.c.scenario_name if c.name == 'scenario_name' else c for c in t.columns] # Replace the t.c.scenario_name with s.c.scenario_name, so we get the new value + # print(f"select columns = {select_columns}") + select_sql = (sqlalchemy.select(select_columns) + .where(sqlalchemy.and_(t.c.scenario_name == source_scenario_name, s.c.scenario_name == target_scenario_name))) + target_columns = [c for c in t.columns] + sql_insert = t.insert().from_select(target_columns, select_sql) + # print(f"sql_insert = {sql_insert}") + + # sql_insert = f"INSERT INTO {db_table.db_table_name} ({target_columns_txt}) SELECT '{target_scenario_name}',{other_source_columns_txt} FROM {db_table.db_table_name} WHERE scenario_name = '{source_scenario_name}'" + if batch_sql: + sql_statements.append(sql_insert) + else: + connection.execute(sql_insert) + if batch_sql: + batch_sql = ";\n".join(sql_statements) + print(batch_sql) + connection.execute(batch_sql) + + def _find_free_duplicate_scenario_name(self, scenario_name: str, scenarios_df=None) -> Optional[str]: + """Finds next free scenario name based on pattern '{scenario_name}_copy_n'. + Will try at maximum 20 attempts. + """ + max_num_attempts = 20 + for i in range(1, max_num_attempts + 1): + new_name = f"{scenario_name}({i})" + free = self._check_free_scenario_name(new_name, scenarios_df) + if free: + return new_name + raise ValueError(f"Cannot find free name for duplicate scenario. Tried {max_num_attempts}. Last attempt = {new_name}. Rename scenarios.") + return None + + def _check_free_scenario_name(self, scenario_name, scenarios_df=None) -> bool: + if scenarios_df is None: + scenarios_df = self.get_scenarios_df() + free = (False if scenario_name in scenarios_df.index else True) + return free + + ############################################## +
    [docs] def rename_scenario_in_db(self, source_scenario_name: str, target_scenario_name: str): + """Rename a scenario. Uses a transaction (when enabled).""" + if self.enable_transactions: + print("Rename scenario within a transaction") + with self.engine.begin() as connection: + # self._rename_scenario_in_db(source_scenario_name, target_scenario_name, connection=connection) + self._rename_scenario_in_db_sql(connection, source_scenario_name, target_scenario_name) + else: + # self._rename_scenario_in_db(source_scenario_name, target_scenario_name) + self._rename_scenario_in_db_sql(self.engine, source_scenario_name, target_scenario_name)
    + + def _rename_scenario_in_db_sql(self, connection, source_scenario_name: str, target_scenario_name: str = None): + """Rename scenario. + Uses 2 steps: + 1. Duplicate scenario + 2. Delete source scenario. + + Problem is that we use scenario_name as a primary key. You should not change the value of primary keys in a DB. + Instead, first copy the data using a new scenario_name, i.e. duplicate a scenario. Next, delete the original scenario. + + Long-term solution: use a scenario_seq sequence key as the PK. With scenario_name as a ordinary column in the scenario table. + + Use of 'insert into select': https://stackoverflow.com/questions/9879830/select-modify-and-insert-into-the-same-table + """ + # 1. Duplicate scenario + self._duplicate_scenario_in_db_sql(connection, source_scenario_name, target_scenario_name) + # 2. Delete scenario + self._delete_scenario_from_db(source_scenario_name, connection=connection) + + def _delete_scenario_from_db(self, scenario_name: str, connection): + """Deletes all rows associated with a given scenario. + Note that it only deletes rows from tables defined in the self.db_tables, i.e. will NOT delete rows in 'auto-inserted' tables! + Must do a 'cascading' delete to ensure not violating FK constraints. In reverse order of how they are inserted. + Also deletes entry in scenario table + Uses SQLAlchemy syntax to generate SQL + TODO: check with 'auto-inserted' tables + TODO: batch all sql statements in single execute. Faster? And will that do the defer integrity checks? + """ + # batch_sql=False # Batch=True does NOT work! + insp = sqlalchemy.inspect(connection) + tables_in_db = insp.get_table_names(schema=self.schema) + # sql_statements = [] + for scenario_table_name, db_table in reversed(self.db_tables.items()): # Note this INCLUDES the SCENARIO table! + if db_table.db_table_name in tables_in_db: + # # sql = f"DELETE FROM {db_table.db_table_name} WHERE scenario_name = '{scenario_name}'" # Old + # t = db_table.table_metadata # A Table() + # sql = t.delete().where(t.c.scenario_name == scenario_name) + # connection.execute(sql) + db_table._delete_scenario_table_from_db(scenario_name, connection) ############################################################################################ # Old Read scenario APIs @@ -912,29 +1388,13 @@

    Source code for dse_do_utils.scenariodbmanager

    ####################################################################################################### # Review ####################################################################################################### -

    [docs] def read_scenario_tables_from_db(self, scenario_name: str, - input_table_names: List[str] = None, - output_table_names: List[str] = None) -> (Inputs, Outputs): - """Loads data for selected input and output tables. - If either list is names is None, will load all tables as defined in db_tables configuration. - """ - if input_table_names is None: # load all tables by default - input_table_names = list(self.input_db_tables.keys()) - if 'Scenario' in input_table_names: input_table_names.remove('Scenario') # Remove the scenario table - if output_table_names is None: # load all tables by default - output_table_names = self.output_db_tables.keys() - inputs = {} - for scenario_table_name in input_table_names: - inputs[scenario_table_name] = self.read_scenario_table_from_db(scenario_name, scenario_table_name) - outputs = {} - for scenario_table_name in output_table_names: - outputs[scenario_table_name] = self.read_scenario_table_from_db(scenario_name, scenario_table_name) - return inputs, outputs
    [docs] def read_scenarios_from_db(self, scenario_names: List[str] = []) -> (Inputs, Outputs): """Multi scenario load. - Reads all tables from set of scenarios""" + Reads all tables from set of scenarios + TODO: avoid use of text SQL. Use SQLAlchemy sql generation. + """ where_scenarios = ','.join([f"'{n}'" for n in scenario_names]) inputs = {} @@ -1063,7 +1523,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/scenariomanager.html b/docs/doc_build/html/_modules/dse_do_utils/scenariomanager.html index e67874c..4485c8b 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/scenariomanager.html +++ b/docs/doc_build/html/_modules/dse_do_utils/scenariomanager.html @@ -6,7 +6,7 @@ - dse_do_utils.scenariomanager — DSE DO Utils 0.5.3.0 documentation + dse_do_utils.scenariomanager — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -1061,7 +1061,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/scenariopicker.html b/docs/doc_build/html/_modules/dse_do_utils/scenariopicker.html index 73f799c..1b33c01 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/scenariopicker.html +++ b/docs/doc_build/html/_modules/dse_do_utils/scenariopicker.html @@ -6,7 +6,7 @@ - dse_do_utils.scenariopicker — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.scenariopicker — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -257,7 +257,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/dse_do_utils/utilities.html b/docs/doc_build/html/_modules/dse_do_utils/utilities.html index c81360b..80bb27e 100644 --- a/docs/doc_build/html/_modules/dse_do_utils/utilities.html +++ b/docs/doc_build/html/_modules/dse_do_utils/utilities.html @@ -6,7 +6,7 @@ - dse_do_utils.utilities — DSE DO Utils 0.5.2.0 documentation + dse_do_utils.utilities — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - + @@ -146,7 +146,7 @@

    Navigation

  • modules |
  • - + diff --git a/docs/doc_build/html/_modules/index.html b/docs/doc_build/html/_modules/index.html index db15847..fb6ad59 100644 --- a/docs/doc_build/html/_modules/index.html +++ b/docs/doc_build/html/_modules/index.html @@ -6,7 +6,7 @@ - Overview: module code — DSE DO Utils 0.5.3.1 documentation + Overview: module code — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - +
    @@ -87,7 +87,7 @@

    Navigation

  • modules |
  • - +
    diff --git a/docs/doc_build/html/_static/bizstyle.js b/docs/doc_build/html/_static/bizstyle.js index a902289..e33e0eb 100644 --- a/docs/doc_build/html/_static/bizstyle.js +++ b/docs/doc_build/html/_static/bizstyle.js @@ -36,6 +36,6 @@ $(window).resize(function(){ $("li.nav-item-0 a").text("Top"); } else { - $("li.nav-item-0 a").text("DSE DO Utils 0.5.3.1 documentation"); + $("li.nav-item-0 a").text("DSE DO Utils 0.5.4.0 documentation"); } }); \ No newline at end of file diff --git a/docs/doc_build/html/_static/documentation_options.js b/docs/doc_build/html/_static/documentation_options.js index e95d97a..25a1886 100644 --- a/docs/doc_build/html/_static/documentation_options.js +++ b/docs/doc_build/html/_static/documentation_options.js @@ -1,6 +1,6 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '0.5.3.1', + VERSION: '0.5.4.0', LANGUAGE: 'None', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/doc_build/html/dse_do_utils.html b/docs/doc_build/html/dse_do_utils.html index e0a4c68..f4ce1cf 100644 --- a/docs/doc_build/html/dse_do_utils.html +++ b/docs/doc_build/html/dse_do_utils.html @@ -6,7 +6,7 @@ - dse_do_utils package — DSE DO Utils 0.5.3.1 documentation + dse_do_utils package — DSE DO Utils 0.5.4.0 documentation @@ -35,7 +35,7 @@

    Navigation

  • previous |
  • - + @@ -1563,6 +1563,54 @@

    dse_do_utils.domodeldeployer moduledse_do_utils.scenariodbmanager.ScenarioDbTable

    +
    +
    +class dse_do_utils.scenariodbmanager.DbCellUpdate(scenario_name, table_name, row_index, column_name, current_value, previous_value, row_idx)[source]¶
    +

    Bases: NamedTuple

    +
    +
    +column_name: str¶
    +

    Alias for field number 3

    +
    + +
    +
    +current_value: Any¶
    +

    Alias for field number 4

    +
    + +
    +
    +previous_value: Any¶
    +

    Alias for field number 5

    +
    + +
    +
    +row_idx: int¶
    +

    Alias for field number 6

    +
    + +
    +
    +row_index: List[Dict[str, Any]]¶
    +

    Alias for field number 2

    +
    + +
    +
    +scenario_name: str¶
    +

    Alias for field number 0

    +
    + +
    +
    +table_name: str¶
    +

    Alias for field number 1

    +
    + +
    +
    class dse_do_utils.scenariodbmanager.KpiTable(db_table_name: str = 'kpis')[source]¶
    @@ -1594,6 +1642,12 @@

    dse_do_utils.domodeldeployer module +
    +delete_scenario_from_db(scenario_name: str)[source]¶
    +

    Delete a scenario. Uses a transaction (when enabled).

    +

    +
    static delete_scenario_name_column(inputs: Dict[str, pandas.core.frame.DataFrame], outputs: Dict[str, pandas.core.frame.DataFrame])[source]¶
    @@ -1607,9 +1661,21 @@

    dse_do_utils.domodeldeployer module +
    +duplicate_scenario_in_db(source_scenario_name: str, target_scenario_name: str)[source]¶
    +

    Duplicate a scenario. Uses a transaction (when enabled).

    +

    + +
    +
    +get_scenario_db_table() dse_do_utils.scenariodbmanager.ScenarioDbTable[source]¶
    +

    Scenario table must be the first in self.input_db_tables

    +
    +
    -get_scenarios_df()[source]¶
    +get_scenarios_df() pandas.core.frame.DataFrame[source]¶

    Return all scenarios in df. Result is indexed by scenario_name. Main API to get all scenarios. The API called by a cached procedure in the dse_do_dashboard.DoDashApp.

    @@ -1633,11 +1699,20 @@

    dse_do_utils.domodeldeployer module
    -read_scenario_from_db(scenario_name: str)[source]¶
    +read_scenario_from_db(scenario_name: str, multi_threaded: bool = False)[source]¶

    Single scenario load. Main API to read a complete scenario. Reads all tables for a single scenario. Returns all tables in one dict

    +

    Note: multi_threaded doesn’t seem to lead to performance improvement. +Fixed: omit reading scenario table as an input.

    +

    + +
    +
    +read_scenario_input_tables_from_db(scenario_name: str)[source]¶
    +

    Convenience method to load all input tables. +Typically used at start if optimization model.

    @@ -1673,8 +1748,9 @@

    dse_do_utils.domodeldeployer module
    read_scenario_tables_from_db(scenario_name: str, input_table_names: Optional[List[str]] = None, output_table_names: Optional[List[str]] = None)[source]¶
    -

    Loads data for selected input and output tables. -If either list is names is None, will load all tables as defined in db_tables configuration.

    +

    Read selected set input and output tables from scenario. +If input_table_names/output_table_names contains a ‘*’, then all input/output tables will be read. +If empty list or None, then no tables will be read.

    @@ -1690,7 +1766,8 @@

    dse_do_utils.domodeldeployer module read_scenarios_from_db(scenario_names: List[str] = [])[source]¶

    Multi scenario load. -Reads all tables from set of scenarios

    +Reads all tables from set of scenarios +TODO: avoid use of text SQL. Use SQLAlchemy sql generation.

    @@ -1705,6 +1782,12 @@

    dse_do_utils.domodeldeployer module +
    +rename_scenario_in_db(source_scenario_name: str, target_scenario_name: str)[source]¶
    +

    Rename a scenario. Uses a transaction (when enabled).

    +

    +
    replace_scenario_in_db(scenario_name: str, inputs: Dict[str, pandas.core.frame.DataFrame] = {}, outputs: Dict[str, pandas.core.frame.DataFrame] = {}, bulk=True)[source]¶
    @@ -1727,6 +1810,12 @@

    dse_do_utils.domodeldeployer module +
    +replace_scenario_tables_in_db(scenario_name, inputs={}, outputs={})[source]¶
    +

    Untested

    +

    +
    set_scenarios_table_read_callback(scenarios_table_read_callback=None)[source]¶
    @@ -1741,6 +1830,28 @@

    dse_do_utils.domodeldeployer module +
    +update_cell_changes_in_db(db_cell_updates: List[dse_do_utils.scenariodbmanager.DbCellUpdate])[source]¶
    +

    Update a set of cells in the DB.

    +
    +
    Parameters
    +

    db_cell_updates –

    +
    +
    Returns
    +

    +
    +
    +

    + +
    +
    +update_scenario_output_tables_in_db(scenario_name, outputs: Dict[str, pandas.core.frame.DataFrame])[source]¶
    +

    Main API to update output from a DO solve in the scenario. +Deletes ALL output tables. Then inserts the given set of tables. +Since this only touches the output tables, more efficient than replacing the whole scenario.

    +
    +
    @@ -1762,7 +1873,7 @@

    dse_do_utils.domodeldeployer module
    -create_table_metadata(metadata, multi_scenario: bool = False)[source]¶
    +create_table_metadata(metadata, multi_scenario: bool = False) sqlalchemy.sql.schema.Table[source]¶

    If multi_scenario, then add a primary key ‘scenario_name’.

    @@ -1783,6 +1894,19 @@

    dse_do_utils.domodeldeployer module +
    +get_sa_column(db_column_name) Optional[sqlalchemy.sql.schema.Column][source]¶
    +

    Returns the SQLAlchemy column with the specified name. +Dynamically creates a dict/hashtable for more efficient access.

    +
    + +
    +
    +get_sa_table() sqlalchemy.sql.schema.Table[source]¶
    +

    Returns the SQLAlchemy Table

    +
    +
    insert_table_in_db_bulk(df: pandas.core.frame.DataFrame, mgr, connection=None)[source]¶
    @@ -2675,7 +2799,7 @@

    Navigation

  • previous |
  • - + diff --git a/docs/doc_build/html/genindex.html b/docs/doc_build/html/genindex.html index 509a946..ddfd741 100644 --- a/docs/doc_build/html/genindex.html +++ b/docs/doc_build/html/genindex.html @@ -6,7 +6,7 @@ - Index — DSE DO Utils 0.5.3.1 documentation + Index — DSE DO Utils 0.5.4.0 documentation @@ -31,7 +31,7 @@

    Navigation

  • modules |
  • - +

    @@ -60,6 +60,7 @@

    Index

    | P | R | S + | T | U | W @@ -134,6 +135,8 @@

    C

  • cleanup() (dse_do_utils.deployeddomodelcpd21.DeployedDOModel_CPD21 method)
  • clear_scenario_data() (dse_do_utils.scenariomanager.ScenarioManager static method) +
  • +
  • column_name (dse_do_utils.scenariodbmanager.DbCellUpdate attribute)
  • continuous_var_series() (dse_do_utils.optimizationengine.OptimizationEngine method)
  • @@ -159,6 +162,8 @@

    C

  • (dse_do_utils.scenariodbmanager.ScenarioDbTable method)
  • +
  • current_value (dse_do_utils.scenariodbmanager.DbCellUpdate attribute) +
  • @@ -166,8 +171,12 @@

    D

    +
  • duplicate_scenario_in_db() (dse_do_utils.scenariodbmanager.ScenarioDbManager method) +
  • @@ -413,12 +424,12 @@

    G

  • get_kpi_output_table() (dse_do_utils.optimizationengine.OptimizationEngine method)
  • - - + +
  • get_sa_column() (dse_do_utils.scenariodbmanager.ScenarioDbTable method) +
  • +
  • get_sa_table() (dse_do_utils.scenariodbmanager.ScenarioDbTable method) +
  • +
  • get_scenario_db_table() (dse_do_utils.scenariodbmanager.ScenarioDbManager method) +
  • get_scenario_picker_ui() (dse_do_utils.scenariopicker.ScenarioPicker method)
  • get_scenario_refresh_button() (dse_do_utils.scenariopicker.ScenarioPicker method) @@ -646,10 +663,10 @@

    P

  • post_process_inline_table_get_dataframe() (dse_do_utils.deployeddomodelcpd21.DeployedDOModel_CPD21 static method)
  • - - +
    • post_process_processed() (dse_do_utils.deployeddomodelcpd21.DeployedDOModel_CPD21 method)
    • prep_parameters() (dse_do_utils.datamanager.DataManager method) @@ -659,6 +676,8 @@

      P

    • prepare_input_data_frames() (dse_do_utils.datamanager.DataManager method)
    • prepare_output_data_frames() (dse_do_utils.datamanager.DataManager method) +
    • +
    • previous_value (dse_do_utils.scenariodbmanager.DbCellUpdate attribute)
    • print_hello() (dse_do_utils.datamanager.DataManager method)
    • @@ -673,6 +692,8 @@

      R

      @@ -708,6 +737,8 @@

      R

      S

      +

      T

      + + +
      +

      U

      + @@ -814,7 +859,7 @@

      Navigation

    • modules |
    • - + diff --git a/docs/doc_build/html/index.html b/docs/doc_build/html/index.html index 01def9c..d5262d0 100644 --- a/docs/doc_build/html/index.html +++ b/docs/doc_build/html/index.html @@ -6,7 +6,7 @@ - Welcome to DSE DO Utils documentation! — DSE DO Utils 0.5.3.1 documentation + Welcome to DSE DO Utils documentation! — DSE DO Utils 0.5.4.0 documentation @@ -35,7 +35,7 @@

      Navigation

    • next |
    • - + @@ -128,7 +128,7 @@

      Navigation

    • next |
    • - + diff --git a/docs/doc_build/html/modules.html b/docs/doc_build/html/modules.html index 6a3f500..471d122 100644 --- a/docs/doc_build/html/modules.html +++ b/docs/doc_build/html/modules.html @@ -6,7 +6,7 @@ - dse_do_utils — DSE DO Utils 0.5.3.1 documentation + dse_do_utils — DSE DO Utils 0.5.4.0 documentation @@ -39,7 +39,7 @@

      Navigation

    • previous |
    • - + @@ -127,7 +127,7 @@

      Navigation

    • previous |
    • - + diff --git a/docs/doc_build/html/objects.inv b/docs/doc_build/html/objects.inv index e11b45a874f00081f1c2fe429a16077ad57d01ec..ded957f08660eaa4bdc979d82a70df86f1d87bdc 100644 GIT binary patch delta 2204 zcmV;N2xIsE5P=eqL<2M~Fp)<+f0N@h5QgvbD?-6_Q$WFm8!&sIU}~pWD2^&wQW9-s zNl0=s;n$-N+i@nzB$m~l(@Y%aeWaGvYPH*@mLaN8GwZ|f4SqVjK~sGidI}Oge{`jVgJ`MhvHd*uxcc6e_YAMcNZ9U zi)-vQvlI4~OxE8U?$}V&BO$oW;afPm9Yd8R z)><48mJ|^s33joO%xx5%rb}0d7-j7j#U;WVM7D;JwZ&xFi!+!wWR+;*B~wUu?XOCW z4fa~hpdA(sD=BU89ywh3e@nhPAn7Tk#I<4i*l=4?7Mpg?u7Y}TX)bl+AOWgzU<585 z+AtwhS;=S<+qT>y+tf8ShtUkCg*q@wm3M$lH` z`DkLt;7}SLgu)aK05{H7W5NKg&{|3EltF181I)2#n2DkxEd8#GJB(7V8;O2>eoTOT z&-jM$k_d)nqf1!f3ZbMMev6YlX4R|_ZE77Oq8KqZW*%zXeZa%|*?=rFJ5E}|&Py)g zOK#yd4AY$Oa~vgwe~e-5ph;n|g&(9X7h)!qQ0+xO<5cUJ@TgLY{w+=yLC$H$6cP|6 zSA0DSY8H=?P%~K1DLum+>!vawqI8J$7E3GZkT!}iYs01ILu=Wl+vWtKxMKTla@@bq zgvtDLeEY%=fV|myQ|JXg8ms+rOYd!<^wPf9^&@m|0`rOOAz|0~wau zIXH!~DEG4ZwXZQ47;F1Ph>zEu<8mD?)VUi`YNLBizg-YVBa$?G(aN~|TQ8?7%eHMF z2dZQC&Tp$N(--0Vc%Gfu=QI7_Ix&gf#i=cmh~z{WVW+8! zVNZ~g>$;)df8NgQy5Uro>#eb#b{F#~uzbQg?;hqwjdZ)Y@d2$RhB2Anv)7^mAAC?3 z!aJix8Cl;#zsRCWV7c|{tuCV)=bSNWZ_DXb3>&|sloJf+=f#cJHMP=>E-AXj(qbRd zb9e9quS}ec*!lInY8<;$InDrNSeJDlS1?dF!f{KOfAscO6X)CospV5R__;lpIS=pE zjngRI(hQju?|WK5H+r=0CNjB0+=f~Wp&AOuiE}PGope0rSS&0rT9R_(4XDQ7XdTI- zhzQQ4gT=(Nk46kVgasO<+*~&7PPB-w;7GyEbaex&?xAqNnwd45Ys?1KSl)fG7c%}% zkJ66UfBe6=6=HJDCCl`u=R(V;!?RIq7X|@=Lg2||qZ$j|r;8}4ezhB^ z5N2sL0|LGuLc8=A>)2{e>yml9i&}y1F z&vceQH%@g?&13afH^D|2_AbpXw~%g=_^#ue!0bL0mwsZk&LZTTX@#mrY^TG1X{?`E zf7br4c^N}JR=+YK{?^_WWySoODVkdL<Q+V?uFeU)o((UK_UKX|)H@H)@YK zeE|}dlmSSeY~;(!r@2y0yVwMI?yB+2$Q?aJ-Wqa;V%$@lj8@ng(v_X=e$H8~Q?+En zY77Agse3orO~*Yvt>!>Q(?P&1%NFbNvA#c#i)0+4fP4u8O+Dusk_Z29^=Oc+X%A~Z&~~VK!QnLyvBs*ypQ{K z3*D=j^=d=KtOu#@M4a{t24XS5Ug1Q(ctj1Hta=Ty{|Yj`Q0Thu6hBfXiFr)5Vz*MJ zp4OONW*KTbf1Hp9xg_|*w}k~UfBsxj@ek$K(*lCiN_TFkT`kx_*_n_#rl+5n(U?F*1{W&z_(^;A-jYQeKg7j6-z7b@OG$SqI2OB~LXf41co-Il&} zL0J#8-GiD|?RbmOR~~{1r)(9mJ9t9*1>hAUt9CJm0p1g+$#Df!zCY>xRmW|4aX^IiPrtD(?_GlLD;9LCG-n6cf{{i8dzu#Wyj6FuNZnvOAN(aOPf_6pY0Z$}ay!Hg4yWJ# e=?^Sw_F!2UO4wlaQoW6e!cK#oyDS}Bqfl*8V delta 2073 zcmV+!2J68{j8L<2J}F_A|-e{ql6HE;fB+;R z0s#~NEvvu2_#i3Ck}MI#GACOU^*-1ocCq-7sAY&M)H*KgLhn8_inf9+-Y)3D8`au1 z;>YyXo1YeM?vCChn*RJg{L39@Wkf1WDOz&D5VnY7hy7p69Ex`Z!m^dbe{m%j@6Ir8 z7T4IVXD4henXJDx+_6JZkBGc7&A%w0)%Kv%V2M}EWOj}Nh3ek_|FBmh!nbgAI}TMU zthLx7EGZ&N66|6lnOiG5O_weaG0NI+ic5q!h-?ERYm3RUXJ;^R$STpsE>lQ%<$I;Z z274`L&=VFdD=BU87CBt|e=c9|ko1&N;@U91Z@4Wfi%l1^%b;FdnoHd}NPub`7=cTN z)=UUhRx~W?U^rMmB4-+zASRmUXD*VTMnGt&A& zV&CF?gU^UhOd418LxiV$8Wx@+g#dh0HL?!7)B7^Pk|68-*op8)xp z@fG2U2!>^=OL*Y|p=26U1 zuHn`U)12~i93_>Ef8p6dlfqyN|ByCZh?!7Ab)A03P#c)=s8WOeHBMha&gsTf5)dVq zd^HPdmW+{5GkDJ_JwuH3p$v#9U1Gh)(#krdjUvq2(Di(2ExYBmIe{pS*nXNE=kIgj zmwq0>=v*`|g*P>=4t)>i_wg~8jv3QxH=m=szo0I z)iHDD*VU@@Mz}xTXD8Zxq90NxCNaA>wP6yGoGN21WXoy@M(-umc@4^zmmDB-e7X;F zf}FLkE9%{Le`arnQ(39E!Ft**=3bzBz&dXa^Q=bNZf<=+Yl&e@=3DksRN$R2>Oy&E zlqe(XTj&Q_R0}LOe!SIXRAbB;qqer3Ud^!aLrOWpaDQIicv({`-RhE}8!RpMB|UdL z|KOF0(TJTN-|N<~8 z@rM5wf2TrBuDE2G`Set%de}W0wKBfk{lu+fa&#`eNwbp5@#QXL&v<>h2dd8pmp%?o znbz=A%{TxwTn{Zr8_i*wU)9`qoE>_?ChT8vHWvl~fkNTQWvf~XZqr#5lwI9KDwJ7T z&47TnLuhyclofGjPxF2yDXW&gjT9D9SMj(ae{5$yO0P910(Fmx!3s!9?t;VkZg$YA+L^7i zbDKGzfp*5{s6{v_J-8Mu{_IiGO7=yge~C|)H6|MGnkKG zUA@jE9wWSuY=l{fH!QvaAi)%zFEQb}d2yUiOYG2A#hFUoS(WqUw;K`1R+Wc<)#xul z_FqB9?~h*Aof3*==5G5LCU#3@nrV$0WU6qa<$cDE;%kDxye%w<@z;`ypOjD6e+2}m z?O0w>ySPt@vIFxrQkd?p2AJUW9JsBWmjviUKZ2_49j`nJj5Vh}_b?Boyfxu#UuN3w z;8jE_JS5wKJ$uSrh)8`f0+f{RPALpOSOKG*#s&awYM+6WGtX^YRZlx9=?Glbj?Fc~ z^z#`v5pqvu+$0W{O1oXAe#Y3Pf1s>0{I{T{9rCzF7!F*338y6kVxQm%Ox+f?5v9FzGR%dUy^%Aj?FUU&U=!s7z#)L*4M{zXT-v+)NV$A6oEdD$dN*F~i5c5} zsd2PX3+w0ys>tHa8P?I2e}%?mx3S zA0~+MvmK`v_g_CQ?msX7jud}q8-9l?o-6y|&NurEOz+ - Python Module Index — DSE DO Utils 0.5.3.1 documentation + Python Module Index — DSE DO Utils 0.5.4.0 documentation @@ -34,7 +34,7 @@

      Navigation

    • modules |
    • - + @@ -168,7 +168,7 @@

      Navigation

    • modules |
    • - + diff --git a/docs/doc_build/html/readme_link.html b/docs/doc_build/html/readme_link.html index 97210e2..7b8b0a7 100644 --- a/docs/doc_build/html/readme_link.html +++ b/docs/doc_build/html/readme_link.html @@ -6,7 +6,7 @@ - Read me — DSE DO Utils 0.5.3.1 documentation + Read me — DSE DO Utils 0.5.4.0 documentation @@ -39,7 +39,7 @@

      Navigation

    • previous |
    • - + @@ -269,7 +269,7 @@

      Navigation

    • previous |
    • - + diff --git a/docs/doc_build/html/search.html b/docs/doc_build/html/search.html index 369e7a0..b82a2ca 100644 --- a/docs/doc_build/html/search.html +++ b/docs/doc_build/html/search.html @@ -6,7 +6,7 @@ - Search — DSE DO Utils 0.5.3.1 documentation + Search — DSE DO Utils 0.5.4.0 documentation @@ -37,7 +37,7 @@

      Navigation

    • modules |
    • - + @@ -96,7 +96,7 @@

      Navigation

    • modules |
    • - + diff --git a/docs/doc_build/html/searchindex.js b/docs/doc_build/html/searchindex.js index ae215ad..97e8490 100644 --- a/docs/doc_build/html/searchindex.js +++ b/docs/doc_build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["dse_do_utils","index","modules","readme_link"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["dse_do_utils.rst","index.rst","modules.rst","readme_link.rst"],objects:{"":{dse_do_utils:[0,0,0,"-"]},"dse_do_utils.cpd25utilities":{add_file_as_data_asset_cpd25:[0,1,1,""],add_file_path_as_data_asset_cpd25:[0,1,1,""],add_file_path_as_data_asset_wsc:[0,1,1,""],write_data_asset_as_file_cpd25:[0,1,1,""],write_data_asset_as_file_wsc:[0,1,1,""]},"dse_do_utils.datamanager":{DataManager:[0,2,1,""]},"dse_do_utils.datamanager.DataManager":{apply_and_concat:[0,3,1,""],df_crossjoin_ai:[0,3,1,""],df_crossjoin_mi:[0,3,1,""],df_crossjoin_si:[0,3,1,""],extract_solution:[0,3,1,""],get_parameter_value:[0,3,1,""],get_raw_table_by_name:[0,3,1,""],prep_parameters:[0,3,1,""],prepare_data_frames:[0,3,1,""],prepare_input_data_frames:[0,3,1,""],prepare_output_data_frames:[0,3,1,""],print_hello:[0,3,1,""],print_inputs_outputs_summary:[0,3,1,""]},"dse_do_utils.deployeddomodel":{DeployedDOModel:[0,2,1,""]},"dse_do_utils.deployeddomodel.DeployedDOModel":{execute_model:[0,3,1,""],extract_solution:[0,3,1,""],get_deployment_id:[0,3,1,""],get_job_status:[0,3,1,""],get_outputs:[0,3,1,""],get_solve_details:[0,3,1,""],get_solve_details_objective:[0,3,1,""],get_solve_payload:[0,3,1,""],get_solve_status:[0,3,1,""],get_space_id:[0,3,1,""],monitor_execution:[0,3,1,""],solve:[0,3,1,""]},"dse_do_utils.deployeddomodelcpd21":{DeployedDOModel_CPD21:[0,2,1,""]},"dse_do_utils.deployeddomodelcpd21.DeployedDOModel_CPD21":{cleanup:[0,3,1,""],execute_model:[0,3,1,""],get_debug_dump_name_and_url:[0,3,1,""],get_debug_file_url:[0,3,1,""],get_execution_service_model_url:[0,3,1,""],get_execution_status:[0,3,1,""],get_headers:[0,3,1,""],get_input_files:[0,3,1,""],get_job_url:[0,3,1,""],get_kill_job_url:[0,3,1,""],get_log_file_name_and_url:[0,3,1,""],get_log_file_url:[0,3,1,""],get_objective:[0,3,1,""],get_solution_name_and_url:[0,3,1,""],get_solve_config:[0,3,1,""],get_solve_status:[0,3,1,""],get_solve_url:[0,3,1,""],get_stop_job_url:[0,3,1,""],kill_job:[0,3,1,""],monitor_execution:[0,3,1,""],post_process_container:[0,3,1,""],post_process_container_get_dataframe:[0,3,1,""],post_process_failed:[0,3,1,""],post_process_inline_table:[0,3,1,""],post_process_inline_table_get_dataframe:[0,3,1,""],post_process_interrupted:[0,3,1,""],post_process_processed:[0,3,1,""],retrieve_debug_materials:[0,3,1,""],retrieve_file:[0,3,1,""],retrieve_solution:[0,3,1,""],retrieve_solve_configuration:[0,3,1,""],set_output_settings_in_solve_configuration:[0,3,1,""],solve:[0,3,1,""],stop_job:[0,3,1,""]},"dse_do_utils.domodelexporter":{DOModelExporter:[0,2,1,""]},"dse_do_utils.domodelexporter.DOModelExporter":{export_do_models:[0,3,1,""],get_access_token_curl:[0,3,1,""],get_access_token_web:[0,3,1,""],get_do_model_export_curl:[0,3,1,""],get_do_model_export_web:[0,3,1,""],get_project_id:[0,3,1,""],write_do_model_to_file:[0,3,1,""]},"dse_do_utils.mapmanager":{MapManager:[0,2,1,""]},"dse_do_utils.mapmanager.MapManager":{add_full_screen:[0,3,1,""],add_layer_control:[0,3,1,""],create_blank_map:[0,3,1,""],get_arrows:[0,3,1,""],get_bearing:[0,3,1,""],get_html_table:[0,3,1,""],get_popup_table:[0,3,1,""],kansas_city_coord:[0,4,1,""]},"dse_do_utils.multiscenariomanager":{MultiScenarioManager:[0,2,1,""]},"dse_do_utils.multiscenariomanager.MultiScenarioManager":{add_data_file_to_project:[0,3,1,""],env_is_wscloud:[0,3,1,""],get_all_scenario_names:[0,3,1,""],get_data_directory:[0,3,1,""],get_dd_client:[0,3,1,""],get_multi_scenario_data:[0,3,1,""],get_root_directory:[0,3,1,""],get_scenarios_df:[0,3,1,""],load_data_from_scenario:[0,3,1,""],merge_scenario_data:[0,3,1,""],write_data_to_excel:[0,3,1,""]},"dse_do_utils.optimizationengine":{MyProgressListener:[0,2,1,""],OptimizationEngine:[0,2,1,""]},"dse_do_utils.optimizationengine.MyProgressListener":{notify_progress:[0,3,1,""]},"dse_do_utils.optimizationengine.OptimizationEngine":{add_mip_progress_kpis:[0,3,1,""],binary_var_series:[0,3,1,""],binary_var_series_s:[0,3,1,""],continuous_var_series:[0,3,1,""],continuous_var_series_s:[0,3,1,""],export_as_cpo:[0,3,1,""],export_as_cpo_s:[0,3,1,""],export_as_lp:[0,3,1,""],export_as_lp_s:[0,3,1,""],get_kpi_output_table:[0,3,1,""],integer_var_series:[0,3,1,""],integer_var_series_s:[0,3,1,""],solve:[0,3,1,""]},"dse_do_utils.plotlymanager":{PlotlyManager:[0,2,1,""]},"dse_do_utils.plotlymanager.PlotlyManager":{get_dash_tab_layout_m:[0,3,1,""],get_plotly_fig_m:[0,3,1,""]},"dse_do_utils.scenariodbmanager":{AutoScenarioDbTable:[0,2,1,""],BusinessKpiTable:[0,2,1,""],KpiTable:[0,2,1,""],ParameterTable:[0,2,1,""],ScenarioDbManager:[0,2,1,""],ScenarioDbTable:[0,2,1,""],ScenarioTable:[0,2,1,""]},"dse_do_utils.scenariodbmanager.AutoScenarioDbTable":{create_table_metadata:[0,3,1,""],insert_table_in_db_bulk:[0,3,1,""]},"dse_do_utils.scenariodbmanager.ScenarioDbManager":{add_scenario_name_to_dfs:[0,3,1,""],create_schema:[0,3,1,""],delete_scenario_name_column:[0,3,1,""],drop_all_tables:[0,3,1,""],get_scenarios_df:[0,3,1,""],insert_scenarios_in_db:[0,3,1,""],insert_tables_in_db:[0,3,1,""],read_scenario_from_db:[0,3,1,""],read_scenario_table_from_db:[0,3,1,""],read_scenario_table_from_db_cached:[0,3,1,""],read_scenario_tables_from_db:[0,3,1,""],read_scenario_tables_from_db_cached:[0,3,1,""],read_scenarios_from_db:[0,3,1,""],read_scenarios_table_from_db_cached:[0,3,1,""],replace_scenario_in_db:[0,3,1,""],set_scenarios_table_read_callback:[0,3,1,""],set_table_read_callback:[0,3,1,""]},"dse_do_utils.scenariodbmanager.ScenarioDbTable":{add_scenario_name_to_fk_constraint:[0,3,1,""],camel_case_to_snake_case:[0,3,1,""],create_table_metadata:[0,3,1,""],df_column_names_to_snake_case:[0,3,1,""],get_db_table_name:[0,3,1,""],get_df_column_names:[0,3,1,""],insert_table_in_db_bulk:[0,3,1,""],sqlcol:[0,3,1,""]},"dse_do_utils.scenariomanager":{Platform:[0,2,1,""],ScenarioManager:[0,2,1,""]},"dse_do_utils.scenariomanager.Platform":{CPD25:[0,4,1,""],CPD40:[0,4,1,""],CPDaaS:[0,4,1,""],Local:[0,4,1,""]},"dse_do_utils.scenariomanager.ScenarioManager":{add_data_file_to_project_s:[0,3,1,""],add_data_file_using_project_lib:[0,3,1,""],add_data_file_using_ws_lib:[0,3,1,""],add_data_file_using_ws_lib_s:[0,3,1,""],add_data_into_scenario:[0,3,1,""],add_data_into_scenario_s:[0,3,1,""],add_file_as_data_asset:[0,3,1,""],add_file_as_data_asset_s:[0,3,1,""],clear_scenario_data:[0,3,1,""],create_new_scenario:[0,3,1,""],detect_platform:[0,3,1,""],env_is_cpd25:[0,3,1,""],env_is_cpd40:[0,3,1,""],env_is_dsx:[0,3,1,""],env_is_wscloud:[0,3,1,""],export_model_as_lp:[0,3,1,""],get_data_directory:[0,3,1,""],get_dd_client:[0,3,1,""],get_do_scenario:[0,3,1,""],get_kpis_table_as_dataframe:[0,3,1,""],get_root_directory:[0,3,1,""],load_data:[0,3,1,""],load_data_from_csv:[0,3,1,""],load_data_from_csv_s:[0,3,1,""],load_data_from_excel:[0,3,1,""],load_data_from_excel_s:[0,3,1,""],load_data_from_scenario:[0,3,1,""],load_data_from_scenario_s:[0,3,1,""],print_table_names:[0,3,1,""],replace_data_in_scenario:[0,3,1,""],replace_data_into_scenario_s:[0,3,1,""],update_solve_output_into_scenario:[0,3,1,""],write_data_into_scenario:[0,3,1,""],write_data_into_scenario_s:[0,3,1,""],write_data_to_csv:[0,3,1,""],write_data_to_csv_s:[0,3,1,""],write_data_to_excel:[0,3,1,""],write_data_to_excel_s:[0,3,1,""]},"dse_do_utils.scenariopicker":{ScenarioPicker:[0,2,1,""]},"dse_do_utils.scenariopicker.ScenarioPicker":{ScenarioRefreshButton:[0,2,1,""],default_scenario:[0,4,1,""],get_dd_client:[0,3,1,""],get_scenario_picker_ui:[0,3,1,""],get_scenario_refresh_button:[0,3,1,""],get_scenario_select_drop_down:[0,3,1,""],get_selected_scenario:[0,3,1,""],load_selected_scenario_data:[0,3,1,""],widgets:[0,4,1,""]},"dse_do_utils.utilities":{add_sys_path:[0,1,1,""],list_file_hierarchy:[0,1,1,""]},dse_do_utils:{cpd25utilities:[0,0,0,"-"],datamanager:[0,0,0,"-"],deployeddomodel:[0,0,0,"-"],deployeddomodelcpd21:[0,0,0,"-"],domodelexporter:[0,0,0,"-"],mapmanager:[0,0,0,"-"],module_reload:[0,1,1,""],multiscenariomanager:[0,0,0,"-"],optimizationengine:[0,0,0,"-"],plotly_cpd_workaround:[0,0,0,"-"],plotlymanager:[0,0,0,"-"],scenariodbmanager:[0,0,0,"-"],scenariomanager:[0,0,0,"-"],scenariopicker:[0,0,0,"-"],utilities:[0,0,0,"-"],version:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute"},terms:{"0":[0,1],"02ea6d480895":0,"04":0,"0596001673":0,"085594":0,"1":[0,3],"100":0,"1132":0,"16":0,"1993":0,"2":[0,3],"2005586":0,"2016":0,"21c8ac71":0,"23690284":0,"25a0fe88e4":0,"26c1":0,"3":[0,3],"30080":[],"31":0,"3810":0,"39":0,"4":[0,3],"41":0,"447a":0,"458550":0,"469":0,"49a5":0,"4bd2":0,"5":[0,1],"50":0,"5401":0,"585241":0,"58952002":[],"5de6560a1cfa":0,"6":0,"600":0,"7":3,"785":0,"8021":0,"8364":0,"94":0,"95":0,"96":0,"9f28":0,"abstract":0,"case":[0,3],"class":[0,1],"default":[0,3],"do":0,"enum":0,"export":[0,3],"float":0,"function":[0,3],"import":[0,1],"int":0,"new":0,"return":0,"static":0,"true":0,"while":0,A:[0,3],And:0,As:0,At:0,But:[0,3],By:0,FOR:0,For:[0,3],IF:[],IN:[],If:0,In:[0,3],Is:0,It:[0,3],NOT:[0,3],No:0,Not:0,On:0,One:0,Or:0,That:0,The:[0,3],Then:[0,3],These:0,To:[0,3],Will:0,With:0,_:0,__index__:0,__init__:0,_base:0,_export_:0,_multi_output:0,_output:0,_table_index_:0,_xlx:0,a567:0,a933:0,aa50:0,abbrevi:0,abc:0,abl:[0,3],about:0,abov:0,abspath:0,accept:0,access:[0,3],access_project_or_spac:0,access_tok:0,access_token:0,accesstoken:0,accordingli:0,ad:[0,3],adapt:[],add:[0,3],add_child:0,add_data_file_to_project:0,add_data_file_to_project_:0,add_data_file_using_project_lib:0,add_data_file_using_ws_lib:0,add_data_file_using_ws_lib_:0,add_data_into_scenario:0,add_data_into_scenario_:0,add_file_as_data_asset:0,add_file_as_data_asset_:0,add_file_as_data_asset_cpd25:0,add_file_path_as_data_asset_cpd25:0,add_file_path_as_data_asset_wsc:0,add_full_screen:0,add_layer_control:0,add_mip_progress_kpi:0,add_scenario_name_to_df:0,add_scenario_name_to_fk_constraint:0,add_sys_path:0,add_to:0,addit:0,advanc:0,advantag:0,advis:0,after:0,air:3,alia:0,all:[0,3],allow:[0,3],along:0,alreadi:0,also:[0,3],altern:0,alwai:0,an:[0,3],ani:0,anoth:0,api:[0,3],apicli:0,app:0,appdomain:[],appear:0,appli:[0,3],applic:0,apply_and_concat:0,ar:[0,3],arg:0,argument:0,around:[0,3],arrow:0,asset:[0,3],asset_nam:0,assign:0,associ:[],assum:0,attach:0,attachment_typ:0,attempt:0,attribut:0,auto:[],auto_insert:0,autodetect:0,automat:[0,3],autoreload:0,autoscenariodbt:0,avail:[0,3],avoid:[0,3],axi:0,b7bf7fd8:0,back:0,backward:3,base:0,bash:0,basic:3,bear:0,been:[0,3],befor:0,being:0,below:0,best:0,best_bound_kpi_nam:0,better:0,between:[0,3],bewar:0,binary_var_seri:0,binary_var_series_:0,binaryvartyp:0,blank:0,blob:0,bludb:[],blue:0,bobhaffn:0,bool:0,both:[0,3],bound:0,box:0,br:0,build:0,builder:[0,1],bulk:0,business_kpi:0,businesskpit:0,button:0,c:0,cach:0,call:0,callback:0,came:0,camel_case_to_snake_cas:0,camelcas:0,can:[0,3],cancel:0,cannot:[0,3],cartesian:0,cascad:[],categor:0,categori:0,caus:0,cell:[0,3],certain:0,certif:0,ch04s23:0,challeng:0,chang:0,channel:3,charact:0,chart:0,check:0,child:0,clean:0,cleanup:0,clear:0,clear_scenario_data:0,client:[0,3],cloud:[0,3],cluster4:0,cluster:0,cluster_nam:0,co:0,code:[0,3],cogno:0,collabor:0,collect:[],color:0,column:0,column_nam:0,columns_metadata:0,com:0,combin:[0,3],command:0,commun:0,compass:0,compat:[0,3],complet:0,complex:0,compon:0,comput:0,conda:3,condit:0,conf:0,configur:[0,3],connect:[0,3],constant:0,constraint:[],constraints_metadata:0,constructor:0,contain:[0,3],content:2,context:0,continuous_var_seri:0,continuous_var_series_:0,continuousvartyp:0,control:0,conveni:0,convers:0,convert:0,cookbook:0,coord:0,copi:0,copy_to_csv:0,core:0,corner:0,correctli:0,could:0,count:0,counti:0,cp4d25:0,cp4daa:0,cp4dv2:0,cp4dv4:0,cp:[0,3],cpd25:0,cpd25util:2,cpd2:0,cpd3:[0,3],cpd40:0,cpd4:3,cpd:[0,3],cpd_cluster_host:0,cpdaa:0,cpdv2:[0,1],cpdv3:[0,3],cpdv4:[0,1],cplex:[0,3],cpo:0,cpolab:0,cpsaa:0,creat:[0,3],create_blank_map:0,create_database_engin:[],create_db2_engin:[],create_new_scenario:0,create_schema:0,create_sqllite_engin:[],create_table_metadata:0,creation:0,credenti:0,cross:0,crossjoin:3,csv:[0,3],csv_directori:0,csv_name_pattern:0,curl:0,current:0,current_dir:0,cursor:[],custom:0,d4c69a0d8158:0,d:[0,3],dash:0,dashboard:0,dashenterpris:0,data:[0,3],data_asset:[0,3],data_by_scenario:0,data_id:0,data_manag:0,data_url:0,databas:[],datafram:0,datamanag:[2,3],datascienceelit:0,dataset:0,datetim:0,db2:[],db2_credenti:[],db:0,db_tabl:0,db_table_nam:0,dd:[0,3],dd_scenario:0,de:0,debug:0,debug_file_data_url:0,debug_file_dir:0,decis:3,decision_optimization_cli:0,def:0,default_max_oaas_time_limit_sec:0,default_max_run_time_sec:0,default_scenario:0,default_valu:0,defin:0,definit:0,delai:0,delet:0,delete_scenario_from_db:[],delete_scenario_name_column:0,depend:[0,3],deploi:0,deploy:[0,3],deployed_model_nam:0,deployeddomodel:2,deployeddomodel_cpd21:0,deployeddomodelcpd21:2,deployment_id:0,deprec:0,design:[0,3],despit:0,detail:3,detect_platform:0,develop:[0,3],df1:0,df2:0,df:0,df_column_names_to_snake_cas:0,df_crossjoin_ai:0,df_crossjoin_mi:0,df_crossjoin_si:0,df_dict:0,dict:0,dictionari:0,differ:[0,3],direct:0,directori:0,disadvantag:0,disk:0,dm:0,do4w:0,do_model_nam:0,do_util:0,doc:0,docplex:[0,3],document:[0,3],dodashapp:0,doe:0,doesn:0,domodeldeploy:2,domodelexport:2,don:[],done:0,down:[0,3],download:[0,3],driven:0,drop:[0,3],drop_all_t:0,drop_column_nam:0,dropdown:0,dse:[0,3],dse_do_dashboard:0,dse_do_util:1,dsx:0,dsx_project_dir:0,dsxuser:0,due:0,dump:0,dumpzipnam:0,dure:0,dvar:[0,3],e:[0,3],each:0,easi:3,easier:0,echo:0,ef365b2c:0,effect:0,eg:0,either:0,element:0,els:0,emb:0,empti:[0,3],en:0,enable_sqlite_fk:0,enable_transact:0,encod:[],end:0,engin:[0,3],ensur:0,entiti:0,entri:0,enumer:0,env_is_cpd25:0,env_is_cpd40:0,env_is_dsx:0,env_is_wscloud:0,environ:[0,1],error:0,establish:0,evalu:0,exampl:0,excel:[0,3],excel_file_nam:0,excel_output_file_nam:0,excel_test:0,excelfil:0,excelwrit:0,exclud:0,execut:0,execute_model:0,execution_result:0,execution_statu:0,execution_status_json:0,exist:[0,3],expect:0,experi:0,explicit:0,explicitli:0,export_as_cpo:0,export_as_cpo_:0,export_as_lp:0,export_as_lp_:0,export_do_model:0,export_model:0,export_model_as_lp:0,expos:0,express:0,extend:0,extended_columns_metadata:0,extens:0,extract:0,extract_dvar_nam:0,extract_solut:0,f:0,fail:0,fake:3,fals:0,faster:0,featur:3,featuregroup:0,field:0,fig:0,file:[0,3],file_nam:0,file_path:0,filenam:0,filter:0,find:0,firefox:0,first:0,fk:[],fkc:0,flask:0,folder:0,folium:[0,3],follow:[0,3],forc:0,foreign:0,foreignkeyconstraint:0,form:0,format:0,former:[0,3],found:0,frame:0,free:0,from:[0,3],full:0,full_project_nam:0,func:0,futur:[0,3],g:[0,3],gap:[0,3],gener:0,get:0,get_access_token_curl:0,get_access_token_web:0,get_all_scenario_nam:0,get_arrow:0,get_bear:0,get_dash_tab_layout_m:0,get_data_directori:0,get_db2_connection_str:[],get_db_table_nam:0,get_dd_client:0,get_debug_dump_name_and_url:0,get_debug_file_url:0,get_deployment_id:0,get_df_column_nam:0,get_do_model_export_curl:0,get_do_model_export_web:0,get_do_scenario:0,get_execution_service_model_url:0,get_execution_statu:0,get_head:0,get_html_tabl:0,get_input_fil:0,get_job_statu:0,get_job_url:0,get_kill_job_url:0,get_kpi_output_t:0,get_kpis_table_as_datafram:0,get_log_file_name_and_url:0,get_log_file_url:0,get_multi_scenario_data:0,get_object:0,get_output:0,get_parameter_valu:0,get_plotly_fig_m:0,get_popup_t:0,get_project_id:0,get_raw_table_by_nam:0,get_root_directori:0,get_scenario_picker_ui:0,get_scenario_refresh_button:0,get_scenario_select_drop_down:0,get_scenarios_df:0,get_selected_scenario:0,get_solution_name_and_url:0,get_solve_config:0,get_solve_detail:0,get_solve_details_object:0,get_solve_payload:0,get_solve_statu:0,get_solve_url:0,get_space_id:0,get_stop_job_url:0,get_tab_layout_:0,getcwd:0,gist:0,git:0,github:[0,3],githubpag:3,give:0,given:[],glob:0,grand:0,greatli:3,gsilabs_scnfo:0,h:0,ha:[0,3],hack:3,hand:0,happen:0,hard:0,have:[0,3],height:0,hello:0,here:0,hierarch:0,hold:0,home:0,host:[],hostnam:[],how:[0,3],howev:0,html:0,http:0,hypothet:0,i:0,ibm:[0,3],ibm_watson_machine_learn:0,ibm_watson_studio_lib:0,icon:0,icpd:3,id:0,ignor:[0,3],imp:0,impact:3,implement:0,improv:3,includ:0,inconsist:0,independ:0,index:[0,1],indic:0,individu:3,info:0,inform:0,init:0,initi:0,initialize_db_tables_metadata:[],inline_t:0,inner:0,input:0,input_csv_name_pattern:0,input_db_t:0,input_table_nam:0,insecurerequestwarn:0,insert:0,insert_scenarios_in_db:0,insert_single_scenario_tables_in_db:[],insert_table_in_db:[],insert_table_in_db_bulk:0,insert_tables_in_db:0,instal:[0,1],installationreadm:3,instanc:0,instead:0,integ:0,integer_var_list:0,integer_var_seri:0,integer_var_series_:0,integervartyp:0,intend:3,interact:[0,3],interfac:3,intermedi:0,intern:[0,3],internet:3,interrupt:0,interv:0,io:0,ipynb:0,ipywidget:0,issu:0,itself:3,jerom:0,job:0,job_config_json:0,job_detail:0,job_uid:0,join:0,json:0,jupyt:[0,3],jupyterlab:3,just:0,kansas_city_coord:0,karg:0,keep:0,kei:0,keyword:0,kill:0,kill_job:0,km:0,kpi:0,kpitabl:0,kwarg:0,lambda:0,last:0,lat:0,later:0,latest:0,layer_control_posit:0,leav:0,left:0,let:0,level:0,lib:0,librari:0,like:[0,3],limit:[0,3],line:0,list:0,list_file_hierarchi:0,lite:[],load:0,load_data:0,load_data_from_csv:0,load_data_from_csv_:0,load_data_from_excel:0,load_data_from_excel_:0,load_data_from_scenario:0,load_data_from_scenario_:0,load_from_excel:0,load_selected_scenario_data:0,local:[0,3],local_root:0,locat:0,log:0,log_file_nam:0,logic:0,lon:0,longer:[0,3],look:0,loop:0,lp:[0,3],m:0,machin:0,made:0,mai:0,main:[0,1],maintain:0,major:0,make:0,manag:0,mani:3,manipul:[0,3],manual:0,map:[0,3],mapmanag:[2,3],marker:0,master:0,match:0,max_oaas_time_limit_sec:0,max_run_time_sec:0,maximum:0,mb:3,md:3,mdl:0,me:[0,1],mean:0,medium:0,member:0,menu:[0,3],merg:0,merge_scenario_data:0,messag:0,metadata:0,method:0,mgr:0,might:3,minu:0,mip_gap_kpi_nam:0,miss:0,mix:0,mkonrad:0,mode:0,model1:0,model2:0,model:[0,1],model_build:0,model_nam:0,modelbuild:0,modifi:0,modul:[1,2],module_reload:0,moment:3,monitor:0,monitor_execut:0,monitor_loop_delay_sec:0,more:[0,3],most:0,mostli:3,move:3,mp:0,msm:0,multi:0,multi_scenario:0,multiindex:0,multipl:[0,3],multiscenariomanag:2,must:0,my:0,my_default_scenario:0,my_do_model:0,my_funct:0,my_input_column_nam:0,my_input_valu:0,my_output_column_name_1:0,my_output_column_name_2:0,my_output_value1:0,my_output_value_1:0,my_output_value_2:0,my_schema:[],my_tabl:0,myexcelfil:0,myexcelfileoutput:0,myfil:0,mymodel:0,myoptimizationengin:0,myprogresslisten:0,mytabl:0,n:0,n_arrow:0,name:0,namedtupl:0,nbviewer:0,necessari:0,need:0,neither:3,net:0,never:0,new_path:0,new_scenario_nam:0,next:0,nodefault:3,non:0,none:0,not_start:0,note:[0,3],notebook:[0,3],notify_progress:0,now:0,number:0,oaa:0,object:0,off:0,ok:0,onc:0,one:0,ones:0,onli:[0,3],open:0,oper:0,optim:[0,3],optimizationengin:[2,3],option:[0,3],order:0,ordereddict:0,ore:3,oreilli:0,org:0,organ:0,origin:0,os:0,other:[0,3],otherwis:0,out:0,output:0,output_csv_name_pattern:0,output_db_t:0,output_table_nam:0,outsid:3,overrid:0,overwrit:0,overwritten:0,p1:0,p2:0,packag:[1,2,3],page:[0,1],page_id:0,page_sourc:0,paid:[],pak:3,panda:0,panel:0,param:0,param_nam:0,param_typ:0,paramet:0,parametert:0,pardir:0,parent:0,parent_dir:0,parent_dir_2:0,pars:0,parse_html:0,part:0,particular:[0,3],pass:0,password1:[],password:0,past:0,path:0,pattern:0,payload:0,pd:0,per:0,perform:3,period:0,person:0,phase:0,pick:3,picker:0,pip:[0,3],place:0,placehold:0,plain:0,platform:0,plot:0,plotli:0,plotly_cpd_workaround:2,plotlymanag:2,plugin:0,point:0,poll:0,polylin:0,popul:0,popup:0,popup_t:0,port:[],possibl:0,post:[0,3],post_process_contain:0,post_process_container_get_datafram:0,post_process_fail:0,post_process_inline_t:0,post_process_inline_table_get_datafram:0,post_process_interrupt:0,post_process_process:0,practic:0,pre:[0,3],prefer:3,prep_paramet:0,prepar:0,prepare_data_fram:0,prepare_input_data_fram:0,prepare_output_data_fram:0,prevent:0,previou:3,primari:0,print:0,print_hello:0,print_inputs_outputs_summari:0,print_table_nam:0,problem:0,procedur:0,process:[0,3],product:0,productionplan:0,progress:0,progress_data:0,progressdata:0,project:[0,3],project_access_token:0,project_data:[0,3],project_id:0,project_lib:0,project_nam:0,properli:[],properti:0,property_1:0,property_2:0,provid:[0,3],put:3,pwd:0,py:0,pydata:0,pypi:3,python:[0,3],question:0,queu:0,quot:0,rais:0,raw:0,re:0,reach:0,read:[0,1],read_csv:0,read_scenario_from_db:0,read_scenario_table_from_db:0,read_scenario_table_from_db_cach:0,read_scenario_table_from_db_callback:0,read_scenario_tables_from_db:0,read_scenario_tables_from_db_cach:0,read_scenarios_from_db:0,read_scenarios_table_from_db_cach:0,read_scenarios_table_from_db_callback:0,readthedoc:0,reason:0,record:0,reduc:0,refactor:3,refer:0,refine_conflict:0,refresh:0,regist:0,regular:0,regularli:[0,3],rel:0,relat:0,relationship:0,releas:3,relev:0,reliabl:0,reload:0,remot:0,remov:0,renam:0,repeatedli:0,replac:0,replace_data_in_scenario:0,replace_data_into_scenario_:0,replace_scenario_in_db:0,repositori:3,repres:0,represent:0,request:0,requir:[0,1],rerun:0,resourc:0,respons:0,rest:0,restor:0,restrict:0,result:0,retreiv:0,retriev:0,retrieve_debug_materi:0,retrieve_fil:0,retrieve_solut:0,retrieve_solve_configur:0,revers:[],right:0,root:0,root_dir:0,rotat:0,round:0,rout:0,routin:0,row:0,run:[0,3],runtime_env_apsx_url:0,s:0,same:[0,3],save:0,scenario:[0,3],scenario_1:0,scenario_nam:0,scenario_table_nam:0,scenariodbmanag:2,scenariodbt:0,scenariomanag:[2,3],scenariopick:[2,3],scenariorefreshbutton:0,scenarios_table_read_callback:0,scenariot:0,schema:0,scnfo:0,scnfo_dash_pycharm_github:0,scope:1,screen:0,script:3,search:[0,1],second:0,secur:[],see:[0,3],seem:0,select:0,self:0,separ:[0,3],sequenc:0,seri:0,server:0,servic:0,service_configuration_json:0,service_nam:0,session:[],set:0,set_index:0,set_output_settings_in_solve_configur:0,set_scenarios_table_read_callback:0,set_table_read_callback:0,setup:0,share:3,sheet:0,should:[0,3],show:0,side:0,signific:3,similar:0,simpl:0,simplest:0,simpli:0,sinc:0,singl:0,site:0,size:0,skip:0,sm:0,small:0,snake_cas:0,so:0,solut:0,solution_count_kpi_nam:0,solutionlisten:0,solv:0,solve_config:0,solve_config_json:0,solve_payload:0,solve_phase_kpi_nam:0,solve_statu:0,solve_time_kpi_nam:0,solvesolut:0,some:[0,3],someth:0,somewhat:0,sourc:[0,3],sp:0,space:0,space_id:0,space_nam:0,spd:0,specif:[],specifi:0,speed:0,spreadhseet:3,spreadsheet:0,sql:0,sqlalchemi:0,sqlcol:0,ssl:0,stackoverflow:0,stamp:0,standard:0,start:0,startpath:0,state:0,statement:0,statu:0,step:0,stop:0,stop_job:0,storag:0,store:[0,3],str:0,strftime:0,string:0,strongli:0,structur:0,studio:[0,3],sub:0,subclass:[0,3],submiss:0,submodul:2,succesfulli:0,suffici:0,suggest:0,summari:0,support:0,suppress:0,suppress_warn:0,sure:0,sy:0,system:[0,3],t:0,tabl:[0,3],table_index_sheet:0,table_nam:0,table_read_callback:0,target:[0,1],task:0,templat:0,template_scenario_nam:0,temporari:0,termin:0,test:0,text:0,than:0,thei:0,them:0,therebi:0,therefor:[0,3],thi:[0,3],thing:0,those:0,thu:0,time:0,todo:0,token:0,tooltip:0,top:0,topleft:0,track:0,transact:0,translat:0,tree:0,truth:0,tupl:0,turn:0,two:0,txt:0,type:0,typic:[0,3],ugli:0,ui:0,un:0,under:0,unfortun:0,uniqu:0,unknown:0,unnam:0,unverifi:0,up:[0,3],updat:0,update_solve_output_into_scenario:0,upload:[0,3],url:0,urllib3:0,us:[0,3],usag:[0,3],user1:[],user:0,user_access_token:0,user_nam:0,usernam:[],util:[2,3],v0:3,v2:3,v4:3,valid:0,valu:0,value_1:0,value_2:0,value_format:0,valueerror:0,variabl:0,venv:0,veri:3,verif:0,verifi:0,verify_integr:0,version:[2,3],via:0,view:0,violat:[],visibl:0,visual:[0,3],w:0,wa:0,wai:0,want:0,warehous:[],warn:0,watson:[0,3],watsonmachinelearningapicli:0,we:0,web:0,well:0,what:0,whatev:0,wheel:3,when:[0,3],where:0,which:0,whole:0,widget:0,widget_button:0,widget_select:0,width:0,wil:0,window:0,within:[0,3],without:0,wml:1,wml_credenti:0,work:[0,3],workaround:[],world:0,worri:[],would:0,write:[0,3],write_data_asset_as_file_cpd25:0,write_data_asset_as_file_wsc:0,write_data_into_scenario:0,write_data_into_scenario_:0,write_data_to_csv:0,write_data_to_csv_:0,write_data_to_excel:0,write_data_to_excel_:0,write_do_model_to_fil:0,writer:0,written:0,ws:[0,3],wsl1:0,wsl:[0,3],wslib:0,wslv1:3,wsuser:0,www:0,xdvar:0,xl:0,xlsx:0,xlx:0,xxxxxx:0,y:0,yet:0,you:0,your:0,yyyymmdd_hhmm:0,zip:[0,3],zoom_start:0},titles:["dse_do_utils package","Welcome to DSE DO Utils documentation!","dse_do_utils","Read me"],titleterms:{"0":3,"5":3,"class":3,"do":[1,3],"import":3,builder:3,content:[0,1],cpd25util:0,cpdv2:3,cpdv4:3,custom:3,datamanag:0,deployeddomodel:0,deployeddomodelcpd21:0,document:1,domodeldeploy:0,domodelexport:0,dse:1,dse_do_util:[0,2,3],environ:3,indic:1,instal:3,main:3,mapmanag:0,me:3,model:3,modul:[0,3],multiscenariomanag:0,optimizationengin:0,packag:0,plotly_cpd_workaround:0,plotlymanag:0,read:3,requir:3,scenariodbmanag:0,scenariomanag:0,scenariopick:0,scope:3,submodul:0,tabl:1,target:3,util:[0,1],version:0,welcom:1,wml:3}}) \ No newline at end of file +Search.setIndex({docnames:["dse_do_utils","index","modules","readme_link"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":4,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1,sphinx:56},filenames:["dse_do_utils.rst","index.rst","modules.rst","readme_link.rst"],objects:{"":{dse_do_utils:[0,0,0,"-"]},"dse_do_utils.cpd25utilities":{add_file_as_data_asset_cpd25:[0,1,1,""],add_file_path_as_data_asset_cpd25:[0,1,1,""],add_file_path_as_data_asset_wsc:[0,1,1,""],write_data_asset_as_file_cpd25:[0,1,1,""],write_data_asset_as_file_wsc:[0,1,1,""]},"dse_do_utils.datamanager":{DataManager:[0,2,1,""]},"dse_do_utils.datamanager.DataManager":{apply_and_concat:[0,3,1,""],df_crossjoin_ai:[0,3,1,""],df_crossjoin_mi:[0,3,1,""],df_crossjoin_si:[0,3,1,""],extract_solution:[0,3,1,""],get_parameter_value:[0,3,1,""],get_raw_table_by_name:[0,3,1,""],prep_parameters:[0,3,1,""],prepare_data_frames:[0,3,1,""],prepare_input_data_frames:[0,3,1,""],prepare_output_data_frames:[0,3,1,""],print_hello:[0,3,1,""],print_inputs_outputs_summary:[0,3,1,""]},"dse_do_utils.deployeddomodel":{DeployedDOModel:[0,2,1,""]},"dse_do_utils.deployeddomodel.DeployedDOModel":{execute_model:[0,3,1,""],extract_solution:[0,3,1,""],get_deployment_id:[0,3,1,""],get_job_status:[0,3,1,""],get_outputs:[0,3,1,""],get_solve_details:[0,3,1,""],get_solve_details_objective:[0,3,1,""],get_solve_payload:[0,3,1,""],get_solve_status:[0,3,1,""],get_space_id:[0,3,1,""],monitor_execution:[0,3,1,""],solve:[0,3,1,""]},"dse_do_utils.deployeddomodelcpd21":{DeployedDOModel_CPD21:[0,2,1,""]},"dse_do_utils.deployeddomodelcpd21.DeployedDOModel_CPD21":{cleanup:[0,3,1,""],execute_model:[0,3,1,""],get_debug_dump_name_and_url:[0,3,1,""],get_debug_file_url:[0,3,1,""],get_execution_service_model_url:[0,3,1,""],get_execution_status:[0,3,1,""],get_headers:[0,3,1,""],get_input_files:[0,3,1,""],get_job_url:[0,3,1,""],get_kill_job_url:[0,3,1,""],get_log_file_name_and_url:[0,3,1,""],get_log_file_url:[0,3,1,""],get_objective:[0,3,1,""],get_solution_name_and_url:[0,3,1,""],get_solve_config:[0,3,1,""],get_solve_status:[0,3,1,""],get_solve_url:[0,3,1,""],get_stop_job_url:[0,3,1,""],kill_job:[0,3,1,""],monitor_execution:[0,3,1,""],post_process_container:[0,3,1,""],post_process_container_get_dataframe:[0,3,1,""],post_process_failed:[0,3,1,""],post_process_inline_table:[0,3,1,""],post_process_inline_table_get_dataframe:[0,3,1,""],post_process_interrupted:[0,3,1,""],post_process_processed:[0,3,1,""],retrieve_debug_materials:[0,3,1,""],retrieve_file:[0,3,1,""],retrieve_solution:[0,3,1,""],retrieve_solve_configuration:[0,3,1,""],set_output_settings_in_solve_configuration:[0,3,1,""],solve:[0,3,1,""],stop_job:[0,3,1,""]},"dse_do_utils.domodelexporter":{DOModelExporter:[0,2,1,""]},"dse_do_utils.domodelexporter.DOModelExporter":{export_do_models:[0,3,1,""],get_access_token_curl:[0,3,1,""],get_access_token_web:[0,3,1,""],get_do_model_export_curl:[0,3,1,""],get_do_model_export_web:[0,3,1,""],get_project_id:[0,3,1,""],write_do_model_to_file:[0,3,1,""]},"dse_do_utils.mapmanager":{MapManager:[0,2,1,""]},"dse_do_utils.mapmanager.MapManager":{add_full_screen:[0,3,1,""],add_layer_control:[0,3,1,""],create_blank_map:[0,3,1,""],get_arrows:[0,3,1,""],get_bearing:[0,3,1,""],get_html_table:[0,3,1,""],get_popup_table:[0,3,1,""],kansas_city_coord:[0,4,1,""]},"dse_do_utils.multiscenariomanager":{MultiScenarioManager:[0,2,1,""]},"dse_do_utils.multiscenariomanager.MultiScenarioManager":{add_data_file_to_project:[0,3,1,""],env_is_wscloud:[0,3,1,""],get_all_scenario_names:[0,3,1,""],get_data_directory:[0,3,1,""],get_dd_client:[0,3,1,""],get_multi_scenario_data:[0,3,1,""],get_root_directory:[0,3,1,""],get_scenarios_df:[0,3,1,""],load_data_from_scenario:[0,3,1,""],merge_scenario_data:[0,3,1,""],write_data_to_excel:[0,3,1,""]},"dse_do_utils.optimizationengine":{MyProgressListener:[0,2,1,""],OptimizationEngine:[0,2,1,""]},"dse_do_utils.optimizationengine.MyProgressListener":{notify_progress:[0,3,1,""]},"dse_do_utils.optimizationengine.OptimizationEngine":{add_mip_progress_kpis:[0,3,1,""],binary_var_series:[0,3,1,""],binary_var_series_s:[0,3,1,""],continuous_var_series:[0,3,1,""],continuous_var_series_s:[0,3,1,""],export_as_cpo:[0,3,1,""],export_as_cpo_s:[0,3,1,""],export_as_lp:[0,3,1,""],export_as_lp_s:[0,3,1,""],get_kpi_output_table:[0,3,1,""],integer_var_series:[0,3,1,""],integer_var_series_s:[0,3,1,""],solve:[0,3,1,""]},"dse_do_utils.plotlymanager":{PlotlyManager:[0,2,1,""]},"dse_do_utils.plotlymanager.PlotlyManager":{get_dash_tab_layout_m:[0,3,1,""],get_plotly_fig_m:[0,3,1,""]},"dse_do_utils.scenariodbmanager":{AutoScenarioDbTable:[0,2,1,""],BusinessKpiTable:[0,2,1,""],DbCellUpdate:[0,2,1,""],KpiTable:[0,2,1,""],ParameterTable:[0,2,1,""],ScenarioDbManager:[0,2,1,""],ScenarioDbTable:[0,2,1,""],ScenarioTable:[0,2,1,""]},"dse_do_utils.scenariodbmanager.AutoScenarioDbTable":{create_table_metadata:[0,3,1,""],insert_table_in_db_bulk:[0,3,1,""]},"dse_do_utils.scenariodbmanager.DbCellUpdate":{column_name:[0,4,1,""],current_value:[0,4,1,""],previous_value:[0,4,1,""],row_idx:[0,4,1,""],row_index:[0,4,1,""],scenario_name:[0,4,1,""],table_name:[0,4,1,""]},"dse_do_utils.scenariodbmanager.ScenarioDbManager":{add_scenario_name_to_dfs:[0,3,1,""],create_schema:[0,3,1,""],delete_scenario_from_db:[0,3,1,""],delete_scenario_name_column:[0,3,1,""],drop_all_tables:[0,3,1,""],duplicate_scenario_in_db:[0,3,1,""],get_scenario_db_table:[0,3,1,""],get_scenarios_df:[0,3,1,""],insert_scenarios_in_db:[0,3,1,""],insert_tables_in_db:[0,3,1,""],read_scenario_from_db:[0,3,1,""],read_scenario_input_tables_from_db:[0,3,1,""],read_scenario_table_from_db:[0,3,1,""],read_scenario_table_from_db_cached:[0,3,1,""],read_scenario_tables_from_db:[0,3,1,""],read_scenario_tables_from_db_cached:[0,3,1,""],read_scenarios_from_db:[0,3,1,""],read_scenarios_table_from_db_cached:[0,3,1,""],rename_scenario_in_db:[0,3,1,""],replace_scenario_in_db:[0,3,1,""],replace_scenario_tables_in_db:[0,3,1,""],set_scenarios_table_read_callback:[0,3,1,""],set_table_read_callback:[0,3,1,""],update_cell_changes_in_db:[0,3,1,""],update_scenario_output_tables_in_db:[0,3,1,""]},"dse_do_utils.scenariodbmanager.ScenarioDbTable":{add_scenario_name_to_fk_constraint:[0,3,1,""],camel_case_to_snake_case:[0,3,1,""],create_table_metadata:[0,3,1,""],df_column_names_to_snake_case:[0,3,1,""],get_db_table_name:[0,3,1,""],get_df_column_names:[0,3,1,""],get_sa_column:[0,3,1,""],get_sa_table:[0,3,1,""],insert_table_in_db_bulk:[0,3,1,""],sqlcol:[0,3,1,""]},"dse_do_utils.scenariomanager":{Platform:[0,2,1,""],ScenarioManager:[0,2,1,""]},"dse_do_utils.scenariomanager.Platform":{CPD25:[0,4,1,""],CPD40:[0,4,1,""],CPDaaS:[0,4,1,""],Local:[0,4,1,""]},"dse_do_utils.scenariomanager.ScenarioManager":{add_data_file_to_project_s:[0,3,1,""],add_data_file_using_project_lib:[0,3,1,""],add_data_file_using_ws_lib:[0,3,1,""],add_data_file_using_ws_lib_s:[0,3,1,""],add_data_into_scenario:[0,3,1,""],add_data_into_scenario_s:[0,3,1,""],add_file_as_data_asset:[0,3,1,""],add_file_as_data_asset_s:[0,3,1,""],clear_scenario_data:[0,3,1,""],create_new_scenario:[0,3,1,""],detect_platform:[0,3,1,""],env_is_cpd25:[0,3,1,""],env_is_cpd40:[0,3,1,""],env_is_dsx:[0,3,1,""],env_is_wscloud:[0,3,1,""],export_model_as_lp:[0,3,1,""],get_data_directory:[0,3,1,""],get_dd_client:[0,3,1,""],get_do_scenario:[0,3,1,""],get_kpis_table_as_dataframe:[0,3,1,""],get_root_directory:[0,3,1,""],load_data:[0,3,1,""],load_data_from_csv:[0,3,1,""],load_data_from_csv_s:[0,3,1,""],load_data_from_excel:[0,3,1,""],load_data_from_excel_s:[0,3,1,""],load_data_from_scenario:[0,3,1,""],load_data_from_scenario_s:[0,3,1,""],print_table_names:[0,3,1,""],replace_data_in_scenario:[0,3,1,""],replace_data_into_scenario_s:[0,3,1,""],update_solve_output_into_scenario:[0,3,1,""],write_data_into_scenario:[0,3,1,""],write_data_into_scenario_s:[0,3,1,""],write_data_to_csv:[0,3,1,""],write_data_to_csv_s:[0,3,1,""],write_data_to_excel:[0,3,1,""],write_data_to_excel_s:[0,3,1,""]},"dse_do_utils.scenariopicker":{ScenarioPicker:[0,2,1,""]},"dse_do_utils.scenariopicker.ScenarioPicker":{ScenarioRefreshButton:[0,2,1,""],default_scenario:[0,4,1,""],get_dd_client:[0,3,1,""],get_scenario_picker_ui:[0,3,1,""],get_scenario_refresh_button:[0,3,1,""],get_scenario_select_drop_down:[0,3,1,""],get_selected_scenario:[0,3,1,""],load_selected_scenario_data:[0,3,1,""],widgets:[0,4,1,""]},"dse_do_utils.utilities":{add_sys_path:[0,1,1,""],list_file_hierarchy:[0,1,1,""]},dse_do_utils:{cpd25utilities:[0,0,0,"-"],datamanager:[0,0,0,"-"],deployeddomodel:[0,0,0,"-"],deployeddomodelcpd21:[0,0,0,"-"],domodelexporter:[0,0,0,"-"],mapmanager:[0,0,0,"-"],module_reload:[0,1,1,""],multiscenariomanager:[0,0,0,"-"],optimizationengine:[0,0,0,"-"],plotly_cpd_workaround:[0,0,0,"-"],plotlymanager:[0,0,0,"-"],scenariodbmanager:[0,0,0,"-"],scenariomanager:[0,0,0,"-"],scenariopicker:[0,0,0,"-"],utilities:[0,0,0,"-"],version:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:method","4":"py:attribute"},terms:{"0":[0,1],"02ea6d480895":0,"04":0,"0596001673":0,"085594":0,"1":[0,3],"100":0,"1132":0,"16":0,"1993":0,"2":[0,3],"2005586":0,"2016":0,"21c8ac71":0,"23690284":0,"25a0fe88e4":0,"26c1":0,"3":[0,3],"31":0,"3810":0,"39":0,"4":[0,3],"41":0,"447a":0,"458550":0,"469":0,"49a5":0,"4bd2":0,"5":[0,1],"50":0,"5401":0,"585241":0,"5de6560a1cfa":0,"6":0,"600":0,"7":3,"785":0,"8021":0,"8364":0,"94":0,"95":0,"96":0,"9f28":0,"abstract":0,"case":[0,3],"class":[0,1],"default":[0,3],"do":0,"enum":0,"export":[0,3],"float":0,"function":[0,3],"import":[0,1],"int":0,"new":0,"return":0,"static":0,"true":0,"while":0,A:[0,3],And:0,As:0,At:0,But:[0,3],By:0,FOR:0,For:[0,3],If:0,In:[0,3],Is:0,It:[0,3],NOT:[0,3],No:0,Not:0,On:0,One:0,Or:0,That:0,The:[0,3],Then:[0,3],These:0,To:[0,3],Will:0,With:0,_:0,__index__:0,__init__:0,_base:0,_export_:0,_multi_output:0,_output:0,_table_index_:0,_xlx:0,a567:0,a933:0,aa50:0,abbrevi:0,abc:0,abl:[0,3],about:0,abov:0,abspath:0,accept:0,access:[0,3],access_project_or_spac:0,access_tok:0,access_token:0,accesstoken:0,accordingli:0,ad:[0,3],add:[0,3],add_child:0,add_data_file_to_project:0,add_data_file_to_project_:0,add_data_file_using_project_lib:0,add_data_file_using_ws_lib:0,add_data_file_using_ws_lib_:0,add_data_into_scenario:0,add_data_into_scenario_:0,add_file_as_data_asset:0,add_file_as_data_asset_:0,add_file_as_data_asset_cpd25:0,add_file_path_as_data_asset_cpd25:0,add_file_path_as_data_asset_wsc:0,add_full_screen:0,add_layer_control:0,add_mip_progress_kpi:0,add_scenario_name_to_df:0,add_scenario_name_to_fk_constraint:0,add_sys_path:0,add_to:0,addit:0,advanc:0,advantag:0,advis:0,after:0,air:3,alia:0,all:[0,3],allow:[0,3],along:0,alreadi:0,also:[0,3],altern:0,alwai:0,an:[0,3],ani:0,anoth:0,api:[0,3],apicli:0,app:0,appear:0,appli:[0,3],applic:0,apply_and_concat:0,ar:[0,3],arg:0,argument:0,around:[0,3],arrow:0,asset:[0,3],asset_nam:0,assign:0,assum:0,attach:0,attachment_typ:0,attempt:0,attribut:0,auto_insert:0,autodetect:0,automat:[0,3],autoreload:0,autoscenariodbt:0,avail:[0,3],avoid:[0,3],axi:0,b7bf7fd8:0,back:0,backward:3,base:0,bash:0,basic:3,bear:0,been:[0,3],befor:0,being:0,below:0,best:0,best_bound_kpi_nam:0,better:0,between:[0,3],bewar:0,binary_var_seri:0,binary_var_series_:0,binaryvartyp:0,blank:0,blob:0,blue:0,bobhaffn:0,bool:0,both:[0,3],bound:0,box:0,br:0,build:0,builder:[0,1],bulk:0,business_kpi:0,businesskpit:0,button:0,c:0,cach:0,call:0,callback:0,came:0,camel_case_to_snake_cas:0,camelcas:0,can:[0,3],cancel:0,cannot:[0,3],cartesian:0,categor:0,categori:0,caus:0,cell:[0,3],certain:0,certif:0,ch04s23:0,challeng:0,chang:0,channel:3,charact:0,chart:0,check:0,child:0,clean:0,cleanup:0,clear:0,clear_scenario_data:0,client:[0,3],cloud:[0,3],cluster4:0,cluster:0,cluster_nam:0,co:0,code:[0,3],cogno:0,collabor:0,color:0,column:0,column_nam:0,columns_metadata:0,com:0,combin:[0,3],command:0,commun:0,compass:0,compat:[0,3],complet:0,complex:0,compon:0,comput:0,conda:3,condit:0,conf:0,configur:[0,3],connect:[0,3],constant:0,constraints_metadata:0,constructor:0,contain:[0,3],content:2,context:0,continuous_var_seri:0,continuous_var_series_:0,continuousvartyp:0,control:0,conveni:0,convers:0,convert:0,cookbook:0,coord:0,copi:0,copy_to_csv:0,core:0,corner:0,correctli:0,could:0,count:0,counti:0,cp4d25:0,cp4daa:0,cp4dv2:0,cp4dv4:0,cp:[0,3],cpd25:0,cpd25util:2,cpd2:0,cpd3:[0,3],cpd40:0,cpd4:3,cpd:[0,3],cpd_cluster_host:0,cpdaa:0,cpdv2:[0,1],cpdv3:[0,3],cpdv4:[0,1],cplex:[0,3],cpo:0,cpolab:0,cpsaa:0,creat:[0,3],create_blank_map:0,create_new_scenario:0,create_schema:0,create_table_metadata:0,creation:0,credenti:0,cross:0,crossjoin:3,csv:[0,3],csv_directori:0,csv_name_pattern:0,curl:0,current:0,current_dir:0,current_valu:0,custom:0,d4c69a0d8158:0,d:[0,3],dash:0,dashboard:0,dashenterpris:0,data:[0,3],data_asset:[0,3],data_by_scenario:0,data_id:0,data_manag:0,data_url:0,datafram:0,datamanag:[2,3],datascienceelit:0,dataset:0,datetim:0,db:0,db_cell_upd:0,db_column_nam:0,db_table_nam:0,dbcellupd:0,dd:[0,3],dd_scenario:0,de:0,debug:0,debug_file_data_url:0,debug_file_dir:0,decis:3,decision_optimization_cli:0,def:0,default_max_oaas_time_limit_sec:0,default_max_run_time_sec:0,default_scenario:0,default_valu:0,defin:0,definit:0,delai:0,delet:0,delete_scenario_from_db:0,delete_scenario_name_column:0,depend:[0,3],deploi:0,deploy:[0,3],deployed_model_nam:0,deployeddomodel:2,deployeddomodel_cpd21:0,deployeddomodelcpd21:2,deployment_id:0,deprec:0,design:[0,3],despit:0,detail:3,detect_platform:0,develop:[0,3],df1:0,df2:0,df:0,df_column_names_to_snake_cas:0,df_crossjoin_ai:0,df_crossjoin_mi:0,df_crossjoin_si:0,df_dict:0,dict:0,dictionari:0,differ:[0,3],direct:0,directori:0,disadvantag:0,disk:0,dm:0,do4w:0,do_model_nam:0,do_util:0,doc:0,docplex:[0,3],document:[0,3],dodashapp:0,doe:0,doesn:0,domodeldeploy:2,domodelexport:2,done:0,down:[0,3],download:[0,3],driven:0,drop:[0,3],drop_all_t:0,drop_column_nam:0,dropdown:0,dse:[0,3],dse_do_dashboard:0,dse_do_util:1,dsx:0,dsx_project_dir:0,dsxuser:0,due:0,dump:0,dumpzipnam:0,duplic:0,duplicate_scenario_in_db:0,dure:0,dvar:[0,3],dynam:0,e:[0,3],each:0,easi:3,easier:0,echo:0,ef365b2c:0,effect:0,effici:0,eg:0,either:0,element:0,els:0,emb:0,empti:[0,3],en:0,enabl:0,enable_sqlite_fk:0,enable_transact:0,end:0,engin:[0,3],ensur:0,entiti:0,entri:0,enumer:0,env_is_cpd25:0,env_is_cpd40:0,env_is_dsx:0,env_is_wscloud:0,environ:[0,1],error:0,establish:0,evalu:0,exampl:0,excel:[0,3],excel_file_nam:0,excel_output_file_nam:0,excel_test:0,excelfil:0,excelwrit:0,exclud:0,execut:0,execute_model:0,execution_result:0,execution_statu:0,execution_status_json:0,exist:[0,3],expect:0,experi:0,explicit:0,explicitli:0,export_as_cpo:0,export_as_cpo_:0,export_as_lp:0,export_as_lp_:0,export_do_model:0,export_model:0,export_model_as_lp:0,expos:0,express:0,extend:0,extended_columns_metadata:0,extens:0,extract:0,extract_dvar_nam:0,extract_solut:0,f:0,fail:0,fake:3,fals:0,faster:0,featur:3,featuregroup:0,field:0,fig:0,file:[0,3],file_nam:0,file_path:0,filenam:0,filter:0,find:0,firefox:0,first:0,fix:0,fkc:0,flask:0,folder:0,folium:[0,3],follow:[0,3],forc:0,foreign:0,foreignkeyconstraint:0,form:0,format:0,former:[0,3],found:0,frame:0,free:0,from:[0,3],full:0,full_project_nam:0,func:0,futur:[0,3],g:[0,3],gap:[0,3],gener:0,get:0,get_access_token_curl:0,get_access_token_web:0,get_all_scenario_nam:0,get_arrow:0,get_bear:0,get_dash_tab_layout_m:0,get_data_directori:0,get_db_table_nam:0,get_dd_client:0,get_debug_dump_name_and_url:0,get_debug_file_url:0,get_deployment_id:0,get_df_column_nam:0,get_do_model_export_curl:0,get_do_model_export_web:0,get_do_scenario:0,get_execution_service_model_url:0,get_execution_statu:0,get_head:0,get_html_tabl:0,get_input_fil:0,get_job_statu:0,get_job_url:0,get_kill_job_url:0,get_kpi_output_t:0,get_kpis_table_as_datafram:0,get_log_file_name_and_url:0,get_log_file_url:0,get_multi_scenario_data:0,get_object:0,get_output:0,get_parameter_valu:0,get_plotly_fig_m:0,get_popup_t:0,get_project_id:0,get_raw_table_by_nam:0,get_root_directori:0,get_sa_column:0,get_sa_t:0,get_scenario_db_t:0,get_scenario_picker_ui:0,get_scenario_refresh_button:0,get_scenario_select_drop_down:0,get_scenarios_df:0,get_selected_scenario:0,get_solution_name_and_url:0,get_solve_config:0,get_solve_detail:0,get_solve_details_object:0,get_solve_payload:0,get_solve_statu:0,get_solve_url:0,get_space_id:0,get_stop_job_url:0,get_tab_layout_:0,getcwd:0,gist:0,git:0,github:[0,3],githubpag:3,give:0,given:0,glob:0,grand:0,greatli:3,gsilabs_scnfo:0,h:0,ha:[0,3],hack:3,hand:0,happen:0,hard:0,hashtabl:0,have:[0,3],height:0,hello:0,here:0,hierarch:0,hold:0,home:0,how:[0,3],howev:0,html:0,http:0,hypothet:0,i:0,ibm:[0,3],ibm_watson_machine_learn:0,ibm_watson_studio_lib:0,icon:0,icpd:3,id:0,ignor:[0,3],imp:0,impact:3,implement:0,improv:[0,3],includ:0,inconsist:0,independ:0,index:[0,1],indic:0,individu:3,info:0,inform:0,init:0,initi:0,inline_t:0,inner:0,input:0,input_csv_name_pattern:0,input_db_t:0,input_table_nam:0,insecurerequestwarn:0,insert:0,insert_scenarios_in_db:0,insert_table_in_db_bulk:0,insert_tables_in_db:0,instal:[0,1],installationreadm:3,instanc:0,instead:0,integ:0,integer_var_list:0,integer_var_seri:0,integer_var_series_:0,integervartyp:0,intend:3,interact:[0,3],interfac:3,intermedi:0,intern:[0,3],internet:3,interrupt:0,interv:0,io:0,ipynb:0,ipywidget:0,issu:0,itself:3,jerom:0,job:0,job_config_json:0,job_detail:0,job_uid:0,join:0,json:0,jupyt:[0,3],jupyterlab:3,just:0,kansas_city_coord:0,karg:0,keep:0,kei:0,keyword:0,kill:0,kill_job:0,km:0,kpi:0,kpitabl:0,kwarg:0,lambda:0,last:0,lat:0,later:0,latest:0,layer_control_posit:0,lead:0,leav:0,left:0,let:0,level:0,lib:0,librari:0,like:[0,3],limit:[0,3],line:0,list:0,list_file_hierarchi:0,load:0,load_data:0,load_data_from_csv:0,load_data_from_csv_:0,load_data_from_excel:0,load_data_from_excel_:0,load_data_from_scenario:0,load_data_from_scenario_:0,load_from_excel:0,load_selected_scenario_data:0,local:[0,3],local_root:0,locat:0,log:0,log_file_nam:0,logic:0,lon:0,longer:[0,3],look:0,loop:0,lp:[0,3],m:0,machin:0,made:0,mai:0,main:[0,1],maintain:0,major:0,make:0,manag:0,mani:3,manipul:[0,3],manual:0,map:[0,3],mapmanag:[2,3],marker:0,master:0,match:0,max_oaas_time_limit_sec:0,max_run_time_sec:0,maximum:0,mb:3,md:3,mdl:0,me:[0,1],mean:0,medium:0,member:0,menu:[0,3],merg:0,merge_scenario_data:0,messag:0,metadata:0,method:0,mgr:0,might:3,minu:0,mip_gap_kpi_nam:0,miss:0,mix:0,mkonrad:0,mode:0,model1:0,model2:0,model:[0,1],model_build:0,model_nam:0,modelbuild:0,modifi:0,modul:[1,2],module_reload:0,moment:3,monitor:0,monitor_execut:0,monitor_loop_delay_sec:0,more:[0,3],most:0,mostli:3,move:3,mp:0,msm:0,multi:0,multi_scenario:0,multi_thread:0,multiindex:0,multipl:[0,3],multiscenariomanag:2,must:0,my:0,my_default_scenario:0,my_do_model:0,my_funct:0,my_input_column_nam:0,my_input_valu:0,my_output_column_name_1:0,my_output_column_name_2:0,my_output_value1:0,my_output_value_1:0,my_output_value_2:0,my_tabl:0,myexcelfil:0,myexcelfileoutput:0,myfil:0,mymodel:0,myoptimizationengin:0,myprogresslisten:0,mytabl:0,n:0,n_arrow:0,name:0,namedtupl:0,nbviewer:0,necessari:0,need:0,neither:3,net:0,never:0,new_path:0,new_scenario_nam:0,next:0,nodefault:3,non:0,none:0,not_start:0,note:[0,3],notebook:[0,3],notify_progress:0,now:0,number:0,oaa:0,object:0,off:0,ok:0,omit:0,onc:0,one:0,ones:0,onli:[0,3],open:0,oper:0,optim:[0,3],optimizationengin:[2,3],option:[0,3],order:0,ordereddict:0,ore:3,oreilli:0,org:0,organ:0,origin:0,os:0,other:[0,3],otherwis:0,out:0,output:0,output_csv_name_pattern:0,output_db_t:0,output_table_nam:0,outsid:3,overrid:0,overwrit:0,overwritten:0,p1:0,p2:0,packag:[1,2,3],page:[0,1],page_id:0,page_sourc:0,pak:3,panda:0,panel:0,param:0,param_nam:0,param_typ:0,paramet:0,parametert:0,pardir:0,parent:0,parent_dir:0,parent_dir_2:0,pars:0,parse_html:0,part:0,particular:[0,3],pass:0,password:0,past:0,path:0,pattern:0,payload:0,pd:0,per:0,perform:[0,3],period:0,person:0,phase:0,pick:3,picker:0,pip:[0,3],place:0,placehold:0,plain:0,platform:0,plot:0,plotli:0,plotly_cpd_workaround:2,plotlymanag:2,plugin:0,point:0,poll:0,polylin:0,popul:0,popup:0,popup_t:0,possibl:0,post:[0,3],post_process_contain:0,post_process_container_get_datafram:0,post_process_fail:0,post_process_inline_t:0,post_process_inline_table_get_datafram:0,post_process_interrupt:0,post_process_process:0,practic:0,pre:[0,3],prefer:3,prep_paramet:0,prepar:0,prepare_data_fram:0,prepare_input_data_fram:0,prepare_output_data_fram:0,prevent:0,previou:3,previous_valu:0,primari:0,print:0,print_hello:0,print_inputs_outputs_summari:0,print_table_nam:0,problem:0,procedur:0,process:[0,3],product:0,productionplan:0,progress:0,progress_data:0,progressdata:0,project:[0,3],project_access_token:0,project_data:[0,3],project_id:0,project_lib:0,project_nam:0,properti:0,property_1:0,property_2:0,provid:[0,3],put:3,pwd:0,py:0,pydata:0,pypi:3,python:[0,3],question:0,queu:0,quot:0,rais:0,raw:0,re:0,reach:0,read:[0,1],read_csv:0,read_scenario_from_db:0,read_scenario_input_tables_from_db:0,read_scenario_table_from_db:0,read_scenario_table_from_db_cach:0,read_scenario_table_from_db_callback:0,read_scenario_tables_from_db:0,read_scenario_tables_from_db_cach:0,read_scenarios_from_db:0,read_scenarios_table_from_db_cach:0,read_scenarios_table_from_db_callback:0,readthedoc:0,reason:0,record:0,reduc:0,refactor:3,refer:0,refine_conflict:0,refresh:0,regist:0,regular:0,regularli:[0,3],rel:0,relat:0,relationship:0,releas:3,relev:0,reliabl:0,reload:0,remot:0,remov:0,renam:0,rename_scenario_in_db:0,repeatedli:0,replac:0,replace_data_in_scenario:0,replace_data_into_scenario_:0,replace_scenario_in_db:0,replace_scenario_tables_in_db:0,repositori:3,repres:0,represent:0,request:0,requir:[0,1],rerun:0,resourc:0,respons:0,rest:0,restor:0,restrict:0,result:0,retreiv:0,retriev:0,retrieve_debug_materi:0,retrieve_fil:0,retrieve_solut:0,retrieve_solve_configur:0,right:0,root:0,root_dir:0,rotat:0,round:0,rout:0,routin:0,row:0,row_idx:0,row_index:0,run:[0,3],runtime_env_apsx_url:0,s:0,same:[0,3],save:0,scenario:[0,3],scenario_1:0,scenario_nam:0,scenario_table_nam:0,scenariodbmanag:2,scenariodbt:0,scenariomanag:[2,3],scenariopick:[2,3],scenariorefreshbutton:0,scenarios_table_read_callback:0,scenariot:0,schema:0,scnfo:0,scnfo_dash_pycharm_github:0,scope:1,screen:0,script:3,search:[0,1],second:0,see:[0,3],seem:0,select:0,self:0,separ:[0,3],sequenc:0,seri:0,server:0,servic:0,service_configuration_json:0,service_nam:0,set:0,set_index:0,set_output_settings_in_solve_configur:0,set_scenarios_table_read_callback:0,set_table_read_callback:0,setup:0,share:3,sheet:0,should:[0,3],show:0,side:0,signific:3,similar:0,simpl:0,simplest:0,simpli:0,sinc:0,singl:0,site:0,size:0,skip:0,sm:0,small:0,snake_cas:0,so:0,solut:0,solution_count_kpi_nam:0,solutionlisten:0,solv:0,solve_config:0,solve_config_json:0,solve_payload:0,solve_phase_kpi_nam:0,solve_statu:0,solve_time_kpi_nam:0,solvesolut:0,some:[0,3],someth:0,somewhat:0,sourc:[0,3],source_scenario_nam:0,sp:0,space:0,space_id:0,space_nam:0,spd:0,specifi:0,speed:0,spreadhseet:3,spreadsheet:0,sql:0,sqlalchemi:0,sqlcol:0,ssl:0,stackoverflow:0,stamp:0,standard:0,start:0,startpath:0,state:0,statement:0,statu:0,step:0,stop:0,stop_job:0,storag:0,store:[0,3],str:0,strftime:0,string:0,strongli:0,structur:0,studio:[0,3],sub:0,subclass:[0,3],submiss:0,submodul:2,succesfulli:0,suffici:0,suggest:0,summari:0,support:0,suppress:0,suppress_warn:0,sure:0,sy:0,system:[0,3],t:0,tabl:[0,3],table_index_sheet:0,table_nam:0,table_read_callback:0,target:[0,1],target_scenario_nam:0,task:0,templat:0,template_scenario_nam:0,temporari:0,termin:0,test:0,text:0,than:0,thei:0,them:0,therebi:0,therefor:[0,3],thi:[0,3],thing:0,those:0,thu:0,time:0,todo:0,token:0,tooltip:0,top:0,topleft:0,touch:0,track:0,transact:0,translat:0,tree:0,truth:0,tupl:0,turn:0,two:0,txt:0,type:0,typic:[0,3],ugli:0,ui:0,un:0,under:0,unfortun:0,uniqu:0,unknown:0,unnam:0,untest:0,unverifi:0,up:[0,3],updat:0,update_cell_changes_in_db:0,update_scenario_output_tables_in_db:0,update_solve_output_into_scenario:0,upload:[0,3],url:0,urllib3:0,us:[0,3],usag:[0,3],user:0,user_access_token:0,user_nam:0,util:[2,3],v0:3,v2:3,v4:3,valid:0,valu:0,value_1:0,value_2:0,value_format:0,valueerror:0,variabl:0,venv:0,veri:3,verif:0,verifi:0,verify_integr:0,version:[2,3],via:0,view:0,visibl:0,visual:[0,3],w:0,wa:0,wai:0,want:0,warn:0,watson:[0,3],watsonmachinelearningapicli:0,we:0,web:0,well:0,what:0,whatev:0,wheel:3,when:[0,3],where:0,which:0,whole:0,widget:0,widget_button:0,widget_select:0,width:0,wil:0,window:0,within:[0,3],without:0,wml:1,wml_credenti:0,work:[0,3],world:0,would:0,write:[0,3],write_data_asset_as_file_cpd25:0,write_data_asset_as_file_wsc:0,write_data_into_scenario:0,write_data_into_scenario_:0,write_data_to_csv:0,write_data_to_csv_:0,write_data_to_excel:0,write_data_to_excel_:0,write_do_model_to_fil:0,writer:0,written:0,ws:[0,3],wsl1:0,wsl:[0,3],wslib:0,wslv1:3,wsuser:0,www:0,xdvar:0,xl:0,xlsx:0,xlx:0,xxxxxx:0,y:0,yet:0,you:0,your:0,yyyymmdd_hhmm:0,zip:[0,3],zoom_start:0},titles:["dse_do_utils package","Welcome to DSE DO Utils documentation!","dse_do_utils","Read me"],titleterms:{"0":3,"5":3,"class":3,"do":[1,3],"import":3,builder:3,content:[0,1],cpd25util:0,cpdv2:3,cpdv4:3,custom:3,datamanag:0,deployeddomodel:0,deployeddomodelcpd21:0,document:1,domodeldeploy:0,domodelexport:0,dse:1,dse_do_util:[0,2,3],environ:3,indic:1,instal:3,main:3,mapmanag:0,me:3,model:3,modul:[0,3],multiscenariomanag:0,optimizationengin:0,packag:0,plotly_cpd_workaround:0,plotlymanag:0,read:3,requir:3,scenariodbmanag:0,scenariomanag:0,scenariopick:0,scope:3,submodul:0,tabl:1,target:3,util:[0,1],version:0,welcom:1,wml:3}}) \ No newline at end of file diff --git a/dse_do_utils/version.py b/dse_do_utils/version.py index 9b9a90d..cedca3c 100644 --- a/dse_do_utils/version.py +++ b/dse_do_utils/version.py @@ -9,4 +9,4 @@ See https://stackoverflow.com/questions/458550/standard-way-to-embed-version-into-python-package """ -__version__ = "0.5.4.0b" +__version__ = "0.5.4.0"