python-libei 0.5.0__tar.gz → 0.5.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.
- {python_libei-0.5.0 → python_libei-0.5.1}/PKG-INFO +2 -2
- {python_libei-0.5.0 → python_libei-0.5.1}/README.md +1 -1
- {python_libei-0.5.0 → python_libei-0.5.1}/pyproject.toml +1 -1
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/__init__.py +1 -1
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/portal.py +133 -46
- {python_libei-0.5.0 → python_libei-0.5.1}/src/python_libei.egg-info/PKG-INFO +2 -2
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_inputcapture.py +51 -6
- {python_libei-0.5.0 → python_libei-0.5.1}/LICENSE +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/setup.cfg +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_capi/__init__.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_capi/libei.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_capi/libeis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_capi/liboeffis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_capi/loader.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/_cobject.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/ei.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/eis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/oeffis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/libei/py.typed +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/python_libei.egg-info/SOURCES.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/python_libei.egg-info/dependency_links.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/python_libei.egg-info/requires.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/src/python_libei.egg-info/top_level.txt +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_cobject.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_documentation_shape.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_documented_examples.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_ei_objects.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_eis_objects.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_integration_extras.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_integration_socketpair.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_loader.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_oeffis.py +0 -0
- {python_libei-0.5.0 → python_libei-0.5.1}/tests/test_portal.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -156,7 +156,7 @@ and why, is in
|
|
|
156
156
|
|
|
157
157
|
## Status
|
|
158
158
|
|
|
159
|
-
Beta (`0.5.
|
|
159
|
+
Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
160
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
161
|
|
|
162
162
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -119,7 +119,7 @@ and why, is in
|
|
|
119
119
|
|
|
120
120
|
## Status
|
|
121
121
|
|
|
122
|
-
Beta (`0.5.
|
|
122
|
+
Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
123
123
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
124
124
|
|
|
125
125
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-libei"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
description = "Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -926,6 +926,53 @@ def _input_capture_version(
|
|
|
926
926
|
return int(version)
|
|
927
927
|
|
|
928
928
|
|
|
929
|
+
def _create_input_capture_session_v1(
|
|
930
|
+
connection: Any,
|
|
931
|
+
Gio: Any,
|
|
932
|
+
GLib: Any,
|
|
933
|
+
busname: str,
|
|
934
|
+
types: int,
|
|
935
|
+
timeout: float,
|
|
936
|
+
) -> str:
|
|
937
|
+
"""Create an InputCapture session on a portal that predates v2.
|
|
938
|
+
|
|
939
|
+
v1 has no ``Start`` at all -- that method, like ``CreateSession2``, was
|
|
940
|
+
added in version 2. Here ``CreateSession`` itself carries
|
|
941
|
+
``capabilities`` and raises the consent dialog, and the session is ready
|
|
942
|
+
for ``GetZones`` / ``SetPointerBarriers`` / ``Enable`` the moment its
|
|
943
|
+
``Response`` arrives. ``ConnectToEIS`` is a v1 original, so the rest of
|
|
944
|
+
this class works against such a portal unchanged.
|
|
945
|
+
|
|
946
|
+
Returns the session handle. ``persist_mode`` and ``restore_token`` have
|
|
947
|
+
no v1 equivalent -- both are documented as version 2 additions to
|
|
948
|
+
``Start``, which does not exist here -- so the caller must reject them
|
|
949
|
+
before getting this far rather than have them silently ignored.
|
|
950
|
+
"""
|
|
951
|
+
code, results = _request(
|
|
952
|
+
connection,
|
|
953
|
+
Gio,
|
|
954
|
+
GLib,
|
|
955
|
+
busname,
|
|
956
|
+
_INPUT_CAPTURE,
|
|
957
|
+
"CreateSession",
|
|
958
|
+
"(sa{sv})",
|
|
959
|
+
("",),
|
|
960
|
+
{
|
|
961
|
+
"session_handle_token": GLib.Variant("s", uuid.uuid4().hex),
|
|
962
|
+
"capabilities": GLib.Variant("u", int(types)),
|
|
963
|
+
},
|
|
964
|
+
timeout,
|
|
965
|
+
)
|
|
966
|
+
if code != 0:
|
|
967
|
+
raise PortalDeniedError(
|
|
968
|
+
"CreateSession", "the user declined the input-capture consent dialog"
|
|
969
|
+
)
|
|
970
|
+
session_handle = results.get("session_handle")
|
|
971
|
+
if not isinstance(session_handle, str):
|
|
972
|
+
raise PortalError("CreateSession returned no session_handle")
|
|
973
|
+
return session_handle
|
|
974
|
+
|
|
975
|
+
|
|
929
976
|
def _wait_for_signal(
|
|
930
977
|
connection: Any,
|
|
931
978
|
Gio: Any,
|
|
@@ -1392,11 +1439,18 @@ class InputCaptureSession:
|
|
|
1392
1439
|
Blocks until ``CreateSession2`` -> ``Start`` -> ``ConnectToEIS``
|
|
1393
1440
|
resolves, prompting the user for consent along the way unless
|
|
1394
1441
|
``restore_token`` lets the portal skip that. Raises
|
|
1395
|
-
:class:`
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1442
|
+
:class:`PortalDeniedError` if consent is declined, and
|
|
1443
|
+
:class:`PortalTimeoutError` if any one round trip exceeds ``timeout``
|
|
1444
|
+
seconds.
|
|
1445
|
+
|
|
1446
|
+
A portal older than v2 has neither ``CreateSession2`` nor ``Start``,
|
|
1447
|
+
and is negotiated through the deprecated v1 ``CreateSession``
|
|
1448
|
+
instead -- one call that carries ``capabilities`` and raises the
|
|
1449
|
+
consent dialog itself. Everything after negotiation is identical;
|
|
1450
|
+
``ConnectToEIS`` is a v1 original. The one thing v1 cannot do is
|
|
1451
|
+
persist: ``persist_mode`` and ``restore_token`` were added to
|
|
1452
|
+
``Start``, so passing either against such a portal raises
|
|
1453
|
+
:class:`PortalVersionError` rather than being quietly ignored.
|
|
1400
1454
|
|
|
1401
1455
|
Returns before anything is actually captured: :meth:`set_pointer_barriers`
|
|
1402
1456
|
and :meth:`enable` still have to be called, and even then nothing
|
|
@@ -1435,62 +1489,95 @@ class InputCaptureSession:
|
|
|
1435
1489
|
raise PortalError(f"cannot reach the session bus: {exc}") from exc
|
|
1436
1490
|
|
|
1437
1491
|
version = _input_capture_version(connection, Gio, GLib, busname, timeout)
|
|
1438
|
-
|
|
1492
|
+
# A version below 2 is not a portal to give up on -- it is a portal
|
|
1493
|
+
# that speaks the older half of the same interface. Only
|
|
1494
|
+
# `CreateSession2` and `Start` are version 2 additions; `GetZones`,
|
|
1495
|
+
# `SetPointerBarriers`, `Enable`, `Disable`, `Release` and, crucially,
|
|
1496
|
+
# `ConnectToEIS` are all v1 originals, so everything this class does
|
|
1497
|
+
# after negotiation works either way and only session creation forks.
|
|
1498
|
+
#
|
|
1499
|
+
# This is not hypothetical. xdg-desktop-portal-gnome 50 reports
|
|
1500
|
+
# version 0 -- it registers the full impl interface and never sets the
|
|
1501
|
+
# property -- and calling `CreateSession2` against it fails with
|
|
1502
|
+
# `UnknownMethod`, while v1 `CreateSession` is dispatched normally.
|
|
1503
|
+
# Note that its introspection XML advertises `CreateSession2` anyway:
|
|
1504
|
+
# that XML is static, so it says nothing about what the frontend will
|
|
1505
|
+
# actually dispatch. The `version` property is the only usable signal,
|
|
1506
|
+
# which is why it is read first and believed.
|
|
1507
|
+
legacy = version < _MIN_INPUT_CAPTURE_VERSION
|
|
1508
|
+
|
|
1509
|
+
if legacy and (persist_mode != PersistMode.NONE or restore_token is not None):
|
|
1439
1510
|
raise PortalVersionError(
|
|
1440
|
-
f"InputCapture version {version}
|
|
1441
|
-
f"
|
|
1511
|
+
f"InputCapture version {version} has no Start method, and "
|
|
1512
|
+
f"persist_mode/restore_token are version "
|
|
1513
|
+
f"{_MIN_INPUT_CAPTURE_VERSION} additions to it -- this portal "
|
|
1514
|
+
f"cannot persist a session. Retry with persist_mode=NONE and "
|
|
1515
|
+
f"no restore_token to negotiate a one-shot session."
|
|
1442
1516
|
)
|
|
1443
1517
|
|
|
1444
1518
|
if capabilities == DeviceType.ALL_DEVICES:
|
|
1445
1519
|
types = _ALL_DEVICE_TYPES
|
|
1446
1520
|
else:
|
|
1447
1521
|
types = capabilities
|
|
1448
|
-
reply = _call_sync(
|
|
1449
|
-
connection,
|
|
1450
|
-
Gio,
|
|
1451
|
-
GLib,
|
|
1452
|
-
busname,
|
|
1453
|
-
_OBJECT_PATH,
|
|
1454
|
-
_INPUT_CAPTURE,
|
|
1455
|
-
"CreateSession2",
|
|
1456
|
-
GLib.Variant(
|
|
1457
|
-
"(a{sv})",
|
|
1458
|
-
({"session_handle_token": GLib.Variant("s", uuid.uuid4().hex)},),
|
|
1459
|
-
),
|
|
1460
|
-
None,
|
|
1461
|
-
int(timeout * 1000),
|
|
1462
|
-
)
|
|
1463
|
-
(results,) = reply.unpack()
|
|
1464
|
-
session_handle = results.get("session_handle")
|
|
1465
|
-
if not isinstance(session_handle, str):
|
|
1466
|
-
raise PortalError("CreateSession2 returned no session_handle")
|
|
1467
1522
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
options["persist_mode"] = GLib.Variant("u", int(persist_mode))
|
|
1475
|
-
if restore_token is not None:
|
|
1476
|
-
options["restore_token"] = GLib.Variant("s", restore_token)
|
|
1477
|
-
code, start_results = _request(
|
|
1523
|
+
if legacy:
|
|
1524
|
+
session_handle = _create_input_capture_session_v1(
|
|
1525
|
+
connection, Gio, GLib, busname, int(types), timeout
|
|
1526
|
+
)
|
|
1527
|
+
else:
|
|
1528
|
+
reply = _call_sync(
|
|
1478
1529
|
connection,
|
|
1479
1530
|
Gio,
|
|
1480
1531
|
GLib,
|
|
1481
1532
|
busname,
|
|
1533
|
+
_OBJECT_PATH,
|
|
1482
1534
|
_INPUT_CAPTURE,
|
|
1483
|
-
"
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1535
|
+
"CreateSession2",
|
|
1536
|
+
GLib.Variant(
|
|
1537
|
+
"(a{sv})",
|
|
1538
|
+
({"session_handle_token": GLib.Variant("s", uuid.uuid4().hex)},),
|
|
1539
|
+
),
|
|
1540
|
+
None,
|
|
1541
|
+
int(timeout * 1000),
|
|
1488
1542
|
)
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1543
|
+
(results,) = reply.unpack()
|
|
1544
|
+
session_handle = results.get("session_handle")
|
|
1545
|
+
if not isinstance(session_handle, str):
|
|
1546
|
+
raise PortalError("CreateSession2 returned no session_handle")
|
|
1547
|
+
|
|
1548
|
+
# Past this point a session exists inside xdg-desktop-portal, and
|
|
1549
|
+
# nothing else can close it -- see RemoteDesktopSession.negotiate's
|
|
1550
|
+
# identical reasoning, which this mirrors line for line.
|
|
1551
|
+
try:
|
|
1552
|
+
# v1 asked for consent and capabilities in CreateSession itself,
|
|
1553
|
+
# and has no Start to call; it can therefore never issue a
|
|
1554
|
+
# restore token either.
|
|
1555
|
+
new_restore_token = None
|
|
1556
|
+
if not legacy:
|
|
1557
|
+
options: dict[str, Any] = {
|
|
1558
|
+
"capabilities": GLib.Variant("u", int(types))
|
|
1559
|
+
}
|
|
1560
|
+
if persist_mode != PersistMode.NONE:
|
|
1561
|
+
options["persist_mode"] = GLib.Variant("u", int(persist_mode))
|
|
1562
|
+
if restore_token is not None:
|
|
1563
|
+
options["restore_token"] = GLib.Variant("s", restore_token)
|
|
1564
|
+
code, start_results = _request(
|
|
1565
|
+
connection,
|
|
1566
|
+
Gio,
|
|
1567
|
+
GLib,
|
|
1568
|
+
busname,
|
|
1569
|
+
_INPUT_CAPTURE,
|
|
1570
|
+
"Start",
|
|
1571
|
+
"(osa{sv})",
|
|
1572
|
+
(session_handle, ""),
|
|
1573
|
+
options,
|
|
1574
|
+
timeout,
|
|
1492
1575
|
)
|
|
1493
|
-
|
|
1576
|
+
if code != 0:
|
|
1577
|
+
raise PortalDeniedError(
|
|
1578
|
+
"Start", "the user declined the input-capture consent dialog"
|
|
1579
|
+
)
|
|
1580
|
+
new_restore_token = start_results.get("restore_token")
|
|
1494
1581
|
|
|
1495
1582
|
eis_fd = _call_for_fd(
|
|
1496
1583
|
connection,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -156,7 +156,7 @@ and why, is in
|
|
|
156
156
|
|
|
157
157
|
## Status
|
|
158
158
|
|
|
159
|
-
Beta (`0.5.
|
|
159
|
+
Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
160
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
161
|
|
|
162
162
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -46,8 +46,8 @@ class FakeInputCaptureConnection:
|
|
|
46
46
|
|
|
47
47
|
Three call shapes, told apart by method name: `Get` (the version
|
|
48
48
|
property), `CreateSession2`/`Enable`/`Disable`/`Release` (plain calls,
|
|
49
|
-
no Request/Response), and `Start`/`GetZones
|
|
50
|
-
(Request-shaped, exactly like RemoteDesktop's
|
|
49
|
+
no Request/Response), and `CreateSession` (v1)/`Start`/`GetZones`/
|
|
50
|
+
`SetPointerBarriers` (Request-shaped, exactly like RemoteDesktop's -- the
|
|
51
51
|
`handle_token` lives in whichever positional argument is a dict, found
|
|
52
52
|
by type rather than by a fixed index, since SetPointerBarriers's
|
|
53
53
|
options argument is not last).
|
|
@@ -78,6 +78,9 @@ class FakeInputCaptureConnection:
|
|
|
78
78
|
self.session_handle = session_handle
|
|
79
79
|
self.responses = responses or {
|
|
80
80
|
"Start": (0, {"capabilities": 3}),
|
|
81
|
+
# v1 CreateSession is Request-shaped too, and answers with the
|
|
82
|
+
# handle CreateSession2 returns directly.
|
|
83
|
+
"CreateSession": (0, {"session_handle": session_handle}),
|
|
81
84
|
"GetZones": (0, {"zone_set": 1, "zones": [(1920, 1080, 0, 0)]}),
|
|
82
85
|
"SetPointerBarriers": (0, {"failed_barriers": []}),
|
|
83
86
|
}
|
|
@@ -209,17 +212,59 @@ def test_successful_negotiation_calls_every_step_in_order() -> None:
|
|
|
209
212
|
assert session.session_handle == connection.session_handle
|
|
210
213
|
|
|
211
214
|
|
|
212
|
-
|
|
215
|
+
@pytest.mark.parametrize("version", [1, 0])
|
|
216
|
+
def test_pre_v2_portal_negotiates_through_v1_create_session(version: int) -> None:
|
|
217
|
+
"""v1 has no CreateSession2 and no Start -- one call does both jobs.
|
|
218
|
+
|
|
219
|
+
version 0 takes the same path: xdg-desktop-portal-gnome 50 registers the
|
|
220
|
+
whole impl interface and never sets the property, so 0 reaches us from a
|
|
221
|
+
portal that speaks v1 perfectly well.
|
|
222
|
+
"""
|
|
223
|
+
connection = FakeInputCaptureConnection(version=version)
|
|
224
|
+
with install_fake_gi(connection):
|
|
225
|
+
session = portal.InputCaptureSession.negotiate(connection=connection)
|
|
226
|
+
assert [c[0] for c in connection.calls] == [
|
|
227
|
+
"Get",
|
|
228
|
+
"CreateSession",
|
|
229
|
+
"ConnectToEIS",
|
|
230
|
+
]
|
|
231
|
+
assert session.session_handle == connection.session_handle
|
|
232
|
+
assert session.eis_fd == connection.fd_responses["ConnectToEIS"]
|
|
233
|
+
# v1 can never issue one: restore_token is a v2 addition to Start.
|
|
234
|
+
assert session.restore_token is None
|
|
235
|
+
# capabilities ride on CreateSession here, not on Start.
|
|
236
|
+
options = next(v for v in connection.calls[1][1] if isinstance(v, dict))
|
|
237
|
+
assert options["capabilities"].value == int(portal._ALL_DEVICE_TYPES)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_v1_portal_refuses_persistence_rather_than_ignoring_it() -> None:
|
|
241
|
+
"""persist_mode/restore_token are v2 additions to a method v1 lacks."""
|
|
213
242
|
connection = FakeInputCaptureConnection(version=1)
|
|
214
243
|
with install_fake_gi(connection):
|
|
215
|
-
with pytest.raises(portal.PortalVersionError):
|
|
216
|
-
portal.InputCaptureSession.negotiate(
|
|
217
|
-
|
|
244
|
+
with pytest.raises(portal.PortalVersionError, match="cannot persist"):
|
|
245
|
+
portal.InputCaptureSession.negotiate(
|
|
246
|
+
connection=connection, persist_mode=portal.PersistMode.UNTIL_REVOKED
|
|
247
|
+
)
|
|
248
|
+
# Refused before creating anything that would need cleaning up.
|
|
218
249
|
assert connection.calls == [
|
|
219
250
|
("Get", ("org.freedesktop.portal.InputCapture", "version"))
|
|
220
251
|
]
|
|
221
252
|
|
|
222
253
|
|
|
254
|
+
def test_declined_v1_create_session_raises_denied_error() -> None:
|
|
255
|
+
"""v1 raises its consent dialog in CreateSession, so that is the step."""
|
|
256
|
+
connection = FakeInputCaptureConnection(
|
|
257
|
+
version=1,
|
|
258
|
+
responses={"CreateSession": (1, {})}, # 1 == user cancelled
|
|
259
|
+
)
|
|
260
|
+
with install_fake_gi(connection):
|
|
261
|
+
with pytest.raises(portal.PortalDeniedError) as excinfo:
|
|
262
|
+
portal.InputCaptureSession.negotiate(connection=connection)
|
|
263
|
+
assert excinfo.value.step == "CreateSession"
|
|
264
|
+
# No session handle came back, so there is nothing to Close.
|
|
265
|
+
assert [c[0] for c in connection.calls] == ["Get", "CreateSession"]
|
|
266
|
+
|
|
267
|
+
|
|
223
268
|
def test_a_missing_session_handle_raises_portal_error() -> None:
|
|
224
269
|
class NoHandleConnection(FakeInputCaptureConnection):
|
|
225
270
|
def call_sync(self, *args: Any, **kwargs: Any) -> FakeReply:
|
|
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
|