ebm 0.99.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ebm/__init__.py +0 -0
- ebm/__main__.py +152 -0
- ebm/__version__.py +1 -0
- ebm/cmd/__init__.py +0 -0
- ebm/cmd/calibrate.py +83 -0
- ebm/cmd/calibrate_excel_com_io.py +128 -0
- ebm/cmd/heating_systems_by_year.py +18 -0
- ebm/cmd/helpers.py +134 -0
- ebm/cmd/initialize.py +167 -0
- ebm/cmd/migrate.py +92 -0
- ebm/cmd/pipeline.py +227 -0
- ebm/cmd/prepare_main.py +174 -0
- ebm/cmd/result_handler.py +272 -0
- ebm/cmd/run_calculation.py +221 -0
- ebm/data/area.csv +92 -0
- ebm/data/area_new_residential_buildings.csv +3 -0
- ebm/data/area_per_person.csv +12 -0
- ebm/data/building_code_parameters.csv +9 -0
- ebm/data/energy_need_behaviour_factor.csv +6 -0
- ebm/data/energy_need_improvements.csv +7 -0
- ebm/data/energy_need_original_condition.csv +534 -0
- ebm/data/heating_system_efficiencies.csv +13 -0
- ebm/data/heating_system_forecast.csv +9 -0
- ebm/data/heating_system_initial_shares.csv +1113 -0
- ebm/data/holiday_home_energy_consumption.csv +24 -0
- ebm/data/holiday_home_stock.csv +25 -0
- ebm/data/improvement_building_upgrade.csv +9 -0
- ebm/data/new_buildings_residential.csv +32 -0
- ebm/data/population_forecast.csv +51 -0
- ebm/data/s_curve.csv +40 -0
- ebm/energy_consumption.py +307 -0
- ebm/extractors.py +115 -0
- ebm/heating_system_forecast.py +472 -0
- ebm/holiday_home_energy.py +341 -0
- ebm/migrations.py +224 -0
- ebm/model/__init__.py +0 -0
- ebm/model/area.py +403 -0
- ebm/model/bema.py +149 -0
- ebm/model/building_category.py +150 -0
- ebm/model/building_condition.py +78 -0
- ebm/model/calibrate_energy_requirements.py +84 -0
- ebm/model/calibrate_heating_systems.py +180 -0
- ebm/model/column_operations.py +157 -0
- ebm/model/construction.py +827 -0
- ebm/model/data_classes.py +223 -0
- ebm/model/database_manager.py +410 -0
- ebm/model/dataframemodels.py +115 -0
- ebm/model/defaults.py +30 -0
- ebm/model/energy_need.py +6 -0
- ebm/model/energy_need_filter.py +182 -0
- ebm/model/energy_purpose.py +115 -0
- ebm/model/energy_requirement.py +353 -0
- ebm/model/energy_use.py +202 -0
- ebm/model/enums.py +8 -0
- ebm/model/exceptions.py +4 -0
- ebm/model/file_handler.py +388 -0
- ebm/model/filter_scurve_params.py +83 -0
- ebm/model/filter_tek.py +152 -0
- ebm/model/heat_pump.py +53 -0
- ebm/model/heating_systems.py +20 -0
- ebm/model/heating_systems_parameter.py +17 -0
- ebm/model/heating_systems_projection.py +3 -0
- ebm/model/heating_systems_share.py +28 -0
- ebm/model/scurve.py +224 -0
- ebm/model/tek.py +1 -0
- ebm/s_curve.py +515 -0
- ebm/services/__init__.py +0 -0
- ebm/services/calibration_writer.py +262 -0
- ebm/services/console.py +106 -0
- ebm/services/excel_loader.py +66 -0
- ebm/services/files.py +38 -0
- ebm/services/spreadsheet.py +289 -0
- ebm/temp_calc.py +99 -0
- ebm/validators.py +565 -0
- ebm-0.99.5.dist-info/METADATA +212 -0
- ebm-0.99.5.dist-info/RECORD +80 -0
- ebm-0.99.5.dist-info/WHEEL +5 -0
- ebm-0.99.5.dist-info/entry_points.txt +3 -0
- ebm-0.99.5.dist-info/licenses/LICENSE +21 -0
- ebm-0.99.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,262 @@
|
|
1
|
+
import typing
|
2
|
+
from datetime import datetime
|
3
|
+
import os
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
import pandas as pd
|
7
|
+
|
8
|
+
from ebm.model import building_category
|
9
|
+
from ebm.model.heating_systems import HeatingSystems
|
10
|
+
from ebm.model.energy_purpose import EnergyPurpose
|
11
|
+
from ebm.services.excel_loader import access_excel_sheet
|
12
|
+
from ebm.services.spreadsheet import SpreadsheetCell
|
13
|
+
|
14
|
+
KEY_ERROR_COLOR = 0xC0C0C0
|
15
|
+
COLOR_AUTO = -4105
|
16
|
+
|
17
|
+
|
18
|
+
class ComCalibrationReader:
|
19
|
+
filename: str
|
20
|
+
sheet_name: str
|
21
|
+
df: pd.DataFrame
|
22
|
+
|
23
|
+
def __init__(self, workbook_name=None, sheet_name=None):
|
24
|
+
wb, sh = os.environ.get('EBM_CALIBRATION_SHEET', '!').split('!')
|
25
|
+
self.workbook_name = wb if workbook_name is None else workbook_name
|
26
|
+
self.sheet_name = sh if sheet_name is None else sheet_name
|
27
|
+
|
28
|
+
def extract(self) -> typing.Tuple:
|
29
|
+
sheet = access_excel_sheet(self.workbook_name, self.sheet_name)
|
30
|
+
used_range = sheet.UsedRange
|
31
|
+
|
32
|
+
values = used_range.Value
|
33
|
+
logger.debug(f'Found {len(values)} rows in {sheet}')
|
34
|
+
|
35
|
+
return used_range.Value
|
36
|
+
|
37
|
+
def transform(self, com_calibration_table: typing.Tuple) -> pd.DataFrame:
|
38
|
+
def replace_building_category(row):
|
39
|
+
unknown_heating_systems = set()
|
40
|
+
for factor_name in row[2].split(' and '):
|
41
|
+
try:
|
42
|
+
bc = building_category.from_norsk(row[0])
|
43
|
+
except ValueError as value_error:
|
44
|
+
if row[0].lower() in ('yrksebygg', 'yrkesbygg'):
|
45
|
+
bc = building_category.NON_RESIDENTIAL
|
46
|
+
elif row[0].lower() == 'bolig':
|
47
|
+
bc = building_category.RESIDENTIAL
|
48
|
+
erq = 'energy_requirement' if row[1].lower() == 'energibehov' else 'energy_consumption'
|
49
|
+
variabel = factor_name
|
50
|
+
extra = None
|
51
|
+
if erq == 'energy_requirement':
|
52
|
+
variabel = EnergyPurpose(factor_name) if factor_name.lower() != 'elspesifikt' else EnergyPurpose.ELECTRICAL_EQUIPMENT
|
53
|
+
else:
|
54
|
+
variabel = factor_name
|
55
|
+
extra = row[-1]
|
56
|
+
if not extra or extra.strip() in ('?', ''):
|
57
|
+
extra = HeatingSystems.ELECTRICITY
|
58
|
+
|
59
|
+
if variabel not in [h for h in HeatingSystems]:
|
60
|
+
unknown_heating_systems.add(variabel)
|
61
|
+
if extra not in [h for h in HeatingSystems]:
|
62
|
+
unknown_heating_systems.add(extra)
|
63
|
+
yield bc, erq, variabel, row[3], extra
|
64
|
+
if len(unknown_heating_systems) > 0:
|
65
|
+
unknowns = ','.join([f'"{hs}"' for hs in unknown_heating_systems])
|
66
|
+
msg = f'Unknown heating systems {unknowns}'
|
67
|
+
raise ValueError(msg)
|
68
|
+
|
69
|
+
|
70
|
+
def handle_rows(rows):
|
71
|
+
for row in rows:
|
72
|
+
yield from replace_building_category(row)
|
73
|
+
|
74
|
+
logger.debug(f'Transform {self.sheet_name}')
|
75
|
+
data = com_calibration_table[1:]
|
76
|
+
|
77
|
+
data = list(handle_rows([r for r in data if r[1].lower().replace('_','').replace(' ','') in ('energibehov',
|
78
|
+
'heatingsystem')]))
|
79
|
+
|
80
|
+
df = pd.DataFrame(data, columns=['building_category', 'group', 'variable', 'heating_rv_factor', 'extra'])
|
81
|
+
|
82
|
+
return df
|
83
|
+
|
84
|
+
|
85
|
+
class ExcelComCalibrationResultWriter:
|
86
|
+
"""
|
87
|
+
A class to handle the extraction, transformation, and loading of a pd.Dataframe to a open Excel spreadsheet.
|
88
|
+
The Dataframe is expected to have two columns in the index. The first index column is used to match the
|
89
|
+
first row of the Excel range. The index second column is used to match the first column in the Excel range. The
|
90
|
+
rest of the range is filled out by the last column in the dataframe.
|
91
|
+
|
92
|
+
Excel range: A1:D4
|
93
|
+
|
94
|
+
XXXXXX | index1 | index2 | index3
|
95
|
+
indexA value1 value4 value7
|
96
|
+
indexB value2 value5 value8
|
97
|
+
indexC value3 value6 value8
|
98
|
+
|
99
|
+
Dataframe:
|
100
|
+
|
101
|
+
clumn1, colmn2, colmn3, colmn4
|
102
|
+
index1, indexA, valueA, value1
|
103
|
+
index1, indexB, valueB, value2
|
104
|
+
index1, indexC, valueC, value3
|
105
|
+
index2, indexA, valueD, value4
|
106
|
+
index2, indexB, valueE, value5
|
107
|
+
index2, indexC, valueF, value6
|
108
|
+
index3, indexA, valueG, value7
|
109
|
+
index3, indexB, valueH, value8
|
110
|
+
index3, indexC, valueI, value9
|
111
|
+
|
112
|
+
|
113
|
+
Attributes
|
114
|
+
----------
|
115
|
+
workbook : str
|
116
|
+
The name of the workbook.
|
117
|
+
sheet : str
|
118
|
+
The name of the sheet.
|
119
|
+
df : pd.DataFrame
|
120
|
+
The DataFrame containing the data.
|
121
|
+
cells_to_update : typing.List[SpreadsheetCell]
|
122
|
+
List of cells to update.
|
123
|
+
rows : typing.List[SpreadsheetCell]
|
124
|
+
List of row header cells.
|
125
|
+
columns : typing.List[SpreadsheetCell]
|
126
|
+
List of column header cells.
|
127
|
+
|
128
|
+
Methods
|
129
|
+
-------
|
130
|
+
extract() -> typing.Tuple[typing.List[SpreadsheetCell], typing.List[SpreadsheetCell]]
|
131
|
+
Extracts the target cells and initializes row and column headers.
|
132
|
+
transform(df) -> typing.Iterable[SpreadsheetCell]
|
133
|
+
Transforms the DataFrame into a list of SpreadsheetCell objects to update.
|
134
|
+
load()
|
135
|
+
Loads the updated values into the Excel sheet.
|
136
|
+
"""
|
137
|
+
workbook: str
|
138
|
+
sheet: str
|
139
|
+
target_cells: str
|
140
|
+
df: pd.DataFrame
|
141
|
+
cells_to_update: typing.List[SpreadsheetCell]
|
142
|
+
rows: typing.List[SpreadsheetCell]
|
143
|
+
columns: typing.List[SpreadsheetCell]
|
144
|
+
|
145
|
+
def __init__(self,
|
146
|
+
excel_filename=None,
|
147
|
+
workbook='Kalibreringsark.xlsx',
|
148
|
+
sheet='Ut',
|
149
|
+
target_cells=None):
|
150
|
+
"""
|
151
|
+
Initializes the HeatingSystemsDistributionWriter with empty lists for cells to update, rows, and columns.
|
152
|
+
|
153
|
+
Parameters
|
154
|
+
----------
|
155
|
+
excel_filename : str, optional
|
156
|
+
Name of the target spreadsheet. If there is no ! and sheet name in excel_filename, the parameter sheet is
|
157
|
+
used instead
|
158
|
+
workbook : str, optinal
|
159
|
+
Optional name of the spreadsheet to used for reading and writing. (default is 'Kalibreringsark.xlsx')
|
160
|
+
sheet : str, optional
|
161
|
+
Optional name of the sheet used for reading and writing. (default is 'Ut')
|
162
|
+
target_cells : str, optional
|
163
|
+
A range of cells that contain the data to update from the dataframe
|
164
|
+
|
165
|
+
|
166
|
+
"""
|
167
|
+
|
168
|
+
self.workbook, self.sheet = os.environ.get('EBM_CALIBRATION_OUT', f'{workbook}!{sheet}').split('!')
|
169
|
+
|
170
|
+
self.workbook = workbook
|
171
|
+
self.sheet = sheet
|
172
|
+
self.target_cells = target_cells
|
173
|
+
if not target_cells:
|
174
|
+
self.target_cells = target_cells = os.environ.get('EBM_CALIBRATION_ENERGY_HEATING_SYSTEMS_DISTRIBUTION')
|
175
|
+
|
176
|
+
if excel_filename:
|
177
|
+
if '!' in excel_filename:
|
178
|
+
self.workbook, self.sheet = excel_filename.split('!')
|
179
|
+
else:
|
180
|
+
self.workbook = excel_filename
|
181
|
+
self.cells_to_update = []
|
182
|
+
self.rows = []
|
183
|
+
self.columns = []
|
184
|
+
|
185
|
+
def extract(self) -> typing.Tuple[
|
186
|
+
typing.Dict[int, SpreadsheetCell],
|
187
|
+
typing.Dict[int, SpreadsheetCell],
|
188
|
+
typing.Iterable[SpreadsheetCell]]:
|
189
|
+
"""
|
190
|
+
Extracts the target cells and initializes row and column headers.
|
191
|
+
|
192
|
+
Returns
|
193
|
+
-------
|
194
|
+
typing.Tuple[
|
195
|
+
typing.Dict[int, SpreadsheetCell],
|
196
|
+
typing.Dict[int, SpreadsheetCell],
|
197
|
+
typing.Iterable[SpreadsheetCell]]
|
198
|
+
|
199
|
+
A tuple containing lists of row, column header cells and cells to update.
|
200
|
+
"""
|
201
|
+
# Create an instance of the Excel application
|
202
|
+
sheet = access_excel_sheet(self.workbook, self.sheet)
|
203
|
+
|
204
|
+
# Make index of columns and rows
|
205
|
+
first_row = SpreadsheetCell.first_row(self.target_cells)
|
206
|
+
self.columns = {cell.column: cell.replace(value=sheet.Cells(cell.row, cell.column).Value) for cell in first_row[1:]}
|
207
|
+
|
208
|
+
first_column = SpreadsheetCell.first_column(self.target_cells)
|
209
|
+
self.rows = {cell.row: cell.replace(value=sheet.Cells(cell.row, cell.column).Value) for cell in first_column[1:]}
|
210
|
+
|
211
|
+
# Initialize value cells
|
212
|
+
self.values = SpreadsheetCell.submatrix(self.target_cells)
|
213
|
+
|
214
|
+
return self.rows, self.columns, self.values
|
215
|
+
|
216
|
+
def transform(self, df: pd.DataFrame) -> typing.Iterable[SpreadsheetCell]:
|
217
|
+
"""
|
218
|
+
Transforms the DataFrame into a list of SpreadsheetCell objects to update.
|
219
|
+
|
220
|
+
Parameters
|
221
|
+
----------
|
222
|
+
df : pd.DataFrame
|
223
|
+
The DataFrame containing the data.
|
224
|
+
|
225
|
+
Returns
|
226
|
+
-------
|
227
|
+
typing.Iterable[SpreadsheetCell]
|
228
|
+
An iterable of SpreadsheetCell objects to update.
|
229
|
+
"""
|
230
|
+
self.cells_to_update = []
|
231
|
+
for cell in self.values:
|
232
|
+
try:
|
233
|
+
row_header = self.columns[cell.column].value
|
234
|
+
column_header = self.rows[cell.row].value
|
235
|
+
if row_header not in df.index:
|
236
|
+
raise KeyError(f'"{row_header}" not found')
|
237
|
+
elif (row_header, column_header) not in df.index:
|
238
|
+
raise KeyError(f'"{column_header}" for "{row_header}" not found')
|
239
|
+
column_name = 'heating_system_share' if 'heating_system_share' in df.columns else df.columns[-1]
|
240
|
+
value = df.loc[(row_header, column_header), column_name]
|
241
|
+
except KeyError as ex:
|
242
|
+
logger.warning(f'KeyError {str(ex)} while loading data for cell {cell.spreadsheet_cell()}')
|
243
|
+
value = f'KeyError {str(ex)}'
|
244
|
+
self.cells_to_update.append(SpreadsheetCell(row=cell.row, column=cell.column, value=value))
|
245
|
+
|
246
|
+
return self.cells_to_update
|
247
|
+
|
248
|
+
def load(self):
|
249
|
+
"""
|
250
|
+
Loads the updated values into the Excel sheet defined in obj.workbook and obj.sheet.
|
251
|
+
"""
|
252
|
+
sheet = access_excel_sheet(self.workbook, self.sheet)
|
253
|
+
|
254
|
+
# Update cells
|
255
|
+
for cell_to_update in self.cells_to_update:
|
256
|
+
cell = sheet.Cells(cell_to_update.row, cell_to_update.column)
|
257
|
+
if isinstance(cell_to_update.value, str) and cell_to_update.value.startswith('KeyError'):
|
258
|
+
cell.Value = 0
|
259
|
+
cell.Font.Color = KEY_ERROR_COLOR
|
260
|
+
else:
|
261
|
+
sheet.Cells(cell_to_update.row, cell_to_update.column).Value = cell_to_update.value
|
262
|
+
cell.Font.ColorIndex = COLOR_AUTO
|
ebm/services/console.py
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
"""
|
2
|
+
Convert a pandas.DataFrame object into a rich.Table object for stylized printing in Python.
|
3
|
+
From: https://gist.github.com/avi-perl/83e77d069d97edbdde188a4f41a015c4
|
4
|
+
|
5
|
+
Also available as pypi package rich-tools
|
6
|
+
"""
|
7
|
+
import contextlib
|
8
|
+
from datetime import datetime
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
import pandas as pd
|
12
|
+
from rich import box
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.errors import NotRenderableError
|
15
|
+
from rich.table import Table
|
16
|
+
|
17
|
+
from ebm.services.spreadsheet import iter_cells
|
18
|
+
|
19
|
+
console = Console()
|
20
|
+
|
21
|
+
|
22
|
+
def rich_display_dataframe(df, title="Dataframe") -> None:
|
23
|
+
"""Display dataframe as table using rich library.
|
24
|
+
Args:
|
25
|
+
df (pd.DataFrame): dataframe to display
|
26
|
+
title (str, optional): title of the table. Defaults to "Dataframe".
|
27
|
+
Raises:
|
28
|
+
NotRenderableError: if dataframe cannot be rendered
|
29
|
+
Returns:
|
30
|
+
rich.table.Table: rich table
|
31
|
+
"""
|
32
|
+
from rich import print
|
33
|
+
from rich.table import Table
|
34
|
+
|
35
|
+
# ensure dataframe contains only string values
|
36
|
+
df = df.astype(str)
|
37
|
+
|
38
|
+
table = Table(title=title)
|
39
|
+
table.add_column('year')
|
40
|
+
for col in df.columns:
|
41
|
+
table.add_column(col)
|
42
|
+
for row, (year, cell,) in zip(df.values, enumerate(iter_cells(first_column='E', left_padding=' '), 2010), ):
|
43
|
+
with contextlib.suppress(NotRenderableError):
|
44
|
+
the_row = [f'{year}{cell}', *row]
|
45
|
+
table.add_row(*the_row)
|
46
|
+
print(table)
|
47
|
+
|
48
|
+
|
49
|
+
def df_to_table(
|
50
|
+
pandas_dataframe: pd.DataFrame,
|
51
|
+
rich_table: Table,
|
52
|
+
show_index: bool = True,
|
53
|
+
index_name: Optional[str] = None,
|
54
|
+
) -> Table:
|
55
|
+
"""Convert a pandas.DataFrame obj into a rich.Table obj.
|
56
|
+
Args:
|
57
|
+
pandas_dataframe (DataFrame): A Pandas DataFrame to be converted to a rich Table.
|
58
|
+
rich_table (Table): A rich Table that should be populated by the DataFrame values.
|
59
|
+
show_index (bool): Add a column with a row count to the table. Defaults to True.
|
60
|
+
index_name (str, optional): The column name to give to the index column. Defaults to None, showing no value.
|
61
|
+
Returns:
|
62
|
+
Table: The rich Table instance passed, populated with the DataFrame values."""
|
63
|
+
|
64
|
+
if show_index:
|
65
|
+
index_name = str(index_name) if index_name else ""
|
66
|
+
rich_table.add_column(index_name)
|
67
|
+
|
68
|
+
for column in pandas_dataframe.columns:
|
69
|
+
rich_table.add_column(str(column))
|
70
|
+
|
71
|
+
for index, value_list in enumerate(pandas_dataframe.values.tolist()):
|
72
|
+
row = [str(index)] if show_index else []
|
73
|
+
row += [str(x) for x in value_list]
|
74
|
+
rich_table.add_row(*row)
|
75
|
+
|
76
|
+
return rich_table
|
77
|
+
|
78
|
+
|
79
|
+
if __name__ == "__main__":
|
80
|
+
sample_data = {
|
81
|
+
"Date": [
|
82
|
+
datetime(year=2019, month=12, day=20),
|
83
|
+
datetime(year=2018, month=5, day=25),
|
84
|
+
datetime(year=2017, month=12, day=15),
|
85
|
+
],
|
86
|
+
"Title": [
|
87
|
+
"Star Wars: The Rise of Skywalker",
|
88
|
+
"[red]Solo[/red]: A Star Wars Story",
|
89
|
+
"Star Wars Ep. VIII: The Last Jedi",
|
90
|
+
],
|
91
|
+
"Production Budget": ["$275,000,000", "$275,000,000", "$262,000,000"],
|
92
|
+
"Box Office": ["$375,126,118", "$393,151,347", "$1,332,539,889"],
|
93
|
+
}
|
94
|
+
df = pd.DataFrame(sample_data)
|
95
|
+
|
96
|
+
# Initiate a Table instance to be modified
|
97
|
+
table = Table(show_header=True, header_style="bold magenta")
|
98
|
+
|
99
|
+
# Modify the table instance to have the data from the DataFrame
|
100
|
+
table = df_to_table(df, table)
|
101
|
+
|
102
|
+
# Update the style of the table
|
103
|
+
table.row_styles = ["none", "dim"]
|
104
|
+
table.box = box.SIMPLE_HEAD
|
105
|
+
|
106
|
+
console.print(table)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import win32com.client
|
4
|
+
from loguru import logger
|
5
|
+
from win32com.universal import com_error
|
6
|
+
|
7
|
+
|
8
|
+
def access_excel_sheet(workbook_name: str, sheet_name: str) -> win32com.client.CDispatch:
|
9
|
+
"""
|
10
|
+
Opens the specified sheet in the specified workbook using COM.
|
11
|
+
|
12
|
+
Parameters
|
13
|
+
----------
|
14
|
+
workbook_name : str
|
15
|
+
The name of the workbook.
|
16
|
+
sheet_name : str
|
17
|
+
The name of the sheet.
|
18
|
+
|
19
|
+
Returns
|
20
|
+
-------
|
21
|
+
win32com.client.CDispatch
|
22
|
+
The specified sheet object.
|
23
|
+
"""
|
24
|
+
logging.debug(f'Opening sheet {sheet_name} in {workbook_name}')
|
25
|
+
workbooks = []
|
26
|
+
try:
|
27
|
+
excel = win32com.client.Dispatch("Excel.Application")
|
28
|
+
# Get the currently open workbooks
|
29
|
+
workbooks = excel.Workbooks
|
30
|
+
except AttributeError as attr_err:
|
31
|
+
logger.exception(attr_err)
|
32
|
+
msg = f'Got an AttributeError while opening {workbook_name} !{sheet_name}. Is the spreadsheet busy?'
|
33
|
+
raise IOError(msg)
|
34
|
+
# raise attr_err
|
35
|
+
|
36
|
+
for workbook in workbooks:
|
37
|
+
logger.debug(f"Found open Workbook: {workbook.Name}")
|
38
|
+
logger.debug(f'Using {workbook_name} {sheet_name}')
|
39
|
+
# Access a specific workbook by name
|
40
|
+
try:
|
41
|
+
if workbook_name not in [n.Name for n in workbooks]:
|
42
|
+
ex_msg = f'No open workbook named: \'{workbook_name}\''
|
43
|
+
raise IOError(ex_msg)
|
44
|
+
workbook = workbooks[workbook_name]
|
45
|
+
except com_error as ex:
|
46
|
+
logger.error(f'Error opening {workbook_name}')
|
47
|
+
if not workbooks:
|
48
|
+
logger.error('No open workbooks')
|
49
|
+
else:
|
50
|
+
logger.info('Open workbooks: ')
|
51
|
+
for wb in workbooks:
|
52
|
+
logger.info(f'Found workbook {wb.Name}')
|
53
|
+
raise ex
|
54
|
+
# Now you can interact with the workbook, for example, read a cell value
|
55
|
+
sheet = []
|
56
|
+
try:
|
57
|
+
if sheet_name not in [n.Name for n in workbook.Sheets]:
|
58
|
+
ex_msg = f'{workbook_name} exists and is open, but there is no sheet named: \'{sheet_name}\''
|
59
|
+
raise IOError(ex_msg)
|
60
|
+
sheet = workbook.Sheets(sheet_name)
|
61
|
+
except com_error as ex:
|
62
|
+
logger.error(f'Error opening {sheet_name}')
|
63
|
+
for s in workbook.Sheets:
|
64
|
+
logger.error(f'Found sheet {s.Name}')
|
65
|
+
raise ex
|
66
|
+
return sheet
|
ebm/services/files.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import os
|
2
|
+
import pathlib
|
3
|
+
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
def make_unique_path(path: pathlib.Path):
|
7
|
+
path = pathlib.Path(path)
|
8
|
+
counter = 1
|
9
|
+
new_path = path
|
10
|
+
|
11
|
+
while new_path.exists():
|
12
|
+
new_path = path.with_stem(f"{path.stem}_{counter}")
|
13
|
+
counter += 1
|
14
|
+
|
15
|
+
return new_path
|
16
|
+
|
17
|
+
|
18
|
+
def file_is_writable(output_file: pathlib.Path) -> bool:
|
19
|
+
if not output_file.is_file():
|
20
|
+
# If the parent directory is writable we should be good to go
|
21
|
+
return os.access(output_file.parent, os.W_OK)
|
22
|
+
|
23
|
+
access = os.access(output_file, os.W_OK)
|
24
|
+
if not access:
|
25
|
+
logger.error(f'Permission denied: {output_file}. The file is not writable.')
|
26
|
+
return False
|
27
|
+
|
28
|
+
# It is not enough to check that the file is writable in Windows. We must also check that it is possible to open
|
29
|
+
# the file
|
30
|
+
try:
|
31
|
+
with output_file.open('a'):
|
32
|
+
pass
|
33
|
+
except PermissionError as ex:
|
34
|
+
# Unable to open a file that is reported as writable by the operating system. In that case it is a good chance
|
35
|
+
# that the file is already open. Error log our assumption and return False
|
36
|
+
logger.error(str(ex) + '. Is the file already open?')
|
37
|
+
return False
|
38
|
+
return True
|