dotask 0.3.0__tar.gz → 0.3.2__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.
- dotask-0.3.2/PKG-INFO +149 -0
- dotask-0.3.2/README.md +130 -0
- dotask-0.3.2/dotask/__init__.py +4 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask/task.py +5 -2
- dotask-0.3.2/dotask/util/__init__.py +6 -0
- dotask-0.3.2/dotask/util/timer.py +32 -0
- dotask-0.3.2/dotask.egg-info/PKG-INFO +149 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask.egg-info/SOURCES.txt +2 -1
- {dotask-0.3.0 → dotask-0.3.2}/setup.cfg +1 -1
- dotask-0.3.0/PKG-INFO +0 -83
- dotask-0.3.0/README.md +0 -64
- dotask-0.3.0/dotask/__init__.py +0 -2
- dotask-0.3.0/dotask/util/__init__.py +0 -3
- dotask-0.3.0/dotask.egg-info/PKG-INFO +0 -83
- {dotask-0.3.0 → dotask-0.3.2}/LICENSE +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask/util/limit.py +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask/util/logger.py +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask/util/shell.py +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask.egg-info/dependency_links.txt +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/dotask.egg-info/top_level.txt +0 -0
- {dotask-0.3.0 → dotask-0.3.2}/pyproject.toml +0 -0
dotask-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotask
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: 发布订阅注解方式
|
|
5
|
+
Home-page: https://gitee.com/d-yz/task-manager
|
|
6
|
+
Author: dyz
|
|
7
|
+
Author-email: 837701454@qq.com
|
|
8
|
+
Keywords: 发布订阅,注解,dotask
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
## 版本说明
|
|
21
|
+
### 0.1.0
|
|
22
|
+
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
23
|
+
- 常用参数:
|
|
24
|
+
- 生产者: `role="producer",topic="自定义"`
|
|
25
|
+
- 消费者: `role="consumer",subscribe="自定义",publish_after="自定义"`
|
|
26
|
+
- 说明:
|
|
27
|
+
- `role`: 用于区分上下游 `producer/consumer`
|
|
28
|
+
- `topic`: 发布的主题
|
|
29
|
+
- `subscribe`: 对哪些主题感兴趣
|
|
30
|
+
- `publish_after`: 消费完后继续发布新主题
|
|
31
|
+
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
32
|
+
### 0.2.x
|
|
33
|
+
1. dotask暴露`topic_manager`=>可以更好的管理生产消费者
|
|
34
|
+
2. 迁移`logger`至`util`包下
|
|
35
|
+
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
36
|
+
1. `max_concurrent`:控制并发执行的shell数量
|
|
37
|
+
2. 引入`dotask.Shell` 调用`Shell(max_concurrent=?).local_shell_execute(cmd,callback)`来使用
|
|
38
|
+
### 0.3.x
|
|
39
|
+
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
40
|
+
- 参数说明
|
|
41
|
+
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
42
|
+
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
43
|
+
2. `topic_manager`内部维护`unfinished_tasks`队列,记录所有消费失败的数据及其异常
|
|
44
|
+
3. 添加定时器`Timer`,可以配合`unfinished_tasks`再次处理未消费完成的数据
|
|
45
|
+
|
|
46
|
+
## 快速开始
|
|
47
|
+
### 安装
|
|
48
|
+
`pip install dotask`
|
|
49
|
+
### 示例
|
|
50
|
+
1. 简单的生产发布模式
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
from dotask import task
|
|
54
|
+
from dotask.util import logger
|
|
55
|
+
import random
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
|
|
59
|
+
@task(role="producer",topic="scan")
|
|
60
|
+
def a():
|
|
61
|
+
return random.randint(1,10)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
65
|
+
def b(data):
|
|
66
|
+
if data >5:
|
|
67
|
+
return data
|
|
68
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
69
|
+
|
|
70
|
+
@task(role="consumer",subscribe="sayHi")
|
|
71
|
+
def c(data):
|
|
72
|
+
logger.debug(f"c触发")
|
|
73
|
+
logger.info(f"消费:{data}")
|
|
74
|
+
//=====================================================================
|
|
75
|
+
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
76
|
+
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
77
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
78
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
79
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
80
|
+
2026-02-08 17:30:38 - INFO - 消费:8
|
|
81
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
82
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
83
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
84
|
+
2026-02-08 17:30:38 - INFO - 消费:6
|
|
85
|
+
//======================================================================
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. 令牌桶+定时器使用
|
|
89
|
+
```
|
|
90
|
+
from dotask import task,topic_manager
|
|
91
|
+
from dotask.util import logger,TokenBucket,Timer
|
|
92
|
+
import random
|
|
93
|
+
|
|
94
|
+
token_bucket = TokenBucket(capacity=1, rate=1)
|
|
95
|
+
|
|
96
|
+
def error_inspect():
|
|
97
|
+
logger.warning(f"未完成任务:{topic_manager.unfinished_tasks.qsize()}")
|
|
98
|
+
|
|
99
|
+
item = topic_manager.unfinished_tasks.get()
|
|
100
|
+
logger.warning(f"其一:{item}")
|
|
101
|
+
logger.warning(f"重新发布失败数据,{item.get("unfinished_data")}")
|
|
102
|
+
topic_manager.publish(item.get("occurred"),item.get("unfinished_data"))
|
|
103
|
+
|
|
104
|
+
if __name__ == '__main__':
|
|
105
|
+
|
|
106
|
+
Timer(interval=2,task=error_inspect).start()
|
|
107
|
+
|
|
108
|
+
@task(role="producer",topic="scan")
|
|
109
|
+
def a():
|
|
110
|
+
if token_bucket.get_token(block=True):
|
|
111
|
+
return random.randint(1,10)
|
|
112
|
+
else:
|
|
113
|
+
logger.warning("令牌桶无可用令牌,本次生成数据失败")
|
|
114
|
+
return -1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
118
|
+
def b(data):
|
|
119
|
+
if data >5:
|
|
120
|
+
return data
|
|
121
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
122
|
+
|
|
123
|
+
@task(role="consumer",subscribe="sayHi")
|
|
124
|
+
def c(data):
|
|
125
|
+
logger.debug(f"c触发,接收数据{data}")
|
|
126
|
+
raise Exception("处理失败")
|
|
127
|
+
//===========================================================================
|
|
128
|
+
2026-02-11 05:33:49 - INFO - Timer started,runs every 2 seconds
|
|
129
|
+
2026-02-11 05:33:49 - INFO - publish topic-[scan]:1
|
|
130
|
+
2026-02-11 05:33:49 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
131
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:3
|
|
132
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:3,不会继续发布sayHi主题
|
|
133
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:1
|
|
134
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
135
|
+
2026-02-11 05:33:51 - WARNING - 未完成任务:0
|
|
136
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:8
|
|
137
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
138
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
139
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
140
|
+
2026-02-11 05:33:51 - WARNING - 其一:{'unfinished_data': 8, 'occurred': 'sayHi', 'error': Exception('处理失败')}
|
|
141
|
+
2026-02-11 05:33:51 - WARNING - 重新发布失败数据,8
|
|
142
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
143
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
144
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
145
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:7
|
|
146
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:7
|
|
147
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据7
|
|
148
|
+
//==============================================================================
|
|
149
|
+
```
|
dotask-0.3.2/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
## 版本说明
|
|
2
|
+
### 0.1.0
|
|
3
|
+
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
4
|
+
- 常用参数:
|
|
5
|
+
- 生产者: `role="producer",topic="自定义"`
|
|
6
|
+
- 消费者: `role="consumer",subscribe="自定义",publish_after="自定义"`
|
|
7
|
+
- 说明:
|
|
8
|
+
- `role`: 用于区分上下游 `producer/consumer`
|
|
9
|
+
- `topic`: 发布的主题
|
|
10
|
+
- `subscribe`: 对哪些主题感兴趣
|
|
11
|
+
- `publish_after`: 消费完后继续发布新主题
|
|
12
|
+
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
13
|
+
### 0.2.x
|
|
14
|
+
1. dotask暴露`topic_manager`=>可以更好的管理生产消费者
|
|
15
|
+
2. 迁移`logger`至`util`包下
|
|
16
|
+
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
17
|
+
1. `max_concurrent`:控制并发执行的shell数量
|
|
18
|
+
2. 引入`dotask.Shell` 调用`Shell(max_concurrent=?).local_shell_execute(cmd,callback)`来使用
|
|
19
|
+
### 0.3.x
|
|
20
|
+
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
21
|
+
- 参数说明
|
|
22
|
+
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
23
|
+
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
24
|
+
2. `topic_manager`内部维护`unfinished_tasks`队列,记录所有消费失败的数据及其异常
|
|
25
|
+
3. 添加定时器`Timer`,可以配合`unfinished_tasks`再次处理未消费完成的数据
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
### 安装
|
|
29
|
+
`pip install dotask`
|
|
30
|
+
### 示例
|
|
31
|
+
1. 简单的生产发布模式
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
from dotask import task
|
|
35
|
+
from dotask.util import logger
|
|
36
|
+
import random
|
|
37
|
+
|
|
38
|
+
if __name__ == '__main__':
|
|
39
|
+
|
|
40
|
+
@task(role="producer",topic="scan")
|
|
41
|
+
def a():
|
|
42
|
+
return random.randint(1,10)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
46
|
+
def b(data):
|
|
47
|
+
if data >5:
|
|
48
|
+
return data
|
|
49
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
50
|
+
|
|
51
|
+
@task(role="consumer",subscribe="sayHi")
|
|
52
|
+
def c(data):
|
|
53
|
+
logger.debug(f"c触发")
|
|
54
|
+
logger.info(f"消费:{data}")
|
|
55
|
+
//=====================================================================
|
|
56
|
+
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
57
|
+
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
58
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
59
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
60
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
61
|
+
2026-02-08 17:30:38 - INFO - 消费:8
|
|
62
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
63
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
64
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
65
|
+
2026-02-08 17:30:38 - INFO - 消费:6
|
|
66
|
+
//======================================================================
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. 令牌桶+定时器使用
|
|
70
|
+
```
|
|
71
|
+
from dotask import task,topic_manager
|
|
72
|
+
from dotask.util import logger,TokenBucket,Timer
|
|
73
|
+
import random
|
|
74
|
+
|
|
75
|
+
token_bucket = TokenBucket(capacity=1, rate=1)
|
|
76
|
+
|
|
77
|
+
def error_inspect():
|
|
78
|
+
logger.warning(f"未完成任务:{topic_manager.unfinished_tasks.qsize()}")
|
|
79
|
+
|
|
80
|
+
item = topic_manager.unfinished_tasks.get()
|
|
81
|
+
logger.warning(f"其一:{item}")
|
|
82
|
+
logger.warning(f"重新发布失败数据,{item.get("unfinished_data")}")
|
|
83
|
+
topic_manager.publish(item.get("occurred"),item.get("unfinished_data"))
|
|
84
|
+
|
|
85
|
+
if __name__ == '__main__':
|
|
86
|
+
|
|
87
|
+
Timer(interval=2,task=error_inspect).start()
|
|
88
|
+
|
|
89
|
+
@task(role="producer",topic="scan")
|
|
90
|
+
def a():
|
|
91
|
+
if token_bucket.get_token(block=True):
|
|
92
|
+
return random.randint(1,10)
|
|
93
|
+
else:
|
|
94
|
+
logger.warning("令牌桶无可用令牌,本次生成数据失败")
|
|
95
|
+
return -1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
99
|
+
def b(data):
|
|
100
|
+
if data >5:
|
|
101
|
+
return data
|
|
102
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
103
|
+
|
|
104
|
+
@task(role="consumer",subscribe="sayHi")
|
|
105
|
+
def c(data):
|
|
106
|
+
logger.debug(f"c触发,接收数据{data}")
|
|
107
|
+
raise Exception("处理失败")
|
|
108
|
+
//===========================================================================
|
|
109
|
+
2026-02-11 05:33:49 - INFO - Timer started,runs every 2 seconds
|
|
110
|
+
2026-02-11 05:33:49 - INFO - publish topic-[scan]:1
|
|
111
|
+
2026-02-11 05:33:49 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
112
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:3
|
|
113
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:3,不会继续发布sayHi主题
|
|
114
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:1
|
|
115
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
116
|
+
2026-02-11 05:33:51 - WARNING - 未完成任务:0
|
|
117
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:8
|
|
118
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
119
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
120
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
121
|
+
2026-02-11 05:33:51 - WARNING - 其一:{'unfinished_data': 8, 'occurred': 'sayHi', 'error': Exception('处理失败')}
|
|
122
|
+
2026-02-11 05:33:51 - WARNING - 重新发布失败数据,8
|
|
123
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
124
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
125
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
126
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:7
|
|
127
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:7
|
|
128
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据7
|
|
129
|
+
//==============================================================================
|
|
130
|
+
```
|
|
@@ -2,10 +2,10 @@ import threading
|
|
|
2
2
|
import functools
|
|
3
3
|
import time
|
|
4
4
|
import queue
|
|
5
|
+
from queue import Queue
|
|
5
6
|
from typing import Optional, Callable, Any, List, Set, Dict
|
|
6
7
|
from dotask.util import logger
|
|
7
8
|
|
|
8
|
-
|
|
9
9
|
# ------------------------------
|
|
10
10
|
# 主题管理器: 发布接口适配
|
|
11
11
|
# ------------------------------
|
|
@@ -15,6 +15,7 @@ class TopicManager:
|
|
|
15
15
|
self.topic_locks: Dict[str, threading.Lock] = {}
|
|
16
16
|
self.stop_flag = False
|
|
17
17
|
self.global_lock = threading.Lock()
|
|
18
|
+
self.unfinished_tasks:Queue[Dict[str,Any]] = queue.Queue()
|
|
18
19
|
|
|
19
20
|
def get_topic_queue(self, topic: str) -> queue.Queue:
|
|
20
21
|
with self.global_lock:
|
|
@@ -69,7 +70,8 @@ topic_manager = TopicManager()
|
|
|
69
70
|
# ------------------------------
|
|
70
71
|
def task(
|
|
71
72
|
func: Optional[Callable] = None,
|
|
72
|
-
role: str = "
|
|
73
|
+
role: str = ""
|
|
74
|
+
"",
|
|
73
75
|
topic: Optional[str] = None,
|
|
74
76
|
subscribe: Optional[str] = None,
|
|
75
77
|
publish_after: Optional[str] = None, # 新增:消费后发布的主题(多个用,分隔)
|
|
@@ -151,6 +153,7 @@ def task(
|
|
|
151
153
|
|
|
152
154
|
except Exception as e:
|
|
153
155
|
logger.error(f"Consumer [{thread_name}] failed to process message: {e}")
|
|
156
|
+
topic_manager.unfinished_tasks.put({"unfinished_data":data,"occurred":subscribe,"error":e})
|
|
154
157
|
|
|
155
158
|
# 开始消费
|
|
156
159
|
topic_manager.consume(subscribe_topics, handler)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from dotask.util import logger
|
|
3
|
+
|
|
4
|
+
class Timer:
|
|
5
|
+
def __init__(self,interval:float,task):
|
|
6
|
+
self.interval = interval
|
|
7
|
+
self.task = task
|
|
8
|
+
# 存储定时对象
|
|
9
|
+
self.timer = None
|
|
10
|
+
# 标记定时器是否运行
|
|
11
|
+
self.is_running = False
|
|
12
|
+
def _run_task(self):
|
|
13
|
+
"""
|
|
14
|
+
内部方法:执行任务并重启定时器
|
|
15
|
+
"""
|
|
16
|
+
self.is_running = True
|
|
17
|
+
try:
|
|
18
|
+
self.task()
|
|
19
|
+
finally:
|
|
20
|
+
# 任务完毕后,重新创建定时器
|
|
21
|
+
self.timer = threading.Timer(self.interval, self._run_task)
|
|
22
|
+
self.timer.start()
|
|
23
|
+
def start(self):
|
|
24
|
+
if not self.is_running:
|
|
25
|
+
self.timer = threading.Timer(self.interval, self._run_task)
|
|
26
|
+
self.timer.start()
|
|
27
|
+
logger.info(f"Timer started,runs every {self.interval} seconds")
|
|
28
|
+
def stop(self):
|
|
29
|
+
if self.timer and self.is_running:
|
|
30
|
+
self.timer.cancel()
|
|
31
|
+
self.is_running = False
|
|
32
|
+
logger.info(f"Timer stopped")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotask
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: 发布订阅注解方式
|
|
5
|
+
Home-page: https://gitee.com/d-yz/task-manager
|
|
6
|
+
Author: dyz
|
|
7
|
+
Author-email: 837701454@qq.com
|
|
8
|
+
Keywords: 发布订阅,注解,dotask
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
## 版本说明
|
|
21
|
+
### 0.1.0
|
|
22
|
+
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
23
|
+
- 常用参数:
|
|
24
|
+
- 生产者: `role="producer",topic="自定义"`
|
|
25
|
+
- 消费者: `role="consumer",subscribe="自定义",publish_after="自定义"`
|
|
26
|
+
- 说明:
|
|
27
|
+
- `role`: 用于区分上下游 `producer/consumer`
|
|
28
|
+
- `topic`: 发布的主题
|
|
29
|
+
- `subscribe`: 对哪些主题感兴趣
|
|
30
|
+
- `publish_after`: 消费完后继续发布新主题
|
|
31
|
+
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
32
|
+
### 0.2.x
|
|
33
|
+
1. dotask暴露`topic_manager`=>可以更好的管理生产消费者
|
|
34
|
+
2. 迁移`logger`至`util`包下
|
|
35
|
+
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
36
|
+
1. `max_concurrent`:控制并发执行的shell数量
|
|
37
|
+
2. 引入`dotask.Shell` 调用`Shell(max_concurrent=?).local_shell_execute(cmd,callback)`来使用
|
|
38
|
+
### 0.3.x
|
|
39
|
+
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
40
|
+
- 参数说明
|
|
41
|
+
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
42
|
+
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
43
|
+
2. `topic_manager`内部维护`unfinished_tasks`队列,记录所有消费失败的数据及其异常
|
|
44
|
+
3. 添加定时器`Timer`,可以配合`unfinished_tasks`再次处理未消费完成的数据
|
|
45
|
+
|
|
46
|
+
## 快速开始
|
|
47
|
+
### 安装
|
|
48
|
+
`pip install dotask`
|
|
49
|
+
### 示例
|
|
50
|
+
1. 简单的生产发布模式
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
from dotask import task
|
|
54
|
+
from dotask.util import logger
|
|
55
|
+
import random
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
|
|
59
|
+
@task(role="producer",topic="scan")
|
|
60
|
+
def a():
|
|
61
|
+
return random.randint(1,10)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
65
|
+
def b(data):
|
|
66
|
+
if data >5:
|
|
67
|
+
return data
|
|
68
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
69
|
+
|
|
70
|
+
@task(role="consumer",subscribe="sayHi")
|
|
71
|
+
def c(data):
|
|
72
|
+
logger.debug(f"c触发")
|
|
73
|
+
logger.info(f"消费:{data}")
|
|
74
|
+
//=====================================================================
|
|
75
|
+
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
76
|
+
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
77
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
78
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
79
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
80
|
+
2026-02-08 17:30:38 - INFO - 消费:8
|
|
81
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
82
|
+
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
83
|
+
2026-02-08 17:30:38 - DEBUG - c触发
|
|
84
|
+
2026-02-08 17:30:38 - INFO - 消费:6
|
|
85
|
+
//======================================================================
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. 令牌桶+定时器使用
|
|
89
|
+
```
|
|
90
|
+
from dotask import task,topic_manager
|
|
91
|
+
from dotask.util import logger,TokenBucket,Timer
|
|
92
|
+
import random
|
|
93
|
+
|
|
94
|
+
token_bucket = TokenBucket(capacity=1, rate=1)
|
|
95
|
+
|
|
96
|
+
def error_inspect():
|
|
97
|
+
logger.warning(f"未完成任务:{topic_manager.unfinished_tasks.qsize()}")
|
|
98
|
+
|
|
99
|
+
item = topic_manager.unfinished_tasks.get()
|
|
100
|
+
logger.warning(f"其一:{item}")
|
|
101
|
+
logger.warning(f"重新发布失败数据,{item.get("unfinished_data")}")
|
|
102
|
+
topic_manager.publish(item.get("occurred"),item.get("unfinished_data"))
|
|
103
|
+
|
|
104
|
+
if __name__ == '__main__':
|
|
105
|
+
|
|
106
|
+
Timer(interval=2,task=error_inspect).start()
|
|
107
|
+
|
|
108
|
+
@task(role="producer",topic="scan")
|
|
109
|
+
def a():
|
|
110
|
+
if token_bucket.get_token(block=True):
|
|
111
|
+
return random.randint(1,10)
|
|
112
|
+
else:
|
|
113
|
+
logger.warning("令牌桶无可用令牌,本次生成数据失败")
|
|
114
|
+
return -1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
118
|
+
def b(data):
|
|
119
|
+
if data >5:
|
|
120
|
+
return data
|
|
121
|
+
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
122
|
+
|
|
123
|
+
@task(role="consumer",subscribe="sayHi")
|
|
124
|
+
def c(data):
|
|
125
|
+
logger.debug(f"c触发,接收数据{data}")
|
|
126
|
+
raise Exception("处理失败")
|
|
127
|
+
//===========================================================================
|
|
128
|
+
2026-02-11 05:33:49 - INFO - Timer started,runs every 2 seconds
|
|
129
|
+
2026-02-11 05:33:49 - INFO - publish topic-[scan]:1
|
|
130
|
+
2026-02-11 05:33:49 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
131
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:3
|
|
132
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:3,不会继续发布sayHi主题
|
|
133
|
+
2026-02-11 05:33:50 - INFO - publish topic-[scan]:1
|
|
134
|
+
2026-02-11 05:33:50 - WARNING - 本次生成数字:1,不会继续发布sayHi主题
|
|
135
|
+
2026-02-11 05:33:51 - WARNING - 未完成任务:0
|
|
136
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:8
|
|
137
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
138
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
139
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
140
|
+
2026-02-11 05:33:51 - WARNING - 其一:{'unfinished_data': 8, 'occurred': 'sayHi', 'error': Exception('处理失败')}
|
|
141
|
+
2026-02-11 05:33:51 - WARNING - 重新发布失败数据,8
|
|
142
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:8
|
|
143
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据8
|
|
144
|
+
2026-02-11 05:33:51 - ERROR - Consumer [None] failed to process message: 处理失败
|
|
145
|
+
2026-02-11 05:33:51 - INFO - publish topic-[scan]:7
|
|
146
|
+
2026-02-11 05:33:51 - INFO - publish topic-[sayHi]:7
|
|
147
|
+
2026-02-11 05:33:51 - DEBUG - c触发,接收数据7
|
|
148
|
+
//==============================================================================
|
|
149
|
+
```
|
dotask-0.3.0/PKG-INFO
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: dotask
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: 发布订阅注解方式
|
|
5
|
-
Home-page: https://gitee.com/d-yz/task-manager
|
|
6
|
-
Author: dyz
|
|
7
|
-
Author-email: 837701454@qq.com
|
|
8
|
-
Keywords: 发布订阅,注解,dotask
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
|
-
## 版本说明
|
|
21
|
-
### 0.1.0
|
|
22
|
-
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
23
|
-
- 常用参数:
|
|
24
|
-
- 生产者: role="producer",topic="自定义"
|
|
25
|
-
- 消费者: role="consumer",subscribe="自定义",publish_after="自定义"
|
|
26
|
-
- 说明:
|
|
27
|
-
- role: 用于区分上下游 producer/consumer
|
|
28
|
-
- topic: 发布的主题
|
|
29
|
-
- subscribe: 对哪些主题感兴趣
|
|
30
|
-
- publish_after: 消费完后继续发布新主题
|
|
31
|
-
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
32
|
-
### 0.2.x
|
|
33
|
-
1. dotask暴露topic_manager=>可以更好的管理生产消费者
|
|
34
|
-
2. 迁移logger至util包下
|
|
35
|
-
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
36
|
-
1. max_concurrent:控制并发执行的shell数量
|
|
37
|
-
2. 引入dotask.Shell 调用Shell(max_concurrent=?).local_shell_execute(cmd,callback)来使用
|
|
38
|
-
### 0.3.x
|
|
39
|
-
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
40
|
-
- 参数说明
|
|
41
|
-
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
42
|
-
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
43
|
-
## 快速开始
|
|
44
|
-
### 安装
|
|
45
|
-
`pip install dotask`
|
|
46
|
-
### 示例
|
|
47
|
-
1. 简单的生产发布模式
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
from dotask import task
|
|
51
|
-
from dotask.util import logger
|
|
52
|
-
import random
|
|
53
|
-
|
|
54
|
-
if __name__ == '__main__':
|
|
55
|
-
|
|
56
|
-
@task(role="producer",topic="scan")
|
|
57
|
-
def a():
|
|
58
|
-
return random.randint(1,10)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
62
|
-
def b(data):
|
|
63
|
-
if data >5:
|
|
64
|
-
return data
|
|
65
|
-
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
66
|
-
|
|
67
|
-
@task(role="consumer",subscribe="sayHi")
|
|
68
|
-
def c(data):
|
|
69
|
-
logger.debug(f"c触发")
|
|
70
|
-
logger.info(f"消费:{data}")
|
|
71
|
-
//=====================================================================
|
|
72
|
-
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
73
|
-
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
74
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
75
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
76
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
77
|
-
2026-02-08 17:30:38 - INFO - 消费:8
|
|
78
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
79
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
80
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
81
|
-
2026-02-08 17:30:38 - INFO - 消费:6
|
|
82
|
-
//======================================================================
|
|
83
|
-
```
|
dotask-0.3.0/README.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
## 版本说明
|
|
2
|
-
### 0.1.0
|
|
3
|
-
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
4
|
-
- 常用参数:
|
|
5
|
-
- 生产者: role="producer",topic="自定义"
|
|
6
|
-
- 消费者: role="consumer",subscribe="自定义",publish_after="自定义"
|
|
7
|
-
- 说明:
|
|
8
|
-
- role: 用于区分上下游 producer/consumer
|
|
9
|
-
- topic: 发布的主题
|
|
10
|
-
- subscribe: 对哪些主题感兴趣
|
|
11
|
-
- publish_after: 消费完后继续发布新主题
|
|
12
|
-
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
13
|
-
### 0.2.x
|
|
14
|
-
1. dotask暴露topic_manager=>可以更好的管理生产消费者
|
|
15
|
-
2. 迁移logger至util包下
|
|
16
|
-
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
17
|
-
1. max_concurrent:控制并发执行的shell数量
|
|
18
|
-
2. 引入dotask.Shell 调用Shell(max_concurrent=?).local_shell_execute(cmd,callback)来使用
|
|
19
|
-
### 0.3.x
|
|
20
|
-
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
21
|
-
- 参数说明
|
|
22
|
-
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
23
|
-
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
24
|
-
## 快速开始
|
|
25
|
-
### 安装
|
|
26
|
-
`pip install dotask`
|
|
27
|
-
### 示例
|
|
28
|
-
1. 简单的生产发布模式
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
from dotask import task
|
|
32
|
-
from dotask.util import logger
|
|
33
|
-
import random
|
|
34
|
-
|
|
35
|
-
if __name__ == '__main__':
|
|
36
|
-
|
|
37
|
-
@task(role="producer",topic="scan")
|
|
38
|
-
def a():
|
|
39
|
-
return random.randint(1,10)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
43
|
-
def b(data):
|
|
44
|
-
if data >5:
|
|
45
|
-
return data
|
|
46
|
-
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
47
|
-
|
|
48
|
-
@task(role="consumer",subscribe="sayHi")
|
|
49
|
-
def c(data):
|
|
50
|
-
logger.debug(f"c触发")
|
|
51
|
-
logger.info(f"消费:{data}")
|
|
52
|
-
//=====================================================================
|
|
53
|
-
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
54
|
-
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
55
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
56
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
57
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
58
|
-
2026-02-08 17:30:38 - INFO - 消费:8
|
|
59
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
60
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
61
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
62
|
-
2026-02-08 17:30:38 - INFO - 消费:6
|
|
63
|
-
//======================================================================
|
|
64
|
-
```
|
dotask-0.3.0/dotask/__init__.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: dotask
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: 发布订阅注解方式
|
|
5
|
-
Home-page: https://gitee.com/d-yz/task-manager
|
|
6
|
-
Author: dyz
|
|
7
|
-
Author-email: 837701454@qq.com
|
|
8
|
-
Keywords: 发布订阅,注解,dotask
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Description-Content-Type: text/markdown
|
|
17
|
-
License-File: LICENSE
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
|
|
20
|
-
## 版本说明
|
|
21
|
-
### 0.1.0
|
|
22
|
-
- 在方法上添加@task就可以直接开启一个线程执行任务
|
|
23
|
-
- 常用参数:
|
|
24
|
-
- 生产者: role="producer",topic="自定义"
|
|
25
|
-
- 消费者: role="consumer",subscribe="自定义",publish_after="自定义"
|
|
26
|
-
- 说明:
|
|
27
|
-
- role: 用于区分上下游 producer/consumer
|
|
28
|
-
- topic: 发布的主题
|
|
29
|
-
- subscribe: 对哪些主题感兴趣
|
|
30
|
-
- publish_after: 消费完后继续发布新主题
|
|
31
|
-
- 特别的:方法的返回值可以进行传递,若没有返回值则不会发布新主题(只针对消费者和消费者之间)
|
|
32
|
-
### 0.2.x
|
|
33
|
-
1. dotask暴露topic_manager=>可以更好的管理生产消费者
|
|
34
|
-
2. 迁移logger至util包下
|
|
35
|
-
3. 添加本地调用shell工具函数->可以很好的适配@task消费者
|
|
36
|
-
1. max_concurrent:控制并发执行的shell数量
|
|
37
|
-
2. 引入dotask.Shell 调用Shell(max_concurrent=?).local_shell_execute(cmd,callback)来使用
|
|
38
|
-
### 0.3.x
|
|
39
|
-
1. 添加令牌桶,通过`from dotask.util import TokenBucket`使用,可以更好的限制生产消费速率
|
|
40
|
-
- 参数说明
|
|
41
|
-
- TokenBucket(capacity=1, rate=0),capacity:令牌桶容量,rate:每秒生成速率
|
|
42
|
-
- get_token(block=True),block:决定令牌桶无令牌时是阻塞还是返回false
|
|
43
|
-
## 快速开始
|
|
44
|
-
### 安装
|
|
45
|
-
`pip install dotask`
|
|
46
|
-
### 示例
|
|
47
|
-
1. 简单的生产发布模式
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
from dotask import task
|
|
51
|
-
from dotask.util import logger
|
|
52
|
-
import random
|
|
53
|
-
|
|
54
|
-
if __name__ == '__main__':
|
|
55
|
-
|
|
56
|
-
@task(role="producer",topic="scan")
|
|
57
|
-
def a():
|
|
58
|
-
return random.randint(1,10)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@task(role="consumer",subscribe="scan",publish_after="sayHi")
|
|
62
|
-
def b(data):
|
|
63
|
-
if data >5:
|
|
64
|
-
return data
|
|
65
|
-
logger.warning(f"本次生成数字:{data},不会继续发布sayHi主题")
|
|
66
|
-
|
|
67
|
-
@task(role="consumer",subscribe="sayHi")
|
|
68
|
-
def c(data):
|
|
69
|
-
logger.debug(f"c触发")
|
|
70
|
-
logger.info(f"消费:{data}")
|
|
71
|
-
//=====================================================================
|
|
72
|
-
2026-02-08 17:30:37 - INFO - 发布主题-[scan]:5
|
|
73
|
-
2026-02-08 17:30:37 - WARNING - 本次生成数字:5,不会继续发布sayHi主题
|
|
74
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:8
|
|
75
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:8
|
|
76
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
77
|
-
2026-02-08 17:30:38 - INFO - 消费:8
|
|
78
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[scan]:6
|
|
79
|
-
2026-02-08 17:30:38 - INFO - 发布主题-[sayHi]:6
|
|
80
|
-
2026-02-08 17:30:38 - DEBUG - c触发
|
|
81
|
-
2026-02-08 17:30:38 - INFO - 消费:6
|
|
82
|
-
//======================================================================
|
|
83
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|