dpd-stream 0.2.0__tar.gz → 0.2.1__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.
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/CHANGELOG.md +11 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/CITATION.cff +2 -2
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/PKG-INFO +3 -2
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/__init__.py +1 -1
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/store.py +9 -4
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream.egg-info/PKG-INFO +3 -2
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream.egg-info/SOURCES.txt +1 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream.egg-info/requires.txt +2 -1
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/pyproject.toml +5 -2
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_features.py +14 -0
- dpd_stream-0.2.1/tests/test_resources.py +96 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/LICENSE.md +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/MANIFEST.in +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/README.md +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_absorb.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_blocks.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_frontend.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_idtests.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_nl.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/_passes.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/api.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/estimator.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/inference.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/__init__.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/cells.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/idtests.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/moments.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/nl.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/kernels/panel.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/layout.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/profile.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/py.typed +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/results.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream/simulate.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream.egg-info/dependency_links.txt +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/dpd_stream.egg-info/top_level.txt +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/setup.cfg +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_absorb.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_headroom.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_idtests.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_index_dtype.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_nl.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_oracle.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_profile.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_published.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_pydynpd.py +0 -0
- {dpd_stream-0.2.0 → dpd_stream-0.2.1}/tests/test_stata.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- **Lower memory reading a panel.** Checking that (panel, time) identifies the observations no
|
|
6
|
+
longer holds a set of every row; repeated times are found as the blocks are written, with the
|
|
7
|
+
same error and the same results. On 9.6 million rows the fit's memory no longer rises to
|
|
8
|
+
1.55 GB at that step, and a fit now stays near 1.1 GB from 10 to 24 million rows.
|
|
9
|
+
- **Memory tests.** `pytest -m resource` fits difference and system GMM on a 24-million-row
|
|
10
|
+
panel under a fixed memory limit; CI runs them on Linux, macOS and Windows. The default
|
|
11
|
+
`pytest` run leaves them out.
|
|
12
|
+
- Requires hdfe-stream 0.9.0 or later, which reads Parquet in less memory.
|
|
13
|
+
|
|
3
14
|
## 0.2.0
|
|
4
15
|
|
|
5
16
|
- **Fit resources (CPUs and memory) are constrained to your job's limits automatically.** On a
|
|
@@ -7,8 +7,8 @@ authors:
|
|
|
7
7
|
- family-names: Tucker
|
|
8
8
|
given-names: Lee
|
|
9
9
|
email: lee.c.tucker@census.gov
|
|
10
|
-
version: 0.2.
|
|
11
|
-
date-released: 2026-
|
|
10
|
+
version: 0.2.1
|
|
11
|
+
date-released: 2026-11-05
|
|
12
12
|
license: MIT
|
|
13
13
|
repository-code: "https://github.com/leetucker/dpd-stream"
|
|
14
14
|
url: "https://github.com/leetucker/dpd-stream"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dpd-stream
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Out-of-core dynamic panel GMM: Arellano-Bond difference GMM and Blundell-Bond system GMM, by streamed passes
|
|
5
5
|
Author: Lee Tucker
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE.md
|
|
28
|
-
Requires-Dist: hdfe-stream[formula]>=0.
|
|
28
|
+
Requires-Dist: hdfe-stream[formula]>=0.9.0
|
|
29
29
|
Requires-Dist: stream-limits>=0.1.1
|
|
30
30
|
Requires-Dist: polars!=1.41.*,>=1.37
|
|
31
31
|
Requires-Dist: numpy>=1.24
|
|
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
|
44
44
|
Requires-Dist: sphinx-design>=0.6; extra == "docs"
|
|
45
45
|
Provides-Extra: test
|
|
46
46
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
47
|
+
Requires-Dist: stream-limits[testing]>=0.2.1; extra == "test"
|
|
47
48
|
Requires-Dist: pandas>=2.0; extra == "test"
|
|
48
49
|
Dynamic: license-file
|
|
49
50
|
|
|
@@ -86,18 +86,18 @@ def build(lf: pl.LazyFrame, bases, panel, *, tdelta=None, cluster=(), weights=No
|
|
|
86
86
|
raise ValueError(f"time variable {tvar!r} must be numeric (integer periods), "
|
|
87
87
|
f"got {schema[tvar]}")
|
|
88
88
|
lf = lf.filter(pl.col(pid).is_not_null() & pl.col(tvar).is_not_null())
|
|
89
|
+
# (repeated times within a panel are found while the blocks are written,
|
|
90
|
+
# below: counting the distinct (panel, time) pairs here would hold a set
|
|
91
|
+
# of every row in memory)
|
|
89
92
|
info = engine.collect(lf.select(
|
|
90
93
|
pl.len().alias("n"), pl.col(tvar).min().alias("tmin"), pl.col(tvar).max().alias("tmax"),
|
|
91
|
-
pl.
|
|
94
|
+
pl.col(pid).n_unique().alias("np"),
|
|
92
95
|
(pl.col(tvar) != pl.col(tvar).round(0)).any().alias("frac")), "panel structure")
|
|
93
96
|
n = int(info["n"][0])
|
|
94
97
|
if n == 0:
|
|
95
98
|
raise ValueError("no observations with a panel and a time")
|
|
96
99
|
if info["frac"][0]:
|
|
97
100
|
raise ValueError(f"time variable {tvar!r} must hold whole numbers")
|
|
98
|
-
if int(info["nu"][0]) != n:
|
|
99
|
-
raise ValueError(f"repeated time values within panel: ({pid}, {tvar}) does not "
|
|
100
|
-
"identify the observations")
|
|
101
101
|
tmin, tmax = int(info["tmin"][0]), int(info["tmax"][0])
|
|
102
102
|
if tdelta is None:
|
|
103
103
|
tdelta = 1
|
|
@@ -194,6 +194,11 @@ def build(lf: pl.LazyFrame, bases, panel, *, tdelta=None, cluster=(), weights=No
|
|
|
194
194
|
frame = frame.sort([pid, _T]) # whole panels: sorted by time within each
|
|
195
195
|
ids = frame[pid]
|
|
196
196
|
starts = (ids != ids.shift(1)).fill_null(True).to_numpy()
|
|
197
|
+
# a block holds whole panels, so a repeated time is two neighbouring rows
|
|
198
|
+
if ((~starts[1:]) & (np.diff(frame[_T].to_numpy()) == 0)).any():
|
|
199
|
+
sample.unlink(missing_ok=True)
|
|
200
|
+
raise ValueError(f"repeated time values within panel: ({pid}, {tvar}) does not "
|
|
201
|
+
"identify the observations")
|
|
197
202
|
pcode = np.cumsum(starts).astype(np.int64) - 1 + code
|
|
198
203
|
code = int(pcode[-1]) + 1
|
|
199
204
|
B = (frame.select(base_names).to_numpy().astype(np.float64)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dpd-stream
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Out-of-core dynamic panel GMM: Arellano-Bond difference GMM and Blundell-Bond system GMM, by streamed passes
|
|
5
5
|
Author: Lee Tucker
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,7 +25,7 @@ Classifier: Typing :: Typed
|
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE.md
|
|
28
|
-
Requires-Dist: hdfe-stream[formula]>=0.
|
|
28
|
+
Requires-Dist: hdfe-stream[formula]>=0.9.0
|
|
29
29
|
Requires-Dist: stream-limits>=0.1.1
|
|
30
30
|
Requires-Dist: polars!=1.41.*,>=1.37
|
|
31
31
|
Requires-Dist: numpy>=1.24
|
|
@@ -44,6 +44,7 @@ Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
|
44
44
|
Requires-Dist: sphinx-design>=0.6; extra == "docs"
|
|
45
45
|
Provides-Extra: test
|
|
46
46
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
47
|
+
Requires-Dist: stream-limits[testing]>=0.2.1; extra == "test"
|
|
47
48
|
Requires-Dist: pandas>=2.0; extra == "test"
|
|
48
49
|
Dynamic: license-file
|
|
49
50
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dpd-stream"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Out-of-core dynamic panel GMM: Arellano-Bond difference GMM and Blundell-Bond system GMM, by streamed passes"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -30,7 +30,7 @@ requires-python = ">=3.10"
|
|
|
30
30
|
dependencies = [
|
|
31
31
|
# the row store, run directories, the formula compiler and the result
|
|
32
32
|
# base classes (hdfe_stream.core's stable names)
|
|
33
|
-
"hdfe-stream[formula]>=0.
|
|
33
|
+
"hdfe-stream[formula]>=0.9.0", # 0.9.0: lower-memory Parquet batch reads (iter_groups)
|
|
34
34
|
"stream-limits>=0.1.1", # thread and memory limits for jobs on a scheduler (0.1.1: Windows memory)
|
|
35
35
|
"polars>=1.37,!=1.41.*",
|
|
36
36
|
"numpy>=1.24",
|
|
@@ -65,6 +65,7 @@ docs = [
|
|
|
65
65
|
]
|
|
66
66
|
test = [
|
|
67
67
|
"pytest>=7.0",
|
|
68
|
+
"stream-limits[testing]>=0.2.1", # resource_runner, for tests/test_resources.py
|
|
68
69
|
"pandas>=2.0",
|
|
69
70
|
]
|
|
70
71
|
|
|
@@ -76,6 +77,8 @@ include = ["dpd_stream*"]
|
|
|
76
77
|
|
|
77
78
|
[tool.pytest.ini_options]
|
|
78
79
|
testpaths = ["tests"]
|
|
80
|
+
# memory tests (tests/test_resources.py) are slow: run them with `pytest -m resource`
|
|
81
|
+
addopts = "-m 'not resource'"
|
|
79
82
|
filterwarnings = [
|
|
80
83
|
"ignore::DeprecationWarning",
|
|
81
84
|
"ignore::FutureWarning",
|
|
@@ -59,6 +59,20 @@ def test_fweights_must_be_constant(panel_data):
|
|
|
59
59
|
verbose=False)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
@pytest.mark.parametrize("where", ["first", "middle", "last"])
|
|
63
|
+
def test_repeated_time_within_a_panel(panel_data, where):
|
|
64
|
+
"""Found while the blocks are written (each holds whole panels, sorted by
|
|
65
|
+
time), in any panel and with the rows in any order; a time shared by two
|
|
66
|
+
panels is not a repeat."""
|
|
67
|
+
ids = panel_data["id"].unique().sort()
|
|
68
|
+
pid = {"first": ids[0], "middle": ids[len(ids) // 2], "last": ids[-1]}[where]
|
|
69
|
+
df = pl.concat([panel_data, panel_data.filter(pl.col("id") == pid).head(1)])
|
|
70
|
+
with pytest.raises(ValueError, match="repeated time values within panel"):
|
|
71
|
+
dpd_stream(FML, df.sample(fraction=1.0, shuffle=True, seed=2), panel=("id", "year"),
|
|
72
|
+
verbose=False, block_mb=0.01)
|
|
73
|
+
assert dpd_stream(FML, panel_data, panel=("id", "year"), verbose=False).n_obs > 0
|
|
74
|
+
|
|
75
|
+
|
|
62
76
|
def test_iterated(panel_data):
|
|
63
77
|
two = dpd_stream(FML, panel_data, panel=("id", "year"), verbose=False)
|
|
64
78
|
it0 = dpd_stream(FML, panel_data, panel=("id", "year"), steps="iterated", maxiter=0,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Memory tests: the streamed fits stay inside a memory limit on data that
|
|
2
|
+
would not fit in it.
|
|
3
|
+
|
|
4
|
+
Run with ``pytest -m resource`` (the default run leaves them out: they take
|
|
5
|
+
about two minutes). Each fit runs in a child process under stream-limits'
|
|
6
|
+
``resource_runner``: a watchdog on every platform, or a kernel limit when
|
|
7
|
+
``STREAM_LIMITS_TEST_CGROUP_PARENT`` names a cgroup set up for it.
|
|
8
|
+
|
|
9
|
+
The panel is 3 million firms over 8 years, 24 million rows, 0.96 GB in
|
|
10
|
+
memory, written as six Parquet files and read as a glob. With
|
|
11
|
+
``max_memory_gb=1.0`` difference and system GMM hold about 1.1 GB of private
|
|
12
|
+
memory (not counting memory-mapped scratch files, which the kernel can
|
|
13
|
+
reclaim), most of it buffers whose sizes are capped: the same on 9.6 million
|
|
14
|
+
rows. The limit, 1.6 GB, leaves 0.5 GB for platform differences; a fit that
|
|
15
|
+
held the panel in memory would need at least 2 GB. Measured on Linux (WSL2,
|
|
16
|
+
2 threads): difference GMM 1.09 GB, system GMM 1.07 GB.
|
|
17
|
+
|
|
18
|
+
The limit is on private memory, which stream-limits measures on Linux and
|
|
19
|
+
Windows. On macOS it measures resident memory, which counts the mapped
|
|
20
|
+
scratch files, so there the limit is a looser safety net.
|
|
21
|
+
On Windows, private memory is committed memory: a buffer or an allocator
|
|
22
|
+
segment counts in full once allocated, touched or not (an hdfe-stream logit
|
|
23
|
+
fit had 1.61 GB committed while 1.14 GB was resident). Measured in CI
|
|
24
|
+
(GitHub's windows-latest, 2 threads): difference GMM 1.46 GB, system GMM
|
|
25
|
+
1.38 GB; the limit there is 1.8 GB, which a fit holding the panel would
|
|
26
|
+
still exceed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
import polars as pl
|
|
35
|
+
import pytest
|
|
36
|
+
|
|
37
|
+
from dpd_stream.simulate import simulate_dynamic
|
|
38
|
+
|
|
39
|
+
pytestmark = pytest.mark.resource
|
|
40
|
+
|
|
41
|
+
PARTS, PANELS_PER_PART, T = 6, 500_000, 8 # 24 million rows
|
|
42
|
+
BUDGET_GB = 1.0 # the fit's max_memory_gb
|
|
43
|
+
LIMIT_GB = {"darwin": 2.6, "win32": 1.8}.get(sys.platform, 1.6)
|
|
44
|
+
FML = "y ~ l(y, 1) + x + w | year | gmm(y, 2:4) + gmm(x, 1:3) + iv(w)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture(scope="module")
|
|
48
|
+
def big_panel(tmp_path_factory):
|
|
49
|
+
"""Simulated in parts (the whole panel at once needs 7 GB to simulate),
|
|
50
|
+
each part's firms numbered after the last part's."""
|
|
51
|
+
folder = tmp_path_factory.mktemp("big")
|
|
52
|
+
size = 0
|
|
53
|
+
for i in range(PARTS):
|
|
54
|
+
d = simulate_dynamic(PANELS_PER_PART, T, seed=100 + i)
|
|
55
|
+
d = d.with_columns(pl.col("id") + i * PANELS_PER_PART)
|
|
56
|
+
d.write_parquet(folder / f"part{i}.parquet")
|
|
57
|
+
size += d.estimated_size()
|
|
58
|
+
assert size > 0.9e9
|
|
59
|
+
return folder / "*.parquet"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _fit_in_child(resource_runner, system: bool, path, workdir) -> dict:
|
|
63
|
+
code = f"""
|
|
64
|
+
import json
|
|
65
|
+
from dpd_stream import dpd_stream
|
|
66
|
+
r = dpd_stream({FML!r}, {str(path)!r}, panel=("id", "year"), system={system},
|
|
67
|
+
workdir={str(workdir)!r}, verbose=False, max_memory_gb={BUDGET_GB})
|
|
68
|
+
print(json.dumps(dict(n_obs=int(r.n_obs), coef={{k: float(v) for k, v in r.coef().items()}})))
|
|
69
|
+
"""
|
|
70
|
+
result = resource_runner([sys.executable, "-c", code], memory_gb=LIMIT_GB,
|
|
71
|
+
threads=2, timeout=900)
|
|
72
|
+
report = {k: result.as_dict()[k] for k in ("status", "mode", "memory_measure",
|
|
73
|
+
"peak_private_bytes", "peak_rss_bytes",
|
|
74
|
+
"elapsed_s")}
|
|
75
|
+
print(json.dumps(report))
|
|
76
|
+
assert result.status == "ok", (report, result.stderr[-3000:])
|
|
77
|
+
if result.kernel:
|
|
78
|
+
assert result.kernel["events"].get("oom_kill", 0) == 0
|
|
79
|
+
return json.loads(result.stdout.strip().splitlines()[-1])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# The checks against the simulated coefficients (simulate_dynamic's defaults:
|
|
83
|
+
# rho 0.5, beta 1) are sanity checks only: agreement with xtabond2 is the rest
|
|
84
|
+
# of the suite's job.
|
|
85
|
+
|
|
86
|
+
def test_difference_gmm_streams_inside_the_limit(resource_runner, big_panel, tmp_path):
|
|
87
|
+
out = _fit_in_child(resource_runner, False, big_panel, tmp_path / "work")
|
|
88
|
+
assert out["n_obs"] == PARTS * PANELS_PER_PART * (T - 2)
|
|
89
|
+
assert abs(out["coef"]["l(y, 1)"] - 0.5) < 0.02
|
|
90
|
+
assert abs(out["coef"]["x"] - 1.0) < 0.02
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_system_gmm_streams_inside_the_limit(resource_runner, big_panel, tmp_path):
|
|
94
|
+
out = _fit_in_child(resource_runner, True, big_panel, tmp_path / "work")
|
|
95
|
+
assert abs(out["coef"]["l(y, 1)"] - 0.5) < 0.02
|
|
96
|
+
assert abs(out["coef"]["x"] - 1.0) < 0.02
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|