cloudnetpy-qc 1.24.3__py3-none-any.whl → 1.25.1__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.
- cloudnetpy_qc/coverage.py +66 -0
- cloudnetpy_qc/quality.py +66 -58
- cloudnetpy_qc/utils.py +48 -0
- cloudnetpy_qc/version.py +2 -2
- {cloudnetpy_qc-1.24.3.dist-info → cloudnetpy_qc-1.25.1.dist-info}/METADATA +2 -1
- {cloudnetpy_qc-1.24.3.dist-info → cloudnetpy_qc-1.25.1.dist-info}/RECORD +9 -8
- {cloudnetpy_qc-1.24.3.dist-info → cloudnetpy_qc-1.25.1.dist-info}/WHEEL +0 -0
- {cloudnetpy_qc-1.24.3.dist-info → cloudnetpy_qc-1.25.1.dist-info}/licenses/LICENSE +0 -0
- {cloudnetpy_qc-1.24.3.dist-info → cloudnetpy_qc-1.25.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import netCDF4
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from cloudnetpy_qc.variables import Product
|
|
7
|
+
|
|
8
|
+
RESOLUTIONS = {
|
|
9
|
+
Product.DISDROMETER: datetime.timedelta(minutes=1),
|
|
10
|
+
Product.L3_CF: datetime.timedelta(hours=1),
|
|
11
|
+
Product.L3_IWC: datetime.timedelta(hours=1),
|
|
12
|
+
Product.L3_LWC: datetime.timedelta(hours=1),
|
|
13
|
+
Product.MWR: datetime.timedelta(minutes=5),
|
|
14
|
+
Product.MWR_MULTI: datetime.timedelta(minutes=30),
|
|
15
|
+
Product.MWR_SINGLE: datetime.timedelta(minutes=5),
|
|
16
|
+
Product.WEATHER_STATION: datetime.timedelta(minutes=10),
|
|
17
|
+
Product.RAIN_GAUGE: datetime.timedelta(minutes=1),
|
|
18
|
+
Product.DOPPLER_LIDAR_WIND: datetime.timedelta(hours=1.5),
|
|
19
|
+
}
|
|
20
|
+
DEFAULT_RESOLUTION = datetime.timedelta(seconds=30)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def data_coverage(
|
|
24
|
+
nc: netCDF4.Dataset,
|
|
25
|
+
) -> tuple[float, datetime.timedelta, datetime.timedelta] | None:
|
|
26
|
+
time = np.array(nc["time"][:])
|
|
27
|
+
time_unit = datetime.timedelta(hours=1)
|
|
28
|
+
try:
|
|
29
|
+
n_time = len(time)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return None
|
|
32
|
+
if n_time < 2:
|
|
33
|
+
return None
|
|
34
|
+
if nc.cloudnet_file_type == "model":
|
|
35
|
+
expected_res = _model_resolution(nc)
|
|
36
|
+
else:
|
|
37
|
+
product = Product(nc.cloudnet_file_type)
|
|
38
|
+
expected_res = RESOLUTIONS.get(product, DEFAULT_RESOLUTION)
|
|
39
|
+
duration = get_duration(nc)
|
|
40
|
+
bins = max(1, duration // expected_res)
|
|
41
|
+
hist, _ = np.histogram(time, bins=bins, range=(0, duration / time_unit))
|
|
42
|
+
coverage = np.count_nonzero(hist > 0) / len(hist)
|
|
43
|
+
actual_res = np.median(np.diff(time)) * time_unit
|
|
44
|
+
return coverage, expected_res, actual_res
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _model_resolution(nc: netCDF4.Dataset) -> datetime.timedelta:
|
|
48
|
+
source = nc.source.lower()
|
|
49
|
+
if "gdas" in source or "ecmwf open" in source:
|
|
50
|
+
return datetime.timedelta(hours=3)
|
|
51
|
+
return datetime.timedelta(hours=1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_duration(nc: netCDF4.Dataset) -> datetime.timedelta:
|
|
55
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
56
|
+
if now.date() == _get_date(nc):
|
|
57
|
+
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
58
|
+
duration = now - midnight
|
|
59
|
+
else:
|
|
60
|
+
duration = datetime.timedelta(days=1)
|
|
61
|
+
return duration
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_date(nc: netCDF4.Dataset) -> datetime.date:
|
|
65
|
+
date_in_file = [int(getattr(nc, x)) for x in ("year", "month", "day")]
|
|
66
|
+
return datetime.date(*date_in_file)
|
cloudnetpy_qc/quality.py
CHANGED
|
@@ -14,9 +14,12 @@ from typing import NamedTuple, TypedDict
|
|
|
14
14
|
import netCDF4
|
|
15
15
|
import numpy as np
|
|
16
16
|
import scipy.stats
|
|
17
|
+
from cftime import num2pydate
|
|
17
18
|
from numpy import ma
|
|
18
19
|
from requests import RequestException
|
|
19
20
|
|
|
21
|
+
from cloudnetpy_qc.coverage import data_coverage, get_duration
|
|
22
|
+
|
|
20
23
|
from . import utils
|
|
21
24
|
from .variables import LEVELS, VARIABLES, Product
|
|
22
25
|
from .version import __version__
|
|
@@ -53,6 +56,7 @@ class FileReport(NamedTuple):
|
|
|
53
56
|
timestamp: datetime.datetime
|
|
54
57
|
qc_version: str
|
|
55
58
|
tests: list[TestReport]
|
|
59
|
+
data_coverage: float | None
|
|
56
60
|
|
|
57
61
|
def to_dict(self) -> dict:
|
|
58
62
|
return {
|
|
@@ -72,8 +76,9 @@ class FileReport(NamedTuple):
|
|
|
72
76
|
|
|
73
77
|
|
|
74
78
|
class SiteMeta(TypedDict):
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
time: np.ndarray | None
|
|
80
|
+
latitude: float | np.ndarray | None
|
|
81
|
+
longitude: float | np.ndarray | None
|
|
77
82
|
altitude: float | None
|
|
78
83
|
|
|
79
84
|
|
|
@@ -84,6 +89,7 @@ def run_tests(
|
|
|
84
89
|
ignore_tests: list[str] | None = None,
|
|
85
90
|
) -> FileReport:
|
|
86
91
|
filename = Path(filename)
|
|
92
|
+
coverage = None
|
|
87
93
|
if isinstance(product, str):
|
|
88
94
|
product = Product(product)
|
|
89
95
|
with netCDF4.Dataset(filename) as nc:
|
|
@@ -110,11 +116,15 @@ def run_tests(
|
|
|
110
116
|
test_instance._add_error(
|
|
111
117
|
f"Failed to run test: {err} ({type(err).__name__})"
|
|
112
118
|
)
|
|
119
|
+
logging.exception("Failed to run test:")
|
|
113
120
|
test_reports.append(test_instance.report)
|
|
121
|
+
if test_instance.coverage is not None:
|
|
122
|
+
coverage = test_instance.coverage
|
|
114
123
|
return FileReport(
|
|
115
124
|
timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
|
|
116
125
|
qc_version=__version__,
|
|
117
126
|
tests=test_reports,
|
|
127
|
+
data_coverage=coverage,
|
|
118
128
|
)
|
|
119
129
|
|
|
120
130
|
|
|
@@ -124,6 +134,7 @@ class Test:
|
|
|
124
134
|
name: str
|
|
125
135
|
description: str
|
|
126
136
|
products: Iterable[Product] = Product.all()
|
|
137
|
+
coverage: float | None = None
|
|
127
138
|
|
|
128
139
|
def __init__(
|
|
129
140
|
self, nc: netCDF4.Dataset, filename: Path, product: Product, site_meta: SiteMeta
|
|
@@ -190,19 +201,6 @@ class Test:
|
|
|
190
201
|
)
|
|
191
202
|
self._add_warning(msg)
|
|
192
203
|
|
|
193
|
-
def _get_date(self):
|
|
194
|
-
date_in_file = [int(getattr(self.nc, x)) for x in ("year", "month", "day")]
|
|
195
|
-
return datetime.date(*date_in_file)
|
|
196
|
-
|
|
197
|
-
def _get_duration(self) -> datetime.timedelta:
|
|
198
|
-
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
199
|
-
if now.date() == self._get_date():
|
|
200
|
-
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
201
|
-
duration = now - midnight
|
|
202
|
-
else:
|
|
203
|
-
duration = datetime.timedelta(days=1)
|
|
204
|
-
return duration
|
|
205
|
-
|
|
206
204
|
|
|
207
205
|
# --------------------#
|
|
208
206
|
# ------ Infos ------ #
|
|
@@ -287,45 +285,12 @@ class TestDataCoverage(Test):
|
|
|
287
285
|
name = "Data coverage"
|
|
288
286
|
description = "Test that file contains enough data."
|
|
289
287
|
|
|
290
|
-
RESOLUTIONS = {
|
|
291
|
-
Product.DISDROMETER: datetime.timedelta(minutes=1),
|
|
292
|
-
Product.L3_CF: datetime.timedelta(hours=1),
|
|
293
|
-
Product.L3_IWC: datetime.timedelta(hours=1),
|
|
294
|
-
Product.L3_LWC: datetime.timedelta(hours=1),
|
|
295
|
-
Product.MWR: datetime.timedelta(minutes=5),
|
|
296
|
-
Product.MWR_MULTI: datetime.timedelta(minutes=30),
|
|
297
|
-
Product.MWR_SINGLE: datetime.timedelta(minutes=5),
|
|
298
|
-
Product.WEATHER_STATION: datetime.timedelta(minutes=10),
|
|
299
|
-
Product.RAIN_GAUGE: datetime.timedelta(minutes=1),
|
|
300
|
-
Product.DOPPLER_LIDAR_WIND: datetime.timedelta(hours=1.5),
|
|
301
|
-
}
|
|
302
|
-
DEFAULT_RESOLUTION = datetime.timedelta(seconds=30)
|
|
303
|
-
|
|
304
|
-
def _model_resolution(self):
|
|
305
|
-
source = self.nc.source.lower()
|
|
306
|
-
if "gdas" in source or "ecmwf open" in source:
|
|
307
|
-
return datetime.timedelta(hours=3)
|
|
308
|
-
return datetime.timedelta(hours=1)
|
|
309
|
-
|
|
310
288
|
def run(self):
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
try:
|
|
314
|
-
n_time = len(time)
|
|
315
|
-
except (TypeError, ValueError):
|
|
316
|
-
return
|
|
317
|
-
if n_time < 2:
|
|
289
|
+
coverage, expected_res, actual_res = data_coverage(self.nc)
|
|
290
|
+
if coverage is None:
|
|
318
291
|
return
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
else:
|
|
322
|
-
expected_res = self.RESOLUTIONS.get(self.product, self.DEFAULT_RESOLUTION)
|
|
323
|
-
duration = self._get_duration()
|
|
324
|
-
bins = max(1, duration // expected_res)
|
|
325
|
-
hist, _bin_edges = np.histogram(
|
|
326
|
-
time, bins=bins, range=(0, duration / time_unit)
|
|
327
|
-
)
|
|
328
|
-
missing = np.count_nonzero(hist == 0) / len(hist) * 100
|
|
292
|
+
self.coverage = coverage
|
|
293
|
+
missing = (1 - coverage) * 100
|
|
329
294
|
if missing > 20:
|
|
330
295
|
message = f"{round(missing)}% of day's data is missing."
|
|
331
296
|
if missing > 60:
|
|
@@ -333,7 +298,6 @@ class TestDataCoverage(Test):
|
|
|
333
298
|
else:
|
|
334
299
|
self._add_info(message)
|
|
335
300
|
|
|
336
|
-
actual_res = np.median(np.diff(time)) * time_unit
|
|
337
301
|
if actual_res > expected_res * 1.05:
|
|
338
302
|
self._add_warning(
|
|
339
303
|
f"Expected a measurement with interval at least {expected_res},"
|
|
@@ -789,7 +753,7 @@ class TestModelData(Test):
|
|
|
789
753
|
if n_time < 2:
|
|
790
754
|
return
|
|
791
755
|
|
|
792
|
-
duration = self.
|
|
756
|
+
duration = get_duration(self.nc)
|
|
793
757
|
should_be_data_until = duration / time_unit
|
|
794
758
|
|
|
795
759
|
for key in ("temperature", "pressure", "q"):
|
|
@@ -838,14 +802,27 @@ class TestCoordinates(Test):
|
|
|
838
802
|
self._add_error(f"Variable '{key}' is missing")
|
|
839
803
|
|
|
840
804
|
if "latitude" in self.nc.variables and "longitude" in self.nc.variables:
|
|
841
|
-
site_lat = self.site_meta["latitude"]
|
|
842
|
-
site_lon = self.site_meta["longitude"]
|
|
805
|
+
site_lat = np.atleast_1d(self.site_meta["latitude"])
|
|
806
|
+
site_lon = np.atleast_1d(self.site_meta["longitude"])
|
|
843
807
|
file_lat = np.atleast_1d(self.nc["latitude"][:])
|
|
844
808
|
file_lon = np.atleast_1d(self.nc["longitude"][:])
|
|
845
809
|
file_lon[file_lon > 180] -= 360
|
|
846
|
-
|
|
810
|
+
|
|
811
|
+
if self.site_meta.get("time") and file_lat.size > 1 and file_lon.size > 1:
|
|
812
|
+
site_time = self._read_site_time()
|
|
813
|
+
file_time = self._read_file_time()
|
|
814
|
+
idx = utils.find_closest(file_time, site_time)
|
|
815
|
+
file_lat = file_lat[idx]
|
|
816
|
+
file_lon = file_lon[idx]
|
|
817
|
+
else:
|
|
818
|
+
file_lat, file_lon = utils.average_coordinate(file_lat, file_lon)
|
|
819
|
+
site_lat, site_lon = utils.average_coordinate(site_lat, site_lon)
|
|
820
|
+
|
|
821
|
+
dist = np.atleast_1d(
|
|
822
|
+
utils.haversine(site_lat, site_lon, file_lat, file_lon)
|
|
823
|
+
)
|
|
847
824
|
i = np.argmax(dist)
|
|
848
|
-
max_dist =
|
|
825
|
+
max_dist = self._calc_max_dist(site_lat, site_lon)
|
|
849
826
|
if dist[i] > max_dist:
|
|
850
827
|
self._add_error(
|
|
851
828
|
f"Variables 'latitude' and 'longitude' do not match "
|
|
@@ -867,6 +844,37 @@ class TestCoordinates(Test):
|
|
|
867
844
|
f"but received {round(file_alt[i])}\u00a0m"
|
|
868
845
|
)
|
|
869
846
|
|
|
847
|
+
def _read_site_time(self):
|
|
848
|
+
for dt in self.site_meta["time"]:
|
|
849
|
+
if (
|
|
850
|
+
not isinstance(dt, datetime.datetime)
|
|
851
|
+
or dt.tzinfo is None
|
|
852
|
+
or dt.tzinfo.utcoffset(dt) is None
|
|
853
|
+
):
|
|
854
|
+
raise ValueError("Naive datetimes are not supported")
|
|
855
|
+
naive_dt = [
|
|
856
|
+
dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
|
857
|
+
for dt in self.site_meta["time"]
|
|
858
|
+
]
|
|
859
|
+
return np.array(naive_dt, dtype="datetime64[s]")
|
|
860
|
+
|
|
861
|
+
def _read_file_time(self):
|
|
862
|
+
naive_dt = num2pydate(
|
|
863
|
+
self.nc["time"][:], self.nc["time"].units, self.nc["time"].calendar
|
|
864
|
+
)
|
|
865
|
+
return np.array(naive_dt, dtype="datetime64[s]")
|
|
866
|
+
|
|
867
|
+
def _calc_max_dist(self, latitude, longitude):
|
|
868
|
+
if self.nc.cloudnet_file_type == "model":
|
|
869
|
+
angle = 1 # Model resolution should be at least 1 degrees.
|
|
870
|
+
half_angle = angle / 2
|
|
871
|
+
min_lat = np.maximum(-90, latitude - half_angle)
|
|
872
|
+
max_lat = np.minimum(90, latitude + half_angle)
|
|
873
|
+
min_lon = np.maximum(-180, longitude - half_angle)
|
|
874
|
+
max_lon = np.minimum(180, longitude + half_angle)
|
|
875
|
+
return utils.haversine(min_lat, min_lon, max_lat, max_lon)
|
|
876
|
+
return 10
|
|
877
|
+
|
|
870
878
|
|
|
871
879
|
# ------------------------------#
|
|
872
880
|
# ------ Error / Warning ------ #
|
cloudnetpy_qc/utils.py
CHANGED
|
@@ -141,3 +141,51 @@ def haversine(
|
|
|
141
141
|
|
|
142
142
|
a = np.sin(dlat / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2
|
|
143
143
|
return 2 * r * np.arcsin(np.sqrt(a))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def find_closest(x: npt.NDArray, x_new: npt.NDArray) -> npt.NDArray[np.intp]:
|
|
147
|
+
"""Find the closest values between two arrays.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
x: Sorted array.
|
|
151
|
+
x_new: Sorted array.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Indices into `x` which correspond to the closest values in `x_new`.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> x = np.array([0.9, 1.2, 2.0, 2.1])
|
|
158
|
+
>>> x_new = np.array([1, 2])
|
|
159
|
+
>>> find_closest(x, x_new)
|
|
160
|
+
array([0, 2])
|
|
161
|
+
"""
|
|
162
|
+
idx = np.searchsorted(x, x_new)
|
|
163
|
+
idx_left = np.clip(idx - 1, 0, len(x) - 1)
|
|
164
|
+
idx_right = np.clip(idx, 0, len(x) - 1)
|
|
165
|
+
diff_left = np.abs(x_new - x[idx_left])
|
|
166
|
+
diff_right = np.abs(x_new - x[idx_right])
|
|
167
|
+
return np.where(diff_left < diff_right, idx_left, idx_right)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def average_coordinate(
|
|
171
|
+
latitude: npt.NDArray, longitude: npt.NDArray
|
|
172
|
+
) -> tuple[float, float]:
|
|
173
|
+
"""Calculate average position from given coordinates.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
latitude: Array of latitudes.
|
|
177
|
+
longitude: Array of longitudes.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of average latitude and longitude.
|
|
181
|
+
"""
|
|
182
|
+
if latitude.size == longitude.size == 1:
|
|
183
|
+
return latitude[0], longitude[0]
|
|
184
|
+
latitude = np.radians(latitude)
|
|
185
|
+
longitude = np.radians(longitude)
|
|
186
|
+
x = np.mean(np.cos(latitude) * np.cos(longitude))
|
|
187
|
+
y = np.mean(np.cos(latitude) * np.sin(longitude))
|
|
188
|
+
z = np.mean(np.sin(latitude))
|
|
189
|
+
avg_lat = np.degrees(np.atan2(z, np.sqrt(x * x + y * y)))
|
|
190
|
+
avg_lon = np.degrees(np.atan2(y, x))
|
|
191
|
+
return avg_lat, avg_lon
|
cloudnetpy_qc/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudnetpy_qc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.25.1
|
|
4
4
|
Summary: Quality control routines for CloudnetPy products
|
|
5
5
|
Author-email: Finnish Meteorological Institute <actris-cloudnet@fmi.fi>
|
|
6
6
|
License: MIT License
|
|
@@ -80,6 +80,7 @@ print(json_object)
|
|
|
80
80
|
- `timestamp`: UTC timestamp of the test
|
|
81
81
|
- `qcVersion`: `cloudnetpy-qc` version
|
|
82
82
|
- `tests`: `Test[]`
|
|
83
|
+
- `data_coverage`: float
|
|
83
84
|
|
|
84
85
|
### `Test`
|
|
85
86
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
cloudnetpy_qc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cloudnetpy_qc/coverage.py,sha256=WeLiiGRYIjIvXo2QR7Z03WD_RPaV2D8d1R_oQiX68h8,2291
|
|
2
3
|
cloudnetpy_qc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
cloudnetpy_qc/quality.py,sha256=
|
|
4
|
-
cloudnetpy_qc/utils.py,sha256=
|
|
4
|
+
cloudnetpy_qc/quality.py,sha256=ucw1J8A2Zv_O9tpmTk7U0L7Jz2RM8CwdIO3uoZ9Zims,37802
|
|
5
|
+
cloudnetpy_qc/utils.py,sha256=zFVryl56le2cyfDSCyGCzMHACcsjK7AJZknii1fgfgg,5393
|
|
5
6
|
cloudnetpy_qc/variables.py,sha256=YrOw2qCdY8eTT3nlExpdtFU5YmVCC-yF9c0VQ-wWDzw,50851
|
|
6
|
-
cloudnetpy_qc/version.py,sha256=
|
|
7
|
+
cloudnetpy_qc/version.py,sha256=Bnwds0OVjmA2IPtPdBu-BuuX2U-aLyOtSRhGEDwdQgo,102
|
|
7
8
|
cloudnetpy_qc/data/area-type-table.xml,sha256=LQGp6rk8d-jZVjeFWPK_NjG2Kk1atvLlQXmV4UXggKI,17788
|
|
8
9
|
cloudnetpy_qc/data/cf-standard-name-table.xml,sha256=NvW9oPvPwMR1YRIwxajTwvF83oarcwsxkUqnQgqPO3k,4406495
|
|
9
10
|
cloudnetpy_qc/data/data_quality_config.ini,sha256=tQpbRQUlA3iz_8wCRMJfdKrP8ByWiM2SxtqszSoxQeA,1242
|
|
10
11
|
cloudnetpy_qc/data/standardized-region-list.xml,sha256=gLRE2G7RQLD9hmvW5dTzyK7XPhORxWv2bfbrvAp5Uto,6426
|
|
11
|
-
cloudnetpy_qc-1.
|
|
12
|
-
cloudnetpy_qc-1.
|
|
13
|
-
cloudnetpy_qc-1.
|
|
14
|
-
cloudnetpy_qc-1.
|
|
15
|
-
cloudnetpy_qc-1.
|
|
12
|
+
cloudnetpy_qc-1.25.1.dist-info/licenses/LICENSE,sha256=P0wszB7Cq2M390SKaqT4DGvECfwGOMdTTdxsWBpEUUc,1094
|
|
13
|
+
cloudnetpy_qc-1.25.1.dist-info/METADATA,sha256=C0iV4oPzBW18ik7MwNE6Mr1uwTkS9clAHzrZ5NL0X6I,8193
|
|
14
|
+
cloudnetpy_qc-1.25.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
15
|
+
cloudnetpy_qc-1.25.1.dist-info/top_level.txt,sha256=shrf8A1KyrrnhbHocc4gHmTl38YY-DHflgf-gXiKnKs,14
|
|
16
|
+
cloudnetpy_qc-1.25.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|