pygazpar 1.2.8__py39-none-any.whl → 1.3.0a6__py39-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pygazpar/excelparser.py CHANGED
@@ -1,138 +1,136 @@
1
- import logging
2
- from datetime import datetime
3
- from pygazpar.enum import Frequency
4
- from pygazpar.enum import PropertyName
5
- from openpyxl.worksheet.worksheet import Worksheet
6
- from openpyxl.cell.cell import Cell
7
- from openpyxl import load_workbook
8
- from typing import Any, List, Dict
9
-
10
-
11
- FIRST_DATA_LINE_NUMBER = 10
12
-
13
- Logger = logging.getLogger(__name__)
14
-
15
-
16
- # ------------------------------------------------------------------------------------------------------------
17
- class ExcelParser:
18
-
19
- # ------------------------------------------------------
20
- @staticmethod
21
- def parse(dataFilename: str, dataReadingFrequency: Frequency) -> List[Dict[str, Any]]:
22
-
23
- parseByFrequency = {
24
- Frequency.HOURLY: ExcelParser.__parseHourly,
25
- Frequency.DAILY: ExcelParser.__parseDaily,
26
- Frequency.WEEKLY: ExcelParser.__parseWeekly,
27
- Frequency.MONTHLY: ExcelParser.__parseMonthly
28
- }
29
-
30
- Logger.debug(f"Loading Excel data file '{dataFilename}'...")
31
-
32
- workbook = load_workbook(filename=dataFilename)
33
-
34
- worksheet = workbook.active
35
-
36
- res = parseByFrequency[dataReadingFrequency](worksheet) # type: ignore
37
-
38
- workbook.close()
39
-
40
- Logger.debug("Processed Excel %s data: %s", dataReadingFrequency, res)
41
-
42
- return res
43
-
44
- # ------------------------------------------------------
45
- @staticmethod
46
- def __fillRow(row: Dict, propertyName: str, cell: Cell, isNumber: bool):
47
-
48
- if cell.value is not None:
49
- if isNumber:
50
- if type(cell.value) is str:
51
- if len(cell.value.strip()) > 0:
52
- row[propertyName] = float(cell.value.replace(',', '.'))
53
- else:
54
- row[propertyName] = cell.value
55
- else:
56
- row[propertyName] = cell.value.strip() if type(cell.value) is str else cell.value
57
-
58
- # ------------------------------------------------------
59
- @staticmethod
60
- def __parseHourly(worksheet: Worksheet) -> List[Dict[str, Any]]:
61
- return []
62
-
63
- # ------------------------------------------------------
64
- @staticmethod
65
- def __parseDaily(worksheet: Worksheet) -> List[Dict[str, Any]]:
66
-
67
- res = []
68
-
69
- # Timestamp of the data.
70
- data_timestamp = datetime.now().isoformat()
71
-
72
- minRowNum = FIRST_DATA_LINE_NUMBER
73
- maxRowNum = len(worksheet['B'])
74
- for rownum in range(minRowNum, maxRowNum + 1):
75
- row = {}
76
- if worksheet.cell(column=2, row=rownum).value is not None:
77
- ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
78
- ExcelParser.__fillRow(row, PropertyName.START_INDEX.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
79
- ExcelParser.__fillRow(row, PropertyName.END_INDEX.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
80
- ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=5, row=rownum), True) # type: ignore
81
- ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=6, row=rownum), True) # type: ignore
82
- ExcelParser.__fillRow(row, PropertyName.CONVERTER_FACTOR.value, worksheet.cell(column=7, row=rownum), True) # type: ignore
83
- ExcelParser.__fillRow(row, PropertyName.TEMPERATURE.value, worksheet.cell(column=8, row=rownum), True) # type: ignore
84
- ExcelParser.__fillRow(row, PropertyName.TYPE.value, worksheet.cell(column=9, row=rownum), False) # type: ignore
85
- row[PropertyName.TIMESTAMP.value] = data_timestamp
86
- res.append(row)
87
-
88
- Logger.debug(f"Daily data read successfully between row #{minRowNum} and row #{maxRowNum}")
89
-
90
- return res
91
-
92
- # ------------------------------------------------------
93
- @staticmethod
94
- def __parseWeekly(worksheet: Worksheet) -> List[Dict[str, Any]]:
95
-
96
- res = []
97
-
98
- # Timestamp of the data.
99
- data_timestamp = datetime.now().isoformat()
100
-
101
- minRowNum = FIRST_DATA_LINE_NUMBER
102
- maxRowNum = len(worksheet['B'])
103
- for rownum in range(minRowNum, maxRowNum + 1):
104
- row = {}
105
- if worksheet.cell(column=2, row=rownum).value is not None:
106
- ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
107
- ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
108
- ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
109
- row[PropertyName.TIMESTAMP.value] = data_timestamp
110
- res.append(row)
111
-
112
- Logger.debug(f"Weekly data read successfully between row #{minRowNum} and row #{maxRowNum}")
113
-
114
- return res
115
-
116
- # ------------------------------------------------------
117
- @staticmethod
118
- def __parseMonthly(worksheet: Worksheet) -> List[Dict[str, Any]]:
119
-
120
- res = []
121
-
122
- # Timestamp of the data.
123
- data_timestamp = datetime.now().isoformat()
124
-
125
- minRowNum = FIRST_DATA_LINE_NUMBER
126
- maxRowNum = len(worksheet['B'])
127
- for rownum in range(minRowNum, maxRowNum + 1):
128
- row = {}
129
- if worksheet.cell(column=2, row=rownum).value is not None:
130
- ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
131
- ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
132
- ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
133
- row[PropertyName.TIMESTAMP.value] = data_timestamp
134
- res.append(row)
135
-
136
- Logger.debug(f"Monthly data read successfully between row #{minRowNum} and row #{maxRowNum}")
137
-
138
- return res
1
+ import logging
2
+ from datetime import datetime
3
+ from pygazpar.enum import Frequency
4
+ from pygazpar.enum import PropertyName
5
+ from openpyxl.worksheet.worksheet import Worksheet
6
+ from openpyxl.cell.cell import Cell
7
+ from openpyxl import load_workbook
8
+ from typing import Any, List, Dict
9
+
10
+
11
+ FIRST_DATA_LINE_NUMBER = 10
12
+
13
+ Logger = logging.getLogger(__name__)
14
+
15
+
16
+ # ------------------------------------------------------------------------------------------------------------
17
+ class ExcelParser:
18
+
19
+ # ------------------------------------------------------
20
+ @staticmethod
21
+ def parse(dataFilename: str, dataReadingFrequency: Frequency) -> List[Dict[str, Any]]:
22
+
23
+ parseByFrequency = {
24
+ Frequency.HOURLY: ExcelParser.__parseHourly,
25
+ Frequency.DAILY: ExcelParser.__parseDaily,
26
+ Frequency.WEEKLY: ExcelParser.__parseWeekly,
27
+ Frequency.MONTHLY: ExcelParser.__parseMonthly
28
+ }
29
+
30
+ Logger.debug(f"Loading Excel data file '{dataFilename}'...")
31
+
32
+ workbook = load_workbook(filename=dataFilename)
33
+
34
+ worksheet = workbook.active
35
+
36
+ res = parseByFrequency[dataReadingFrequency](worksheet)
37
+
38
+ workbook.close()
39
+
40
+ return res
41
+
42
+ # ------------------------------------------------------
43
+ @staticmethod
44
+ def __fillRow(row: Dict, propertyName: str, cell: Cell, isNumber: bool):
45
+
46
+ if cell.value is not None:
47
+ if isNumber:
48
+ if type(cell.value) is str:
49
+ if len(cell.value.strip()) > 0:
50
+ row[propertyName] = float(cell.value.replace(',', '.'))
51
+ else:
52
+ row[propertyName] = cell.value
53
+ else:
54
+ row[propertyName] = cell.value.strip() if type(cell.value) is str else cell.value
55
+
56
+ # ------------------------------------------------------
57
+ @staticmethod
58
+ def __parseHourly(worksheet: Worksheet) -> List[Dict[str, Any]]:
59
+ return []
60
+
61
+ # ------------------------------------------------------
62
+ @staticmethod
63
+ def __parseDaily(worksheet: Worksheet) -> List[Dict[str, Any]]:
64
+
65
+ res = []
66
+
67
+ # Timestamp of the data.
68
+ data_timestamp = datetime.now().isoformat()
69
+
70
+ minRowNum = FIRST_DATA_LINE_NUMBER
71
+ maxRowNum = len(worksheet['B'])
72
+ for rownum in range(minRowNum, maxRowNum + 1):
73
+ row = {}
74
+ if worksheet.cell(column=2, row=rownum).value is not None:
75
+ ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
76
+ ExcelParser.__fillRow(row, PropertyName.START_INDEX.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
77
+ ExcelParser.__fillRow(row, PropertyName.END_INDEX.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
78
+ ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=5, row=rownum), True) # type: ignore
79
+ ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=6, row=rownum), True) # type: ignore
80
+ ExcelParser.__fillRow(row, PropertyName.CONVERTER_FACTOR.value, worksheet.cell(column=7, row=rownum), True) # type: ignore
81
+ ExcelParser.__fillRow(row, PropertyName.TEMPERATURE.value, worksheet.cell(column=8, row=rownum), True) # type: ignore
82
+ ExcelParser.__fillRow(row, PropertyName.TYPE.value, worksheet.cell(column=9, row=rownum), False) # type: ignore
83
+ row[PropertyName.TIMESTAMP.value] = data_timestamp
84
+ res.append(row)
85
+
86
+ Logger.debug(f"Daily data read successfully between row #{minRowNum} and row #{maxRowNum}")
87
+
88
+ return res
89
+
90
+ # ------------------------------------------------------
91
+ @staticmethod
92
+ def __parseWeekly(worksheet: Worksheet) -> List[Dict[str, Any]]:
93
+
94
+ res = []
95
+
96
+ # Timestamp of the data.
97
+ data_timestamp = datetime.now().isoformat()
98
+
99
+ minRowNum = FIRST_DATA_LINE_NUMBER
100
+ maxRowNum = len(worksheet['B'])
101
+ for rownum in range(minRowNum, maxRowNum + 1):
102
+ row = {}
103
+ if worksheet.cell(column=2, row=rownum).value is not None:
104
+ ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
105
+ ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
106
+ ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
107
+ row[PropertyName.TIMESTAMP.value] = data_timestamp
108
+ res.append(row)
109
+
110
+ Logger.debug(f"Weekly data read successfully between row #{minRowNum} and row #{maxRowNum}")
111
+
112
+ return res
113
+
114
+ # ------------------------------------------------------
115
+ @staticmethod
116
+ def __parseMonthly(worksheet: Worksheet) -> List[Dict[str, Any]]:
117
+
118
+ res = []
119
+
120
+ # Timestamp of the data.
121
+ data_timestamp = datetime.now().isoformat()
122
+
123
+ minRowNum = FIRST_DATA_LINE_NUMBER
124
+ maxRowNum = len(worksheet['B'])
125
+ for rownum in range(minRowNum, maxRowNum + 1):
126
+ row = {}
127
+ if worksheet.cell(column=2, row=rownum).value is not None:
128
+ ExcelParser.__fillRow(row, PropertyName.TIME_PERIOD.value, worksheet.cell(column=2, row=rownum), False) # type: ignore
129
+ ExcelParser.__fillRow(row, PropertyName.VOLUME.value, worksheet.cell(column=3, row=rownum), True) # type: ignore
130
+ ExcelParser.__fillRow(row, PropertyName.ENERGY.value, worksheet.cell(column=4, row=rownum), True) # type: ignore
131
+ row[PropertyName.TIMESTAMP.value] = data_timestamp
132
+ res.append(row)
133
+
134
+ Logger.debug(f"Monthly data read successfully between row #{minRowNum} and row #{maxRowNum}")
135
+
136
+ return res
pygazpar/jsonparser.py CHANGED
@@ -29,7 +29,7 @@ class JsonParser:
29
29
 
30
30
  for releve in data[pceIdentifier]['releves']:
31
31
  temperature = releve['temperature']
32
- if temperature is None and temperatures is not None and len(temperatures) > 0:
32
+ if temperature is None:
33
33
  temperature = temperatures.get(releve['journeeGaziere'])
34
34
 
35
35
  item = {}
pygazpar/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.8"
1
+ __version__ = "1.3.0a6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygazpar
3
- Version: 1.2.8
3
+ Version: 1.3.0a6
4
4
  Summary: Retrieve gas consumption from GrDF web site (French Gas Company)
5
5
  Home-page: https://github.com/ssenart/pygazpar
6
6
  Author: Stephane Senart
@@ -17,22 +17,19 @@ Platform: any
17
17
  Classifier: Development Status :: 5 - Production/Stable
18
18
  Classifier: Topic :: Software Development :: Libraries
19
19
  Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.8
20
22
  Classifier: Programming Language :: Python :: 3.9
21
23
  Classifier: Programming Language :: Python :: 3.10
22
24
  Classifier: Programming Language :: Python :: 3.11
23
- Classifier: Programming Language :: Python :: 3.12
24
- Classifier: Programming Language :: Python :: 3.13
25
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.7
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE.md
28
- Requires-Dist: openpyxl>=2.6.3
29
- Requires-Dist: requests>=2.26.0
28
+ Requires-Dist: openpyxl >=2.6.3
29
+ Requires-Dist: requests >=2.26.0
30
30
  Requires-Dist: pandas
31
31
 
32
32
  # PyGazpar
33
-
34
- ## $\text{\color{green}{!!! This library is working again. CAPTCHA has been removed !!!}}$
35
-
36
33
  PyGazpar is a Python library for getting natural gas consumption from GrDF French provider.
37
34
 
38
35
  Their natural gas meter is called Gazpar. It is wireless and transmit the gas consumption once per day.
@@ -217,40 +214,6 @@ All notable changes to this project will be documented in this file.
217
214
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
218
215
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
219
216
 
220
- ## [1.2.8](https://github.com/ssenart/PyGazpar/compare/1.2.8...1.2.7) - 2025-01-11
221
-
222
- ### Added
223
- - [#81](https://github.com/ssenart/PyGazpar/issues/81): Add meter/temperature debug log messages to help investigation in case of errors.
224
-
225
- ## [1.2.7](https://github.com/ssenart/PyGazpar/compare/1.2.7...1.2.6) - 2025-01-06
226
-
227
- ### Fixed
228
- - [#79](https://github.com/ssenart/PyGazpar/issues/79): Fix some unittests that wrongly failed because of the new year.
229
-
230
- ## [1.2.6](https://github.com/ssenart/PyGazpar/compare/1.2.6...1.2.5) - 2025-01-03
231
-
232
- ### Fixed
233
- - [#77](https://github.com/ssenart/PyGazpar/issues/77): Some error may occur while requesting data from GrDF API.
234
-
235
- ## [1.2.5](https://github.com/ssenart/PyGazpar/compare/1.2.5...1.2.4) - 2024-12-21
236
-
237
- ### Fixed
238
- - [#75](https://github.com/ssenart/PyGazpar/issues/75): Fix an error when no temperature data is available.
239
-
240
- ## [1.2.4](https://github.com/ssenart/PyGazpar/compare/1.2.4...1.2.3) - 2024-10-09
241
-
242
- ### Fixed
243
- - [#72](https://github.com/ssenart/PyGazpar/issues/72): Remove the warning message "UserWarning: Boolean Series key will be reindexed to match DataFrame index. df = pd.concat([df[(df["count"] >= 7)], df.tail(1)[df["count"] < 7]])".
244
-
245
- ## [1.2.3](https://github.com/ssenart/PyGazpar/compare/1.2.3...1.2.1) - 2024-10-05
246
-
247
- ### Added
248
- - [#70](https://github.com/ssenart/PyGazpar/issues/70): Add Python 3.12 support.
249
-
250
- ## [1.2.2](https://github.com/ssenart/PyGazpar/compare/1.2.1...1.2.2) - 2024-05-08
251
-
252
- ### Fixed
253
- - [#65](https://github.com/ssenart/PyGazpar/issues/65): [Bug] PermissionError happens when loading data from Excel file.
254
217
 
255
218
  ## [1.2.1](https://github.com/ssenart/PyGazpar/compare/1.2.0...1.2.1) - 2024-05-04
256
219
 
@@ -1,11 +1,11 @@
1
1
  pygazpar/__init__.py,sha256=qshO_XZbDA2Wrt80ABDs0MoScqJytClAuIJjAnILglk,309
2
2
  pygazpar/__main__.py,sha256=Pt3PInX7QiWcs0aBKZN90NTaU8KFnrQiZ5Hsow1eR5U,3177
3
3
  pygazpar/client.py,sha256=JdVm0jZbeibwtTumcRbUSFadfXnCUClPMjL95_J6p5Y,2595
4
- pygazpar/datasource.py,sha256=SQ6ko4ElWp9BBid-TcBo9iWiCMO4VL-_pTkGb6sCGDE,21400
4
+ pygazpar/datasource.py,sha256=gqOREo9OQaBmqWgaDIgShJrP1dZNVFJoIxUu9Rfxmec,21374
5
5
  pygazpar/enum.py,sha256=3ZCk4SziXF6pxgP3MuQ1qxYfqB3X5DOV8Rtd0GHsK9w,898
6
- pygazpar/excelparser.py,sha256=A9s0A1xpIxGiqLvVd80p7ntb89Grq9-tVb46Al0xJx8,6149
7
- pygazpar/jsonparser.py,sha256=OrRdMZNBi7rI4dRGoRW7gjyFwJFk-IvtkRZ_t1XVFrI,1859
8
- pygazpar/version.py,sha256=CfVXm0wwlKPW0khOcwhWw61TpgtZiLijCePsAIOK3aU,22
6
+ pygazpar/excelparser.py,sha256=glWlbj22pxYjHGKurOFmhzcVAoWCvfOHn7_Y6GgHUPo,5915
7
+ pygazpar/jsonparser.py,sha256=AWdU3h7UohsOov8HpeP8GNuqcnDmM4r3I7-CI_crDvA,1804
8
+ pygazpar/version.py,sha256=rcyBYFWxUArZ5y56YkLypabOiwSZfCl48nXHK0ebB20,24
9
9
  pygazpar/resources/daily_data_sample.json,sha256=YJovtrNUMs257magTfyxiewLmecySFypcelbGFUUeT8,199583
10
10
  pygazpar/resources/hourly_data_sample.json,sha256=N1F-Xz3GaBn2H1p7uKzhkhKCQV8QVR0t76XD6wmFtXA,3
11
11
  pygazpar/resources/monthly_data_sample.json,sha256=yrr4SqrB2MubeVU2HX_FRDZKHIhC0LXCqkO1iqnFWcg,3351
@@ -16,12 +16,12 @@ samples/excelSample.py,sha256=ltAl-bBz9-U9YI802JpcIswra-vDS7tR_KL5VNdxJ5c,765
16
16
  samples/jsonSample.py,sha256=sYAIusdEJhZdwDAMgHqoWcwDR0FA2eWhSt_2gL_mJRk,736
17
17
  samples/testSample.py,sha256=UeirdEtezHwfZDv_75oxul17YzGWn5yZuHfJYTF3Ez0,387
18
18
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- tests/test_client.py,sha256=BFLz0GNk8qEP4d2-7cAaIcLWpkSnDZo3wRb0AlXBNOQ,5765
19
+ tests/test_client.py,sha256=OwYBeNC66WiykU1IUEf4eZacAU49xQJXsFQY6kYiXCQ,5111
20
20
  tests/test_datafileparser.py,sha256=nAeUpOHtelblMpmbrrnf-2GuMjK5ai65veDoymceprE,818
21
- tests/test_datasource.py,sha256=NTeD3yQEi4fTw8ZWz2cuUUdeFC8QVjChBMw5jyqia40,6149
22
- pygazpar-1.2.8.dist-info/LICENSE.md,sha256=XsCJx_7_BC9tvmE0ZxS1cTNR7ekurog_ea9ybdZ-8tc,1073
23
- pygazpar-1.2.8.dist-info/METADATA,sha256=Zu5z1G4sFqNKO0t93-GRaCJsInFFi46B3POXgYsV3c0,19435
24
- pygazpar-1.2.8.dist-info/WHEEL,sha256=E6NuJA_MIt2CDvbuRmqVFG99302DhLXEpOp5wbsbDeU,93
25
- pygazpar-1.2.8.dist-info/entry_points.txt,sha256=c_FMZPYlRv1w9EqfgWhlkdJOoje7FcglI0UMm2oRLoI,53
26
- pygazpar-1.2.8.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
- pygazpar-1.2.8.dist-info/RECORD,,
21
+ tests/test_datasource.py,sha256=2BCrnUh9ZbR_dAdvKT9498u5a-jGnC4aZafP7V9iZn8,5983
22
+ pygazpar-1.3.0a6.dist-info/LICENSE.md,sha256=XsCJx_7_BC9tvmE0ZxS1cTNR7ekurog_ea9ybdZ-8tc,1073
23
+ pygazpar-1.3.0a6.dist-info/METADATA,sha256=EBrPJTTN7QVCXLQvvxcXiWH54BB7QRoHqTNXHthuktk,17766
24
+ pygazpar-1.3.0a6.dist-info/WHEEL,sha256=kG0f_S63jJ569yg8d_OdUrqQSrnKsZBVKN5Kb1RSnpA,93
25
+ pygazpar-1.3.0a6.dist-info/entry_points.txt,sha256=c_FMZPYlRv1w9EqfgWhlkdJOoje7FcglI0UMm2oRLoI,53
26
+ pygazpar-1.3.0a6.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
+ pygazpar-1.3.0a6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py39-none-any
5
5