rectanglepy 0.0.39__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.
@@ -0,0 +1,333 @@
1
+ import math
2
+ import multiprocessing
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import quadprog
7
+ import statsmodels.api as sm
8
+ from joblib import Parallel, delayed, parallel_backend
9
+ from loguru import logger
10
+
11
+ from rectanglepy.pp.rectangle_signature import RectangleSignatureResult
12
+
13
+
14
+ def _scale_weights(weights: np.ndarray) -> np.ndarray:
15
+ min_weight = np.nextafter(min(weights), np.float64(1.0)) # prevent division by zero
16
+ return weights / min_weight
17
+
18
+
19
+ def solve_qp(
20
+ signature: pd.DataFrame,
21
+ bulk: pd.Series,
22
+ prev_assignments: list[int or str] = None,
23
+ prev_solution: pd.Series = None,
24
+ gld: np.ndarray = None,
25
+ multiplier: int = None,
26
+ ) -> np.ndarray:
27
+ """Performs quadratic programming optimization to solve the deconvolution problem.
28
+
29
+ Parameters
30
+ ----------
31
+ signature : pd.DataFrame
32
+ The signature matrix for deconvolution. Each row represents a gene and each column represents a cell type.
33
+ bulk : pd.Series
34
+ The bulk data for deconvolution. Each entry represents the expression level of a gene.
35
+ prev_assignments : default is None
36
+ A list of previous assignments of cell types to genes. If provided, the function will use these assignments as additional QP constraints.
37
+ prev_solution : default is None
38
+ A series of previous solution for each cell type. If provided, the function will use the prev solution as additional QP constraints..
39
+ gld : default is None
40
+ An array of gene length data. If provided, the function will use this data to adjust the weights.
41
+ multiplier : default is None
42
+ A multiplier for the weights. If provided, the function will use this multiplier to adjust the weights.
43
+
44
+ Returns
45
+ -------
46
+ np.ndarray
47
+ An array of corrected cell fractions. Each entry represents the fraction of a cell type in the bulk data.
48
+
49
+ Notes
50
+ -----
51
+ This function uses quadratic programming to solve the deconvolution problem. The objective is to minimize the difference between the observed bulk data and the data predicted by the signature matrix and the cell fractions. The function also includes constraints to ensure that the cell fractions are non-negative and sum to 1, and to make the solution similar to the previous assignments and weights if they are provided.
52
+ """
53
+ # ------------------ QP-based deconvolution
54
+ # Minimize 1/2 x^T G x - a^T x
55
+ # Subject to C.T x >= b
56
+ if multiplier is None:
57
+ a = np.dot(signature.T, bulk).astype("double")
58
+ G = np.dot(signature.T, signature).astype("double")
59
+ else:
60
+ weights = np.square(1 / (signature @ gld))
61
+ weights_dampened = np.clip(_scale_weights(weights), None, multiplier)
62
+ W = np.diag(weights_dampened)
63
+ G = np.dot(signature.T, np.dot(W, signature))
64
+ a = np.dot(signature.T, np.dot(W, bulk))
65
+
66
+ # Compute the constraints matrices
67
+ n_genes = G.shape[0]
68
+ # Constraint 1: sum of all fractions is below or equal to 1
69
+ C1 = -np.ones((n_genes, 1))
70
+ b1 = -np.ones(1)
71
+ # Constraint 2: every fraction is greater or equal to 0
72
+ C2 = np.eye(n_genes)
73
+ b2 = np.zeros(n_genes)
74
+ # Constraint 3: if a previous solution is provided, the new solution should be similar to the previous one
75
+ prev_constraints, prev_b = [], []
76
+ if prev_solution is not None:
77
+ for cluster in prev_solution.index:
78
+ C_upper = [-np.ones(1) if str(x) == str(cluster) else np.zeros(1) for x in prev_assignments]
79
+ C_lower = [np.ones(1) if str(x) == str(cluster) else np.zeros(1) for x in prev_assignments]
80
+ prev_constraints.extend([C_upper, C_lower])
81
+ prev_weight = prev_solution.loc[cluster]
82
+ # allow a 3% variation in the previous weights
83
+ prev_weight_upper = min(1, prev_weight + 0.03)
84
+ prev_weight_lower = max(0, prev_weight - 0.03)
85
+ prev_b.extend([prev_weight_upper * np.ones(1) * -1, prev_weight_lower * np.ones(1)])
86
+
87
+ C = np.concatenate((C1, C2, *prev_constraints), axis=1)
88
+ b = np.concatenate((b1, b2, *prev_b), axis=0)
89
+
90
+ scale = np.linalg.norm(G)
91
+ solution = quadprog.solve_qp(G / scale, a / scale, C, b, factorized=False)
92
+ return solution[0]
93
+
94
+
95
+ def _find_dampening_constant(signature: pd.DataFrame, bulk: pd.Series, qp_gld: np.ndarray) -> int:
96
+ solutions_std = []
97
+ np.random.seed(1)
98
+ weights = np.square(1 / (np.dot(signature, qp_gld)))
99
+ weights_scaled = _scale_weights(weights)
100
+ weights_scaled_no_inf = weights_scaled[weights_scaled != np.inf]
101
+ qp_gld_sum = sum(qp_gld)
102
+ # try multiple values of the dampening constant (multiplier)
103
+ # for each, calculate the variance of the dampened weighted solution for a subset of genes
104
+ max_range = 40
105
+ multiplier_range = min(max_range, math.ceil(np.log2(max(weights_scaled_no_inf))))
106
+ for i in range(multiplier_range):
107
+ solutions = []
108
+ multiplier = 2**i
109
+ weights_dampened = np.array([multiplier if multiplier <= x else x for x in weights_scaled]).astype("double")
110
+ for _ in range(100):
111
+ subset = np.random.choice(len(signature), size=len(signature) // 2, replace=False)
112
+ bulk_subset = bulk.iloc[list(subset)]
113
+ signature_subset = signature.iloc[subset, :]
114
+ fit = sm.WLS(bulk_subset, -1 + signature_subset, weights=weights_dampened[subset]).fit()
115
+ solution = fit.params * qp_gld_sum / sum(fit.params)
116
+ solutions.append(solution)
117
+ solutions_df = pd.DataFrame(solutions)
118
+
119
+ solutions_std.append(solutions_df.std(axis=0))
120
+ solutions_std_df = pd.DataFrame(solutions_std)
121
+ means = solutions_std_df.apply(lambda x: np.mean(x**2), axis=1)
122
+ best_dampening_constant = means.idxmin()
123
+ return best_dampening_constant
124
+
125
+
126
+ def _calculate_dwls(
127
+ signature: pd.DataFrame,
128
+ bulk: pd.Series,
129
+ prev_assignments: list[int or str] = None,
130
+ prev_weights: pd.Series = None,
131
+ ) -> pd.Series:
132
+ genes = list(set(signature.index) & set(bulk.index))
133
+ signature = signature.loc[genes].sort_index()
134
+ bulk = bulk.loc[genes].sort_index().astype("double")
135
+
136
+ approximate_solution = solve_qp(signature, bulk, prev_assignments, prev_weights)
137
+ dampening_constant = _find_dampening_constant(signature, bulk, approximate_solution)
138
+ multiplier = 2**dampening_constant
139
+
140
+ max_iterations = 1000
141
+ convergence_threshold = 0.002
142
+ change = 1
143
+ iterations = 2
144
+ solutions_sum = approximate_solution
145
+ while (change > convergence_threshold) and (iterations < max_iterations):
146
+ dampened_solution = solve_qp(signature, bulk, prev_assignments, prev_weights, approximate_solution, multiplier)
147
+ solutions_sum += dampened_solution
148
+ solution_averages = solutions_sum / iterations
149
+ change = np.linalg.norm(solution_averages - approximate_solution, 1)
150
+ approximate_solution = solution_averages
151
+ iterations += 1
152
+
153
+ if iterations == max_iterations:
154
+ logger.warning("Dampened weighted least squares did not converge, using last solution")
155
+
156
+ return pd.Series(approximate_solution, index=signature.columns)
157
+
158
+
159
+ def deconvolution(
160
+ signatures: RectangleSignatureResult,
161
+ bulks: pd.DataFrame,
162
+ correct_mrna_bias: bool = True,
163
+ n_cpus: int = None,
164
+ ) -> pd.DataFrame:
165
+ """Performs recursive deconvolution using rectangle signatures and bulk data.
166
+
167
+ Parameters
168
+ ----------
169
+ signatures : RectangleSignatureResult
170
+ The rectangle signature result containing the signature data and results.
171
+ bulks : pandas.DataFrame
172
+ The tpm normalized bulk data for deconvolution. Rows are samples and columns are genes.
173
+ correct_mrna_bias : bool
174
+ A flag indicating whether to correct for mRNA bias. Defaults to True.
175
+ n_cpus : int
176
+ The number of CPUs to use for parallel processing. If not provided, the function will use all available CPUs. Each CPU will process a bulk sample.
177
+
178
+ Returns
179
+ -------
180
+ A DataFrame containing the estimated cell fractions resulting from deconvolution. Each row represents a sample and each column represents a cell type.
181
+
182
+ """
183
+ if n_cpus is not None:
184
+ num_processes = n_cpus
185
+ else:
186
+ num_processes = multiprocessing.cpu_count()
187
+
188
+ with parallel_backend("loky", inner_max_num_threads=1):
189
+ results = Parallel(
190
+ n_jobs=num_processes,
191
+ )(
192
+ delayed(_process_bulk)(signatures, i, bulk, bulks.columns, correct_mrna_bias)
193
+ for i, bulk in enumerate(bulks.values)
194
+ )
195
+
196
+ result_df = pd.DataFrame(results, index=bulks.index)
197
+
198
+ # Return the result DataFrame
199
+ return result_df
200
+
201
+
202
+ def _process_bulk(
203
+ signatures: RectangleSignatureResult, i: int, bulk: pd.Series, var_names: pd.Index, correct_mrna_bias: bool
204
+ ) -> pd.Series:
205
+ try:
206
+ logger.info(f"Deconvolute fractions for bulk: {i}")
207
+ bulk = pd.Series(bulk, index=var_names)
208
+ result = _deconvolute(signatures, bulk, correct_mrna_bias=correct_mrna_bias)
209
+ logger.info(f"Finished deconvolution for bulk: {i}")
210
+ return result
211
+ except Exception as e:
212
+ logger.warning(f"Deconvolution failed for bulk: {i} with error: {e}")
213
+ return pd.Series(index=signatures.pseudobulk_sig_cpm.columns)
214
+
215
+
216
+ def _deconvolute(signatures: RectangleSignatureResult, bulk: pd.Series, correct_mrna_bias: bool = True) -> pd.Series:
217
+ bulk_direct_reduced = bulk[bulk.index.isin(signatures.signature_genes)]
218
+ signature_genes_direct_reduced = signatures.signature_genes[
219
+ signatures.signature_genes.isin(bulk_direct_reduced.index)
220
+ ]
221
+ pseudobulk_sig_cpm = signatures.pseudobulk_sig_cpm
222
+ clustered_pseudobulk_sig_cpm = signatures.clustered_pseudobulk_sig_cpm
223
+ bias_factors = signatures.bias_factors
224
+
225
+ if not correct_mrna_bias:
226
+ bias_factors = bias_factors * 0 + 1
227
+
228
+ signature = pseudobulk_sig_cpm.loc[signature_genes_direct_reduced] * bias_factors
229
+ start_fractions = _calculate_dwls(signature, bulk)
230
+
231
+ if clustered_pseudobulk_sig_cpm is None:
232
+ start_fractions = correct_for_unknown_cell_content(bulk, pseudobulk_sig_cpm, start_fractions, bias_factors)
233
+ return start_fractions
234
+
235
+ cluster_bias_factors = signatures.clustered_bias_factors
236
+ if not correct_mrna_bias:
237
+ cluster_bias_factors = cluster_bias_factors * 0 + 1
238
+
239
+ bulk_rec_reduced = bulk[bulk.index.isin(clustered_pseudobulk_sig_cpm.index)]
240
+ clustered_signature_genes = signatures.clustered_signature_genes[
241
+ signatures.clustered_signature_genes.isin(bulk_rec_reduced.index)
242
+ ]
243
+ clustered_signature = clustered_pseudobulk_sig_cpm.loc[clustered_signature_genes] * cluster_bias_factors
244
+
245
+ union_genes = list(set(signature_genes_direct_reduced) | set(clustered_signature_genes))
246
+ union_direct_signature = pseudobulk_sig_cpm.loc[union_genes] * bias_factors
247
+
248
+ try:
249
+ clustered_fractions = _calculate_dwls(clustered_signature, bulk)
250
+ recursive_fractions = _calculate_dwls(signature, bulk, signatures.assignments, clustered_fractions)
251
+ except Exception as e:
252
+ logger.warning(f"Recursive deconvolution failed with error: {e}")
253
+ return start_fractions
254
+
255
+ union_direct_fraction = _calculate_dwls(union_direct_signature, bulk)
256
+
257
+ averaged_start_fractions = (start_fractions + union_direct_fraction) / 2
258
+
259
+ final_fractions = []
260
+ cell_types_with_low_number_of_marker_genes = signatures.cell_types_with_low_number_of_marker_genes()
261
+ for cell_type in list(averaged_start_fractions.index):
262
+ if cell_type in cell_types_with_low_number_of_marker_genes:
263
+ final_fractions.append(recursive_fractions[cell_type])
264
+ else:
265
+ final_fractions.append(averaged_start_fractions[cell_type])
266
+
267
+ final_fractions = pd.Series(final_fractions, index=averaged_start_fractions.index)
268
+
269
+ final_fractions = correct_for_unknown_cell_content(bulk, pseudobulk_sig_cpm, final_fractions, bias_factors)
270
+ return final_fractions
271
+
272
+
273
+ def correct_for_unknown_cell_content(
274
+ bulk: pd.Series, pseudo_signature_cpm: pd.DataFrame, estimates: pd.Series, bias_factors: pd.Series
275
+ ) -> pd.Series:
276
+ r"""Performs correction for unknown cell content using the pseudo signature and bulk data.
277
+
278
+ Reconstructs the bulk expression profiles through the estimated cell fractions (weighted by the mRNA bias factors) and cell-type-specific expression profiles (i.e. signature).
279
+
280
+ .. math::
281
+ \text{bulk_est} = \text{pseudo_signature} \times (\text{estimates}^T \times \text{bias_factors})
282
+
283
+ The unknown cellular content is then calculated as the difference of per-sample overall expression levels in the true vs. reconstructed bulk, divided by the overall expression in the true bulk.
284
+
285
+ .. math::
286
+ \text{ukn_cc} = \frac{\text{bulk} - \text{bulk_est}}{\sum_{i=1}^{n} x_i}
287
+
288
+ Finally, the methods corrects (i.e. scales) the cell fraction estimates so that their sum equals 1 - the unknown cellular content and returns this corrected value to the user.
289
+
290
+ .. math::
291
+ \text{estimates_fix} = \frac{\text{estimates}}{\sum_{i=1}^{n} x_i} \times (1 - \text{ukn_cc})
292
+
293
+ Parameters
294
+ ----------
295
+ bulk
296
+ The bulk count data for deconvolution, indexed by gene. Normalized by TPM.
297
+ pseudo_signature_cpm
298
+ Averaged sc data, indexed by gene. Normalized by CPM. Contains all genes.
299
+ estimates
300
+ The estimated cell fractions resulting from the deconvolution. Indexed by cell type.
301
+ bias_factors
302
+ The mRNA bias factors of the sc data atlas.
303
+
304
+ Returns
305
+ -------
306
+ pd.Series: The corrected cell fractions, indexed by cell type. Adds an "Unknown" cell type.
307
+ """
308
+ if estimates.sum() == 0:
309
+ estimates_fix = estimates
310
+ # analysis fails if all cell fractions are zero, so we set the unknown cell content to ß
311
+ estimates_fix.loc["Unknown"] = 0
312
+ return estimates_fix
313
+
314
+ signature = pseudo_signature_cpm.sort_index()
315
+ bulk = bulk.sort_index()
316
+
317
+ # Reconstruct the bulk expression profiles through matrix multiplication
318
+ # of the estimated cell fractions (weighted by the scaling factors) and
319
+ # cell-type-specific expression profiles (i.e. signature)
320
+ bulk_est = pd.Series(np.dot(signature, (estimates.T * bias_factors).T))
321
+ bulk_est.index = signature.index
322
+
323
+ # Calculate the unknown cellular content ad the difference of
324
+ # per-sample overall expression levels in the true vs. reconstructed
325
+ # bulk RNA-seq data, divided by the overall expression in the true bulk
326
+ ukn_cc = (bulk - bulk_est).sum() / (bulk.sum())
327
+ ukn_cc = max(0, ukn_cc)
328
+ # Correct (i.e. scale) the cell fraction estimates so that their sum
329
+ # equals 1 - the unknown cellular content estimated above
330
+ estimates_fix = estimates / estimates.sum() * (1 - ukn_cc)
331
+ estimates_fix.loc["Unknown"] = abs(ukn_cc)
332
+
333
+ return estimates_fix
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: rectanglepy
3
+ Version: 0.0.39
4
+ Summary: A very interesting piece of code
5
+ Project-URL: Documentation, https://rectanglepy.readthedocs.io/
6
+ Project-URL: Source, https://github.com/bernheder/Rectangle
7
+ Project-URL: Home-page, https://github.com/bernheder/Rectangle
8
+ Author: Bernhard Eder
9
+ Maintainer-email: Bernhard Eder <Bernhard.Eder@student.uibk.ac.at>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2023, Bernhard Eder
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Requires-Python: >=3.10
33
+ Requires-Dist: loguru
34
+ Requires-Dist: pydeseq2==0.4.1
35
+ Requires-Dist: quadprog==0.1.11
36
+ Provides-Extra: dev
37
+ Requires-Dist: bump2version; extra == 'dev'
38
+ Requires-Dist: pre-commit; extra == 'dev'
39
+ Requires-Dist: twine>=4.0.2; extra == 'dev'
40
+ Provides-Extra: doc
41
+ Requires-Dist: ipykernel; extra == 'doc'
42
+ Requires-Dist: ipython; extra == 'doc'
43
+ Requires-Dist: myst-nb; extra == 'doc'
44
+ Requires-Dist: sphinx-autodoc-typehints; extra == 'doc'
45
+ Requires-Dist: sphinx-book-theme>=1.0.0; extra == 'doc'
46
+ Requires-Dist: sphinx-copybutton; extra == 'doc'
47
+ Requires-Dist: sphinx>=4; extra == 'doc'
48
+ Requires-Dist: sphinxcontrib-bibtex>=1.0.0; extra == 'doc'
49
+ Provides-Extra: test
50
+ Requires-Dist: pytest; extra == 'test'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # Rectangle
54
+
55
+ [![Tests][badge-tests]][link-tests]
56
+ [![Documentation][badge-docs]][link-docs]
57
+
58
+ [badge-tests]: https://img.shields.io/github/actions/workflow/status/bernheder/Rectangle/test.yaml?branch=main
59
+ [link-tests]: https://github.com/bernheder/Rectangle/actions/workflows/test.yml
60
+ [badge-docs]: https://img.shields.io/readthedocs/Rectangle
61
+
62
+ Rectangle embodies a ”second-generation” deconvolution methodology, setting itself apart from other second-generation
63
+ methods through its efficiency in signature creation and its handling of challenges within the deconvolution process.
64
+ To establish a precise deconvolution method, we employ a hierarchical analysis approach, incorporate mRNA bias
65
+ correction, and provide an estimation of unknown cellular content.
66
+
67
+ ## Getting started
68
+
69
+ Please refer to the [documentation][link-docs]. In particular, the
70
+
71
+ - [API documentation][link-api].
72
+
73
+ ## Installation
74
+
75
+ You need to have Python 3.10 or newer installed on your system. If you don't have
76
+ Python installed, we recommend installing [Mambaforge](https://github.com/conda-forge/miniforge#mambaforge).
77
+
78
+ How to install Rectangle:
79
+
80
+ <!--
81
+ 1) Install the latest release of `Rectangle` from `PyPI <https://pypi.org/project/rectanglepy/>`_:
82
+
83
+ ```bash
84
+ pip install rectanglepy
85
+ ```
86
+ -->
87
+
88
+ ## Release notes
89
+
90
+ See the [changelog][changelog].
91
+
92
+ ## Contact
93
+
94
+ If you found a bug, please use the [issue tracker][issue-tracker].
95
+
96
+ ## Citation
97
+
98
+ > t.b.a
99
+
100
+ [scverse-discourse]: https://discourse.scverse.org/
101
+ [issue-tracker]: https://github.com/bernheder/Rectangle/issues
102
+ [changelog]: https://Rectanglepy.readthedocs.io/latest/changelog.html
103
+ [link-docs]: https://Rectanglepy.readthedocs.io
104
+ [link-api]: https://Rectanglepy.readthedocs.io/latest/api.html
@@ -0,0 +1,14 @@
1
+ rectanglepy/__init__.py,sha256=WntxT4hQzXFIGQOVxYG_kag47PO8VygUBQMQLem6xak,211
2
+ rectanglepy/rectangle.py,sha256=yTkdrM4KWLvPQMa2so-FfevpYhcj-McHZ3KGPXpbm7o,4732
3
+ rectanglepy/data/hao1_annotations_small.csv,sha256=uV1IB3xKS7SyUvxO2LxJ-zkG8DDLRb8cFPNfB8OcB4s,2896
4
+ rectanglepy/data/hao1_counts_small.csv,sha256=H-LAZQfRmMsXYgflOmpvooa-jIGKgJvBXeNzKdJStdU,2694169
5
+ rectanglepy/data/small_fino_bulks.csv,sha256=vBdUCVOww7vzQh-ECIt9HKpF1wkeFRYOVOPgI4mlYzk,91148
6
+ rectanglepy/pp/__init__.py,sha256=aEhro1Qq8ZAqMUaOxgLbuvSGRk5VhLxIkbnjV6vr1qk,185
7
+ rectanglepy/pp/create_signature.py,sha256=hrvSAom0wZaNc1HOD0U0GVXH2eNm7ZbF3RAKiEvTdB4,19253
8
+ rectanglepy/pp/rectangle_signature.py,sha256=8qxHLy_im9UX2miCVRHE9nn7IHwLExLlkQz9krDYc_Y,3468
9
+ rectanglepy/tl/__init__.py,sha256=0GCo2syQrjTZ4GJtj2qiFDiBIi3mUd-SBU6SSc7N7EA,80
10
+ rectanglepy/tl/deconvolution.py,sha256=hKZkhzZUcLk-GRdL2f7_jHvCz1CaDmfUvu0gkJVNH9k,14808
11
+ rectanglepy-0.0.39.dist-info/METADATA,sha256=oiib2qv_GEDnvKDMdIc3tkCUeygzFBMctQBPwjqHlUg,4113
12
+ rectanglepy-0.0.39.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
13
+ rectanglepy-0.0.39.dist-info/licenses/LICENSE,sha256=uMoqBtwv3v_iNXuN-0ShGN4XVvdx7K7-SvcCmLVg_I0,1071
14
+ rectanglepy-0.0.39.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.21.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023, Bernhard Eder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.