funcast 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.
funcast/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+ FunCast — Prévision fonctionnelle avec covariates.
3
+
4
+ Référence : Sezgin et al. (2025), "FunCast: a forecasting model for
5
+ functional data using covariates", Statistics and Computing.
6
+ """
7
+
8
+ from funcast.basis import bspline_basis, fourier_basis, get_basis
9
+ from funcast.model import FunCast
10
+ from funcast.selection import select_h_rrss
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = [
14
+ "FunCast",
15
+ "get_basis",
16
+ "bspline_basis",
17
+ "fourier_basis",
18
+ "select_h_rrss",
19
+ ]
funcast/basis.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ Functional basis for FunCast.
3
+ """
4
+
5
+ import numpy as np
6
+ from scipy.interpolate import BSpline
7
+
8
+
9
+ def bspline_basis(t: np.ndarray, n_basis: int, degree: int = 3) -> np.ndarray:
10
+ """
11
+ Create a cubic B-spline basis matrix.
12
+
13
+ Parameters
14
+ ----------
15
+ t : np.ndarray
16
+ Evaluation points.
17
+ n_basis : int
18
+ Number of basis functions.
19
+ degree : int
20
+ Spline degrees. Default is 3.
21
+
22
+ Returns
23
+ -------
24
+ B : np.ndarray
25
+ Basis matrix.
26
+ """
27
+ n_basis = max(n_basis, degree + 1)
28
+
29
+ t_min, t_max = t.min(), t.max()
30
+ n_inner = n_basis - degree - 1
31
+
32
+ if n_inner > 0:
33
+ inner_knots = np.linspace(t_min, t_max, n_inner + 2)[1:-1]
34
+ else:
35
+ inner_knots = np.array([])
36
+
37
+ knots = np.concatenate(
38
+ [np.repeat(t_min, degree + 1), inner_knots, np.repeat(t_max, degree + 1)]
39
+ )
40
+
41
+ B = np.zeros((len(t), n_basis))
42
+ for j in range(n_basis):
43
+ coef = np.zeros(n_basis)
44
+ coef[j] = 1.0
45
+ spl = BSpline(knots, coef, degree)
46
+ B[:, j] = spl(t)
47
+ return B
48
+
49
+
50
+ def fourier_basis(t: np.ndarray, n_basis: int) -> np.ndarray:
51
+ """
52
+ Create a Fourier basis.
53
+
54
+ Parameters
55
+ ----------
56
+ t : np.ndarray
57
+ Evaluation points.
58
+ n_basis : int
59
+ Number of basis functions.
60
+
61
+ Returns
62
+ -------
63
+ B : np.ndarray
64
+ Basis matrix.
65
+ """
66
+ T = t.max() - t.min()
67
+ B = np.ones((len(t), n_basis))
68
+ for k in range(1, n_basis):
69
+ freq = (k + 1) // 2
70
+ if k % 2 == 1:
71
+ B[:, k] = np.cos(2 * np.pi * freq * t / T)
72
+ else:
73
+ B[:, k] = np.sin(2 * np.pi * freq * t / T)
74
+ return B
75
+
76
+
77
+ def get_basis(t: np.ndarray, n_basis: int, basis_type: str = "bspline") -> np.ndarray:
78
+ """
79
+ Create a basis of functions.
80
+
81
+ Parameters
82
+ ----------
83
+ t : np.ndarray
84
+ Evaluation points.
85
+ n_basis : int
86
+ Number of basis functions.
87
+ basis_type : str
88
+ Type of function basis, "bspline" or "fourier". Default is "bspline".
89
+
90
+ Returns
91
+ -------
92
+ B : np.ndarray
93
+ Basis matrix.
94
+ """
95
+ if basis_type == "bspline":
96
+ return bspline_basis(t, n_basis)
97
+ elif basis_type == "fourier":
98
+ return fourier_basis(t, n_basis)
99
+ else:
100
+ raise ValueError(
101
+ f"unknown basis_type : '{basis_type}'. Choose 'bspline' or 'fourier'."
102
+ )
funcast/model.py ADDED
@@ -0,0 +1,347 @@
1
+ """
2
+ FunCast model implementation.
3
+ """
4
+
5
+ import numpy as np
6
+ from scipy.linalg import lstsq
7
+ from sklearn.base import BaseEstimator
8
+
9
+ from funcast.basis import get_basis
10
+ from funcast.selection import select_h_rrss
11
+
12
+ if hasattr(np, "trapezoid"):
13
+ _trapezoid = np.trapezoid # type: ignore[attr-defined]
14
+ else:
15
+ _trapezoid = np.trapz # type: ignore[attr-defined]
16
+
17
+
18
+ class FunCast(BaseEstimator):
19
+ """
20
+ FunCast model.
21
+
22
+ Parameters
23
+ ----------
24
+ K : int, optionnal
25
+ Number of basis functions for the future of Y. Default is 10.
26
+ s : float, optionnal
27
+ Smoothing coefficient. Default is 0.5.
28
+ basis_type : str
29
+ Type of function basis, "bspline" or "fourier". Default is "bspline".
30
+ auto_h : bool, optionnal
31
+ If True, h_l is optimized with RRSS. Default is True.
32
+ h_list : list, optionnal
33
+ Values of h_l if auto_h is False. Default is None.
34
+ degree : int, optionnal
35
+ B-splines degree. Default is 3.
36
+ rcond : float or None, optionnal
37
+ Pseudo-inverse threshold. Default is None.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ K: int = 10,
43
+ s: float = 0.5,
44
+ basis_type: str = "bspline",
45
+ auto_h: bool = True,
46
+ h_list: list[int] | None = None,
47
+ degree: int = 3,
48
+ rcond: float | None = None,
49
+ ):
50
+ self.K = K
51
+ self.s = s
52
+ self.basis_type = basis_type
53
+ self.auto_h = auto_h
54
+ self.h_list = h_list
55
+ self.degree = degree
56
+ self.rcond = rcond
57
+
58
+ def _compute_h(
59
+ self,
60
+ covariates_past: list[np.ndarray],
61
+ t_past: np.ndarray,
62
+ ) -> list[int]:
63
+ """
64
+ Compute h_l for each covariate.
65
+
66
+ Parameters
67
+ ----------
68
+ covariates_past : list of array-like
69
+ Past of covariates.
70
+ t_past : array-like
71
+ Past timestamps.
72
+
73
+ Returns
74
+ -------
75
+ h_values : list
76
+ Values of h_l.
77
+ """
78
+ h_values = []
79
+ for ell, X in enumerate(covariates_past):
80
+ if self.auto_h:
81
+ h = select_h_rrss(
82
+ X,
83
+ t_past,
84
+ basis_type=self.basis_type,
85
+ degree=self.degree,
86
+ )
87
+ else:
88
+ if self.h_list is None or len(self.h_list) <= ell:
89
+ raise ValueError(
90
+ "h_list required and of length p+1 if auto_h=False."
91
+ )
92
+ h = max(self.h_list[ell], self.degree + 1)
93
+ h_values.append(h)
94
+ return h_values
95
+
96
+ def _compute_q(self, h_values: list[int]) -> list[int]:
97
+ """
98
+ Compute q_l.
99
+
100
+ Parameters
101
+ ----------
102
+ h_values : list
103
+ Values of h_l.
104
+
105
+ Returns
106
+ -------
107
+ list
108
+ Values of q_l.
109
+ """
110
+ min_q = self.degree + 1
111
+ return [max(min_q, round((1 - self.s) * h)) for h in h_values]
112
+
113
+ def _project_covariates(
114
+ self,
115
+ covariates_past: list[np.ndarray],
116
+ t_past: np.ndarray,
117
+ h_values: list[int],
118
+ ) -> tuple[list[np.ndarray], list[np.ndarray]]:
119
+ """
120
+ Project each covariate on theta basis.
121
+
122
+ Parameters
123
+ ----------
124
+ covariates_past : list of array-like
125
+ Past of covariates.
126
+ t_past : array-like
127
+ Past timestamps.
128
+ h_values : list
129
+ Values of h_l.
130
+
131
+ Returns
132
+ -------
133
+ tuple
134
+ Expansion coefficients of the covariates.
135
+ """
136
+ C_list, theta_list = [], []
137
+ for X, h in zip(covariates_past, h_values):
138
+ theta = get_basis(t_past, h, self.basis_type)
139
+ thetaT_theta = theta.T @ theta
140
+ C, _, _, _ = lstsq(thetaT_theta, (X @ theta).T, cond=self.rcond)
141
+ C_list.append(C.T)
142
+ theta_list.append(theta)
143
+ return C_list, theta_list
144
+
145
+ def _compute_J_matrices(
146
+ self,
147
+ theta_list: list[np.ndarray],
148
+ t_past: np.ndarray,
149
+ q_values: list[int],
150
+ ) -> list[np.ndarray]:
151
+ """
152
+ Compute the inner product matrix.
153
+
154
+ Parameters
155
+ ----------
156
+ theta_list : list of array-like
157
+ Basis for covariates.
158
+ t_past : array-like
159
+ Past timestamps.
160
+ q_values : list
161
+ Values of q_l.
162
+
163
+ Returns
164
+ -------
165
+ J_list : list
166
+ Inner product matrix.
167
+ """
168
+ J_list = []
169
+ for theta, q_ell in zip(theta_list, q_values):
170
+ B_ell = get_basis(t_past, q_ell, self.basis_type)
171
+ integrand = theta[:, :, np.newaxis] * B_ell[:, np.newaxis, :]
172
+ J = _trapezoid(integrand, x=t_past, axis=0)
173
+ J_list.append(np.asarray(J))
174
+ return J_list
175
+
176
+ def _build_design_matrix(
177
+ self,
178
+ C_list: list[np.ndarray],
179
+ J_list: list[np.ndarray],
180
+ t_future: np.ndarray,
181
+ q_values: list[int],
182
+ ) -> np.ndarray:
183
+ """
184
+ Build the design matrix.
185
+
186
+ Parameters
187
+ ----------
188
+ C_list : list of array-like
189
+ Expansion coefficients of the covariates.
190
+ J_list : list
191
+ Inner product matrix.
192
+ t_future : array-like
193
+ Future timestamps.
194
+ q_values : list
195
+ Values of q_l.
196
+
197
+ Returns
198
+ -------
199
+ X_design : array-like
200
+ Design matrix.
201
+ """
202
+ n = C_list[0].shape[0]
203
+ m2 = len(t_future)
204
+ K = self.K
205
+ q_total = sum(q_values)
206
+ psi = get_basis(t_future, K, self.basis_type)
207
+ C_full = np.concatenate(C_list, axis=1)
208
+ X_design = np.zeros((n * m2, K * q_total))
209
+ for j, t_j in enumerate(t_future):
210
+ psi_t = psi[j, :]
211
+ xi_parts = []
212
+ for ell, (J, q_ell) in enumerate(zip(J_list, q_values)):
213
+ h_ell = J.shape[0]
214
+ c_start = sum(C_list[e].shape[1] for e in range(ell))
215
+ c_end = c_start + h_ell
216
+ c_i_ell = C_full[:, c_start:c_end]
217
+ cJ = c_i_ell @ J
218
+ block = np.einsum("k,nq->nkq", psi_t, cJ).reshape(n, K * q_ell)
219
+ xi_parts.append(block)
220
+ xi_j = np.concatenate(xi_parts, axis=1)
221
+ X_design[j::m2, :] = xi_j
222
+ return X_design
223
+
224
+ def fit(
225
+ self,
226
+ Y_past: np.ndarray,
227
+ Y_future: np.ndarray,
228
+ t_past: np.ndarray,
229
+ t_future: np.ndarray,
230
+ covariates_past: list[np.ndarray] | None = None,
231
+ ) -> "FunCast":
232
+ """
233
+ Fit FunCast.
234
+
235
+ Parameters
236
+ ----------
237
+ Y_past : array-like
238
+ Past of Y.
239
+ Y_future : array-like
240
+ Future of Y.
241
+ t_past : array-like
242
+ Past timestamps.
243
+ t_future : array-like
244
+ Future timestamps.
245
+ covariates_past : list of array-like
246
+ Past of covariates.
247
+
248
+ Returns
249
+ -------
250
+ self
251
+ """
252
+ all_covariates = [Y_past] + (covariates_past or [])
253
+
254
+ self.h_values_ = self._compute_h(all_covariates, t_past)
255
+ self.q_values_ = self._compute_q(self.h_values_)
256
+ self.C_list_, self.theta_list_ = self._project_covariates(
257
+ all_covariates, t_past, self.h_values_
258
+ )
259
+ self.J_list_ = self._compute_J_matrices(
260
+ self.theta_list_, t_past, self.q_values_
261
+ )
262
+
263
+ X_design = self._build_design_matrix(
264
+ self.C_list_, self.J_list_, t_future, self.q_values_
265
+ )
266
+ y = Y_future.ravel()
267
+ self.b_hat_, _, _, _ = lstsq(X_design, y, cond=self.rcond)
268
+
269
+ self.t_past_ = t_past
270
+ self.t_future_ = t_future
271
+ self.m2_ = Y_future.shape[1]
272
+ self.n_covariates_ = len(all_covariates)
273
+
274
+ return self
275
+
276
+ def predict(
277
+ self,
278
+ Y_past_new: np.ndarray,
279
+ covariates_past_new: list[np.ndarray] | None = None,
280
+ ) -> np.ndarray:
281
+ """
282
+ Predict the future of Y.
283
+
284
+ Parameters
285
+ ----------
286
+ Y_past_new : array-like
287
+ Past of Y for inference.
288
+ covariates_past_new : list of array-like
289
+ Past of covariates for inference.
290
+
291
+ Returns
292
+ -------
293
+ Y_pred : array-like
294
+ Prediction of Y.
295
+ """
296
+ all_new = [Y_past_new] + (covariates_past_new or [])
297
+
298
+ C_new_list = []
299
+ for X_new, theta, h in zip(all_new, self.theta_list_, self.h_values_):
300
+ thetaT_theta = theta.T @ theta
301
+ C_new, _, _, _ = lstsq(thetaT_theta, (X_new @ theta).T, cond=self.rcond)
302
+ C_new_list.append(np.asarray(C_new).T) # ← np.asarray() clarifie le type
303
+
304
+ X_new_design = self._build_design_matrix(
305
+ C_new_list, self.J_list_, self.t_future_, self.q_values_
306
+ )
307
+
308
+ n_new = Y_past_new.shape[0]
309
+ y_pred = X_new_design @ self.b_hat_
310
+ return np.asarray(y_pred).reshape(n_new, self.m2_)
311
+
312
+ def score(
313
+ self,
314
+ Y_past: np.ndarray,
315
+ Y_future: np.ndarray,
316
+ covariates_past: list[np.ndarray] | None = None,
317
+ metric: str = "rmse",
318
+ ) -> float:
319
+ """
320
+ Compute RMSE and SMAPE.
321
+
322
+ Parameters
323
+ ----------
324
+ Y_past : array-like
325
+ Past of Y.
326
+ Y_future : array-like
327
+ Future of Y.
328
+ covariates_past : list of array-like or None, optionnal
329
+ Past of covariates. Default is None.
330
+ metric : string, optionnal
331
+ Evaluation metric, "rmse" or "smape". Default is "rmse".
332
+
333
+ Returns
334
+ -------
335
+ float
336
+ Metric value.
337
+ """
338
+ Y_pred = self.predict(Y_past, covariates_past)
339
+
340
+ if metric == "rmse":
341
+ return float(np.sqrt(np.mean((Y_future - Y_pred) ** 2)))
342
+ elif metric == "smape":
343
+ denom = np.abs(Y_future) + np.abs(Y_pred)
344
+ denom = np.where(denom == 0, 1e-8, denom)
345
+ return float(100 * np.mean(np.abs(Y_future - Y_pred) / denom))
346
+ else:
347
+ raise ValueError(f"metric inconnu : '{metric}'. Choisir 'rmse' ou 'smape'.")
funcast/selection.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ Automatic selection of h_l with RRSS criterion.
3
+ """
4
+
5
+ import warnings
6
+
7
+ import numpy as np
8
+ from scipy.linalg import lstsq
9
+
10
+ from funcast.basis import get_basis
11
+
12
+
13
+ def select_h_rrss(
14
+ X: np.ndarray,
15
+ t: np.ndarray,
16
+ h_candidates: list[int] | None = None,
17
+ basis_type: str = "bspline",
18
+ degree: int = 3,
19
+ ) -> int:
20
+ """
21
+ Selects h_l with RRSS criterion.
22
+
23
+ Parameters
24
+ ----------
25
+ X : array-like
26
+ Covariate realization.
27
+ t : array-like
28
+ Temporal grid.
29
+ h_candidates : list or None, optionnal
30
+ Candidates for h_l values. Default is None.
31
+ basis_type : str
32
+ Type of function basis. Default is "bspline"
33
+ degree : int
34
+ Spline degrees. Default is 3.
35
+
36
+ Returns
37
+ -------
38
+ h_opt : int
39
+ h_l value that minimizes RRSS.
40
+ """
41
+ n, m1 = X.shape
42
+ min_h = degree + 1
43
+
44
+ if h_candidates is None:
45
+ max_h = max(min_h + 1, min(m1 // 2, 30))
46
+ h_candidates = list(range(min_h, max_h + 1))
47
+ else:
48
+ h_candidates = [h for h in h_candidates if h >= min_h]
49
+
50
+ if not h_candidates:
51
+ warnings.warn(f"No valid candidate for h_l. Set h={min_h}.")
52
+ return min_h
53
+
54
+ best_h, best_rrss = h_candidates[0], np.inf
55
+
56
+ for h in h_candidates:
57
+ denom = m1 - h
58
+ if denom <= 0:
59
+ continue
60
+ try:
61
+ theta = get_basis(t, h, basis_type)
62
+ thetaT_theta = theta.T @ theta
63
+ C, _, _, _ = lstsq(thetaT_theta, (X @ theta).T)
64
+ X_hat = (theta @ C).T
65
+ residuals = X - X_hat
66
+ rrss = np.sqrt(np.sum(residuals**2) / (n * denom))
67
+ if rrss < best_rrss:
68
+ best_rrss = rrss
69
+ best_h = h
70
+ except np.linalg.LinAlgError:
71
+ continue
72
+
73
+ return best_h
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: funcast
3
+ Version: 0.1.0
4
+ Summary: Functional data forecasting with covariates (FunCast model)
5
+ Project-URL: Homepage, https://github.com/SelmanSzgn/funcast
6
+ Project-URL: Repository, https://github.com/SelmanSzgn/funcast
7
+ Project-URL: Documentation, https://funcast.readthedocs.io
8
+ Project-URL: Bug Tracker, https://github.com/SelmanSzgn/funcast/issues
9
+ Author-email: Selman Sezgin <selman.szg@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2025 Selman Sezgin
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ License-File: LICENSE
20
+ Keywords: B-spline,forecasting,functional data analysis,statistics,time series
21
+ Classifier: Development Status :: 3 - Alpha
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: License :: OSI Approved :: MIT License
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
30
+ Requires-Python: >=3.10
31
+ Requires-Dist: numpy>=1.24
32
+ Requires-Dist: scikit-learn>=1.3
33
+ Requires-Dist: scipy>=1.10
34
+ Provides-Extra: dev
35
+ Requires-Dist: mypy>=1.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
37
+ Requires-Dist: pytest>=7.0; extra == 'dev'
38
+ Requires-Dist: ruff>=0.4; extra == 'dev'
39
+ Provides-Extra: docs
40
+ Requires-Dist: myst-parser>=2.0; extra == 'docs'
41
+ Requires-Dist: sphinx-autodoc-typehints>=1.25; extra == 'docs'
42
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
43
+ Requires-Dist: sphinx>=7.0; extra == 'docs'
44
+ Provides-Extra: examples
45
+ Requires-Dist: jupyter>=1.0; extra == 'examples'
46
+ Requires-Dist: matplotlib>=3.7; extra == 'examples'
47
+ Description-Content-Type: text/markdown
48
+
49
+ Python implementation of the FunCast functional data forecasting model.
50
+
51
+ ## Paper reference
52
+
53
+ Sezgin et al. (2025), *"Funcast: a forecasting model for functional data using covariates"*, under review at Journal of Statistical Planning and Inference (JSPI).
54
+
55
+ Authors : Selman Sezgin (a, b), Julien Jacques (a), Kahina Mokrani (b) and Sylvain Allio (b)
56
+
57
+ (a) ERIC, Université Lumière Lyon 2, Lyon, France
58
+
59
+ (b) Orange Research, Belfort, France
60
+
61
+ ## Installation
62
+ ``
63
+ pip install funcast
64
+ ``
65
+
66
+ ## Quick start
67
+ ```
68
+ import numpy as np
69
+ from funcast import FunCast
70
+
71
+ n, m1, m2 = 50, 100, 20
72
+ t_past = np.linspace(0, 1, m1)
73
+ t_future = np.linspace(1, 1.2, m2)
74
+ Y_past = np.random.randn(n, m1)
75
+ Y_future = np.random.randn(n, m2)
76
+
77
+ model = FunCast(K=8, s=0.5)
78
+ model.fit(Y_past, Y_future, t_past, t_future)
79
+ Y_pred = model.predict(Y_past)
80
+ ```
@@ -0,0 +1,8 @@
1
+ funcast/__init__.py,sha256=yieC42oHrighMK0HsqFAoZ5_cwHemzVeDyR1_7bz6Ds,490
2
+ funcast/basis.py,sha256=7Dt4n4h64eC82uIBg3MrBWhTSDRiIokMMHD79WW5obA,2446
3
+ funcast/model.py,sha256=cC8hNRiW-I0YdL_3NoFP3oUujjx_2bpUP_PQ5SCPk8o,10421
4
+ funcast/selection.py,sha256=oATDIddwwRnosfEYOBIpD9BLq1U8WjZ5BYtQuqVi9EQ,1854
5
+ funcast-0.1.0.dist-info/METADATA,sha256=w7a3Iknx8DRxfTTd7MIktskT_YxIxwG9mjYvUGAPv6w,3667
6
+ funcast-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ funcast-0.1.0.dist-info/licenses/LICENSE,sha256=y_187Ur1UaN3ShFLycik_BY8GE_uTgR4HzuhfYBmImM,1077
8
+ funcast-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Selman Sezgin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.