Pyjo-Reactor-Twisted 0.2.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.
@@ -0,0 +1,475 @@
1
+ """
2
+ Pyjo.Reactor.Twisted - Low-level event reactor with Twisted support
3
+ ===================================================================
4
+ ::
5
+
6
+ from Pyjo.Reactor.Twisted import Twisted
7
+
8
+ # Watch if handle becomes readable or writable
9
+ reactor = Twisted()
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.Twisted
17
+
18
+ # Watch if handle becomes readable or writable
19
+ reactor = Pyjo.Reactor.Twisted.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.Twisted` is a low-level event reactor based on the reactor
45
+ of `Twisted <https://twisted.org>`_, the one that the application installed
46
+ before, or else the reactor of poll of Twisted, or of select where there is no
47
+ poll or it does not work well, as on macOS. Its default on Linux, the reactor
48
+ of epoll, is not installed, as epoll keeps its state in the kernel: a socket
49
+ that is closed before it is removed leaves it, which Pyjoyment does, and the
50
+ state is shared with the child of a fork, as a worker of
51
+ :mod:`Pyjo.Server.Prefork`, so an application that installs it has neither.
52
+ An application of Twisted that imports ``twisted.internet.reactor`` itself
53
+ calls :func:`install` before, which installs the one of poll or select.
54
+ ``PYJO_REACTOR`` selects it for :mod:`Pyjo.IOLoop`::
55
+
56
+ PYJO_REACTOR=Pyjo.Reactor.Twisted
57
+
58
+ The reactor of Twisted is unique in a process, so only one object of this
59
+ class uses it at a time, and :meth:`~Pyjo_Reactor_Twisted.new` returns a
60
+ :mod:`Pyjo.Reactor.Poll` object while it lives, as the constructor of the
61
+ class does. It runs in the thread that
62
+ made it, so the threads of :mod:`Pyjo.Server.Prethread` get
63
+ :mod:`Pyjo.Reactor.Poll` objects. When the reactor of Twisted is running
64
+ already, as under ``twistd`` or Scrapy, :meth:`~Pyjo_Reactor_Twisted.start`
65
+ and :meth:`~Pyjo_Reactor_Twisted.one_tick` return right away, and the events
66
+ are handled by the reactor of Twisted that runs.
67
+
68
+ Events
69
+ ------
70
+
71
+ :mod:`Pyjo.Reactor.Twisted` inherits all events from :mod:`Pyjo.Reactor.Poll`.
72
+
73
+ See also
74
+ --------
75
+
76
+ :mod:`Pyjo.Reactor.Poll`, :mod:`Pyjoyment`, https://github.com/pyjoyment/Pyjoyment.
77
+
78
+ Classes
79
+ -------
80
+ """
81
+
82
+ import Pyjo.Exception
83
+ import Pyjo.Reactor
84
+ import Pyjo.Reactor.Poll
85
+
86
+ from Pyjo.Reactor import Handle, IOCallback, TimerCallback
87
+
88
+ import importlib.metadata
89
+ import os
90
+ import select
91
+ import sys
92
+ import threading
93
+ import weakref
94
+
95
+ from collections.abc import Callable
96
+ from typing import Any, Self, TypedDict
97
+
98
+ from twisted.internet.interfaces import IReadDescriptor, IWriteDescriptor
99
+
100
+ # zope.interface has no stubs, as Twisted uses it
101
+ from zope.interface import implementer # pyright: ignore[reportMissingTypeStubs]
102
+
103
+
104
+ __version__ = importlib.metadata.version('Pyjo-Reactor-Twisted')
105
+ """The version of the distribution, of its metadata, which the workflow Automated version sets in pyproject.toml."""
106
+
107
+ # The object that uses the reactor of Twisted, which is unique, as Mojo::Reactor::EV counts the objects of EV
108
+ _active: 'weakref.ref[Pyjo_Reactor_Twisted] | None' = None
109
+
110
+
111
+ class _Timer(TypedDict):
112
+ cb: TimerCallback | None
113
+ after: float
114
+ recurring: bool
115
+ call: Any
116
+ fire: Callable[[], None]
117
+
118
+
119
+ class Pyjo_Reactor_Twisted(Pyjo.Reactor.Poll.object):
120
+ """
121
+ :mod:`Pyjo.Reactor.Twisted` inherits all methods from
122
+ :mod:`Pyjo.Reactor.Poll` and implements the following new ones.
123
+ """
124
+
125
+ _timers: dict[str, _Timer] # pyright: ignore[reportIncompatibleVariableOverride]
126
+
127
+ # We have to fall back to Pyjo.Reactor.Poll, since the reactor of Twisted is unique; in the constructor of the class,
128
+ # as new and the constructor make the same object, and Python does not call __init__ for an object of another class
129
+ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
130
+ if _active is not None and _active() is not None:
131
+ return Pyjo.Reactor.Poll.new(*args, **kwargs)
132
+ return super().__new__(cls)
133
+
134
+ @classmethod
135
+ def new(cls, *args: Any, **kwargs: Any) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
136
+ """::
137
+
138
+ reactor = Pyjo.Reactor.Twisted.new()
139
+
140
+ Construct a new :mod:`Pyjo.Reactor.Twisted` object, or a
141
+ :mod:`Pyjo.Reactor.Poll` object while another one uses the reactor of
142
+ Twisted.
143
+ """
144
+ return cls(*args, **kwargs)
145
+
146
+ def __init__(self) -> None:
147
+ global _active
148
+ super().__init__()
149
+ # The reactor that the application installed, or one of poll or select, which keep their state in the process
150
+ install()
151
+ from twisted.internet import reactor
152
+
153
+ self._twisted: Any = reactor
154
+ self._descriptors: dict[int, _Descriptor] = {}
155
+ self._ticks = 0
156
+ self._timers = {} # pyright: ignore[reportIncompatibleVariableOverride]
157
+ _active = weakref.ref(self)
158
+ weakref.finalize(self, _release, reactor, self._descriptors, self._timers)
159
+
160
+ def again(self, id: str, after: float | None = None) -> None:
161
+ """::
162
+
163
+ reactor.again(id)
164
+ reactor.again(id, 0.5)
165
+
166
+ Restart timer and optionally change the invocation time. Note that this
167
+ method requires an active timer.
168
+ """
169
+ if (timer := self._timers.get(id)) is None:
170
+ Pyjo.Exception.object.throw('Timer not active')
171
+ if after is not None:
172
+ timer['after'] = after
173
+ if (call := timer['call']).active():
174
+ call.reset(timer['after'])
175
+ else:
176
+ timer['call'] = self._twisted.callLater(timer['after'], timer['fire'])
177
+
178
+ def io(self, handle: Handle, cb: IOCallback) -> Self:
179
+ """::
180
+
181
+ reactor = reactor.io(handle, cb)
182
+
183
+ Watch handle for I/O events, invoking the callback whenever handle
184
+ becomes readable or writable.
185
+ """
186
+ fd = _fileno(handle)
187
+ self._unwatch(fd)
188
+ self._io[fd] = {'cb': cb, 'mode': 0}
189
+ self._descriptors[fd] = _Descriptor(weakref.ref(self), fd)
190
+ return self.watch(handle, True, True)
191
+
192
+ def one_tick(self) -> None:
193
+ """::
194
+
195
+ reactor.one_tick()
196
+
197
+ Run reactor until an event occurs or no events are being watched
198
+ anymore, or return right away while the reactor of Twisted runs.
199
+ """
200
+ twisted = self._twisted
201
+ if twisted.running:
202
+ return
203
+
204
+ running = self._running
205
+ if not running:
206
+ self._running = 1
207
+
208
+ try:
209
+ ticks = self._ticks
210
+ while self._ticks == ticks and self._running:
211
+ # The cleanups that finalizers deferred, before the reactor waits
212
+ if Pyjo.Reactor._DEFERRED.get(threading.get_ident()): # pyright: ignore[reportPrivateUsage]
213
+ Pyjo.Reactor._drain() # pyright: ignore[reportPrivateUsage]
214
+
215
+ # Stop automatically if there is nothing to watch
216
+ if not len(self._timers) and not len(self._io):
217
+ return self.stop()
218
+
219
+ # I/O, at most until the next timer or for half a second, then the timers that are due, as the reactor of
220
+ # poll has them
221
+ timeout = twisted.timeout()
222
+ twisted.doIteration(0.5 if timeout is None else min(timeout, 0.5))
223
+ twisted.runUntilCurrent()
224
+ finally:
225
+ if not running:
226
+ self._running = running
227
+
228
+ def recurring(self, after: float, cb: TimerCallback) -> str:
229
+ """::
230
+
231
+ id = reactor.recurring(0.25, cb)
232
+
233
+ Create a new recurring timer, invoking the callback repeatedly after a
234
+ given amount of time in seconds.
235
+ """
236
+ return self._timer(True, after, cb)
237
+
238
+ def remove(self, remove: Handle | str) -> bool:
239
+ """::
240
+
241
+ boolean = reactor.remove(handle)
242
+ boolean = reactor.remove(id)
243
+
244
+ Remove handle or timer.
245
+ """
246
+ if isinstance(remove, str):
247
+ if (timer := self._timers.pop(remove, None)) is None:
248
+ return False
249
+ _cancel(timer['call'])
250
+ return True
251
+ fd = _fileno(remove)
252
+ if self._io.pop(fd, None) is None:
253
+ return False
254
+ self._unwatch(fd)
255
+ return True
256
+
257
+ def reset(self) -> None:
258
+ """::
259
+
260
+ reactor.reset()
261
+
262
+ Remove all handles and timers.
263
+ """
264
+ _release(self._twisted, self._descriptors, self._timers)
265
+ self._events = {}
266
+ self._io = {}
267
+ self._next_tick.clear()
268
+ self._next_timer = None
269
+ self._running = 0
270
+
271
+ def start(self) -> None:
272
+ """::
273
+
274
+ reactor.start()
275
+
276
+ Start watching for I/O and timer events, this will block until
277
+ :meth:`stop` is called or no events are being watched anymore, or
278
+ return right away while the reactor of Twisted runs.
279
+ """
280
+ if self._twisted.running:
281
+ return
282
+ super().start()
283
+
284
+ def timer(self, after: float, cb: TimerCallback) -> str:
285
+ """::
286
+
287
+ id = reactor.timer(0.5, cb)
288
+
289
+ Create a new timer, invoking the callback after a given amount of time
290
+ in seconds.
291
+ """
292
+ return self._timer(False, after, cb)
293
+
294
+ def watch(self, handle: Handle, read: bool, write: bool) -> Self:
295
+ """::
296
+
297
+ reactor = reactor.watch(handle, readable, writable)
298
+
299
+ Change I/O events to watch handle for with true and false values. Note
300
+ that this method requires an active I/O watcher.
301
+ """
302
+ fd = _fileno(handle)
303
+ if fd not in self._io or (descriptor := self._descriptors.get(fd)) is None:
304
+ Pyjo.Exception.object.throw('I/O watcher not active')
305
+ twisted = self._twisted
306
+ if read and not descriptor.reading:
307
+ twisted.addReader(descriptor)
308
+ elif not read and descriptor.reading:
309
+ twisted.removeReader(descriptor)
310
+ if write and not descriptor.writing:
311
+ twisted.addWriter(descriptor)
312
+ elif not write and descriptor.writing:
313
+ twisted.removeWriter(descriptor)
314
+ descriptor.reading, descriptor.writing = read, write
315
+ return self
316
+
317
+ def _fire(self, fd: int, writable: bool) -> None:
318
+ if (io := self._io.get(fd)) is None:
319
+ return
320
+ self._ticks += 1
321
+ self._try('I/O watcher', io['cb'], writable)
322
+
323
+ def _timer(self, recurring: bool, after: float, cb: TimerCallback) -> str:
324
+ id = self._id()
325
+ ref = weakref.ref(self)
326
+
327
+ # The call of Twisted refers to the reactor weakly, as the reactor of Twisted lives as long as the process, and a
328
+ # recurring timer takes the function from the timer, as a reference to itself would make a cycle, which keeps the
329
+ # callback until the garbage collector runs
330
+ def fire() -> None:
331
+ if (reactor := ref()) is None or (timer := reactor._timers.get(id)) is None:
332
+ return
333
+ if timer['recurring']:
334
+ timer['call'] = reactor._twisted.callLater(timer['after'], timer['fire'])
335
+ else:
336
+ del reactor._timers[id]
337
+ reactor._ticks += 1
338
+ reactor._try('Timer', cb)
339
+
340
+ self._timers[id] = {
341
+ 'cb': cb,
342
+ 'after': after,
343
+ 'recurring': recurring,
344
+ 'call': self._twisted.callLater(after, fire),
345
+ 'fire': fire,
346
+ }
347
+ return id
348
+
349
+ def _unwatch(self, fd: int) -> None:
350
+ if (descriptor := self._descriptors.pop(fd, None)) is not None:
351
+ _unwatch(self._twisted, descriptor)
352
+
353
+ def _wake(self) -> None:
354
+ self._twisted.callFromThread(lambda: None)
355
+
356
+
357
+ # A handle of the reactor of Twisted, which calls the I/O watcher of its file descriptor; the decorator of zope.interface
358
+ # has no types
359
+ @implementer(IReadDescriptor, IWriteDescriptor) # pyright: ignore[reportUntypedClassDecorator]
360
+ class _Descriptor:
361
+ def __init__(self, reactor: 'weakref.ref[Pyjo_Reactor_Twisted]', fd: int) -> None:
362
+ self.fd = fd
363
+ self.reactor = reactor
364
+ self.reading = self.writing = False
365
+
366
+ # The reactor of Twisted removes the handle at a hang-up or an error without data, as the refusal of a connection,
367
+ # where the reactor of poll calls the watcher, which finds the error of the socket, so the watcher is called as the
368
+ # handle was watched, readable and writable
369
+ def connectionLost(self, reason: Any) -> None:
370
+ reading, writing = self.reading, self.writing
371
+ self.reading = self.writing = False
372
+ if (reactor := self.reactor()) is None:
373
+ return
374
+ if reading:
375
+ reactor._fire(self.fd, False) # pyright: ignore[reportPrivateUsage]
376
+ if writing and reactor._descriptors.get(self.fd) is self: # pyright: ignore[reportPrivateUsage]
377
+ reactor._fire(self.fd, True) # pyright: ignore[reportPrivateUsage]
378
+
379
+ def doRead(self) -> None:
380
+ if (reactor := self.reactor()) is not None:
381
+ reactor._fire(self.fd, False) # pyright: ignore[reportPrivateUsage]
382
+
383
+ def doWrite(self) -> None:
384
+ if (reactor := self.reactor()) is not None:
385
+ reactor._fire(self.fd, True) # pyright: ignore[reportPrivateUsage]
386
+
387
+ def fileno(self) -> int:
388
+ return self.fd
389
+
390
+ def logPrefix(self) -> str:
391
+ return 'Pyjo.Reactor.Twisted'
392
+
393
+
394
+ # The child of a fork has the waker of the parent, the pipe that wakes the reactor of Twisted from other threads, whose
395
+ # wake-ups the parent and the child would read from each other, so the child gets one of its own
396
+ def _after_fork_in_child() -> None:
397
+ if _active is None or (reactor := _active()) is None:
398
+ return
399
+ twisted = reactor._twisted # pyright: ignore[reportPrivateUsage]
400
+ if (waker := getattr(twisted, 'waker', None)) is None:
401
+ return
402
+ twisted._removeInternalReader(waker)
403
+ waker.connectionLost(None)
404
+ twisted.waker = None
405
+ twisted.installWaker()
406
+
407
+
408
+ def _cancel(call: Any) -> None:
409
+ if call.active():
410
+ call.cancel()
411
+
412
+
413
+ def _fileno(handle: Handle) -> int:
414
+ fd = handle if isinstance(handle, int) else handle.fileno()
415
+ if fd < 0:
416
+ Pyjo.Exception.object.throw('Handle is closed')
417
+ return fd
418
+
419
+
420
+ # The reactor of poll, or of select where there is no poll or it does not work well, as on macOS, which Twisted itself
421
+ # prefers to poll there, in the place of the default of Twisted, which is epoll on Linux
422
+ def install() -> None:
423
+ """::
424
+
425
+ Pyjo.Reactor.Twisted.install()
426
+
427
+ Install the reactor of Twisted that :mod:`Pyjo.Reactor.Twisted` prefers,
428
+ of poll, or of select where there is no poll or it does not work well, as
429
+ on macOS, in the place of the default of Twisted, epoll on Linux. An
430
+ application of Twisted calls it before it imports
431
+ ``twisted.internet.reactor``, which installs the default. It does nothing
432
+ where a reactor of Twisted is installed already. ::
433
+
434
+ import Pyjo.Reactor.Twisted
435
+
436
+ Pyjo.Reactor.Twisted.install()
437
+
438
+ from twisted.internet import reactor
439
+ """
440
+ if 'twisted.internet.reactor' in sys.modules:
441
+ return
442
+ if sys.platform != 'darwin' and hasattr(select, 'poll'):
443
+ from twisted.internet import pollreactor
444
+
445
+ pollreactor.install()
446
+ else:
447
+ from twisted.internet import selectreactor
448
+
449
+ selectreactor.install()
450
+
451
+
452
+ # The handles and the calls that an object leaves in the reactor of Twisted, at a reset and when it is freed
453
+ def _release(twisted: Any, descriptors: dict[int, _Descriptor], timers: dict[str, _Timer]) -> None:
454
+ for descriptor in descriptors.values():
455
+ _unwatch(twisted, descriptor)
456
+ descriptors.clear()
457
+ for timer in timers.values():
458
+ _cancel(timer['call'])
459
+ timers.clear()
460
+
461
+
462
+ def _unwatch(twisted: Any, descriptor: _Descriptor) -> None:
463
+ if descriptor.reading:
464
+ twisted.removeReader(descriptor)
465
+ if descriptor.writing:
466
+ twisted.removeWriter(descriptor)
467
+ descriptor.reading = descriptor.writing = False
468
+
469
+
470
+ if hasattr(os, 'register_at_fork'):
471
+ os.register_at_fork(after_in_child=_after_fork_in_child)
472
+
473
+ new = Pyjo_Reactor_Twisted.new
474
+ object = Pyjo_Reactor_Twisted
475
+ Twisted = Pyjo_Reactor_Twisted
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.5
2
+ Name: Pyjo-Reactor-Twisted
3
+ Version: 0.2.0
4
+ Summary: Low level event reactor with Twisted support for Pyjoyment.
5
+ Project-URL: Homepage, https://github.com/pyjoyment/Pyjo-Reactor-Twisted
6
+ Project-URL: Documentation, https://pyjoyment.readthedocs.io/projects/pyjo-reactor-twisted/en/stable/
7
+ Author-email: Piotr Roszatycki <piotr.roszatycki@gmail.com>
8
+ License-Expression: Artistic-2.0
9
+ License-File: LICENSE
10
+ Keywords: pyjoyment,reactor,twisted
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Framework :: Twisted
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: pyjoyment>=0.2.0
17
+ Requires-Dist: twisted>=26.10.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ [![Home](https://img.shields.io/badge/%F0%9F%8F%A0-Pyjoyment-02a7a0)](https://github.com/pyjoyment/Pyjoyment)
21
+ [![GitHub](https://img.shields.io/github/stars/pyjoyment/Pyjo-Reactor-Twisted?style=flat&label=GitHub%20%E2%AD%90&logo=github)](https://github.com/pyjoyment/Pyjo-Reactor-Twisted)
22
+ [![Documentation](https://img.shields.io/badge/docs-Read_the_Docs-blue?logo=readthedocs&logoColor=white)](https://pyjoyment.readthedocs.io/projects/pyjo-reactor-twisted/en/stable/)
23
+ [![License](https://img.shields.io/github/license/pyjoyment/Pyjo-Reactor-Twisted.svg)](https://github.com/pyjoyment/Pyjo-Reactor-Twisted/blob/main/LICENSE)
24
+ [![PyPI](https://img.shields.io/pypi/v/Pyjo-Reactor-Twisted?logo=pypi&logoColor=white)](https://pypi.org/project/Pyjo-Reactor-Twisted/)
25
+ [![Tests](https://github.com/pyjoyment/Pyjo-Reactor-Twisted/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/pyjoyment/Pyjo-Reactor-Twisted/actions/workflows/tests.yaml)
26
+
27
+ # Pyjo-Reactor-Twisted
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 [Twisted](https://twisted.org) support for
37
+ [Pyjoyment](https://github.com/pyjoyment/Pyjoyment).
38
+
39
+ `Pyjo.Reactor.Twisted` runs the I/O and the timers of `Pyjo.IOLoop` in the
40
+ reactor of Twisted, so that a Pyjoyment application and the code of Twisted
41
+ share one event loop.
42
+
43
+ ## Installation
44
+
45
+ From [PyPI](https://pypi.org/project/Pyjo-Reactor-Twisted/), with
46
+ [Pyjoyment](https://pypi.org/project/Pyjoyment/) and
47
+ [Twisted](https://pypi.org/project/Twisted/), which it depends on:
48
+
49
+ ```sh
50
+ pip install Pyjo-Reactor-Twisted
51
+ ```
52
+
53
+ or, in a project managed with [uv](https://docs.astral.sh/uv/):
54
+
55
+ ```sh
56
+ uv add Pyjo-Reactor-Twisted
57
+ ```
58
+
59
+ The development version is installed from the Git repository:
60
+
61
+ ```sh
62
+ pip install git+https://github.com/pyjoyment/Pyjo-Reactor-Twisted
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ Select the reactor with `PYJO_REACTOR` before `Pyjo.IOLoop` is loaded:
68
+
69
+ ```sh
70
+ PYJO_REACTOR=Pyjo.Reactor.Twisted python myapp.py daemon
71
+ ```
72
+
73
+ The reactor of Twisted is unique in a process, so only one `Pyjo.IOLoop` uses
74
+ it at a time; the others get `Pyjo.Reactor.Poll`. When the reactor of Twisted
75
+ is running already, as under `twistd` or Scrapy, `Pyjo.IOLoop.start()` returns
76
+ right away and the events are handled by the reactor that runs.
77
+
78
+ ## Examples
79
+
80
+ `examples/hello_world_twisted.py` runs `Pyjo.HelloWorld` on the server of
81
+ Pyjoyment in the event loop that Twisted runs, with `reactor.run()`, in a
82
+ `with` block, which closes the listen sockets of the server at its end:
83
+
84
+ ```python
85
+ import os
86
+
87
+
88
+ # The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
89
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'
90
+
91
+ import Pyjo.Reactor.Twisted # noqa: E402
92
+
93
+
94
+ # The reactor of Twisted of poll or select, before Twisted is imported, which would install its default, epoll on Linux
95
+ Pyjo.Reactor.Twisted.install()
96
+
97
+ import Pyjo.HelloWorld # noqa: E402
98
+ import Pyjo.Server.Daemon # noqa: E402
99
+
100
+ from twisted.internet import reactor # noqa: E402
101
+
102
+
103
+ # The server of Pyjoyment, which closes its listen sockets at the end of the block
104
+ with Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://*:3000']) as daemon:
105
+ daemon.start()
106
+
107
+ # Twisted runs the event loop, with the server of Pyjoyment in it
108
+ reactor.run()
109
+ ```
110
+
111
+ ```console
112
+ $ python examples/hello_world_twisted.py
113
+ Web application available at http://127.0.0.1:3000
114
+ ```
115
+
116
+ ```console
117
+ $ curl http://127.0.0.1:3000/
118
+ Your Pyjo is working!
119
+ ```
120
+
121
+ `examples/user_agent_twisted.py` is an application of Twisted, a server of
122
+ `twisted.web`, whose timer requests it every second with the user agent of
123
+ Pyjoyment, without blocking the reactor of Twisted. The user agent is in a
124
+ `with` block, which closes its connections at its end, so the callback shows
125
+ the error of a request that it interrupts:
126
+
127
+ ```python
128
+ import os
129
+
130
+
131
+ # The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
132
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'
133
+
134
+ import Pyjo.Reactor.Twisted # noqa: E402
135
+
136
+
137
+ # The reactor of Twisted of poll or select, before Twisted is imported, which would install its default, epoll on Linux
138
+ Pyjo.Reactor.Twisted.install()
139
+
140
+ import Pyjo.UserAgent # noqa: E402
141
+
142
+ from twisted.internet import reactor, task # noqa: E402
143
+ from twisted.web import resource, server # noqa: E402
144
+
145
+
146
+ # An application of Twisted
147
+ class Hello(resource.Resource):
148
+ isLeaf = True
149
+
150
+ def render_GET(self, request):
151
+ return b'Hello from Twisted!'
152
+
153
+
154
+ reactor.listenTCP(3000, server.Site(Hello()))
155
+
156
+ # The user agent of Pyjoyment, which closes its connections at the end of the block
157
+ with Pyjo.UserAgent.new() as ua:
158
+ # A request every second, which does not block the reactor of Twisted; a request that the end of the block
159
+ # interrupts gets an error
160
+ def fetch():
161
+ def done(ua, tx):
162
+ print(tx.error['message'] if tx.error else f'{tx.res.code} {tx.res.text}')
163
+
164
+ ua.get('http://127.0.0.1:3000/', done)
165
+
166
+ task.LoopingCall(fetch).start(1)
167
+ reactor.run()
168
+ ```
169
+
170
+ ```console
171
+ $ python examples/user_agent_twisted.py
172
+ 200 Hello from Twisted!
173
+ 200 Hello from Twisted!
174
+ 200 Hello from Twisted!
175
+ ```
176
+
177
+ `examples/loopback_twisted.py` is a server of Pyjoyment and its user agent on
178
+ the reactor of Twisted, a blocking request, which runs the event loop until
179
+ the response, and the class of the reactor, which Build checks with the wheel:
180
+
181
+ ```python
182
+ import os
183
+
184
+
185
+ # The reactor of Pyjoyment on the reactor of Twisted, selected before Pyjo.IOLoop is loaded
186
+ os.environ['PYJO_REACTOR'] = 'Pyjo.Reactor.Twisted'
187
+
188
+ import Pyjo.HelloWorld # noqa: E402
189
+ import Pyjo.IOLoop # noqa: E402
190
+ import Pyjo.Server.Daemon # noqa: E402
191
+ import Pyjo.UserAgent # noqa: E402
192
+
193
+
194
+ # The server, on a port that the system picks, and the user agent, in the same event loop, which close their sockets at
195
+ # the end of the block
196
+ with (
197
+ Pyjo.Server.Daemon.new(app=Pyjo.HelloWorld.new(), listen=['http://127.0.0.1'], silent=True) as daemon,
198
+ Pyjo.UserAgent.new(ioloop=Pyjo.IOLoop.singleton) as ua,
199
+ ):
200
+ # A blocking request, which runs the event loop of the server on the reactor of Twisted until the response
201
+ tx = ua.get(f'http://127.0.0.1:{daemon.start().ports()[0]}/')
202
+ print(type(Pyjo.IOLoop.singleton.reactor).__name__)
203
+ print(tx.result.code, tx.result.text)
204
+ ```
205
+
206
+ ```console
207
+ $ python examples/loopback_twisted.py
208
+ Pyjo_Reactor_Twisted
209
+ 200 Your Pyjo is working!
210
+ ```
211
+
212
+ The first two call `Pyjo.Reactor.Twisted.install()` before they import
213
+ Twisted, which would install its default, epoll on Linux, in the place of poll
214
+ or select; the third does not import Twisted, so the reactor installs it.
215
+
216
+ ## Documentation
217
+
218
+ The API of the module and the examples are on
219
+ [Read the Docs](https://pyjoyment.readthedocs.io/projects/pyjo-reactor-twisted/en/stable/).
220
+
221
+ ## Status
222
+
223
+ An experiment: the first reactor of Pyjoyment in a distribution of its own.
224
+
225
+ ## License
226
+
227
+ The Artistic License 2.0, as Pyjoyment.
@@ -0,0 +1,5 @@
1
+ Pyjo/Reactor/Twisted.py,sha256=QxvO_T03TG-AxcBxCbYSVHkFKjzNsBh4ZMjtYBVZ3mM,16879
2
+ pyjo_reactor_twisted-0.2.0.dist-info/METADATA,sha256=qnRETtC8Zolg3ovjr-acV9dIaAqBOnPUHgDmTpcd38w,8013
3
+ pyjo_reactor_twisted-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
4
+ pyjo_reactor_twisted-0.2.0.dist-info/licenses/LICENSE,sha256=wqf_RI5i8Of0e_6xwFe7wBhw72jXAkF_9N8Ny9ScaP4,8894
5
+ pyjo_reactor_twisted-0.2.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.