0%

pycallgraph2是一个Python模块,用于为Python应用程序创建可视化的调用关系图。

pycallgraph2 · PyPI

安装pycallgraph2

1
pip install pycallgraph2

安装graphviz

Linux

1
sudo apt install graphviz

Windows

1
https://graphviz.gitlab.io/_pages/Download/windows/graphviz-2.38.msi

通过

1
dot -V

可以确认安装成功

直接在命令行运行pycallgraph

1
usage: pycallgraph [options] OUTPUT_TYPE [output_options] -- SCRIPT.py [ARG ...]

例如

1
pycallgraph graphviz --output-file=my_callgraph.png -- ./test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

import redis
from dataclasses import dataclass

@dataclass
class Message:
key: str = ""
value: bytes = b""

class RedisMQProducer:
def __init__(self, *args, **kw):
self.client = redis.Redis(*args, **kw)

def send(self, topic: str, key: str, value: bytes):
redis_key = f"MQ_{topic}"
if len(key) >= 32:
raise Exception("key超长")
message = key.encode() + (b'\0' * (32 - len(key))) + value
self.client.lpush(redis_key, message)


def create_redis_mq_producer(server):
if server.find(':') != -1:
host, port = server.split(":", 1)
else:
host = server
port = 6379
return RedisMQProducer(host=host, port=port, db=0)


class RedisMQConsumer:
def __init__(self, *args, **kw):
self.client = redis.Redis(*args, **kw)
self.topics = []


def subscribe(self, topics):
self.topics = topics

def __iter__(self):
self.idx = 0
return self

# 阻塞地获取下一个新消息
def __next__(self):
if not self.topics:
raise StopIteration
while True:
topic = self.topics[self.idx]
self.idx = (self.idx + 1) % len(self.topics)
redis_key = f"MQ_{topic}"
data = self.client.rpop(redis_key)
if data:
return Message(key=data[:32].decode().strip('\0'), value=data[32:])

# 获取新消息并返回,直到所有订阅的topic中都没有新消息或者获取到了数量max_records个消息
def get_messages(self, max_records=20):
ret = []
topics = self.topics
while topics:
topics_bak = topics
topics = []
for topic in topics_bak:
redis_key = f"MQ_{topic}"
data = self.client.rpop(redis_key)
if data:
try:
ret.append(Message(key=data[:32].decode().strip('\0'), value=data[32:]))
except Exception as e:
pass
topics.append(topic)
if len(ret) >= max_records:
return ret
return ret


def create_redis_mq_consumer(server):
if server.find(':') != -1:
host, port = server.split(":", 1)
else:
host = server
port = 6379
return RedisMQConsumer(host=host, port=port, db=0)

对kafka-python进行封装,增加了去重,因为默认情况下Kafka只保证不丢失数据,不保证极端情况不重复,这里增加去重处理后可保证不重不漏,还实现了从指定时间还是订阅的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

import kafka # pip install kafka-python
import time
import uuid
import datetime
import queue
from dataclasses import dataclass

@dataclass
class Message:
key: str = ""
value: bytes = b""

class KafkaProducer(kafka.KafkaProducer):
def __init__(self, *args, **kw):
super().__init__(*args, **kw)

def send(self, topic: str, key: str, value: bytes):
# 生成一个长32位的id用于去重,同时顺便记录消息生成时间
# 默认情况下Kafka保证消息不丢失,但并不能保证不重复,所以我们通过id来自行去重
id = "{}{}".format(datetime.datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3], uuid.uuid4().hex[:15])
super().send(topic, key=key.encode(), value=id.encode()+value)


def create_kafka_producer(server):
retry_times = 3
for i in range(1, retry_times+1):
try:
producer = KafkaProducer(
bootstrap_servers=[server],
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)
return producer
except Exception as e:
if i == retry_times:
raise e
else:
time.sleep(0.1)


class KafkaConsumer(kafka.KafkaConsumer):
def __init__(self, **kw):
super().__init__(**kw)
self.q = queue.Queue()
self.s = set()
self.DUP_DETECT_SIZE = 1000 # 去重检测大小

# 从最新消息开始订阅(subscribe在父类有实现)
# def subscribe(self, topic):
# pass

# 实现从一个历史时间点进行消息订阅(能订阅到的消息取决于Kafka服务器配置的保留策略,基于目前的配置可以保证72小时内的消息可以重复消费)
def subscribe_from_datetime(self, partition, topic, dt):
if type(dt) is int or type(dt) is float:
ts = dt
elif isinstance(dt, datetime.datetime):
ts = dt.timestamp()
else:
ts = time.mktime(time.strptime(f"{dt}", r"%Y-%m-%d %H:%M:%S"))
offset = self._get_offset_for_time(partition, topic, ts)
tp = kafka.TopicPartition(topic, partition)
super().assign([tp])
super().seek(tp, offset)

def __iter__(self):
return self

# 重新封装阻塞取消息方式,增加去重
def __next__(self):
while True:
message = super().__next__()
key = message.key
if not key:
continue
id = message.value[:32]
if id in self.s: # 重复的消息
continue
if len(self.s) >= self.DUP_DETECT_SIZE:
e = self.q.get()
self.s.remove(e)
self.s.add(id)
self.q.put(id)

return Message(key=key.decode(), value=message.value[32:])

# 重新封装非阻塞取消息方式,增加去重
def get_messages(self, max_records=20):
r = super().poll(timeout_ms=max_records*25, max_records=max_records)
ret = []
for messages in r.values():
for message in messages:
key = message.key
if not key:
continue
id = message.value[:32]
if id in self.s: # 重复的消息
continue
if len(self.s) >= self.DUP_DETECT_SIZE:
e = self.q.get()
self.s.remove(e)
self.s.add(uuid)
self.q.put(uuid)

ret.append(Message(key=key.decode(), value=message.value[32:]))
return ret

def _get_latest_offset(self, partition, topic):
tp = kafka.TopicPartition(topic, partition)
super(KafkaConsumer, self).assign([tp])
off_set_dict = super(KafkaConsumer, self).end_offsets([tp])
return list(off_set_dict.values())[0]

def _get_offset_for_time(self, partition, topic, ts):
tp = kafka.TopicPartition(topic, partition)
super(KafkaConsumer, self).assign([tp])
offset_dict = super(KafkaConsumer, self).offsets_for_times({tp: int(ts*1000)})
offset = list(offset_dict.values())[0]
if offset is None:
return self._get_latest_offset(partition, topic)
else:
return offset.offset


def create_kafka_consumer(server, group_id):
retry_times = 3
for i in range(1, retry_times+1):
try:
consumer = KafkaConsumer(
bootstrap_servers=[server],
group_id=group_id,
auto_offset_reset='latest', # earliest, latest
enable_auto_commit=True,
api_version=(2,3,1),
api_version_auto_timeout_ms=5000
)
return consumer
except Exception as e:
if i == retry_times:
raise e
else:
time.sleep(0.1)


mount命令的执行需要sudo权限,其实有一些命令允许普通用户进行挂载操作

fusermount 用户空间挂载NFS共享

这个玩意我就没试成功过

smbnetfs 用户空间挂载samba共享

smbnetfs <挂载点>
ls <挂载点>/<用户名>:<密码>@<hostIP>/<samba共享路径>

sshfs 用户空间基于SSH挂载共享

卸载可以统一使用 fusermount -u
强制(延迟)卸载使用 fusermount -z

备份压缩

tar_backup.sh 将指定目录压缩备份到指定目录下并自动轮转保留最近5次备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash

if [ "$#" -lt 2 ]; then
script_name=$(basename "$0")
echo "Usage: ${script_name} <需要备份的路径> <备份到的路径> [--exclude <需要排除的路径> ...]"
exit 1
fi

src_path=$1
dst_path=$2
shift 2
tar_params=$@

if [ ! -d "${dst_path}" ]; then
mkdir -p ${dst_path}
fi

for file in $(find ${dst_path} -name "backup-*" -type f | sort -r | tail -n +5 );
do
rm -f $file;
done;

if [ "${dst_path: -1}" != "/" ]; then
dst_path="${dst_path}/"
fi

tar cJf ${dst_path}/backup-$(date "+%Y%m%d-%H%M%S-%N").tar.xz ${tar_params} ${src_path} 2>&-

解压还原

解压到当前路径

1
tar -xvf backup-20240617-235910-016764400.tar.xz

解压到指定路径

1
tar -xvf backup-20240617-235910-016764400.tar.xz -C /path/to/destination

对于使用绝对路径的备份,解压到原路径

1
tar -xvf backup-20240617-235910-016764400.tar.xz -C /

有时候我们希望函数调用的缓存能够夸进程使用或者在程序重启后仍然有效。

一个显然的想法是缓存到redis,于是就有了下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118

import os
import functools
import redis
from redis.connection import ConnectionPool
import msgpack
import msgpack_numpy
import numpy as np
import pandas as pd
import io

class _HashedSeq(list):
__slots__ = 'hashvalue'

def __init__(self, tup, hash=hash):
self[:] = tup
self.hashvalue = hash(tup)

def __hash__(self):
return self.hashvalue


def _make_key(args, kw, typed=False):
key = args
if kw:
kwd_mark = (object(),)
key += kwd_mark
for item in kw.items():
key += item
if typed:
key += tuple(type(v) for v in args)
if kw:
key += tuple(type(v) for v in kw.values())
elif len(key) == 1 and type(key[0]) in {int, str}:
return key[0]
return _HashedSeq(key)


def dumps_to_feather(df):
columns = df.columns
df.columns = [str(e) for e in df.columns]
buffer = io.BytesIO() # 创建一个内存中的字节流缓冲区
df.to_feather(buffer) # 将 DataFrame 序列化为 Feather 格式并存储到缓冲区
buffer.seek(0) # 重新定位到字节流的开头
serialized_data = buffer.getvalue() # 获取序列化后的二进制数据
return {"data": serialized_data, "columns": list(columns)}


def loads_from_feather(data):
buffer = io.BytesIO(data["data"]) # 创建一个内存中的字节流缓冲区,并加载序列化的数据
df = pd.read_feather(buffer) # 从 Feather 格式的序列化数据中加载 DataFrame
df.columns = data["columns"]
return df


def _custom_encode(obj):
if isinstance(obj, set):
return {b'__set__': list(obj)} # 将 set 对象转换为字典形式
if isinstance(obj, pd.DataFrame):
return {b'__feather__': dumps_to_feather(obj)}
return msgpack_numpy.encode(obj)


def _custom_decode(obj):
if b'__set__' in obj: # 判断是否是我们之前转换过的 set 对象
return set(obj[b'__set__']) # 将字典形式的 set 转换回原始的 set 对象
if b'__feather__' in obj:
return loads_from_feather(obj[b'__feather__'])
return msgpack_numpy.decode(obj) # 不需要转换的情况下直接返回原始对象


class RedisTTLCache:
def __init__(self, host='localhost', port=6379):
self.pool = ConnectionPool(host=host, port=port)

def cache(self, ttl, scope=os.path.basename(__file__)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 构建唯一的缓存键
cache_key = f"FUNC_CACHE_{(scope, func.__name__, _make_key(args, kwargs))}"

# 尝试从Redis中获取缓存
redis_client = redis.Redis(connection_pool=self.pool)
result = redis_client.get(cache_key)
if result is not None:
# 如果缓存存在,直接返回结果
result = msgpack.loads(result, object_hook=_custom_decode)
return result

# 如果缓存中不存在该结果,则重新计算函数结果
result = func(*args, **kwargs)

# 将计算结果存入Redis缓存
redis_client.set(cache_key, msgpack.dumps(result, default=_custom_encode), ex=ttl)

return result
return wrapper
return decorator


if __name__ == '__main__':
redis_ttl_cache = RedisTTLCache(host="localhost")
ttl_cached = redis_ttl_cache.cache

@ttl_cached(ttl=5)
def func(a, b):
print("Hello world")
return {
'result': a + b,
'colors': {'red', 'green', 'blue'},
'arr': np.array([[0, 1], [2,3]]),
'df': pd.DataFrame([[0, 1], [2,3]]),
}

print(func(1, 2))
print(func(1, 2))

这里的主要问题是使用什么作为redis的key来确保真正相同的调用使用到相同的缓存。

如果我们在key中拼入进程ID,那么则是非常保守的,不同进程将无法使用到相同的缓存。

我这里默认使用 脚本文件名+函数名+参数 作为key,但是或许有两个相同脚本名的脚本中定义了同名的但功能不同的函数,这时候不应该使用相同的缓存。

总之我们只能折中处理,因为我的目的就是跨进程使用缓存,所以没有特别保守,同时提供了scope参数,可以自定义一个拼入key的标识,在必要时由用户决定能否使用相同的缓存。

有时我们希望使用一些对数据大小或类型有限制的数据结构,例如树状数组(要求整型并且因为内存的关系最大数据范围通常为1e6量级)。

但是如果我们的数据个数在1e6之内,并且我们只关心数据的序,我们就可以在非负整数和我们的数据集之间建立映射来使用树状数组(或其他有限制的数据结构)。

那么这里我们就期望一个帮助建立映射的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

// 返回一个整型数组包含 [0, arr.size()) 的数字,且和arr具有一致的序
template<typename T, typename CMP=less<T>>
vector<int> get_rank(const vector<T>& arr, CMP cmp=CMP()){
vector<int> idx(arr.size());
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](auto i, auto j){
return cmp(arr[i], arr[j]);
});
// 我们在不改变arr的情况下对其下标数组进行排序
// 排序好后,对于任意i < j有arr[idx[i]] <= arr[idx[j]] (1)
// 如果我们构造一个rank,令rank[idx[i]] = i, 0<=i<n,
// 那么显然对于任意i < j有rank[idx[i]]=i < j=rank[idx[j]]
// 可以看到如果忽略(1)式中的等号,rank就已经和arr具有一致的序
// 所以下面我们就按这个思路,再顺便处理一下并列的情况就行了
vector<int> rank(arr.size());
rank[idx[0]] = 0;
for(int i=1; i<arr.size(); i++){
if(arr[idx[i]] == arr[idx[i-1]]){
rank[idx[i]] = rank[idx[i-1]];
} else {
rank[idx[i]] = i;
}
}
return rank;
}

上面的得到rank实际上是 arr -> 同序非负整数集 的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# -*- coding: utf-8 -*-
import os
import openpyxl # 解析xlsx
import xlrd # 解析xls
import csv # 解析csv


"""
类功能概述:
本代码实现了一个支持读取多种格式(xlsx、xls、csv)Excel 文件的功能。
主要包含 `Workbook` 和 `Sheet` 相关类,提供了统一的接口来操作不同格式文件。

使用步骤:
1. 创建 `Workbook` 对象,传入文件路径,根据文件后缀自动选择合适的解析器。
2. 通过 `Workbook` 对象的 `sheet_by_index` 方法获取指定索引的工作表(`Sheet` 对象)。
3. 使用 `Sheet` 对象的 `cell_value` 方法获取指定单元格的值,`max_row` 方法获取最大行数,`max_col` 方法获取最大列数。
"""


class Sheet:
"""
`Sheet` 类是所有工作表类的基类,定义了获取单元格值、最大行数和最大列数的接口。

方法:
cell_value(row, col): 获取指定行和列的单元格值。
max_row(): 获取工作表的最大行数。
max_col(): 获取工作表的最大列数。
"""

def cell_value(self, row, col):
pass

def max_row(self):
pass

def max_col(self):
pass


class XlsxSheet(Sheet):
def __init__(self, sheet):
self.sheet = sheet

def cell_value(self, row, col):
return self.sheet.cell(row=row+1, column=col+1).value

def max_row(self):
return self.sheet.max_row

def max_col(self):
return self.sheet.max_column


class XlsSheet(Sheet):
def __init__(self, sheet):
self.sheet = sheet

def cell_value(self, row, col):
return self.sheet.cell_value(row, col)

def max_row(self):
return self.sheet.nrows

def max_col(self):
return self.sheet.ncols


class CsvSheet(Sheet):
def __init__(self, rows):
self.rows = rows

def cell_value(self, row, col):
if row < len(self.rows) and col < len(self.rows[row]):
return self.rows[row][col]
return None

def max_row(self):
return len(self.rows)

def max_col(self):
max_cols = 0
for row in self.rows:
max_cols = max(max_cols, len(row))
return max_cols


class _Workbook:
def sheet_by_index(self, index):
pass


class XlsxFile(_Workbook):
def __init__(self, path):
self.workbook = openpyxl.load_workbook(path)

def sheet_by_index(self, index):
sheet = self.workbook[self.workbook.sheetnames[index]]
return XlsxSheet(sheet)


class XlsFile(_Workbook):
def __init__(self, path):
self.workbook = xlrd.open_workbook(path)

def sheet_by_index(self, index):
sheet = self.workbook.sheet_by_index(index)
return XlsSheet(sheet)


class CsvFile(_Workbook):
def __init__(self, path):
encodings = ['utf-8-sig', 'utf-8']
for encoding in encodings:
try:
with open(path, 'r', encoding=encoding) as file:
reader = csv.reader(file)
self.rows = list(reader)
break
except UnicodeDecodeError:
continue
else:
raise UnicodeDecodeError("无法使用支持的编码读取文件")

def sheet_by_index(self, index):
if index == 0:
return CsvSheet(self.rows)
return None


class Workbook:
"""
`Workbook` 类是对外提供的工作簿类,根据文件后缀自动选择合适的解析器。

属性:
impl: 具体的工作簿实现对象(`XlsxFile`、`XlsFile` 或 `CsvFile`)。

方法:
__init__(path): 初始化工作簿对象,根据文件后缀选择合适的解析器。
sheet_by_index(index): 获取指定索引的工作表。
"""

def __init__(self, path):
filedir, filename = os.path.split(path)
_, extname = os.path.splitext(filename)
if extname == ".xlsx":
self.impl = XlsxFile(path)
elif extname == ".xls":
self.impl = XlsFile(path)
elif extname == ".csv":
self.impl = CsvFile(path)
else:
raise IOError("未知的文件类型")

def sheet_by_index(self, index):
return self.impl.sheet_by_index(index)


让Sublime Text使用WSL中的Python执行.py脚本

找到C:\Program Files\Sublime Text\Packages\Python.sublime-package

copy一份将增加后缀.zip用解压软件打开。

我们增加一个文件来定义新的构建方式,新建文件WSL Python.sublime-build,输入内容

1
2
3
4
5
6
{
"cmd":["python.bat","${file}"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"path":"C:/Program Files/Sublime Text/helper",
"selector": "source.python",
}

大概意思是由C:/Program Files/Sublime Text/helper/python.bat来执行通过source.python选出的文件,选出的理解为所有*.py即可。

新建文件C:/Program Files/Sublime Text/helper/python.bat,输入内容

1
2
3
4
5
6
@echo off
set str=%1
set str=%str:\=/%
set str=%str:C:=/mnt/c%
set str=%str:D:=/mnt/d%
C:\windows\system32\wsl.exe python -u %str%

重启Sublime Text,构建*.py时选择WSL Python即可。

让Sublime Text使用WSL中的g++编译运行.cpp文件

找到C:\Program Files\Sublime Text\Packages\C++.sublime-package

copy一份将增加后缀.zip用解压软件打开。

我们增加一个文件来定义新的构建方式,新建文件WSL C++ Single File.sublime-build,输入内容

1
2
3
4
5
6
{
"cmd":["cpp.bat","${file}", "${file_path}", "${file_base_name}"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"path":"C:/Program Files/Sublime Text/helper",
"selector": "source.c++",
}

大概意思是由C:/Program Files/Sublime Text/helper/cpp.bat来执行通过source.c++选出的文件,选出的理解为所有*.cpp即可。

新建文件C:/Program Files/Sublime Text/helper/cpp.bat,输入内容

1
2
3
4
5
6
7
8
9
10
11
12
@echo off
set file=%1
set file_path=%2
set file_base_name=%3
set file=%file:\=/%
set file=%file:C:=/mnt/c%
set file=%file:D:=/mnt/d%
set file_path=%file_path:\=/%
set file_path=%file_path:C:=/mnt/c%
set file_path=%file_path:D:=/mnt/d%

C:\windows\system32\wsl.exe g++ -std=c++17 %file% -o %file_path%/%file_base_name% && C:\windows\system32\wsl.exe %file_path%/%file_base_name%

重启Sublime Text,构建*.cpp时选择WSL C++ Single File即可。

下面的代码实现了3个有用的东西:

  • KillableThread 一个可kill的线程,并且可以通过join返回线程方法的数据,以及在join时把线程内的异常重新抛出到调用join的线程。
  • timeout装饰器 超时自动抛出TimeoutError异常。
  • retry装饰器 提供异常时自动重试,可以指定重试次数,0表示不重试,重试次数超过后会把最后一次的异常向外抛出。

func_utils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# -*- coding: utf-8 -*-
import threading
import time
import inspect
import ctypes
import traceback
import sys, os
from functools import wraps

class KillableThread(threading.Thread):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._return_value = None
self._exception = None

def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")

def kill(self):
KillableThread._async_raise(self.ident, SystemExit)

def run(self):
try:
self._return_value = self._target(*self._args, **self._kwargs)
except Exception as e:
self._exception = e

def join(self):
super().join()
if self._exception is not None:
raise self._exception
return self._return_value

def join(self, timeout):
super().join(timeout)
if self._exception is not None:
raise self._exception
return self._return_value


def _get_thread(tid):
for t in threading.enumerate():
if t.ident == tid:
return t
return None


def _get_frame_stack(tid):
for thread_id, stack in sys._current_frames().items():
if thread_id == tid:
return stack
return None


def _get_formated_frame_stack(tid):
info = []
th = _get_thread(tid)
stack = _get_frame_stack(tid)
info.append('%s thread_id=%d' % (th.name, tid))
for filename, lineno, _, line in traceback.extract_stack(stack):
info.append(' at %s(%s:%d)' % (line, filename[filename.rfind(os.path.sep) + 1:], lineno))
return '\n'.join(info)


def timeout(seconds):
"""
Decorator to execute a function with a specified timeout.

Args:
- seconds (int): The time limit in seconds for the function to complete.

Returns:
- function: The decorated function.

Raises:
- TimeoutError: If the function does not complete within the specified time limit.

Usage:
@timeout(seconds=10)
def my_function():
# Function body
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
th = KillableThread(target=func, args=args, kwargs=kwargs)
th.daemon = True
th.start()
ret = th.join(seconds)
if th.is_alive():
formated_frame_stack = _get_formated_frame_stack(th.ident)
th.kill()
raise TimeoutError(f"{repr(func)} timeout. Frame stack:\n{formated_frame_stack}")
return ret
return wrapper
return decorator


def retry(retries=1, retry_interval=0):
"""
Decorator to retry a function a specified number of times with a given interval between retries.

Args:
- retries (int): The number of times the function should be retried if it raises an exception. If set to 1, the function will be attempted initially and retried once.
- retry_interval (int): The time interval in seconds to wait between retries.

Returns:
- function: The decorated function.

Raises:
- The original exception raised by the function if all retries fail.

Usage:
@retry(retries=2, retry_interval=2)
def my_function():
# Function body
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(retries+1):
try:
return func(*args, **kwargs)
except Exception as e:
if i < retries:
time.sleep(retry_interval)
else:
raise e
return wrapper
return decorator