layrz-sdk 3.0.7__py3-none-any.whl → 3.0.9__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 +11 -9
- layrz_sdk/entities/general/asset_operation_mode.py +4 -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 +130 -66
- layrz_sdk/entities/reports/row.py +7 -5
- 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.7.dist-info → layrz_sdk-3.0.9.dist-info}/METADATA +6 -1
- layrz_sdk-3.0.9.dist-info/RECORD +69 -0
- {layrz_sdk-3.0.7.dist-info → layrz_sdk-3.0.9.dist-info}/WHEEL +1 -1
- layrz_sdk-3.0.7.dist-info/RECORD +0 -69
- {layrz_sdk-3.0.7.dist-info → layrz_sdk-3.0.9.dist-info}/LICENSE +0 -0
- {layrz_sdk-3.0.7.dist-info → layrz_sdk-3.0.9.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,147 @@ 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:
|
|
91
|
+
if password:
|
|
92
|
+
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
64
93
|
return self._export_json()
|
|
65
94
|
else:
|
|
66
95
|
raise AttributeError(f'Unsupported export format: {export_format}')
|
|
67
96
|
|
|
68
97
|
if self.export_format == ReportFormat.MICROSOFT_EXCEL:
|
|
69
|
-
return self._export_xlsx(path)
|
|
98
|
+
return self._export_xlsx(path=path, password=password, msoffice_crypt_path=msoffice_crypt_path)
|
|
70
99
|
elif self.export_format == ReportFormat.JSON:
|
|
100
|
+
if password:
|
|
101
|
+
return {'name': self.name, 'is_protected': True, 'pages': []}
|
|
71
102
|
return self._export_json()
|
|
72
103
|
else:
|
|
73
104
|
raise AttributeError(f'Unsupported export format: {self.export_format}')
|
|
74
105
|
|
|
75
|
-
def export_as_json(self) ->
|
|
76
|
-
"""
|
|
106
|
+
def export_as_json(self: Self) -> Dict[str, Any]:
|
|
107
|
+
"""Returns the report as a JSON dict"""
|
|
77
108
|
return self._export_json()
|
|
78
109
|
|
|
79
|
-
def _export_json(self) ->
|
|
80
|
-
"""
|
|
110
|
+
def _export_json(self: Self) -> Dict[str, Any]:
|
|
111
|
+
"""Returns a JSON dict of the report"""
|
|
81
112
|
json_pages = []
|
|
82
113
|
for page in self.pages:
|
|
83
114
|
headers = []
|
|
84
115
|
for header in page.headers:
|
|
85
|
-
headers.append(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
headers.append(
|
|
117
|
+
{
|
|
118
|
+
'content': header.content,
|
|
119
|
+
'text_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
120
|
+
'color': header.color,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
90
123
|
rows = []
|
|
91
124
|
for row in page.rows:
|
|
92
125
|
cells = []
|
|
93
126
|
for cell in row.content:
|
|
94
|
-
cells.append(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
cells.append(
|
|
128
|
+
{
|
|
129
|
+
'content': cell.content,
|
|
130
|
+
'text_color': '#000000' if use_black(cell.color) else '#ffffff',
|
|
131
|
+
'color': cell.color,
|
|
132
|
+
'data_type': cell.data_type.value,
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
rows.append(
|
|
136
|
+
{
|
|
137
|
+
'content': cells,
|
|
138
|
+
'compact': row.compact,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
json_pages.append(
|
|
142
|
+
{
|
|
143
|
+
'name': page.name,
|
|
144
|
+
'headers': headers,
|
|
145
|
+
'rows': rows,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
109
148
|
|
|
110
149
|
return {
|
|
111
150
|
'name': self.name,
|
|
112
151
|
'pages': json_pages,
|
|
113
152
|
}
|
|
114
153
|
|
|
115
|
-
def _export_xlsx(
|
|
116
|
-
|
|
154
|
+
def _export_xlsx(
|
|
155
|
+
self: Self,
|
|
156
|
+
path: str,
|
|
157
|
+
password: str = None,
|
|
158
|
+
msoffice_crypt_path: str = None,
|
|
159
|
+
) -> str:
|
|
160
|
+
"""Export to Microsoft Excel (.xslx)"""
|
|
117
161
|
|
|
118
162
|
full_path = os.path.join(path, self.filename)
|
|
119
163
|
book = xlsxwriter.Workbook(full_path)
|
|
120
164
|
|
|
165
|
+
pages_name = []
|
|
166
|
+
|
|
121
167
|
for page in self.pages:
|
|
122
|
-
sheet_name = page.name[0:
|
|
168
|
+
sheet_name = page.name[0:20]
|
|
169
|
+
|
|
170
|
+
if sheet_name in pages_name:
|
|
171
|
+
sheet_name = f'{sheet_name} ({pages_name.count(sheet_name) + 1})'
|
|
123
172
|
|
|
124
173
|
# Allow only numbers, letters, spaces and _ or - characters
|
|
125
174
|
# Other characters will be removed
|
|
@@ -135,19 +184,21 @@ class Report:
|
|
|
135
184
|
sheet.freeze_panes(1, 0)
|
|
136
185
|
|
|
137
186
|
for i, header in enumerate(page.headers):
|
|
138
|
-
style = book.add_format(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
style = book.add_format(
|
|
188
|
+
{
|
|
189
|
+
'align': header.align.value,
|
|
190
|
+
'font_color': '#000000' if use_black(header.color) else '#ffffff',
|
|
191
|
+
'bg_color': header.color,
|
|
192
|
+
'bold': header.bold,
|
|
193
|
+
'valign': 'vcenter',
|
|
194
|
+
'font_size': 11,
|
|
195
|
+
'top': 1,
|
|
196
|
+
'left': 1,
|
|
197
|
+
'right': 1,
|
|
198
|
+
'bottom': 1,
|
|
199
|
+
'font_name': 'Aptos Narrow',
|
|
200
|
+
}
|
|
201
|
+
)
|
|
151
202
|
sheet.write(0, i, header.content, style)
|
|
152
203
|
|
|
153
204
|
for i, row in enumerate(page.rows):
|
|
@@ -178,7 +229,8 @@ class Report:
|
|
|
178
229
|
elif cell.data_type == ReportDataType.CURRENCY:
|
|
179
230
|
value = float(cell.content)
|
|
180
231
|
style.update(
|
|
181
|
-
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
232
|
+
{'num_format': f'"{cell.currency_symbol}" * #,##0.00;[Red]"{cell.currency_symbol}" * #,##0.00'}
|
|
233
|
+
)
|
|
182
234
|
else:
|
|
183
235
|
value = cell.content
|
|
184
236
|
|
|
@@ -192,11 +244,23 @@ class Report:
|
|
|
192
244
|
sheet.autofit()
|
|
193
245
|
book.close()
|
|
194
246
|
|
|
247
|
+
if password and msoffice_crypt_path:
|
|
248
|
+
new_path = os.path.join(path, f'encrypted_{self.filename}')
|
|
249
|
+
log.debug(f'Executing `{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"`')
|
|
250
|
+
os.system(f'{msoffice_crypt_path} -e -p "{password}" "{full_path}" "{new_path}"')
|
|
251
|
+
os.remove(full_path)
|
|
252
|
+
|
|
253
|
+
with open(new_path, 'rb') as f:
|
|
254
|
+
with open(full_path, 'wb') as f2:
|
|
255
|
+
f2.write(f.read())
|
|
256
|
+
|
|
257
|
+
os.remove(new_path)
|
|
258
|
+
|
|
195
259
|
return full_path
|
|
196
260
|
|
|
197
261
|
|
|
198
262
|
class ReportConfiguration:
|
|
199
|
-
"""
|
|
263
|
+
"""
|
|
200
264
|
Report Configuration class
|
|
201
265
|
---
|
|
202
266
|
Attributes
|
|
@@ -204,19 +268,19 @@ class ReportConfiguration:
|
|
|
204
268
|
- pages_count : Number of pages in the report
|
|
205
269
|
"""
|
|
206
270
|
|
|
207
|
-
def __init__(self, title: str, pages_count: int) -> None:
|
|
271
|
+
def __init__(self: Self, title: str, pages_count: int) -> None:
|
|
208
272
|
self.title = title
|
|
209
273
|
self.pages_count = pages_count
|
|
210
274
|
|
|
211
275
|
@property
|
|
212
|
-
def _readable(self) -> str:
|
|
213
|
-
"""
|
|
276
|
+
def _readable(self: Self) -> str | None | bool:
|
|
277
|
+
"""Readable property"""
|
|
214
278
|
return f'ReportConfiguration(title={self.title}, pages_count={self.pages_count})'
|
|
215
279
|
|
|
216
|
-
def __repr__(self) -> str:
|
|
217
|
-
"""
|
|
280
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
281
|
+
"""Readable property"""
|
|
218
282
|
return self._readable
|
|
219
283
|
|
|
220
|
-
def __str__(self) -> str:
|
|
221
|
-
"""
|
|
284
|
+
def __str__(self: Self) -> str | None | bool:
|
|
285
|
+
"""Readable property"""
|
|
222
286
|
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,8 +17,8 @@ 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:
|
|
@@ -28,14 +30,14 @@ class ReportRow:
|
|
|
28
30
|
raise DeprecationWarning('height is deprecated.')
|
|
29
31
|
|
|
30
32
|
@property
|
|
31
|
-
def _readable(self) -> str:
|
|
33
|
+
def _readable(self: Self) -> str | None | bool:
|
|
32
34
|
""" Readable property """
|
|
33
35
|
return f'ReportRow(content={self.content})'
|
|
34
36
|
|
|
35
|
-
def __str__(self) -> str:
|
|
37
|
+
def __str__(self: Self) -> str | None | bool:
|
|
36
38
|
""" Readable property """
|
|
37
39
|
return self._readable
|
|
38
40
|
|
|
39
|
-
def __repr__(self) -> str:
|
|
41
|
+
def __repr__(self: Self) -> str | None | bool:
|
|
40
42
|
""" Readable property """
|
|
41
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:
|