python-libei 0.7.0__py3-none-any.whl → 0.8.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.
- libei/__init__.py +1 -1
- libei/portal.py +219 -24
- {python_libei-0.7.0.dist-info → python_libei-0.8.0.dist-info}/METADATA +2 -2
- {python_libei-0.7.0.dist-info → python_libei-0.8.0.dist-info}/RECORD +7 -7
- {python_libei-0.7.0.dist-info → python_libei-0.8.0.dist-info}/WHEEL +1 -1
- {python_libei-0.7.0.dist-info → python_libei-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {python_libei-0.7.0.dist-info → python_libei-0.8.0.dist-info}/top_level.txt +0 -0
libei/__init__.py
CHANGED
libei/portal.py
CHANGED
|
@@ -90,6 +90,8 @@ __all__ = [
|
|
|
90
90
|
"PortalVersionError",
|
|
91
91
|
"PortalDeniedError",
|
|
92
92
|
"PortalTimeoutError",
|
|
93
|
+
"PortalSessionClosedError",
|
|
94
|
+
"PortalProtocolError",
|
|
93
95
|
"RemoteDesktopSession",
|
|
94
96
|
"Activation",
|
|
95
97
|
"InputCaptureSession",
|
|
@@ -185,6 +187,45 @@ class PortalDeniedError(PortalError):
|
|
|
185
187
|
self.message = message
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
class PortalProtocolError(PortalError):
|
|
191
|
+
"""A portal signal arrived with a payload that does not match the spec.
|
|
192
|
+
|
|
193
|
+
Every signal :func:`_wait_for_signal` waits on is specified as
|
|
194
|
+
``(o session_handle, a{sv} options)``, and two kinds of mismatch used
|
|
195
|
+
to fail in two different, equally unhelpful ways. The *shape* being
|
|
196
|
+
wrong -- an empty tuple, a non-string handle, a wrong-length tuple, a
|
|
197
|
+
non-dict options -- either raised ``IndexError`` *inside* the GLib
|
|
198
|
+
signal callback, where PyGI prints the traceback to stderr and
|
|
199
|
+
swallows it so the wait hung invisibly (unboundedly, with
|
|
200
|
+
``timeout=None``), or slipped through to the caller and crashed it with
|
|
201
|
+
``ValueError``/``AttributeError``. A *value* inside a correctly-shaped
|
|
202
|
+
options being wrong -- an ``activation_id`` that is not an integer, a
|
|
203
|
+
``cursor_position`` that is not two numbers -- crashed the caller later
|
|
204
|
+
still, doing arithmetic on it. Both now end the wait with this error.
|
|
205
|
+
|
|
206
|
+
``detail`` distinguishes the two: ``None`` for a shape fault (the
|
|
207
|
+
default message names the expected shape), a phrase for a value fault
|
|
208
|
+
(naming the field). ``payload`` is whatever best shows the fault -- the
|
|
209
|
+
whole signal tuple for a shape fault, the options dict for a value one.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(self, step: str, payload: object, detail: str | None = None) -> None:
|
|
213
|
+
"""Name the signal, what arrived, and -- for a value fault -- which."""
|
|
214
|
+
reason = detail or (
|
|
215
|
+
"the portal spec gives it the shape (session_handle, options dict)"
|
|
216
|
+
)
|
|
217
|
+
super().__init__(
|
|
218
|
+
f"{step} arrived with a malformed payload: {reason}, got {payload!r}"
|
|
219
|
+
)
|
|
220
|
+
# `step` mirrors the attribute every sibling portal error raised
|
|
221
|
+
# from the same `_wait_for_signal` call site carries
|
|
222
|
+
# (PortalTimeoutError, PortalSessionClosedError), so a caller that
|
|
223
|
+
# catches PortalError and reads `.step` works across all of them.
|
|
224
|
+
self.step = step
|
|
225
|
+
self.payload = payload
|
|
226
|
+
self.detail = detail
|
|
227
|
+
|
|
228
|
+
|
|
188
229
|
class PortalSessionClosedError(PortalError):
|
|
189
230
|
"""The portal session ended while something was still waiting on it.
|
|
190
231
|
|
|
@@ -1021,6 +1062,98 @@ def _create_input_capture_session_v1(
|
|
|
1021
1062
|
return session_handle
|
|
1022
1063
|
|
|
1023
1064
|
|
|
1065
|
+
def _classify_signal_payload(
|
|
1066
|
+
args: tuple[Any, ...], session_handle: str, signal: str
|
|
1067
|
+
) -> str:
|
|
1068
|
+
"""How `_wait_for_signal` should treat one unpacked signal payload.
|
|
1069
|
+
|
|
1070
|
+
``"take"`` -- it is this session's, correctly shaped, record it.
|
|
1071
|
+
``"drop"`` -- another session's; keep waiting (logged, since a dropped
|
|
1072
|
+
signal is otherwise indistinguishable from one that never fired).
|
|
1073
|
+
``"malformed"`` -- not the ``(o session_handle, a{sv} options)`` shape
|
|
1074
|
+
the spec gives every signal here.
|
|
1075
|
+
|
|
1076
|
+
Two shape checks with the session filter between them, so how a
|
|
1077
|
+
malformed payload is treated depends on how it is broken. A payload
|
|
1078
|
+
whose *options* are the wrong shape (the second check) is reached only
|
|
1079
|
+
after the filter has matched its handle, so another session's is already
|
|
1080
|
+
dropped before that check runs -- it is raised only for this session. A
|
|
1081
|
+
payload with no first element, or one that is not a string, cannot be
|
|
1082
|
+
matched to any session at all: there is no handle to compare it with, so
|
|
1083
|
+
it is malformed for every possible waiter and is raised here whichever
|
|
1084
|
+
session it might have been meant for -- the filter cannot run before it,
|
|
1085
|
+
only after. In the single-session process this almost always is, the
|
|
1086
|
+
distinction never arises.
|
|
1087
|
+
"""
|
|
1088
|
+
if not args or not isinstance(args[0], str):
|
|
1089
|
+
return "malformed"
|
|
1090
|
+
if args[0] != session_handle:
|
|
1091
|
+
logger.debug(
|
|
1092
|
+
"ignoring %s for session %s while waiting for %s",
|
|
1093
|
+
signal,
|
|
1094
|
+
args[0],
|
|
1095
|
+
session_handle,
|
|
1096
|
+
)
|
|
1097
|
+
return "drop"
|
|
1098
|
+
if len(args) != 2 or not isinstance(args[1], dict):
|
|
1099
|
+
return "malformed"
|
|
1100
|
+
return "take"
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _deactivation_id(options: dict[str, Any], signal: str) -> int:
|
|
1104
|
+
"""The ``activation_id`` from Deactivated's options, as the spec's ``u``.
|
|
1105
|
+
|
|
1106
|
+
``_wait_for_signal`` checks the payload's *shape* -- that options is a
|
|
1107
|
+
dict -- but not the types of the values in it. ``activation_id`` is a
|
|
1108
|
+
``u``, and a non-conforming portal that put something else there would
|
|
1109
|
+
crash the caller with ``ValueError``/``TypeError``/``OverflowError``
|
|
1110
|
+
(the last from ``int(float('inf'))``) past the shape check. Converting
|
|
1111
|
+
it here turns that into the same typed :class:`PortalProtocolError` the
|
|
1112
|
+
shape check raises, so a caller that guards against malformed signals
|
|
1113
|
+
catches one error, not two.
|
|
1114
|
+
"""
|
|
1115
|
+
try:
|
|
1116
|
+
return int(options.get("activation_id", 0))
|
|
1117
|
+
except (ValueError, TypeError, OverflowError) as exc:
|
|
1118
|
+
raise PortalProtocolError(
|
|
1119
|
+
signal, options, "activation_id is not an integer"
|
|
1120
|
+
) from exc
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _activation_from_options(options: dict[str, Any], signal: str) -> Activation:
|
|
1124
|
+
"""Build an :class:`Activation` from Activated's options, spec-typed.
|
|
1125
|
+
|
|
1126
|
+
The companion to the shape check in ``_wait_for_signal`` for the value
|
|
1127
|
+
level: every field the portal spec types is converted here under one
|
|
1128
|
+
guard, so a non-conforming value in any of them -- an ``activation_id``
|
|
1129
|
+
that is not a ``u``, a ``cursor_position`` that is not two ``d``s, a
|
|
1130
|
+
``barrier_id`` that is not a ``u`` -- becomes the same typed
|
|
1131
|
+
:class:`PortalProtocolError` rather than a crash in the caller later,
|
|
1132
|
+
doing arithmetic on a string. One guard rather than three means the
|
|
1133
|
+
next field added to the signal is validated by construction, not by a
|
|
1134
|
+
fourth hand-rolled check nobody remembers to write.
|
|
1135
|
+
"""
|
|
1136
|
+
try:
|
|
1137
|
+
activation_id = int(options.get("activation_id", 0))
|
|
1138
|
+
cursor = options.get("cursor_position")
|
|
1139
|
+
if cursor is None:
|
|
1140
|
+
cursor_position = None
|
|
1141
|
+
else:
|
|
1142
|
+
cx, cy = cursor # (dd): exactly two, or this unpacking raises
|
|
1143
|
+
cursor_position = (float(cx), float(cy))
|
|
1144
|
+
barrier = options.get("barrier_id")
|
|
1145
|
+
barrier_id = None if barrier is None else int(barrier)
|
|
1146
|
+
except (ValueError, TypeError, OverflowError) as exc:
|
|
1147
|
+
raise PortalProtocolError(
|
|
1148
|
+
signal, options, "an option value is not the type the spec gives it"
|
|
1149
|
+
) from exc
|
|
1150
|
+
return Activation(
|
|
1151
|
+
activation_id=activation_id,
|
|
1152
|
+
cursor_position=cursor_position,
|
|
1153
|
+
barrier_id=barrier_id,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
|
|
1024
1157
|
def _wait_for_signal(
|
|
1025
1158
|
connection: Any,
|
|
1026
1159
|
Gio: Any,
|
|
@@ -1056,12 +1189,41 @@ def _wait_for_signal(
|
|
|
1056
1189
|
D-Bus specification restricts plain ``arg0=`` match rules to arguments
|
|
1057
1190
|
of type STRING, and this one is an OBJECT_PATH, which is the same shape
|
|
1058
1191
|
of silent non-delivery all over again.
|
|
1192
|
+
|
|
1193
|
+
A payload that does not match the spec's ``(o session_handle, a{sv}
|
|
1194
|
+
options)`` shape raises :class:`PortalProtocolError` rather than being
|
|
1195
|
+
unpacked on faith -- see that class for why the alternative was not an
|
|
1196
|
+
exception but a hang.
|
|
1059
1197
|
"""
|
|
1060
1198
|
loop = GLib.MainLoop()
|
|
1061
1199
|
result: dict[str, Any] = {}
|
|
1200
|
+
malformed: dict[str, Any] = {}
|
|
1062
1201
|
timed_out = False
|
|
1202
|
+
# Distinct from `timed_out`: whether `on_timeout` ran at all, either
|
|
1203
|
+
# path. `on_timeout` returns False whichever branch it takes, and False
|
|
1204
|
+
# is what makes GLib deregister the source -- so the finally block must
|
|
1205
|
+
# key its "already removed?" check on "did on_timeout fire", not on "did
|
|
1206
|
+
# the wait time out". When a signal and the deadline land in the same
|
|
1207
|
+
# poll, on_timeout takes the `_settled()` early return (leaving
|
|
1208
|
+
# `timed_out` False) yet its False return still removed the source;
|
|
1209
|
+
# keying on `timed_out` there would double-remove it and trip the very
|
|
1210
|
+
# "Source ID N was not found" critical the guard exists to avoid.
|
|
1211
|
+
timeout_fired = False
|
|
1063
1212
|
session_closed = False
|
|
1064
1213
|
|
|
1214
|
+
def _settled() -> bool:
|
|
1215
|
+
"""Whether some terminal outcome has already been recorded.
|
|
1216
|
+
|
|
1217
|
+
GLib dispatches every source ready in one main-context iteration,
|
|
1218
|
+
regardless of an earlier ``loop.quit()``, so two outcomes -- a
|
|
1219
|
+
signal and the timeout, a signal and Closed -- can both fire in the
|
|
1220
|
+
same poll. Every callback checks this first so the *first* recorded
|
|
1221
|
+
outcome wins and the post-loop raise order is never handed a second
|
|
1222
|
+
one to misreport (a delivered activation dropped for a timeout, a
|
|
1223
|
+
protocol fault overwritten by session-closed).
|
|
1224
|
+
"""
|
|
1225
|
+
return bool(result) or bool(malformed) or timed_out or session_closed
|
|
1226
|
+
|
|
1065
1227
|
def on_closed(*_args: Any) -> None:
|
|
1066
1228
|
"""End the wait: a closed session can never deliver the signal.
|
|
1067
1229
|
|
|
@@ -1076,7 +1238,7 @@ def _wait_for_signal(
|
|
|
1076
1238
|
that merely had not fired yet.
|
|
1077
1239
|
"""
|
|
1078
1240
|
nonlocal session_closed
|
|
1079
|
-
if
|
|
1241
|
+
if _settled():
|
|
1080
1242
|
return
|
|
1081
1243
|
logger.debug("session %s closed while waiting for %s", session_handle, signal)
|
|
1082
1244
|
session_closed = True
|
|
@@ -1092,21 +1254,22 @@ def _wait_for_signal(
|
|
|
1092
1254
|
*_a: Any,
|
|
1093
1255
|
) -> None:
|
|
1094
1256
|
"""Record the first matching signal; anything else is logged and dropped."""
|
|
1095
|
-
if
|
|
1257
|
+
if _settled(): # a subscription outliving its wait fires twice
|
|
1096
1258
|
return
|
|
1097
1259
|
args = params.unpack()
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1260
|
+
# The verdict is computed by `_classify_signal_payload`, but ACTED
|
|
1261
|
+
# on here and the error raised only after the loop, because this
|
|
1262
|
+
# runs inside a GLib callback: PyGI prints an exception raised here
|
|
1263
|
+
# to stderr and swallows it, so a bare raise on a malformed payload
|
|
1264
|
+
# would leave the loop running -- a timeout at best, an unbounded
|
|
1265
|
+
# hang with timeout=None, both indistinguishable from a compositor
|
|
1266
|
+
# that never fired.
|
|
1267
|
+
verdict = _classify_signal_payload(args, session_handle, signal)
|
|
1268
|
+
if verdict == "drop":
|
|
1269
|
+
return
|
|
1270
|
+
if verdict == "malformed":
|
|
1271
|
+
malformed["args"] = args
|
|
1272
|
+
loop.quit()
|
|
1110
1273
|
return
|
|
1111
1274
|
logger.debug("received %s for session %s", signal, session_handle)
|
|
1112
1275
|
result["args"] = args
|
|
@@ -1114,7 +1277,16 @@ def _wait_for_signal(
|
|
|
1114
1277
|
|
|
1115
1278
|
def on_timeout() -> bool:
|
|
1116
1279
|
"""Stop the loop and record that it timed out rather than finished."""
|
|
1117
|
-
nonlocal timed_out
|
|
1280
|
+
nonlocal timed_out, timeout_fired
|
|
1281
|
+
# Set first, before the early return: GLib removes the source the
|
|
1282
|
+
# moment this returns False (which both paths do), so the finally
|
|
1283
|
+
# block keys "already removed" on this, not on `timed_out`.
|
|
1284
|
+
timeout_fired = True
|
|
1285
|
+
if _settled():
|
|
1286
|
+
# A signal delivered in the same poll as the deadline has
|
|
1287
|
+
# already recorded its outcome; the timeout must not overwrite
|
|
1288
|
+
# a real activation with PortalTimeoutError.
|
|
1289
|
+
return False
|
|
1118
1290
|
timed_out = True
|
|
1119
1291
|
loop.quit()
|
|
1120
1292
|
return False
|
|
@@ -1148,7 +1320,7 @@ def _wait_for_signal(
|
|
|
1148
1320
|
session_handle,
|
|
1149
1321
|
)
|
|
1150
1322
|
try:
|
|
1151
|
-
if not result and not session_closed:
|
|
1323
|
+
if not result and not session_closed and not malformed:
|
|
1152
1324
|
timeout_source = None
|
|
1153
1325
|
if timeout is not None:
|
|
1154
1326
|
timeout_source = GLib.timeout_add(int(timeout * 1000), on_timeout)
|
|
@@ -1164,13 +1336,41 @@ def _wait_for_signal(
|
|
|
1164
1336
|
# every real timeout this session hit, never against
|
|
1165
1337
|
# test_portal.py's fake GLib.source_remove, which just
|
|
1166
1338
|
# clears pending_timeout with no complaint either way.
|
|
1167
|
-
|
|
1339
|
+
# Keyed on `timeout_fired` (did on_timeout run), not
|
|
1340
|
+
# `timed_out` (did the wait time out): a signal landing in
|
|
1341
|
+
# the same poll as the deadline runs on_timeout down its
|
|
1342
|
+
# `_settled` early return, which removes the source yet
|
|
1343
|
+
# leaves `timed_out` False.
|
|
1344
|
+
if timeout_source is not None and not timeout_fired:
|
|
1168
1345
|
GLib.source_remove(timeout_source)
|
|
1169
1346
|
finally:
|
|
1170
1347
|
connection.signal_unsubscribe(subscription)
|
|
1171
1348
|
connection.signal_unsubscribe(closed_subscription)
|
|
1349
|
+
return _signal_outcome(
|
|
1350
|
+
signal, timeout, result, malformed, timed_out, session_closed
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _signal_outcome(
|
|
1355
|
+
signal: str,
|
|
1356
|
+
timeout: float | None,
|
|
1357
|
+
result: dict[str, Any],
|
|
1358
|
+
malformed: dict[str, Any],
|
|
1359
|
+
timed_out: bool,
|
|
1360
|
+
session_closed: bool,
|
|
1361
|
+
) -> tuple[Any, ...]:
|
|
1362
|
+
"""Raise for the terminal outcome `_wait_for_signal` recorded, or return it.
|
|
1363
|
+
|
|
1364
|
+
Order is load-bearing and matches the callbacks' `_settled` guard:
|
|
1365
|
+
whichever outcome was recorded first is the only one set, so this order
|
|
1366
|
+
only ever decides between genuinely simultaneous dispatches, where a
|
|
1367
|
+
session that has closed or a payload that was malformed is the more
|
|
1368
|
+
useful answer than "timed out".
|
|
1369
|
+
"""
|
|
1172
1370
|
if session_closed:
|
|
1173
1371
|
raise PortalSessionClosedError(signal)
|
|
1372
|
+
if malformed:
|
|
1373
|
+
raise PortalProtocolError(signal, malformed["args"])
|
|
1174
1374
|
if timed_out:
|
|
1175
1375
|
# timed_out is only ever set inside on_timeout, itself only ever
|
|
1176
1376
|
# registered when timeout is not None -- so this always holds, but
|
|
@@ -1497,12 +1697,7 @@ class InputCaptureSession:
|
|
|
1497
1697
|
self.session_handle,
|
|
1498
1698
|
timeout,
|
|
1499
1699
|
)
|
|
1500
|
-
|
|
1501
|
-
return Activation(
|
|
1502
|
-
activation_id=int(options.get("activation_id", 0)),
|
|
1503
|
-
cursor_position=tuple(cursor) if cursor is not None else None,
|
|
1504
|
-
barrier_id=options.get("barrier_id"),
|
|
1505
|
-
)
|
|
1700
|
+
return _activation_from_options(options, "Activated")
|
|
1506
1701
|
|
|
1507
1702
|
def wait_for_deactivation(self, timeout: float | None = None) -> int:
|
|
1508
1703
|
"""Block until the current capture ends, returning its activation_id."""
|
|
@@ -1520,7 +1715,7 @@ class InputCaptureSession:
|
|
|
1520
1715
|
self.session_handle,
|
|
1521
1716
|
timeout,
|
|
1522
1717
|
)
|
|
1523
|
-
return
|
|
1718
|
+
return _deactivation_id(options, "Deactivated")
|
|
1524
1719
|
|
|
1525
1720
|
def close(self) -> None:
|
|
1526
1721
|
"""End the portal session, and close the EIS fd if unclaimed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Inject, receive and capture input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis, plus the RemoteDesktop and InputCapture portals
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -175,7 +175,7 @@ and why, is in
|
|
|
175
175
|
|
|
176
176
|
## Status
|
|
177
177
|
|
|
178
|
-
Beta (`0.
|
|
178
|
+
Beta (`0.8.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
179
179
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
180
180
|
|
|
181
181
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
libei/__init__.py,sha256=
|
|
1
|
+
libei/__init__.py,sha256=8k-acXmeIea963zVksZW5Ymby8ZYL_aO6B4o7g4_Y9w,1858
|
|
2
2
|
libei/_cobject.py,sha256=XjbUNJpZ87jAyPWh6dmRslk6cMy831oMRFhWmfjLDFo,13344
|
|
3
3
|
libei/ei.py,sha256=W4akKc_BNVfL0FroSXLGD1Z_L3OmYjZT5QDHNnEtMP8,49367
|
|
4
4
|
libei/eis.py,sha256=X_umAaAAbAMC9d2RdnMReeJZI61WsnKz3cySY5YrGr0,45841
|
|
5
5
|
libei/oeffis.py,sha256=gJHR90s9VhQrgkjWUMDx-lxZ8qKxkttnqbx_xo-7oxs,10035
|
|
6
|
-
libei/portal.py,sha256=
|
|
6
|
+
libei/portal.py,sha256=FdtVdUUKrq2GOcDBtunLCISw7RYO4Mwyk3ci26OH330,81621
|
|
7
7
|
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
libei/_capi/__init__.py,sha256=raISSWSV9hAcE92DcLCz-vUb1IPcsmxRZaS-iAoQJBE,211
|
|
9
9
|
libei/_capi/libei.py,sha256=qsccHa_komCxtvPYi6AobszhFbEG2qA8QYJJVr8SaGM,11356
|
|
10
10
|
libei/_capi/libeis.py,sha256=C_Uh7DwNncFC8A4wuMhviAj5C_TUmKHtLJYq0Qhhz1E,13142
|
|
11
11
|
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
12
|
libei/_capi/loader.py,sha256=xW67uk35ckgeFaheDGrHC05ZM5w_cxBGvX5lG_GUSu8,6625
|
|
13
|
-
python_libei-0.
|
|
14
|
-
python_libei-0.
|
|
15
|
-
python_libei-0.
|
|
16
|
-
python_libei-0.
|
|
17
|
-
python_libei-0.
|
|
13
|
+
python_libei-0.8.0.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
+
python_libei-0.8.0.dist-info/METADATA,sha256=SNey10DqR4IKZ_T_XCKmafkR0ioMfxHbdbaSl9IWG-c,22882
|
|
15
|
+
python_libei-0.8.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
16
|
+
python_libei-0.8.0.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
+
python_libei-0.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|