layrz-sdk 3.1.14__py3-none-any.whl → 3.1.15__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.
Potentially problematic release.
This version of layrz-sdk might be problematic. Click here for more details.
- layrz_sdk/__init__.py +1 -1
- layrz_sdk/constants.py +19 -5
- layrz_sdk/entities/__init__.py +138 -129
- layrz_sdk/entities/asset.py +71 -60
- layrz_sdk/entities/asset_operation_mode.py +31 -31
- layrz_sdk/entities/broadcast_request.py +12 -12
- layrz_sdk/entities/broadcast_response.py +12 -12
- layrz_sdk/entities/broadcast_result.py +20 -20
- layrz_sdk/entities/broadcast_status.py +28 -28
- layrz_sdk/entities/case.py +52 -52
- layrz_sdk/entities/case_ignored_status.py +26 -26
- layrz_sdk/entities/case_status.py +23 -23
- layrz_sdk/entities/charts/axis_config.py +15 -15
- layrz_sdk/entities/charts/bar_chart.py +175 -175
- layrz_sdk/entities/charts/chart_alignment.py +27 -27
- layrz_sdk/entities/charts/chart_color.py +44 -44
- layrz_sdk/entities/charts/chart_configuration.py +10 -10
- layrz_sdk/entities/charts/chart_data_serie.py +19 -19
- layrz_sdk/entities/charts/chart_data_serie_type.py +28 -28
- layrz_sdk/entities/charts/chart_data_type.py +27 -27
- layrz_sdk/entities/charts/chart_render_technology.py +30 -30
- layrz_sdk/entities/charts/column_chart.py +201 -201
- layrz_sdk/entities/charts/html_chart.py +38 -38
- layrz_sdk/entities/charts/line_chart.py +248 -248
- layrz_sdk/entities/charts/map_center_type.py +22 -22
- layrz_sdk/entities/charts/map_chart.py +108 -108
- layrz_sdk/entities/charts/map_point.py +22 -22
- layrz_sdk/entities/charts/number_chart.py +54 -54
- layrz_sdk/entities/charts/pie_chart.py +131 -131
- layrz_sdk/entities/charts/radar_chart.py +81 -81
- layrz_sdk/entities/charts/radial_bar_chart.py +131 -131
- layrz_sdk/entities/charts/scatter_chart.py +210 -210
- layrz_sdk/entities/charts/scatter_serie.py +13 -13
- layrz_sdk/entities/charts/scatter_serie_item.py +8 -8
- layrz_sdk/entities/charts/table_chart.py +54 -54
- layrz_sdk/entities/charts/table_header.py +8 -8
- layrz_sdk/entities/charts/table_row.py +9 -9
- layrz_sdk/entities/charts/timeline_chart.py +79 -79
- layrz_sdk/entities/charts/timeline_serie.py +10 -10
- layrz_sdk/entities/charts/timeline_serie_item.py +12 -12
- layrz_sdk/entities/checkpoint.py +17 -17
- layrz_sdk/entities/comment.py +16 -16
- layrz_sdk/entities/custom_field.py +10 -10
- layrz_sdk/entities/custom_report_page.py +40 -40
- layrz_sdk/entities/device.py +18 -13
- layrz_sdk/entities/event.py +23 -23
- layrz_sdk/entities/geofence.py +11 -11
- layrz_sdk/entities/last_message.py +12 -12
- layrz_sdk/entities/message.py +23 -23
- layrz_sdk/entities/modbus/__init__.py +9 -0
- layrz_sdk/entities/modbus/config.py +19 -0
- layrz_sdk/entities/modbus/parameter.py +110 -0
- layrz_sdk/entities/modbus/schema.py +10 -0
- layrz_sdk/entities/modbus/status.py +16 -0
- layrz_sdk/entities/modbus/wait.py +134 -0
- layrz_sdk/entities/outbound_service.py +10 -10
- layrz_sdk/entities/position.py +116 -116
- layrz_sdk/entities/presence_type.py +16 -16
- layrz_sdk/entities/report.py +289 -289
- layrz_sdk/entities/report_col.py +40 -40
- layrz_sdk/entities/report_configuration.py +8 -8
- layrz_sdk/entities/report_data_type.py +28 -28
- layrz_sdk/entities/report_format.py +27 -27
- layrz_sdk/entities/report_header.py +43 -43
- layrz_sdk/entities/report_page.py +15 -15
- layrz_sdk/entities/report_row.py +28 -28
- layrz_sdk/entities/sensor.py +11 -11
- layrz_sdk/entities/static_position.py +17 -0
- layrz_sdk/entities/telemetry/__init__.py +6 -0
- layrz_sdk/entities/telemetry/assetmessage.py +159 -0
- layrz_sdk/entities/telemetry/devicemessage.py +122 -0
- layrz_sdk/entities/text_alignment.py +26 -26
- layrz_sdk/entities/trigger.py +11 -11
- layrz_sdk/entities/user.py +10 -10
- layrz_sdk/entities/waypoint.py +18 -18
- layrz_sdk/helpers/__init__.py +5 -5
- layrz_sdk/helpers/color.py +44 -44
- layrz_sdk/lcl/__init__.py +5 -5
- layrz_sdk/lcl/core.py +848 -848
- {layrz_sdk-3.1.14.dist-info → layrz_sdk-3.1.15.dist-info}/METADATA +51 -49
- layrz_sdk-3.1.15.dist-info/RECORD +85 -0
- {layrz_sdk-3.1.14.dist-info → layrz_sdk-3.1.15.dist-info}/licenses/LICENSE +6 -6
- layrz_sdk-3.1.14.dist-info/RECORD +0 -75
- {layrz_sdk-3.1.14.dist-info → layrz_sdk-3.1.15.dist-info}/WHEEL +0 -0
- {layrz_sdk-3.1.14.dist-info → layrz_sdk-3.1.15.dist-info}/top_level.txt +0 -0
layrz_sdk/entities/report.py
CHANGED
|
@@ -1,289 +1,289 @@
|
|
|
1
|
-
"""Report class"""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import time
|
|
7
|
-
import warnings
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Any, Optional
|
|
10
|
-
|
|
11
|
-
import xlsxwriter
|
|
12
|
-
from pydantic import BaseModel, Field, field_validator
|
|
13
|
-
|
|
14
|
-
from layrz_sdk.helpers.color import use_black
|
|
15
|
-
|
|
16
|
-
from .custom_report_page import CustomReportPage
|
|
17
|
-
from .report_data_type import ReportDataType
|
|
18
|
-
from .report_format import ReportFormat
|
|
19
|
-
from .report_page import ReportPage
|
|
20
|
-
|
|
21
|
-
if sys.version_info >= (3, 11):
|
|
22
|
-
from typing import Self
|
|
23
|
-
else:
|
|
24
|
-
from typing_extensions import Self
|
|
25
|
-
|
|
26
|
-
log = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Report(BaseModel):
|
|
30
|
-
"""Report definition"""
|
|
31
|
-
|
|
32
|
-
name: str = Field(description='Name of the report. Length should be less than 60 characters')
|
|
33
|
-
pages: list[ReportPage | CustomReportPage] = Field(
|
|
34
|
-
description='List of report pages',
|
|
35
|
-
default_factory=list,
|
|
36
|
-
)
|
|
37
|
-
export_format: Optional[ReportFormat] = Field(description='Export format of the report', default=None)
|
|
38
|
-
|
|
39
|
-
@field_validator('export_format', mode='before')
|
|
40
|
-
def _validate_export_format(cls: 'Report', value: Any) -> Any:
|
|
41
|
-
if value is not None:
|
|
42
|
-
warnings.warn(
|
|
43
|
-
'export_format is deprecated, use the export method instead',
|
|
44
|
-
DeprecationWarning,
|
|
45
|
-
stacklevel=2,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
return value
|
|
49
|
-
|
|
50
|
-
@property
|
|
51
|
-
def filename(self: Self) -> str:
|
|
52
|
-
"""Report filename"""
|
|
53
|
-
return f'{self.name}_{int(time.time() * 1000)}.xlsx'
|
|
54
|
-
|
|
55
|
-
def export(
|
|
56
|
-
self: Self,
|
|
57
|
-
path: str | Path,
|
|
58
|
-
export_format: Optional[ReportFormat] = None,
|
|
59
|
-
password: Optional[str] = None,
|
|
60
|
-
msoffice_crypt_path: str = '/opt/msoffice/bin/msoffice-crypt.exe',
|
|
61
|
-
) -> Path | dict[str, Any]:
|
|
62
|
-
"""
|
|
63
|
-
Export report to file
|
|
64
|
-
|
|
65
|
-
:param path: Path to save the report
|
|
66
|
-
:type path: str | Path
|
|
67
|
-
:param export_format: Format to export the report
|
|
68
|
-
:type export_format: ReportFormat
|
|
69
|
-
:param password: Password to protect the file (Only works with Microsoft Excel format)
|
|
70
|
-
:type password: str
|
|
71
|
-
:param msoffice_crypt_path: Path to the msoffice-crypt.exe executable, used to encrypt the file
|
|
72
|
-
:type msoffice_crypt_path: str
|
|
73
|
-
:return: Full path of the exported file or JSON representation of the report
|
|
74
|
-
:rtype: Path | dict
|
|
75
|
-
:raises AttributeError: If the export format is not supported
|
|
76
|
-
"""
|
|
77
|
-
if export_format:
|
|
78
|
-
if export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
79
|
-
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
80
|
-
elif export_format == ReportFormat.JSON:
|
|
81
|
-
if password:
|
|
82
|
-
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
83
|
-
return self._export_json()
|
|
84
|
-
else:
|
|
85
|
-
raise AttributeError(f'Unsupported export format: {export_format}')
|
|
86
|
-
|
|
87
|
-
if self.export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
88
|
-
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
89
|
-
elif self.export_format == ReportFormat.JSON:
|
|
90
|
-
if password:
|
|
91
|
-
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
92
|
-
return self._export_json()
|
|
93
|
-
else:
|
|
94
|
-
raise AttributeError(f'Unsupported export format: {self.export_format}')
|
|
95
|
-
|
|
96
|
-
def export_as_json(self: Self) -> dict[str, Any]:
|
|
97
|
-
"""Returns the report as a JSON dict"""
|
|
98
|
-
return self._export_json()
|
|
99
|
-
|
|
100
|
-
def _export_json(self: Self) -> dict[str, Any]:
|
|
101
|
-
"""Returns a JSON dict of the report"""
|
|
102
|
-
json_pages = []
|
|
103
|
-
for page in self.pages:
|
|
104
|
-
if isinstance(page, CustomReportPage):
|
|
105
|
-
continue
|
|
106
|
-
|
|
107
|
-
headers = []
|
|
108
|
-
for header in page.headers:
|
|
109
|
-
headers.append(
|
|
110
|
-
{
|
|
111
|
-
'content': header.content,
|
|
112
|
-
'text_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
113
|
-
'color': header.color,
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
rows = []
|
|
117
|
-
for row in page.rows:
|
|
118
|
-
cells = []
|
|
119
|
-
for cell in row.content:
|
|
120
|
-
cells.append(
|
|
121
|
-
{
|
|
122
|
-
'content': cell.content,
|
|
123
|
-
'text_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
124
|
-
'color': cell.color,
|
|
125
|
-
'data_type': cell.data_type.value,
|
|
126
|
-
}
|
|
127
|
-
)
|
|
128
|
-
rows.append(
|
|
129
|
-
{
|
|
130
|
-
'content': cells,
|
|
131
|
-
'compact': row.compact,
|
|
132
|
-
}
|
|
133
|
-
)
|
|
134
|
-
json_pages.append(
|
|
135
|
-
{
|
|
136
|
-
'name': page.name,
|
|
137
|
-
'headers': headers,
|
|
138
|
-
'rows': rows,
|
|
139
|
-
}
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
'name': self.name,
|
|
144
|
-
'pages': json_pages,
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
def _export_xlsx(
|
|
148
|
-
self: Self,
|
|
149
|
-
path: str | Path,
|
|
150
|
-
password: Optional[str] = None,
|
|
151
|
-
msoffice_crypt_path: Optional[str] = None,
|
|
152
|
-
) -> Path:
|
|
153
|
-
"""
|
|
154
|
-
Export to Microsoft Excel (.xslx)
|
|
155
|
-
|
|
156
|
-
:param path: Path to save the report
|
|
157
|
-
:type path: str | Path
|
|
158
|
-
:param password: Password to protect the file
|
|
159
|
-
:type password: str
|
|
160
|
-
:param msoffice_crypt_path: Path to the msoffice-crypt.exe executable, used to encrypt the file
|
|
161
|
-
:type msoffice_crypt_path: str
|
|
162
|
-
|
|
163
|
-
:return: Full path of the exported file
|
|
164
|
-
:rtype: Path
|
|
165
|
-
|
|
166
|
-
:raises AttributeError: If the export format is not supported
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
if isinstance(path, str):
|
|
170
|
-
path = Path(path).resolve()
|
|
171
|
-
|
|
172
|
-
full_path = path / self.filename
|
|
173
|
-
if full_path.exists():
|
|
174
|
-
log.warning(f'File {full_path} already exists, overwriting it')
|
|
175
|
-
os.remove(full_path)
|
|
176
|
-
|
|
177
|
-
book = xlsxwriter.Workbook(full_path)
|
|
178
|
-
|
|
179
|
-
pages_name: list[str] = []
|
|
180
|
-
|
|
181
|
-
for page in self.pages:
|
|
182
|
-
sheet_name = page.name[0:20]
|
|
183
|
-
|
|
184
|
-
if sheet_name in pages_name:
|
|
185
|
-
sheet_name = f'{sheet_name} ({pages_name.count(sheet_name) + 1})'
|
|
186
|
-
|
|
187
|
-
# Allow only numbers, letters, spaces and _ or - characters
|
|
188
|
-
# Other characters will be removed
|
|
189
|
-
sheet_name = ''.join(e for e in sheet_name if e.isalnum() or e in [' ', '_', '-'])
|
|
190
|
-
sheet = book.add_worksheet(sheet_name)
|
|
191
|
-
|
|
192
|
-
if isinstance(page, CustomReportPage):
|
|
193
|
-
if page.extended_builder:
|
|
194
|
-
page.extended_builder(sheet=sheet, workbook=book)
|
|
195
|
-
elif page.builder:
|
|
196
|
-
page.builder(sheet)
|
|
197
|
-
else:
|
|
198
|
-
raise AttributeError('Custom report page must have a builder or extended_builder function')
|
|
199
|
-
|
|
200
|
-
sheet.autofit()
|
|
201
|
-
continue
|
|
202
|
-
|
|
203
|
-
if page.freeze_header:
|
|
204
|
-
sheet.freeze_panes(1, 0)
|
|
205
|
-
|
|
206
|
-
for i, header in enumerate(page.headers):
|
|
207
|
-
style = book.add_format(
|
|
208
|
-
{
|
|
209
|
-
'align': header.align.value,
|
|
210
|
-
'font_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
211
|
-
'bg_color': header.color,
|
|
212
|
-
'bold': header.bold,
|
|
213
|
-
'valign': 'vcenter',
|
|
214
|
-
'font_size': 11,
|
|
215
|
-
'top': 1,
|
|
216
|
-
'left': 1,
|
|
217
|
-
'right': 1,
|
|
218
|
-
'bottom': 1,
|
|
219
|
-
'font_name': 'Aptos Narrow',
|
|
220
|
-
}
|
|
221
|
-
)
|
|
222
|
-
sheet.write(0, i, header.content, style)
|
|
223
|
-
|
|
224
|
-
for i, row in enumerate(page.rows):
|
|
225
|
-
for j, cell in enumerate(row.content):
|
|
226
|
-
style = {
|
|
227
|
-
'align': cell.align.value,
|
|
228
|
-
'font_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
229
|
-
'bg_color': cell.color,
|
|
230
|
-
'bold': cell.bold,
|
|
231
|
-
'valign': 'vcenter',
|
|
232
|
-
'font_size': 11,
|
|
233
|
-
'top': 1,
|
|
234
|
-
'left': 1,
|
|
235
|
-
'right': 1,
|
|
236
|
-
'bottom': 1,
|
|
237
|
-
'font_name': 'Aptos Narrow',
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
value: Any = None
|
|
241
|
-
|
|
242
|
-
if cell.data_type == ReportDataType.BOOL:
|
|
243
|
-
value = 'Yes' if cell.content else 'No'
|
|
244
|
-
elif cell.data_type == ReportDataType.DATETIME:
|
|
245
|
-
value = cell.content.strftime(cell.datetime_format)
|
|
246
|
-
elif cell.data_type == ReportDataType.INT:
|
|
247
|
-
try:
|
|
248
|
-
value = int(cell.content)
|
|
249
|
-
except ValueError:
|
|
250
|
-
value = cell.content
|
|
251
|
-
log.warning(f'Invalid int value: {cell.content} in cell {i + 1}, {j}')
|
|
252
|
-
elif cell.data_type == ReportDataType.FLOAT:
|
|
253
|
-
try:
|
|
254
|
-
value = float(cell.content)
|
|
255
|
-
style.update({'num_format': '0.00'})
|
|
256
|
-
except ValueError:
|
|
257
|
-
value = cell.content
|
|
258
|
-
log.warning(f'Invalid float value: {cell.content} in cell {i + 1}, {j}')
|
|
259
|
-
elif cell.data_type == ReportDataType.CURRENCY:
|
|
260
|
-
value = float(cell.content)
|
|
261
|
-
style.update(
|
|
262
|
-
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
263
|
-
)
|
|
264
|
-
else:
|
|
265
|
-
value = cell.content
|
|
266
|
-
|
|
267
|
-
sheet.write(i + 1, j, value, book.add_format(style))
|
|
268
|
-
|
|
269
|
-
if row.compact:
|
|
270
|
-
sheet.set_row(i + 1, None, None, {'level': 1, 'hidden': True})
|
|
271
|
-
else:
|
|
272
|
-
sheet.set_row(i + 1, None, None, {'collapsed': True})
|
|
273
|
-
|
|
274
|
-
sheet.autofit()
|
|
275
|
-
book.close()
|
|
276
|
-
|
|
277
|
-
if password and msoffice_crypt_path:
|
|
278
|
-
new_path = os.path.join(path, f'encrypted_{self.filename}')
|
|
279
|
-
log.debug(f'Executing `{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"`')
|
|
280
|
-
os.system(f'{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"')
|
|
281
|
-
os.remove(full_path)
|
|
282
|
-
|
|
283
|
-
with open(new_path, 'rb') as f:
|
|
284
|
-
with open(full_path, 'wb') as f2:
|
|
285
|
-
f2.write(f.read())
|
|
286
|
-
|
|
287
|
-
os.remove(new_path)
|
|
288
|
-
|
|
289
|
-
return full_path
|
|
1
|
+
"""Report class"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import warnings
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import xlsxwriter
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
|
+
|
|
14
|
+
from layrz_sdk.helpers.color import use_black
|
|
15
|
+
|
|
16
|
+
from .custom_report_page import CustomReportPage
|
|
17
|
+
from .report_data_type import ReportDataType
|
|
18
|
+
from .report_format import ReportFormat
|
|
19
|
+
from .report_page import ReportPage
|
|
20
|
+
|
|
21
|
+
if sys.version_info >= (3, 11):
|
|
22
|
+
from typing import Self
|
|
23
|
+
else:
|
|
24
|
+
from typing_extensions import Self
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Report(BaseModel):
|
|
30
|
+
"""Report definition"""
|
|
31
|
+
|
|
32
|
+
name: str = Field(description='Name of the report. Length should be less than 60 characters')
|
|
33
|
+
pages: list[ReportPage | CustomReportPage] = Field(
|
|
34
|
+
description='List of report pages',
|
|
35
|
+
default_factory=list,
|
|
36
|
+
)
|
|
37
|
+
export_format: Optional[ReportFormat] = Field(description='Export format of the report', default=None)
|
|
38
|
+
|
|
39
|
+
@field_validator('export_format', mode='before')
|
|
40
|
+
def _validate_export_format(cls: 'Report', value: Any) -> Any:
|
|
41
|
+
if value is not None:
|
|
42
|
+
warnings.warn(
|
|
43
|
+
'export_format is deprecated, use the export method instead',
|
|
44
|
+
DeprecationWarning,
|
|
45
|
+
stacklevel=2,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def filename(self: Self) -> str:
|
|
52
|
+
"""Report filename"""
|
|
53
|
+
return f'{self.name}_{int(time.time() * 1000)}.xlsx'
|
|
54
|
+
|
|
55
|
+
def export(
|
|
56
|
+
self: Self,
|
|
57
|
+
path: str | Path,
|
|
58
|
+
export_format: Optional[ReportFormat] = None,
|
|
59
|
+
password: Optional[str] = None,
|
|
60
|
+
msoffice_crypt_path: str = '/opt/msoffice/bin/msoffice-crypt.exe',
|
|
61
|
+
) -> Path | dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Export report to file
|
|
64
|
+
|
|
65
|
+
:param path: Path to save the report
|
|
66
|
+
:type path: str | Path
|
|
67
|
+
:param export_format: Format to export the report
|
|
68
|
+
:type export_format: ReportFormat
|
|
69
|
+
:param password: Password to protect the file (Only works with Microsoft Excel format)
|
|
70
|
+
:type password: str
|
|
71
|
+
:param msoffice_crypt_path: Path to the msoffice-crypt.exe executable, used to encrypt the file
|
|
72
|
+
:type msoffice_crypt_path: str
|
|
73
|
+
:return: Full path of the exported file or JSON representation of the report
|
|
74
|
+
:rtype: Path | dict
|
|
75
|
+
:raises AttributeError: If the export format is not supported
|
|
76
|
+
"""
|
|
77
|
+
if export_format:
|
|
78
|
+
if export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
79
|
+
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
80
|
+
elif export_format == ReportFormat.JSON:
|
|
81
|
+
if password:
|
|
82
|
+
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
83
|
+
return self._export_json()
|
|
84
|
+
else:
|
|
85
|
+
raise AttributeError(f'Unsupported export format: {export_format}')
|
|
86
|
+
|
|
87
|
+
if self.export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
88
|
+
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
89
|
+
elif self.export_format == ReportFormat.JSON:
|
|
90
|
+
if password:
|
|
91
|
+
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
92
|
+
return self._export_json()
|
|
93
|
+
else:
|
|
94
|
+
raise AttributeError(f'Unsupported export format: {self.export_format}')
|
|
95
|
+
|
|
96
|
+
def export_as_json(self: Self) -> dict[str, Any]:
|
|
97
|
+
"""Returns the report as a JSON dict"""
|
|
98
|
+
return self._export_json()
|
|
99
|
+
|
|
100
|
+
def _export_json(self: Self) -> dict[str, Any]:
|
|
101
|
+
"""Returns a JSON dict of the report"""
|
|
102
|
+
json_pages = []
|
|
103
|
+
for page in self.pages:
|
|
104
|
+
if isinstance(page, CustomReportPage):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
headers = []
|
|
108
|
+
for header in page.headers:
|
|
109
|
+
headers.append(
|
|
110
|
+
{
|
|
111
|
+
'content': header.content,
|
|
112
|
+
'text_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
113
|
+
'color': header.color,
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
rows = []
|
|
117
|
+
for row in page.rows:
|
|
118
|
+
cells = []
|
|
119
|
+
for cell in row.content:
|
|
120
|
+
cells.append(
|
|
121
|
+
{
|
|
122
|
+
'content': cell.content,
|
|
123
|
+
'text_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
124
|
+
'color': cell.color,
|
|
125
|
+
'data_type': cell.data_type.value,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
rows.append(
|
|
129
|
+
{
|
|
130
|
+
'content': cells,
|
|
131
|
+
'compact': row.compact,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
json_pages.append(
|
|
135
|
+
{
|
|
136
|
+
'name': page.name,
|
|
137
|
+
'headers': headers,
|
|
138
|
+
'rows': rows,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
'name': self.name,
|
|
144
|
+
'pages': json_pages,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def _export_xlsx(
|
|
148
|
+
self: Self,
|
|
149
|
+
path: str | Path,
|
|
150
|
+
password: Optional[str] = None,
|
|
151
|
+
msoffice_crypt_path: Optional[str] = None,
|
|
152
|
+
) -> Path:
|
|
153
|
+
"""
|
|
154
|
+
Export to Microsoft Excel (.xslx)
|
|
155
|
+
|
|
156
|
+
:param path: Path to save the report
|
|
157
|
+
:type path: str | Path
|
|
158
|
+
:param password: Password to protect the file
|
|
159
|
+
:type password: str
|
|
160
|
+
:param msoffice_crypt_path: Path to the msoffice-crypt.exe executable, used to encrypt the file
|
|
161
|
+
:type msoffice_crypt_path: str
|
|
162
|
+
|
|
163
|
+
:return: Full path of the exported file
|
|
164
|
+
:rtype: Path
|
|
165
|
+
|
|
166
|
+
:raises AttributeError: If the export format is not supported
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
if isinstance(path, str):
|
|
170
|
+
path = Path(path).resolve()
|
|
171
|
+
|
|
172
|
+
full_path = path / self.filename
|
|
173
|
+
if full_path.exists():
|
|
174
|
+
log.warning(f'File {full_path} already exists, overwriting it')
|
|
175
|
+
os.remove(full_path)
|
|
176
|
+
|
|
177
|
+
book = xlsxwriter.Workbook(full_path)
|
|
178
|
+
|
|
179
|
+
pages_name: list[str] = []
|
|
180
|
+
|
|
181
|
+
for page in self.pages:
|
|
182
|
+
sheet_name = page.name[0:20]
|
|
183
|
+
|
|
184
|
+
if sheet_name in pages_name:
|
|
185
|
+
sheet_name = f'{sheet_name} ({pages_name.count(sheet_name) + 1})'
|
|
186
|
+
|
|
187
|
+
# Allow only numbers, letters, spaces and _ or - characters
|
|
188
|
+
# Other characters will be removed
|
|
189
|
+
sheet_name = ''.join(e for e in sheet_name if e.isalnum() or e in [' ', '_', '-'])
|
|
190
|
+
sheet = book.add_worksheet(sheet_name)
|
|
191
|
+
|
|
192
|
+
if isinstance(page, CustomReportPage):
|
|
193
|
+
if page.extended_builder:
|
|
194
|
+
page.extended_builder(sheet=sheet, workbook=book)
|
|
195
|
+
elif page.builder:
|
|
196
|
+
page.builder(sheet)
|
|
197
|
+
else:
|
|
198
|
+
raise AttributeError('Custom report page must have a builder or extended_builder function')
|
|
199
|
+
|
|
200
|
+
sheet.autofit()
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if page.freeze_header:
|
|
204
|
+
sheet.freeze_panes(1, 0)
|
|
205
|
+
|
|
206
|
+
for i, header in enumerate(page.headers):
|
|
207
|
+
style = book.add_format(
|
|
208
|
+
{
|
|
209
|
+
'align': header.align.value,
|
|
210
|
+
'font_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
211
|
+
'bg_color': header.color,
|
|
212
|
+
'bold': header.bold,
|
|
213
|
+
'valign': 'vcenter',
|
|
214
|
+
'font_size': 11,
|
|
215
|
+
'top': 1,
|
|
216
|
+
'left': 1,
|
|
217
|
+
'right': 1,
|
|
218
|
+
'bottom': 1,
|
|
219
|
+
'font_name': 'Aptos Narrow',
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
sheet.write(0, i, header.content, style)
|
|
223
|
+
|
|
224
|
+
for i, row in enumerate(page.rows):
|
|
225
|
+
for j, cell in enumerate(row.content):
|
|
226
|
+
style = {
|
|
227
|
+
'align': cell.align.value,
|
|
228
|
+
'font_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
229
|
+
'bg_color': cell.color,
|
|
230
|
+
'bold': cell.bold,
|
|
231
|
+
'valign': 'vcenter',
|
|
232
|
+
'font_size': 11,
|
|
233
|
+
'top': 1,
|
|
234
|
+
'left': 1,
|
|
235
|
+
'right': 1,
|
|
236
|
+
'bottom': 1,
|
|
237
|
+
'font_name': 'Aptos Narrow',
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
value: Any = None
|
|
241
|
+
|
|
242
|
+
if cell.data_type == ReportDataType.BOOL:
|
|
243
|
+
value = 'Yes' if cell.content else 'No'
|
|
244
|
+
elif cell.data_type == ReportDataType.DATETIME:
|
|
245
|
+
value = cell.content.strftime(cell.datetime_format)
|
|
246
|
+
elif cell.data_type == ReportDataType.INT:
|
|
247
|
+
try:
|
|
248
|
+
value = int(cell.content)
|
|
249
|
+
except ValueError:
|
|
250
|
+
value = cell.content
|
|
251
|
+
log.warning(f'Invalid int value: {cell.content} in cell {i + 1}, {j}')
|
|
252
|
+
elif cell.data_type == ReportDataType.FLOAT:
|
|
253
|
+
try:
|
|
254
|
+
value = float(cell.content)
|
|
255
|
+
style.update({'num_format': '0.00'})
|
|
256
|
+
except ValueError:
|
|
257
|
+
value = cell.content
|
|
258
|
+
log.warning(f'Invalid float value: {cell.content} in cell {i + 1}, {j}')
|
|
259
|
+
elif cell.data_type == ReportDataType.CURRENCY:
|
|
260
|
+
value = float(cell.content)
|
|
261
|
+
style.update(
|
|
262
|
+
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
value = cell.content
|
|
266
|
+
|
|
267
|
+
sheet.write(i + 1, j, value, book.add_format(style))
|
|
268
|
+
|
|
269
|
+
if row.compact:
|
|
270
|
+
sheet.set_row(i + 1, None, None, {'level': 1, 'hidden': True})
|
|
271
|
+
else:
|
|
272
|
+
sheet.set_row(i + 1, None, None, {'collapsed': True})
|
|
273
|
+
|
|
274
|
+
sheet.autofit()
|
|
275
|
+
book.close()
|
|
276
|
+
|
|
277
|
+
if password and msoffice_crypt_path:
|
|
278
|
+
new_path = os.path.join(path, f'encrypted_{self.filename}')
|
|
279
|
+
log.debug(f'Executing `{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"`')
|
|
280
|
+
os.system(f'{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"')
|
|
281
|
+
os.remove(full_path)
|
|
282
|
+
|
|
283
|
+
with open(new_path, 'rb') as f:
|
|
284
|
+
with open(full_path, 'wb') as f2:
|
|
285
|
+
f2.write(f.read())
|
|
286
|
+
|
|
287
|
+
os.remove(new_path)
|
|
288
|
+
|
|
289
|
+
return full_path
|
layrz_sdk/entities/report_col.py
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
"""Report col"""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
import warnings
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel, Field, field_validator
|
|
8
|
-
|
|
9
|
-
from .report_data_type import ReportDataType
|
|
10
|
-
from .text_alignment import TextAlignment
|
|
11
|
-
|
|
12
|
-
if sys.version_info >= (3, 11):
|
|
13
|
-
from typing import Self
|
|
14
|
-
else:
|
|
15
|
-
from typing_extensions import Self
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ReportCol(BaseModel):
|
|
19
|
-
"""Report column entity"""
|
|
20
|
-
|
|
21
|
-
content: Any = Field(description='Column content')
|
|
22
|
-
color: str = Field(description='Column color', default='#ffffff')
|
|
23
|
-
text_color: Optional[str] = Field(description='Column text color', default=None)
|
|
24
|
-
align: TextAlignment = Field(description='Column text alignment', default=TextAlignment.LEFT)
|
|
25
|
-
data_type: ReportDataType = Field(description='Column data type', default=ReportDataType.STR)
|
|
26
|
-
datetime_format: str = Field(description='Datetime format', default='%Y-%m-%d %H:%M:%S')
|
|
27
|
-
currency_symbol: str = Field(description='Currency symbol', default='')
|
|
28
|
-
bold: bool = Field(description='Bold text', default=False)
|
|
29
|
-
|
|
30
|
-
@field_validator('text_color', mode='before')
|
|
31
|
-
def _validate_text_color(cls: Self, value: Any) -> Any:
|
|
32
|
-
"""Validate text color"""
|
|
33
|
-
if value is not None:
|
|
34
|
-
warnings.warn(
|
|
35
|
-
'text_color is deprecated, the algorithm will calculate the rigth text color instead',
|
|
36
|
-
DeprecationWarning,
|
|
37
|
-
stacklevel=2,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
return value
|
|
1
|
+
"""Report col"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
|
+
|
|
9
|
+
from .report_data_type import ReportDataType
|
|
10
|
+
from .text_alignment import TextAlignment
|
|
11
|
+
|
|
12
|
+
if sys.version_info >= (3, 11):
|
|
13
|
+
from typing import Self
|
|
14
|
+
else:
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ReportCol(BaseModel):
|
|
19
|
+
"""Report column entity"""
|
|
20
|
+
|
|
21
|
+
content: Any = Field(description='Column content')
|
|
22
|
+
color: str = Field(description='Column color', default='#ffffff')
|
|
23
|
+
text_color: Optional[str] = Field(description='Column text color', default=None)
|
|
24
|
+
align: TextAlignment = Field(description='Column text alignment', default=TextAlignment.LEFT)
|
|
25
|
+
data_type: ReportDataType = Field(description='Column data type', default=ReportDataType.STR)
|
|
26
|
+
datetime_format: str = Field(description='Datetime format', default='%Y-%m-%d %H:%M:%S')
|
|
27
|
+
currency_symbol: str = Field(description='Currency symbol', default='')
|
|
28
|
+
bold: bool = Field(description='Bold text', default=False)
|
|
29
|
+
|
|
30
|
+
@field_validator('text_color', mode='before')
|
|
31
|
+
def _validate_text_color(cls: Self, value: Any) -> Any:
|
|
32
|
+
"""Validate text color"""
|
|
33
|
+
if value is not None:
|
|
34
|
+
warnings.warn(
|
|
35
|
+
'text_color is deprecated, the algorithm will calculate the rigth text color instead',
|
|
36
|
+
DeprecationWarning,
|
|
37
|
+
stacklevel=2,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return value
|