python-libei 0.5.1__py3-none-any.whl → 0.5.2__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/_capi/__init__.py +5 -2
- libei/ei.py +71 -15
- libei/eis.py +65 -17
- libei/oeffis.py +7 -2
- libei/portal.py +108 -30
- {python_libei-0.5.1.dist-info → python_libei-0.5.2.dist-info}/METADATA +13 -28
- python_libei-0.5.2.dist-info/RECORD +17 -0
- python_libei-0.5.1.dist-info/RECORD +0 -17
- {python_libei-0.5.1.dist-info → python_libei-0.5.2.dist-info}/WHEEL +0 -0
- {python_libei-0.5.1.dist-info → python_libei-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {python_libei-0.5.1.dist-info → python_libei-0.5.2.dist-info}/top_level.txt +0 -0
libei/__init__.py
CHANGED
libei/_capi/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
"""Low-level ctypes bindings.
|
|
2
|
-
|
|
1
|
+
"""Low-level ctypes bindings.
|
|
2
|
+
|
|
3
|
+
Not part of the public API -- use ``libei.ei``, ``libei.eis`` and
|
|
4
|
+
``libei.oeffis`` instead.
|
|
5
|
+
"""
|
|
3
6
|
|
|
4
7
|
from . import libei, libeis, liboeffis
|
|
5
8
|
|
libei/ei.py
CHANGED
|
@@ -87,6 +87,7 @@ class Error(Exception):
|
|
|
87
87
|
"""
|
|
88
88
|
|
|
89
89
|
def __init__(self, message: str, errno: int | None = None) -> None:
|
|
90
|
+
"""Record the failure message and, where libei reported one, errno."""
|
|
90
91
|
super().__init__(message)
|
|
91
92
|
self.message = message
|
|
92
93
|
self.errno = errno
|
|
@@ -199,6 +200,12 @@ class _LogPriority(enum.IntEnum):
|
|
|
199
200
|
|
|
200
201
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
201
202
|
class XkbModifiersEvent:
|
|
203
|
+
"""XKB modifier state from a KEYBOARD_MODIFIERS event.
|
|
204
|
+
|
|
205
|
+
``depressed``/``latched``/``locked`` are XKB's own mod-state bitmasks;
|
|
206
|
+
``group`` is the active keyboard layout group.
|
|
207
|
+
"""
|
|
208
|
+
|
|
202
209
|
depressed: int
|
|
203
210
|
latched: int
|
|
204
211
|
locked: int
|
|
@@ -207,48 +214,76 @@ class XkbModifiersEvent:
|
|
|
207
214
|
|
|
208
215
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
209
216
|
class KeyEvent:
|
|
217
|
+
"""Key code and press state from a KEYBOARD_KEY event.
|
|
218
|
+
|
|
219
|
+
``key`` is a Linux ``KEY_*`` code, the same numbering
|
|
220
|
+
:meth:`Device.keyboard_key` sends.
|
|
221
|
+
"""
|
|
222
|
+
|
|
210
223
|
key: int
|
|
211
224
|
is_press: bool
|
|
212
225
|
|
|
213
226
|
|
|
214
227
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
215
228
|
class ButtonEvent:
|
|
229
|
+
"""Button code and press state from a BUTTON_BUTTON event.
|
|
230
|
+
|
|
231
|
+
``button`` is a Linux ``BTN_*`` code, the same numbering
|
|
232
|
+
:meth:`Device.button` sends.
|
|
233
|
+
"""
|
|
234
|
+
|
|
216
235
|
button: int
|
|
217
236
|
is_press: bool
|
|
218
237
|
|
|
219
238
|
|
|
220
239
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
221
240
|
class PointerEvent:
|
|
241
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
242
|
+
|
|
222
243
|
dx: float
|
|
223
244
|
dy: float
|
|
224
245
|
|
|
225
246
|
|
|
226
247
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
227
248
|
class PointerAbsoluteEvent:
|
|
249
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
250
|
+
|
|
251
|
+
In the logical pixel space of the :class:`Region` the emitting device
|
|
252
|
+
covers -- see :meth:`Event.pointer_absolute_event`.
|
|
253
|
+
"""
|
|
254
|
+
|
|
228
255
|
x: float
|
|
229
256
|
y: float
|
|
230
257
|
|
|
231
258
|
|
|
232
259
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
233
260
|
class ScrollEvent:
|
|
261
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
262
|
+
|
|
234
263
|
dx: float
|
|
235
264
|
dy: float
|
|
236
265
|
|
|
237
266
|
|
|
238
267
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
239
268
|
class ScrollDiscreteEvent:
|
|
269
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
270
|
+
|
|
240
271
|
dx: int
|
|
241
272
|
dy: int
|
|
242
273
|
|
|
243
274
|
|
|
244
275
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
245
276
|
class ScrollStopEvent:
|
|
277
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
278
|
+
|
|
246
279
|
stop_x: bool
|
|
247
280
|
stop_y: bool
|
|
248
281
|
|
|
249
282
|
|
|
250
283
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
251
284
|
class TouchEvent:
|
|
285
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
286
|
+
|
|
252
287
|
touchid: int
|
|
253
288
|
x: float
|
|
254
289
|
y: float
|
|
@@ -256,17 +291,26 @@ class TouchEvent:
|
|
|
256
291
|
|
|
257
292
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
258
293
|
class TouchUpEvent:
|
|
294
|
+
"""Touch id and cancellation flag from a TOUCH_UP event.
|
|
295
|
+
|
|
296
|
+
See :meth:`Event.touch_up_event` for when ``is_cancel`` is trustworthy.
|
|
297
|
+
"""
|
|
298
|
+
|
|
259
299
|
touchid: int
|
|
260
300
|
is_cancel: bool
|
|
261
301
|
|
|
262
302
|
|
|
263
303
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
264
304
|
class TextUtf8Event:
|
|
305
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
306
|
+
|
|
265
307
|
text: str
|
|
266
308
|
|
|
267
309
|
|
|
268
310
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
269
311
|
class TextKeysymEvent:
|
|
312
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
313
|
+
|
|
270
314
|
keysym: int
|
|
271
315
|
is_press: bool
|
|
272
316
|
|
|
@@ -545,9 +589,10 @@ class Device(CObject):
|
|
|
545
589
|
return self
|
|
546
590
|
|
|
547
591
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
548
|
-
"""Commit the events queued since the last frame as one logical
|
|
549
|
-
|
|
550
|
-
time.
|
|
592
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
593
|
+
|
|
594
|
+
``timestamp`` defaults to the context's current time.
|
|
595
|
+
"""
|
|
551
596
|
if timestamp is None:
|
|
552
597
|
timestamp = _capi.libei.now(_capi.libei.device_get_context(self))
|
|
553
598
|
_capi.libei.device_frame(self, timestamp)
|
|
@@ -564,8 +609,10 @@ class Device(CObject):
|
|
|
564
609
|
return self
|
|
565
610
|
|
|
566
611
|
def button(self, button: int, is_press: bool) -> Device:
|
|
567
|
-
"""Queue a button press or release.
|
|
568
|
-
|
|
612
|
+
"""Queue a button press or release.
|
|
613
|
+
|
|
614
|
+
``button`` is a Linux ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``).
|
|
615
|
+
"""
|
|
569
616
|
_capi.libei.device_button_button(self, button, is_press)
|
|
570
617
|
return self
|
|
571
618
|
|
|
@@ -760,8 +807,11 @@ class Event(CObject):
|
|
|
760
807
|
|
|
761
808
|
@property
|
|
762
809
|
def event_type(self) -> EventType | int:
|
|
763
|
-
"""The event's type
|
|
764
|
-
|
|
810
|
+
"""The event's type.
|
|
811
|
+
|
|
812
|
+
Returns a raw int for a value newer than this package's
|
|
813
|
+
:class:`EventType` table -- see its docstring.
|
|
814
|
+
"""
|
|
765
815
|
raw = _capi.libei.event_get_type(self)
|
|
766
816
|
try:
|
|
767
817
|
return EventType(raw)
|
|
@@ -1009,11 +1059,14 @@ class Context(CObject):
|
|
|
1009
1059
|
_wrappable = False
|
|
1010
1060
|
|
|
1011
1061
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1062
|
+
"""Wrap a freshly created ``struct ei *`` and arm its log handler.
|
|
1063
|
+
|
|
1064
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1065
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1066
|
+
actually reaches this constructor -- Context (and Sender/
|
|
1067
|
+
Receiver) are always built directly via cls(cls._new()) in
|
|
1068
|
+
create_for_fd()/create_for_socket().
|
|
1069
|
+
"""
|
|
1017
1070
|
super().__init__(pointer, _adopt=_adopt)
|
|
1018
1071
|
self._name: str | None = None
|
|
1019
1072
|
_capi.libei.log_set_handler(self, _log_handler)
|
|
@@ -1081,7 +1134,8 @@ class Context(CObject):
|
|
|
1081
1134
|
"""Use an already-connected socket as the transport.
|
|
1082
1135
|
|
|
1083
1136
|
libei takes ownership of a raw int fd and closes it itself; a file
|
|
1084
|
-
object is duplicated first, so the caller's own object stays valid.
|
|
1137
|
+
object is duplicated first, so the caller's own object stays valid.
|
|
1138
|
+
"""
|
|
1085
1139
|
# ei_setup_backend_fd() takes ownership of the fd and will close it
|
|
1086
1140
|
# itself. A raw int is assumed to already be one the caller is
|
|
1087
1141
|
# handing off (matching what eis.Eis.add_client()/oeffis.eis_fd
|
|
@@ -1099,7 +1153,8 @@ class Context(CObject):
|
|
|
1099
1153
|
"""Connect to an EIS socket by path.
|
|
1100
1154
|
|
|
1101
1155
|
``None`` uses ``$LIBEI_SOCKET``; a relative path is resolved
|
|
1102
|
-
against ``$XDG_RUNTIME_DIR``.
|
|
1156
|
+
against ``$XDG_RUNTIME_DIR``.
|
|
1157
|
+
"""
|
|
1103
1158
|
encoded = os.fspath(path).encode("utf-8") if path else None
|
|
1104
1159
|
err = _capi.libei.setup_backend_socket(self, encoded)
|
|
1105
1160
|
if err < 0:
|
|
@@ -1162,7 +1217,8 @@ class Context(CObject):
|
|
|
1162
1217
|
"""Read from the connection and queue any events that arrive.
|
|
1163
1218
|
|
|
1164
1219
|
Call this before iterating :attr:`events`, which only drains what
|
|
1165
|
-
is already queued.
|
|
1220
|
+
is already queued.
|
|
1221
|
+
"""
|
|
1166
1222
|
_capi.libei.dispatch(self)
|
|
1167
1223
|
|
|
1168
1224
|
|
libei/eis.py
CHANGED
|
@@ -55,6 +55,7 @@ class Error(Exception):
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
def __init__(self, message: str, errno: int | None = None) -> None:
|
|
58
|
+
"""Record the failure message and, where libeis reported one, errno."""
|
|
58
59
|
super().__init__(message)
|
|
59
60
|
self.message = message
|
|
60
61
|
self.errno = errno
|
|
@@ -166,48 +167,75 @@ class _LogPriority(enum.IntEnum):
|
|
|
166
167
|
|
|
167
168
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
168
169
|
class KeyEvent:
|
|
170
|
+
"""Key code and press state received on a KEYBOARD_KEY event.
|
|
171
|
+
|
|
172
|
+
``key`` is a Linux ``KEY_*`` code, as sent by the client's
|
|
173
|
+
``ei.Device.keyboard_key``.
|
|
174
|
+
"""
|
|
175
|
+
|
|
169
176
|
key: int
|
|
170
177
|
is_press: bool
|
|
171
178
|
|
|
172
179
|
|
|
173
180
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
174
181
|
class ButtonEvent:
|
|
182
|
+
"""Button code and press state received on a BUTTON_BUTTON event.
|
|
183
|
+
|
|
184
|
+
``button`` is a Linux ``BTN_*`` code, as sent by the client's
|
|
185
|
+
``ei.Device.button``.
|
|
186
|
+
"""
|
|
187
|
+
|
|
175
188
|
button: int
|
|
176
189
|
is_press: bool
|
|
177
190
|
|
|
178
191
|
|
|
179
192
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
180
193
|
class PointerEvent:
|
|
194
|
+
"""Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
|
|
195
|
+
|
|
181
196
|
dx: float
|
|
182
197
|
dy: float
|
|
183
198
|
|
|
184
199
|
|
|
185
200
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
186
201
|
class PointerAbsoluteEvent:
|
|
202
|
+
"""Absolute position from a POINTER_MOTION_ABSOLUTE event.
|
|
203
|
+
|
|
204
|
+
In the logical pixel space of the region the sending device covers.
|
|
205
|
+
"""
|
|
206
|
+
|
|
187
207
|
x: float
|
|
188
208
|
y: float
|
|
189
209
|
|
|
190
210
|
|
|
191
211
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
192
212
|
class ScrollEvent:
|
|
213
|
+
"""Smooth scroll deltas from a SCROLL_DELTA event."""
|
|
214
|
+
|
|
193
215
|
dx: float
|
|
194
216
|
dy: float
|
|
195
217
|
|
|
196
218
|
|
|
197
219
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
198
220
|
class ScrollDiscreteEvent:
|
|
221
|
+
"""Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
|
|
222
|
+
|
|
199
223
|
dx: int
|
|
200
224
|
dy: int
|
|
201
225
|
|
|
202
226
|
|
|
203
227
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
204
228
|
class ScrollStopEvent:
|
|
229
|
+
"""Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
|
|
230
|
+
|
|
205
231
|
stop_x: bool
|
|
206
232
|
stop_y: bool
|
|
207
233
|
|
|
208
234
|
|
|
209
235
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
210
236
|
class TouchEvent:
|
|
237
|
+
"""Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
|
|
238
|
+
|
|
211
239
|
touchid: int
|
|
212
240
|
x: float
|
|
213
241
|
y: float
|
|
@@ -215,17 +243,23 @@ class TouchEvent:
|
|
|
215
243
|
|
|
216
244
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
217
245
|
class TouchUpEvent:
|
|
246
|
+
"""Touch id and cancellation flag from a TOUCH_UP event."""
|
|
247
|
+
|
|
218
248
|
touchid: int
|
|
219
249
|
is_cancel: bool
|
|
220
250
|
|
|
221
251
|
|
|
222
252
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
223
253
|
class TextUtf8Event:
|
|
254
|
+
"""UTF-8 text carried by a TEXT_UTF8 event."""
|
|
255
|
+
|
|
224
256
|
text: str
|
|
225
257
|
|
|
226
258
|
|
|
227
259
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
228
260
|
class TextKeysymEvent:
|
|
261
|
+
"""Keysym and press state from a TEXT_KEYSYM event."""
|
|
262
|
+
|
|
229
263
|
keysym: int
|
|
230
264
|
is_press: bool
|
|
231
265
|
|
|
@@ -576,8 +610,10 @@ class Device(CObject):
|
|
|
576
610
|
return self
|
|
577
611
|
|
|
578
612
|
def frame(self, timestamp: int | None = None) -> Device:
|
|
579
|
-
"""Commit the events queued since the last frame as one logical
|
|
580
|
-
|
|
613
|
+
"""Commit the events queued since the last frame as one logical hardware event.
|
|
614
|
+
|
|
615
|
+
``timestamp`` defaults to the context's current time.
|
|
616
|
+
"""
|
|
581
617
|
if timestamp is None:
|
|
582
618
|
timestamp = _capi.libeis.now(_capi.libeis.device_get_context(self))
|
|
583
619
|
_capi.libeis.device_frame(self, timestamp)
|
|
@@ -837,8 +873,11 @@ class Event(CObject):
|
|
|
837
873
|
|
|
838
874
|
@property
|
|
839
875
|
def event_type(self) -> EventType | int:
|
|
840
|
-
"""The event's type
|
|
841
|
-
|
|
876
|
+
"""The event's type.
|
|
877
|
+
|
|
878
|
+
Returns a raw int for a value newer than this package's
|
|
879
|
+
:class:`EventType` table -- see its docstring.
|
|
880
|
+
"""
|
|
842
881
|
raw = _capi.libeis.event_get_type(self)
|
|
843
882
|
try:
|
|
844
883
|
return EventType(raw)
|
|
@@ -1069,10 +1108,13 @@ class Eis(CObject):
|
|
|
1069
1108
|
_wrappable = False
|
|
1070
1109
|
|
|
1071
1110
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1111
|
+
"""Wrap a freshly created ``struct eis *`` and arm its log handler.
|
|
1112
|
+
|
|
1113
|
+
_adopt is accepted and forwarded for signature consistency with
|
|
1114
|
+
CObject, but with _wrappable = False, _get_or_create() never
|
|
1115
|
+
actually reaches this constructor -- Eis is always built directly
|
|
1116
|
+
via cls(cls._new()) in create_for_fd().
|
|
1117
|
+
"""
|
|
1076
1118
|
super().__init__(pointer, _adopt=_adopt)
|
|
1077
1119
|
_capi.libeis.log_set_handler(self, _log_handler)
|
|
1078
1120
|
_capi.libeis.log_set_priority(self, _LogPriority.DEBUG)
|
|
@@ -1146,7 +1188,8 @@ class Eis(CObject):
|
|
|
1146
1188
|
"""Read from the connection and queue any events that arrive.
|
|
1147
1189
|
|
|
1148
1190
|
Call this before iterating :attr:`events`, which only drains what
|
|
1149
|
-
is already queued.
|
|
1191
|
+
is already queued.
|
|
1192
|
+
"""
|
|
1150
1193
|
_capi.libeis.dispatch(self)
|
|
1151
1194
|
|
|
1152
1195
|
def add_client(self) -> int:
|
|
@@ -1172,14 +1215,16 @@ class Eis(CObject):
|
|
|
1172
1215
|
|
|
1173
1216
|
@classmethod
|
|
1174
1217
|
def create_for_fd(cls, flags: Sequence[Flag] = ()) -> Eis:
|
|
1175
|
-
"""Create a server using the fd backend
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1218
|
+
"""Create a server using the fd backend.
|
|
1219
|
+
|
|
1220
|
+
The one real compositors use, since it keeps each client's fd
|
|
1221
|
+
private rather than exposing a connectable socket path. Call
|
|
1222
|
+
:meth:`add_client` once per connection you want to accept.
|
|
1179
1223
|
|
|
1180
1224
|
``flags`` are applied here rather than left to the caller because
|
|
1181
1225
|
:meth:`set_flag` has to run before the backend is set up, and this
|
|
1182
|
-
method does both.
|
|
1226
|
+
method does both.
|
|
1227
|
+
"""
|
|
1183
1228
|
server = cls(cls._new())
|
|
1184
1229
|
for flag in flags:
|
|
1185
1230
|
server.set_flag(flag)
|
|
@@ -1190,9 +1235,12 @@ class Eis(CObject):
|
|
|
1190
1235
|
|
|
1191
1236
|
@classmethod
|
|
1192
1237
|
def create_for_socket(cls, path: Path, flags: Sequence[Flag] = ()) -> Eis:
|
|
1193
|
-
"""Create a server listening on a Unix socket
|
|
1194
|
-
|
|
1195
|
-
|
|
1238
|
+
"""Create a server listening on a Unix socket.
|
|
1239
|
+
|
|
1240
|
+
As a compositor would (this is the path a real
|
|
1241
|
+
``ei_setup_backend_socket()`` client connects to). See
|
|
1242
|
+
:meth:`create_for_fd` on ``flags``.
|
|
1243
|
+
"""
|
|
1196
1244
|
server = cls(cls._new())
|
|
1197
1245
|
for flag in flags:
|
|
1198
1246
|
server.set_flag(flag)
|
libei/oeffis.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"""Pythonic wrapper around liboeffis
|
|
2
|
-
|
|
1
|
+
"""Pythonic wrapper around liboeffis.
|
|
2
|
+
|
|
3
|
+
Negotiates an EIS connection through the
|
|
4
|
+
``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal.
|
|
3
5
|
|
|
4
6
|
This is the path a sandboxed or otherwise non-privileged client uses to get
|
|
5
7
|
an EI socket: it asks the portal, the user is shown a consent dialog, and on
|
|
@@ -52,6 +54,7 @@ class DisconnectedError(Exception):
|
|
|
52
54
|
"""The portal session ended unexpectedly (error, or denied by the user)."""
|
|
53
55
|
|
|
54
56
|
def __init__(self, message: str | None) -> None:
|
|
57
|
+
"""Record why the session ended."""
|
|
55
58
|
super().__init__(message)
|
|
56
59
|
self.message = message
|
|
57
60
|
|
|
@@ -60,6 +63,7 @@ class SessionClosedError(DisconnectedError):
|
|
|
60
63
|
"""The portal explicitly closed the session (not necessarily an error)."""
|
|
61
64
|
|
|
62
65
|
def __init__(self) -> None:
|
|
66
|
+
"""Build the fixed "Session closed" message."""
|
|
63
67
|
super().__init__(message="Session closed")
|
|
64
68
|
|
|
65
69
|
|
|
@@ -100,6 +104,7 @@ class Oeffis:
|
|
|
100
104
|
"""
|
|
101
105
|
|
|
102
106
|
def __init__(self) -> None:
|
|
107
|
+
"""Create the underlying liboeffis context (no portal call yet)."""
|
|
103
108
|
pointer = _capi.liboeffis.new(None)
|
|
104
109
|
if not pointer:
|
|
105
110
|
raise DisconnectedError("oeffis_new() returned NULL")
|
libei/portal.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
"""Negotiate an EIS connection by driving a portal directly over D-Bus
|
|
2
|
-
|
|
1
|
+
"""Negotiate an EIS connection by driving a portal directly over D-Bus.
|
|
2
|
+
|
|
3
|
+
Not through :mod:`libei.oeffis` -- see that module instead for the simpler,
|
|
4
|
+
liboeffis-backed path.
|
|
3
5
|
|
|
4
6
|
Two portals, two directions. :class:`RemoteDesktopSession` negotiates
|
|
5
7
|
``org.freedesktop.portal.RemoteDesktop`` to *inject* input; below it,
|
|
@@ -69,6 +71,7 @@ Three things worth knowing before building on this:
|
|
|
69
71
|
|
|
70
72
|
from __future__ import annotations
|
|
71
73
|
|
|
74
|
+
import contextlib
|
|
72
75
|
import enum
|
|
73
76
|
import logging
|
|
74
77
|
import os
|
|
@@ -104,7 +107,7 @@ _MIN_REMOTE_DESKTOP_VERSION = 2 # ConnectToEIS needs v2+
|
|
|
104
107
|
_MIN_INPUT_CAPTURE_VERSION = 2 # CreateSession2 is a v2-only method
|
|
105
108
|
|
|
106
109
|
_DEFAULT_TIMEOUT = 60.0
|
|
107
|
-
"""Seconds to wait for one portal round trip. Generous, because a
|
|
110
|
+
"""Seconds to wait for one portal round trip. Generous, because a user has
|
|
108
111
|
to see and answer the consent dialog `Start` raises -- but bounded, because
|
|
109
112
|
the alternative is a caller wedged forever if the portal dies after
|
|
110
113
|
accepting the call and before sending its `Response`."""
|
|
@@ -156,11 +159,12 @@ class PortalTimeoutError(PortalError):
|
|
|
156
159
|
|
|
157
160
|
Distinct from a decline: the portal accepted the call and then never
|
|
158
161
|
sent its ``Response`` signal. Most often the consent dialog is simply
|
|
159
|
-
still waiting for a
|
|
162
|
+
still waiting for a user, so raise the timeout rather than treating
|
|
160
163
|
this as a failure if that is expected.
|
|
161
164
|
"""
|
|
162
165
|
|
|
163
166
|
def __init__(self, step: str, timeout: float) -> None:
|
|
167
|
+
"""Name which step timed out and after how long."""
|
|
164
168
|
super().__init__(f"{step} did not answer within {timeout:g}s")
|
|
165
169
|
self.step = step
|
|
166
170
|
self.timeout = timeout
|
|
@@ -175,6 +179,7 @@ class PortalDeniedError(PortalError):
|
|
|
175
179
|
"""
|
|
176
180
|
|
|
177
181
|
def __init__(self, step: str, message: str | None = None) -> None:
|
|
182
|
+
"""Name which step was refused and, where the portal gave one, why."""
|
|
178
183
|
super().__init__(message or f"{step} was not approved")
|
|
179
184
|
self.step = step
|
|
180
185
|
self.message = message
|
|
@@ -481,10 +486,16 @@ def _request(
|
|
|
481
486
|
try:
|
|
482
487
|
loop.run()
|
|
483
488
|
finally:
|
|
484
|
-
#
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
|
|
489
|
+
# on_timeout() returns False, which is GLib's own signal to
|
|
490
|
+
# deregister a fired one-shot source -- removing it again
|
|
491
|
+
# here trips a real "Source ID N was not found when
|
|
492
|
+
# attempting to remove it" warning, the same bug already
|
|
493
|
+
# fixed for _wait_for_signal below. Only remove it when the
|
|
494
|
+
# *Response* woke the loop instead; leaving a live source in
|
|
495
|
+
# that case holds a reference to this closure and fires it
|
|
496
|
+
# into a dead loop on some later request.
|
|
497
|
+
if not timed_out:
|
|
498
|
+
GLib.source_remove(timeout_source)
|
|
488
499
|
finally:
|
|
489
500
|
for subscription in subscriptions:
|
|
490
501
|
connection.signal_unsubscribe(subscription)
|
|
@@ -584,6 +595,11 @@ class RemoteDesktopSession:
|
|
|
584
595
|
session_handle: str | None = None,
|
|
585
596
|
busname: str = _BUS_NAME,
|
|
586
597
|
) -> None:
|
|
598
|
+
"""Hold a negotiated session's handle, connection and EIS fd.
|
|
599
|
+
|
|
600
|
+
Not for direct use -- built by :meth:`negotiate` once ``Start`` and
|
|
601
|
+
``ConnectToEIS`` have both already succeeded.
|
|
602
|
+
"""
|
|
587
603
|
self._connection = connection
|
|
588
604
|
self._eis_fd: int | None = eis_fd
|
|
589
605
|
self._session_handle = session_handle
|
|
@@ -687,10 +703,9 @@ class RemoteDesktopSession:
|
|
|
687
703
|
return
|
|
688
704
|
eis_fd = getattr(self, "_eis_fd", None)
|
|
689
705
|
if eis_fd is not None:
|
|
690
|
-
|
|
706
|
+
# an exception here is only printed to stderr anyway
|
|
707
|
+
with contextlib.suppress(OSError):
|
|
691
708
|
os.close(eis_fd)
|
|
692
|
-
except OSError:
|
|
693
|
-
pass # an exception here is only printed to stderr anyway
|
|
694
709
|
|
|
695
710
|
@classmethod
|
|
696
711
|
def negotiate(
|
|
@@ -819,7 +834,7 @@ class RemoteDesktopSession:
|
|
|
819
834
|
timeout=timeout,
|
|
820
835
|
)
|
|
821
836
|
except BaseException:
|
|
822
|
-
# BaseException, not Exception: `Start` blocks on a
|
|
837
|
+
# BaseException, not Exception: `Start` blocks on a user
|
|
823
838
|
# answering a consent dialog, so Ctrl-C during that wait is a
|
|
824
839
|
# routine way out of this function -- and it strands an
|
|
825
840
|
# approved session exactly as a decline does.
|
|
@@ -980,10 +995,10 @@ def _wait_for_signal(
|
|
|
980
995
|
busname: str,
|
|
981
996
|
interface: str,
|
|
982
997
|
signal: str,
|
|
983
|
-
|
|
998
|
+
session_handle: str,
|
|
984
999
|
timeout: float | None,
|
|
985
1000
|
) -> tuple[Any, ...]:
|
|
986
|
-
"""Block for one emission of ``signal``
|
|
1001
|
+
"""Block for one emission of ``signal`` for ``session_handle``, unpacked.
|
|
987
1002
|
|
|
988
1003
|
Unlike `_request`, nothing here *triggers* the signal: ``Activated`` and
|
|
989
1004
|
``Deactivated`` fire whenever the compositor decides a pointer barrier
|
|
@@ -991,7 +1006,23 @@ def _wait_for_signal(
|
|
|
991
1006
|
before this is even called (a long-enabled session, subscribed to
|
|
992
1007
|
late) or not for a long time. ``timeout=None`` waits indefinitely --
|
|
993
1008
|
the read a caller wants when there is nothing else useful to do but
|
|
994
|
-
wait for a
|
|
1009
|
+
wait for a user to move the pointer.
|
|
1010
|
+
|
|
1011
|
+
**These signals are emitted on the portal object, not on the session
|
|
1012
|
+
object.** Subscribing with the session handle as the D-Bus object path
|
|
1013
|
+
-- the obvious reading of "a signal for this session", and what this
|
|
1014
|
+
did until 2026-09-09 -- matches nothing, delivers nothing, and is
|
|
1015
|
+
indistinguishable from a compositor that never fires the signal at all:
|
|
1016
|
+
it cost this project a live investigation across two GNOME versions, a
|
|
1017
|
+
standalone C reproducer and an upstream Mutter bug report before an
|
|
1018
|
+
xdg-desktop-portal developer pointed out the mistake. The session is
|
|
1019
|
+
identified by the signal's own first argument instead (``o
|
|
1020
|
+
session_handle``, per the portal spec), so the subscription is on
|
|
1021
|
+
`_OBJECT_PATH` and the filtering happens here, on the payload. It is
|
|
1022
|
+
deliberately not done with ``signal_subscribe``'s ``arg0`` filter: the
|
|
1023
|
+
D-Bus specification restricts plain ``arg0=`` match rules to arguments
|
|
1024
|
+
of type STRING, and this one is an OBJECT_PATH, which is the same shape
|
|
1025
|
+
of silent non-delivery all over again.
|
|
995
1026
|
"""
|
|
996
1027
|
loop = GLib.MainLoop()
|
|
997
1028
|
result: dict[str, Any] = {}
|
|
@@ -1008,7 +1039,22 @@ def _wait_for_signal(
|
|
|
1008
1039
|
) -> None:
|
|
1009
1040
|
if result: # a subscription that outlives its own wait can fire twice
|
|
1010
1041
|
return
|
|
1011
|
-
|
|
1042
|
+
args = params.unpack()
|
|
1043
|
+
if args[0] != session_handle:
|
|
1044
|
+
# Logged, not silently dropped: this filter can hide a signal
|
|
1045
|
+
# that *did* arrive just as effectively as the wrong-path
|
|
1046
|
+
# subscription above did, and the two are indistinguishable
|
|
1047
|
+
# from the outside -- both look exactly like a compositor that
|
|
1048
|
+
# never fired. One debug line is what tells them apart.
|
|
1049
|
+
logger.debug(
|
|
1050
|
+
"ignoring %s for session %s while waiting for %s",
|
|
1051
|
+
signal,
|
|
1052
|
+
args[0],
|
|
1053
|
+
session_handle,
|
|
1054
|
+
)
|
|
1055
|
+
return
|
|
1056
|
+
logger.debug("received %s for session %s", signal, session_handle)
|
|
1057
|
+
result["args"] = args
|
|
1012
1058
|
loop.quit()
|
|
1013
1059
|
|
|
1014
1060
|
def on_timeout() -> bool:
|
|
@@ -1021,12 +1067,20 @@ def _wait_for_signal(
|
|
|
1021
1067
|
busname,
|
|
1022
1068
|
interface,
|
|
1023
1069
|
signal,
|
|
1024
|
-
|
|
1070
|
+
_OBJECT_PATH,
|
|
1025
1071
|
None,
|
|
1026
1072
|
Gio.DBusSignalFlags.NONE,
|
|
1027
1073
|
on_signal,
|
|
1028
1074
|
None,
|
|
1029
1075
|
)
|
|
1076
|
+
logger.debug(
|
|
1077
|
+
"waiting up to %s for %s.%s on %s for session %s",
|
|
1078
|
+
"forever" if timeout is None else f"{timeout:g}s",
|
|
1079
|
+
interface,
|
|
1080
|
+
signal,
|
|
1081
|
+
_OBJECT_PATH,
|
|
1082
|
+
session_handle,
|
|
1083
|
+
)
|
|
1030
1084
|
try:
|
|
1031
1085
|
if not result:
|
|
1032
1086
|
timeout_source = None
|
|
@@ -1035,7 +1089,16 @@ def _wait_for_signal(
|
|
|
1035
1089
|
try:
|
|
1036
1090
|
loop.run()
|
|
1037
1091
|
finally:
|
|
1038
|
-
|
|
1092
|
+
# A fired timeout source has already deregistered itself --
|
|
1093
|
+
# on_timeout() returns False, which is GLib's signal to
|
|
1094
|
+
# remove it -- so only remove it here when the *signal*
|
|
1095
|
+
# woke the loop instead. Removing it unconditionally trips
|
|
1096
|
+
# a real GLib warning ("Source ID N was not found when
|
|
1097
|
+
# attempting to remove it"), confirmed live: it fired on
|
|
1098
|
+
# every real timeout this session hit, never against
|
|
1099
|
+
# test_portal.py's fake GLib.source_remove, which just
|
|
1100
|
+
# clears pending_timeout with no complaint either way.
|
|
1101
|
+
if timeout_source is not None and not timed_out:
|
|
1039
1102
|
GLib.source_remove(timeout_source)
|
|
1040
1103
|
finally:
|
|
1041
1104
|
connection.signal_unsubscribe(subscription)
|
|
@@ -1049,8 +1112,9 @@ def _wait_for_signal(
|
|
|
1049
1112
|
|
|
1050
1113
|
|
|
1051
1114
|
class Activation(NamedTuple):
|
|
1052
|
-
"""One ``Activated`` signal's payload
|
|
1053
|
-
|
|
1115
|
+
"""One ``Activated`` signal's payload.
|
|
1116
|
+
|
|
1117
|
+
See :meth:`InputCaptureSession.wait_for_activation`.
|
|
1054
1118
|
"""
|
|
1055
1119
|
|
|
1056
1120
|
activation_id: int
|
|
@@ -1101,10 +1165,10 @@ class InputCaptureSession:
|
|
|
1101
1165
|
|
|
1102
1166
|
**Never live-tested.** Every other class in this module that talks to a
|
|
1103
1167
|
real portal carries a hand-verification note in its own docstring; this
|
|
1104
|
-
one does not, because verifying it means a
|
|
1105
|
-
consent dialog *and* accepting that their pointer will be diverted
|
|
1106
|
-
from their own desktop for the length of the test -- not something
|
|
1107
|
-
trigger without asking first, unlike everything else here. Designed
|
|
1168
|
+
one does not, because verifying it means a developer clicking through
|
|
1169
|
+
the consent dialog *and* accepting that their pointer will be diverted
|
|
1170
|
+
away from their own desktop for the length of the test -- not something
|
|
1171
|
+
to trigger without asking first, unlike everything else here. Designed
|
|
1108
1172
|
against ``/usr/share/dbus-1/interfaces/org.freedesktop.portal.
|
|
1109
1173
|
InputCapture.xml`` (the shipped portal spec, not the header alone) and
|
|
1110
1174
|
unit-tested against a fake connection reproducing that spec's documented
|
|
@@ -1120,6 +1184,11 @@ class InputCaptureSession:
|
|
|
1120
1184
|
restore_token: str | None,
|
|
1121
1185
|
busname: str = _BUS_NAME,
|
|
1122
1186
|
) -> None:
|
|
1187
|
+
"""Hold a negotiated capture session's handle, connection and EIS fd.
|
|
1188
|
+
|
|
1189
|
+
Not for direct use -- built by :meth:`negotiate` once ``ConnectToEIS``
|
|
1190
|
+
has already succeeded.
|
|
1191
|
+
"""
|
|
1123
1192
|
self._connection = connection
|
|
1124
1193
|
self._session_handle: str | None = session_handle
|
|
1125
1194
|
self._eis_fd: int | None = eis_fd
|
|
@@ -1320,13 +1389,24 @@ class InputCaptureSession:
|
|
|
1320
1389
|
)
|
|
1321
1390
|
if code != 0:
|
|
1322
1391
|
raise PortalDeniedError("SetPointerBarriers")
|
|
1323
|
-
|
|
1392
|
+
failed = list(results.get("failed_barriers", []))
|
|
1393
|
+
# A partially-refused set is the quiet failure here: the call
|
|
1394
|
+
# succeeds, some barriers stand, and an edge the caller believes is
|
|
1395
|
+
# armed simply never triggers. Callers see only the returned list,
|
|
1396
|
+
# which they may or may not act on -- this says it either way.
|
|
1397
|
+
logger.debug(
|
|
1398
|
+
"SetPointerBarriers: %d requested %s, refused: %s",
|
|
1399
|
+
len(barriers),
|
|
1400
|
+
[b[0] for b in barriers],
|
|
1401
|
+
failed or "none",
|
|
1402
|
+
)
|
|
1403
|
+
return failed
|
|
1324
1404
|
|
|
1325
1405
|
def wait_for_activation(self, timeout: float | None = None) -> Activation:
|
|
1326
1406
|
"""Block until the compositor activates capture, or ``timeout``.
|
|
1327
1407
|
|
|
1328
1408
|
Only returns once a real barrier crossing has been reported --
|
|
1329
|
-
which, on hardware, means a
|
|
1409
|
+
which, on hardware, means a user moved a physical pointer across
|
|
1330
1410
|
one. There is no way to trigger this synthetically (see the class
|
|
1331
1411
|
docstring's third paragraph), so this call can legitimately hang
|
|
1332
1412
|
until someone does that, and `timeout=None` -- the default -- waits
|
|
@@ -1418,10 +1498,8 @@ class InputCaptureSession:
|
|
|
1418
1498
|
return
|
|
1419
1499
|
eis_fd = getattr(self, "_eis_fd", None)
|
|
1420
1500
|
if eis_fd is not None:
|
|
1421
|
-
|
|
1501
|
+
with contextlib.suppress(OSError):
|
|
1422
1502
|
os.close(eis_fd)
|
|
1423
|
-
except OSError:
|
|
1424
|
-
pass
|
|
1425
1503
|
|
|
1426
1504
|
@classmethod
|
|
1427
1505
|
def negotiate(
|
|
@@ -1590,7 +1668,7 @@ class InputCaptureSession:
|
|
|
1590
1668
|
timeout,
|
|
1591
1669
|
)
|
|
1592
1670
|
except BaseException:
|
|
1593
|
-
# BaseException, not Exception: Start blocks on a
|
|
1671
|
+
# BaseException, not Exception: Start blocks on a user
|
|
1594
1672
|
# answering a consent dialog, so Ctrl-C during that wait is a
|
|
1595
1673
|
# routine way out of this function -- and it strands an
|
|
1596
1674
|
# approved session exactly as a decline does.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
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.2`), 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
|
|
@@ -187,11 +187,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
187
|
portal paths are the part likeliest to come up short off Linux, since
|
|
188
188
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
189
189
|
- CPython 3.10 or newer (tested on 3.13)
|
|
190
|
-
- The native
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `libei.portal` only: PyGObject
|
|
194
|
-
|
|
190
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
191
|
+
supply. Which package provides them on your distribution — and what to do
|
|
192
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
193
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
194
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
195
195
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
196
196
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
197
197
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -224,22 +224,11 @@ Exactly what was run, when, and against which versions:
|
|
|
224
224
|
|
|
225
225
|
## Install
|
|
226
226
|
|
|
227
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
228
|
-
|
|
229
227
|
```sh
|
|
230
228
|
pip install python-libei
|
|
231
229
|
```
|
|
232
230
|
|
|
233
|
-
|
|
234
|
-
`pip show python-libei`, but `from libei import ei`.
|
|
235
|
-
|
|
236
|
-
Pure Python, no build step: the wheel is `py3-none-any` and ctypes talks to
|
|
237
|
-
the native libraries directly, so there is no compiler, no headers and no
|
|
238
|
-
`libei-devel` involved at install time. What `pip` does *not* bring is the
|
|
239
|
-
native libraries themselves -- see [Requirements](#requirements) above; on
|
|
240
|
-
Fedora, `sudo dnf install libei libeis liboeffis`.
|
|
241
|
-
|
|
242
|
-
To track `main` instead, or to hack on it, install from a checkout:
|
|
231
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
243
232
|
|
|
244
233
|
```sh
|
|
245
234
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -247,15 +236,11 @@ cd python-libei
|
|
|
247
236
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
248
237
|
```
|
|
249
238
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if not ei.is_available():
|
|
257
|
-
... # fall back to another input backend
|
|
258
|
-
```
|
|
239
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
240
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
241
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
242
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
243
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
259
244
|
|
|
260
245
|
## Concepts
|
|
261
246
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
libei/__init__.py,sha256=SwaEhWSSGzxTEOqlW6PsfGC24QWz2A2FUrktq4AztTM,1858
|
|
2
|
+
libei/_cobject.py,sha256=msrbviABSWjc5fKsXHSCg7nQ0Y4Etcq_Dkh0fblW-sY,12845
|
|
3
|
+
libei/ei.py,sha256=5LAvMDOYS0AdwEHpKKZ3Anky5yIcDvr926A7ydL_Vmk,48289
|
|
4
|
+
libei/eis.py,sha256=HepGyded_soT84GUu66iSz9IcGP6Bm_Y2750-YK9GDg,44912
|
|
5
|
+
libei/oeffis.py,sha256=MhUPKD2Sk2OzKikaztNoL5k1CresqjdY7EPWuCPCMIM,9477
|
|
6
|
+
libei/portal.py,sha256=dGOX5Rf5slbEKq-Ie0od4_ZgStd8UruwMbSnaOO2QI8,68682
|
|
7
|
+
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
libei/_capi/__init__.py,sha256=raISSWSV9hAcE92DcLCz-vUb1IPcsmxRZaS-iAoQJBE,211
|
|
9
|
+
libei/_capi/libei.py,sha256=gpp42bhqPonhVD2zmFWCl4rdk_lm4XvD_7W2Rw5RsuI,11308
|
|
10
|
+
libei/_capi/libeis.py,sha256=jQR0Z0YN9qa71mAcSJ__-UaoF8PsVsBs8OXwjfqstFk,13080
|
|
11
|
+
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
+
libei/_capi/loader.py,sha256=k7fb_Nz0fg5QkuLJ_duKXj8bESSktLQ2ZyS5LNQ5v2I,4689
|
|
13
|
+
python_libei-0.5.2.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
+
python_libei-0.5.2.dist-info/METADATA,sha256=IIL9EKM_LBc2PoqLzGQKu5sZHQ9l4ivyH5W3uq61CW0,20086
|
|
15
|
+
python_libei-0.5.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
+
python_libei-0.5.2.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
+
python_libei-0.5.2.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
libei/__init__.py,sha256=ZknUZWRNN4JfCH08NGpbBhqr_e2YZjHnk8B1aXe2J9Y,1858
|
|
2
|
-
libei/_cobject.py,sha256=msrbviABSWjc5fKsXHSCg7nQ0Y4Etcq_Dkh0fblW-sY,12845
|
|
3
|
-
libei/ei.py,sha256=Da3ezXnFPXtejlcJFH8bcE5f4pxKZXXqb6ltCh2zAdE,46704
|
|
4
|
-
libei/eis.py,sha256=aYWOCN1NlRchcWd6NjIUi1eYMFWJbp0UHMcQFEJ0ii4,43642
|
|
5
|
-
libei/oeffis.py,sha256=UcnDwLErFxB0xuktNpIzAK01ylpl6ZrmaGnWiUOcFOo,9302
|
|
6
|
-
libei/portal.py,sha256=LGRINrYQ-x46i0pzGOzpM6ZT3xek_AoExNIgR2BZRB8,64498
|
|
7
|
-
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
libei/_capi/__init__.py,sha256=3VxixYYlr_ZcuiK0GwrCpbE3VvEv_O990mJzdIJ8i74,209
|
|
9
|
-
libei/_capi/libei.py,sha256=gpp42bhqPonhVD2zmFWCl4rdk_lm4XvD_7W2Rw5RsuI,11308
|
|
10
|
-
libei/_capi/libeis.py,sha256=jQR0Z0YN9qa71mAcSJ__-UaoF8PsVsBs8OXwjfqstFk,13080
|
|
11
|
-
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
-
libei/_capi/loader.py,sha256=k7fb_Nz0fg5QkuLJ_duKXj8bESSktLQ2ZyS5LNQ5v2I,4689
|
|
13
|
-
python_libei-0.5.1.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
-
python_libei-0.5.1.dist-info/METADATA,sha256=Bg1meYzLmMAp9HqeMWbU4SzZZOurlSGzIR49ZmoJNgo,20475
|
|
15
|
-
python_libei-0.5.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
-
python_libei-0.5.1.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
-
python_libei-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|