mxlpy 0.9.0__py3-none-any.whl → 0.11.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.
- mxlpy/__init__.py +2 -0
- mxlpy/fit.py +1 -1
- mxlpy/identify.py +68 -0
- mxlpy/plot.py +43 -0
- mxlpy/report.py +209 -0
- {mxlpy-0.9.0.dist-info → mxlpy-0.11.0.dist-info}/METADATA +10 -1
- {mxlpy-0.9.0.dist-info → mxlpy-0.11.0.dist-info}/RECORD +9 -7
- {mxlpy-0.9.0.dist-info → mxlpy-0.11.0.dist-info}/WHEEL +0 -0
- {mxlpy-0.9.0.dist-info → mxlpy-0.11.0.dist-info}/licenses/LICENSE +0 -0
mxlpy/__init__.py
CHANGED
mxlpy/fit.py
CHANGED
@@ -154,7 +154,7 @@ def _time_course_residual(
|
|
154
154
|
par_names: list[str],
|
155
155
|
data: pd.DataFrame,
|
156
156
|
model: Model,
|
157
|
-
y0: dict[str, float],
|
157
|
+
y0: dict[str, float] | None,
|
158
158
|
integrator: Callable[[Callable, ArrayLike], IntegratorProtocol],
|
159
159
|
) -> float:
|
160
160
|
"""Calculate residual error between model time course and experimental data.
|
mxlpy/identify.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
"""Numerical parameter identification estimations."""
|
2
|
+
|
3
|
+
from functools import partial
|
4
|
+
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
from tqdm import tqdm
|
8
|
+
|
9
|
+
from mxlpy import fit
|
10
|
+
from mxlpy.distributions import LogNormal, sample
|
11
|
+
from mxlpy.model import Model
|
12
|
+
from mxlpy.parallel import parallelise
|
13
|
+
from mxlpy.types import Array
|
14
|
+
|
15
|
+
__all__ = ["profile_likelihood"]
|
16
|
+
|
17
|
+
|
18
|
+
def _mc_fit_time_course_worker(
|
19
|
+
p0: pd.Series,
|
20
|
+
model: Model,
|
21
|
+
data: pd.DataFrame,
|
22
|
+
) -> float:
|
23
|
+
p_fit = fit.time_course(model=model, p0=p0.to_dict(), data=data)
|
24
|
+
return fit._time_course_residual( # noqa: SLF001
|
25
|
+
par_values=list(p_fit.values()),
|
26
|
+
par_names=list(p_fit.keys()),
|
27
|
+
data=data,
|
28
|
+
model=model,
|
29
|
+
y0=None,
|
30
|
+
integrator=fit.DefaultIntegrator,
|
31
|
+
)
|
32
|
+
|
33
|
+
|
34
|
+
def profile_likelihood(
|
35
|
+
model: Model,
|
36
|
+
data: pd.DataFrame,
|
37
|
+
parameter_name: str,
|
38
|
+
parameter_values: Array,
|
39
|
+
n_random: int = 10,
|
40
|
+
) -> pd.Series:
|
41
|
+
"""Estimate the profile likelihood of model parameters given data.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
model: The model to be fitted.
|
45
|
+
data: The data to fit the model to.
|
46
|
+
parameter_name: The name of the parameter to profile.
|
47
|
+
parameter_values: The values of the parameter to profile.
|
48
|
+
n_random: Number of Monte Carlo samples.
|
49
|
+
|
50
|
+
"""
|
51
|
+
parameter_distributions = sample(
|
52
|
+
{k: LogNormal(np.log(v), sigma=1) for k, v in model.parameters.items()},
|
53
|
+
n=n_random,
|
54
|
+
)
|
55
|
+
|
56
|
+
res = {}
|
57
|
+
for value in tqdm(parameter_values, desc=parameter_name):
|
58
|
+
model.update_parameter(parameter_name, value)
|
59
|
+
res[value] = parallelise(
|
60
|
+
partial(_mc_fit_time_course_worker, model=model, data=data),
|
61
|
+
inputs=list(
|
62
|
+
parameter_distributions.drop(columns=parameter_name).iterrows()
|
63
|
+
),
|
64
|
+
disable_tqdm=True,
|
65
|
+
)
|
66
|
+
errors = pd.DataFrame(res, dtype=float).T.abs().mean(axis=1)
|
67
|
+
errors.index.name = "fitting error"
|
68
|
+
return errors
|
mxlpy/plot.py
CHANGED
@@ -18,12 +18,17 @@ Functions:
|
|
18
18
|
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
|
+
import contextlib
|
22
|
+
|
23
|
+
from cycler import cycler
|
24
|
+
|
21
25
|
__all__ = [
|
22
26
|
"FigAx",
|
23
27
|
"FigAxs",
|
24
28
|
"Linestyle",
|
25
29
|
"add_grid",
|
26
30
|
"bars",
|
31
|
+
"context",
|
27
32
|
"grid_layout",
|
28
33
|
"heatmap",
|
29
34
|
"heatmap_from_2d_idx",
|
@@ -66,6 +71,8 @@ from mpl_toolkits.mplot3d import Axes3D
|
|
66
71
|
from mxlpy.label_map import LabelMapper
|
67
72
|
|
68
73
|
if TYPE_CHECKING:
|
74
|
+
from collections.abc import Generator
|
75
|
+
|
69
76
|
from matplotlib.collections import QuadMesh
|
70
77
|
|
71
78
|
from mxlpy.linear_label_map import LinearLabelMapper
|
@@ -259,6 +266,12 @@ def rotate_xlabels(
|
|
259
266
|
|
260
267
|
|
261
268
|
def show(fig: Figure | None = None) -> None:
|
269
|
+
"""Show the given figure or the current figure.
|
270
|
+
|
271
|
+
Args:
|
272
|
+
fig: Figure to show.
|
273
|
+
|
274
|
+
"""
|
262
275
|
if fig is None:
|
263
276
|
plt.show()
|
264
277
|
else:
|
@@ -266,9 +279,39 @@ def show(fig: Figure | None = None) -> None:
|
|
266
279
|
|
267
280
|
|
268
281
|
def reset_prop_cycle(ax: Axes) -> None:
|
282
|
+
"""Reset the property cycle of the given axis.
|
283
|
+
|
284
|
+
Args:
|
285
|
+
ax: Axis to reset the property cycle of.
|
286
|
+
|
287
|
+
"""
|
269
288
|
ax.set_prop_cycle(plt.rcParams["axes.prop_cycle"])
|
270
289
|
|
271
290
|
|
291
|
+
@contextlib.contextmanager
|
292
|
+
def context(
|
293
|
+
colors: list[str] | None = None,
|
294
|
+
line_width: float | None = None,
|
295
|
+
) -> Generator[None, None, None]:
|
296
|
+
"""Context manager to set the defaults for plots.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
colors: colors to use for the plot.
|
300
|
+
line_width: line width to use for the plot.
|
301
|
+
|
302
|
+
"""
|
303
|
+
rc = {}
|
304
|
+
|
305
|
+
if colors is not None:
|
306
|
+
rc["axes.prop_cycle"] = cycler(color=colors)
|
307
|
+
|
308
|
+
if line_width is not None:
|
309
|
+
rc["lines.linewidth"] = line_width
|
310
|
+
|
311
|
+
with plt.rc_context(rc):
|
312
|
+
yield
|
313
|
+
|
314
|
+
|
272
315
|
##########################################################################
|
273
316
|
# General plot layout
|
274
317
|
##########################################################################
|
mxlpy/report.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
"""Generate a report comparing two models."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from datetime import UTC, datetime
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import cast
|
7
|
+
|
8
|
+
import sympy
|
9
|
+
|
10
|
+
from mxlpy.meta.source_tools import fn_to_sympy
|
11
|
+
from mxlpy.model import Model
|
12
|
+
|
13
|
+
__all__ = ["AnalysisFn", "markdown"]
|
14
|
+
|
15
|
+
type AnalysisFn = Callable[[Model, Model, Path], tuple[str, Path]]
|
16
|
+
|
17
|
+
|
18
|
+
def _list_of_symbols(args: list[str]) -> list[sympy.Symbol | sympy.Expr]:
|
19
|
+
return [sympy.Symbol(arg) for arg in args]
|
20
|
+
|
21
|
+
|
22
|
+
def _new_removed_changed[T](
|
23
|
+
d1: dict[str, T], d2: dict[str, T]
|
24
|
+
) -> tuple[dict[str, T], list[str], dict[str, tuple[T, T]]]:
|
25
|
+
s1 = set(d1)
|
26
|
+
s2 = set(d2)
|
27
|
+
|
28
|
+
removed = sorted(s1 - s2)
|
29
|
+
new = {k: d2[k] for k in s2 - s1}
|
30
|
+
changed = {k: (v1, v2) for k in s1 - set(removed) if (v1 := d1[k]) != (v2 := d2[k])}
|
31
|
+
return new, removed, changed
|
32
|
+
|
33
|
+
|
34
|
+
def markdown(
|
35
|
+
m1: Model,
|
36
|
+
m2: Model,
|
37
|
+
analyses: list[AnalysisFn] | None = None,
|
38
|
+
rel_change: float = 1e-2,
|
39
|
+
img_path: Path = Path(),
|
40
|
+
) -> str:
|
41
|
+
"""Generate a markdown report comparing two models.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
m1: The first model to compare.
|
45
|
+
m2: The second model to compare.
|
46
|
+
analyses: A list of functions that take a Path and return a tuple of a string and a Path. Defaults to None.
|
47
|
+
rel_change: The relative change threshold for numerical differences. Defaults to 1e-2.
|
48
|
+
img_path: The path to save images. Defaults to Path().
|
49
|
+
|
50
|
+
"""
|
51
|
+
content: list[str] = [
|
52
|
+
f"# Report: {datetime.now(UTC).strftime('%Y-%m-%d')}",
|
53
|
+
]
|
54
|
+
|
55
|
+
# Variables
|
56
|
+
new_variables, removed_variables, changed_variables = _new_removed_changed(
|
57
|
+
m1.variables, m2.variables
|
58
|
+
)
|
59
|
+
variables = []
|
60
|
+
variables.extend(
|
61
|
+
f"| <span style='color:green'>{k}<span> | - | {v} |"
|
62
|
+
for k, v in new_variables.items()
|
63
|
+
)
|
64
|
+
variables.extend(
|
65
|
+
f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
|
66
|
+
for k, (v1, v2) in changed_variables.items()
|
67
|
+
)
|
68
|
+
variables.extend(
|
69
|
+
f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_variables
|
70
|
+
)
|
71
|
+
if len(variables) >= 1:
|
72
|
+
content.extend(
|
73
|
+
(
|
74
|
+
"## Variables\n",
|
75
|
+
"| Name | Old Value | New Value |",
|
76
|
+
"| ---- | --------- | --------- |",
|
77
|
+
)
|
78
|
+
)
|
79
|
+
content.append("\n".join(variables))
|
80
|
+
|
81
|
+
# Parameters
|
82
|
+
new_parameters, removed_parameters, changed_parameters = _new_removed_changed(
|
83
|
+
m1.parameters, m2.parameters
|
84
|
+
)
|
85
|
+
pars = []
|
86
|
+
pars.extend(
|
87
|
+
f"| <span style='color:green'>{k}<span> | - | {v} |"
|
88
|
+
for k, v in new_parameters.items()
|
89
|
+
)
|
90
|
+
pars.extend(
|
91
|
+
f"| <span style='color: orange'>{k}</span> | {v1} | {v2} |"
|
92
|
+
for k, (v1, v2) in changed_parameters.items()
|
93
|
+
)
|
94
|
+
pars.extend(
|
95
|
+
f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_parameters
|
96
|
+
)
|
97
|
+
if len(pars) >= 1:
|
98
|
+
content.extend(
|
99
|
+
(
|
100
|
+
"## Parameters\n",
|
101
|
+
"| Name | Old Value | New Value |",
|
102
|
+
"| ---- | --------- | --------- |",
|
103
|
+
)
|
104
|
+
)
|
105
|
+
content.append("\n".join(pars))
|
106
|
+
|
107
|
+
# Derived
|
108
|
+
new_derived, removed_derived, changed_derived = _new_removed_changed(
|
109
|
+
m1.derived, m2.derived
|
110
|
+
)
|
111
|
+
derived = []
|
112
|
+
for k, v in new_derived.items():
|
113
|
+
expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
|
114
|
+
derived.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
|
115
|
+
for k, (v1, v2) in changed_derived.items():
|
116
|
+
expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
|
117
|
+
expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
|
118
|
+
derived.append(
|
119
|
+
f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
|
120
|
+
)
|
121
|
+
derived.extend(
|
122
|
+
f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_derived
|
123
|
+
)
|
124
|
+
if len(derived) >= 1:
|
125
|
+
content.extend(
|
126
|
+
(
|
127
|
+
"## Derived\n",
|
128
|
+
"| Name | Old Value | New Value |",
|
129
|
+
"| ---- | --------- | --------- |",
|
130
|
+
)
|
131
|
+
)
|
132
|
+
content.append("\n".join(derived))
|
133
|
+
|
134
|
+
# Reactions
|
135
|
+
new_reactions, removed_reactions, changed_reactions = _new_removed_changed(
|
136
|
+
m1.reactions, m2.reactions
|
137
|
+
)
|
138
|
+
reactions = []
|
139
|
+
for k, v in new_reactions.items():
|
140
|
+
expr = sympy.latex(fn_to_sympy(v.fn, _list_of_symbols(v.args)))
|
141
|
+
reactions.append(f"| <span style='color:green'>{k}<span> | - | ${expr}$ |")
|
142
|
+
for k, (v1, v2) in changed_reactions.items():
|
143
|
+
expr1 = sympy.latex(fn_to_sympy(v1.fn, _list_of_symbols(v1.args)))
|
144
|
+
expr2 = sympy.latex(fn_to_sympy(v2.fn, _list_of_symbols(v2.args)))
|
145
|
+
reactions.append(
|
146
|
+
f"| <span style='color: orange'>{k}</span> | ${expr1}$ | ${expr2}$ |"
|
147
|
+
)
|
148
|
+
reactions.extend(
|
149
|
+
f"| <span style='color:red'>{k}</span> | - | - |" for k in removed_reactions
|
150
|
+
)
|
151
|
+
|
152
|
+
if len(reactions) >= 1:
|
153
|
+
content.extend(
|
154
|
+
(
|
155
|
+
"## Reactions\n",
|
156
|
+
"| Name | Old Value | New Value |",
|
157
|
+
"| ---- | --------- | --------- |",
|
158
|
+
)
|
159
|
+
)
|
160
|
+
content.append("\n".join(reactions))
|
161
|
+
|
162
|
+
# Now check for any numerical differences
|
163
|
+
dependent = []
|
164
|
+
d1 = m1.get_dependent()
|
165
|
+
d2 = m2.get_dependent()
|
166
|
+
rel_diff = ((d1 - d2) / d1).dropna()
|
167
|
+
for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
|
168
|
+
k = cast(str, k)
|
169
|
+
dependent.append(
|
170
|
+
f"| <span style='color:orange'>{k}</span> | {d1[k]:.2f} | {d2[k]:.2f} | {v:.1%} "
|
171
|
+
)
|
172
|
+
if len(dependent) >= 1:
|
173
|
+
content.extend(
|
174
|
+
(
|
175
|
+
"## Numerical differences of dependent values\n",
|
176
|
+
"| Name | Old Value | New Value | Relative Change | ",
|
177
|
+
"| ---- | --------- | --------- | --------------- | ",
|
178
|
+
)
|
179
|
+
)
|
180
|
+
content.append("\n".join(dependent))
|
181
|
+
|
182
|
+
rhs = []
|
183
|
+
r1 = m1.get_right_hand_side()
|
184
|
+
r2 = m2.get_right_hand_side()
|
185
|
+
rel_diff = ((r1 - r2) / r1).dropna()
|
186
|
+
for k, v in rel_diff.loc[rel_diff.abs() >= rel_change].items():
|
187
|
+
k = cast(str, k)
|
188
|
+
rhs.append(
|
189
|
+
f"| <span style='color:orange'>{k}</span> | {r1[k]:.2f} | {r2[k]:.2f} | {v:.1%} "
|
190
|
+
)
|
191
|
+
if len(rhs) >= 1:
|
192
|
+
content.extend(
|
193
|
+
(
|
194
|
+
"## Numerical differences of right hand side values\n",
|
195
|
+
"| Name | Old Value | New Value | Relative Change | ",
|
196
|
+
"| ---- | --------- | --------- | --------------- | ",
|
197
|
+
)
|
198
|
+
)
|
199
|
+
content.append("\n".join(rhs))
|
200
|
+
|
201
|
+
# Comparison functions
|
202
|
+
if analyses is not None:
|
203
|
+
for f in analyses:
|
204
|
+
name, img_path = f(m1, m2, img_path)
|
205
|
+
content.append(name)
|
206
|
+
# content.append(f"")
|
207
|
+
content.append(f"<img src='{img_path}' alt='{name}' width='500'/>")
|
208
|
+
|
209
|
+
return "\n".join(content)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mxlpy
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: A package to build metabolic models
|
5
5
|
Author-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
|
6
6
|
Maintainer-email: Marvin van Aalst <marvin.vanaalst@gmail.com>
|
@@ -59,6 +59,8 @@ Provides-Extra: torch
|
|
59
59
|
Requires-Dist: torch>=2.5.1; extra == 'torch'
|
60
60
|
Description-Content-Type: text/markdown
|
61
61
|
|
62
|
+
<img src="docs/assets/logo-diagram.png" style="display: block; max-height: 30rem; margin: auto; padding: 0" alt='mxlpy-logo'>
|
63
|
+
|
62
64
|
# mxlpy
|
63
65
|
|
64
66
|
[](https://pypi.python.org/pypi/mxlpy)
|
@@ -84,6 +86,13 @@ pixi add python assimulo
|
|
84
86
|
pixi add --pypi mxlpy[torch]
|
85
87
|
```
|
86
88
|
|
89
|
+
## How to cite
|
90
|
+
|
91
|
+
If you use this software in your scientific work, please cite [this article](...):
|
92
|
+
|
93
|
+
- [doi](https://doi.org/)
|
94
|
+
- [bibtex file](https://fillme.out)
|
95
|
+
|
87
96
|
|
88
97
|
## Development setup
|
89
98
|
|
@@ -1,7 +1,8 @@
|
|
1
|
-
mxlpy/__init__.py,sha256=
|
1
|
+
mxlpy/__init__.py,sha256=XZYNFyDC5rWcKi6139mq04cROI7LwJvxB2_3ApKwcvY,4194
|
2
2
|
mxlpy/distributions.py,sha256=ce6RTqn19YzMMec-u09fSIUA8A92M6rehCuHuXWcX7A,8734
|
3
|
-
mxlpy/fit.py,sha256=
|
3
|
+
mxlpy/fit.py,sha256=vJ0AWCvERxPkxgwuOmL9rsH4vXnlBSco4vG-5X98RK8,8085
|
4
4
|
mxlpy/fns.py,sha256=ct_RFj9koW8vXHyr27GnbZUHUS_zfs4rDysybuFiOaU,4599
|
5
|
+
mxlpy/identify.py,sha256=af52SCG4nlY9sSw22goaIheuvXR09QYK4ksCT24QHWI,1946
|
5
6
|
mxlpy/label_map.py,sha256=urv-QTb0MUEKjwWvKtJSB8H2kvhLn1EKfRIH7awQQ8Y,17769
|
6
7
|
mxlpy/linear_label_map.py,sha256=2lgERcUVDLXruRI08HBYJo_wK654y46voLUeBTzBy3k,10312
|
7
8
|
mxlpy/mc.py,sha256=GIuJJ-9QRqGsd2xl1LmjmMc-bOdihVShbFmXvu4o5p4,17305
|
@@ -11,8 +12,9 @@ mxlpy/npe.py,sha256=oiRLA43-qf-AcS2KpQfJIOt7-Ev9Aj5sF6TMq9bJn84,8747
|
|
11
12
|
mxlpy/parallel.py,sha256=kX4Td5YoovDwZp6kX_3cfO6QtHSS9ieJ0bMZiKs3Xv8,5002
|
12
13
|
mxlpy/parameterise.py,sha256=2jMhhO-bHTFP_0kXercJekeATAZYBg5FrK1MQ_mWGpk,654
|
13
14
|
mxlpy/paths.py,sha256=TK2wO4N9lG-UV1JGfeB64q48JVDbwqIUj63rl55MKuQ,1022
|
14
|
-
mxlpy/plot.py,sha256=
|
15
|
+
mxlpy/plot.py,sha256=z1JW7Si1JQyNMj_MMLkgbLkOkSjVcfAZJGjm_WqCgT4,24355
|
15
16
|
mxlpy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
+
mxlpy/report.py,sha256=TYG-cKo9u6izlnjB4vgu6I2hYrtS9e8wYc_A4MrHq-g,6982
|
16
18
|
mxlpy/scan.py,sha256=3k4084d4eg-3Ok24cL8KWPF7P1L6fbEtiaOM9VC5Eko,19210
|
17
19
|
mxlpy/simulator.py,sha256=T9t2jZ6U5NyK1ICF1UkST8M8v4EPV_H98kzZ4TvQK-w,20115
|
18
20
|
mxlpy/types.py,sha256=ksdn76Sdw0XhQEpQepcETvuGqcJolfrmbIRBT0R_2Bg,13612
|
@@ -42,7 +44,7 @@ mxlpy/surrogates/_torch.py,sha256=E_1eDUlPSVFwROkdMDCqYwwHE-61pjNMJWotnhjzge0,58
|
|
42
44
|
mxlpy/symbolic/__init__.py,sha256=3hQjCMw8-6iOxeUdfnCg8449fF_BRF2u6lCM1GPpkRY,222
|
43
45
|
mxlpy/symbolic/strikepy.py,sha256=r6nRtckV1nxKq3i1bYYWZOkzwZ5XeKQuZM5ck44vUo0,20010
|
44
46
|
mxlpy/symbolic/symbolic_model.py,sha256=YL9noEeP3_0DoKXwMPELtfmPuP6mgNcLIJgDRCkyB7A,2434
|
45
|
-
mxlpy-0.
|
46
|
-
mxlpy-0.
|
47
|
-
mxlpy-0.
|
48
|
-
mxlpy-0.
|
47
|
+
mxlpy-0.11.0.dist-info/METADATA,sha256=N1-dMC1k0QUlSF2ZN_URFUyg119f9QUz7xcBS2Ia_PQ,4536
|
48
|
+
mxlpy-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
49
|
+
mxlpy-0.11.0.dist-info/licenses/LICENSE,sha256=bEzjyjy1stQhfRDVaVHa3xV1x-V8emwdlbMvYO8Zo84,35073
|
50
|
+
mxlpy-0.11.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|