tobac 1.6.2__py3-none-any.whl → 1.6.3__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.
tobac/__init__.py CHANGED
@@ -6,9 +6,7 @@ if sys.version_info < (3, 7):
6
6
  Support for Python versions less than 3.7 is deprecated.
7
7
  Version 1.5 of tobac will require Python 3.7 or later.
8
8
  Python {py} detected. \n\n
9
- """.format(
10
- py=".".join(str(v) for v in sys.version_info[:3])
11
- )
9
+ """.format(py=".".join(str(v) for v in sys.version_info[:3]))
12
10
 
13
11
  print(warning)
14
12
 
@@ -1555,9 +1555,8 @@ def feature_detection_multithreshold(
1555
1555
  label_fields[i].data[~wh_all_labels] = 0
1556
1556
 
1557
1557
  else:
1558
- features = None
1559
- label_fields = None
1560
- logging.debug("No features detected")
1558
+ features = internal_utils.coordinates.make_empty_features_dataframe(is_3D=is_3D)
1559
+ logging.debug("No features detected; returning empty features DataFrame")
1561
1560
 
1562
1561
  logging.debug("feature detection completed")
1563
1562
 
@@ -1258,6 +1258,12 @@ def segmentation(
1258
1258
  name="segmentation_mask",
1259
1259
  ).assign_attrs(threshold=threshold)
1260
1260
 
1261
+ if features is None or features.empty:
1262
+ logging.debug(
1263
+ "No features provided; returning empty segmentation mask and empty features DataFrame"
1264
+ )
1265
+ return segmentation_out_data, features
1266
+
1261
1267
  features_out_list = []
1262
1268
 
1263
1269
  if len(field.coords[time_var_name]) == 1:
@@ -1,6 +1,8 @@
1
1
  import pytest
2
2
  import tobac.segmentation as seg
3
3
  import numpy as np
4
+ import xarray as xr
5
+ import pandas as pd
4
6
  from tobac import segmentation, feature_detection, testing
5
7
  from tobac.utils import periodic_boundaries as pbc_utils
6
8
 
@@ -1181,3 +1183,43 @@ def test_seg_alt_unseed_num(below_thresh, above_thresh, error):
1181
1183
 
1182
1184
  seg_out_arr = seg_output.core_data()
1183
1185
  assert np.all(correct_seg_arr == seg_out_arr)
1186
+
1187
+
1188
+ def test_segmentation_returns_early_for_empty_features():
1189
+ """
1190
+ Tests that segmentation exits early and returns an empty segmentation mask and the empty features input when no features are provided.
1191
+ """
1192
+ from tobac.utils.internal import make_empty_features_dataframe
1193
+
1194
+ field = xr.DataArray(
1195
+ np.zeros((2, 10, 12), dtype=float),
1196
+ dims=("time", "y", "x"),
1197
+ coords={"time": pd.date_range("2000-01-01", periods=2, freq="2min")},
1198
+ name="test_field",
1199
+ )
1200
+
1201
+ df = make_empty_features_dataframe(is_3D=False)
1202
+ seg_mask, features_out = seg.segmentation(
1203
+ features=df,
1204
+ field=field,
1205
+ dxy=1000.0,
1206
+ threshold=1.0,
1207
+ )
1208
+
1209
+ # mask exists and is all zeros
1210
+ assert seg_mask.shape == field.shape
1211
+ assert np.all(seg_mask.data == 0)
1212
+
1213
+ # features returned unchanged and still empty
1214
+ assert features_out is df
1215
+ assert features_out.empty
1216
+
1217
+ sef_mask_none, features_out_none = seg.segmentation(
1218
+ features=None,
1219
+ field=field,
1220
+ dxy=1000.0,
1221
+ threshold=1.0,
1222
+ )
1223
+
1224
+ assert np.all(sef_mask_none.data == 0)
1225
+ assert features_out_none is None
@@ -5,6 +5,7 @@ import tobac.feature_detection as feat_detect
5
5
  import pytest
6
6
  import numpy as np
7
7
  import xarray as xr
8
+ import pandas as pd
8
9
  from pandas.testing import assert_frame_equal
9
10
 
10
11
 
@@ -1319,3 +1320,92 @@ def test_banded_feature():
1319
1320
  assert len(fd_output) == 1
1320
1321
  assert fd_output.iloc[0]["hdim_1"] == pytest.approx(24.5)
1321
1322
  assert fd_output.iloc[0]["hdim_2"] == pytest.approx(24.5)
1323
+
1324
+
1325
+ def test_feature_detection_2d_no_features_returns_empty_df():
1326
+ """
1327
+ Test that 2D feature detection returns an empty, properly formatted DataFrame when no features are detected.
1328
+ """
1329
+ # 2D + time => (time, y, x)
1330
+ data = np.zeros((2, 10, 12), dtype=float)
1331
+ da = xr.DataArray(
1332
+ data,
1333
+ dims=("time", "y", "x"),
1334
+ coords={"time": pd.date_range("2000-01-01", periods=2, freq="2min")},
1335
+ name="field",
1336
+ )
1337
+
1338
+ features = feat_detect.feature_detection_multithreshold(
1339
+ da,
1340
+ dxy=1000.0,
1341
+ threshold=[1e9],
1342
+ return_labels=False,
1343
+ )
1344
+
1345
+ assert isinstance(features, pd.DataFrame)
1346
+ assert features.empty
1347
+
1348
+ expected_cols = [
1349
+ "frame",
1350
+ "idx",
1351
+ "hdim_1",
1352
+ "hdim_2",
1353
+ "num",
1354
+ "threshold_value",
1355
+ "feature",
1356
+ "time",
1357
+ "timestr",
1358
+ "y",
1359
+ "x",
1360
+ "latitude",
1361
+ "longitude",
1362
+ ]
1363
+ assert list(features.columns) == expected_cols
1364
+
1365
+
1366
+ def test_feature_detection_3d_no_features_returns_empty_df():
1367
+ """
1368
+ Test that 3D feature detection returns an empty, properly formatted DataFrame when no features are detected.
1369
+ """
1370
+ # 3D + time => (time, z, y, x)
1371
+ data = np.zeros((2, 3, 10, 12), dtype=float)
1372
+ da = xr.DataArray(
1373
+ data,
1374
+ dims=("time", "z", "y", "x"),
1375
+ coords={
1376
+ "time": pd.date_range("2000-01-01", periods=2, freq="2min"),
1377
+ "z": np.arange(3),
1378
+ "y": np.arange(10),
1379
+ "x": np.arange(12),
1380
+ },
1381
+ name="field",
1382
+ )
1383
+
1384
+ features = feat_detect.feature_detection_multithreshold(
1385
+ da,
1386
+ dxy=1000.0,
1387
+ threshold=[1e9],
1388
+ return_labels=False,
1389
+ )
1390
+
1391
+ assert isinstance(features, pd.DataFrame)
1392
+ assert features.empty
1393
+
1394
+ expected_cols = [
1395
+ "frame",
1396
+ "idx",
1397
+ "vdim",
1398
+ "hdim_1",
1399
+ "hdim_2",
1400
+ "num",
1401
+ "threshold_value",
1402
+ "feature",
1403
+ "time",
1404
+ "timestr",
1405
+ "z",
1406
+ "y",
1407
+ "x",
1408
+ "latitude",
1409
+ "longitude",
1410
+ ]
1411
+ assert list(features.columns) == expected_cols
@@ -4,6 +4,7 @@ import tobac.testing as tbtest
4
4
  import pytest
5
5
  import numpy as np
6
6
  import xarray as xr
7
+ import pandas.api.types as ptypes
7
8
 
8
9
 
9
10
  @pytest.mark.parametrize(
@@ -95,3 +96,90 @@ def test_detect_latlon_coord_name(
95
96
  )
96
97
  assert out_lat_name == expected_result[0]
97
98
  assert out_lon_name == expected_result[1]
99
+
100
+
101
+ def test_make_empty_features_dataframe_2d():
102
+ """
103
+ Test that the empty 2D feature DataFrame has the correct columns and dtypes.
104
+ """
105
+ df = internal_utils.make_empty_features_dataframe(is_3D=False)
106
+
107
+ expected_cols = [
108
+ "frame",
109
+ "idx",
110
+ "hdim_1",
111
+ "hdim_2",
112
+ "num",
113
+ "threshold_value",
114
+ "feature",
115
+ "time",
116
+ "timestr",
117
+ "y",
118
+ "x",
119
+ "latitude",
120
+ "longitude",
121
+ ]
122
+
123
+ assert list(df.columns) == expected_cols
124
+ assert df.empty
125
+
126
+ assert ptypes.is_integer_dtype(df["frame"].dtype)
127
+ assert ptypes.is_integer_dtype(df["idx"].dtype)
128
+ assert ptypes.is_float_dtype(df["hdim_1"].dtype)
129
+ assert ptypes.is_float_dtype(df["hdim_2"].dtype)
130
+ assert ptypes.is_integer_dtype(df["num"].dtype)
131
+ assert ptypes.is_integer_dtype(df["threshold_value"].dtype)
132
+ assert ptypes.is_integer_dtype(df["feature"].dtype)
133
+
134
+ assert ptypes.is_datetime64_ns_dtype(df["time"].dtype)
135
+ assert df["timestr"].dtype == object
136
+
137
+ assert ptypes.is_float_dtype(df["y"].dtype)
138
+ assert ptypes.is_float_dtype(df["x"].dtype)
139
+ assert ptypes.is_float_dtype(df["latitude"].dtype)
140
+ assert ptypes.is_float_dtype(df["longitude"].dtype)
141
+
142
+
143
+ def test_make_empty_features_dataframe_3d_schema_and_dtypes():
144
+ """
145
+ Test that the empty 3D feature DataFrame has the correct columns and dtypes.
146
+ """
147
+ df = internal_utils.make_empty_features_dataframe(is_3D=True)
148
+
149
+ expected_cols = [
150
+ "frame",
151
+ "idx",
152
+ "vdim",
153
+ "hdim_1",
154
+ "hdim_2",
155
+ "num",
156
+ "threshold_value",
157
+ "feature",
158
+ "time",
159
+ "timestr",
160
+ "z",
161
+ "y",
162
+ "x",
163
+ "latitude",
164
+ "longitude",
165
+ ]
166
+ assert list(df.columns) == expected_cols
167
+ assert df.empty
168
+
169
+ assert ptypes.is_integer_dtype(df["frame"].dtype)
170
+ assert ptypes.is_integer_dtype(df["idx"].dtype)
171
+ assert ptypes.is_float_dtype(df["vdim"].dtype)
172
+ assert ptypes.is_float_dtype(df["hdim_1"].dtype)
173
+ assert ptypes.is_float_dtype(df["hdim_2"].dtype)
174
+ assert ptypes.is_integer_dtype(df["num"].dtype)
175
+ assert ptypes.is_integer_dtype(df["threshold_value"].dtype)
176
+ assert ptypes.is_integer_dtype(df["feature"].dtype)
177
+
178
+ assert ptypes.is_datetime64_ns_dtype(df["time"].dtype)
179
+ assert df["timestr"].dtype == object
180
+
181
+ assert ptypes.is_float_dtype(df["z"].dtype)
182
+ assert ptypes.is_float_dtype(df["y"].dtype)
183
+ assert ptypes.is_float_dtype(df["x"].dtype)
184
+ assert ptypes.is_float_dtype(df["latitude"].dtype)
185
+ assert ptypes.is_float_dtype(df["longitude"].dtype)
@@ -428,3 +428,59 @@ def detect_latlon_coord_name(
428
428
  out_lon = test_lon_name
429
429
  break
430
430
  return out_lat, out_lon
431
+
432
+
433
+ def make_empty_features_dataframe(is_3D: bool) -> pd.DataFrame:
434
+ """
435
+ Create an empty but properly formatted feature DataFrame.
436
+
437
+ Returns an empty pandas DataFrame with the same columns and dtypes as the feature detection output.
438
+ This is used when no features are detected, to ensure a consistent return type and avoid returning ``None``.
439
+
440
+ Parameters
441
+ ----------
442
+ is_3d : bool
443
+ Whether the feature detection is 3D. If True, include vertical coordinate columns.
444
+
445
+ Returns
446
+ -------
447
+ pandas.DataFrame
448
+ Empty DataFrame with the correct columns and dtypes for feature output.
449
+ """
450
+
451
+ if is_3D:
452
+ schema = {
453
+ "frame": "int64",
454
+ "idx": "int64",
455
+ "vdim": "float64",
456
+ "hdim_1": "float64",
457
+ "hdim_2": "float64",
458
+ "num": "int64",
459
+ "threshold_value": "int64",
460
+ "feature": "int64",
461
+ "time": "datetime64[ns]",
462
+ "timestr": "object",
463
+ "z": "float64",
464
+ "y": "float64",
465
+ "x": "float64",
466
+ "latitude": "float64",
467
+ "longitude": "float64",
468
+ }
469
+ else:
470
+ schema = {
471
+ "frame": "int64",
472
+ "idx": "int64",
473
+ "hdim_1": "float64",
474
+ "hdim_2": "float64",
475
+ "num": "int64",
476
+ "threshold_value": "int64",
477
+ "feature": "int64",
478
+ "time": "datetime64[ns]",
479
+ "timestr": "object",
480
+ "y": "float64",
481
+ "x": "float64",
482
+ "latitude": "float64",
483
+ "longitude": "float64",
484
+ }
485
+
486
+ return pd.DataFrame({c: pd.Series(dtype=t) for c, t in schema.items()})
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tobac
3
- Version: 1.6.2
3
+ Version: 1.6.3
4
4
  Summary: A package for identifying and tracking atmospheric phenomena
5
5
  Author-email: Max Heikenfeld <max.heikenfeld@physics.ox.ac.uk>, William Jones <william.jones@physics.ox.ac.uk>, Fabian Senf <senf@tropos.de>, Sean Freeman <sean.freeman@uah.edu>, Julia Kukulies <kukulies@ucar.edu>, Kelcy Brunner <Kelcy.Brunner@ttu.edu>, Sven Starzer <sven.starzer@proton.me>
6
- License-Expression: BSD-3-Clause
6
+ License: BSD-3-Clause
7
7
  Project-URL: Homepage, http://github.com/tobac-project/tobac
8
8
  Project-URL: Documentation, http://tobac.io
9
9
  Project-URL: Issues, http://github.com/tobac-project/tobac/issues
@@ -32,7 +32,7 @@ Requires-Dist: numpy
32
32
  Requires-Dist: scipy
33
33
  Requires-Dist: scikit-image
34
34
  Requires-Dist: scikit-learn
35
- Requires-Dist: pandas
35
+ Requires-Dist: pandas<3
36
36
  Requires-Dist: matplotlib
37
37
  Requires-Dist: scitools-iris
38
38
  Requires-Dist: xarray
@@ -77,7 +77,7 @@ Requires-Dist: dask; extra == "all"
77
77
  Requires-Dist: intake==0.7.0; extra == "all"
78
78
  Requires-Dist: intake-xarray==0.7.0; extra == "all"
79
79
  Requires-Dist: healpix; extra == "all"
80
- Requires-Dist: easygems; extra == "all"
80
+ Requires-Dist: easygems==0.1.2; extra == "all"
81
81
  Dynamic: license-file
82
82
 
83
83
  # tobac - Tracking and Object-based Analysis of Clouds
@@ -1,6 +1,6 @@
1
- tobac/__init__.py,sha256=Au5BW9GLvQ6zR8GFQWf84MhPNIVPw0PtXC4asKwhHSk,3124
1
+ tobac/__init__.py,sha256=ATNzG0Kt3Nag2yfCR2dWq5GIjR5kpr8kQMcOlJ6I0LI,3110
2
2
  tobac/centerofgravity.py,sha256=4aEqK-T3w1zQ1CwqSSyD7AKQpnu3tmGqG8Rsp4ieAcM,7312
3
- tobac/feature_detection.py,sha256=DVgngbmCYeOFKt9dCrMZ-d7jEtdo44TDi-kmoAETbjw,71504
3
+ tobac/feature_detection.py,sha256=AomXobZ6zU-vs92P4ZTiKmbudvmbUymm9FxbVDglaxg,71577
4
4
  tobac/merge_split.py,sha256=Y3jMCkb3IbWztZ9261JDvJMCRdDPT1AO-snDAWe2cjc,12291
5
5
  tobac/plotting.py,sha256=PBlISvYu9dWNCdHBNNvkWjC32sPQ04nAFncBIyH4u6Q,78926
6
6
  tobac/testing.py,sha256=zj6k0Oag8jEzbcZctQ46fpmLMZolIhshX1DIZ7EoLzg,38842
@@ -11,12 +11,12 @@ tobac/analysis/cell_analysis.py,sha256=0lt-u1qSQylVE_Sy4xYksWPnA83DUoGTzVyPExqsd
11
11
  tobac/analysis/feature_analysis.py,sha256=Z23yLxkMR3L7GvPraZ4LNEjHVIo1LcFEHGFWdDc-3hs,6726
12
12
  tobac/analysis/spatial.py,sha256=Gryz3N3HYTDkKM3qQBeyJeZac9vuXgrHg1vvJcfcJD4,22774
13
13
  tobac/segmentation/__init__.py,sha256=jal-uj581IXyJRJNoor7ktxH_EqDAmnvs5mJDhPSPKQ,225
14
- tobac/segmentation/watershed_segmentation.py,sha256=J6Yo14HuqjVsunzoORLLw8WVefqn-GpLiIh6mWlDO0o,54848
14
+ tobac/segmentation/watershed_segmentation.py,sha256=JMi70cIFetnNiL5QYOhJq0yJs71lbFuwlam96eNd6Kc,55071
15
15
  tobac/tests/test_analysis_spatial.py,sha256=FLl7CJ2N9x7OR1ipON-OhdyMOMiR1egAEShEQoxN7wI,31997
16
16
  tobac/tests/test_convert.py,sha256=xOrUPfUCtUlHLAN1HpEp72g5XiOii6bB7U4COQRHSTQ,8821
17
17
  tobac/tests/test_datetime.py,sha256=UKQqdA5yHWHYBrqp8ebn82kQZt07Mwh1r0BGNHQ3PZ4,7120
18
18
  tobac/tests/test_decorators.py,sha256=Hbus1UPA1uKoAXiDsjITBzZFPwMRb15ete9XawY8ylg,4903
19
- tobac/tests/test_feature_detection.py,sha256=6BoOf2nTn8cCtyEP-0zNKe5VQjSXrLKLOVem8EI3B0E,38910
19
+ tobac/tests/test_feature_detection.py,sha256=78_PWq-KGktFdjJqiqK8E62G8L0XjEAoJIGsCUOMbK4,41058
20
20
  tobac/tests/test_generators.py,sha256=zvbv0DRw9EYFEggjBgNtYvxtOTZUlJBbl_lyO20rl5k,7829
21
21
  tobac/tests/test_import.py,sha256=NtyS_dUKIm9eBERHx3H_0x0WUQPB1A5MfnyiJVP1gWc,663
22
22
  tobac/tests/test_iris_xarray_match_utils.py,sha256=4ku2ALQxT-2fUQQauNgwlkzYG7-K4ZAafaYt-cW-ljk,7996
@@ -28,10 +28,10 @@ tobac/tests/test_tracking.py,sha256=AIvPuNfrv4mfeq7ZMSPhODbgbZfmIT7SEPPrjkXX0Mo,
28
28
  tobac/tests/test_utils.py,sha256=Ep0i-A7n1akEz-XaI6uctBNYYvE6GRuwBRgR8z27asg,22145
29
29
  tobac/tests/test_utils_bulk_statistics.py,sha256=w3CtQ3sKSGFD2B-R6Sg50zAerVAUNIIyNmJrYA-_vNs,21398
30
30
  tobac/tests/test_utils_coordinates.py,sha256=dDyhrrYDHgjlzOt80QDvsVUARy4719dnbj-qDXPtMJw,11022
31
- tobac/tests/test_utils_internal.py,sha256=GF6DaEb4C0zCWOTLvB0e-PVhNl0mX_73HXE2BxxI_jc,2985
31
+ tobac/tests/test_utils_internal.py,sha256=ByaSZeuvySw1mPu0jBfwZ4Au6_4Y9HP1_FHm4E_q_x8,5625
32
32
  tobac/tests/test_xarray_utils.py,sha256=pAFQ73gN--ph1WTqjgfCTj-u8lw_7448PL-m7Ikex0A,6852
33
33
  tobac/tests/segmentation_tests/test_iris_xarray_segmentation.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- tobac/tests/segmentation_tests/test_segmentation.py,sha256=qow8b54Rsz4_Gqvu0GIxwdr-AuVxIxsPtckOK-xWFqI,39257
34
+ tobac/tests/segmentation_tests/test_segmentation.py,sha256=jKIR58qEPTMjxCvmkMCG-CvOMf4OYrIwotHubRA6UZg,40443
35
35
  tobac/tests/segmentation_tests/test_segmentation_time_pad.py,sha256=HF2VHf0qihisaIK65Zgx1NazCIYqRWqsgQEkk1oEhLs,3432
36
36
  tobac/utils/__init__.py,sha256=_Oo5j2RSJimy_MehrN0f4uedvq57chGgbtOcp17Hrwo,627
37
37
  tobac/utils/bulk_statistics.py,sha256=1JpL4wUB8kTb0JE3BnnD0GPxfbN_NzeWBymsZULn2NA,15382
@@ -42,12 +42,12 @@ tobac/utils/generators.py,sha256=cqhh9PevrqOWFxZKxLsPOywanuoxULoxDrVOb7QWcD0,323
42
42
  tobac/utils/mask.py,sha256=hcqgLs-Bw5WOl6zjDW0q2qNW8z8UfSGM1fb_8L9ysVc,9696
43
43
  tobac/utils/periodic_boundaries.py,sha256=pVgXHiRzLX7-8mniOLayh9FTsWVue3QWgW8kpizYay4,14948
44
44
  tobac/utils/internal/__init__.py,sha256=R7RSo6H59_VWM5YiwluwwVyhb3hO0xhhr212x8MMC5w,54
45
- tobac/utils/internal/coordinates.py,sha256=11CQpFakt0R2DtCJZxF68e2zuFGAJUhx7UXd2NBJ-JU,13799
45
+ tobac/utils/internal/coordinates.py,sha256=71WsuGtai3rVp3uSzQgnLqSvSCiOU9v84uUTz78Thiw,15489
46
46
  tobac/utils/internal/iris_utils.py,sha256=kvIQmSEnC3e0oZSeOgOZJ9GdGbmSb-jaXtaUUE1AZ8o,17333
47
47
  tobac/utils/internal/label_props.py,sha256=3xfnHrjZH_OZ3BAewVvRf3xIzNS5ubPFbsuIg8I8QIQ,2654
48
48
  tobac/utils/internal/xarray_utils.py,sha256=8EeG5gDQ8kTyOPSLR3AZTxUeQlgjVUrU9F_RGj9qBBM,14708
49
- tobac-1.6.2.dist-info/licenses/LICENSE,sha256=K4976MT1KjThzaCHoyvqiMt8QQCS-ldJ5jJXGzUTpHk,1517
50
- tobac-1.6.2.dist-info/METADATA,sha256=fCoMZqJ3JzIQYnBetaoQa98ZBhuBFTkrMqjVI20A3so,7769
51
- tobac-1.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
- tobac-1.6.2.dist-info/top_level.txt,sha256=9D2iec8rq7k-nI49UeTTBQYgBYULzK9HzTf06r-5SEI,6
53
- tobac-1.6.2.dist-info/RECORD,,
49
+ tobac-1.6.3.dist-info/licenses/LICENSE,sha256=K4976MT1KjThzaCHoyvqiMt8QQCS-ldJ5jJXGzUTpHk,1517
50
+ tobac-1.6.3.dist-info/METADATA,sha256=rOe3pbqJaajIScCfFIjF9_iJ_CKMAtTSa38KknCH-jw,7767
51
+ tobac-1.6.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
52
+ tobac-1.6.3.dist-info/top_level.txt,sha256=9D2iec8rq7k-nI49UeTTBQYgBYULzK9HzTf06r-5SEI,6
53
+ tobac-1.6.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5