bs-python-utils 0.8.3__py3-none-any.whl → 0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,11 +10,10 @@ Note:
10
10
 
11
11
  The sequence of steps is as follows:
12
12
 
13
- * choose a number of Chebyshev nodes for numerical integration and optimize
14
- the weights: `v = solve_for_v(y, n_nodes)`
15
- * to obtain the $(u_1,u_2)$ quantiles for $(u_1, u_2)\\in [0,1]$, run
13
+ 1. optimize the weights: `v = solve_for_v(y, n_nodes)` given `n_nodes` Chebyshev nodes for numerical integration
14
+ 2. to obtain the $(u_1,u_2)$ quantiles for $(u_1, u_2)\\in [0,1]$, run
16
15
  `qtiles_y = bivariate_quantiles_v(y, v, u1, u2)`
17
- * to compute the vector ranks for all points in the sample (the barycenters
16
+ 3. to compute the vector ranks for all points in the sample (the barycenters
18
17
  of the cells in the power diagram):
19
18
  `ranks_y = bivariate_ranks_v(y, v, n_nodes)`
20
19
 
@@ -33,200 +32,131 @@ from bs_python_utils.bsutils import bs_error_abort
33
32
  from bs_python_utils.chebyshev import Interval, cheb_get_nodes_1d
34
33
 
35
34
 
36
- def _compute_ab(y_sorted: np.ndarray, v_sorted: np.ndarray) -> TwoArrays:
37
- """Build the `A` and `B` matrices used in the dual optimisation."""
38
- y1 = y_sorted[:, 0]
35
+ def _compute_ad(y: np.ndarray) -> TwoArrays:
36
+ """Build the `A` and `dy2` matrices used in the dual optimisation."""
37
+ y1 = y[:, 0]
39
38
  dy1 = np.subtract.outer(y1, y1)
40
- y2 = y_sorted[:, 1]
39
+ y2 = y[:, 1]
41
40
  dy2 = np.subtract.outer(y2, y2)
42
41
  np.fill_diagonal(dy2, 1.0)
43
- dv = np.subtract.outer(v_sorted, v_sorted)
44
- with np.errstate(divide="ignore", invalid="ignore"):
45
- a_mat = np.divide(dy1.T, dy2, where=np.abs(dy2) > 1e-12)
46
- b_mat = np.divide(dv.T, dy2, where=np.abs(dy2) > 1e-12)
47
- a_mat = np.nan_to_num(a_mat, nan=0.0, posinf=0.0, neginf=0.0)
48
- b_mat = np.nan_to_num(b_mat, nan=0.0, posinf=0.0, neginf=0.0)
49
- return a_mat, b_mat
42
+ dy2 = dy2.T
43
+ a_mat = np.divide(dy1, dy2)
44
+ return a_mat, dy2
50
45
 
51
46
 
52
- def _compute_u2_bounds(
53
- k: int, u1: np.ndarray, a_mat: np.ndarray, b_mat: np.ndarray
47
+ def _compute_m_M(
48
+ v: np.ndarray, a_mat: np.ndarray, dy2: np.ndarray, tau1_nodes: np.ndarray
54
49
  ) -> TwoArrays:
55
- """Return the admissible interval of ``u2`` that selects index ``k``."""
56
- n = a_mat.shape[0]
57
- m = u1.size
58
- if k == 0:
59
- left_bound = np.zeros(m)
60
- a_right = a_mat[0, 1:]
61
- b_right = b_mat[0, 1:]
62
- if a_right.size:
63
- right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
64
- else:
65
- right_bound = np.ones(m)
66
- elif 1 <= k < n - 1:
67
- a_left = a_mat[k, :k]
68
- b_left = b_mat[k, :k]
69
- if a_left.size:
70
- left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
71
- else:
72
- left_bound = np.zeros(m)
73
- a_right = a_mat[k, (k + 1) :]
74
- b_right = b_mat[k, (k + 1) :]
75
- if a_right.size:
76
- right_bound = np.min(np.outer(u1, a_right) - b_right, 1)
77
- else:
78
- right_bound = np.ones(m)
79
- elif k == n - 1:
80
- a_left = a_mat[-1, :-1]
81
- b_left = b_mat[-1, :-1]
82
- if a_left.size:
83
- left_bound = np.max(np.outer(u1, a_left) - b_left, 1)
84
- else:
85
- left_bound = np.zeros(m)
86
- right_bound = np.ones(m)
87
- else:
88
- bs_error_abort(f"{k=} is not compatible with {n=}")
89
- left_bound = np.clip(left_bound, 0.0, 1.0)
90
- right_bound = np.clip(right_bound, 0.0, 1.0)
91
-
92
- return left_bound, right_bound
93
-
94
-
95
- def bivariate_quantiles_v(y: np.ndarray, u: np.ndarray, v: np.ndarray) -> np.ndarray:
50
+ """Build the `m` and `M` matrices used in the dual optimisation."""
51
+ dv = np.subtract.outer(v, v)
52
+ b_mat = dv / dy2
53
+ np.fill_diagonal(dy2, 0.0)
54
+ EPS = 1e-12
55
+ maskp = dy2 < EPS
56
+ maskm = dy2 > -EPS
57
+ n, n_nodes = v.size, tau1_nodes.size
58
+ m_low = np.empty((n, n_nodes))
59
+ m_high = np.empty((n, n_nodes))
60
+ for i, tau1 in enumerate(tau1_nodes):
61
+ f_mat = tau1 * a_mat - b_mat
62
+ f_matp = f_mat.copy()
63
+ f_matm = f_mat.copy()
64
+ f_matp[maskp] = 1
65
+ f_matm[maskm] = 0
66
+ m_low[:, i] = np.max(f_matm, axis=1)
67
+ m_high[:, i] = np.min(f_matp, axis=1)
68
+ return np.clip(m_low, 0.0, 1.0), np.clip(m_high, 0.0, 1.0)
69
+
70
+
71
+ def bivariate_quantiles_v(y: np.ndarray, tau: np.ndarray, v: np.ndarray) -> np.ndarray:
96
72
  """Evaluate vector quantiles for a given set of dual weights.
97
73
 
98
74
  Args:
99
75
  y: Observations with shape ``(n, 2)``.
100
- u: Evaluation points in ``[0, 1]^2`` (shape ``(m, 2)``).
76
+ tau: Evaluation points in ``[0, 1]^2`` (shape ``(m, 2)``).
101
77
  v: Dual weights solving the optimal transport problem (length ``n``).
102
78
 
103
79
  Returns:
104
80
  Array of quantile locations with shape ``(m, 2)``.
105
81
  """
106
- u = np.atleast_2d(u)
107
- if u.shape[1] != 2:
108
- bs_error_abort("u must have two columns")
109
- m = u.shape[0]
110
- q = np.empty((m, 2))
111
- block = max(1, min(m, 5_000))
112
- for start in range(0, m, block):
113
- stop = min(start + block, m)
114
- chunk = u[start:stop]
115
- net_val = chunk @ y.T - v
116
- k_max = np.argmax(net_val, axis=1)
117
- q[start:stop] = y[k_max]
82
+ if tau.shape[1] != 2:
83
+ bs_error_abort("tau must have two columns")
84
+ q = y[np.argmax(tau @ y.T - v, axis=1), :]
118
85
  return cast(np.ndarray, q)
119
86
 
120
87
 
121
- def bivariate_ranks_v(
122
- y: np.ndarray, v: np.ndarray, n_nodes: int = 32, presorted: bool = False
123
- ) -> np.ndarray:
124
- """Compute the barycentric ranks of each observation given optimal weights.
125
-
126
- Args:
127
- y: Observations with shape ``(n, 2)``.
128
- v: Dual weights returned by ``solve_for_v_``.
129
- n_nodes: Number of Chebyshev nodes used in the quadrature.
130
- presorted: Set to ``True`` when ``y``/``v`` are pre-sorted by the
131
- second coordinate.
132
-
133
- Returns:
134
- Array of average ranks (shape ``(n, 2)``) with ``nan`` for zero-mass cells.
135
- """
136
- n, d = y.shape
137
-
138
- if d != 2:
139
- bs_error_abort(f"only works for 2-dimensional y, not for {d}")
140
-
141
- interval01 = Interval(0.0, 1.0)
142
- u1_nodes, u1_weights = cheb_get_nodes_1d(interval01, n_nodes)
143
-
144
- if presorted:
145
- sort_order = np.arange(n)
146
- y_sorted = y
147
- v_sorted = v
148
- else:
149
- sort_order = np.argsort(y[:, 1])
150
- y_sorted = y[sort_order, :]
151
- v_sorted = v[sort_order]
152
-
153
- a_mat, b_mat = _compute_ab(y_sorted, v_sorted)
154
-
155
- average_ranks = np.zeros((n, 2))
156
-
157
- for k in range(n):
158
- left_bounds, right_bounds = _compute_u2_bounds(k, u1_nodes, a_mat, b_mat)
159
- pos_diffs = np.maximum(right_bounds - left_bounds, 0.0)
160
- pos_diffs_sq = np.maximum(
161
- right_bounds * right_bounds - left_bounds * left_bounds, 0.0
162
- )
163
- prob_k = pos_diffs @ u1_weights
164
- if prob_k <= 1e-12:
165
- average_ranks[sort_order[k], :] = np.array([np.nan, np.nan])
166
- continue
167
- average_ranks[sort_order[k], 0] = ((u1_nodes * pos_diffs) @ u1_weights) / prob_k
168
- average_ranks[sort_order[k], 1] = ((pos_diffs_sq @ u1_weights) / 2.0) / prob_k
169
-
170
- return average_ranks
171
-
172
-
173
88
  def _objgrad(
174
- v_sorted: np.ndarray, args: list, gr: bool = False
175
- ) -> float | tuple[float, np.ndarray]:
176
- """computes the expectation of $\\psi(U, v)$ and perhaps its gradient wrt `v`
89
+ v: np.ndarray, args: list, gr: bool = False
90
+ ) -> float | tuple[float, np.ndarray, np.ndarray]:
91
+ """computes the expectation of $\\psi(U, v)$ and perhaps its gradient wrt `v` or the bivariate ranks
177
92
 
178
93
  Args:
179
- v_sorted: an `n`-vector of weights, sorted by increasing `y[:, 1]`
180
- args: a list of other arguments `[y_sorted, u1_nodes, u1_weights, verbose]`
181
- gr: if `True`, we also evaluate the gradient
94
+ v: an `(n-1)`-vector
95
+ args: a list of other arguments `[y, a_mat, dy2, tau1_nodes, tau1_weights, verbose]`
96
+ gr: if `False`, we only return the value of the objective function
97
+ if `True`, we also return the gradient and the bivariate ranks
182
98
 
183
99
  Returns:
184
- the value of the expectation and perhaps its gradient
100
+ the value of the expectation and perhaps its gradient and the bivariate ranks
185
101
  """
186
- y_sorted = args[0]
187
- n = y_sorted.shape[0]
188
- u1_nodes = args[1]
189
- u1_weights = args[2]
190
- vs1 = np.append(v_sorted, -np.sum(v_sorted))
191
- a_mat, b_mat = _compute_ab(y_sorted, vs1)
192
-
102
+ y = args[0]
103
+ y1 = y[:, 0]
104
+ y2 = y[:, 1]
105
+ n = y.shape[0]
106
+ a_mat, dy2 = args[1], args[2]
107
+ tau1_nodes = args[3]
108
+ tau1_weights = args[4]
109
+ vs1 = np.append(v, -np.sum(v))
110
+ m, M = _compute_m_M(vs1, a_mat, dy2, tau1_nodes)
111
+ # print(f"m is {m}")
112
+ # print(f"M is {M}")
113
+ # import sys
114
+
115
+ # sys.exit(1)
116
+
117
+ EPS = 1e-12
193
118
  obj_val = 0.0
194
119
  probs = np.zeros(n)
120
+ bivrank = np.zeros((n, 2))
195
121
  for k in range(n):
196
- left_bounds, right_bounds = _compute_u2_bounds(k, u1_nodes, a_mat, b_mat)
197
- pos_diffs = np.maximum(right_bounds - left_bounds, 0.0)
198
- pos_diffs_sq = np.maximum(
199
- right_bounds * right_bounds - left_bounds * left_bounds, 0.0
200
- )
201
- obj_val += (
202
- y_sorted[k, 0] * ((u1_nodes * pos_diffs) @ u1_weights)
203
- + y_sorted[k, 1] * (pos_diffs_sq @ u1_weights) / 2.0
204
- )
205
- probs[k] = pos_diffs @ u1_weights
206
- obj_val -= vs1[k] * probs[k]
122
+ Mk = M[k, :]
123
+ mk = m[k, :]
124
+ pos_diffs = np.maximum(Mk - mk, 0.0)
125
+ # print(f"pos_diffs for k={k} are {pos_diffs}")
126
+ pos_diffs_sq = np.maximum(Mk * Mk - mk * mk, 0.0)
127
+ probs[k] = pos_diffs @ tau1_weights
128
+ # print(f"probs[{k}] = {probs[k]}")
129
+ factor1 = (tau1_nodes * pos_diffs) @ tau1_weights
130
+ factor2 = (pos_diffs_sq @ tau1_weights) / 2.0
131
+ obj_val += y1[k] * factor1 + y2[k] * factor2 - vs1[k] * probs[k]
132
+ if probs[k] > EPS:
133
+ bivrank[k, 0] = factor1 / probs[k]
134
+ bivrank[k, 1] = factor2 / probs[k]
135
+
136
+ # print(f"{np.min(probs)=}")
207
137
 
208
138
  if gr:
209
139
  grad_val = probs[-1] - probs[:-1]
210
- return obj_val, grad_val
140
+ return obj_val, grad_val, bivrank
211
141
  else:
212
- return cast(float, obj_val)
142
+ return obj_val
213
143
 
214
144
 
215
- def _obj(v_sorted: np.ndarray, args: list):
216
- return _objgrad(v_sorted, args)
145
+ def _obj(v: np.ndarray, args: list):
146
+ return _objgrad(v, args)
217
147
 
218
148
 
219
- def _grad(v_sorted: np.ndarray, args: list):
220
- res_objg = cast(tuple[float, np.ndarray], _objgrad(v_sorted, args, gr=True))
149
+ def _grad(v: np.ndarray, args: list):
150
+ res_objg = cast(tuple[float, np.ndarray], _objgrad(v, args, gr=True))
221
151
  grad_val = res_objg[1]
222
- verbose = args[3]
152
+ verbose = args[-1]
223
153
  if verbose:
224
154
  print(f"The error on the gradient is {npmaxabs(grad_val)}")
225
155
  return grad_val
226
156
 
227
157
 
228
- def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.ndarray:
229
- """Solve the dual optimisation to obtain the optimal weights ``v``.
158
+ def _solve_for_v(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> TwoArrays:
159
+ """Solve the dual optimisation to obtain the optimal weights ``v`` and the bivariate ranks
230
160
 
231
161
  Args:
232
162
  y: Observations with shape ``(n, 2)``.
@@ -236,22 +166,21 @@ def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.
236
166
  Returns:
237
167
  Array of length ``n`` containing the optimal weights (including the
238
168
  residual term).
169
+ Array of shape ``(n, 2)`` containing the bivariate ranks (the barycenters of the cells in the power diagram).
239
170
  """
240
- n, d = y.shape
171
+ d = y.shape[1]
241
172
 
242
173
  if d != 2:
243
174
  bs_error_abort(f"only works for 2-dimensional y, not for {d}")
244
175
 
245
- # sort by increasing y[:, 1]
246
- sort_order = np.argsort(y[:, 1])
247
- y_sorted = y[sort_order, :]
248
-
249
- v0 = np.mean(y_sorted[:-1, :], 1)
176
+ v0 = np.mean(y[:-1, :], 1)
250
177
 
251
178
  interval01 = Interval(0.0, 1.0)
252
- u1_nodes, u1_weights = cheb_get_nodes_1d(interval01, n_nodes)
179
+ tau1_nodes, tau1_weights = cheb_get_nodes_1d(interval01, n_nodes)
253
180
 
254
- argsog = [y_sorted, u1_nodes, u1_weights, verbose]
181
+ a_mat, dy2 = _compute_ad(y)
182
+
183
+ argsog = [y, a_mat, dy2, tau1_nodes, tau1_weights, verbose]
255
184
 
256
185
  res = minimize_free(_obj, _grad, v0, args=argsog)
257
186
  if verbose:
@@ -262,45 +191,48 @@ def solve_for_v_(y: np.ndarray, n_nodes: int = 32, verbose: bool = False) -> np.
262
191
  vstar = res.x
263
192
  if verbose:
264
193
  print(f"The final gradient over v is close to 0: error {npmaxabs(res.jac)}")
265
- vstar1_sorted = np.append(vstar, -np.sum(vstar))
266
-
267
- # revert to original order
268
- vstar1 = np.zeros_like(vstar1_sorted)
269
- vstar1[sort_order] = vstar1_sorted
270
-
271
- return vstar1
194
+ _, _, bivranks = cast(tuple, _objgrad(vstar, argsog, gr=True))
195
+ vstar = np.append(vstar, -np.sum(vstar))
196
+ return cast(np.ndarray, vstar), cast(np.ndarray, bivranks)
272
197
 
273
198
 
274
- def bivariate_quantiles(
275
- y: np.ndarray, u: np.ndarray, n_nodes: int = 32, verbose: bool = False
199
+ def bivariate_ranks(
200
+ y: np.ndarray,
201
+ n_nodes: int = 32,
202
+ verbose: bool = False,
276
203
  ) -> np.ndarray:
277
- """Solve for the dual weights then evaluate bivariate quantiles.
204
+ """Compute the barycentric ranks of each observation.
278
205
 
279
206
  Args:
280
- y: Observations, shape ``(n, 2)``.
281
- u: Query points in ``[0, 1]^2`` (shape ``(m, 2)``).
282
- n_nodes: Number of Chebyshev nodes for the quadrature.
283
- verbose: Print optimisation diagnostics when ``True``.
207
+ y: Observations with shape ``(n, 2)``.
208
+ n_nodes: Number of Chebyshev nodes used in the quadrature.
209
+ verbose: Print diagnostics when ``True``.
284
210
 
285
211
  Returns:
286
- Bivariate quantiles at ``u``.
212
+ Array of average ranks (shape ``(n, 2)``) with ``nan`` for zero-mass cells.
287
213
  """
288
- v = solve_for_v_(y, n_nodes, verbose)
289
- return bivariate_quantiles_v(y, u, v)
214
+ d = y.shape[1]
215
+
216
+ if d != 2:
217
+ bs_error_abort(f"only works for 2-dimensional y, not for {d}")
290
218
 
219
+ _, bivranks = _solve_for_v(y, n_nodes, verbose)
220
+ return cast(np.ndarray, bivranks)
291
221
 
292
- def bivariate_ranks(
293
- y: np.ndarray, n_nodes: int = 32, verbose: bool = False
222
+
223
+ def bivariate_quantiles(
224
+ y: np.ndarray, tau: np.ndarray, n_nodes: int = 32, verbose: bool = False
294
225
  ) -> np.ndarray:
295
- """Compute ranks by first solving for the optimal weights ``v``.
226
+ """Solve for the dual weights then evaluate bivariate quantiles.
296
227
 
297
228
  Args:
298
229
  y: Observations, shape ``(n, 2)``.
230
+ tau: Query points in ``[0, 1]^2`` (shape ``(m, 2)``).
299
231
  n_nodes: Number of Chebyshev nodes for the quadrature.
300
232
  verbose: Print optimisation diagnostics when ``True``.
301
233
 
302
234
  Returns:
303
- Average ranks with shape ``(n, 2)``.
235
+ Bivariate quantiles at ``u``.
304
236
  """
305
- v = solve_for_v_(y, n_nodes, verbose)
306
- return bivariate_ranks_v(y, v, n_nodes)
237
+ v, _ = _solve_for_v(y, n_nodes, verbose)
238
+ return bivariate_quantiles_v(y, tau, v)
@@ -46,7 +46,7 @@ class ColorFormatter(logging.Formatter):
46
46
 
47
47
  def get_logger(
48
48
  name: str,
49
- level=logging.INFO,
49
+ level: int = logging.INFO,
50
50
  log_to_file: bool = False,
51
51
  ) -> logging.Logger:
52
52
  """Create and return a logger with colored output.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bs-python-utils
3
- Version: 0.8.3
3
+ Version: 0.9
4
4
  Summary: Utilities programs for my Python code
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -25,6 +25,7 @@ Requires-Dist: statsmodels>=0.14.5
25
25
  Requires-Dist: streamlit>=1.49.1
26
26
  Requires-Dist: vega-datasets>=0.9.0
27
27
  Requires-Dist: colorama>=0.4.6
28
+ Requires-Dist: pyarrow>=21.0.0
28
29
  Dynamic: license-file
29
30
 
30
31
  ## bs-python-utils
@@ -42,6 +43,9 @@ Dynamic: license-file
42
43
 
43
44
  ### Release notes
44
45
 
46
+ #### 0.8.5 (January 25, 2026)
47
+ Added colored logger.
48
+
45
49
  #### 0.8.2 (October 26, 2025)
46
50
  Included grids for sparse Gaussian integration.
47
51
 
@@ -1,8 +1,8 @@
1
1
  bs_python_utils/Timer.py,sha256=Rwj0Gec6VUq9Y6Rmykc5wL3Xd9dIj7Un_QgbJc2cTxo,1966
2
2
  bs_python_utils/__init__.py,sha256=XJ62LhKlvBaZH5ItthEpR7W6Sb9FEKi1d-6TZhP8DLc,1353
3
- bs_python_utils/bivariate_quantiles.py,sha256=Z-KGBUE6TlgayemskZb9C91ISSDwf3_PPoGMlrBr9mo,10231
3
+ bs_python_utils/bivariate_quantiles.py,sha256=NaoNvt0QHEssFvju9YTD6dcHycd8NyWRG3cJADuk_W0,7859
4
4
  bs_python_utils/bs_altair.py,sha256=4-MyCBrpuem48BhZV0dBhPZBgrxyl9PivdY3gaSUdN8,35127
5
- bs_python_utils/bs_logging.py,sha256=qRfb1B6LLLsgMTl4hMsUnfpE3H_-CKlHXawtEhZUT_Q,5043
5
+ bs_python_utils/bs_logging.py,sha256=CGi_pLeBSVd7FXsoafCI9xDu9eXLMnJebgY8Ku3-qP8,5050
6
6
  bs_python_utils/bs_mathstr.py,sha256=zW2ECxDD-29H81s_PNE1UKbIXJZtuhReJwcYim_4YTg,3595
7
7
  bs_python_utils/bs_mem.py,sha256=WaTikTlMBDBAakS6ZBh9EZuLYtNO_1XqcFXL33ws4vY,4668
8
8
  bs_python_utils/bs_opt.py,sha256=iq1fzPFm20kylg21uWXVDcfQWY7c7OpE9OYNBBaxbYU,17797
@@ -42,8 +42,8 @@ bs_python_utils/examples/examples_opt.py,sha256=pcWuTzOtTDWVJtKJJSUmaRIaPwDnAY3G
42
42
  bs_python_utils/examples/examples_seaborn.py,sha256=pwGx0pDVNGEtLMKgTqOEzoXkOsXgRND8-cJIxJQEY5o,783
43
43
  bs_python_utils/examples/examples_sklearn.py,sha256=TAnPRdRIiokrWBUUqVKkji5KXGCXyy_AjTIAEcmhtIY,792
44
44
  bs_python_utils/examples/examples_sparse_gaussian.py,sha256=xz3U2z97h-sxoxFmMDbtETRynVcGDUVahr9HCK42BtU,911
45
- bs_python_utils-0.8.3.dist-info/licenses/LICENSE,sha256=J03S1L_wQN4qquuluScbRUE09on78nCnNRpaUpOvHcw,1073
46
- bs_python_utils-0.8.3.dist-info/METADATA,sha256=ronR1HpDM8IF8ELj9QZqqoMDoSGzWj443RJF6RC35dA,3933
47
- bs_python_utils-0.8.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
48
- bs_python_utils-0.8.3.dist-info/top_level.txt,sha256=4sbCpeuqAsdWmoAmZju1vf1ZFMJrSx310eS5SCyrAeQ,16
49
- bs_python_utils-0.8.3.dist-info/RECORD,,
45
+ bs_python_utils-0.9.dist-info/licenses/LICENSE,sha256=J03S1L_wQN4qquuluScbRUE09on78nCnNRpaUpOvHcw,1073
46
+ bs_python_utils-0.9.dist-info/METADATA,sha256=EuB4Uk_EJb6_RmwvLN6G7i9NRh3vHCROA9lbC-jsGCg,4015
47
+ bs_python_utils-0.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
48
+ bs_python_utils-0.9.dist-info/top_level.txt,sha256=4sbCpeuqAsdWmoAmZju1vf1ZFMJrSx310eS5SCyrAeQ,16
49
+ bs_python_utils-0.9.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.2)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5