layrz-sdk 3.0.6__py3-none-any.whl → 3.0.8__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/entities/__init__.py +55 -9
- layrz_sdk/entities/broadcasts/request.py +5 -4
- layrz_sdk/entities/broadcasts/response.py +5 -4
- layrz_sdk/entities/broadcasts/result.py +6 -5
- layrz_sdk/entities/broadcasts/service.py +5 -4
- layrz_sdk/entities/broadcasts/status.py +4 -3
- layrz_sdk/entities/cases/case.py +13 -12
- layrz_sdk/entities/cases/comment.py +5 -4
- layrz_sdk/entities/cases/trigger.py +5 -4
- layrz_sdk/entities/charts/__init__.py +1 -1
- layrz_sdk/entities/charts/alignment.py +4 -3
- layrz_sdk/entities/charts/bar.py +9 -7
- layrz_sdk/entities/charts/color.py +5 -4
- layrz_sdk/entities/charts/column.py +9 -7
- layrz_sdk/entities/charts/configuration.py +9 -7
- layrz_sdk/entities/charts/data_type.py +4 -3
- layrz_sdk/entities/charts/exceptions.py +6 -5
- layrz_sdk/entities/charts/html.py +5 -3
- layrz_sdk/entities/charts/line.py +9 -8
- layrz_sdk/entities/charts/map.py +13 -13
- layrz_sdk/entities/charts/number.py +5 -3
- layrz_sdk/entities/charts/pie.py +9 -7
- layrz_sdk/entities/charts/radar.py +6 -4
- layrz_sdk/entities/charts/radial_bar.py +9 -7
- layrz_sdk/entities/charts/render_technology.py +4 -3
- layrz_sdk/entities/charts/scatter.py +12 -10
- layrz_sdk/entities/charts/serie.py +5 -3
- layrz_sdk/entities/charts/serie_type.py +4 -3
- layrz_sdk/entities/charts/table.py +15 -17
- layrz_sdk/entities/charts/timeline.py +7 -6
- layrz_sdk/entities/checkpoints/checkpoint.py +6 -5
- layrz_sdk/entities/checkpoints/geofence.py +5 -4
- layrz_sdk/entities/checkpoints/waypoint.py +5 -4
- layrz_sdk/entities/events/event.py +5 -4
- layrz_sdk/entities/formatting/text_align.py +4 -3
- layrz_sdk/entities/general/asset.py +16 -10
- layrz_sdk/entities/general/asset_operation_mode.py +5 -3
- layrz_sdk/entities/general/custom_field.py +5 -4
- layrz_sdk/entities/general/device.py +5 -4
- layrz_sdk/entities/general/sensor.py +5 -4
- layrz_sdk/entities/general/user.py +5 -4
- layrz_sdk/entities/repcom/transaction.py +3 -2
- layrz_sdk/entities/reports/col.py +9 -9
- layrz_sdk/entities/reports/format.py +11 -7
- layrz_sdk/entities/reports/header.py +12 -10
- layrz_sdk/entities/reports/page.py +10 -8
- layrz_sdk/entities/reports/report.py +126 -66
- layrz_sdk/entities/reports/row.py +9 -8
- layrz_sdk/entities/telemetry/message.py +6 -5
- layrz_sdk/entities/telemetry/position.py +5 -4
- layrz_sdk/helpers/color.py +6 -5
- layrz_sdk/lcl/core.py +131 -120
- {layrz_sdk-3.0.6.dist-info → layrz_sdk-3.0.8.dist-info}/METADATA +6 -1
- layrz_sdk-3.0.8.dist-info/RECORD +69 -0
- {layrz_sdk-3.0.6.dist-info → layrz_sdk-3.0.8.dist-info}/WHEEL +1 -1
- layrz_sdk-3.0.6.dist-info/RECORD +0 -69
- {layrz_sdk-3.0.6.dist-info → layrz_sdk-3.0.8.dist-info}/LICENSE +0 -0
- {layrz_sdk-3.0.6.dist-info → layrz_sdk-3.0.8.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Report class"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
2
4
|
import os
|
|
3
5
|
import time
|
|
4
6
|
import warnings
|
|
7
|
+
from typing import Any, Dict, List, Self
|
|
5
8
|
|
|
6
9
|
import xlsxwriter
|
|
7
10
|
|
|
@@ -11,6 +14,8 @@ from .col import ReportDataType
|
|
|
11
14
|
from .format import ReportFormat
|
|
12
15
|
from .page import CustomReportPage, ReportPage
|
|
13
16
|
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
14
19
|
|
|
15
20
|
class Report:
|
|
16
21
|
"""
|
|
@@ -23,103 +28,143 @@ class Report:
|
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
30
|
def __init__(
|
|
26
|
-
self,
|
|
31
|
+
self: Self,
|
|
27
32
|
name: str,
|
|
28
|
-
pages:
|
|
33
|
+
pages: List[ReportPage | CustomReportPage],
|
|
29
34
|
export_format: ReportFormat = None,
|
|
30
35
|
) -> None:
|
|
31
36
|
self.name = name
|
|
32
37
|
self.pages = pages
|
|
33
38
|
|
|
34
39
|
if export_format is not None:
|
|
35
|
-
warnings.warn(
|
|
36
|
-
|
|
40
|
+
warnings.warn(
|
|
41
|
+
'export_format is deprecated, submit the export format in the `export()` method instead',
|
|
42
|
+
DeprecationWarning,
|
|
43
|
+
stacklevel=2,
|
|
44
|
+
)
|
|
37
45
|
|
|
38
46
|
self.export_format = export_format
|
|
39
47
|
|
|
40
48
|
@property
|
|
41
|
-
def filename(self) -> str:
|
|
42
|
-
"""
|
|
49
|
+
def filename(self: Self) -> str | None | bool:
|
|
50
|
+
"""Report filename"""
|
|
43
51
|
return f'{self.name}_{int(time.time() * 1000)}.xlsx'
|
|
44
52
|
|
|
45
53
|
@property
|
|
46
|
-
def _readable(self) -> str:
|
|
47
|
-
"""
|
|
54
|
+
def _readable(self: Self) -> str | None | bool:
|
|
55
|
+
"""Readable property"""
|
|
48
56
|
return f'Report(name={self.name}, pages={len(self.pages)})'
|
|
49
57
|
|
|
50
|
-
def __repr__(self) -> str:
|
|
51
|
-
"""
|
|
58
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
59
|
+
"""Readable property"""
|
|
52
60
|
return self._readable
|
|
53
61
|
|
|
54
|
-
def __str__(self) -> str:
|
|
55
|
-
"""
|
|
62
|
+
def __str__(self: Self) -> str | None | bool:
|
|
63
|
+
"""Readable property"""
|
|
56
64
|
return self._readable
|
|
57
65
|
|
|
58
|
-
def export(
|
|
59
|
-
|
|
66
|
+
def export(
|
|
67
|
+
self: Self,
|
|
68
|
+
path: str,
|
|
69
|
+
export_format: ReportFormat = None,
|
|
70
|
+
password: str = None,
|
|
71
|
+
msoffice_crypt_path: str = '/opt/msoffice/bin/msoffice-crypt.exe',
|
|
72
|
+
) -> str | Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Export report to file
|
|
75
|
+
|
|
76
|
+
Arguments
|
|
77
|
+
- path : Path to save the report
|
|
78
|
+
- export_format : Format to export the report
|
|
79
|
+
- password : Password to protect the file (Only works with Microsoft Excel format)
|
|
80
|
+
- msoffice_crypt_path : Path to the msoffice-crypt.exe executable, used to encrypt the file
|
|
81
|
+
if is None, the file will not be encrypted
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
- str : Full path of the exported file
|
|
85
|
+
- dict : JSON representation of the report
|
|
86
|
+
"""
|
|
60
87
|
if export_format:
|
|
61
88
|
if export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
62
|
-
return self._export_xlsx(path)
|
|
89
|
+
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
63
90
|
elif export_format == ReportFormat.JSON:
|
|
64
91
|
return self._export_json()
|
|
65
92
|
else:
|
|
66
93
|
raise AttributeError(f'Unsupported export format: {export_format}')
|
|
67
94
|
|
|
68
95
|
if self.export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
69
|
-
return self._export_xlsx(path)
|
|
96
|
+
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
70
97
|
elif self.export_format == ReportFormat.JSON:
|
|
71
98
|
return self._export_json()
|
|
72
99
|
else:
|
|
73
100
|
raise AttributeError(f'Unsupported export format: {self.export_format}')
|
|
74
101
|
|
|
75
|
-
def export_as_json(self) ->
|
|
76
|
-
"""
|
|
102
|
+
def export_as_json(self: Self) -> Dict[str, Any]:
|
|
103
|
+
"""Returns the report as a JSON dict"""
|
|
77
104
|
return self._export_json()
|
|
78
105
|
|
|
79
|
-
def _export_json(self) ->
|
|
80
|
-
"""
|
|
106
|
+
def _export_json(self: Self) -> Dict[str, Any]:
|
|
107
|
+
"""Returns a JSON dict of the report"""
|
|
81
108
|
json_pages = []
|
|
82
109
|
for page in self.pages:
|
|
83
110
|
headers = []
|
|
84
111
|
for header in page.headers:
|
|
85
|
-
headers.append(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
112
|
+
headers.append(
|
|
113
|
+
{
|
|
114
|
+
'content': header.content,
|
|
115
|
+
'text_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
116
|
+
'color': header.color,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
90
119
|
rows = []
|
|
91
120
|
for row in page.rows:
|
|
92
121
|
cells = []
|
|
93
122
|
for cell in row.content:
|
|
94
|
-
cells.append(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
cells.append(
|
|
124
|
+
{
|
|
125
|
+
'content': cell.content,
|
|
126
|
+
'text_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
127
|
+
'color': cell.color,
|
|
128
|
+
'data_type': cell.data_type.value,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
rows.append(
|
|
132
|
+
{
|
|
133
|
+
'content': cells,
|
|
134
|
+
'compact': row.compact,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
json_pages.append(
|
|
138
|
+
{
|
|
139
|
+
'name': page.name,
|
|
140
|
+
'headers': headers,
|
|
141
|
+
'rows': rows,
|
|
142
|
+
}
|
|
143
|
+
)
|
|
109
144
|
|
|
110
145
|
return {
|
|
111
146
|
'name': self.name,
|
|
112
147
|
'pages': json_pages,
|
|
113
148
|
}
|
|
114
149
|
|
|
115
|
-
def _export_xlsx(
|
|
116
|
-
|
|
150
|
+
def _export_xlsx(
|
|
151
|
+
self: Self,
|
|
152
|
+
path: str,
|
|
153
|
+
password: str = None,
|
|
154
|
+
msoffice_crypt_path: str = None,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""Export to Microsoft Excel (.xslx)"""
|
|
117
157
|
|
|
118
158
|
full_path = os.path.join(path, self.filename)
|
|
119
159
|
book = xlsxwriter.Workbook(full_path)
|
|
120
160
|
|
|
161
|
+
pages_name = []
|
|
162
|
+
|
|
121
163
|
for page in self.pages:
|
|
122
|
-
sheet_name = page.name[0:
|
|
164
|
+
sheet_name = page.name[0:20]
|
|
165
|
+
|
|
166
|
+
if sheet_name in pages_name:
|
|
167
|
+
sheet_name = f'{sheet_name} ({pages_name.count(sheet_name) + 1})'
|
|
123
168
|
|
|
124
169
|
# Allow only numbers, letters, spaces and _ or - characters
|
|
125
170
|
# Other characters will be removed
|
|
@@ -135,19 +180,21 @@ class Report:
|
|
|
135
180
|
sheet.freeze_panes(1, 0)
|
|
136
181
|
|
|
137
182
|
for i, header in enumerate(page.headers):
|
|
138
|
-
style = book.add_format(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
183
|
+
style = book.add_format(
|
|
184
|
+
{
|
|
185
|
+
'align': header.align.value,
|
|
186
|
+
'font_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
187
|
+
'bg_color': header.color,
|
|
188
|
+
'bold': header.bold,
|
|
189
|
+
'valign': 'vcenter',
|
|
190
|
+
'font_size': 11,
|
|
191
|
+
'top': 1,
|
|
192
|
+
'left': 1,
|
|
193
|
+
'right': 1,
|
|
194
|
+
'bottom': 1,
|
|
195
|
+
'font_name': 'Aptos Narrow',
|
|
196
|
+
}
|
|
197
|
+
)
|
|
151
198
|
sheet.write(0, i, header.content, style)
|
|
152
199
|
|
|
153
200
|
for i, row in enumerate(page.rows):
|
|
@@ -178,7 +225,8 @@ class Report:
|
|
|
178
225
|
elif cell.data_type == ReportDataType.CURRENCY:
|
|
179
226
|
value = float(cell.content)
|
|
180
227
|
style.update(
|
|
181
|
-
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
228
|
+
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
229
|
+
)
|
|
182
230
|
else:
|
|
183
231
|
value = cell.content
|
|
184
232
|
|
|
@@ -192,11 +240,23 @@ class Report:
|
|
|
192
240
|
sheet.autofit()
|
|
193
241
|
book.close()
|
|
194
242
|
|
|
243
|
+
if password and msoffice_crypt_path:
|
|
244
|
+
new_path = os.path.join(path, f'encrypted_{self.filename}')
|
|
245
|
+
log.debug(f'Executing `{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"`')
|
|
246
|
+
os.system(f'{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"')
|
|
247
|
+
os.remove(full_path)
|
|
248
|
+
|
|
249
|
+
with open(new_path, 'rb') as f:
|
|
250
|
+
with open(full_path, 'wb') as f2:
|
|
251
|
+
f2.write(f.read())
|
|
252
|
+
|
|
253
|
+
os.remove(new_path)
|
|
254
|
+
|
|
195
255
|
return full_path
|
|
196
256
|
|
|
197
257
|
|
|
198
258
|
class ReportConfiguration:
|
|
199
|
-
"""
|
|
259
|
+
"""
|
|
200
260
|
Report Configuration class
|
|
201
261
|
---
|
|
202
262
|
Attributes
|
|
@@ -204,19 +264,19 @@ class ReportConfiguration:
|
|
|
204
264
|
- pages_count : Number of pages in the report
|
|
205
265
|
"""
|
|
206
266
|
|
|
207
|
-
def __init__(self, title: str, pages_count: int) -> None:
|
|
267
|
+
def __init__(self: Self, title: str, pages_count: int) -> None:
|
|
208
268
|
self.title = title
|
|
209
269
|
self.pages_count = pages_count
|
|
210
270
|
|
|
211
271
|
@property
|
|
212
|
-
def _readable(self) -> str:
|
|
213
|
-
"""
|
|
272
|
+
def _readable(self: Self) -> str | None | bool:
|
|
273
|
+
"""Readable property"""
|
|
214
274
|
return f'ReportConfiguration(title={self.title}, pages_count={self.pages_count})'
|
|
215
275
|
|
|
216
|
-
def __repr__(self) -> str:
|
|
217
|
-
"""
|
|
276
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
277
|
+
"""Readable property"""
|
|
218
278
|
return self._readable
|
|
219
279
|
|
|
220
|
-
def __str__(self) -> str:
|
|
221
|
-
"""
|
|
280
|
+
def __str__(self: Self) -> str | None | bool:
|
|
281
|
+
"""Readable property"""
|
|
222
282
|
return self._readable
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
""" Report row """
|
|
2
2
|
|
|
3
|
+
from typing import List, Self
|
|
4
|
+
|
|
3
5
|
from .col import ReportCol
|
|
4
6
|
|
|
5
7
|
|
|
@@ -15,28 +17,27 @@ class ReportRow:
|
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
content:
|
|
20
|
+
self: Self,
|
|
21
|
+
content: List[ReportCol],
|
|
20
22
|
height: float = None,
|
|
21
23
|
compact: bool = False,
|
|
22
24
|
) -> None:
|
|
23
25
|
""" Constructor """
|
|
24
26
|
self.content = content
|
|
27
|
+
self.compact = compact
|
|
25
28
|
|
|
26
29
|
if height is not None:
|
|
27
|
-
raise DeprecationWarning('height is deprecated
|
|
28
|
-
|
|
29
|
-
self.compact = compact
|
|
30
|
+
raise DeprecationWarning('height is deprecated.')
|
|
30
31
|
|
|
31
32
|
@property
|
|
32
|
-
def _readable(self) -> str:
|
|
33
|
+
def _readable(self: Self) -> str | None | bool:
|
|
33
34
|
""" Readable property """
|
|
34
35
|
return f'ReportRow(content={self.content})'
|
|
35
36
|
|
|
36
|
-
def __str__(self) -> str:
|
|
37
|
+
def __str__(self: Self) -> str | None | bool:
|
|
37
38
|
""" Readable property """
|
|
38
39
|
return self._readable
|
|
39
40
|
|
|
40
|
-
def __repr__(self) -> str:
|
|
41
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
41
42
|
""" Readable property """
|
|
42
43
|
return self._readable
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
""" Message entity """
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Self
|
|
3
4
|
|
|
4
5
|
from .position import Position
|
|
5
6
|
|
|
@@ -18,12 +19,12 @@ class Message:
|
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
def __init__(
|
|
21
|
-
self,
|
|
22
|
+
self: Self,
|
|
22
23
|
pk: int,
|
|
23
24
|
asset_id: int,
|
|
24
25
|
position: Position = None,
|
|
25
|
-
payload:
|
|
26
|
-
sensors:
|
|
26
|
+
payload: Any = None,
|
|
27
|
+
sensors: Any = None,
|
|
27
28
|
received_at: datetime = None,
|
|
28
29
|
) -> None:
|
|
29
30
|
""" Constructor """
|
|
@@ -32,4 +33,4 @@ class Message:
|
|
|
32
33
|
self.position = position or Position()
|
|
33
34
|
self.payload = payload or {}
|
|
34
35
|
self.sensors = sensors or {}
|
|
35
|
-
self.received_at = received_at or datetime.now()
|
|
36
|
+
self.received_at = received_at or datetime.now(timezone.utc)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
""" Position entity """
|
|
2
|
+
from typing import Self
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class Position:
|
|
@@ -16,7 +17,7 @@ class Position:
|
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
def __init__(
|
|
19
|
-
self,
|
|
20
|
+
self: Self,
|
|
20
21
|
latitude: float = None,
|
|
21
22
|
longitude: float = None,
|
|
22
23
|
altitude: float = None,
|
|
@@ -35,15 +36,15 @@ class Position:
|
|
|
35
36
|
self.satellites = satellites
|
|
36
37
|
|
|
37
38
|
@property
|
|
38
|
-
def _readable(self) -> str:
|
|
39
|
+
def _readable(self: Self) -> str | None | bool:
|
|
39
40
|
""" Readable """
|
|
40
41
|
return f'Position(latitude={self.latitude}, longitude={self.longitude}, altitude={self.altitude}, ' +\
|
|
41
42
|
f'speed={self.speed}, direction={self.direction}, hdop={self.hdop}, satellites={self.satellites})'
|
|
42
43
|
|
|
43
|
-
def __str__(self) -> str:
|
|
44
|
+
def __str__(self: Self) -> str | None | bool:
|
|
44
45
|
""" Readable property """
|
|
45
46
|
return self._readable
|
|
46
47
|
|
|
47
|
-
def __repr__(self) -> str:
|
|
48
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
48
49
|
""" Readable property """
|
|
49
50
|
return self._readable
|
layrz_sdk/helpers/color.py
CHANGED
|
@@ -2,27 +2,28 @@
|
|
|
2
2
|
Color helpers
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from typing import Tuple
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
def convert_to_rgba(hex_color: str) -> Tuple[int, int, int, int]:
|
|
7
9
|
"""
|
|
8
10
|
Convert Hex (or Hexa) color to RGB (or RGBA) color
|
|
9
|
-
|
|
10
11
|
Arguments
|
|
11
12
|
---------
|
|
12
13
|
hex_color (str): Hex (or Hexa) color
|
|
13
14
|
Returns
|
|
14
15
|
-------
|
|
15
16
|
tuple(r,g,b,a): Combination of colors. When the argument (hex_color) is Hex, the alpha channel is set to 1.
|
|
16
|
-
|
|
17
|
+
"""
|
|
17
18
|
|
|
18
19
|
if not hex_color.startswith('#'):
|
|
19
20
|
raise ValueError('Invalid color, must starts with #')
|
|
20
21
|
|
|
21
22
|
hex_color = hex_color.replace('#', '')
|
|
22
23
|
if len(hex_color) == 6:
|
|
23
|
-
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) + (1,
|
|
24
|
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + (1,)
|
|
24
25
|
|
|
25
|
-
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4, 6))
|
|
26
|
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def use_black(color: str) -> bool:
|