matrix-hat 0.1.0__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.
matrix_hat/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """matrix-hat: hat matrix and leverage diagnostics for linear regression.
2
+
3
+ Quick start
4
+ -----------
5
+ >>> import numpy as np
6
+ >>> from matrix_hat import leverage, hat_matrix
7
+ >>> X = np.array([[1.0], [2.0], [3.0], [10.0]])
8
+ >>> leverage(X, add_intercept=True).round(2)
9
+ array([0.43, 0.33, 0.27, 0.97])
10
+ """
11
+
12
+ from .core import (
13
+ effective_rank,
14
+ hat_matrix,
15
+ high_leverage_points,
16
+ leverage,
17
+ orthonormal_basis,
18
+ )
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = [
23
+ "hat_matrix",
24
+ "leverage",
25
+ "high_leverage_points",
26
+ "orthonormal_basis",
27
+ "effective_rank",
28
+ "__version__",
29
+ ]
@@ -0,0 +1,34 @@
1
+ """Private input validation and design-matrix helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from numpy.typing import ArrayLike, NDArray
7
+
8
+
9
+ def _as_2d(X: ArrayLike) -> NDArray[np.floating]:
10
+ """Return ``X`` as a 2-D float array, promoting 1-D input to a column."""
11
+ arr = np.asarray(X, dtype=float)
12
+ if arr.ndim == 1:
13
+ arr = arr.reshape(-1, 1)
14
+ if arr.ndim != 2:
15
+ raise ValueError(f"X must be 1- or 2-dimensional, got {arr.ndim} dimensions")
16
+ if arr.size == 0:
17
+ raise ValueError("X must not be empty")
18
+ return arr
19
+
20
+
21
+ def _design(X: ArrayLike, add_intercept: bool) -> NDArray[np.floating]:
22
+ """Build the design matrix, optionally prepending an intercept column."""
23
+ arr = _as_2d(X)
24
+ if add_intercept:
25
+ intercept = np.ones((arr.shape[0], 1), dtype=float)
26
+ arr = np.hstack([intercept, arr])
27
+ return arr
28
+
29
+
30
+ def _default_tol(s: NDArray[np.floating], shape: tuple[int, int]) -> float:
31
+ """Default singular-value tolerance, matching ``numpy.linalg.matrix_rank``."""
32
+ if s.size == 0:
33
+ return 0.0
34
+ return float(s.max()) * max(shape) * np.finfo(float).eps
matrix_hat/core.py ADDED
@@ -0,0 +1,354 @@
1
+ """Core hat matrix and leverage computations.
2
+
3
+ The *hat matrix* of a design matrix ``X`` is the orthogonal projection onto
4
+ the column space of ``X``:
5
+
6
+ .. math::
7
+
8
+ H = X (X^{\\top} X)^{-1} X^{\\top}
9
+
10
+ It is called the *hat* matrix because it maps the observed response ``y`` onto
11
+ its fitted values (``y-hat``): :math:`\\hat{y} = H y`. The diagonal entries
12
+ :math:`h_{ii}` are the *leverages*, a standard regression diagnostic measuring
13
+ how far the ``i``-th observation lies from the centre of the predictor space.
14
+
15
+ Computations here are based on a rank-revealing SVD rather than an explicit
16
+ matrix inverse, which is numerically stable and gracefully handles
17
+ rank-deficient design matrices.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import numpy as np
23
+ from numpy.typing import ArrayLike, NDArray
24
+
25
+ from ._validation import _default_tol, _design
26
+
27
+ __all__ = [
28
+ "orthonormal_basis",
29
+ "hat_matrix",
30
+ "leverage",
31
+ "high_leverage_points",
32
+ "effective_rank",
33
+ ]
34
+
35
+
36
+ def orthonormal_basis(
37
+ X: ArrayLike,
38
+ add_intercept: bool = False,
39
+ tol: float | None = None,
40
+ ) -> NDArray[np.floating]:
41
+ """Return an orthonormal basis ``Q`` for the column space of ``X``.
42
+
43
+ The basis is obtained from the SVD and spans the same subspace as the
44
+ (possibly rank-deficient) columns of the design matrix. The hat matrix
45
+ satisfies :math:`H = Q Q^{\\top}`.
46
+
47
+ Parameters
48
+ ----------
49
+ X : array_like
50
+ Design matrix of shape ``(n_samples, n_features)``. A 1-D array is
51
+ treated as a single column.
52
+ add_intercept : bool, default False
53
+ If True, prepend a column of ones to ``X``.
54
+ tol : float, optional
55
+ Singular values below ``tol`` are treated as zero when determining the
56
+ rank. Defaults to a value scaled by the largest singular value and the
57
+ matrix size (same rule as :func:`numpy.linalg.matrix_rank`).
58
+
59
+ Returns
60
+ -------
61
+ ndarray
62
+ Array of shape ``(n_samples, rank)`` with orthonormal columns.
63
+
64
+ Raises
65
+ ------
66
+ ValueError
67
+ If ``X`` is empty or has more than two dimensions.
68
+
69
+ See Also
70
+ --------
71
+ hat_matrix : Full projection matrix ``H = Q @ Q.T``.
72
+ leverage : Diagonal of ``H`` without forming the full matrix.
73
+ effective_rank : Numerical rank equal to ``Q.shape[1]``.
74
+
75
+ Notes
76
+ -----
77
+ Using a truncated SVD avoids forming :math:`(X^{\\top} X)^{-1}` and remains
78
+ well-behaved for ill-conditioned or rank-deficient designs.
79
+
80
+ References
81
+ ----------
82
+ .. [1] Hoaglin, D. C. and Welsch, R. E. (1978). "The Hat Matrix in
83
+ Regression and ANOVA". *The American Statistician*, 32(1), 17--22.
84
+
85
+ Examples
86
+ --------
87
+ >>> import numpy as np
88
+ >>> from matrix_hat import orthonormal_basis
89
+ >>> X = np.array([[1.0], [2.0], [3.0]])
90
+ >>> Q = orthonormal_basis(X, add_intercept=True)
91
+ >>> Q.shape
92
+ (3, 2)
93
+ >>> np.allclose(Q.T @ Q, np.eye(Q.shape[1]))
94
+ True
95
+ """
96
+ design = _design(X, add_intercept)
97
+ U, s, _ = np.linalg.svd(design, full_matrices=False)
98
+ if tol is None:
99
+ tol = _default_tol(s, design.shape)
100
+ rank = int(np.sum(s > tol))
101
+ return U[:, :rank]
102
+
103
+
104
+ def effective_rank(
105
+ X: ArrayLike,
106
+ add_intercept: bool = False,
107
+ tol: float | None = None,
108
+ ) -> int:
109
+ """Return the numerical rank of the (optionally augmented) design matrix.
110
+
111
+ Equivalent to the number of columns of :func:`orthonormal_basis`, i.e. the
112
+ number of singular values strictly larger than ``tol``.
113
+
114
+ Parameters
115
+ ----------
116
+ X : array_like
117
+ Design matrix of shape ``(n_samples, n_features)``. A 1-D array is
118
+ treated as a single column.
119
+ add_intercept : bool, default False
120
+ If True, prepend a column of ones to ``X`` before ranking.
121
+ tol : float, optional
122
+ Singular-value cut-off passed to :func:`orthonormal_basis`.
123
+
124
+ Returns
125
+ -------
126
+ int
127
+ Numerical rank of the design matrix.
128
+
129
+ Raises
130
+ ------
131
+ ValueError
132
+ If ``X`` is empty or has more than two dimensions.
133
+
134
+ See Also
135
+ --------
136
+ orthonormal_basis : Orthonormal basis whose width equals the rank.
137
+ leverage : Leverages sum to this rank.
138
+
139
+ Notes
140
+ -----
141
+ For a full-rank design with an intercept, the rank is
142
+ ``n_features + 1``.
143
+
144
+ References
145
+ ----------
146
+ .. [1] Hoaglin, D. C. and Welsch, R. E. (1978). "The Hat Matrix in
147
+ Regression and ANOVA". *The American Statistician*, 32(1), 17--22.
148
+
149
+ Examples
150
+ --------
151
+ >>> import numpy as np
152
+ >>> from matrix_hat import effective_rank
153
+ >>> X = np.array([[1.0, 2.0], [2.0, 4.0], [3.0, 6.0]]) # rank 1
154
+ >>> effective_rank(X)
155
+ 1
156
+ >>> effective_rank(X, add_intercept=True)
157
+ 2
158
+ """
159
+ return orthonormal_basis(X, add_intercept=add_intercept, tol=tol).shape[1]
160
+
161
+
162
+ def hat_matrix(
163
+ X: ArrayLike,
164
+ add_intercept: bool = False,
165
+ tol: float | None = None,
166
+ ) -> NDArray[np.floating]:
167
+ """Compute the full hat (projection) matrix ``H``.
168
+
169
+ Parameters
170
+ ----------
171
+ X : array_like
172
+ Design matrix of shape ``(n_samples, n_features)``. A 1-D array is
173
+ treated as a single column.
174
+ add_intercept : bool, default False
175
+ If True, prepend a column of ones before projecting.
176
+ tol : float, optional
177
+ Rank tolerance passed to :func:`orthonormal_basis`.
178
+
179
+ Returns
180
+ -------
181
+ ndarray
182
+ Symmetric idempotent matrix of shape ``(n_samples, n_samples)``.
183
+
184
+ Raises
185
+ ------
186
+ ValueError
187
+ If ``X`` is empty or has more than two dimensions.
188
+
189
+ See Also
190
+ --------
191
+ leverage : Prefer this for large ``n``; returns only the diagonal.
192
+ orthonormal_basis : Factors ``H`` as ``Q @ Q.T``.
193
+ high_leverage_points : Indices with large diagonal entries.
194
+
195
+ Notes
196
+ -----
197
+ The full hat matrix is ``n_samples`` by ``n_samples``. For large samples
198
+ prefer :func:`leverage`, which returns only the diagonal without forming
199
+ the full matrix. Algebraically,
200
+ :math:`H` is the orthogonal projector onto the column space of ``X``, so
201
+ :math:`H = H^{\\top} = H^{2}`.
202
+
203
+ References
204
+ ----------
205
+ .. [1] Hoaglin, D. C. and Welsch, R. E. (1978). "The Hat Matrix in
206
+ Regression and ANOVA". *The American Statistician*, 32(1), 17--22.
207
+
208
+ Examples
209
+ --------
210
+ >>> import numpy as np
211
+ >>> from matrix_hat import hat_matrix
212
+ >>> X = np.array([[1.0], [2.0], [3.0]])
213
+ >>> H = hat_matrix(X, add_intercept=True)
214
+ >>> H.shape
215
+ (3, 3)
216
+ >>> np.allclose(H, H.T) and np.allclose(H @ H, H)
217
+ True
218
+ """
219
+ Q = orthonormal_basis(X, add_intercept=add_intercept, tol=tol)
220
+ return Q @ Q.T
221
+
222
+
223
+ def leverage(
224
+ X: ArrayLike,
225
+ add_intercept: bool = False,
226
+ tol: float | None = None,
227
+ ) -> NDArray[np.floating]:
228
+ """Compute the leverages ``h_ii`` (diagonal of the hat matrix).
229
+
230
+ Computed as the row-wise sum of squares of an orthonormal basis for the
231
+ column space, avoiding formation of the full ``n_samples`` square hat
232
+ matrix.
233
+
234
+ Parameters
235
+ ----------
236
+ X : array_like
237
+ Design matrix of shape ``(n_samples, n_features)``. A 1-D array is
238
+ treated as a single column.
239
+ add_intercept : bool, default False
240
+ If True, prepend a column of ones before projecting.
241
+ tol : float, optional
242
+ Rank tolerance passed to :func:`orthonormal_basis`.
243
+
244
+ Returns
245
+ -------
246
+ ndarray
247
+ Vector of length ``n_samples`` with values in ``[0, 1]`` that sum to
248
+ the rank of the design matrix.
249
+
250
+ Raises
251
+ ------
252
+ ValueError
253
+ If ``X`` is empty or has more than two dimensions.
254
+
255
+ See Also
256
+ --------
257
+ hat_matrix : Full projection matrix whose diagonal is returned here.
258
+ high_leverage_points : Flag observations above a leverage cut-off.
259
+ effective_rank : Equal to ``leverage(...).sum()`` (within rounding).
260
+
261
+ Notes
262
+ -----
263
+ Leverages satisfy :math:`0 \\le h_{ii} \\le 1` and
264
+ :math:`\\sum_i h_{ii} = \\mathrm{rank}(X)`. Their average is therefore
265
+ :math:`p / n`. Memory use is ``O(n p)`` rather than ``O(n^2)``.
266
+
267
+ References
268
+ ----------
269
+ .. [1] Hoaglin, D. C. and Welsch, R. E. (1978). "The Hat Matrix in
270
+ Regression and ANOVA". *The American Statistician*, 32(1), 17--22.
271
+ .. [2] Belsley, D. A., Kuh, E. and Welsch, R. E. (1980). *Regression
272
+ Diagnostics*. Wiley.
273
+
274
+ Examples
275
+ --------
276
+ >>> import numpy as np
277
+ >>> from matrix_hat import leverage
278
+ >>> X = np.array([[1.0], [2.0], [3.0], [10.0]])
279
+ >>> leverage(X, add_intercept=True).round(2)
280
+ array([0.43, 0.33, 0.27, 0.97])
281
+ """
282
+ Q = orthonormal_basis(X, add_intercept=add_intercept, tol=tol)
283
+ return np.einsum("ij,ij->i", Q, Q)
284
+
285
+
286
+ def high_leverage_points(
287
+ X: ArrayLike,
288
+ add_intercept: bool = False,
289
+ threshold: float | None = None,
290
+ factor: float = 2.0,
291
+ tol: float | None = None,
292
+ ) -> NDArray[np.integer]:
293
+ """Return indices of observations whose leverage exceeds a threshold.
294
+
295
+ Parameters
296
+ ----------
297
+ X : array_like
298
+ Design matrix of shape ``(n_samples, n_features)``. A 1-D array is
299
+ treated as a single column.
300
+ add_intercept : bool, default False
301
+ If True, prepend a column of ones before projecting.
302
+ threshold : float, optional
303
+ Absolute leverage cut-off. If omitted, a rule-of-thumb threshold of
304
+ ``factor * p / n`` is used, where ``p`` is the rank (mean leverage is
305
+ ``p / n``) and ``n`` is the number of observations.
306
+ factor : float, default 2.0
307
+ Multiplier for the automatic ``factor * p / n`` threshold. Common
308
+ choices are 2 or 3. Ignored when ``threshold`` is given.
309
+ tol : float, optional
310
+ Rank tolerance passed to :func:`orthonormal_basis`.
311
+
312
+ Returns
313
+ -------
314
+ ndarray
315
+ Integer indices of the flagged observations, in ascending order.
316
+
317
+ Raises
318
+ ------
319
+ ValueError
320
+ If ``X`` is empty or has more than two dimensions.
321
+
322
+ See Also
323
+ --------
324
+ leverage : Leverage values compared against the threshold.
325
+ hat_matrix : Full projection matrix (prefer :func:`leverage` for large ``n``).
326
+
327
+ Notes
328
+ -----
329
+ A common rule of thumb flags observations with
330
+ :math:`h_{ii} > 2p/n` (or :math:`3p/n`) as high leverage [2]_. High
331
+ leverage indicates unusual predictor values, not necessarily an outlier in
332
+ the response.
333
+
334
+ References
335
+ ----------
336
+ .. [1] Hoaglin, D. C. and Welsch, R. E. (1978). "The Hat Matrix in
337
+ Regression and ANOVA". *The American Statistician*, 32(1), 17--22.
338
+ .. [2] Belsley, D. A., Kuh, E. and Welsch, R. E. (1980). *Regression
339
+ Diagnostics*. Wiley.
340
+
341
+ Examples
342
+ --------
343
+ >>> import numpy as np
344
+ >>> from matrix_hat import high_leverage_points
345
+ >>> X = np.array([[1.0], [2.0], [3.0], [4.0], [50.0]])
346
+ >>> high_leverage_points(X, add_intercept=True)
347
+ array([4])
348
+ """
349
+ h = leverage(X, add_intercept=add_intercept, tol=tol)
350
+ if threshold is None:
351
+ n = h.shape[0]
352
+ p = float(np.sum(h)) # rank == trace of the hat matrix
353
+ threshold = factor * p / n
354
+ return np.flatnonzero(h > threshold)
matrix_hat/py.typed ADDED
File without changes
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: matrix-hat
3
+ Version: 0.1.0
4
+ Summary: Hat matrix and leverage diagnostics for linear regression.
5
+ Project-URL: Homepage, https://github.com/aokienz/matrix-hat
6
+ Project-URL: Repository, https://github.com/aokienz/matrix-hat
7
+ Project-URL: Issues, https://github.com/aokienz/matrix-hat/issues
8
+ Author-email: Enzo Aoki <enzoaoki02@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: diagnostics,hat-matrix,influence,leverage,outliers,regression,statistics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: numpy>=1.21
25
+ Provides-Extra: dev
26
+ Requires-Dist: numpydoc>=1.6; extra == 'dev'
27
+ Requires-Dist: pydata-sphinx-theme>=0.15; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: sphinx>=7.0; extra == 'dev'
30
+ Provides-Extra: docs
31
+ Requires-Dist: numpydoc>=1.6; extra == 'docs'
32
+ Requires-Dist: pydata-sphinx-theme>=0.15; extra == 'docs'
33
+ Requires-Dist: sphinx>=7.0; extra == 'docs'
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest>=7.0; extra == 'test'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # matrix-hat
39
+
40
+ [![CI](https://github.com/aokienz/matrix-hat/actions/workflows/ci.yml/badge.svg)](https://github.com/aokienz/matrix-hat/actions/workflows/ci.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/matrix-hat.svg)](https://pypi.org/project/matrix-hat/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/matrix-hat.svg)](https://pypi.org/project/matrix-hat/)
43
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
44
+
45
+ Small, dependency-light toolkit for the **hat matrix** and **leverage**
46
+ diagnostics used in linear regression.
47
+
48
+ The hat matrix of a design matrix `X` is the orthogonal projection onto its
49
+ column space:
50
+
51
+ $$H = X (X^\top X)^{-1} X^\top$$
52
+
53
+ It earns its name because it turns observed responses into fitted values
54
+ ("y-hat"): $\hat{y} = Hy$. Its diagonal entries $h_{ii}$ are the **leverages**,
55
+ which measure how far each observation sits from the centre of the predictor
56
+ space — a key tool for spotting influential points and outliers.
57
+
58
+ **Full documentation** (user guide, math background, API reference) lives in
59
+ [`docs/`](docs/). Build locally with `pip install -e ".[docs]"` then
60
+ `sphinx-build -W -b html docs docs/_build/html`.
61
+
62
+ ## Why this package
63
+
64
+ - **Numerically stable.** Rank-revealing SVD instead of an explicit
65
+ `(XᵀX)⁻¹`, so it stays well-behaved on ill-conditioned or rank-deficient
66
+ designs.
67
+ - **Memory-aware.** `leverage()` returns just the diagonal without ever forming
68
+ the full `n × n` hat matrix.
69
+ - **Tiny footprint.** NumPy is the only runtime dependency.
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ pip install matrix-hat
75
+ ```
76
+
77
+ From source (tests + docs):
78
+
79
+ ```bash
80
+ pip install -e ".[dev]"
81
+ ```
82
+
83
+ ## Quickstart
84
+
85
+ ```python
86
+ import numpy as np
87
+ from matrix_hat import leverage, hat_matrix, high_leverage_points
88
+
89
+ X = np.array([[1.0], [2.0], [3.0], [4.0], [50.0]])
90
+
91
+ # Leverages (diagonal of the hat matrix), with an intercept column added.
92
+ h = leverage(X, add_intercept=True)
93
+ # array([0.267, 0.255, 0.245, 0.235, 0.998]) approx.
94
+
95
+ # Flag high-leverage observations (default rule of thumb: 2 * p / n).
96
+ high_leverage_points(X, add_intercept=True)
97
+ # array([4])
98
+
99
+ # The full projection matrix, when you actually need it.
100
+ H = hat_matrix(X, add_intercept=True)
101
+ ```
102
+
103
+ ## API
104
+
105
+ | Function | Description |
106
+ | --- | --- |
107
+ | `leverage(X, add_intercept=False)` | Leverages `h_ii` (diagonal of `H`) as a length-`n` vector. |
108
+ | `hat_matrix(X, add_intercept=False)` | The full `n × n` projection matrix `H`. |
109
+ | `high_leverage_points(X, threshold=None, factor=2.0)` | Indices of observations above a leverage cut-off. |
110
+ | `orthonormal_basis(X, add_intercept=False)` | Orthonormal basis `Q` for the column space (`H = Q Qᵀ`). |
111
+ | `effective_rank(X, add_intercept=False)` | Numerical rank of the design matrix. |
112
+
113
+ All functions accept a `tol` argument to control the singular-value cut-off
114
+ used for rank determination. See the Sphinx docs for full numpydoc reference
115
+ pages.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ pip install -e ".[dev]"
121
+ pytest
122
+ sphinx-build -W -b html docs docs/_build/html
123
+ ```
124
+
125
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
126
+
127
+ ## Citation
128
+
129
+ If you use this package in research, see [CITATION.cff](CITATION.cff).
130
+
131
+ ## Changelog
132
+
133
+ See [CHANGELOG.md](CHANGELOG.md).
134
+
135
+ ## License
136
+
137
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ matrix_hat/__init__.py,sha256=U47RPmJ1B9fs65p_1DL6pCq1hrBVzYdnR2iRFnUZ-2Y,587
2
+ matrix_hat/_validation.py,sha256=geA4U5oI0-9d4wLVzw64adWx0IC4Dj_TskxdPc0EERA,1145
3
+ matrix_hat/core.py,sha256=X_6b1AUOX7CuU5AXvEMdwebawVLnWX7Eh8GKAKCg-0E,11038
4
+ matrix_hat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ matrix_hat-0.1.0.dist-info/METADATA,sha256=we31r6XpmNNebQkmKqFuKWDG75wIEK-rPfhnyGVry8Q,4795
6
+ matrix_hat-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
7
+ matrix_hat-0.1.0.dist-info/licenses/LICENSE,sha256=yxcYk9fBYppe_dZZt15gsLiMTKRJiUbx0kSKoL3yoQw,1061
8
+ matrix_hat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Enzo
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.