hutool-python 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.
- hutool/__init__.py +174 -0
- hutool/cache/__init__.py +7 -0
- hutool/cache/cache_util.py +47 -0
- hutool/cache/fifo_cache.py +87 -0
- hutool/cache/lfu_cache.py +129 -0
- hutool/cache/lru_cache.py +93 -0
- hutool/cache/timed_cache.py +115 -0
- hutool/captcha/__init__.py +3 -0
- hutool/captcha/captcha_util.py +215 -0
- hutool/core/__init__.py +23 -0
- hutool/core/_base.py +61 -0
- hutool/core/bean.py +214 -0
- hutool/core/codec.py +111 -0
- hutool/core/coll.py +635 -0
- hutool/core/date.py +1024 -0
- hutool/core/exceptions.py +66 -0
- hutool/core/io/__init__.py +0 -0
- hutool/core/io/data_size_util.py +79 -0
- hutool/core/io/file_name_util.py +111 -0
- hutool/core/io/file_util.py +650 -0
- hutool/core/io/io_util.py +133 -0
- hutool/core/io/path_util.py +247 -0
- hutool/core/io/resource_util.py +137 -0
- hutool/core/map.py +933 -0
- hutool/core/math_util.py +105 -0
- hutool/core/net.py +288 -0
- hutool/core/text/__init__.py +0 -0
- hutool/core/text/csv_util.py +54 -0
- hutool/core/text/str_builder.py +224 -0
- hutool/core/text/unicode_util.py +58 -0
- hutool/core/tree.py +242 -0
- hutool/core/util/__init__.py +63 -0
- hutool/core/util/array_util.py +503 -0
- hutool/core/util/boolean_util.py +124 -0
- hutool/core/util/charset_util.py +60 -0
- hutool/core/util/class_util.py +136 -0
- hutool/core/util/coordinate_util.py +186 -0
- hutool/core/util/credit_code_util.py +110 -0
- hutool/core/util/desensitized_util.py +194 -0
- hutool/core/util/enum_util.py +94 -0
- hutool/core/util/escape_util.py +97 -0
- hutool/core/util/hash_util.py +243 -0
- hutool/core/util/hex_util.py +140 -0
- hutool/core/util/id_util.py +147 -0
- hutool/core/util/idcard_util.py +300 -0
- hutool/core/util/number_util.py +720 -0
- hutool/core/util/object_util.py +294 -0
- hutool/core/util/page_util.py +61 -0
- hutool/core/util/phone_util.py +140 -0
- hutool/core/util/random_util.py +112 -0
- hutool/core/util/re_util.py +231 -0
- hutool/core/util/reflect_util.py +135 -0
- hutool/core/util/runtime_util.py +89 -0
- hutool/core/util/str_util.py +2320 -0
- hutool/core/util/system_util.py +62 -0
- hutool/core/util/url_util.py +232 -0
- hutool/core/util/version_util.py +41 -0
- hutool/core/util/xml_util.py +158 -0
- hutool/core/util/zip_util.py +126 -0
- hutool/cron/__init__.py +4 -0
- hutool/cron/cron_pattern.py +123 -0
- hutool/cron/cron_util.py +115 -0
- hutool/crypto/__init__.py +5 -0
- hutool/crypto/digest_util.py +167 -0
- hutool/crypto/secure_util.py +311 -0
- hutool/crypto/sign_util.py +74 -0
- hutool/dfa/__init__.py +3 -0
- hutool/dfa/sensitive_util.py +114 -0
- hutool/extra/__init__.py +6 -0
- hutool/extra/emoji_util.py +90 -0
- hutool/extra/pinyin_util.py +44 -0
- hutool/extra/qr_code_util.py +58 -0
- hutool/extra/template_util.py +41 -0
- hutool/http/__init__.py +6 -0
- hutool/http/html_util.py +88 -0
- hutool/http/http_request.py +188 -0
- hutool/http/http_response.py +139 -0
- hutool/http/http_util.py +237 -0
- hutool/json_util.py +251 -0
- hutool/jwt_util.py +57 -0
- hutool/setting/__init__.py +5 -0
- hutool/setting/props_util.py +79 -0
- hutool/setting/setting_util.py +80 -0
- hutool/setting/yaml_util.py +45 -0
- hutool_python-1.0.0.dist-info/LICENSE +127 -0
- hutool_python-1.0.0.dist-info/METADATA +438 -0
- hutool_python-1.0.0.dist-info/RECORD +89 -0
- hutool_python-1.0.0.dist-info/WHEEL +5 -0
- hutool_python-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LineCaptcha:
|
|
9
|
+
"""线段干扰验证码。
|
|
10
|
+
|
|
11
|
+
在图片上绘制随机字符,并添加随机干扰线段以增加识别难度。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, width: int = 100, height: int = 35, code_count: int = 4, line_count: int = 5) -> None:
|
|
15
|
+
"""初始化线段干扰验证码。
|
|
16
|
+
|
|
17
|
+
:param width: 图片宽度,默认100像素
|
|
18
|
+
:param height: 图片高度,默认35像素
|
|
19
|
+
:param code_count: 验证码字符个数,默认4
|
|
20
|
+
:param line_count: 干扰线条数,默认5
|
|
21
|
+
"""
|
|
22
|
+
self._width: int = width
|
|
23
|
+
self._height: int = height
|
|
24
|
+
self._code_count: int = code_count
|
|
25
|
+
self._line_count: int = line_count
|
|
26
|
+
self._code: str = ""
|
|
27
|
+
|
|
28
|
+
def create_code(self) -> str:
|
|
29
|
+
"""生成随机验证码字符串并绘制验证码图片。
|
|
30
|
+
|
|
31
|
+
:return: 生成的验证码字符串
|
|
32
|
+
"""
|
|
33
|
+
chars = string.ascii_letters + string.digits
|
|
34
|
+
self._code = "".join(random.choices(chars, k=self._code_count))
|
|
35
|
+
return self._code
|
|
36
|
+
|
|
37
|
+
def verify(self, code: str) -> bool:
|
|
38
|
+
"""校验输入的验证码是否正确(不区分大小写)。
|
|
39
|
+
|
|
40
|
+
:param code: 用户输入的验证码
|
|
41
|
+
:return: 验证成功返回True,否则返回False
|
|
42
|
+
"""
|
|
43
|
+
if not self._code or not code:
|
|
44
|
+
return False
|
|
45
|
+
return self._code.lower() == code.lower()
|
|
46
|
+
|
|
47
|
+
def get_image_bytes(self, fmt: str = "png") -> bytes:
|
|
48
|
+
"""生成验证码图片的字节数据。
|
|
49
|
+
|
|
50
|
+
:param fmt: 图片格式,默认"png"
|
|
51
|
+
:return: 图片的字节数据
|
|
52
|
+
"""
|
|
53
|
+
if not self._code:
|
|
54
|
+
self.create_code()
|
|
55
|
+
|
|
56
|
+
img = Image.new("RGB", (self._width, self._height), (255, 255, 255))
|
|
57
|
+
draw = ImageDraw.Draw(img)
|
|
58
|
+
|
|
59
|
+
# 尝试加载字体,失败则使用默认字体
|
|
60
|
+
try:
|
|
61
|
+
font = ImageFont.truetype("arial.ttf", size=int(self._height * 0.6))
|
|
62
|
+
except OSError:
|
|
63
|
+
font = ImageFont.load_default()
|
|
64
|
+
|
|
65
|
+
# 绘制验证码字符
|
|
66
|
+
char_width = self._width // (self._code_count + 1)
|
|
67
|
+
for i, ch in enumerate(self._code):
|
|
68
|
+
x = char_width * i + random.randint(2, 8)
|
|
69
|
+
y = random.randint(0, max(1, self._height // 4))
|
|
70
|
+
color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150))
|
|
71
|
+
draw.text((x, y), ch, fill=color, font=font)
|
|
72
|
+
|
|
73
|
+
# 绘制干扰线
|
|
74
|
+
for _ in range(self._line_count):
|
|
75
|
+
x1 = random.randint(0, self._width)
|
|
76
|
+
y1 = random.randint(0, self._height)
|
|
77
|
+
x2 = random.randint(0, self._width)
|
|
78
|
+
y2 = random.randint(0, self._height)
|
|
79
|
+
color = (random.randint(100, 200), random.randint(100, 200), random.randint(100, 200))
|
|
80
|
+
draw.line([(x1, y1), (x2, y2)], fill=color, width=1)
|
|
81
|
+
|
|
82
|
+
buf = io.BytesIO()
|
|
83
|
+
img.save(buf, format=fmt)
|
|
84
|
+
return buf.getvalue()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ArithmeticCaptcha:
|
|
88
|
+
"""算术验证码。
|
|
89
|
+
|
|
90
|
+
生成简单的数学表达式(如"3+5=?"),用户需计算结果。
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, width: int = 100, height: int = 35) -> None:
|
|
94
|
+
"""初始化算术验证码。
|
|
95
|
+
|
|
96
|
+
:param width: 图片宽度,默认100像素
|
|
97
|
+
:param height: 图片高度,默认35像素
|
|
98
|
+
"""
|
|
99
|
+
self._width: int = width
|
|
100
|
+
self._height: int = height
|
|
101
|
+
self._expression: str = ""
|
|
102
|
+
self._result: str = ""
|
|
103
|
+
|
|
104
|
+
def create_code(self) -> str:
|
|
105
|
+
"""生成随机算术表达式,并计算结果。
|
|
106
|
+
|
|
107
|
+
:return: 生成的算术表达式(如"3+5=?")
|
|
108
|
+
"""
|
|
109
|
+
a = random.randint(1, 20)
|
|
110
|
+
b = random.randint(1, 20)
|
|
111
|
+
op = random.choice(["+", "-", "*"])
|
|
112
|
+
|
|
113
|
+
if op == "+":
|
|
114
|
+
result = a + b
|
|
115
|
+
elif op == "-":
|
|
116
|
+
# 确保结果非负
|
|
117
|
+
if a < b:
|
|
118
|
+
a, b = b, a
|
|
119
|
+
result = a - b
|
|
120
|
+
else:
|
|
121
|
+
result = a - b
|
|
122
|
+
else:
|
|
123
|
+
result = a * b
|
|
124
|
+
|
|
125
|
+
self._expression = f"{a}{op}{b}=?"
|
|
126
|
+
self._result = str(result)
|
|
127
|
+
return self._expression
|
|
128
|
+
|
|
129
|
+
def get_result(self) -> str:
|
|
130
|
+
"""获取算术表达式的正确结果。
|
|
131
|
+
|
|
132
|
+
:return: 表达式结果字符串
|
|
133
|
+
"""
|
|
134
|
+
return self._result
|
|
135
|
+
|
|
136
|
+
def verify(self, code: str) -> bool:
|
|
137
|
+
"""校验用户输入的答案是否正确。
|
|
138
|
+
|
|
139
|
+
:param code: 用户输入的答案
|
|
140
|
+
:return: 答案正确返回True,否则返回False
|
|
141
|
+
"""
|
|
142
|
+
if not self._result or not code:
|
|
143
|
+
return False
|
|
144
|
+
return self._result == code.strip()
|
|
145
|
+
|
|
146
|
+
def get_image_bytes(self, fmt: str = "png") -> bytes:
|
|
147
|
+
"""生成验证码图片的字节数据。
|
|
148
|
+
|
|
149
|
+
:param fmt: 图片格式,默认"png"
|
|
150
|
+
:return: 图片的字节数据
|
|
151
|
+
"""
|
|
152
|
+
if not self._expression:
|
|
153
|
+
self.create_code()
|
|
154
|
+
|
|
155
|
+
img = Image.new("RGB", (self._width, self._height), (255, 255, 255))
|
|
156
|
+
draw = ImageDraw.Draw(img)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
font = ImageFont.truetype("arial.ttf", size=int(self._height * 0.5))
|
|
160
|
+
except OSError:
|
|
161
|
+
font = ImageFont.load_default()
|
|
162
|
+
|
|
163
|
+
# 计算文字居中位置
|
|
164
|
+
bbox = draw.textbbox((0, 0), self._expression, font=font)
|
|
165
|
+
text_width = bbox[2] - bbox[0]
|
|
166
|
+
text_height = bbox[3] - bbox[1]
|
|
167
|
+
x = (self._width - text_width) // 2
|
|
168
|
+
y = (self._height - text_height) // 2
|
|
169
|
+
|
|
170
|
+
color = (random.randint(0, 100), random.randint(0, 100), random.randint(0, 100))
|
|
171
|
+
draw.text((x, y), self._expression, fill=color, font=font)
|
|
172
|
+
|
|
173
|
+
# 绘制干扰线
|
|
174
|
+
for _ in range(3):
|
|
175
|
+
x1 = random.randint(0, self._width)
|
|
176
|
+
y1 = random.randint(0, self._height)
|
|
177
|
+
x2 = random.randint(0, self._width)
|
|
178
|
+
y2 = random.randint(0, self._height)
|
|
179
|
+
line_color = (random.randint(100, 200), random.randint(100, 200), random.randint(100, 200))
|
|
180
|
+
draw.line([(x1, y1), (x2, y2)], fill=line_color, width=1)
|
|
181
|
+
|
|
182
|
+
buf = io.BytesIO()
|
|
183
|
+
img.save(buf, format=fmt)
|
|
184
|
+
return buf.getvalue()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class CaptchaUtil:
|
|
188
|
+
"""验证码工厂类。
|
|
189
|
+
|
|
190
|
+
提供各种验证码的便捷创建方法。
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def create_line_captcha(
|
|
195
|
+
width: int = 100, height: int = 35, code_count: int = 4, line_count: int = 5
|
|
196
|
+
) -> LineCaptcha:
|
|
197
|
+
"""创建线段干扰验证码。
|
|
198
|
+
|
|
199
|
+
:param width: 图片宽度,默认100像素
|
|
200
|
+
:param height: 图片高度,默认35像素
|
|
201
|
+
:param code_count: 验证码字符个数,默认4
|
|
202
|
+
:param line_count: 干扰线条数,默认5
|
|
203
|
+
:return: LineCaptcha实例
|
|
204
|
+
"""
|
|
205
|
+
return LineCaptcha(width, height, code_count, line_count)
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def create_arithmetic_captcha(width: int = 100, height: int = 35) -> ArithmeticCaptcha:
|
|
209
|
+
"""创建算术验证码。
|
|
210
|
+
|
|
211
|
+
:param width: 图片宽度,默认100像素
|
|
212
|
+
:param height: 图片高度,默认35像素
|
|
213
|
+
:return: ArithmeticCaptcha实例
|
|
214
|
+
"""
|
|
215
|
+
return ArithmeticCaptcha(width, height)
|
hutool/core/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PyHutool Core 核心模块。"""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
IllegalArgumentException,
|
|
5
|
+
IllegalStateException,
|
|
6
|
+
IORuntimeException,
|
|
7
|
+
NotInitedException,
|
|
8
|
+
NullPointerException,
|
|
9
|
+
StatefulException,
|
|
10
|
+
UnsupportedOperationException,
|
|
11
|
+
UtilException,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"IORuntimeException",
|
|
16
|
+
"IllegalArgumentException",
|
|
17
|
+
"IllegalStateException",
|
|
18
|
+
"NotInitedException",
|
|
19
|
+
"NullPointerException",
|
|
20
|
+
"StatefulException",
|
|
21
|
+
"UnsupportedOperationException",
|
|
22
|
+
"UtilException",
|
|
23
|
+
]
|
hutool/core/_base.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""基础类和常量,对应 Java cn.hutool.core.lang."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DefaultParam:
|
|
7
|
+
"""默认参数标记类,用于区分用户传入的 None 和未传参。"""
|
|
8
|
+
|
|
9
|
+
_instance = None
|
|
10
|
+
|
|
11
|
+
def __new__(cls):
|
|
12
|
+
if cls._instance is None:
|
|
13
|
+
cls._instance = super().__new__(cls)
|
|
14
|
+
return cls._instance
|
|
15
|
+
|
|
16
|
+
def __repr__(self):
|
|
17
|
+
return "<DEFAULT>"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# 通用常量
|
|
21
|
+
EMPTY: Final[str] = ""
|
|
22
|
+
SPACE: Final[str] = " "
|
|
23
|
+
TAB: Final[str] = "\t"
|
|
24
|
+
DOT: Final[str] = "."
|
|
25
|
+
SLASH: Final[str] = "/"
|
|
26
|
+
BACKSLASH: Final[str] = "\\"
|
|
27
|
+
CR: Final[str] = "\r"
|
|
28
|
+
LF: Final[str] = "\n"
|
|
29
|
+
CRLF: Final[str] = "\r\n"
|
|
30
|
+
COMMA: Final[str] = ","
|
|
31
|
+
COLON: Final[str] = ":"
|
|
32
|
+
SEMICOLON: Final[str] = ";"
|
|
33
|
+
DASH: Final[str] = "-"
|
|
34
|
+
UNDERLINE: Final[str] = "_"
|
|
35
|
+
AT: Final[str] = "@"
|
|
36
|
+
HASH: Final[str] = "#"
|
|
37
|
+
AMP: Final[str] = "&"
|
|
38
|
+
PIPE: Final[str] = "|"
|
|
39
|
+
TILDE: Final[str] = "~"
|
|
40
|
+
PERCENT: Final[str] = "%"
|
|
41
|
+
DOLLAR: Final[str] = "$"
|
|
42
|
+
QUESTION: Final[str] = "?"
|
|
43
|
+
EXCLAMATION: Final[str] = "!"
|
|
44
|
+
STAR: Final[str] = "*"
|
|
45
|
+
PLUS: Final[str] = "+"
|
|
46
|
+
MINUS: Final[str] = "-"
|
|
47
|
+
EQ: Final[str] = "="
|
|
48
|
+
LT: Final[str] = "<"
|
|
49
|
+
GT: Final[str] = ">"
|
|
50
|
+
NOT: Final[str] = "^"
|
|
51
|
+
|
|
52
|
+
# 常用编码
|
|
53
|
+
UTF_8: Final[str] = "UTF-8"
|
|
54
|
+
GBK: Final[str] = "GBK"
|
|
55
|
+
ISO_8859_1: Final[str] = "ISO-8859-1"
|
|
56
|
+
US_ASCII: Final[str] = "US-ASCII"
|
|
57
|
+
|
|
58
|
+
# 日期格式
|
|
59
|
+
NORM_DATETIME_PATTERN: Final[str] = "yyyy-MM-dd HH:mm:ss"
|
|
60
|
+
NORM_DATE_PATTERN: Final[str] = "yyyy-MM-dd"
|
|
61
|
+
NORM_TIME_PATTERN: Final[str] = "HH:mm:ss"
|
hutool/core/bean.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Bean工具类,提供对象与字典之间的转换、属性复制等功能"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, Optional, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
__all__ = ("BeanUtil",)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BeanUtil:
|
|
11
|
+
"""Bean工具类,提供对象属性的读写、对象与字典互转、属性复制等功能"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_field_value(bean: Any, field_name: str) -> Any:
|
|
15
|
+
"""
|
|
16
|
+
获取对象字段值
|
|
17
|
+
|
|
18
|
+
:param bean: 对象实例
|
|
19
|
+
:param field_name: 字段名
|
|
20
|
+
:return: 字段值,若字段不存在返回None
|
|
21
|
+
"""
|
|
22
|
+
if bean is None:
|
|
23
|
+
return None
|
|
24
|
+
try:
|
|
25
|
+
return getattr(bean, field_name, None)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def set_field_value(bean: Any, field_name: str, value: Any) -> None:
|
|
31
|
+
"""
|
|
32
|
+
设置对象字段值
|
|
33
|
+
|
|
34
|
+
:param bean: 对象实例
|
|
35
|
+
:param field_name: 字段名
|
|
36
|
+
:param value: 要设置的值
|
|
37
|
+
"""
|
|
38
|
+
if bean is None:
|
|
39
|
+
raise ValueError("Bean不能为None")
|
|
40
|
+
setattr(bean, field_name, value)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def map_to_bean(map_data: dict, bean_class: Type[T], ignore_error: bool = False) -> T:
|
|
44
|
+
"""
|
|
45
|
+
字典转对象
|
|
46
|
+
|
|
47
|
+
:param map_data: 字典数据
|
|
48
|
+
:param bean_class: 目标Bean类型
|
|
49
|
+
:param ignore_error: 是否忽略错误,若为True则跳过无法设置的字段
|
|
50
|
+
:return: Bean对象
|
|
51
|
+
"""
|
|
52
|
+
if map_data is None:
|
|
53
|
+
raise ValueError("map_data不能为None")
|
|
54
|
+
if bean_class is None:
|
|
55
|
+
raise ValueError("bean_class不能为None")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
bean = bean_class()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise TypeError(f"无法实例化{bean_class.__name__}: {e}") from e
|
|
61
|
+
|
|
62
|
+
BeanUtil.fill_bean_with_map(map_data, bean, ignore_error)
|
|
63
|
+
return bean
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def bean_to_map(bean: Any) -> dict:
|
|
67
|
+
"""
|
|
68
|
+
对象转字典,将对象中所有非下划线开头的公共属性转为字典
|
|
69
|
+
|
|
70
|
+
:param bean: Bean对象
|
|
71
|
+
:return: 字典
|
|
72
|
+
"""
|
|
73
|
+
if bean is None:
|
|
74
|
+
raise ValueError("bean不能为None")
|
|
75
|
+
|
|
76
|
+
result = {}
|
|
77
|
+
# 使用__dict__获取实例属性
|
|
78
|
+
if hasattr(bean, "__dict__"):
|
|
79
|
+
for key, value in bean.__dict__.items():
|
|
80
|
+
if not key.startswith("_"):
|
|
81
|
+
result[key] = value
|
|
82
|
+
|
|
83
|
+
# 获取类级别的公共属性(非下划线开头、非方法、非内置类型)
|
|
84
|
+
for key in dir(bean):
|
|
85
|
+
if key.startswith("_"):
|
|
86
|
+
continue
|
|
87
|
+
if key in result:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
value = getattr(bean, key)
|
|
91
|
+
if not callable(value):
|
|
92
|
+
result[key] = value
|
|
93
|
+
except Exception:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def copy_properties(source: Any, target: Any, *ignore_properties: str) -> None:
|
|
100
|
+
"""
|
|
101
|
+
复制属性,将源对象的属性值复制到目标对象
|
|
102
|
+
|
|
103
|
+
:param source: 源对象
|
|
104
|
+
:param target: 目标对象
|
|
105
|
+
:param ignore_properties: 忽略的属性名列表
|
|
106
|
+
"""
|
|
107
|
+
if source is None:
|
|
108
|
+
raise ValueError("source不能为None")
|
|
109
|
+
if target is None:
|
|
110
|
+
raise ValueError("target不能为None")
|
|
111
|
+
|
|
112
|
+
ignore_set = set(ignore_properties)
|
|
113
|
+
|
|
114
|
+
if hasattr(source, "__dict__"):
|
|
115
|
+
for key, value in source.__dict__.items():
|
|
116
|
+
if key.startswith("_") or key in ignore_set:
|
|
117
|
+
continue
|
|
118
|
+
if hasattr(target, key) or not hasattr(target, "__dict__"):
|
|
119
|
+
try:
|
|
120
|
+
setattr(target, key, value)
|
|
121
|
+
except (AttributeError, TypeError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def to_bean(source: Any, clazz: Type[T], copy_options: Optional[dict] = None) -> T:
|
|
126
|
+
"""
|
|
127
|
+
转为指定类型对象
|
|
128
|
+
|
|
129
|
+
:param source: 源对象或字典
|
|
130
|
+
:param clazz: 目标类型
|
|
131
|
+
:param copy_options: 复制选项字典,可包含ignore_properties列表
|
|
132
|
+
:return: 转换后的对象
|
|
133
|
+
"""
|
|
134
|
+
if source is None:
|
|
135
|
+
return None
|
|
136
|
+
if clazz is None:
|
|
137
|
+
raise ValueError("clazz不能为None")
|
|
138
|
+
|
|
139
|
+
if isinstance(source, dict):
|
|
140
|
+
ignore_error = (copy_options or {}).get("ignore_error", False)
|
|
141
|
+
return BeanUtil.map_to_bean(source, clazz, ignore_error=ignore_error)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
target = clazz()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
raise TypeError(f"无法实例化{clazz.__name__}: {e}") from e
|
|
147
|
+
|
|
148
|
+
ignore_properties = (copy_options or {}).get("ignore_properties", ())
|
|
149
|
+
BeanUtil.copy_properties(source, target, *ignore_properties)
|
|
150
|
+
return target
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def to_bean_list(source_list: list, clazz: Type[T]) -> List[T]:
|
|
154
|
+
"""
|
|
155
|
+
转为对象列表
|
|
156
|
+
|
|
157
|
+
:param source_list: 源列表(字典列表或对象列表)
|
|
158
|
+
:param clazz: 目标类型
|
|
159
|
+
:return: 转换后的对象列表
|
|
160
|
+
"""
|
|
161
|
+
if source_list is None:
|
|
162
|
+
return []
|
|
163
|
+
return [BeanUtil.to_bean(item, clazz) for item in source_list]
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def is_readable_bean(obj: Any) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
是否为可读的Bean,即拥有至少一个非下划线开头的公共属性
|
|
169
|
+
|
|
170
|
+
:param obj: 待检测对象
|
|
171
|
+
:return: 是否为可读Bean
|
|
172
|
+
"""
|
|
173
|
+
if obj is None:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
# 检查__dict__中是否有非下划线开头的属性
|
|
177
|
+
if hasattr(obj, "__dict__"):
|
|
178
|
+
for key in obj.__dict__:
|
|
179
|
+
if not key.startswith("_"):
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
# 检查类级别的公共属性
|
|
183
|
+
for key in dir(obj):
|
|
184
|
+
if key.startswith("_"):
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
value = getattr(obj, key)
|
|
188
|
+
if not callable(value):
|
|
189
|
+
return True
|
|
190
|
+
except Exception:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def fill_bean_with_map(map_data: dict, bean: Any, ignore_error: bool = False) -> None:
|
|
197
|
+
"""
|
|
198
|
+
用字典填充Bean,将字典中的键值对设置到Bean的对应属性上
|
|
199
|
+
|
|
200
|
+
:param map_data: 字典数据
|
|
201
|
+
:param bean: Bean对象
|
|
202
|
+
:param ignore_error: 是否忽略设置错误
|
|
203
|
+
"""
|
|
204
|
+
if map_data is None:
|
|
205
|
+
raise ValueError("map_data不能为None")
|
|
206
|
+
if bean is None:
|
|
207
|
+
raise ValueError("bean不能为None")
|
|
208
|
+
|
|
209
|
+
for key, value in map_data.items():
|
|
210
|
+
try:
|
|
211
|
+
setattr(bean, key, value)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
if not ignore_error:
|
|
214
|
+
raise ValueError(f"设置字段{key}失败: {e}") from e
|
hutool/core/codec.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""编解码工具类,提供Base64、Base32等编解码功能"""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
__all__ = ("Base32", "Base64")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base64:
|
|
9
|
+
"""Base64编解码工具类"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def encode(data: bytes) -> str:
|
|
13
|
+
"""
|
|
14
|
+
编码字节数据为Base64字符串
|
|
15
|
+
|
|
16
|
+
:param data: 待编码的字节数据
|
|
17
|
+
:return: Base64编码字符串
|
|
18
|
+
"""
|
|
19
|
+
if data is None:
|
|
20
|
+
raise ValueError("待编码数据不能为None")
|
|
21
|
+
return base64.b64encode(data).decode("ascii")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def encode_str(data: str, charset: str = "utf-8") -> str:
|
|
25
|
+
"""
|
|
26
|
+
编码字符串为Base64字符串
|
|
27
|
+
|
|
28
|
+
:param data: 待编码的字符串
|
|
29
|
+
:param charset: 字符编码,默认utf-8
|
|
30
|
+
:return: Base64编码字符串
|
|
31
|
+
"""
|
|
32
|
+
if data is None:
|
|
33
|
+
raise ValueError("待编码字符串不能为None")
|
|
34
|
+
return base64.b64encode(data.encode(charset)).decode("ascii")
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def decode(base64_str: str) -> bytes:
|
|
38
|
+
"""
|
|
39
|
+
解码Base64字符串为字节数据
|
|
40
|
+
|
|
41
|
+
:param base64_str: Base64编码字符串
|
|
42
|
+
:return: 解码后的字节数据
|
|
43
|
+
"""
|
|
44
|
+
if base64_str is None:
|
|
45
|
+
raise ValueError("Base64字符串不能为None")
|
|
46
|
+
return base64.b64decode(base64_str)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def decode_str(base64_str: str, charset: str = "utf-8") -> str:
|
|
50
|
+
"""
|
|
51
|
+
解码Base64字符串为字符串
|
|
52
|
+
|
|
53
|
+
:param base64_str: Base64编码字符串
|
|
54
|
+
:param charset: 字符编码,默认utf-8
|
|
55
|
+
:return: 解码后的字符串
|
|
56
|
+
"""
|
|
57
|
+
if base64_str is None:
|
|
58
|
+
raise ValueError("Base64字符串不能为None")
|
|
59
|
+
return base64.b64decode(base64_str).decode(charset)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def encode_url_safe(data: bytes) -> str:
|
|
63
|
+
"""
|
|
64
|
+
编码为URL安全的Base64字符串(使用-和_代替+和/)
|
|
65
|
+
|
|
66
|
+
:param data: 待编码的字节数据
|
|
67
|
+
:return: URL安全的Base64编码字符串
|
|
68
|
+
"""
|
|
69
|
+
if data is None:
|
|
70
|
+
raise ValueError("待编码数据不能为None")
|
|
71
|
+
return base64.urlsafe_b64encode(data).decode("ascii")
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def decode_url_safe(base64_str: str) -> bytes:
|
|
75
|
+
"""
|
|
76
|
+
解码URL安全的Base64字符串
|
|
77
|
+
|
|
78
|
+
:param base64_str: URL安全的Base64编码字符串
|
|
79
|
+
:return: 解码后的字节数据
|
|
80
|
+
"""
|
|
81
|
+
if base64_str is None:
|
|
82
|
+
raise ValueError("Base64字符串不能为None")
|
|
83
|
+
return base64.urlsafe_b64decode(base64_str)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Base32:
|
|
87
|
+
"""Base32编解码工具类"""
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def encode(data: bytes) -> str:
|
|
91
|
+
"""
|
|
92
|
+
编码字节数据为Base32字符串
|
|
93
|
+
|
|
94
|
+
:param data: 待编码的字节数据
|
|
95
|
+
:return: Base32编码字符串
|
|
96
|
+
"""
|
|
97
|
+
if data is None:
|
|
98
|
+
raise ValueError("待编码数据不能为None")
|
|
99
|
+
return base64.b32encode(data).decode("ascii")
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def decode(base32_str: str) -> bytes:
|
|
103
|
+
"""
|
|
104
|
+
解码Base32字符串为字节数据
|
|
105
|
+
|
|
106
|
+
:param base32_str: Base32编码字符串
|
|
107
|
+
:return: 解码后的字节数据
|
|
108
|
+
"""
|
|
109
|
+
if base32_str is None:
|
|
110
|
+
raise ValueError("Base32字符串不能为None")
|
|
111
|
+
return base64.b32decode(base32_str)
|