python-libei 0.5.0__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 CHANGED
@@ -31,6 +31,6 @@ the full breakdown, including which features need which libei version.
31
31
  Beta: the API is not frozen.
32
32
  """
33
33
 
34
- __version__ = "0.5.0"
34
+ __version__ = "0.5.2"
35
35
 
36
36
  __all__ = ["__version__"]
libei/_capi/__init__.py CHANGED
@@ -1,5 +1,8 @@
1
- """Low-level ctypes bindings. Not part of the public API -- use
2
- ``libei.ei``, ``libei.eis`` and ``libei.oeffis`` instead."""
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
- hardware event. ``timestamp`` defaults to the context's current
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. ``button`` is a Linux
568
- ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``)."""
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, or a raw int for a value newer than this
764
- package's :class:`EventType` table -- see its docstring."""
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
- # _adopt is accepted and forwarded for signature consistency with
1013
- # CObject, but with _wrappable = False, _get_or_create() never
1014
- # actually reaches this constructor -- Context (and Sender/
1015
- # Receiver) are always built directly via cls(cls._new()) in
1016
- # create_for_fd()/create_for_socket().
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
- hardware event. ``timestamp`` defaults to the context's current time."""
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, or a raw int for a value newer than this
841
- package's :class:`EventType` table -- see its docstring."""
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
- # _adopt is accepted and forwarded for signature consistency with
1073
- # CObject, but with _wrappable = False, _get_or_create() never
1074
- # actually reaches this constructor -- Eis is always built directly
1075
- # via cls(cls._new()) in create_for_fd().
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 -- the one real compositors
1176
- use, since it keeps each client's fd private rather than exposing a
1177
- connectable socket path. Call :meth:`add_client` once per
1178
- connection you want to accept.
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, as a compositor
1194
- would (this is the path a real ``ei_setup_backend_socket()`` client
1195
- connects to). See :meth:`create_for_fd` on ``flags``."""
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 -- negotiates an EIS connection through
2
- the ``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal.
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
- rather than through :mod:`libei.oeffis`.
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 human has
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 human, so raise the timeout rather than treating
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
- # Removing an already-fired one-shot source is harmless
485
- # (GLib warns at most); leaking a live one holds a reference
486
- # to this closure and fires it into a dead loop later.
487
- GLib.source_remove(timeout_source)
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
- try:
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 human
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.
@@ -926,6 +941,53 @@ def _input_capture_version(
926
941
  return int(version)
927
942
 
928
943
 
944
+ def _create_input_capture_session_v1(
945
+ connection: Any,
946
+ Gio: Any,
947
+ GLib: Any,
948
+ busname: str,
949
+ types: int,
950
+ timeout: float,
951
+ ) -> str:
952
+ """Create an InputCapture session on a portal that predates v2.
953
+
954
+ v1 has no ``Start`` at all -- that method, like ``CreateSession2``, was
955
+ added in version 2. Here ``CreateSession`` itself carries
956
+ ``capabilities`` and raises the consent dialog, and the session is ready
957
+ for ``GetZones`` / ``SetPointerBarriers`` / ``Enable`` the moment its
958
+ ``Response`` arrives. ``ConnectToEIS`` is a v1 original, so the rest of
959
+ this class works against such a portal unchanged.
960
+
961
+ Returns the session handle. ``persist_mode`` and ``restore_token`` have
962
+ no v1 equivalent -- both are documented as version 2 additions to
963
+ ``Start``, which does not exist here -- so the caller must reject them
964
+ before getting this far rather than have them silently ignored.
965
+ """
966
+ code, results = _request(
967
+ connection,
968
+ Gio,
969
+ GLib,
970
+ busname,
971
+ _INPUT_CAPTURE,
972
+ "CreateSession",
973
+ "(sa{sv})",
974
+ ("",),
975
+ {
976
+ "session_handle_token": GLib.Variant("s", uuid.uuid4().hex),
977
+ "capabilities": GLib.Variant("u", int(types)),
978
+ },
979
+ timeout,
980
+ )
981
+ if code != 0:
982
+ raise PortalDeniedError(
983
+ "CreateSession", "the user declined the input-capture consent dialog"
984
+ )
985
+ session_handle = results.get("session_handle")
986
+ if not isinstance(session_handle, str):
987
+ raise PortalError("CreateSession returned no session_handle")
988
+ return session_handle
989
+
990
+
929
991
  def _wait_for_signal(
930
992
  connection: Any,
931
993
  Gio: Any,
@@ -933,10 +995,10 @@ def _wait_for_signal(
933
995
  busname: str,
934
996
  interface: str,
935
997
  signal: str,
936
- path: str,
998
+ session_handle: str,
937
999
  timeout: float | None,
938
1000
  ) -> tuple[Any, ...]:
939
- """Block for one emission of ``signal`` on ``path``, unpacked.
1001
+ """Block for one emission of ``signal`` for ``session_handle``, unpacked.
940
1002
 
941
1003
  Unlike `_request`, nothing here *triggers* the signal: ``Activated`` and
942
1004
  ``Deactivated`` fire whenever the compositor decides a pointer barrier
@@ -944,7 +1006,23 @@ def _wait_for_signal(
944
1006
  before this is even called (a long-enabled session, subscribed to
945
1007
  late) or not for a long time. ``timeout=None`` waits indefinitely --
946
1008
  the read a caller wants when there is nothing else useful to do but
947
- wait for a human to move the pointer.
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.
948
1026
  """
949
1027
  loop = GLib.MainLoop()
950
1028
  result: dict[str, Any] = {}
@@ -961,7 +1039,22 @@ def _wait_for_signal(
961
1039
  ) -> None:
962
1040
  if result: # a subscription that outlives its own wait can fire twice
963
1041
  return
964
- result["args"] = params.unpack()
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
965
1058
  loop.quit()
966
1059
 
967
1060
  def on_timeout() -> bool:
@@ -974,12 +1067,20 @@ def _wait_for_signal(
974
1067
  busname,
975
1068
  interface,
976
1069
  signal,
977
- path,
1070
+ _OBJECT_PATH,
978
1071
  None,
979
1072
  Gio.DBusSignalFlags.NONE,
980
1073
  on_signal,
981
1074
  None,
982
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
+ )
983
1084
  try:
984
1085
  if not result:
985
1086
  timeout_source = None
@@ -988,7 +1089,16 @@ def _wait_for_signal(
988
1089
  try:
989
1090
  loop.run()
990
1091
  finally:
991
- if timeout_source is not None:
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:
992
1102
  GLib.source_remove(timeout_source)
993
1103
  finally:
994
1104
  connection.signal_unsubscribe(subscription)
@@ -1002,8 +1112,9 @@ def _wait_for_signal(
1002
1112
 
1003
1113
 
1004
1114
  class Activation(NamedTuple):
1005
- """One ``Activated`` signal's payload -- see
1006
- :meth:`InputCaptureSession.wait_for_activation`.
1115
+ """One ``Activated`` signal's payload.
1116
+
1117
+ See :meth:`InputCaptureSession.wait_for_activation`.
1007
1118
  """
1008
1119
 
1009
1120
  activation_id: int
@@ -1054,10 +1165,10 @@ class InputCaptureSession:
1054
1165
 
1055
1166
  **Never live-tested.** Every other class in this module that talks to a
1056
1167
  real portal carries a hand-verification note in its own docstring; this
1057
- one does not, because verifying it means a human clicking through the
1058
- consent dialog *and* accepting that their pointer will be diverted away
1059
- from their own desktop for the length of the test -- not something to
1060
- 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
1061
1172
  against ``/usr/share/dbus-1/interfaces/org.freedesktop.portal.
1062
1173
  InputCapture.xml`` (the shipped portal spec, not the header alone) and
1063
1174
  unit-tested against a fake connection reproducing that spec's documented
@@ -1073,6 +1184,11 @@ class InputCaptureSession:
1073
1184
  restore_token: str | None,
1074
1185
  busname: str = _BUS_NAME,
1075
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
+ """
1076
1192
  self._connection = connection
1077
1193
  self._session_handle: str | None = session_handle
1078
1194
  self._eis_fd: int | None = eis_fd
@@ -1273,13 +1389,24 @@ class InputCaptureSession:
1273
1389
  )
1274
1390
  if code != 0:
1275
1391
  raise PortalDeniedError("SetPointerBarriers")
1276
- return list(results.get("failed_barriers", []))
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
1277
1404
 
1278
1405
  def wait_for_activation(self, timeout: float | None = None) -> Activation:
1279
1406
  """Block until the compositor activates capture, or ``timeout``.
1280
1407
 
1281
1408
  Only returns once a real barrier crossing has been reported --
1282
- which, on hardware, means a human moved a physical pointer across
1409
+ which, on hardware, means a user moved a physical pointer across
1283
1410
  one. There is no way to trigger this synthetically (see the class
1284
1411
  docstring's third paragraph), so this call can legitimately hang
1285
1412
  until someone does that, and `timeout=None` -- the default -- waits
@@ -1371,10 +1498,8 @@ class InputCaptureSession:
1371
1498
  return
1372
1499
  eis_fd = getattr(self, "_eis_fd", None)
1373
1500
  if eis_fd is not None:
1374
- try:
1501
+ with contextlib.suppress(OSError):
1375
1502
  os.close(eis_fd)
1376
- except OSError:
1377
- pass
1378
1503
 
1379
1504
  @classmethod
1380
1505
  def negotiate(
@@ -1392,11 +1517,18 @@ class InputCaptureSession:
1392
1517
  Blocks until ``CreateSession2`` -> ``Start`` -> ``ConnectToEIS``
1393
1518
  resolves, prompting the user for consent along the way unless
1394
1519
  ``restore_token`` lets the portal skip that. Raises
1395
- :class:`PortalVersionError` if the compositor's InputCapture portal
1396
- predates ``CreateSession2`` (needs v2+ -- this module never speaks
1397
- the deprecated v1 ``CreateSession``), :class:`PortalDeniedError` if
1398
- ``Start`` is declined, and :class:`PortalTimeoutError` if any one
1399
- round trip exceeds ``timeout`` seconds.
1520
+ :class:`PortalDeniedError` if consent is declined, and
1521
+ :class:`PortalTimeoutError` if any one round trip exceeds ``timeout``
1522
+ seconds.
1523
+
1524
+ A portal older than v2 has neither ``CreateSession2`` nor ``Start``,
1525
+ and is negotiated through the deprecated v1 ``CreateSession``
1526
+ instead -- one call that carries ``capabilities`` and raises the
1527
+ consent dialog itself. Everything after negotiation is identical;
1528
+ ``ConnectToEIS`` is a v1 original. The one thing v1 cannot do is
1529
+ persist: ``persist_mode`` and ``restore_token`` were added to
1530
+ ``Start``, so passing either against such a portal raises
1531
+ :class:`PortalVersionError` rather than being quietly ignored.
1400
1532
 
1401
1533
  Returns before anything is actually captured: :meth:`set_pointer_barriers`
1402
1534
  and :meth:`enable` still have to be called, and even then nothing
@@ -1435,62 +1567,95 @@ class InputCaptureSession:
1435
1567
  raise PortalError(f"cannot reach the session bus: {exc}") from exc
1436
1568
 
1437
1569
  version = _input_capture_version(connection, Gio, GLib, busname, timeout)
1438
- if version < _MIN_INPUT_CAPTURE_VERSION:
1570
+ # A version below 2 is not a portal to give up on -- it is a portal
1571
+ # that speaks the older half of the same interface. Only
1572
+ # `CreateSession2` and `Start` are version 2 additions; `GetZones`,
1573
+ # `SetPointerBarriers`, `Enable`, `Disable`, `Release` and, crucially,
1574
+ # `ConnectToEIS` are all v1 originals, so everything this class does
1575
+ # after negotiation works either way and only session creation forks.
1576
+ #
1577
+ # This is not hypothetical. xdg-desktop-portal-gnome 50 reports
1578
+ # version 0 -- it registers the full impl interface and never sets the
1579
+ # property -- and calling `CreateSession2` against it fails with
1580
+ # `UnknownMethod`, while v1 `CreateSession` is dispatched normally.
1581
+ # Note that its introspection XML advertises `CreateSession2` anyway:
1582
+ # that XML is static, so it says nothing about what the frontend will
1583
+ # actually dispatch. The `version` property is the only usable signal,
1584
+ # which is why it is read first and believed.
1585
+ legacy = version < _MIN_INPUT_CAPTURE_VERSION
1586
+
1587
+ if legacy and (persist_mode != PersistMode.NONE or restore_token is not None):
1439
1588
  raise PortalVersionError(
1440
- f"InputCapture version {version} is too old for "
1441
- f"CreateSession2 (need {_MIN_INPUT_CAPTURE_VERSION}+)"
1589
+ f"InputCapture version {version} has no Start method, and "
1590
+ f"persist_mode/restore_token are version "
1591
+ f"{_MIN_INPUT_CAPTURE_VERSION} additions to it -- this portal "
1592
+ f"cannot persist a session. Retry with persist_mode=NONE and "
1593
+ f"no restore_token to negotiate a one-shot session."
1442
1594
  )
1443
1595
 
1444
1596
  if capabilities == DeviceType.ALL_DEVICES:
1445
1597
  types = _ALL_DEVICE_TYPES
1446
1598
  else:
1447
1599
  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
1600
 
1468
- # Past this point a session exists inside xdg-desktop-portal, and
1469
- # nothing else can close it -- see RemoteDesktopSession.negotiate's
1470
- # identical reasoning, which this mirrors line for line.
1471
- try:
1472
- options: dict[str, Any] = {"capabilities": GLib.Variant("u", int(types))}
1473
- if persist_mode != PersistMode.NONE:
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(
1601
+ if legacy:
1602
+ session_handle = _create_input_capture_session_v1(
1603
+ connection, Gio, GLib, busname, int(types), timeout
1604
+ )
1605
+ else:
1606
+ reply = _call_sync(
1478
1607
  connection,
1479
1608
  Gio,
1480
1609
  GLib,
1481
1610
  busname,
1611
+ _OBJECT_PATH,
1482
1612
  _INPUT_CAPTURE,
1483
- "Start",
1484
- "(osa{sv})",
1485
- (session_handle, ""),
1486
- options,
1487
- timeout,
1613
+ "CreateSession2",
1614
+ GLib.Variant(
1615
+ "(a{sv})",
1616
+ ({"session_handle_token": GLib.Variant("s", uuid.uuid4().hex)},),
1617
+ ),
1618
+ None,
1619
+ int(timeout * 1000),
1488
1620
  )
1489
- if code != 0:
1490
- raise PortalDeniedError(
1491
- "Start", "the user declined the input-capture consent dialog"
1621
+ (results,) = reply.unpack()
1622
+ session_handle = results.get("session_handle")
1623
+ if not isinstance(session_handle, str):
1624
+ raise PortalError("CreateSession2 returned no session_handle")
1625
+
1626
+ # Past this point a session exists inside xdg-desktop-portal, and
1627
+ # nothing else can close it -- see RemoteDesktopSession.negotiate's
1628
+ # identical reasoning, which this mirrors line for line.
1629
+ try:
1630
+ # v1 asked for consent and capabilities in CreateSession itself,
1631
+ # and has no Start to call; it can therefore never issue a
1632
+ # restore token either.
1633
+ new_restore_token = None
1634
+ if not legacy:
1635
+ options: dict[str, Any] = {
1636
+ "capabilities": GLib.Variant("u", int(types))
1637
+ }
1638
+ if persist_mode != PersistMode.NONE:
1639
+ options["persist_mode"] = GLib.Variant("u", int(persist_mode))
1640
+ if restore_token is not None:
1641
+ options["restore_token"] = GLib.Variant("s", restore_token)
1642
+ code, start_results = _request(
1643
+ connection,
1644
+ Gio,
1645
+ GLib,
1646
+ busname,
1647
+ _INPUT_CAPTURE,
1648
+ "Start",
1649
+ "(osa{sv})",
1650
+ (session_handle, ""),
1651
+ options,
1652
+ timeout,
1492
1653
  )
1493
- new_restore_token = start_results.get("restore_token")
1654
+ if code != 0:
1655
+ raise PortalDeniedError(
1656
+ "Start", "the user declined the input-capture consent dialog"
1657
+ )
1658
+ new_restore_token = start_results.get("restore_token")
1494
1659
 
1495
1660
  eis_fd = _call_for_fd(
1496
1661
  connection,
@@ -1503,7 +1668,7 @@ class InputCaptureSession:
1503
1668
  timeout,
1504
1669
  )
1505
1670
  except BaseException:
1506
- # BaseException, not Exception: Start blocks on a human
1671
+ # BaseException, not Exception: Start blocks on a user
1507
1672
  # answering a consent dialog, so Ctrl-C during that wait is a
1508
1673
  # routine way out of this function -- and it strands an
1509
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.0
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.0`), published on [PyPI](https://pypi.org/project/python-libei/)
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 libraries: on Fedora, `sudo dnf install libei libeis liboeffis`;
191
- on FreeBSD, `pkg install libei` (the `x11/libei` port), which supplies all
192
- three sonames including `liboeffis`
193
- - `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
194
- whatever GObject-introspection libraries your distro needs for `Gio` --
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
- The distribution is named `python-libei`, the import is `libei` -- so
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
- Importing is always safe, even where the native libraries are missing — they
251
- are loaded on first use, not at import. Check before you rely on them:
252
-
253
- ```python
254
- from libei import ei
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=ovsIL7-rEp8LMbiWdmlGSoU6r4lNS_stMLULlkFgwGg,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=-OAg_g0Q6s0BNN2B6bzfyD2T5N_dCuHbSpqtcL8dCUk,60488
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.0.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
14
- python_libei-0.5.0.dist-info/METADATA,sha256=YDry3cY8Id0Z9bwQh_27SZkR8U8Ntf-S2t7Elf2nojo,20475
15
- python_libei-0.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
- python_libei-0.5.0.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
17
- python_libei-0.5.0.dist-info/RECORD,,