dclab 0.63.1__cp310-cp310-macosx_11_0_arm64.whl → 0.64.0__cp310-cp310-macosx_11_0_arm64.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 dclab might be problematic. Click here for more details.

dclab/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.63.1'
21
- __version_tuple__ = version_tuple = (0, 63, 1)
20
+ __version__ = version = '0.64.0'
21
+ __version_tuple__ = version_tuple = (0, 64, 0)
Binary file
dclab/http_utils.py CHANGED
@@ -254,7 +254,7 @@ class ResoluteRequestsSession(requests.Session):
254
254
  break
255
255
  else:
256
256
  raise requests.exceptions.ReadTimeout(
257
- f"Resolut sesion failed for {args} and {kwargs}!")
257
+ f"Resolute session failed for {args} and {kwargs}!")
258
258
  return resp
259
259
 
260
260
 
dclab/kde/base.py CHANGED
@@ -2,7 +2,12 @@ import warnings
2
2
 
3
3
  import numpy as np
4
4
 
5
- from .methods import bin_width_doane, get_bad_vals, methods
5
+ from .methods import bin_width_doane_div5, get_bad_vals, methods
6
+ from .contours import find_contours_level, get_quantile_levels
7
+
8
+
9
+ class ContourSpacingTooLarge(UserWarning):
10
+ pass
6
11
 
7
12
 
8
13
  class KernelDensityEstimator:
@@ -107,6 +112,139 @@ class KernelDensityEstimator:
107
112
  yscale: str
108
113
  See `xscale`.
109
114
 
115
+ Returns
116
+ -------
117
+ X, Y, Z : coordinates
118
+ The kernel density Z evaluated on a rectangular grid (X,Y).
119
+ """
120
+ warnings.warn("`get_contour` is deprecated; please use "
121
+ "`get_raster` instead", DeprecationWarning)
122
+ return self.get_raster(
123
+ xax=xax, yax=yax, xacc=xacc, yacc=yacc,
124
+ kde_type=kde_type, kde_kwargs=kde_kwargs,
125
+ xscale=xscale, yscale=yscale
126
+ )
127
+
128
+ def get_contour_lines(self, quantiles=None, xax="area_um", yax="deform",
129
+ xacc=None, yacc=None, kde_type="histogram",
130
+ kde_kwargs=None, xscale="linear", yscale="linear",
131
+ ret_levels=False):
132
+ """Compute contour lines for a given kernel kensity estimate.
133
+
134
+ Parameters
135
+ ----------
136
+ quantiles: list or array of floats
137
+ KDE Quantiles for which contour levels are computed. The
138
+ values must be between 0 and 1. If set to None, use
139
+ [0.5, 0.95] as default.
140
+ xax: str
141
+ Identifier for X axis (e.g. "area_um", "aspect", "deform")
142
+ yax: str
143
+ Identifier for Y axis
144
+ xacc: float
145
+ Contour accuracy in x direction
146
+ if set to None, will use :func:`bin_width_doane_div5`
147
+ yacc: float
148
+ Contour accuracy in y direction
149
+ if set to None, will use :func:`bin_width_doane_div5`
150
+ kde_type: str
151
+ The KDE method to use
152
+ kde_kwargs: dict
153
+ Additional keyword arguments to the KDE method
154
+ xscale: str
155
+ If set to "log", take the logarithm of the x-values before
156
+ computing the KDE. This is useful when data are
157
+ displayed on a log-scale. Defaults to "linear".
158
+ yscale: str
159
+ See `xscale`
160
+ ret_levels: bool
161
+ If set to True, return the levels of the contours
162
+ (default: False)
163
+
164
+ Returns
165
+ -------
166
+ contour_lines: list of lists (of lists)
167
+ For every number in `quantiles`, this list contains a list of
168
+ corresponding contour lines. Each contour line is a 2D
169
+ array of shape (N, 2), where N is the number of points in the
170
+ contour line.
171
+ levels: list of floats
172
+ The density levels corresponding to each number in `quantiles`.
173
+ Only returned if `ret_levels` is set to True.
174
+ """
175
+ if not quantiles:
176
+ quantiles = [0.5, 0.95]
177
+ try:
178
+ x, y, density = self.get_raster(
179
+ xax=xax,
180
+ yax=yax,
181
+ xacc=xacc,
182
+ yacc=yacc,
183
+ xscale=xscale,
184
+ yscale=yscale,
185
+ kde_type=kde_type,
186
+ kde_kwargs=kde_kwargs,
187
+ )
188
+ except ValueError:
189
+ # most-likely there is nothing to compute a contour for
190
+ return []
191
+ if density.shape[0] < 3 or density.shape[1] < 3:
192
+ warnings.warn("Contour not possible; spacing may be too large!",
193
+ ContourSpacingTooLarge)
194
+ return []
195
+ levels = get_quantile_levels(
196
+ density=density,
197
+ x=x,
198
+ y=y,
199
+ xp=self.rtdc_ds[xax][self.rtdc_ds.filter.all],
200
+ yp=self.rtdc_ds[yax][self.rtdc_ds.filter.all],
201
+ q=np.array(quantiles),
202
+ normalize=False)
203
+ contours = []
204
+ # Normalize levels to [0, 1]
205
+ nlevels = np.array(levels) / density.max()
206
+ for nlev in nlevels:
207
+ # make sure that the contour levels are not at the boundaries
208
+ if not (np.allclose(nlev, 0, atol=1e-12, rtol=0)
209
+ or np.allclose(nlev, 1, atol=1e-12, rtol=0)):
210
+ cc = find_contours_level(
211
+ density, x=x, y=y, level=nlev)
212
+ contours.append(cc)
213
+ else:
214
+ contours.append([])
215
+ if ret_levels:
216
+ return contours, levels
217
+ else:
218
+ return contours
219
+
220
+ def get_raster(self, xax="area_um", yax="deform", xacc=None, yacc=None,
221
+ kde_type="histogram", kde_kwargs=None, xscale="linear",
222
+ yscale="linear"):
223
+ """Evaluate the kernel density estimate on a grid
224
+
225
+ Parameters
226
+ ----------
227
+ xax: str
228
+ Identifier for X axis (e.g. "area_um", "aspect", "deform")
229
+ yax: str
230
+ Identifier for Y axis
231
+ xacc: float
232
+ Contour accuracy in x direction
233
+ if set to None, will use :func:`bin_width_doane_div5`
234
+ yacc: float
235
+ Contour accuracy in y direction
236
+ if set to None, will use :func:`bin_width_doane_div5`
237
+ kde_type: str
238
+ The KDE method to use
239
+ kde_kwargs: dict
240
+ Additional keyword arguments to the KDE method
241
+ xscale: str
242
+ If set to "log", take the logarithm of the x-values before
243
+ computing the KDE. This is useful when data are
244
+ displayed on a log-scale. Defaults to "linear".
245
+ yscale: str
246
+ See `xscale`.
247
+
110
248
  Returns
111
249
  -------
112
250
  X, Y, Z : coordinates
@@ -128,21 +266,21 @@ class KernelDensityEstimator:
128
266
  a=x,
129
267
  feat=xax,
130
268
  scale=xscale,
131
- method=bin_width_doane,
269
+ method=bin_width_doane_div5,
132
270
  ret_scaled=True)
133
271
 
134
272
  yacc_sc, ys = self.get_spacing(
135
273
  a=y,
136
274
  feat=yax,
137
275
  scale=yscale,
138
- method=bin_width_doane,
276
+ method=bin_width_doane_div5,
139
277
  ret_scaled=True)
140
278
 
141
279
  if xacc is None or xacc == 0:
142
- xacc = xacc_sc / 5
280
+ xacc = xacc_sc
143
281
 
144
282
  if yacc is None or yacc == 0:
145
- yacc = yacc_sc / 5
283
+ yacc = yacc_sc
146
284
 
147
285
  # Ignore infs and nans
148
286
  bad = get_bad_vals(xs, ys)
dclab/kde/methods.py CHANGED
@@ -56,6 +56,16 @@ def bin_width_doane(a):
56
56
  return acc
57
57
 
58
58
 
59
+ def bin_width_doane_div5(a):
60
+ """Compute contour spacing based on Doane's formula divided by five
61
+
62
+ See Also
63
+ --------
64
+ bin_width_doane: method used to compute the bin width
65
+ """
66
+ return bin_width_doane(a) / 5
67
+
68
+
59
69
  def bin_width_percentile(a):
60
70
  """Compute contour spacing based on data percentiles
61
71
 
@@ -635,7 +635,7 @@ class RTDCBase(abc.ABC):
635
635
  The kernel density Z evaluated on a rectangular grid (X,Y).
636
636
  """
637
637
  kde_instance = KernelDensityEstimator(rtdc_ds=self)
638
- xmesh, ymesh, density = kde_instance.get_contour(
638
+ xmesh, ymesh, density = kde_instance.get_raster(
639
639
  xax=xax, yax=yax, xacc=xacc, yacc=yacc, kde_type=kde_type,
640
640
  kde_kwargs=kde_kwargs, xscale=xscale, yscale=yscale
641
641
  )
@@ -746,6 +746,8 @@ class RTDCBase(abc.ABC):
746
746
  "ignored_basins": bd_keys,
747
747
  # basin key
748
748
  "key": bdict["key"],
749
+ # whether the basin is perishable or not
750
+ "perishable": bdict.get("perishable", False),
749
751
  }
750
752
 
751
753
  # Check whether this basin is supported and exists
@@ -783,12 +785,19 @@ class RTDCBase(abc.ABC):
783
785
  b_cls = bc[bdict["format"]]
784
786
  # Try absolute path
785
787
  bna = b_cls(pp, **kwargs)
786
- if bna.verify_basin():
787
- basins.append(bna)
788
- break
788
+
789
+ try:
790
+ absolute_exists = bna.verify_basin()
791
+ except BaseException:
792
+ pass
793
+ else:
794
+ if absolute_exists:
795
+ basins.append(bna)
796
+ break
789
797
  # Try relative path
790
798
  this_path = pathlib.Path(self.path)
791
799
  if this_path.exists():
800
+
792
801
  # Insert relative path
793
802
  bnr = b_cls(this_path.parent / pp, **kwargs)
794
803
  if bnr.verify_basin():
@@ -268,8 +268,8 @@ class Export(object):
268
268
  compression_kwargs: dict
269
269
  Dictionary with the keys "compression" and "compression_opts"
270
270
  which are passed to :func:`h5py.H5File.create_dataset`. The
271
- default is Zstandard compression with the lowest compression
272
- level `hdf5plugin.Zstd(clevel=1)`.
271
+ default is Zstandard compression with the compression
272
+ level 5 `hdf5plugin.Zstd(clevel=5)`.
273
273
  compression: str or None
274
274
  Compression method used for data storage;
275
275
  one of [None, "lzf", "gzip", "szip"].
@@ -300,7 +300,7 @@ class Export(object):
300
300
  # be backwards-compatible
301
301
  compression_kwargs = {"compression": compression}
302
302
  if compression_kwargs is None:
303
- compression_kwargs = hdf5plugin.Zstd(clevel=1)
303
+ compression_kwargs = hdf5plugin.Zstd(clevel=5)
304
304
  path = pathlib.Path(path)
305
305
  # Make sure that path ends with .rtdc
306
306
  if path.suffix not in [".rtdc", ".rtdc~"]:
@@ -505,6 +505,11 @@ class Export(object):
505
505
  # defined in. Since we are exporting, it does not
506
506
  # make sense to store these basins in the output file.
507
507
  continue
508
+ elif bn_dict.get("perishable"):
509
+ # Perishable basins require secret keys or complicated
510
+ # logic to execute in order to refresh them. We do not
511
+ # store them in the output file.
512
+ continue
508
513
  basinmap_orig = bn_dict.get("basin_map")
509
514
  if not filtered:
510
515
  # filtering disabled: just copy basins
@@ -6,9 +6,10 @@ which, when opened in dclab, can access features stored in the input file
6
6
  from __future__ import annotations
7
7
 
8
8
  import abc
9
+ import logging
9
10
  import numbers
10
11
  import threading
11
- from typing import Dict, List, Literal
12
+ from typing import Callable, Dict, List, Literal, Union
12
13
  import uuid
13
14
  import warnings
14
15
  import weakref
@@ -18,6 +19,9 @@ import numpy as np
18
19
  from ..util import copy_if_needed
19
20
 
20
21
 
22
+ logger = logging.getLogger(__name__)
23
+
24
+
21
25
  class BasinFeatureMissingWarning(UserWarning):
22
26
  """Used when a badin feature is defined but not stored"""
23
27
 
@@ -26,6 +30,10 @@ class CyclicBasinDependencyFoundWarning(UserWarning):
26
30
  """Used when a basin is defined in one of its sub-basins"""
27
31
 
28
32
 
33
+ class IgnoringPerishableBasinTTL(UserWarning):
34
+ """Used when refreshing a basin does not support TTL"""
35
+
36
+
29
37
  class BasinmapFeatureMissingError(KeyError):
30
38
  """Used when one of the `basinmap` features is not defined"""
31
39
  pass
@@ -47,6 +55,114 @@ class BasinAvailabilityChecker(threading.Thread):
47
55
  self.basin.is_available()
48
56
 
49
57
 
58
+ class PerishableRecord:
59
+ """A class containing information about perishable basins
60
+
61
+ Perishable basins are basins than may discontinue to work after
62
+ e.g. a specific amount of time (e.g. presigned S3 URLs). With the
63
+ `PerishableRecord`, these basins may be "refreshed" (made
64
+ available again).
65
+ """
66
+ def __init__(self,
67
+ basin,
68
+ expiration_func: Callable = None,
69
+ expiration_kwargs: Dict = None,
70
+ refresh_func: Callable = None,
71
+ refresh_kwargs: Dict = None,
72
+ ):
73
+ """
74
+ Parameters
75
+ ----------
76
+ basin: Basin
77
+ Instance of the perishable basin
78
+ expiration_func: callable
79
+ A function that determines whether the basin has perished.
80
+ It must accept `basin` as the first argument. Calling this
81
+ function should be fast, as it is called every time a feature
82
+ is accessed.
83
+ Note that if you are implementing this in the time domain, then
84
+ you should use `time.time()` (TSE), because you need an absolute
85
+ time measure. `time.monotonic()` for instance does not count up
86
+ when the system goes to sleep. However, keep in mind that if
87
+ a remote machine dictates the expiration time, then that
88
+ remote machine should also transmit the creation time (in case
89
+ there are time offsets).
90
+ expiration_kwargs: dict
91
+ Additional kwargs for `expiration_func`.
92
+ refresh_func: callable
93
+ The function used to refresh the `basin`. It must accept
94
+ `basin` as the first argument.
95
+ refresh_kwargs: dict
96
+ Additional kwargs for `refresh_func`
97
+ """
98
+ if not isinstance(basin, weakref.ProxyType):
99
+ basin = weakref.proxy(basin)
100
+ self.basin = basin
101
+ self.expiration_func = expiration_func
102
+ self.expiration_kwargs = expiration_kwargs or {}
103
+ self.refresh_func = refresh_func
104
+ self.refresh_kwargs = refresh_kwargs or {}
105
+
106
+ def __repr__(self):
107
+ state = "perished" if self.perished() else "valid"
108
+ return f"<PerishableRecord ({state}) at {hex(id(self))}>"
109
+
110
+ def perished(self) -> Union[bool, None]:
111
+ """Determine whether the basin has perished
112
+
113
+ Returns
114
+ -------
115
+ state: bool or None
116
+ True means the basin has perished, False means the basin
117
+ has not perished, and `None` means we don't know
118
+ """
119
+ if self.expiration_func is None:
120
+ return None
121
+ else:
122
+ return self.expiration_func(self.basin, **self.expiration_kwargs)
123
+
124
+ def refresh(self, extend_by: float = None) -> None:
125
+ """Extend the lifetime of the associated perishable basin
126
+
127
+ Parameters
128
+ ----------
129
+ extend_by: float
130
+ Custom argument for extending the life of the basin.
131
+ Normally, this would be a lifetime.
132
+
133
+ Returns
134
+ -------
135
+ basin: dict | None
136
+ Dictionary for instantiating a new basin
137
+ """
138
+ if self.refresh_func is None:
139
+ # The basin is a perishable basin, but we have no way of
140
+ # refreshing it.
141
+ logger.error(f"Cannot refresh basin '{self.basin}'")
142
+ return
143
+
144
+ if extend_by and "extend_by" not in self.refresh_kwargs:
145
+ warnings.warn(
146
+ "Parameter 'extend_by' ignored, because the basin "
147
+ "source does not support it",
148
+ IgnoringPerishableBasinTTL)
149
+ extend_by = None
150
+
151
+ rkw = {}
152
+ rkw.update(self.refresh_kwargs)
153
+
154
+ if extend_by is not None:
155
+ rkw["extend_by"] = extend_by
156
+
157
+ self.refresh_func(self.basin, **rkw)
158
+ logger.info(f"Refreshed basin '{self.basin}'")
159
+
160
+ # If everything went well, reset the current dataset of the basin
161
+ if self.basin._ds is not None:
162
+ self.basin._ds.close()
163
+ self.basin._ds = None
164
+
165
+
50
166
  class Basin(abc.ABC):
51
167
  """A basin represents data from an external source
52
168
 
@@ -76,6 +192,7 @@ class Basin(abc.ABC):
76
192
  mapping_referrer: Dict = None,
77
193
  ignored_basins: List[str] = None,
78
194
  key: str = None,
195
+ perishable=False,
79
196
  **kwargs):
80
197
  """
81
198
 
@@ -115,6 +232,10 @@ class Basin(abc.ABC):
115
232
  Unique key to identify this basin; normally computed from
116
233
  a JSON dump of the basin definition. A random string is used
117
234
  if None is specified.
235
+ perishable: bool or PerishableRecord
236
+ If this is not False, then it must be a :class:`.PerishableRecord`
237
+ that holds the information about the expiration time, and that
238
+ comes with a method `refresh` to extend the lifetime of the basin.
118
239
  kwargs:
119
240
  Additional keyword arguments passed to the `load_dataset`
120
241
  method of the `Basin` subclass.
@@ -130,7 +251,12 @@ class Basin(abc.ABC):
130
251
  self.name = name
131
252
  #: lengthy description of the basin
132
253
  self.description = description
133
- # defining key of the basin
254
+ # perishable record
255
+ if isinstance(perishable, bool) and perishable:
256
+ # Create an empty perishable record
257
+ perishable = PerishableRecord(self)
258
+ self.perishable = perishable
259
+ # define key of the basin
134
260
  self.key = key or str(uuid.uuid4())
135
261
  # features this basin provides
136
262
  self._features = features
@@ -164,10 +290,14 @@ class Basin(abc.ABC):
164
290
  self._av_check.start()
165
291
 
166
292
  def __repr__(self):
293
+ try:
294
+ feature_info = len(self.features)
295
+ except BaseException:
296
+ feature_info = "unknown"
167
297
  options = [
168
298
  self.name,
169
299
  f"mapped {self.mapping}" if self.mapping != "same" else "",
170
- f"features {self._features}" if self.features else "full-featured",
300
+ f"{feature_info} features",
171
301
  f"location {self.location}",
172
302
  ]
173
303
  opt_str = ", ".join([o for o in options if o])
@@ -220,6 +350,10 @@ class Basin(abc.ABC):
220
350
  @property
221
351
  def ds(self):
222
352
  """The :class:`.RTDCBase` instance represented by the basin"""
353
+ if self.perishable and self.perishable.perished():
354
+ # We have perished. Ask the PerishableRecord to refresh this
355
+ # basin so we can access it again.
356
+ self.perishable.refresh()
223
357
  if self._ds is None:
224
358
  if not self.is_available():
225
359
  raise BasinNotAvailableError(f"Basin {self} is not available!")
@@ -265,6 +399,7 @@ class Basin(abc.ABC):
265
399
  "basin_descr": self.description,
266
400
  "basin_feats": self.features,
267
401
  "basin_map": self.basinmap,
402
+ "perishable": bool(self.perishable),
268
403
  }
269
404
 
270
405
  def close(self):
@@ -11,8 +11,10 @@ class DCORAccessError(BaseException):
11
11
 
12
12
  class APIHandler:
13
13
  """Handles the DCOR api with caching for simple queries"""
14
- #: these are cached to minimize network usage
15
- cache_queries = ["basins", "metadata", "size", "feature_list", "valid"]
14
+ #: These are cached to minimize network usage
15
+ #: Note that we are not caching basins, since they may contain
16
+ #: expiring URLs.
17
+ cache_queries = ["metadata", "size", "feature_list", "valid"]
16
18
  #: DCOR API Keys/Tokens in the current session
17
19
  api_keys = []
18
20
 
@@ -52,8 +54,36 @@ class APIHandler:
52
54
  if api_key.strip() and api_key not in APIHandler.api_keys:
53
55
  APIHandler.api_keys.append(api_key)
54
56
 
55
- def _get(self, query, feat=None, trace=None, event=None, api_key="",
56
- retries=13):
57
+ def _get(self,
58
+ query: str,
59
+ feat: str = None,
60
+ trace: str = None,
61
+ event: str = None,
62
+ api_key: str = "",
63
+ timeout: float = None,
64
+ retries: int = 5):
65
+ """Fetch information via the DCOR API
66
+
67
+ Parameters
68
+ ----------
69
+ query: str
70
+ API route
71
+ feat: str
72
+ DEPRECATED (use basins instead), adds f"&feature={feat}" to query
73
+ trace: str
74
+ DEPRECATED (use basins instead), adds f"&trace={trace}" to query
75
+ event: str
76
+ DEPRECATED (use basins instead), adds f"&event={event}" to query
77
+ api_key: str
78
+ DCOR API token to use
79
+ timeout: float
80
+ Request timeout
81
+ retries: int
82
+ Number of retries to fetch the request. For every retry, the
83
+ timeout is increased by two seconds.
84
+ """
85
+ if timeout is None:
86
+ timeout = 1
57
87
  # "version=2" introduced in dclab 0.54.3
58
88
  # (supported since ckanext.dc_serve 0.13.2)
59
89
  qstr = f"&version={self.dcserv_api_version}&query={query}"
@@ -65,13 +95,13 @@ class APIHandler:
65
95
  qstr += f"&event={event}"
66
96
  apicall = self.url + qstr
67
97
  fail_reasons = []
68
- for _ in range(retries):
98
+ for ii in range(retries):
69
99
  try:
70
100
  # try-except both requests and json conversion
71
101
  req = self.session.get(apicall,
72
102
  headers={"Authorization": api_key},
73
103
  verify=self.verify,
74
- timeout=1,
104
+ timeout=timeout + ii * 2,
75
105
  )
76
106
  jreq = req.json()
77
107
  except requests.urllib3.exceptions.ConnectionError: # requests
@@ -92,13 +122,45 @@ class APIHandler:
92
122
  f"Messages: {fail_reasons}")
93
123
  return jreq
94
124
 
95
- def get(self, query, feat=None, trace=None, event=None):
125
+ def get(self,
126
+ query: str,
127
+ feat: str = None,
128
+ trace: str = None,
129
+ event: str = None,
130
+ timeout: float = None,
131
+ retries: int = 5,
132
+ ):
133
+ """Fetch information from DCOR
134
+
135
+ Parameters
136
+ ----------
137
+ query: str
138
+ API route
139
+ feat: str
140
+ DEPRECATED (use basins instead), adds f"&feature={feat}" to query
141
+ trace: str
142
+ DEPRECATED (use basins instead), adds f"&trace={trace}" to query
143
+ event: str
144
+ DEPRECATED (use basins instead), adds f"&event={event}" to query
145
+ timeout: float
146
+ Request timeout
147
+ retries: int
148
+ Number of retries to fetch the request. For every retry, the
149
+ timeout is increased by two seconds.
150
+ """
96
151
  if query in APIHandler.cache_queries and query in self._cache:
97
152
  result = self._cache[query]
98
153
  else:
99
154
  req = {"error": {"message": "No access to API (api key?)"}}
100
155
  for api_key in [self.api_key] + APIHandler.api_keys:
101
- req = self._get(query, feat, trace, event, api_key)
156
+ req = self._get(query=query,
157
+ feat=feat,
158
+ trace=trace,
159
+ event=event,
160
+ api_key=api_key,
161
+ timeout=timeout,
162
+ retries=retries,
163
+ )
102
164
  if req["success"]:
103
165
  self.api_key = api_key # remember working key
104
166
  break
@@ -1,17 +1,23 @@
1
1
  """DCOR client interface"""
2
+ import logging
2
3
  import pathlib
3
4
  import re
5
+ import time
4
6
 
5
7
  from ...util import hashobj
6
8
 
7
9
  from ..config import Configuration
8
10
  from ..core import RTDCBase
11
+ from ..feat_basin import PerishableRecord
9
12
 
10
13
  from . import api
11
14
  from .logs import DCORLogs
12
15
  from .tables import DCORTables
13
16
 
14
17
 
18
+ logger = logging.getLogger(__name__)
19
+
20
+
15
21
  #: Append directories here where dclab should look for certificate bundles
16
22
  #: for a specific host. The directory should contain files named after the
17
23
  #: hostname, e.g. "dcor.mpl.mpg.de.cert".
@@ -73,6 +79,8 @@ class RTDC_DCOR(RTDCBase):
73
79
  super(RTDC_DCOR, self).__init__(*args, **kwargs)
74
80
 
75
81
  self._hash = None
82
+ self._cache_basin_dict = None
83
+ self.cache_basin_dict_time = 600
76
84
  self.path = RTDC_DCOR.get_full_url(url, use_ssl, host)
77
85
 
78
86
  if cert_path is None:
@@ -161,15 +169,106 @@ class RTDC_DCOR(RTDCBase):
161
169
  new_url = f"{scheme}://{netloc}/{api_path}"
162
170
  return new_url
163
171
 
164
- def basins_get_dicts(self):
165
- """Return list of dicts for all basins defined in `self.h5file`"""
172
+ def _basin_refresh(self, basin):
173
+ """Refresh the specified basin"""
174
+ # Retrieve the basin dictionary from DCOR
175
+ basin_dicts = self.basins_get_dicts()
176
+ for bn_dict in basin_dicts:
177
+ if bn_dict.get("name") == basin.name:
178
+ break
179
+ else:
180
+ raise ValueError(f"Basin '{basin.name}' not found in {self}")
181
+
182
+ tre = bn_dict["time_request"]
183
+ ttl = bn_dict["time_expiration"]
184
+ # remember time relative to time.time, subtract 30s to be on safe side
185
+ tex = bn_dict["time_local_request"] + (ttl - tre) - 30
186
+
187
+ if isinstance(basin.perishable, bool):
188
+ logger.debug("Initializing basin perishable %s", basin.name)
189
+ # create a perishable record
190
+ basin.perishable = PerishableRecord(
191
+ basin=basin,
192
+ expiration_func=self._basin_expiration,
193
+ expiration_kwargs={"time_local_expiration": tex},
194
+ refresh_func=self._basin_refresh,
195
+ )
196
+ else:
197
+ logger.debug("Refreshing basin perishable %s", basin.name)
198
+ # only update (this also works with weakref.ProxyType)
199
+ basin.perishable.expiration_kwargs = {"time_local_expiration": tex}
200
+
201
+ if len(bn_dict["urls"]) > 1:
202
+ logger.warning(f"Basin {basin.name} has multiple URLs. I am not "
203
+ f"checking their availability: {bn_dict}")
204
+ basin.location = bn_dict["urls"][0]
205
+
206
+ def _basin_expiration(self, basin, time_local_expiration):
207
+ """Check whether the basin has perished"""
208
+ return time_local_expiration < time.time()
209
+
210
+ def _basins_get_dicts(self):
166
211
  try:
167
- basins = self.api.get(query="basins")
212
+ basin_dicts = self.api.get(query="basins")
213
+ # Fill in missing timing information
214
+ for bn_dict in basin_dicts:
215
+ if (bn_dict.get("format") == "http"
216
+ and "perishable" not in bn_dict):
217
+ # We are communicating with an older version of
218
+ # ckanext-dc_serve. Take a look at the URL and check
219
+ # whether we have a perishable (~1 hour) URL or whether
220
+ # this is a public resource.
221
+ expires_regexp = re.compile(".*expires=([0-9]*)$")
222
+ for url in bn_dict.get("urls", []):
223
+ if match := expires_regexp.match(url.lower()):
224
+ logger.debug("Detected perishable basin: %s",
225
+ bn_dict["name"])
226
+ bn_dict["perishable"] = True
227
+ bn_dict["time_request"] = time.time()
228
+ bn_dict["time_expiration"] = int(match.group(1))
229
+ # add part of the resource ID to the name
230
+ infourl = url.split(bn_dict["name"], 1)[-1]
231
+ infourl = infourl.replace("/", "")
232
+ bn_dict["name"] += f"-{infourl[:5]}"
233
+ break
234
+ else:
235
+ bn_dict["perishable"] = False
236
+ # If we have a perishable basin, add the local request time
237
+ if bn_dict.get("perishable"):
238
+ bn_dict["time_local_request"] = time.time()
168
239
  except api.DCORAccessError:
169
240
  # TODO: Do not catch this exception when all DCOR instances
170
241
  # implement the 'basins' query.
171
242
  # This means that the server does not implement the 'basins' query.
172
- basins = []
243
+ basin_dicts = []
244
+ return basin_dicts
245
+
246
+ def basins_get_dicts(self):
247
+ """Return list of dicts for all basins defined on DCOR
248
+
249
+ The return value of this method is cached for 10 minutes
250
+ (cache time defined in the `cache_basin_dict_time` [s] property).
251
+ """
252
+ if (self._cache_basin_dict is None
253
+ or time.time() > (self._cache_basin_dict[1]
254
+ + self.cache_basin_dict_time)):
255
+ self._cache_basin_dict = (self._basins_get_dicts(), time.time())
256
+ return self._cache_basin_dict[0]
257
+
258
+ def basins_retrieve(self):
259
+ """Same as superclass, but add perishable information"""
260
+ basin_dicts = self.basins_get_dicts()
261
+ basins = super(RTDC_DCOR, self).basins_retrieve()
262
+ for bn in basins:
263
+ for bn_dict in basin_dicts:
264
+ if bn.name == bn_dict.get("name"):
265
+ # Determine whether we have to set a perishable record.
266
+ if bn_dict.get("perishable"):
267
+ # required for `_basin_refresh` to create a record
268
+ bn.perishable = True
269
+ # create the actual record
270
+ self._basin_refresh(bn)
271
+ break
173
272
  return basins
174
273
 
175
274
 
@@ -22,5 +22,5 @@ class DCORLogs:
22
22
  @property
23
23
  def _logs(self):
24
24
  if self._logs_cache is None:
25
- self._logs_cache = self.api.get(query="logs")
25
+ self._logs_cache = self.api.get(query="logs", timeout=5)
26
26
  return self._logs_cache
@@ -27,7 +27,7 @@ class DCORTables:
27
27
  @property
28
28
  def _tables(self):
29
29
  if self._tables_cache is None:
30
- table_data = self.api.get(query="tables")
30
+ table_data = self.api.get(query="tables", timeout=13)
31
31
  # assemble the tables
32
32
  tables = {}
33
33
  for key in table_data:
@@ -66,8 +66,8 @@ class RTDCWriter:
66
66
  compression_kwargs: dict-like
67
67
  Dictionary with the keys "compression" and "compression_opts"
68
68
  which are passed to :func:`h5py.H5File.create_dataset`. The
69
- default is Zstandard compression with the lowest compression
70
- level `hdf5plugin.Zstd(clevel=1)`. To disable compression, use
69
+ default is Zstandard compression with the compression
70
+ level 5 `hdf5plugin.Zstd(clevel=5)`. To disable compression, use
71
71
  `{"compression": None}`.
72
72
  compression: str or None
73
73
  Compression method used for data storage;
@@ -88,7 +88,7 @@ class RTDCWriter:
88
88
  # be backwards-compatible
89
89
  compression_kwargs = {"compression": compression}
90
90
  if compression_kwargs is None:
91
- compression_kwargs = hdf5plugin.Zstd(clevel=1)
91
+ compression_kwargs = hdf5plugin.Zstd(clevel=5)
92
92
 
93
93
  self.mode = mode
94
94
  self.compression_kwargs = compression_kwargs
@@ -209,6 +209,7 @@ class RTDCWriter:
209
209
  basin_map: np.ndarray | Tuple[str, np.ndarray] = None,
210
210
  internal_data: Dict | h5py.Group = None,
211
211
  verify: bool = True,
212
+ perishable: bool = False,
212
213
  ):
213
214
  """Write basin information
214
215
 
@@ -249,9 +250,13 @@ class RTDCWriter:
249
250
  This must be specified when storing internal basins, and it
250
251
  must not be specified for any other basin type.
251
252
  verify: bool
252
- whether to verify the basin before storing it; You might have
253
+ Whether to verify the basin before storing it; You might have
253
254
  set this to False if you would like to write a basin that is
254
255
  e.g. temporarily not available
256
+ perishable: bool
257
+ Whether the basin is perishable. If this is True, then a
258
+ warning will be issued, because perishable basins may not be
259
+ accessed (e.g. time-based URL for private S3 data).
255
260
 
256
261
  Returns
257
262
  -------
@@ -261,6 +266,8 @@ class RTDCWriter:
261
266
 
262
267
  .. versionadded:: 0.58.0
263
268
  """
269
+ if perishable:
270
+ warnings.warn(f"Storing perishable basin {basin_name}")
264
271
  if basin_type == "internal":
265
272
  if internal_data is None:
266
273
  raise ValueError(
@@ -381,6 +388,7 @@ class RTDCWriter:
381
388
  "type": basin_type,
382
389
  "features": None if basin_feats is None else sorted(basin_feats),
383
390
  "mapping": basin_map_name,
391
+ "perishable": perishable,
384
392
  }
385
393
  if basin_type == "file":
386
394
  flocs = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dclab
3
- Version: 0.63.1
3
+ Version: 0.64.0
4
4
  Summary: Library for real-time deformability cytometry (RT-DC)
5
5
  Author: Benedikt Hartmann, Eoghan O'Connell, Maik Herbig, Maximilian Schlögel, Nadia Sbaa, Paul Müller, Philipp Rosendahl, Raghava Alajangi
6
6
  Maintainer-email: Paul Müller <dev@craban.de>
@@ -14,7 +14,7 @@ Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Topic :: Scientific/Engineering :: Visualization
16
16
  Classifier: Intended Audience :: Science/Research
17
- Requires-Python: <4,>=3.8
17
+ Requires-Python: <4,>=3.9
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: h5py<4,>=3.0.0
@@ -1,12 +1,12 @@
1
1
  dclab/kde_methods.py,sha256=f0-zDN7ETintvGB3gSzxwgBb53YtT9jZtzI70EAX50g,365
2
- dclab/_version.py,sha256=qDZMrEPFSKYNbMiOoBOEXqq2P0z4X69tZSzimIIP7H0,513
2
+ dclab/_version.py,sha256=zVb-mNfv7Q80VP6gLIonMXNXR1BAme5zhf4Ww25kC90,513
3
3
  dclab/util.py,sha256=HFT5ZQV6AW8GIIruVMldukbVVdlMyKH50GUfOogAcxI,5860
4
4
  dclab/downsampling.pyx,sha256=OK7zbgGLl5gVyoU8ZBHo9EWwb8C9ChavmLNEvQvC9T0,7258
5
5
  dclab/__init__.py,sha256=wyJWhElQRPcq09vUqUnuquTU_KHgHxv6wQxuxQ988Iw,1583
6
6
  dclab/warn.py,sha256=MjJvyQeuvIXFQ2-fHDzbmXJ0scnHqqRJlIxfuLI_utE,523
7
- dclab/downsampling.cpython-310-darwin.so,sha256=bEuZI1oCLS1NIGtfJynFgrYJNc8o0ItpZ5W7YlL1mY4,241840
7
+ dclab/downsampling.cpython-310-darwin.so,sha256=46G33A6W0loraETqfSHk-fGHzSxdeQlFUIm2BdLaHH0,243392
8
8
  dclab/cached.py,sha256=eWTYBiI-HQM7JuPH-oxa5LLnhAX32GpRwlYg2kQ3sTA,2917
9
- dclab/http_utils.py,sha256=YtZHEwB-BBBo2fCvwhlJvlnWvfWFMHclqol3OIJ7atM,10910
9
+ dclab/http_utils.py,sha256=XHicbHQts5LY3zSNmYqNgAZpKTktotEiwQgJ8d2sBlk,10912
10
10
  dclab/polygon_filter.py,sha256=qexmo-rXe06CUPZhN6EMJy4y4B5gXZeqejdvIB2arOE,13480
11
11
  dclab/statistics.py,sha256=tJDqPlY_Jw2Hhl-s7ugMBSZAxcRuPu4LQuBAZBXz7t8,6355
12
12
  dclab/kde_contours.py,sha256=UlU64lrzMQUZH11oZndW7xf7NFCzwP3FcVujwuqXDCI,278
@@ -48,9 +48,9 @@ dclab/cli/task_join.py,sha256=R04ZmXG2-O82E8le0ywZpy9NzOYoErw8hLx8m4q76ms,9464
48
48
  dclab/cli/task_split.py,sha256=dqW3GWkwi4ceDqRVrQx8ADGk4--KSsD3Q1knkD3ZXI4,6060
49
49
  dclab/cli/task_compress.py,sha256=LH3j41aKq5regAH-ZFdOgnXImf5DrReDJ7ITwUObUSQ,4100
50
50
  dclab/kde/contours.py,sha256=WoRqBj_xK-23FZjtaYly7E2Q8sGZ16q2ILq-DmrlmC8,6742
51
- dclab/kde/methods.py,sha256=SYlAjoST66hEZnRmsdZ6izMmgfebxQxTfBR5PHhzDkE,9208
51
+ dclab/kde/methods.py,sha256=8g4lYUKYqt2pdA9efHVRBDCUUzmePmWPp6rljtJ0XD8,9438
52
52
  dclab/kde/__init__.py,sha256=_WSLPMfxE2su6tmO5mJxUE_9ON16-pqQUQCUlzRtyKI,55
53
- dclab/kde/base.py,sha256=i6bNYYeQSFlq03Z5CZjhBE-V3xpY6YLWivL3guscsnE,7683
53
+ dclab/kde/base.py,sha256=KuV_9_AJ4Sn14C4WAH4eF63URAVYCnaFOcLgsVCaAjo,12961
54
54
  dclab/external/__init__.py,sha256=gb0UdzoMymEURPsTXRqVTT1ZJedJ2ubH3jApBIRkwjk,93
55
55
  dclab/external/packaging/version.py,sha256=9MLL6_EYHvGA1yCGndwL5ZmmDA_wqQsW15GyKrI6siQ,14685
56
56
  dclab/external/packaging/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
@@ -70,12 +70,12 @@ dclab/external/skimage/pnpoly.py,sha256=r8hFNiTz5XlUoNZjosqA0iyv1FPn0l7ewbplgFgk
70
70
  dclab/external/skimage/_find_contours.py,sha256=16v5eeTZBmevG8SSuXtJ6yUpVPhwfSmtc8pDD0nuuOU,9340
71
71
  dclab/external/skimage/__init__.py,sha256=-B2QUKHAFzQuBWuuKvPDC5JIl0Zb-x3OGmbwPaE9VwQ,72
72
72
  dclab/external/skimage/_pnpoly.pyx,sha256=Qdn6xPazDschBqbr46DzB75MB2MnqvdnoTSBMK7kUGE,2504
73
- dclab/external/skimage/_pnpoly.cpython-310-darwin.so,sha256=PUhL3bN-JMD-oTRJ_-RWFw27oG4JaUYH10bHQOOaFgk,221808
73
+ dclab/external/skimage/_pnpoly.cpython-310-darwin.so,sha256=hTaCIjJEYLRAaX_Xjq-glzk_9nLVBWaBA7hY7zhscC8,206656
74
74
  dclab/external/skimage/_find_contours_cy.pyx,sha256=pZJOBhMHzYEMkcz4WQVyjn7jDNrdjCfet47FU1hRAxk,7161
75
- dclab/external/skimage/_find_contours_cy.cpython-310-darwin.so,sha256=ov3bakWSSMfG5BPrnnGuMgevCEpNeAnaZcBDIDJzFyg,204104
75
+ dclab/external/skimage/_find_contours_cy.cpython-310-darwin.so,sha256=nORyRpRTav0kfJaYADOTMrlY-1IR0N-3I_HGQ1RFg_Q,188328
76
76
  dclab/external/skimage/_shared/geometry.pxd,sha256=kRsu9ifv_rL3kbRIgSLf86p0hn2oTMp6s013lZ9bBZM,346
77
77
  dclab/external/skimage/_shared/__init__.py,sha256=2sHZwTtJSlMTa3Q2YSvQW7jrPLMUSqDJQa-ROe5zfcw,37
78
- dclab/external/skimage/_shared/geometry.cpython-310-darwin.so,sha256=owTJ2WwrO0g7RKgckweQI9xOWFwDhsZq_mTQP0VCkN0,55872
78
+ dclab/external/skimage/_shared/geometry.cpython-310-darwin.so,sha256=QxFADGAMur2-nGu0OtFZREUIlS63Q0ELIRiT7iB21XY,55200
79
79
  dclab/external/skimage/_shared/geometry.pyx,sha256=miCHUh6mBDbRRIoaF_0xAER1MRzsCAzFdlYQZhV7RmE,1667
80
80
  dclab/definitions/feat_logic.py,sha256=SXsSlAusgtE3uXcPu84dQwYZ07zxmV37DmPednA3_dM,5823
81
81
  dclab/definitions/meta_parse.py,sha256=YdaTdM8DAMsIFn5ITH9OHYGTXeAOBGWtx22oVjxXcWk,2393
@@ -86,14 +86,14 @@ dclab/definitions/meta_logic.py,sha256=wCgb7DPRHaR8DCWw_VbwNkpslUnazzWfgX0iS8oEe
86
86
  dclab/rtdc_dataset/config.py,sha256=MvBteFya3R6Ch3U6UgTakCsJoBgVykTxS_Z25STWPHU,17432
87
87
  dclab/rtdc_dataset/check.py,sha256=lJNaz4QTe2WNlxik6zSohRHTiAYuP_bKOzSDjPGTUS0,35006
88
88
  dclab/rtdc_dataset/meta_table.py,sha256=ucqBNrgI6rDAuQFuMRckY8lp1LpnYAoRgEsLObWTJCE,648
89
- dclab/rtdc_dataset/feat_basin.py,sha256=ViKdvJcwFM8joysnrBYdZbA5t_wZix-6xn_FsvzpYsQ,21072
89
+ dclab/rtdc_dataset/feat_basin.py,sha256=i1J6iKQQEJXdi3u-TB9y_OdxbkFPBK6_G2jeaGcBsLY,26172
90
90
  dclab/rtdc_dataset/fmt_s3.py,sha256=bU3V_WGyqJhxPCH80X_nlNqq-jXcgoZKv_aUBIqwaL8,11877
91
91
  dclab/rtdc_dataset/feat_temp.py,sha256=XbDIS1iUUkRH0Zp9uVlwvK_untJ7hkOnKshK1Drsnt8,3694
92
92
  dclab/rtdc_dataset/__init__.py,sha256=MUHSGVQJ4Zc0IyU2lf01dpDWyOyNveHip-UjSkmPNvQ,486
93
- dclab/rtdc_dataset/core.py,sha256=EjNWk9SV-2xBTRtf34XosLCOS164vGWKP5dKKLSOSq4,34441
94
- dclab/rtdc_dataset/export.py,sha256=NarfazGkPasyRpnhQ2H_riUP-oUq5QL7lG9WwvLCL2o,33121
93
+ dclab/rtdc_dataset/core.py,sha256=p1Wy9Dq1Ny4i-cfvgPfCZov_lQAhGz0--x59j-g4mTs,34753
94
+ dclab/rtdc_dataset/export.py,sha256=Ukmdz-Mm9iei6vhfp1lh7oNQiAbXvA2o3tRx1XlBWCM,33416
95
95
  dclab/rtdc_dataset/fmt_dict.py,sha256=gumVQOiVVDFUKow_483PY7cxInqo-NiBBnBhIU8s4lg,3009
96
- dclab/rtdc_dataset/writer.py,sha256=jc6ADyxGoujXpoXu1vF2nfZjGFMaO5LbRmoYJZ83JVo,41418
96
+ dclab/rtdc_dataset/writer.py,sha256=BqP4QSzHxCSxp9z7_cJF2v04taO3ZXE3Wg6zJY6fMao,41819
97
97
  dclab/rtdc_dataset/filter.py,sha256=AFPUBzOIi3pqXgUdMQ5CIi9ZeGOKC71rfSZKLMLgtog,10023
98
98
  dclab/rtdc_dataset/fmt_http.py,sha256=vXVxRLXZp2_V1v3xk4lu4VUHYXfNHJdsRkVt3trC1RU,3374
99
99
  dclab/rtdc_dataset/load.py,sha256=5_xGw2P8Mjs0gW-vGr2Kn28j6Qv3BvvMvguMibC_mM4,2761
@@ -123,10 +123,10 @@ dclab/rtdc_dataset/feat_anc_ml/__init__.py,sha256=99jZlz17aBCAxI5xx42XINomMy20Hi
123
123
  dclab/rtdc_dataset/fmt_dcor/access_token.py,sha256=jotLQay138RUlv8wbdF2ishRnyE9N0KwGGBlbCL0wRI,2028
124
124
  dclab/rtdc_dataset/fmt_dcor/basin.py,sha256=tQZ4GumqURjS3eppRrSyUq1zBPD0y_8rwznMRDXiDUs,2526
125
125
  dclab/rtdc_dataset/fmt_dcor/__init__.py,sha256=WjO1uM_Vlof15Y7HkhkV5Xv75q9TDIdOBIuS_I38qps,210
126
- dclab/rtdc_dataset/fmt_dcor/api.py,sha256=GfQdKhkGXN-e9-e5YxgzL81tDeWB2M-UoyouVfXXbVg,4255
127
- dclab/rtdc_dataset/fmt_dcor/logs.py,sha256=1JsMr_4r5j8rkfrrUsiN42_l92GcvDjapYxopZKimnw,583
128
- dclab/rtdc_dataset/fmt_dcor/tables.py,sha256=NaVEwLKmOg7Mz5iAMe2S8C4xRVC_YO3zeT7g5EbQE1M,1682
129
- dclab/rtdc_dataset/fmt_dcor/base.py,sha256=wD127W5LvvhkUy8SvFVVwAR6EEYtzgoWJ4booh45rfA,6588
126
+ dclab/rtdc_dataset/fmt_dcor/api.py,sha256=IhmNpEdVSGHdJUepCkmuyTVOp3fNn1WASPVohAzwhu8,6274
127
+ dclab/rtdc_dataset/fmt_dcor/logs.py,sha256=FHaDGjh4wMd18S2FFr6IVdd23l21S6s3fwZnFbPG83E,594
128
+ dclab/rtdc_dataset/fmt_dcor/tables.py,sha256=a4gMIjH0TgFdz36l_F-xriHTT4LMI64ur-atHyvAp80,1694
129
+ dclab/rtdc_dataset/fmt_dcor/base.py,sha256=cFiZcWG43jOGTY3oNl-E_vdzbm5QZU1CEB7ucrLZP_U,11167
130
130
  dclab/rtdc_dataset/fmt_tdms/event_mask.py,sha256=eZiDHAGG3MCVckEMHsV-YBbL-pETVLomGk1gmpjc8-k,2175
131
131
  dclab/rtdc_dataset/fmt_tdms/naming.py,sha256=biI9l1EO6BuSYgwZG0deacj4i1fMHQcW78AKXEcm5Wc,5373
132
132
  dclab/rtdc_dataset/fmt_tdms/__init__.py,sha256=3Hc6ASVHEmZBW0pCdFiLZcBtNPcEjG6c7eddqeAZRK4,18923
@@ -134,9 +134,9 @@ dclab/rtdc_dataset/fmt_tdms/event_image.py,sha256=-jp7Z-N91e4ieumYQ1huMicj7PMJqw
134
134
  dclab/rtdc_dataset/fmt_tdms/event_trace.py,sha256=Vkym0QKSw2mq1XZl5n8wDkgHXmaZwQGiMAV5AuRSJkE,5215
135
135
  dclab/rtdc_dataset/fmt_tdms/exc.py,sha256=WzrMqnyrzp8gsT8Pf7JKqGGv43ewx7d_qgtirURppRI,813
136
136
  dclab/rtdc_dataset/fmt_tdms/event_contour.py,sha256=kjo0wJx9F0gmmOOyR0NoLw6VEtSl3h63WXXkcbfnoS8,9627
137
- dclab-0.63.1.dist-info/RECORD,,
138
- dclab-0.63.1.dist-info/WHEEL,sha256=IXYzzLeGhzM4nRz5U4mzre-uliJyF_FEfdbcpUEVBrU,136
139
- dclab-0.63.1.dist-info/entry_points.txt,sha256=eOpjgznu-eW-9utUpLU-77O5098YyUEgGF3ksGMdtec,273
140
- dclab-0.63.1.dist-info/top_level.txt,sha256=irvwZMgs1edY1Zj60ZFk7Almb9Zhk4k6E6aC4YPFnnM,6
141
- dclab-0.63.1.dist-info/METADATA,sha256=kaDiBVjVDUqc77B4nSoNNQhk-IE6Rfb7WI9yAV1UABM,4755
142
- dclab-0.63.1.dist-info/licenses/LICENSE,sha256=gLDaVZWRrlnLdyfOrR0qfWjLbOVcjvoJ-kCLUK0fyXA,15360
137
+ dclab-0.64.0.dist-info/RECORD,,
138
+ dclab-0.64.0.dist-info/WHEEL,sha256=6h6JDR7KEjDmDmYh9AYbh3iumxbuiwKCwb1O-T24foc,136
139
+ dclab-0.64.0.dist-info/entry_points.txt,sha256=eOpjgznu-eW-9utUpLU-77O5098YyUEgGF3ksGMdtec,273
140
+ dclab-0.64.0.dist-info/top_level.txt,sha256=irvwZMgs1edY1Zj60ZFk7Almb9Zhk4k6E6aC4YPFnnM,6
141
+ dclab-0.64.0.dist-info/METADATA,sha256=zfrjZaLGPiE_ZPYkVtoUGvGI-CPkjgY-eiKJshd-3bo,4755
142
+ dclab-0.64.0.dist-info/licenses/LICENSE,sha256=gLDaVZWRrlnLdyfOrR0qfWjLbOVcjvoJ-kCLUK0fyXA,15360
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp310-cp310-macosx_11_0_arm64
5
5
  Generator: delocate 0.13.0