cantok 0.0.1__tar.gz
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.
- cantok-0.0.1/LICENSE +21 -0
- cantok-0.0.1/PKG-INFO +339 -0
- cantok-0.0.1/README.md +316 -0
- cantok-0.0.1/cantok/__init__.py +8 -0
- cantok-0.0.1/cantok/tokens/__init__.py +0 -0
- cantok-0.0.1/cantok/tokens/abstract_token.py +76 -0
- cantok-0.0.1/cantok/tokens/condition_token.py +43 -0
- cantok-0.0.1/cantok/tokens/counter_token.py +53 -0
- cantok-0.0.1/cantok/tokens/simple_token.py +9 -0
- cantok-0.0.1/cantok/tokens/timeout_token.py +34 -0
- cantok-0.0.1/cantok.egg-info/PKG-INFO +339 -0
- cantok-0.0.1/cantok.egg-info/SOURCES.txt +23 -0
- cantok-0.0.1/cantok.egg-info/dependency_links.txt +1 -0
- cantok-0.0.1/cantok.egg-info/top_level.txt +2 -0
- cantok-0.0.1/setup.cfg +4 -0
- cantok-0.0.1/setup.py +38 -0
- cantok-0.0.1/tests/readme_examples/__init__.py +0 -0
- cantok-0.0.1/tests/readme_examples/test_examples.py +20 -0
- cantok-0.0.1/tests/units/__init__.py +0 -0
- cantok-0.0.1/tests/units/tokens/__init__.py +0 -0
- cantok-0.0.1/tests/units/tokens/test_abstract_token.py +137 -0
- cantok-0.0.1/tests/units/tokens/test_condition_token.py +138 -0
- cantok-0.0.1/tests/units/tokens/test_counter_token.py +86 -0
- cantok-0.0.1/tests/units/tokens/test_simple_token.py +45 -0
- cantok-0.0.1/tests/units/tokens/test_timeout_token.py +91 -0
cantok-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 pomponchik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cantok-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: cantok
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Implementation of the "Cancellation Token" pattern
|
|
5
|
+
Home-page: https://github.com/pomponchik/cantok
|
|
6
|
+
Author: Evgeniy Blinov
|
|
7
|
+
Author-email: zheni-b@yandex.ru
|
|
8
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
9
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
10
|
+
Classifier: Operating System :: POSIX
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
[](https://pepy.tech/project/cantok)
|
|
27
|
+
[](https://pepy.tech/project/cantok)
|
|
28
|
+
[](https://codecov.io/gh/pomponchik/cantok)
|
|
29
|
+
[](https://github.com/pomponchik/cantok/actions/workflows/tests_and_coverage.yml)
|
|
30
|
+
[](https://pypi.python.org/pypi/cantok)
|
|
31
|
+
[](https://badge.fury.io/py/cantok)
|
|
32
|
+
[](http://mypy-lang.org/)
|
|
33
|
+
[](https://github.com/astral-sh/ruff)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Cancellation Token is a pattern that allows us to refuse to continue calculations that we no longer need. It is implemented out of the box in many programming languages, for example in [C#](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken) and in [Go](https://pkg.go.dev/context). However, there was still no sane implementation in Python, until the [cantok](https://github.com/pomponchik/cantok) library appeared.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Table of contents
|
|
40
|
+
|
|
41
|
+
- [**Quick start**](#quick-start)
|
|
42
|
+
- [**The pattern**](#the-pattern)
|
|
43
|
+
- [**Tokens**](#tokens)
|
|
44
|
+
- [**Simple token**](#simple-token)
|
|
45
|
+
- [**Condition token**](#simple-token)
|
|
46
|
+
- [**Timeout token**](#timeout-token)
|
|
47
|
+
- [**Counter token**](#counter-token)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
Install [it](https://pypi.org/project/cantok/):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install cantok
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
And use:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from random import randint
|
|
62
|
+
from threading import Thread
|
|
63
|
+
|
|
64
|
+
from cantok import ConditionToken, CounterToken, TimeoutToken
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
counter = 0
|
|
68
|
+
|
|
69
|
+
def function(token):
|
|
70
|
+
global counter
|
|
71
|
+
while not token.cancelled:
|
|
72
|
+
counter += 1
|
|
73
|
+
|
|
74
|
+
token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1)
|
|
75
|
+
thread = Thread(target=function, args=(token, ))
|
|
76
|
+
thread.start()
|
|
77
|
+
thread.join()
|
|
78
|
+
|
|
79
|
+
print(counter)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
In this example, we pass a token to the function that describes several restrictions: on the [number of iterations](#counter-token) of the cycle, on [time](#timeout-token), as well as on the [occurrence](#condition-token) of a random unlikely event. When any of the indicated events occur, the cycle stops.
|
|
83
|
+
|
|
84
|
+
Read more about the [possibilities of tokens](#tokens), as well as about the [pattern in general](#the-pattern).
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## The pattern
|
|
88
|
+
|
|
89
|
+
The essence of the pattern is that we pass special objects to functions and constructors, by which the executed code can understand whether it should continue its execution or not. When deciding whether to allow code execution to continue, this object can take into account both the restrictions specified to it, such as the maximum code execution time, and receive signals about the need to stop from the outside, for example from another thread or a coroutine. Thus, we do not nail down the logic associated with stopping code execution, for example, by directly tracking cycle counters, but implement [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) of this restriction.
|
|
90
|
+
|
|
91
|
+
In addition, the pattern assumes that various restrictions can be combined indefinitely with each other: if at least one of the restrictions is not met, code execution will be interrupted. It is assumed that each function in the call stack will call other functions, throwing its token directly to them, or wrapping it in another token, with a stricter restriction imposed on it.
|
|
92
|
+
|
|
93
|
+
Unlike other ways of interrupting code execution, tokens do not force the execution thread to be interrupted forcibly. The interruption occurs "gently", allowing the code to terminate correctly, return all occupied resources and restore consistency.
|
|
94
|
+
|
|
95
|
+
It is highly desirable for library developers to use this pattern for any long-term composite operations. Your function can accept a token as an optional argument, with a default value that imposes minimal restrictions or none at all. If the user wishes, he can transfer his token there, imposing stricter restrictions on the library code. In addition to a more convenient and extensible API, this will give the library an advantage in the form of better testability, because the restrictions are no longer sewn directly into the function, which means they can be made whatever you want for the test. In addition, the library developer no longer needs to think about all the numerous restrictions that can be imposed on his code - the user can take care of it himself if he needs to.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
## Tokens
|
|
99
|
+
|
|
100
|
+
All token classes presented in this library have a uniform interface. And they are all inherited from one class: `AbstractToken`. The only reason why you might want to import it is to use it for a type hint. This example illustrates a type hint suitable for any of the tokens:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from cantok import AbstractToken
|
|
104
|
+
|
|
105
|
+
def function(token: AbstractToken):
|
|
106
|
+
...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Each token object has a `cancelled` attribute and a `cancel()` method. By the attribute, you can find out whether this token has been canceled:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cantok import SimpleToken
|
|
113
|
+
|
|
114
|
+
token = SimpleToken()
|
|
115
|
+
print(token.cancelled) # False
|
|
116
|
+
token.cancel()
|
|
117
|
+
print(token.cancelled) # True
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The cancelled attribute is dynamically calculated and takes into account, among other things, specific conditions that are checked by a specific token. Here is an example with a [token that measures time](#timeout-token):
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from time import sleep
|
|
124
|
+
from cantok import TimeoutToken
|
|
125
|
+
|
|
126
|
+
token = TimeoutToken(5)
|
|
127
|
+
print(token.cancelled) # False
|
|
128
|
+
sleep(10)
|
|
129
|
+
print(token.cancelled) # True
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In addition to this attribute, each token implements the `is_cancelled()` method. It does exactly the same thing as the attribute:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from cantok import SimpleToken
|
|
136
|
+
|
|
137
|
+
token = SimpleToken()
|
|
138
|
+
print(token.cancelled) # False
|
|
139
|
+
print(token.is_cancelled()) # False
|
|
140
|
+
token.cancel()
|
|
141
|
+
print(token.cancelled) # True
|
|
142
|
+
print(token.is_cancelled()) # True
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Choose what you like best. To the author of the library, the use of the attribute seems more beautiful, but the method call more clearly reflects the complexity of the work that is actually being done to answer the question "has the token been canceled?".
|
|
146
|
+
|
|
147
|
+
There is another method opposite to `is_cancelled()` - `keep_on()`. It answers the opposite question, and can be used in the same situations:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from cantok import SimpleToken
|
|
151
|
+
|
|
152
|
+
token = SimpleToken()
|
|
153
|
+
print(token.cancelled) # False
|
|
154
|
+
print(token.keep_on()) # True
|
|
155
|
+
token.cancel()
|
|
156
|
+
print(token.cancelled) # True
|
|
157
|
+
print(token.keep_on()) # False
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
An unlimited number of other tokens can be embedded in one token as arguments during initialization. Each time checking whether it has been canceled, the token first checks its cancellation rules, and if it has not been canceled itself, then it checks the tokens nested in it. Thus, one cancelled token nested in another non-cancelled token cancels it:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from cantok import SimpleToken
|
|
164
|
+
|
|
165
|
+
first_token = SimpleToken()
|
|
166
|
+
second_token = SimpleToken()
|
|
167
|
+
third_token = SimpleToken(first_token, second_token)
|
|
168
|
+
|
|
169
|
+
first_token.cancel()
|
|
170
|
+
|
|
171
|
+
print(first_token.cancelled) # True
|
|
172
|
+
print(second_token.cancelled) # False
|
|
173
|
+
print(third_token.cancelled) # True
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
In addition, any tokens can be summed up among themselves. The summation operation generates another [`SimpleToken`](#simple-token) that includes the previous 2:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from cantok import SimpleToken, TimeoutToken
|
|
180
|
+
|
|
181
|
+
print(repr(SimpleToken() + TimeoutToken(5)))
|
|
182
|
+
# SimpleToken(SimpleToken(cancelled=False), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
This feature is convenient to use if your function has received a token with certain restrictions and wants to throw it into other called functions, imposing additional restrictions:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from cantok import AbstractToken, TimeoutToken
|
|
189
|
+
|
|
190
|
+
def function(token: AbstractToken):
|
|
191
|
+
...
|
|
192
|
+
another_function(token + TimeoutToken(5)) # Imposes an additional restriction on the function being called: work for no more than 5 seconds. At the same time, it does not know anything about what restrictions were imposed earlier.
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Read on about the features of each type of tokens in more detail.
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
### Simple token
|
|
200
|
+
|
|
201
|
+
The base token is `SimpleToken`. It has no built-in automation that can cancel it. The only way to cancel `SimpleToken` is to explicitly call the `cancel()` method from it.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from cantok import SimpleToken
|
|
205
|
+
|
|
206
|
+
token = SimpleToken()
|
|
207
|
+
print(token.cancelled) # False
|
|
208
|
+
token.cancel()
|
|
209
|
+
print(token.cancelled) # True
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`SimpleToken` is also implicitly generated by the operation of summing two other tokens:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from cantok import CounterToken, TimeoutToken
|
|
216
|
+
|
|
217
|
+
print(repr(CounterToken(5) + TimeoutToken(5)))
|
|
218
|
+
# SimpleToken(CounterToken(5, cancelled=False, direct=True), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
There is not much more to tell about it if you have read [the story](#tokens) about tokens in general.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
### Condition token
|
|
225
|
+
|
|
226
|
+
A slightly more complex type of token than [`SimpleToken`](#simple-token) is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as a first argument, answering the question whether it has been canceled.
|
|
227
|
+
|
|
228
|
+
To initialize `ConditionToken`, pass a function to it that does not accept arguments and returns a boolean value. If it returns `True`, it means that the operation has been canceled:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from cantok import ConditionToken
|
|
232
|
+
|
|
233
|
+
counter = 5
|
|
234
|
+
token = ConditionToken(lambda: counter >= 5)
|
|
235
|
+
|
|
236
|
+
while not token.cancelled:
|
|
237
|
+
counter += 1
|
|
238
|
+
|
|
239
|
+
print(counter) # 5
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
By default, if the passed function raises an exception, it will be silently suppressed. However, you can make the raised exceptions explicit by setting the `suppress_exceptions` parameter to `False`:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
def function(): raise ValueError
|
|
246
|
+
|
|
247
|
+
token = ConditionToken(function, suppress_exceptions=False)
|
|
248
|
+
|
|
249
|
+
token.cancelled # ValueError has risen.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
If you still use exception suppression mode, by default, in case of an exception, the `canceled` attribute will contain `False`. If you want to change this, pass it there as the `default` parameter - `True`.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
def function(): raise ValueError
|
|
256
|
+
|
|
257
|
+
print(ConditionToken(function).cancelled) # False
|
|
258
|
+
print(ConditionToken(function, default=False).cancelled) # False
|
|
259
|
+
print(ConditionToken(function, default=True).cancelled) # True
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`ConditionToken` may include other tokens during initialization:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
token = ConditionToken(lambda: False, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens.
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Timeout token
|
|
269
|
+
|
|
270
|
+
`TimeoutToken` is automatically canceled after the time specified in seconds in the class constructor:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from time import sleep
|
|
274
|
+
from cantok import TimeoutToken
|
|
275
|
+
|
|
276
|
+
token = TimeoutToken(5)
|
|
277
|
+
print(token.cancelled) # False
|
|
278
|
+
sleep(10)
|
|
279
|
+
print(token.cancelled) # True
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Just like `ConditionToken`, `TimeoutToken` can include other tokens:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
token = TimeoutToken(45, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens.
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
By default, time is measured using [`perf_counter`](https://docs.python.org/3/library/time.html#time.perf_counter) as the most accurate way to measure time. In extremely rare cases, you may need to use [monotonic](https://docs.python.org/3/library/time.html#time.monotonic_ns)-time, for this use the appropriate initialization argument:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
token = TimeoutToken(33, monotonic=True)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Counter token
|
|
295
|
+
|
|
296
|
+
`CounterToken` is the most ambiguous of the tokens presented by this library. Do not use it if you are not sure that you understand how it works correctly. However, it can be very useful in situations where you want to limit the number of attempts to perform an operation.
|
|
297
|
+
|
|
298
|
+
`CounterToken` is initialized with an integer greater than zero. At each calculation of the answer to the question whether it is canceled, this number is reduced by one. When this number becomes zero, the token is considered canceled:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from cantok import CounterToken
|
|
302
|
+
|
|
303
|
+
token = CounterToken(5)
|
|
304
|
+
counter = 0
|
|
305
|
+
|
|
306
|
+
while not token.cancelled:
|
|
307
|
+
counter += 1
|
|
308
|
+
|
|
309
|
+
print(counter) # 5
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The counter inside the `CounterToken` is reduced under one of three conditions:
|
|
313
|
+
|
|
314
|
+
- Access to the `cancelled` attribute.
|
|
315
|
+
- Calling the `is_cancelled()` method.
|
|
316
|
+
- Calling the `keep_on()` method.
|
|
317
|
+
|
|
318
|
+
If you use `CounterToken` inside other tokens, the wrapping token can specify the status of the `CounterToken`. For security reasons, this operation does not decrease the counter. However, if for some reason you need it to decrease, pass `direct` - `False` as an argument:
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from cantok import SimpleToken, CounterToken
|
|
322
|
+
|
|
323
|
+
first_counter_token = CounterToken(1, direct=False)
|
|
324
|
+
second_counter_token = CounterToken(1, direct=True)
|
|
325
|
+
|
|
326
|
+
print(SimpleToken(first_counter_token, second_counter_token).cancelled) # False
|
|
327
|
+
print(first_counter_token.cancelled) # True
|
|
328
|
+
print(second_counter_token.cancelled) # False
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Like all other tokens, `CounterToken` can accept other tokens as parameters during initialization:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from cantok import SimpleToken, CounterToken, TimeoutToken
|
|
335
|
+
|
|
336
|
+
token = CounterToken(15, SimpleToken(), TimeoutToken(5))
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`CounterToken` is thread-safe.
|
cantok-0.0.1/README.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://pepy.tech/project/cantok)
|
|
4
|
+
[](https://pepy.tech/project/cantok)
|
|
5
|
+
[](https://codecov.io/gh/pomponchik/cantok)
|
|
6
|
+
[](https://github.com/pomponchik/cantok/actions/workflows/tests_and_coverage.yml)
|
|
7
|
+
[](https://pypi.python.org/pypi/cantok)
|
|
8
|
+
[](https://badge.fury.io/py/cantok)
|
|
9
|
+
[](http://mypy-lang.org/)
|
|
10
|
+
[](https://github.com/astral-sh/ruff)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Cancellation Token is a pattern that allows us to refuse to continue calculations that we no longer need. It is implemented out of the box in many programming languages, for example in [C#](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken) and in [Go](https://pkg.go.dev/context). However, there was still no sane implementation in Python, until the [cantok](https://github.com/pomponchik/cantok) library appeared.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Table of contents
|
|
17
|
+
|
|
18
|
+
- [**Quick start**](#quick-start)
|
|
19
|
+
- [**The pattern**](#the-pattern)
|
|
20
|
+
- [**Tokens**](#tokens)
|
|
21
|
+
- [**Simple token**](#simple-token)
|
|
22
|
+
- [**Condition token**](#simple-token)
|
|
23
|
+
- [**Timeout token**](#timeout-token)
|
|
24
|
+
- [**Counter token**](#counter-token)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Install [it](https://pypi.org/project/cantok/):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install cantok
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
And use:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from random import randint
|
|
39
|
+
from threading import Thread
|
|
40
|
+
|
|
41
|
+
from cantok import ConditionToken, CounterToken, TimeoutToken
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
counter = 0
|
|
45
|
+
|
|
46
|
+
def function(token):
|
|
47
|
+
global counter
|
|
48
|
+
while not token.cancelled:
|
|
49
|
+
counter += 1
|
|
50
|
+
|
|
51
|
+
token = ConditionToken(lambda: randint(1, 100_000) == 1984) + CounterToken(400_000, direct=False) + TimeoutToken(1)
|
|
52
|
+
thread = Thread(target=function, args=(token, ))
|
|
53
|
+
thread.start()
|
|
54
|
+
thread.join()
|
|
55
|
+
|
|
56
|
+
print(counter)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
In this example, we pass a token to the function that describes several restrictions: on the [number of iterations](#counter-token) of the cycle, on [time](#timeout-token), as well as on the [occurrence](#condition-token) of a random unlikely event. When any of the indicated events occur, the cycle stops.
|
|
60
|
+
|
|
61
|
+
Read more about the [possibilities of tokens](#tokens), as well as about the [pattern in general](#the-pattern).
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## The pattern
|
|
65
|
+
|
|
66
|
+
The essence of the pattern is that we pass special objects to functions and constructors, by which the executed code can understand whether it should continue its execution or not. When deciding whether to allow code execution to continue, this object can take into account both the restrictions specified to it, such as the maximum code execution time, and receive signals about the need to stop from the outside, for example from another thread or a coroutine. Thus, we do not nail down the logic associated with stopping code execution, for example, by directly tracking cycle counters, but implement [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) of this restriction.
|
|
67
|
+
|
|
68
|
+
In addition, the pattern assumes that various restrictions can be combined indefinitely with each other: if at least one of the restrictions is not met, code execution will be interrupted. It is assumed that each function in the call stack will call other functions, throwing its token directly to them, or wrapping it in another token, with a stricter restriction imposed on it.
|
|
69
|
+
|
|
70
|
+
Unlike other ways of interrupting code execution, tokens do not force the execution thread to be interrupted forcibly. The interruption occurs "gently", allowing the code to terminate correctly, return all occupied resources and restore consistency.
|
|
71
|
+
|
|
72
|
+
It is highly desirable for library developers to use this pattern for any long-term composite operations. Your function can accept a token as an optional argument, with a default value that imposes minimal restrictions or none at all. If the user wishes, he can transfer his token there, imposing stricter restrictions on the library code. In addition to a more convenient and extensible API, this will give the library an advantage in the form of better testability, because the restrictions are no longer sewn directly into the function, which means they can be made whatever you want for the test. In addition, the library developer no longer needs to think about all the numerous restrictions that can be imposed on his code - the user can take care of it himself if he needs to.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Tokens
|
|
76
|
+
|
|
77
|
+
All token classes presented in this library have a uniform interface. And they are all inherited from one class: `AbstractToken`. The only reason why you might want to import it is to use it for a type hint. This example illustrates a type hint suitable for any of the tokens:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from cantok import AbstractToken
|
|
81
|
+
|
|
82
|
+
def function(token: AbstractToken):
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each token object has a `cancelled` attribute and a `cancel()` method. By the attribute, you can find out whether this token has been canceled:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from cantok import SimpleToken
|
|
90
|
+
|
|
91
|
+
token = SimpleToken()
|
|
92
|
+
print(token.cancelled) # False
|
|
93
|
+
token.cancel()
|
|
94
|
+
print(token.cancelled) # True
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The cancelled attribute is dynamically calculated and takes into account, among other things, specific conditions that are checked by a specific token. Here is an example with a [token that measures time](#timeout-token):
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from time import sleep
|
|
101
|
+
from cantok import TimeoutToken
|
|
102
|
+
|
|
103
|
+
token = TimeoutToken(5)
|
|
104
|
+
print(token.cancelled) # False
|
|
105
|
+
sleep(10)
|
|
106
|
+
print(token.cancelled) # True
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
In addition to this attribute, each token implements the `is_cancelled()` method. It does exactly the same thing as the attribute:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from cantok import SimpleToken
|
|
113
|
+
|
|
114
|
+
token = SimpleToken()
|
|
115
|
+
print(token.cancelled) # False
|
|
116
|
+
print(token.is_cancelled()) # False
|
|
117
|
+
token.cancel()
|
|
118
|
+
print(token.cancelled) # True
|
|
119
|
+
print(token.is_cancelled()) # True
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Choose what you like best. To the author of the library, the use of the attribute seems more beautiful, but the method call more clearly reflects the complexity of the work that is actually being done to answer the question "has the token been canceled?".
|
|
123
|
+
|
|
124
|
+
There is another method opposite to `is_cancelled()` - `keep_on()`. It answers the opposite question, and can be used in the same situations:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from cantok import SimpleToken
|
|
128
|
+
|
|
129
|
+
token = SimpleToken()
|
|
130
|
+
print(token.cancelled) # False
|
|
131
|
+
print(token.keep_on()) # True
|
|
132
|
+
token.cancel()
|
|
133
|
+
print(token.cancelled) # True
|
|
134
|
+
print(token.keep_on()) # False
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
An unlimited number of other tokens can be embedded in one token as arguments during initialization. Each time checking whether it has been canceled, the token first checks its cancellation rules, and if it has not been canceled itself, then it checks the tokens nested in it. Thus, one cancelled token nested in another non-cancelled token cancels it:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from cantok import SimpleToken
|
|
141
|
+
|
|
142
|
+
first_token = SimpleToken()
|
|
143
|
+
second_token = SimpleToken()
|
|
144
|
+
third_token = SimpleToken(first_token, second_token)
|
|
145
|
+
|
|
146
|
+
first_token.cancel()
|
|
147
|
+
|
|
148
|
+
print(first_token.cancelled) # True
|
|
149
|
+
print(second_token.cancelled) # False
|
|
150
|
+
print(third_token.cancelled) # True
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
In addition, any tokens can be summed up among themselves. The summation operation generates another [`SimpleToken`](#simple-token) that includes the previous 2:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from cantok import SimpleToken, TimeoutToken
|
|
157
|
+
|
|
158
|
+
print(repr(SimpleToken() + TimeoutToken(5)))
|
|
159
|
+
# SimpleToken(SimpleToken(cancelled=False), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This feature is convenient to use if your function has received a token with certain restrictions and wants to throw it into other called functions, imposing additional restrictions:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from cantok import AbstractToken, TimeoutToken
|
|
166
|
+
|
|
167
|
+
def function(token: AbstractToken):
|
|
168
|
+
...
|
|
169
|
+
another_function(token + TimeoutToken(5)) # Imposes an additional restriction on the function being called: work for no more than 5 seconds. At the same time, it does not know anything about what restrictions were imposed earlier.
|
|
170
|
+
...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Read on about the features of each type of tokens in more detail.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
### Simple token
|
|
177
|
+
|
|
178
|
+
The base token is `SimpleToken`. It has no built-in automation that can cancel it. The only way to cancel `SimpleToken` is to explicitly call the `cancel()` method from it.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from cantok import SimpleToken
|
|
182
|
+
|
|
183
|
+
token = SimpleToken()
|
|
184
|
+
print(token.cancelled) # False
|
|
185
|
+
token.cancel()
|
|
186
|
+
print(token.cancelled) # True
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`SimpleToken` is also implicitly generated by the operation of summing two other tokens:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from cantok import CounterToken, TimeoutToken
|
|
193
|
+
|
|
194
|
+
print(repr(CounterToken(5) + TimeoutToken(5)))
|
|
195
|
+
# SimpleToken(CounterToken(5, cancelled=False, direct=True), TimeoutToken(5, cancelled=False, monotonic=False), cancelled=False)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
There is not much more to tell about it if you have read [the story](#tokens) about tokens in general.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
### Condition token
|
|
202
|
+
|
|
203
|
+
A slightly more complex type of token than [`SimpleToken`](#simple-token) is `ConditionToken`. In addition to everything that `SimpleToken` does, it also checks the condition passed to it as a first argument, answering the question whether it has been canceled.
|
|
204
|
+
|
|
205
|
+
To initialize `ConditionToken`, pass a function to it that does not accept arguments and returns a boolean value. If it returns `True`, it means that the operation has been canceled:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from cantok import ConditionToken
|
|
209
|
+
|
|
210
|
+
counter = 5
|
|
211
|
+
token = ConditionToken(lambda: counter >= 5)
|
|
212
|
+
|
|
213
|
+
while not token.cancelled:
|
|
214
|
+
counter += 1
|
|
215
|
+
|
|
216
|
+
print(counter) # 5
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
By default, if the passed function raises an exception, it will be silently suppressed. However, you can make the raised exceptions explicit by setting the `suppress_exceptions` parameter to `False`:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
def function(): raise ValueError
|
|
223
|
+
|
|
224
|
+
token = ConditionToken(function, suppress_exceptions=False)
|
|
225
|
+
|
|
226
|
+
token.cancelled # ValueError has risen.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If you still use exception suppression mode, by default, in case of an exception, the `canceled` attribute will contain `False`. If you want to change this, pass it there as the `default` parameter - `True`.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
def function(): raise ValueError
|
|
233
|
+
|
|
234
|
+
print(ConditionToken(function).cancelled) # False
|
|
235
|
+
print(ConditionToken(function, default=False).cancelled) # False
|
|
236
|
+
print(ConditionToken(function, default=True).cancelled) # True
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`ConditionToken` may include other tokens during initialization:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
token = ConditionToken(lambda: False, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Timeout token
|
|
246
|
+
|
|
247
|
+
`TimeoutToken` is automatically canceled after the time specified in seconds in the class constructor:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from time import sleep
|
|
251
|
+
from cantok import TimeoutToken
|
|
252
|
+
|
|
253
|
+
token = TimeoutToken(5)
|
|
254
|
+
print(token.cancelled) # False
|
|
255
|
+
sleep(10)
|
|
256
|
+
print(token.cancelled) # True
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Just like `ConditionToken`, `TimeoutToken` can include other tokens:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
token = TimeoutToken(45, SimpleToken(), TimeoutToken(5), CounterToken(20)) # Includes all additional restrictions of the passed tokens.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
By default, time is measured using [`perf_counter`](https://docs.python.org/3/library/time.html#time.perf_counter) as the most accurate way to measure time. In extremely rare cases, you may need to use [monotonic](https://docs.python.org/3/library/time.html#time.monotonic_ns)-time, for this use the appropriate initialization argument:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
token = TimeoutToken(33, monotonic=True)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Counter token
|
|
272
|
+
|
|
273
|
+
`CounterToken` is the most ambiguous of the tokens presented by this library. Do not use it if you are not sure that you understand how it works correctly. However, it can be very useful in situations where you want to limit the number of attempts to perform an operation.
|
|
274
|
+
|
|
275
|
+
`CounterToken` is initialized with an integer greater than zero. At each calculation of the answer to the question whether it is canceled, this number is reduced by one. When this number becomes zero, the token is considered canceled:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from cantok import CounterToken
|
|
279
|
+
|
|
280
|
+
token = CounterToken(5)
|
|
281
|
+
counter = 0
|
|
282
|
+
|
|
283
|
+
while not token.cancelled:
|
|
284
|
+
counter += 1
|
|
285
|
+
|
|
286
|
+
print(counter) # 5
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The counter inside the `CounterToken` is reduced under one of three conditions:
|
|
290
|
+
|
|
291
|
+
- Access to the `cancelled` attribute.
|
|
292
|
+
- Calling the `is_cancelled()` method.
|
|
293
|
+
- Calling the `keep_on()` method.
|
|
294
|
+
|
|
295
|
+
If you use `CounterToken` inside other tokens, the wrapping token can specify the status of the `CounterToken`. For security reasons, this operation does not decrease the counter. However, if for some reason you need it to decrease, pass `direct` - `False` as an argument:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from cantok import SimpleToken, CounterToken
|
|
299
|
+
|
|
300
|
+
first_counter_token = CounterToken(1, direct=False)
|
|
301
|
+
second_counter_token = CounterToken(1, direct=True)
|
|
302
|
+
|
|
303
|
+
print(SimpleToken(first_counter_token, second_counter_token).cancelled) # False
|
|
304
|
+
print(first_counter_token.cancelled) # True
|
|
305
|
+
print(second_counter_token.cancelled) # False
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Like all other tokens, `CounterToken` can accept other tokens as parameters during initialization:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from cantok import SimpleToken, CounterToken, TimeoutToken
|
|
312
|
+
|
|
313
|
+
token = CounterToken(15, SimpleToken(), TimeoutToken(5))
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`CounterToken` is thread-safe.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from cantok.tokens.abstract_token import AbstractToken # noqa: F401
|
|
2
|
+
from cantok.tokens.simple_token import SimpleToken # noqa: F401
|
|
3
|
+
from cantok.tokens.condition_token import ConditionToken # noqa: F401
|
|
4
|
+
from cantok.tokens.counter_token import CounterToken # noqa: F401
|
|
5
|
+
from cantok.tokens.timeout_token import TimeoutToken
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
TimeOutToken = TimeoutToken
|
|
File without changes
|