Pyjo-Reactor-Gevent 0.1.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.
Pyjo/Reactor/Gevent.py ADDED
@@ -0,0 +1,565 @@
1
+ """
2
+ Pyjo.Reactor.Gevent - Low-level event reactor on gevent
3
+ =======================================================
4
+ ::
5
+
6
+ from Pyjo.Reactor.Gevent import Gevent
7
+
8
+ # Watch if handle becomes readable or writable
9
+ reactor = Gevent()
10
+ reactor.io(first, lambda reactor, writable: print(
11
+ 'First handle is writable' if writable else 'First handle is readable'
12
+ ))
13
+
14
+ # Or import the module, as in Google Python Style Guide
15
+
16
+ import Pyjo.Reactor.Gevent
17
+
18
+ # Watch if handle becomes readable or writable
19
+ reactor = Pyjo.Reactor.Gevent.new()
20
+ reactor.io(first, lambda reactor, writable: print(
21
+ 'First handle is writable' if writable else 'First handle is readable'
22
+ ))
23
+
24
+ # Change to watching only if handle becomes writable
25
+ reactor.watch(first, False, True)
26
+
27
+ # Watch a file descriptor if it becomes readable
28
+ reactor.io(fd, lambda reactor, writable: print(
29
+ 'Second handle is writable' if writable else 'Second handle is readable'
30
+ )).watch(fd, True, False)
31
+
32
+ # Add a timer
33
+ def timeout(reactor):
34
+ reactor.remove(first)
35
+ reactor.remove(fd)
36
+ print('Timeout!')
37
+
38
+ reactor.timer(15, timeout)
39
+
40
+ # Start reactor if necessary
41
+ if not reactor.is_running:
42
+ reactor.start()
43
+
44
+ :mod:`Pyjo.Reactor.Gevent` is a low-level event reactor based on the hub of
45
+ `gevent <https://www.gevent.org>`_, whose loop of libev or libuv watches the
46
+ handles and runs the timers, so that the servers of Pyjoyment, as
47
+ :mod:`Pyjo.Server.Daemon`, :mod:`Pyjo.Server.Prefork` and
48
+ :mod:`Pyjo.Server.Prethread`, and its user agent run along with the greenlets
49
+ of a program of gevent. ``PYJO_REACTOR`` selects it for :mod:`Pyjo.IOLoop`::
50
+
51
+ PYJO_REACTOR=Pyjo.Reactor.Gevent python myapp.py daemon
52
+
53
+ The reactor runs in the greenlet that calls :meth:`~Pyjo_Reactor_Gevent.start`
54
+ or :meth:`~Pyjo_Reactor_Gevent.one_tick`, the main one or any other, which
55
+ waits for the events in the hub, where the other greenlets of the program run
56
+ in the meantime, and the callbacks of the reactor run in that greenlet. A
57
+ callback can wait for a greenlet or for a function of gevent, as
58
+ ``gevent.sleep``, which stops the events of the reactor until it returns, but
59
+ not the other greenlets. A blocking request of :mod:`Pyjo.UserAgent`, which
60
+ runs an event loop of its own, waits in the greenlet that makes it, while the
61
+ other greenlets, a server of Pyjoyment among them, run. The reactor cannot run
62
+ in the hub itself, as in a callback of the loop of gevent, where gevent raises
63
+ ``BlockingSwitchOutError``.
64
+
65
+ A reactor uses the hub of the thread that makes it, so the threads of
66
+ :mod:`Pyjo.Server.Prethread` have hubs of their own, and any number of reactors
67
+ share the hub of a thread. The child of a fork, as a worker of
68
+ :mod:`Pyjo.Server.Prefork`, reinitializes the hub, as ``gevent.os.fork`` does.
69
+
70
+ The environment variable ``GEVENT_LOOP`` of gevent selects the loop, libev
71
+ (``libev-cext``, the default) or libuv (``libuv-cffi``), for which
72
+ `cffi <https://pypi.org/project/cffi/>`_ is installed too::
73
+
74
+ pip install Pyjo-Reactor-Gevent cffi
75
+ GEVENT_LOOP=libuv-cffi PYJO_REACTOR=Pyjo.Reactor.Gevent python myapp.py daemon
76
+
77
+ A program that calls ``gevent.monkey.patch_all()`` runs the reactor as well,
78
+ but its threads are greenlets then, and so is the thread of
79
+ :attr:`Pyjo.IOLoop.Pyjo_IOLoop.singleton`, which each greenlet has of its own,
80
+ and :mod:`Pyjo.Server.Prethread` and :mod:`Pyjo.IOLoop.ChildProcess` do not
81
+ work with the threads and the subprocesses of gevent.
82
+
83
+ Events
84
+ ------
85
+
86
+ :mod:`Pyjo.Reactor.Gevent` inherits all events from :mod:`Pyjo.Reactor.Poll`.
87
+
88
+ See also
89
+ --------
90
+
91
+ :mod:`Pyjo.Reactor.Poll`, :mod:`Pyjoyment`, https://github.com/pyjoyment/Pyjoyment.
92
+
93
+ Classes
94
+ -------
95
+ """
96
+
97
+ import Pyjo.Exception
98
+ import Pyjo.Reactor
99
+ import Pyjo.Reactor.Poll
100
+
101
+ from Pyjo.Reactor import Handle, IOCallback, TimerCallback
102
+
103
+ import importlib.metadata
104
+ import os
105
+ import threading
106
+ import weakref
107
+
108
+ from collections.abc import Callable
109
+ from typing import Any, Self, TypedDict
110
+
111
+ import gevent
112
+ import gevent.hub
113
+ import gevent.monkey
114
+
115
+ from greenlet import getcurrent
116
+
117
+
118
+ __version__ = importlib.metadata.version('Pyjo-Reactor-Gevent')
119
+ """The version of the distribution, of its metadata, which the workflow Automated version sets in pyproject.toml."""
120
+
121
+ # The events of a watcher of I/O, the same in libev and libuv
122
+ _READ = 1
123
+ _WRITE = 2
124
+
125
+ # The thread, not the greenlet that threading takes for it with gevent.monkey
126
+ _get_ident: Callable[[], int] = gevent.monkey.get_original('_thread', 'get_ident')
127
+
128
+
129
+ class _Timer(TypedDict):
130
+ cb: TimerCallback | None
131
+ after: float
132
+ recurring: bool
133
+ watcher: Any
134
+ seq: int
135
+
136
+
137
+ class Pyjo_Reactor_Gevent(Pyjo.Reactor.Poll.object):
138
+ """
139
+ :mod:`Pyjo.Reactor.Gevent` inherits all methods from
140
+ :mod:`Pyjo.Reactor.Poll` and implements the following new ones.
141
+ """
142
+
143
+ # Not in Mojolicious: gevent, whose hub runs the watchers and the timers of the reactor, and whose code a finalizer of
144
+ # the garbage collector may interrupt
145
+ event_loop_modules = ('gevent',)
146
+
147
+ _timers: dict[str, _Timer] # pyright: ignore[reportIncompatibleVariableOverride]
148
+
149
+ hub: Any
150
+ """::
151
+
152
+ hub = reactor.hub
153
+
154
+ The hub of gevent of the thread that made the reactor, whose loop watches
155
+ the handles and runs the timers.
156
+ """
157
+
158
+ def __init__(self) -> None:
159
+ super().__init__()
160
+ # gevent has no types for the hub and its loop
161
+ self.hub = gevent.get_hub()
162
+ loop = self._loop = self.hub.loop
163
+ self._libuv: bool = type(loop).__module__.startswith('gevent.libuv')
164
+ self._ref: weakref.ref[Pyjo_Reactor_Gevent] = weakref.ref(self)
165
+ ref = self._ref
166
+
167
+ # The watchers of the handles, which the loop calls in the hub, where the events wait for the greenlet that runs
168
+ # the reactor, and the order of the handles
169
+ self._watchers: dict[int, Any] = {}
170
+ self._ready: dict[int, int] = {}
171
+ self._suspended: set[int] = set()
172
+ self._order: dict[int, int] = {}
173
+
174
+ # The timers that are due, which wait for that greenlet too, and the order of the timers
175
+ self._fired: set[str] = set()
176
+ self._seq = 0
177
+ self._timers = {} # pyright: ignore[reportIncompatibleVariableOverride]
178
+
179
+ # The greenlets that wait for the events, and whether they are woken up already
180
+ self._waiting: list[Any] = []
181
+ self._woken = False
182
+
183
+ # The watchers of the loop are not referenced, so that the reactor does not keep a program of gevent alive when
184
+ # it does not run; one of these timers is, while a greenlet waits, so that the hub does not end, and wakes it up
185
+ # as the reactor of poll wakes up at least every half a second, or after one iteration of the loop, which polls
186
+ # the handles without waiting, when an event is ready already
187
+ self._keepalive: Any = loop.timer(0.5, 0.5)
188
+ self._poll: Any = loop.timer(0, 0)
189
+ self._keepalive_cb = lambda: _notify(ref)
190
+ weakref.finalize(self, _release, self.hub, self._watchers, self._timers, (self._keepalive, self._poll))
191
+
192
+ def again(self, id: str, after: float | None = None) -> None:
193
+ """::
194
+
195
+ reactor.again(id)
196
+ reactor.again(id, 0.5)
197
+
198
+ Restart timer and optionally change the invocation time. Note that this
199
+ method requires an active timer.
200
+ """
201
+ if (timer := self._timers.get(id)) is None:
202
+ Pyjo.Exception.object.throw('Timer not active')
203
+ if after is not None and after != timer['after']:
204
+ timer['after'] = after
205
+ if (watcher := timer['watcher']) is not None:
206
+ timer['watcher'] = None
207
+ watcher.stop()
208
+ watcher.close()
209
+ self._arm(id, timer)
210
+
211
+ def io(self, handle: Handle, cb: IOCallback) -> Self:
212
+ """::
213
+
214
+ reactor = reactor.io(handle, cb)
215
+
216
+ Watch handle for I/O events, invoking the callback whenever handle
217
+ becomes readable or writable.
218
+ """
219
+ fd = _fileno(handle)
220
+ self._unwatch(fd)
221
+ if fd not in self._io:
222
+ self._seq += 1
223
+ self._order[fd] = self._seq
224
+ self._io[fd] = {'cb': cb, 'mode': 0}
225
+ return self.watch(handle, True, True)
226
+
227
+ def one_tick(self) -> None:
228
+ """::
229
+
230
+ reactor.one_tick()
231
+
232
+ Run reactor until an event occurs or no events are being watched
233
+ anymore, while the other greenlets run.
234
+ """
235
+ running = self._running
236
+ if not running:
237
+ self._running = 1
238
+
239
+ try:
240
+ i = 0
241
+ while not i and self._running:
242
+ # The cleanups that finalizers deferred, before the reactor waits
243
+ if Pyjo.Reactor._DEFERRED.get(threading.get_ident()): # pyright: ignore[reportPrivateUsage]
244
+ Pyjo.Reactor._drain() # pyright: ignore[reportPrivateUsage]
245
+
246
+ # Stop automatically if there is nothing to watch
247
+ if not len(self._timers) and not len(self._io):
248
+ return self.stop()
249
+
250
+ self._wait(0 if self._ready or self._fired else 0.5)
251
+ i = self._dispatch()
252
+ finally:
253
+ if not running:
254
+ self._running = running
255
+
256
+ def recurring(self, after: float, cb: TimerCallback) -> str:
257
+ """::
258
+
259
+ id = reactor.recurring(0.25, cb)
260
+
261
+ Create a new recurring timer, invoking the callback repeatedly after a
262
+ given amount of time in seconds.
263
+ """
264
+ return self._timer(True, after, cb)
265
+
266
+ def remove(self, remove: Handle | str) -> bool:
267
+ """::
268
+
269
+ boolean = reactor.remove(handle)
270
+ boolean = reactor.remove(id)
271
+
272
+ Remove handle or timer.
273
+ """
274
+ if isinstance(remove, str):
275
+ if (timer := self._timers.pop(remove, None)) is None:
276
+ return False
277
+ self._fired.discard(remove)
278
+ if (watcher := timer['watcher']) is not None:
279
+ watcher.stop()
280
+ watcher.close()
281
+ return True
282
+ fd = _fileno(remove)
283
+ if self._io.pop(fd, None) is None:
284
+ return False
285
+ del self._order[fd]
286
+ self._unwatch(fd)
287
+ return True
288
+
289
+ def reset(self) -> None:
290
+ """::
291
+
292
+ reactor.reset()
293
+
294
+ Remove all handles and timers.
295
+ """
296
+ _stop(self._watchers, self._timers, ())
297
+ self._ready.clear()
298
+ self._suspended.clear()
299
+ self._order.clear()
300
+ self._fired.clear()
301
+ self._events = {}
302
+ self._io = {}
303
+ self._next_tick.clear()
304
+ self._next_timer = None
305
+ self._running = 0
306
+
307
+ def stop(self) -> None:
308
+ """::
309
+
310
+ reactor.stop()
311
+
312
+ Stop watching for I/O and timer events, also from another greenlet
313
+ than the one that runs the reactor.
314
+ """
315
+ self._running = 0
316
+ self._notify()
317
+
318
+ def timer(self, after: float, cb: TimerCallback) -> str:
319
+ """::
320
+
321
+ id = reactor.timer(0.5, cb)
322
+
323
+ Create a new timer, invoking the callback after a given amount of time
324
+ in seconds.
325
+ """
326
+ return self._timer(False, after, cb)
327
+
328
+ def watch(self, handle: Handle, read: bool, write: bool) -> Self:
329
+ """::
330
+
331
+ reactor = reactor.watch(handle, readable, writable)
332
+
333
+ Change I/O events to watch handle for with true and false values. Note
334
+ that this method requires an active I/O watcher.
335
+ """
336
+ fd = _fileno(handle)
337
+ if (io := self._io.get(fd)) is None:
338
+ Pyjo.Exception.object.throw('I/O watcher not active')
339
+ mode = (_READ if read else 0) | (_WRITE if write else 0)
340
+ if mode == io['mode'] and fd not in self._suspended:
341
+ return self
342
+ io['mode'] = mode
343
+ self._suspended.discard(fd)
344
+ if (watcher := self._watchers.get(fd)) is not None:
345
+ watcher.stop()
346
+ if not mode:
347
+ return self
348
+ if watcher is None:
349
+ # libuv sets a handle to the non-blocking mode when it makes the watcher, and never back, where a terminal,
350
+ # which Pyjo.IOLoop.Stdio reads and writes in the blocking mode, shares the mode with the shell
351
+ blocking = self._libuv and os.get_blocking(fd)
352
+ watcher = self._watchers[fd] = self._loop.io(fd, mode, ref=False)
353
+ if blocking:
354
+ os.set_blocking(fd, True)
355
+ else:
356
+ watcher.events = mode
357
+ watcher.start(_io_ready, self._ref, fd, pass_events=True)
358
+ return self
359
+
360
+ # A timer of no time is due at once, without a timer of the loop, as the reactor of poll runs the ones that a callback
361
+ # of I/O adds after the I/O of the same tick, where the loop would run them after the I/O of the next one
362
+ def _arm(self, id: str, timer: _Timer) -> None:
363
+ if timer['after'] <= 0:
364
+ if (watcher := timer['watcher']) is not None:
365
+ watcher.stop()
366
+ self._fired.add(id)
367
+ self._notify()
368
+ return
369
+ self._fired.discard(id)
370
+ # The time of the loop is updated, as libev and libuv take the time of the start of the iteration, which the
371
+ # callbacks of the reactor may have taken long after
372
+ if (watcher := timer['watcher']) is None:
373
+ # The interval repeats the timer for again, which stops a timer without one; the callback stops a timer
374
+ # that is not recurring
375
+ watcher = timer['watcher'] = self._loop.timer(timer['after'], timer['after'], ref=False)
376
+ watcher.start(_timer_ready, self._ref, id, update=True)
377
+ else:
378
+ watcher.again(_timer_ready, self._ref, id, update=True)
379
+
380
+ # The events that are ready, the handles in the order in which they were added, as the reactor of poll has them,
381
+ # where libev calls the watchers in the reverse order of its backend, then the timers in the order in which they were
382
+ # made
383
+ def _dispatch(self) -> int:
384
+ i = 0
385
+ if self._ready:
386
+ ready, self._ready = self._ready, {}
387
+ order = self._order
388
+ for fd in sorted(ready, key=lambda fd: order.get(fd, 0)) if len(ready) > 1 else ready:
389
+ revents = ready[fd]
390
+ if revents & _READ and (io := self._io.get(fd)) is not None and io['mode'] & _READ:
391
+ i += 1
392
+ self._try('I/O watcher', io['cb'], False)
393
+ if revents & _WRITE and (io := self._io.get(fd)) is not None and io['mode'] & _WRITE:
394
+ i += 1
395
+ self._try('I/O watcher', io['cb'], True)
396
+
397
+ if self._fired:
398
+ fired, self._fired = self._fired, set()
399
+ timers = self._timers
400
+ for id in sorted(fired, key=lambda id: timers[id]['seq'] if id in timers else 0):
401
+ if (t := timers.get(id)) is None:
402
+ continue
403
+
404
+ # Recurring timer of no time, which is due again at the next tick
405
+ if t['recurring']:
406
+ if not t['after']:
407
+ self._fired.add(id)
408
+
409
+ # Normal timer
410
+ else:
411
+ self.remove(id)
412
+
413
+ if (cb := t['cb']) is not None:
414
+ i += 1
415
+ self._try('Timer', cb)
416
+ return i
417
+
418
+ # In the hub, from the loop
419
+ def _io_ready(self, fd: int, revents: int) -> None:
420
+ # An error of the handle, which libuv reports with a status, after which it watches the handle no more, while
421
+ # gevent takes it for watched still: readable and writable, as for poll, and watched again at the next wait, so
422
+ # that the callback is called for as long as the error lasts
423
+ error = revents < 0
424
+ if error:
425
+ revents = _READ | _WRITE
426
+ self._ready[fd] = self._ready.get(fd, 0) | revents
427
+
428
+ # Nobody waits for the reactor: the handle is not watched until somebody does, as the loop would report it on
429
+ # every iteration, which the other greenlets make
430
+ if (error or not self._waiting) and (watcher := self._watchers.get(fd)) is not None:
431
+ watcher.stop()
432
+ self._suspended.add(fd)
433
+ self._notify()
434
+
435
+ # The greenlets that wait are woken up after the events of the iteration of the loop, before it waits again, by a
436
+ # callback of the loop, which gevent runs then
437
+ def _notify(self) -> None:
438
+ if self._waiting and not self._woken:
439
+ self._woken = True
440
+ self._loop.run_callback(_resume, self._ref)
441
+
442
+ # In the hub, from the loop; only the greenlets that still wait, as a greenlet that woke up first may have killed
443
+ # another one, which waits for something else then
444
+ def _resume(self) -> None:
445
+ self._woken = False
446
+ for greenlet in list(self._waiting):
447
+ if greenlet in self._waiting:
448
+ greenlet.switch()
449
+
450
+ def _timer(self, recurring: bool, after: float, cb: TimerCallback) -> str:
451
+ id = self._id()
452
+ self._seq += 1
453
+ timer: _Timer = {'cb': cb, 'after': after, 'recurring': recurring, 'watcher': None, 'seq': self._seq}
454
+ self._timers[id] = timer
455
+ self._arm(id, timer)
456
+ return id
457
+
458
+ # In the hub, from the loop
459
+ def _timer_ready(self, id: str) -> None:
460
+ if (timer := self._timers.get(id)) is None:
461
+ return
462
+ if not timer['recurring'] and (watcher := timer['watcher']) is not None:
463
+ watcher.stop()
464
+ self._fired.add(id)
465
+ self._notify()
466
+
467
+ def _unwatch(self, fd: int) -> None:
468
+ self._ready.pop(fd, None)
469
+ self._suspended.discard(fd)
470
+ if (watcher := self._watchers.pop(fd, None)) is not None:
471
+ watcher.stop()
472
+ watcher.close()
473
+
474
+ # The greenlet waits in the hub, where the other greenlets run, until the loop has an event of the reactor, or the
475
+ # reactor stops; gevent raises BlockingSwitchOutError in the hub itself
476
+ def _wait(self, timeout: float) -> None:
477
+ current = getcurrent()
478
+ for fd in self._suspended:
479
+ if (watcher := self._watchers.get(fd)) is not None:
480
+ watcher.start(_io_ready, self._ref, fd, pass_events=True)
481
+ self._suspended.clear()
482
+
483
+ waiting = self._waiting
484
+ waiting.append(current)
485
+ if len(waiting) == 1:
486
+ self._keepalive.start(self._keepalive_cb, update=True)
487
+ if timeout <= 0 and not self._poll.active:
488
+ self._poll.start(self._keepalive_cb, update=True)
489
+ try:
490
+ self.hub.switch()
491
+ finally:
492
+ waiting.remove(current)
493
+ if not waiting:
494
+ self._keepalive.stop()
495
+ self._poll.stop()
496
+
497
+ # The cleanups that finalizers deferred, from any thread
498
+ def _wake(self) -> None:
499
+ self._loop.run_callback_threadsafe(_notify, self._ref)
500
+
501
+
502
+ # The callbacks of the loop refer to the reactor weakly, as the loop of the hub lives as long as the thread
503
+ def _io_ready(revents: int, ref: 'weakref.ref[Pyjo_Reactor_Gevent]', fd: int) -> None:
504
+ if (reactor := ref()) is not None:
505
+ reactor._io_ready(fd, revents) # pyright: ignore[reportPrivateUsage]
506
+
507
+
508
+ def _timer_ready(ref: 'weakref.ref[Pyjo_Reactor_Gevent]', id: str) -> None:
509
+ if (reactor := ref()) is not None:
510
+ reactor._timer_ready(id) # pyright: ignore[reportPrivateUsage]
511
+
512
+
513
+ def _notify(ref: 'weakref.ref[Pyjo_Reactor_Gevent]') -> None:
514
+ if (reactor := ref()) is not None:
515
+ reactor._notify() # pyright: ignore[reportPrivateUsage]
516
+
517
+
518
+ def _resume(ref: 'weakref.ref[Pyjo_Reactor_Gevent]') -> None:
519
+ if (reactor := ref()) is not None:
520
+ reactor._resume() # pyright: ignore[reportPrivateUsage]
521
+
522
+
523
+ # The child of a fork reinitializes the hub, which gevent.os.fork does, as os.fork of gevent.monkey
524
+ def _after_fork_in_child() -> None:
525
+ if not gevent.monkey.is_module_patched('os'):
526
+ gevent.hub.reinit()
527
+
528
+
529
+ def _fileno(handle: Handle) -> int:
530
+ fd = handle if isinstance(handle, int) else handle.fileno()
531
+ if fd < 0:
532
+ Pyjo.Exception.object.throw('Handle is closed')
533
+ return fd
534
+
535
+
536
+ # The watchers that an object leaves in the loop when it is freed, in the thread of the hub, as the garbage collector runs
537
+ # a finalizer in any thread
538
+ def _release(hub: Any, watchers: dict[int, Any], timers: dict[str, _Timer], aux: tuple[Any, ...]) -> None:
539
+ if hub.thread_ident == _get_ident():
540
+ _stop(watchers, timers, aux)
541
+ else:
542
+ hub.loop.run_callback_threadsafe(_stop, watchers, timers, aux)
543
+
544
+
545
+ def _stop(watchers: dict[int, Any], timers: dict[str, _Timer], aux: tuple[Any, ...]) -> None:
546
+ for watcher in watchers.values():
547
+ watcher.stop()
548
+ watcher.close()
549
+ watchers.clear()
550
+ for timer in timers.values():
551
+ if (watcher := timer['watcher']) is not None:
552
+ watcher.stop()
553
+ watcher.close()
554
+ timers.clear()
555
+ for watcher in aux:
556
+ watcher.stop()
557
+ watcher.close()
558
+
559
+
560
+ if hasattr(os, 'register_at_fork'):
561
+ os.register_at_fork(after_in_child=_after_fork_in_child)
562
+
563
+ new = Pyjo_Reactor_Gevent.new
564
+ object = Pyjo_Reactor_Gevent
565
+ Gevent = Pyjo_Reactor_Gevent
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.5
2
+ Name: Pyjo-Reactor-Gevent
3
+ Version: 0.1.0
4
+ Summary: Low level event reactor with gevent support for Pyjoyment.
5
+ Project-URL: Homepage, https://github.com/pyjoyment/Pyjo-Reactor-Gevent
6
+ Project-URL: Documentation, https://pyjoyment.readthedocs.io/projects/pyjo-reactor-gevent/en/stable/
7
+ Author-email: Piotr Roszatycki <piotr.roszatycki@gmail.com>
8
+ License-Expression: Artistic-2.0
9
+ License-File: LICENSE
10
+ Keywords: gevent,greenlet,pyjoyment,reactor
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Requires-Python: >=3.12
15
+ Requires-Dist: gevent>=26.9.0
16
+ Requires-Dist: pyjoyment>=0.3.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ [![Home](https://img.shields.io/badge/%F0%9F%8F%A0-Pyjoyment-02a7a0)](https://github.com/pyjoyment/Pyjoyment)
20
+ [![GitHub](https://img.shields.io/github/stars/pyjoyment/Pyjo-Reactor-Gevent?style=flat&label=GitHub%20%E2%AD%90&logo=github)](https://github.com/pyjoyment/Pyjo-Reactor-Gevent)
21
+ [![Documentation](https://img.shields.io/badge/docs-Read_the_Docs-blue?logo=readthedocs&logoColor=white)](https://pyjoyment.readthedocs.io/projects/pyjo-reactor-gevent/en/stable/)
22
+ [![License](https://img.shields.io/github/license/pyjoyment/Pyjo-Reactor-Gevent.svg)](https://github.com/pyjoyment/Pyjo-Reactor-Gevent/blob/main/LICENSE)
23
+ [![PyPI](https://img.shields.io/pypi/v/Pyjo-Reactor-Gevent?logo=pypi&logoColor=white)](https://pypi.org/project/Pyjo-Reactor-Gevent/)
24
+ [![Tests](https://github.com/pyjoyment/Pyjo-Reactor-Gevent/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/pyjoyment/Pyjo-Reactor-Gevent/actions/workflows/tests.yaml)
25
+ [![Trunk Check](https://github.com/pyjoyment/Pyjo-Reactor-Gevent/actions/workflows/trunk-check.yaml/badge.svg?branch=main)](https://github.com/pyjoyment/Pyjo-Reactor-Gevent/actions/workflows/trunk-check.yaml)
26
+
27
+ # Pyjo-Reactor-Gevent
28
+
29
+ <p align="center">
30
+ <picture>
31
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pyjoyment/Pyjoyment/main/Pyjoyment/resources/public/pyjo/logo-white-4x.png">
32
+ <img src="https://raw.githubusercontent.com/pyjoyment/Pyjoyment/main/Pyjoyment/resources/public/pyjo/logo-black-4x.png" alt="Pyjoyment" width="322">
33
+ </picture>
34
+ </p>
35
+
36
+ Low level event reactor with [gevent](https://www.gevent.org) support for
37
+ [Pyjoyment](https://github.com/pyjoyment/Pyjoyment).
38
+
39
+ `Pyjo.Reactor.Gevent` is a reactor of Pyjoyment on the hub of gevent, whose
40
+ loop of libev or libuv watches the handles and runs the timers, so that the
41
+ servers of Pyjoyment, `daemon`, `prefork` and `prethread`, and its user agent
42
+ run along with the greenlets of a program of gevent.
43
+
44
+ ## Installation
45
+
46
+ From [PyPI](https://pypi.org/project/Pyjo-Reactor-Gevent/), with
47
+ [Pyjoyment](https://pypi.org/project/Pyjoyment/) and
48
+ [gevent](https://pypi.org/project/gevent/), which it depends on:
49
+
50
+ ```sh
51
+ pip install Pyjo-Reactor-Gevent
52
+ ```
53
+
54
+ or, in a project managed with [uv](https://docs.astral.sh/uv/):
55
+
56
+ ```sh
57
+ uv add Pyjo-Reactor-Gevent
58
+ ```
59
+
60
+ The development version is installed from the Git repository:
61
+
62
+ ```sh
63
+ pip install git+https://github.com/pyjoyment/Pyjo-Reactor-Gevent
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ Select the reactor with `PYJO_REACTOR` before `Pyjo.IOLoop` is loaded:
69
+
70
+ ```sh
71
+ PYJO_REACTOR=Pyjo.Reactor.Gevent python myapp.py daemon
72
+ PYJO_REACTOR=Pyjo.Reactor.Gevent python myapp.py prefork
73
+ ```
74
+
75
+ The reactor runs in the greenlet that starts it, and the other greenlets of the
76
+ program run while it waits for events. A blocking request of the user agent
77
+ waits in the greenlet that makes it, without blocking the others.
78
+
79
+ gevent runs on libev by default. Its loop of libuv needs
80
+ [cffi](https://pypi.org/project/cffi/):
81
+
82
+ ```sh
83
+ pip install Pyjo-Reactor-Gevent cffi
84
+ GEVENT_LOOP=libuv-cffi PYJO_REACTOR=Pyjo.Reactor.Gevent python myapp.py daemon
85
+ ```
86
+
87
+ With `gevent.monkey.patch_all()`, the threads of the program are greenlets, so
88
+ each greenlet has a `Pyjo.IOLoop.singleton` of its own, and
89
+ `Pyjo.Server.Prethread` and `Pyjo.IOLoop.ChildProcess` do not work.
90
+
91
+ ## Examples
92
+
93
+ `examples/hello_world_gevent.py` runs `Pyjo.HelloWorld` on the server of
94
+ Pyjoyment on gevent until the signal `INT` or `TERM`, in a `with` block, which
95
+ closes the listen sockets of the server at its end:
96
+
97
+ ```python
98
+ import os
99
+
100
+
101
+ # The reactor of Pyjoyment on gevent, selected before Pyjo.IOLoop is loaded
102
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Gevent'
103
+
104
+ import Pyjo.HelloWorld # noqa: E402
105
+ import Pyjo.Server.Daemon # noqa: E402
106
+
107
+
108
+ # The server of Pyjoyment on gevent, until the signal INT or TERM, which closes its listen sockets at the end of the block
109
+ with Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://*:3000']) as daemon:
110
+ daemon.run()
111
+ ```
112
+
113
+ ```console
114
+ $ python examples/hello_world_gevent.py
115
+ Web application available at http://127.0.0.1:3000
116
+ ```
117
+
118
+ ```console
119
+ $ curl http://127.0.0.1:3000/
120
+ Your Pyjo is working!
121
+ ```
122
+
123
+ `examples/loopback_gevent.py` is a server of Pyjoyment and its user agent on
124
+ the hub of gevent, a blocking request, which runs the event loop until the
125
+ response, and the class of the reactor and whether it runs on the hub of
126
+ gevent, which Build checks with the wheel:
127
+
128
+ ```python
129
+ import os
130
+
131
+
132
+ # The reactor of Pyjoyment on gevent, selected before Pyjo.IOLoop is loaded
133
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Gevent'
134
+
135
+ import Pyjo.HelloWorld # noqa: E402
136
+ import Pyjo.IOLoop # noqa: E402
137
+ import Pyjo.Server.Daemon # noqa: E402
138
+ import Pyjo.UserAgent # noqa: E402
139
+
140
+ import gevent # noqa: E402
141
+
142
+
143
+ # The server, on a port that the system picks, and the user agent, in the same event loop, which close their sockets at
144
+ # the end of the block
145
+ with (
146
+ Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://127.0.0.1'], silent=True) as daemon,
147
+ Pyjo.UserAgent.new(ioloop=Pyjo.IOLoop.singleton) as ua,
148
+ ):
149
+ # A blocking request, which runs the event loop of the server on the hub of gevent until the response
150
+ tx = ua.get(f'http://127.0.0.1:{daemon.start().ports()[0]}/')
151
+ reactor = Pyjo.IOLoop.singleton.reactor
152
+ print(type(reactor).__name__, 'gevent' if getattr(reactor, 'hub', None) is gevent.get_hub() else 'other')
153
+ print(tx.result.code, tx.result.text)
154
+ ```
155
+
156
+ ```console
157
+ $ python examples/loopback_gevent.py
158
+ Pyjo_Reactor_Gevent gevent
159
+ 200 Your Pyjo is working!
160
+ ```
161
+
162
+ `examples/greenlets_gevent.py` is a server of Pyjoyment in the main greenlet,
163
+ along with two greenlets of the application that request it, one with a socket
164
+ of gevent and one with a blocking request of the user agent of Pyjoyment:
165
+
166
+ ```python
167
+ import os
168
+
169
+
170
+ # The reactor of Pyjoyment on gevent, selected before Pyjo.IOLoop is loaded
171
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Gevent'
172
+
173
+ import Pyjo.HelloWorld # noqa: E402
174
+ import Pyjo.IOLoop # noqa: E402
175
+ import Pyjo.Server.Daemon # noqa: E402
176
+ import Pyjo.UserAgent # noqa: E402
177
+
178
+ import gevent # noqa: E402
179
+ import gevent.socket # noqa: E402
180
+
181
+
182
+ # A greenlet with a socket of gevent, which waits in the hub of gevent, where the server of Pyjoyment runs
183
+ def with_socket(port):
184
+ with gevent.socket.create_connection(('127.0.0.1', port)) as sock:
185
+ sock.sendall(b'GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n')
186
+ response = b''
187
+ while b'\r\n' not in response and (chunk := sock.recv(4096)):
188
+ response += chunk
189
+ status = response.split(b'\r\n')[0].decode()
190
+ return f'gevent.socket: {status}'
191
+
192
+
193
+ # A greenlet with a blocking request of the user agent of Pyjoyment, whose event loop of its own waits in the greenlet
194
+ def with_user_agent(port):
195
+ with Pyjo.UserAgent.new() as ua:
196
+ tx = ua.get(f'http://127.0.0.1:{port}/')
197
+ return f'Pyjo.UserAgent: {tx.result.code} {tx.result.text}'
198
+
199
+
200
+ # The server, on a port that the system picks, which runs in the main greenlet until the greenlets are done, and closes its
201
+ # listen sockets at the end of the block
202
+ with Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://127.0.0.1'], silent=True) as daemon:
203
+ port = daemon.start().ports()[0]
204
+ greenlets = [gevent.spawn(with_socket, port), gevent.spawn(with_user_agent, port)]
205
+ gevent.spawn(lambda: (gevent.joinall(greenlets), Pyjo.IOLoop.stop()))
206
+ Pyjo.IOLoop.start()
207
+ for greenlet in greenlets:
208
+ print(greenlet.value)
209
+ ```
210
+
211
+ ```console
212
+ $ python examples/greenlets_gevent.py
213
+ gevent.socket: HTTP/1.1 200 OK
214
+ Pyjo.UserAgent: 200 Your Pyjo is working!
215
+ ```
216
+
217
+ ## Documentation
218
+
219
+ The API of the module and the examples are on
220
+ [Read the Docs](https://pyjoyment.readthedocs.io/projects/pyjo-reactor-gevent/en/stable/).
221
+
222
+ ## Status
223
+
224
+ A reactor of Pyjoyment in a distribution of its own, as
225
+ [Pyjo-Reactor-Twisted](https://github.com/pyjoyment/Pyjo-Reactor-Twisted) and
226
+ [Pyjo-Reactor-Uvloop](https://github.com/pyjoyment/Pyjo-Reactor-Uvloop).
@@ -0,0 +1,5 @@
1
+ Pyjo/Reactor/Gevent.py,sha256=OiwTIlFvSRSOO7F4k1n5dITVICpQ-p5MgzH77b3BKLw,21198
2
+ pyjo_reactor_gevent-0.1.0.dist-info/METADATA,sha256=2c-4nBCs3_0yayXsjx0WU_g69jGNBVEjOEWHu_42Pjk,8757
3
+ pyjo_reactor_gevent-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
4
+ pyjo_reactor_gevent-0.1.0.dist-info/licenses/LICENSE,sha256=wqf_RI5i8Of0e_6xwFe7wBhw72jXAkF_9N8Ny9ScaP4,8894
5
+ pyjo_reactor_gevent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,201 @@
1
+ The Artistic License 2.0
2
+
3
+ Copyright (c) 2000-2006, The Perl Foundation.
4
+
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ This license establishes the terms under which a given free software
11
+ Package may be copied, modified, distributed, and/or redistributed.
12
+ The intent is that the Copyright Holder maintains some artistic
13
+ control over the development of that Package while still keeping the
14
+ Package available as open source and free software.
15
+
16
+ You are always permitted to make arrangements wholly outside of this
17
+ license directly with the Copyright Holder of a given Package. If the
18
+ terms of this license do not permit the full use that you propose to
19
+ make of the Package, you should contact the Copyright Holder and seek
20
+ a different licensing arrangement.
21
+
22
+ Definitions
23
+
24
+ "Copyright Holder" means the individual(s) or organization(s)
25
+ named in the copyright notice for the entire Package.
26
+
27
+ "Contributor" means any party that has contributed code or other
28
+ material to the Package, in accordance with the Copyright Holder's
29
+ procedures.
30
+
31
+ "You" and "your" means any person who would like to copy,
32
+ distribute, or modify the Package.
33
+
34
+ "Package" means the collection of files distributed by the
35
+ Copyright Holder, and derivatives of that collection and/or of
36
+ those files. A given Package may consist of either the Standard
37
+ Version, or a Modified Version.
38
+
39
+ "Distribute" means providing a copy of the Package or making it
40
+ accessible to anyone else, or in the case of a company or
41
+ organization, to others outside of your company or organization.
42
+
43
+ "Distributor Fee" means any fee that you charge for Distributing
44
+ this Package or providing support for this Package to another
45
+ party. It does not mean licensing fees.
46
+
47
+ "Standard Version" refers to the Package if it has not been
48
+ modified, or has been modified only in ways explicitly requested
49
+ by the Copyright Holder.
50
+
51
+ "Modified Version" means the Package, if it has been changed, and
52
+ such changes were not explicitly requested by the Copyright
53
+ Holder.
54
+
55
+ "Original License" means this Artistic License as Distributed with
56
+ the Standard Version of the Package, in its current version or as
57
+ it may be modified by The Perl Foundation in the future.
58
+
59
+ "Source" form means the source code, documentation source, and
60
+ configuration files for the Package.
61
+
62
+ "Compiled" form means the compiled bytecode, object code, binary,
63
+ or any other form resulting from mechanical transformation or
64
+ translation of the Source form.
65
+
66
+
67
+ Permission for Use and Modification Without Distribution
68
+
69
+ (1) You are permitted to use the Standard Version and create and use
70
+ Modified Versions for any purpose without restriction, provided that
71
+ you do not Distribute the Modified Version.
72
+
73
+
74
+ Permissions for Redistribution of the Standard Version
75
+
76
+ (2) You may Distribute verbatim copies of the Source form of the
77
+ Standard Version of this Package in any medium without restriction,
78
+ either gratis or for a Distributor Fee, provided that you duplicate
79
+ all of the original copyright notices and associated disclaimers. At
80
+ your discretion, such verbatim copies may or may not include a
81
+ Compiled form of the Package.
82
+
83
+ (3) You may apply any bug fixes, portability changes, and other
84
+ modifications made available from the Copyright Holder. The resulting
85
+ Package will still be considered the Standard Version, and as such
86
+ will be subject to the Original License.
87
+
88
+
89
+ Distribution of Modified Versions of the Package as Source
90
+
91
+ (4) You may Distribute your Modified Version as Source (either gratis
92
+ or for a Distributor Fee, and with or without a Compiled form of the
93
+ Modified Version) provided that you clearly document how it differs
94
+ from the Standard Version, including, but not limited to, documenting
95
+ any non-standard features, executables, or modules, and provided that
96
+ you do at least ONE of the following:
97
+
98
+ (a) make the Modified Version available to the Copyright Holder
99
+ of the Standard Version, under the Original License, so that the
100
+ Copyright Holder may include your modifications in the Standard
101
+ Version.
102
+
103
+ (b) ensure that installation of your Modified Version does not
104
+ prevent the user installing or running the Standard Version. In
105
+ addition, the Modified Version must bear a name that is different
106
+ from the name of the Standard Version.
107
+
108
+ (c) allow anyone who receives a copy of the Modified Version to
109
+ make the Source form of the Modified Version available to others
110
+ under
111
+
112
+ (i) the Original License or
113
+
114
+ (ii) a license that permits the licensee to freely copy,
115
+ modify and redistribute the Modified Version using the same
116
+ licensing terms that apply to the copy that the licensee
117
+ received, and requires that the Source form of the Modified
118
+ Version, and of any works derived from it, be made freely
119
+ available in that license fees are prohibited but Distributor
120
+ Fees are allowed.
121
+
122
+
123
+ Distribution of Compiled Forms of the Standard Version
124
+ or Modified Versions without the Source
125
+
126
+ (5) You may Distribute Compiled forms of the Standard Version without
127
+ the Source, provided that you include complete instructions on how to
128
+ get the Source of the Standard Version. Such instructions must be
129
+ valid at the time of your distribution. If these instructions, at any
130
+ time while you are carrying out such distribution, become invalid, you
131
+ must provide new instructions on demand or cease further distribution.
132
+ If you provide valid instructions or cease distribution within thirty
133
+ days after you become aware that the instructions are invalid, then
134
+ you do not forfeit any of your rights under this license.
135
+
136
+ (6) You may Distribute a Modified Version in Compiled form without
137
+ the Source, provided that you comply with Section 4 with respect to
138
+ the Source of the Modified Version.
139
+
140
+
141
+ Aggregating or Linking the Package
142
+
143
+ (7) You may aggregate the Package (either the Standard Version or
144
+ Modified Version) with other packages and Distribute the resulting
145
+ aggregation provided that you do not charge a licensing fee for the
146
+ Package. Distributor Fees are permitted, and licensing fees for other
147
+ components in the aggregation are permitted. The terms of this license
148
+ apply to the use and Distribution of the Standard or Modified Versions
149
+ as included in the aggregation.
150
+
151
+ (8) You are permitted to link Modified and Standard Versions with
152
+ other works, to embed the Package in a larger work of your own, or to
153
+ build stand-alone binary or bytecode versions of applications that
154
+ include the Package, and Distribute the result without restriction,
155
+ provided the result does not expose a direct interface to the Package.
156
+
157
+
158
+ Items That are Not Considered Part of a Modified Version
159
+
160
+ (9) Works (including, but not limited to, modules and scripts) that
161
+ merely extend or make use of the Package, do not, by themselves, cause
162
+ the Package to be a Modified Version. In addition, such works are not
163
+ considered parts of the Package itself, and are not subject to the
164
+ terms of this license.
165
+
166
+
167
+ General Provisions
168
+
169
+ (10) Any use, modification, and distribution of the Standard or
170
+ Modified Versions is governed by this Artistic License. By using,
171
+ modifying or distributing the Package, you accept this license. Do not
172
+ use, modify, or distribute the Package, if you do not accept this
173
+ license.
174
+
175
+ (11) If your Modified Version has been derived from a Modified
176
+ Version made by someone other than you, you are nevertheless required
177
+ to ensure that your Modified Version complies with the requirements of
178
+ this license.
179
+
180
+ (12) This license does not grant you the right to use any trademark,
181
+ service mark, tradename, or logo of the Copyright Holder.
182
+
183
+ (13) This license includes the non-exclusive, worldwide,
184
+ free-of-charge patent license to make, have made, use, offer to sell,
185
+ sell, import and otherwise transfer the Package with respect to any
186
+ patent claims licensable by the Copyright Holder that are necessarily
187
+ infringed by the Package. If you institute patent litigation
188
+ (including a cross-claim or counterclaim) against any party alleging
189
+ that the Package constitutes direct or contributory patent
190
+ infringement, then this Artistic License to you shall terminate on the
191
+ date that such litigation is filed.
192
+
193
+ (14) Disclaimer of Warranty:
194
+ THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
195
+ IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
196
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
197
+ NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
198
+ LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
199
+ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
200
+ DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
201
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.