reykit 1.0.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.
reykit/rmultitask.py ADDED
@@ -0,0 +1,871 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ @Time : 2022-12-19 20:06:20
6
+ @Author : Rey
7
+ @Contact : reyxbo@163.com
8
+ @Explain : Multi task methods.
9
+ """
10
+
11
+
12
+ from __future__ import annotations
13
+ from typing import Any, Optional, Literal, Union
14
+ from collections.abc import Callable, Iterable, Generator, Coroutine
15
+ from threading import RLock as TRLock, get_ident as threading_get_ident
16
+ from concurrent.futures import ThreadPoolExecutor, Future as CFuture, as_completed as concurrent_as_completed
17
+ from asyncio import (
18
+ Future as AFuture,
19
+ Queue as AQueue,
20
+ Lock as ALock,
21
+ run as asyncio_run,
22
+ gather as asyncio_gather,
23
+ iscoroutine
24
+ )
25
+ from asyncio.queues import QueueEmpty
26
+ from aiohttp import ClientSession, ClientResponse
27
+
28
+ from .rexception import throw, check_most_one, check_response_code
29
+ from .rtime import sleep, RTimeMark
30
+ from .rwrap import wrap_thread
31
+
32
+
33
+ __all__ = (
34
+ 'async_run',
35
+ 'async_request',
36
+ 'RThreadLock',
37
+ 'RAsyncLock',
38
+ 'RThreadPool',
39
+ 'RAsyncPool'
40
+ )
41
+
42
+
43
+ def async_run(*coroutines: Coroutine) -> list:
44
+ """
45
+ Asynchronous run `Coroutine` instances.
46
+
47
+ Parameters
48
+ ----------
49
+ coroutines : `Coroutine` instances.
50
+
51
+ Returns
52
+ -------
53
+ Run result list.
54
+ """
55
+
56
+
57
+ # Define.
58
+ async def gather_coroutine() -> AFuture:
59
+ """
60
+ Get `Future` instance.
61
+
62
+ Returns
63
+ -------
64
+ Future instance.
65
+ """
66
+
67
+ # Gather.
68
+ future = await asyncio_gather(*coroutines)
69
+
70
+ return future
71
+
72
+
73
+ # Run.
74
+ result = asyncio_run(gather_coroutine())
75
+
76
+ return result
77
+
78
+
79
+ async def async_request(
80
+ url: str,
81
+ params: Optional[dict] = None,
82
+ data: Optional[Union[dict, str, bytes]] = None,
83
+ json: Optional[dict] = None,
84
+ headers: dict[str, str] = {},
85
+ timeout: Optional[float] = None,
86
+ proxy: Optional[str] = None,
87
+ method: Optional[Literal['get', 'post', 'put', 'patch', 'delete', 'options', 'head']] = None,
88
+ check: Union[bool, int, Iterable[int]] = False,
89
+ handler: Optional[Union[str, tuple[str], Callable[[ClientResponse], Union[Coroutine, Any]]]] = None
90
+ ) -> Any:
91
+ """
92
+ Get asynchronous `Coroutine` instance of send request.
93
+
94
+ Parameters
95
+ ----------
96
+ url : Request URL.
97
+ params : Request URL add parameters.
98
+ data : Request body data. Conflict with parameter `json`.
99
+ - `dict`, Convert to `key=value&...`: format bytes.
100
+ Automatic set `Content-Type` to `application/x-www-form-urlencoded`.
101
+ - `dict and a certain value is 'bytes' type`: Key is parameter name and file name, value is file data.
102
+ Automatic set `Content-Type` to `multipart/form-data`.
103
+ - `str`: File path to read file bytes data.
104
+ Automatic set `Content-Type` to file media type, and `filename` to file name.
105
+ - `bytes`: File bytes data.
106
+ Automatic set `Content-Type` to file media type.
107
+ json : Request body data, convert to `JSON` format. Conflict with parameter `data`.
108
+ Automatic set `Content-Type` to `application/json`.
109
+ headers : Request header data.
110
+ timeout : Request maximun waiting time.
111
+ proxy : Proxy URL.
112
+ method : Request method.
113
+ - `None`: Automatic judge.
114
+ When parameter `data` or `json` not has value, then request method is `get`.
115
+ When parameter `data` or `json` has value, then request method is `post`.
116
+ - `Literal['get', 'post', 'put', 'patch', 'delete', 'options', 'head']`: Use this request method.
117
+ check : Check response code, and throw exception.
118
+ - `Literal[False]`: Not check.
119
+ - `Literal[True]`: Check if is between 200 and 299.
120
+ - `int`: Check if is this value.
121
+ - `Iterable`: Check if is in sequence.
122
+
123
+ handler : Response handler.
124
+ - `None`: Automatic handle.
125
+ `Response 'Content-Type' is 'application/json'`: Use `ClientResponse.json` method.
126
+ `Response 'Content-Type' is 'text/plain; charset=utf-8'`: Use `ClientResponse.text` method.
127
+ `Other`: Use `ClientResponse.read` method.
128
+ - `str`: Get this attribute.
129
+ `Callable`: Execute this method. When return `Coroutine`, then use `await` syntax execute `Coroutine`.
130
+ `Any`: Return this value.
131
+ - `tuple[str]`: Get these attribute.
132
+ `Callable`: Execute this method. When return `Coroutine`, then use `await` syntax execute `Coroutine`.
133
+ `Any`: Return this value.
134
+ - `Callable`, Execute this method. When return `Coroutine`, then use `await`: syntax execute `Coroutine`.
135
+
136
+ Returns
137
+ -------
138
+ Response handler result.
139
+ """
140
+
141
+ # Check.
142
+ check_most_one(data, json)
143
+
144
+ # Handle parameter.
145
+ if method is None:
146
+ if data is None and json is None:
147
+ method = 'get'
148
+ else:
149
+ method = 'post'
150
+
151
+ # Session.
152
+ async with ClientSession() as session:
153
+
154
+ # Request.
155
+ async with session.request(
156
+ method,
157
+ url,
158
+ params=params,
159
+ data=data,
160
+ json=json,
161
+ headers=headers,
162
+ timeout=timeout,
163
+ proxy=proxy
164
+ ) as response:
165
+
166
+ # Check code.
167
+ if check is not False:
168
+ if check is True:
169
+ range_ = None
170
+ else:
171
+ range_ = check
172
+ check_response_code(response.status, range_)
173
+
174
+ # Receive.
175
+ match handler:
176
+
177
+ ## Auto.
178
+ case None:
179
+ match response.content_type:
180
+ case 'application/json':
181
+ result = await response.json()
182
+ case 'text/plain; charset=utf-8':
183
+
184
+ # Set encode type.
185
+ if response.get_encoding() == 'ISO-8859-1':
186
+ encoding = 'utf-8'
187
+ else:
188
+ encoding = None
189
+
190
+ result = await response.text(encoding=encoding)
191
+ case _:
192
+ result = await response.read()
193
+
194
+ ## Attribute.
195
+ case str():
196
+ result = getattr(response, handler)
197
+
198
+ ### Method.
199
+ if callable(result):
200
+ result = result()
201
+
202
+ #### Coroutine.
203
+ if iscoroutine(result):
204
+ result = await result
205
+
206
+ ## Attributes.
207
+ case tuple():
208
+ result = []
209
+ for key in handler:
210
+ result_element = getattr(response, key)
211
+
212
+ ### Method.
213
+ if callable(result_element):
214
+ result_element = result_element()
215
+
216
+ #### Coroutine.
217
+ if iscoroutine(result_element):
218
+ result_element = await result_element
219
+
220
+ result.append(result_element)
221
+
222
+ ## Method.
223
+ case _ if callable(handler):
224
+ result = handler(response)
225
+
226
+ ### Coroutine.
227
+ if iscoroutine(result):
228
+ result = await result
229
+
230
+ ## Throw exception.
231
+ case _:
232
+ throw(TypeError, handler)
233
+
234
+ return result
235
+
236
+
237
+ class RThreadLock():
238
+ """
239
+ Rey's `thread lock` type.
240
+ """
241
+
242
+
243
+ def __init__(self) -> None:
244
+ """
245
+ Build `thread lock` attributes.
246
+ """
247
+
248
+ # Set attribute.
249
+ self.lock = TRLock()
250
+ self.acquire_thread_id: Optional[int] = None
251
+
252
+
253
+ def acquire(
254
+ self,
255
+ timeout: float = None
256
+ ) -> bool:
257
+ """
258
+ Wait and acquire thread lock.
259
+
260
+ Parameters
261
+ ----------
262
+ timeout : Maximum wait seconds.
263
+ - `None`: Not limit.
264
+ - `float`: Use this value.
265
+
266
+ Returns
267
+ -------
268
+ Whether acquire success.
269
+ """
270
+
271
+ # Handle parameter.
272
+ if timeout is None:
273
+ timeout = -1
274
+
275
+ # Acquire.
276
+ result = self.lock.acquire(timeout=timeout)
277
+
278
+ # Update attribute.
279
+ if result:
280
+ thread_id = threading_get_ident()
281
+ self.acquire_thread_id = thread_id
282
+
283
+ return result
284
+
285
+
286
+ def release(self) -> None:
287
+ """
288
+ Release thread lock.
289
+ """
290
+
291
+ # Release.
292
+ self.lock.release()
293
+
294
+ # Update attribute.
295
+ self.acquire_thread_id = None
296
+
297
+
298
+ def __call__(self) -> None:
299
+ """
300
+ Automatic judge, wait and acquire thread lock, or release thread lock.
301
+ """
302
+
303
+ # Release.
304
+ thread_id = threading_get_ident()
305
+ if thread_id == self.acquire_thread_id:
306
+ self.release()
307
+
308
+ # Acquire.
309
+ else:
310
+ self.acquire()
311
+
312
+
313
+ class RAsyncLock():
314
+ """
315
+ Rey's `asynchronous lock` type.
316
+ """
317
+
318
+
319
+ def __init__(self) -> None:
320
+ """
321
+ Build `asynchronous lock` attributes.
322
+ """
323
+
324
+ # Set attribute.
325
+ self.lock = ALock()
326
+
327
+
328
+ def acquire(
329
+ self,
330
+ timeout: float = None
331
+ ) -> bool:
332
+ """
333
+ Wait and acquire thread lock.
334
+
335
+ Parameters
336
+ ----------
337
+ timeout : Maximum wait seconds.
338
+ - `None`: Not limit.
339
+ - `float`: Use this value.
340
+
341
+ Returns
342
+ -------
343
+ Whether acquire success.
344
+ """
345
+
346
+ # Handle parameter.
347
+ if timeout is None:
348
+ timeout = -1
349
+
350
+ # Acquire.
351
+ result = self.lock.acquire()
352
+
353
+ return result
354
+
355
+
356
+ def release(self) -> None:
357
+ """
358
+ Release thread lock.
359
+ """
360
+
361
+ # Release.
362
+ self.lock.release()
363
+
364
+
365
+ class RThreadPool(object):
366
+ """
367
+ Rey's `thread pool` type.
368
+ """
369
+
370
+
371
+ def __init__(
372
+ self,
373
+ task: Callable,
374
+ *args: Any,
375
+ _max_workers: Optional[int] = None,
376
+ **kwargs: Any
377
+ ) -> None:
378
+ """
379
+ Build `thread pool` attributes.
380
+
381
+ Parameters
382
+ ----------
383
+ task : Thread task.
384
+ args : Task default position arguments.
385
+ _max_workers : Maximum number of threads.
386
+ - `None`: Number of CPU + 4, 32 maximum.
387
+ - `int`: Use this value, no maximum limit.
388
+ kwargs : Task default keyword arguments.
389
+ """
390
+
391
+ # Set attribute.
392
+ self.task = task
393
+ self.args = args
394
+ self.kwargs = kwargs
395
+ self.thread_pool = ThreadPoolExecutor(
396
+ _max_workers,
397
+ task.__name__
398
+ )
399
+ self.futures: list[CFuture] = []
400
+
401
+
402
+ def one(
403
+ self,
404
+ *args: Any,
405
+ **kwargs: Any
406
+ ) -> CFuture:
407
+ """
408
+ Add and start a task to the thread pool.
409
+
410
+ Parameters
411
+ ----------
412
+ args : Task position arguments, after default position arguments.
413
+ kwargs : Task keyword arguments, after default keyword arguments.
414
+
415
+ Returns
416
+ -------
417
+ Task instance.
418
+ """
419
+
420
+ # Set parameter.
421
+ func_args = (
422
+ *self.args,
423
+ *args
424
+ )
425
+ func_kwargs = {
426
+ **self.kwargs,
427
+ **kwargs
428
+ }
429
+
430
+ # Submit.
431
+ future = self.thread_pool.submit(
432
+ self.task,
433
+ *func_args,
434
+ **func_kwargs
435
+ )
436
+
437
+ # Save.
438
+ self.futures.append(future)
439
+
440
+ return future
441
+
442
+
443
+ def batch(
444
+ self,
445
+ *args: tuple,
446
+ **kwargs: tuple
447
+ ) -> list[CFuture]:
448
+ """
449
+ Add and start a batch of tasks to the thread pool.
450
+ parameters sequence will combine one by one, and discard excess parameters.
451
+
452
+ Parameters
453
+ ----------
454
+ args : Sequence of task position arguments, after default position arguments.
455
+ kwargs : Sequence of task keyword arguments, after default keyword arguments.
456
+
457
+ Returns
458
+ -------
459
+ Task instance list.
460
+
461
+ Examples
462
+ --------
463
+ >>> func = lambda *args, **kwargs: print(args, kwargs)
464
+ >>> a = (1, 2)
465
+ >>> b = (3, 4, 5)
466
+ >>> c = (11, 12)
467
+ >>> d = (13, 14, 15)
468
+ >>> thread_pool = RThreadPool(func, 0, z=0)
469
+ >>> thread_pool.batch(a, b, c=c, d=d)
470
+ (0, 1, 3) {'z': 0, 'c': 11, 'd': 13}
471
+ (0, 2, 4) {'z': 0, 'c': 12, 'd': 14}
472
+ """
473
+
474
+ # Combine.
475
+ args_zip = zip(*args)
476
+ kwargs_zip = zip(
477
+ *[
478
+ [
479
+ (key, value)
480
+ for value in values
481
+ ]
482
+ for key, values in kwargs.items()
483
+ ]
484
+ )
485
+ params_zip = zip(args_zip, kwargs_zip)
486
+
487
+ # Batch submit.
488
+ futures = [
489
+ self.one(*args_, **dict(kwargs_))
490
+ for args_, kwargs_ in params_zip
491
+ ]
492
+
493
+ # Save.
494
+ self.futures.extend(futures)
495
+
496
+ return futures
497
+
498
+
499
+ def generate(
500
+ self,
501
+ timeout: Optional[float] = None
502
+ ) -> Generator[CFuture]:
503
+ """
504
+ Return the generator of added task instance.
505
+
506
+ Parameters
507
+ ----------
508
+ timeout : Call generator maximum waiting seconds, timeout throw exception.
509
+ - `None`: Infinite.
510
+ - `float`: Set this seconds.
511
+
512
+ Returns
513
+ -------
514
+ Generator of added task instance.
515
+ """
516
+
517
+ # Get parameter.
518
+ self.futures, futures = [], self.futures
519
+
520
+ # Build.
521
+ generator = concurrent_as_completed(
522
+ futures,
523
+ timeout
524
+ )
525
+
526
+ return generator
527
+
528
+
529
+ def repeat(
530
+ self,
531
+ number: int
532
+ ) -> list[CFuture]:
533
+ """
534
+ Add and start a batch of tasks to the thread pool, and only with default parameters.
535
+
536
+ Parameters
537
+ ----------
538
+ number : Number of add.
539
+
540
+ Returns
541
+ -------
542
+ Task instance list.
543
+ """
544
+
545
+ # Batch submit.
546
+ futures = [
547
+ self.one()
548
+ for _ in range(number)
549
+ ]
550
+
551
+ # Save.
552
+ self.futures.extend(futures)
553
+
554
+ return futures
555
+
556
+
557
+ def join(self) -> None:
558
+ """
559
+ Block until all tasks are done.
560
+ """
561
+
562
+ # Generator.
563
+ generator = self.generate()
564
+
565
+ # Wait.
566
+ for _ in generator:
567
+ pass
568
+
569
+
570
+ def __iter__(self) -> Generator:
571
+ """
572
+ Return the generator of task result.
573
+
574
+ Returns
575
+ -------
576
+ Generator of task result.
577
+ """
578
+
579
+ # Generator.
580
+ generator = self.generate()
581
+
582
+ # Generate.
583
+ for future in generator:
584
+ yield future.result()
585
+
586
+
587
+ __call__ = one
588
+
589
+
590
+ __mul__ = repeat
591
+
592
+
593
+ class RAsyncPool(object):
594
+ """
595
+ Rey's `asynchronous pool` type.
596
+ """
597
+
598
+
599
+ def __init__(
600
+ self,
601
+ async_func: Callable[..., Coroutine],
602
+ *args: Any,
603
+ _max_async: int = 10,
604
+ _exc_handler: Optional[Callable] = None,
605
+ **kwargs: Any
606
+ ) -> None:
607
+ """
608
+ Build `asynchronous pool` attributes.
609
+
610
+ Parameters
611
+ ----------
612
+ async_func : Function of create asynchronous `Coroutine`.
613
+ args : Function default position arguments.
614
+ _max_async : Maximum number of asynchronous.
615
+ _exc_handler : `Coroutine` execution exception handler, will return value.
616
+ kwargs : Function default keyword arguments.
617
+ """
618
+
619
+ # Set attribute.
620
+ self.async_func = async_func
621
+ self.args = args
622
+ self.kwargs = kwargs
623
+ self.exc_handler = _exc_handler
624
+ self.queue_input: AQueue[tuple[tuple, dict]] = AQueue()
625
+ self.queue_output = AQueue()
626
+ self.queue_count = 0
627
+
628
+ # Start.
629
+ self._start_workers(_max_async)
630
+
631
+
632
+ @wrap_thread
633
+ def _start_workers(
634
+ self,
635
+ worker_n: int
636
+ ) -> None:
637
+ """
638
+ Start workers of execute asynchronous `Coroutine`.
639
+
640
+ Parameters
641
+ ----------
642
+ worker_n : Number of execute asynchronous `Coroutine` workers.
643
+ """
644
+
645
+
646
+ # Define.
647
+ async def async_worker() -> None:
648
+ """
649
+ Worker of execute asynchronous `Coroutine`.
650
+ """
651
+
652
+ # Loop.
653
+ while True:
654
+
655
+ # Get parameter.
656
+ args, kwargs = await self.queue_input.get()
657
+
658
+ # Execute.
659
+ try:
660
+ result = await self.async_func(*args, **kwargs)
661
+
662
+ # Handle exception.
663
+ except:
664
+ if self.exc_handler is not None:
665
+ result = self.exc_handler()
666
+ await self.queue_output.put(result)
667
+
668
+ ## Count.
669
+ else:
670
+ self.queue_count -= 1
671
+
672
+ else:
673
+ await self.queue_output.put(result)
674
+
675
+
676
+ # Create.
677
+ coroutines = [
678
+ async_worker()
679
+ for _ in range(worker_n)
680
+ ]
681
+
682
+ # Start.
683
+ async_run(*coroutines)
684
+
685
+
686
+ def one(
687
+ self,
688
+ *args: Any,
689
+ **kwargs: Any
690
+ ) -> None:
691
+ """
692
+ Add and start a task to the pool.
693
+
694
+ Parameters
695
+ ----------
696
+ args : Function position arguments, after default position arguments.
697
+ kwargs : Function keyword arguments, after default keyword arguments.
698
+ """
699
+
700
+ # Set parameter.
701
+ func_args = (
702
+ *self.args,
703
+ *args
704
+ )
705
+ func_kwargs = {
706
+ **self.kwargs,
707
+ **kwargs
708
+ }
709
+ item = (
710
+ func_args,
711
+ func_kwargs
712
+ )
713
+
714
+ # Count.
715
+ self.queue_count += 1
716
+
717
+ # Put.
718
+ self.queue_input.put_nowait(item)
719
+
720
+
721
+ def batch(
722
+ self,
723
+ *args: tuple,
724
+ **kwargs: tuple
725
+ ) -> None:
726
+ """
727
+ Add and start a batch of tasks to the pool.
728
+ parameters sequence will combine one by one, and discard excess parameters.
729
+
730
+ Parameters
731
+ ----------
732
+ args : Sequence of function position arguments, after default position arguments.
733
+ kwargs : Sequence of function keyword arguments, after default keyword arguments.
734
+
735
+ Examples
736
+ --------
737
+ >>> async def func(*args, **kwargs):
738
+ ... print(args, kwargs)
739
+ >>> a = (1, 2)
740
+ >>> b = (3, 4, 5)
741
+ >>> c = (11, 12)
742
+ >>> d = (13, 14, 15)
743
+ >>> async_pool = RAsyncPool(func, 0, z=0)
744
+ >>> async_pool.batch(a, b, c=c, d=d)
745
+ (0, 1, 3) {'z': 0, 'c': 11, 'd': 13}
746
+ (0, 2, 4) {'z': 0, 'c': 12, 'd': 14}
747
+ """
748
+
749
+ # Combine.
750
+ args_zip = zip(*args)
751
+ kwargs_zip = zip(
752
+ *[
753
+ [
754
+ (key, value)
755
+ for value in values
756
+ ]
757
+ for key, values in kwargs.items()
758
+ ]
759
+ )
760
+ params_zip = zip(args_zip, kwargs_zip)
761
+
762
+ # Batch submit.
763
+ for args_, kwargs_ in params_zip:
764
+ self.one(*args_, **dict(kwargs_))
765
+
766
+
767
+ def repeat(
768
+ self,
769
+ number: int
770
+ ) -> list[CFuture]:
771
+ """
772
+ Add and start a batch of tasks to the pool, and only with default parameters.
773
+
774
+ Parameters
775
+ ----------
776
+ number : Number of add.
777
+ """
778
+
779
+ # Batch submit.
780
+ for _ in range(number):
781
+ self.one()
782
+
783
+
784
+ def get(
785
+ self,
786
+ timeout: Optional[float] = None
787
+ ) -> Any:
788
+ """
789
+ Get one execution result of asynchronous `Coroutine`, will block.
790
+
791
+ Parameters
792
+ ----------
793
+ timeout : Maximum seconds of block.
794
+
795
+ Returns
796
+ -------
797
+ One execution result.
798
+ """
799
+
800
+ # Set parameter.
801
+ if timeout is not None:
802
+ rtm = RTimeMark()
803
+ rtm()
804
+
805
+ # Loop.
806
+ while True:
807
+
808
+ # Judge.
809
+ if not self.queue_output.empty():
810
+
811
+ # Get.
812
+ try:
813
+ result = self.queue_output.get_nowait()
814
+ except QueueEmpty:
815
+ pass
816
+ else:
817
+
818
+ # Count.
819
+ self.queue_count -= 1
820
+
821
+ return result
822
+
823
+ # Timeout.
824
+ if timeout is not None:
825
+ rtm()
826
+ if rtm.total_spend > timeout:
827
+ throw(TimeoutError, timeout)
828
+
829
+ # Sleep.
830
+ sleep(0.01)
831
+
832
+
833
+ def join(self) -> None:
834
+ """
835
+ Block until all asynchronous `Coroutine` are done.
836
+ """
837
+
838
+ # Generate.
839
+ while True:
840
+
841
+ # Break.
842
+ if self.queue_count == 0:
843
+ break
844
+
845
+ self.get()
846
+
847
+
848
+ def __iter__(self) -> Generator:
849
+ """
850
+ Return the generator of result of asynchronous `Coroutine`.
851
+
852
+ Returns
853
+ -------
854
+ Generator of result of asynchronous `Coroutine`.
855
+ """
856
+
857
+ # Generate.
858
+ while True:
859
+
860
+ # Break.
861
+ if self.queue_count == 0:
862
+ break
863
+
864
+ result = self.get()
865
+ yield result
866
+
867
+
868
+ __call__ = one
869
+
870
+
871
+ __mul__ = repeat