scistackplot 0.1.26__tar.gz → 0.1.28__tar.gz
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.
- {scistackplot-0.1.26 → scistackplot-0.1.28}/.gitignore +4 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/PKG-INFO +2 -1
- {scistackplot-0.1.26 → scistackplot-0.1.28}/pyproject.toml +1 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/__init__.py +2 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/capability.py +5 -2
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/codegen.py +57 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/reduce.py +57 -2
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/spec.py +90 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/README.md +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/groups.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/render/__init__.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/render/base.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/render/mpl.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/render/plotly_.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/resolved.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/roles.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/shape.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/sources/__init__.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/sources/base.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/sources/csv.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/sources/frame.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/table.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/variants.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/xaxis.py +0 -0
- {scistackplot-0.1.26 → scistackplot-0.1.28}/src/scistackplot/ylimits.py +0 -0
|
@@ -20,6 +20,10 @@ __pycache__/
|
|
|
20
20
|
*.duckdb.wal
|
|
21
21
|
*.wal
|
|
22
22
|
scistack-gui/frontend/node_modules/
|
|
23
|
+
# Compiled output of the frontend's React-free unit tests (npm test in
|
|
24
|
+
# scistack-gui/frontend). Regenerated by `tsc -p tsconfig.test.json`; unlike
|
|
25
|
+
# extension/dist/, nothing loads it at runtime.
|
|
26
|
+
scistack-gui/frontend/dist/
|
|
23
27
|
# mkdocs build output (regenerated by `mkdocs build`)
|
|
24
28
|
/site/
|
|
25
29
|
# Runtime output of code_export_service (pipeline-to-code export) — timestamped
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scistackplot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.28
|
|
4
4
|
Summary: Spec-driven plotting for long-format scientific data — standalone, GUI-friendly
|
|
5
5
|
Author: SciStack Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
17
18
|
Classifier: Typing :: Typed
|
|
18
19
|
Requires-Python: >=3.10
|
|
@@ -32,6 +32,7 @@ classifiers = [
|
|
|
32
32
|
"Programming Language :: Python :: 3.10",
|
|
33
33
|
"Programming Language :: Python :: 3.11",
|
|
34
34
|
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
35
36
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
36
37
|
"Typing :: Typed",
|
|
37
38
|
]
|
|
@@ -68,6 +68,7 @@ from .spec import (
|
|
|
68
68
|
FacetOptions,
|
|
69
69
|
Filter,
|
|
70
70
|
LevelGroup,
|
|
71
|
+
LocationFilter,
|
|
71
72
|
MatchOp,
|
|
72
73
|
Matcher,
|
|
73
74
|
PlotKind,
|
|
@@ -122,6 +123,7 @@ __all__ = [
|
|
|
122
123
|
"StyleOptions",
|
|
123
124
|
"Filter",
|
|
124
125
|
"LevelGroup",
|
|
126
|
+
"LocationFilter",
|
|
125
127
|
"VariantSet",
|
|
126
128
|
# data
|
|
127
129
|
"LongTable",
|
|
@@ -402,9 +402,12 @@ def factor_summary(spec: PlotSpec, derived: LongTable) -> list[dict]:
|
|
|
402
402
|
entry["x_available"] = on_x["available"]
|
|
403
403
|
entry["x_reason"] = on_x["reason"]
|
|
404
404
|
|
|
405
|
-
if not spec.filters:
|
|
405
|
+
if not spec.filters and spec.location_filter.is_empty():
|
|
406
406
|
# Nothing filtered: everything is selected, and no frame scan is needed
|
|
407
|
-
# on the common path.
|
|
407
|
+
# on the common path. The location filter has to be checked here too —
|
|
408
|
+
# it narrows rows exactly as a Filter does, and a fast path that only
|
|
409
|
+
# knew about one of them would report "all 12 selected" beside a figure
|
|
410
|
+
# drawing 3, which is the precise failure this readout exists to avoid.
|
|
408
411
|
for entry in factors:
|
|
409
412
|
entry["selected"] = list(entry["levels"])
|
|
410
413
|
return factors
|
|
@@ -352,6 +352,14 @@ def _preamble(spec, table, roles, shape) -> list[str]:
|
|
|
352
352
|
]
|
|
353
353
|
)
|
|
354
354
|
|
|
355
|
+
# Schema locations. Emitted as one vectorised comparison per (prefix, key),
|
|
356
|
+
# which is the SAME rule reduce._location_mask applies — a ragged selection
|
|
357
|
+
# cannot be expressed as `for_each(subject=[...], trial=[...])`, whose keys
|
|
358
|
+
# cross-product, so it has to be a mask inside the function body. The
|
|
359
|
+
# `if _k in df.columns` guard is not defensive clutter: it is how a key the
|
|
360
|
+
# frame lacks goes unconstrained, matching the interactive path exactly.
|
|
361
|
+
lines.extend(_location_lines(spec))
|
|
362
|
+
|
|
355
363
|
filter_lines: list[str] = []
|
|
356
364
|
for flt in spec.filters:
|
|
357
365
|
if flt.include is not None:
|
|
@@ -467,6 +475,32 @@ def _preamble(spec, table, roles, shape) -> list[str]:
|
|
|
467
475
|
return lines
|
|
468
476
|
|
|
469
477
|
|
|
478
|
+
def _location_lines(spec: PlotSpec) -> list[str]:
|
|
479
|
+
"""Generated pandas for ``spec.location_filter`` — empty when it is inert.
|
|
480
|
+
|
|
481
|
+
Kept beside the other preamble emitters rather than inlined so the one test
|
|
482
|
+
that matters can compare its output against
|
|
483
|
+
:func:`scistackplot.reduce._location_mask` on the same frame.
|
|
484
|
+
"""
|
|
485
|
+
prefixes = spec.location_filter.prefixes()
|
|
486
|
+
if not prefixes:
|
|
487
|
+
return []
|
|
488
|
+
plural = "" if len(prefixes) == 1 else "s"
|
|
489
|
+
return [
|
|
490
|
+
f"# schema locations: {len(prefixes)} selection{plural}",
|
|
491
|
+
f"_loc_prefixes = {[[list(pair) for pair in p] for p in prefixes]!r}",
|
|
492
|
+
"_loc_mask = pd.Series(False, index=df.index)",
|
|
493
|
+
"for _p in _loc_prefixes:",
|
|
494
|
+
" _m = pd.Series(True, index=df.index)",
|
|
495
|
+
" for _k, _v in _p:",
|
|
496
|
+
" if _k in df.columns:",
|
|
497
|
+
" _m &= df[_k].astype(str) == _v",
|
|
498
|
+
" _loc_mask |= _m",
|
|
499
|
+
"df = df[_loc_mask]",
|
|
500
|
+
"",
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
|
|
470
504
|
def _facet_layout_args(spec, table: LongTable, facets: list[str]) -> list[str]:
|
|
471
505
|
"""
|
|
472
506
|
seaborn arguments that reproduce the interactive facet arrangement.
|
|
@@ -754,6 +788,7 @@ def _color_level_count(spec: PlotSpec, table: LongTable, color: str) -> int:
|
|
|
754
788
|
levels = [str(level) for level in table.factor(color).levels]
|
|
755
789
|
except KeyError:
|
|
756
790
|
return 2
|
|
791
|
+
levels = _levels_after_location(spec, color, levels)
|
|
757
792
|
for flt in spec.filters:
|
|
758
793
|
if flt.column != color:
|
|
759
794
|
continue
|
|
@@ -766,6 +801,28 @@ def _color_level_count(spec: PlotSpec, table: LongTable, color: str) -> int:
|
|
|
766
801
|
return len(levels)
|
|
767
802
|
|
|
768
803
|
|
|
804
|
+
def _levels_after_location(spec: PlotSpec, column: str, levels: list[str]) -> list[str]:
|
|
805
|
+
"""``levels`` narrowed by ``spec.location_filter``, for one schema key.
|
|
806
|
+
|
|
807
|
+
The rule follows from prefixes constraining only the keys they name: a
|
|
808
|
+
prefix that never mentions this column says nothing about it, so if any
|
|
809
|
+
such prefix is selected the column keeps every level. Only when *every*
|
|
810
|
+
prefix names it do the values they give become the surviving set.
|
|
811
|
+
|
|
812
|
+
Concretely, selecting all of subject 01 plus subject 02's trial 3 leaves
|
|
813
|
+
``trial`` with all its levels — because "all of subject 01" did not name a
|
|
814
|
+
trial — which is what the figure will actually draw.
|
|
815
|
+
"""
|
|
816
|
+
prefixes = spec.location_filter.prefixes()
|
|
817
|
+
if not prefixes:
|
|
818
|
+
return levels
|
|
819
|
+
named = [dict(prefix) for prefix in prefixes]
|
|
820
|
+
if any(column not in prefix for prefix in named):
|
|
821
|
+
return levels
|
|
822
|
+
keep = {prefix[column] for prefix in named}
|
|
823
|
+
return [level for level in levels if level in keep]
|
|
824
|
+
|
|
825
|
+
|
|
769
826
|
#: Column the generated code builds for a nested x axis.
|
|
770
827
|
_X_NESTED = "_x"
|
|
771
828
|
|
|
@@ -504,8 +504,53 @@ def _fanout_notes(spec: PlotSpec, table: LongTable) -> list[str]:
|
|
|
504
504
|
# ---------------------------------------------------------------------------
|
|
505
505
|
|
|
506
506
|
|
|
507
|
+
def _location_mask(frame: pd.DataFrame, spec: PlotSpec) -> "pd.Series | None":
|
|
508
|
+
"""Rows matching any prefix in ``spec.location_filter``, or None if inert.
|
|
509
|
+
|
|
510
|
+
One prefix constrains only the keys it names **that the frame actually
|
|
511
|
+
has**. A key the frame lacks is simply not constrained — the graceful answer
|
|
512
|
+
for a shallower variable (a subject-level Mass has one value for "subject 02
|
|
513
|
+
trial 3", and that is the value that contributes).
|
|
514
|
+
|
|
515
|
+
Each prefix carries its own keys, so a non-contiguous location
|
|
516
|
+
(``subject`` + ``speed``, ``timepoint`` NULL) matches the keys it names
|
|
517
|
+
rather than whatever sits at that position in the schema — see
|
|
518
|
+
:class:`~scistackplot.spec.LocationFilter`.
|
|
519
|
+
|
|
520
|
+
Written as one vectorised comparison per (prefix, key) rather than a row-wise
|
|
521
|
+
tuple match, because :func:`scistackplot.codegen` has to emit the *same*
|
|
522
|
+
rule as readable pandas and the two must not be able to disagree. That makes
|
|
523
|
+
the cost O(prefixes x depth) column comparisons; a single-select is one
|
|
524
|
+
prefix, and a ticked subtree collapses to its own short prefix, so the
|
|
525
|
+
common cases are small by construction.
|
|
526
|
+
"""
|
|
527
|
+
prefixes = spec.location_filter.prefixes()
|
|
528
|
+
if not prefixes:
|
|
529
|
+
return None # inert: clicking into a picker is not a statement
|
|
530
|
+
|
|
531
|
+
named = {key for prefix in prefixes for key, _value in prefix}
|
|
532
|
+
known = [key for key in named if key in frame.columns]
|
|
533
|
+
if not known:
|
|
534
|
+
Log.warn(
|
|
535
|
+
"location filter names none of this table's columns (%s) — ignored",
|
|
536
|
+
", ".join(sorted(named)),
|
|
537
|
+
layer=LAYER,
|
|
538
|
+
)
|
|
539
|
+
return None
|
|
540
|
+
|
|
541
|
+
as_text = {key: frame[key].astype(str) for key in known}
|
|
542
|
+
mask = pd.Series(False, index=frame.index)
|
|
543
|
+
for prefix in prefixes:
|
|
544
|
+
matched = pd.Series(True, index=frame.index)
|
|
545
|
+
for key, value in prefix:
|
|
546
|
+
if key in as_text:
|
|
547
|
+
matched &= as_text[key] == str(value)
|
|
548
|
+
mask |= matched
|
|
549
|
+
return mask
|
|
550
|
+
|
|
551
|
+
|
|
507
552
|
def apply_filters(frame: pd.DataFrame, spec: PlotSpec) -> pd.DataFrame:
|
|
508
|
-
"""Rows surviving ``spec.filters``.
|
|
553
|
+
"""Rows surviving ``spec.filters`` and ``spec.location_filter``.
|
|
509
554
|
|
|
510
555
|
Public because the GUI's pickers report "3 of 12 selected" and that readout
|
|
511
556
|
has to be measured with exactly the rule the figure uses — the same reason
|
|
@@ -517,9 +562,19 @@ def apply_filters(frame: pd.DataFrame, spec: PlotSpec) -> pd.DataFrame:
|
|
|
517
562
|
or ``1`` (int) depending on the source, and a silently empty figure is the
|
|
518
563
|
worst possible answer to a picker the user just clicked.
|
|
519
564
|
"""
|
|
520
|
-
|
|
565
|
+
location = _location_mask(frame, spec)
|
|
566
|
+
if not spec.filters and location is None:
|
|
521
567
|
return frame
|
|
522
568
|
mask = pd.Series(True, index=frame.index)
|
|
569
|
+
if location is not None:
|
|
570
|
+
mask &= location
|
|
571
|
+
Log.debug(
|
|
572
|
+
"location filter: %d -> %d row(s) over %d prefix(es)",
|
|
573
|
+
len(frame),
|
|
574
|
+
int(mask.sum()),
|
|
575
|
+
len(spec.location_filter.prefixes()),
|
|
576
|
+
layer=LAYER,
|
|
577
|
+
)
|
|
523
578
|
for flt in spec.filters:
|
|
524
579
|
if flt.column not in frame.columns:
|
|
525
580
|
# A spec outlives the table it was written against — a filter naming
|
|
@@ -187,6 +187,90 @@ class Filter:
|
|
|
187
187
|
maximum: float | None = None
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class LocationFilter:
|
|
192
|
+
"""Which schema locations to draw, as a set of hierarchy **prefixes**.
|
|
193
|
+
|
|
194
|
+
The difference from :class:`Filter` is the whole reason this exists.
|
|
195
|
+
``Filter`` holds one include-list per column, so a set of them can only ever
|
|
196
|
+
express a **Cartesian product**: ``subject ∈ {01,02} × trial ∈ {3,7}``. That
|
|
197
|
+
is the right shape for "narrow this factor", and the wrong shape for a
|
|
198
|
+
hierarchical location picker, where the user ticks boxes on a tree and means
|
|
199
|
+
something ragged — *all* of subject 01, plus only trials 1–3 of subject 02.
|
|
200
|
+
Real datasets are ragged (subject 01 ran eight trials, subject 02 ran five),
|
|
201
|
+
so the Cartesian form is not merely less expressive, it names combinations
|
|
202
|
+
that never existed.
|
|
203
|
+
|
|
204
|
+
Worse, in a tree UI the per-column form is actively misleading: unticking
|
|
205
|
+
``S01 / trial 3`` would silently drop trial 3 from *every* subject, because
|
|
206
|
+
there is only one ``trial`` include-list to edit.
|
|
207
|
+
|
|
208
|
+
``include`` is a list of prefixes, each a list of ``[key, value]`` pairs
|
|
209
|
+
outermost first: ``[["subject", "01"]]`` is all of subject 01,
|
|
210
|
+
``[["subject", "02"], ["trial", "3"]]`` is one trial. Storing the **minimal
|
|
211
|
+
covering set** (collapse a fully-ticked parent to its own short prefix) is
|
|
212
|
+
what keeps a later-added trial inside an already-selected subject instead of
|
|
213
|
+
silently outside it.
|
|
214
|
+
|
|
215
|
+
**A prefix names its keys rather than relying on position**, and that is not
|
|
216
|
+
verbosity. A dataset schema is a linear hierarchy but a *saved location* need
|
|
217
|
+
not fill it: a cross-cutting result saved at ``subject`` + ``speed`` with
|
|
218
|
+
``timepoint`` NULL is a supported, documented shape
|
|
219
|
+
(docs/claude/schema-hierarchy-contiguity.md), and its location is
|
|
220
|
+
``[["subject","01"], ["speed","SSV"]]``. Read positionally against the
|
|
221
|
+
schema, that second value would be matched against ``timepoint`` — selecting
|
|
222
|
+
nothing, in silence. Naming the key makes the hole impossible to
|
|
223
|
+
misinterpret, and it means the picker sends a node's own path back verbatim
|
|
224
|
+
with no translation step to get wrong.
|
|
225
|
+
|
|
226
|
+
An empty ``include`` is **inert**: it constrains nothing, matching the rule
|
|
227
|
+
an unfilled variant row follows (docs/claude/plot-variant-rows.md §3).
|
|
228
|
+
Clicking into a picker is not a statement about the data.
|
|
229
|
+
|
|
230
|
+
Values are compared as text, like every other selection that crosses JSON.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
#: Schema keys, outermost first. Display order only — matching reads the
|
|
234
|
+
#: keys named inside each prefix, never this list's positions.
|
|
235
|
+
keys: list[str] = field(default_factory=list)
|
|
236
|
+
#: Prefixes to keep. Lists rather than tuples so JSON and TOML round-trip
|
|
237
|
+
#: with no conversion; :meth:`prefixes` is the tuple view for matching.
|
|
238
|
+
include: list[list[list[str]]] = field(default_factory=list)
|
|
239
|
+
|
|
240
|
+
def prefixes(self) -> list[tuple[tuple[str, str], ...]]:
|
|
241
|
+
"""``include`` as tuples of ``(key, value)`` text pairs, empties dropped."""
|
|
242
|
+
out = []
|
|
243
|
+
for entry in self.include:
|
|
244
|
+
prefix = tuple(
|
|
245
|
+
(str(pair[0]), str(pair[1])) for pair in entry if len(pair) == 2
|
|
246
|
+
)
|
|
247
|
+
if prefix:
|
|
248
|
+
out.append(prefix)
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
def is_empty(self) -> bool:
|
|
252
|
+
return not self.prefixes()
|
|
253
|
+
|
|
254
|
+
def to_dict(self) -> dict:
|
|
255
|
+
return {
|
|
256
|
+
"keys": list(self.keys),
|
|
257
|
+
"include": [
|
|
258
|
+
[[str(pair[0]), str(pair[1])] for pair in entry if len(pair) == 2]
|
|
259
|
+
for entry in self.include
|
|
260
|
+
],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def from_dict(cls, raw: dict) -> "LocationFilter":
|
|
265
|
+
return cls(
|
|
266
|
+
keys=list(raw.get("keys") or []),
|
|
267
|
+
include=[
|
|
268
|
+
[[str(pair[0]), str(pair[1])] for pair in entry if len(pair) == 2]
|
|
269
|
+
for entry in (raw.get("include") or [])
|
|
270
|
+
],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
190
274
|
@dataclass(frozen=True)
|
|
191
275
|
class LevelGroup:
|
|
192
276
|
"""A factor derived by bucketing another factor's levels.
|
|
@@ -468,6 +552,10 @@ class PlotSpec:
|
|
|
468
552
|
y_axis: YAxis = field(default_factory=YAxis)
|
|
469
553
|
style: StyleOptions = field(default_factory=StyleOptions)
|
|
470
554
|
filters: list[Filter] = field(default_factory=list)
|
|
555
|
+
#: Which schema locations to draw. Written by the schema location picker,
|
|
556
|
+
#: which REPLACED the flat per-key pickers — see :class:`LocationFilter` for
|
|
557
|
+
#: why a set of ``Filter``s cannot express the same selection.
|
|
558
|
+
location_filter: LocationFilter = field(default_factory=LocationFilter)
|
|
471
559
|
#: Variables joined in as FACTORS rather than plotted — a subject-level
|
|
472
560
|
#: ``Condition`` holding stim/sham, say. They classify as CATEGORICAL and so
|
|
473
561
|
#: are rightly refused as measures; as factors they take a role like any
|
|
@@ -569,6 +657,7 @@ class PlotSpec:
|
|
|
569
657
|
"cols": [_matcher_to_dict(m) for m in self.facet.cols],
|
|
570
658
|
}
|
|
571
659
|
raw["y_axis"] = self.y_axis.to_dict()
|
|
660
|
+
raw["location_filter"] = self.location_filter.to_dict()
|
|
572
661
|
raw["variant_sets"] = [s.to_dict() for s in self.variant_sets]
|
|
573
662
|
raw["level_groups"] = [g.to_dict() for g in self.level_groups]
|
|
574
663
|
raw["x_layers"] = list(self.x_layers)
|
|
@@ -593,6 +682,7 @@ class PlotSpec:
|
|
|
593
682
|
y_axis=YAxis.from_dict(raw.get("y_axis") or {}),
|
|
594
683
|
style=StyleOptions(**(raw.get("style") or {})),
|
|
595
684
|
filters=[Filter(**f) for f in (raw.get("filters") or [])],
|
|
685
|
+
location_filter=LocationFilter.from_dict(raw.get("location_filter") or {}),
|
|
596
686
|
factor_variables=list(raw.get("factor_variables") or []),
|
|
597
687
|
level_groups=[
|
|
598
688
|
LevelGroup.from_dict(g) for g in (raw.get("level_groups") or [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|