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.
Files changed (89) hide show
  1. hutool/__init__.py +174 -0
  2. hutool/cache/__init__.py +7 -0
  3. hutool/cache/cache_util.py +47 -0
  4. hutool/cache/fifo_cache.py +87 -0
  5. hutool/cache/lfu_cache.py +129 -0
  6. hutool/cache/lru_cache.py +93 -0
  7. hutool/cache/timed_cache.py +115 -0
  8. hutool/captcha/__init__.py +3 -0
  9. hutool/captcha/captcha_util.py +215 -0
  10. hutool/core/__init__.py +23 -0
  11. hutool/core/_base.py +61 -0
  12. hutool/core/bean.py +214 -0
  13. hutool/core/codec.py +111 -0
  14. hutool/core/coll.py +635 -0
  15. hutool/core/date.py +1024 -0
  16. hutool/core/exceptions.py +66 -0
  17. hutool/core/io/__init__.py +0 -0
  18. hutool/core/io/data_size_util.py +79 -0
  19. hutool/core/io/file_name_util.py +111 -0
  20. hutool/core/io/file_util.py +650 -0
  21. hutool/core/io/io_util.py +133 -0
  22. hutool/core/io/path_util.py +247 -0
  23. hutool/core/io/resource_util.py +137 -0
  24. hutool/core/map.py +933 -0
  25. hutool/core/math_util.py +105 -0
  26. hutool/core/net.py +288 -0
  27. hutool/core/text/__init__.py +0 -0
  28. hutool/core/text/csv_util.py +54 -0
  29. hutool/core/text/str_builder.py +224 -0
  30. hutool/core/text/unicode_util.py +58 -0
  31. hutool/core/tree.py +242 -0
  32. hutool/core/util/__init__.py +63 -0
  33. hutool/core/util/array_util.py +503 -0
  34. hutool/core/util/boolean_util.py +124 -0
  35. hutool/core/util/charset_util.py +60 -0
  36. hutool/core/util/class_util.py +136 -0
  37. hutool/core/util/coordinate_util.py +186 -0
  38. hutool/core/util/credit_code_util.py +110 -0
  39. hutool/core/util/desensitized_util.py +194 -0
  40. hutool/core/util/enum_util.py +94 -0
  41. hutool/core/util/escape_util.py +97 -0
  42. hutool/core/util/hash_util.py +243 -0
  43. hutool/core/util/hex_util.py +140 -0
  44. hutool/core/util/id_util.py +147 -0
  45. hutool/core/util/idcard_util.py +300 -0
  46. hutool/core/util/number_util.py +720 -0
  47. hutool/core/util/object_util.py +294 -0
  48. hutool/core/util/page_util.py +61 -0
  49. hutool/core/util/phone_util.py +140 -0
  50. hutool/core/util/random_util.py +112 -0
  51. hutool/core/util/re_util.py +231 -0
  52. hutool/core/util/reflect_util.py +135 -0
  53. hutool/core/util/runtime_util.py +89 -0
  54. hutool/core/util/str_util.py +2320 -0
  55. hutool/core/util/system_util.py +62 -0
  56. hutool/core/util/url_util.py +232 -0
  57. hutool/core/util/version_util.py +41 -0
  58. hutool/core/util/xml_util.py +158 -0
  59. hutool/core/util/zip_util.py +126 -0
  60. hutool/cron/__init__.py +4 -0
  61. hutool/cron/cron_pattern.py +123 -0
  62. hutool/cron/cron_util.py +115 -0
  63. hutool/crypto/__init__.py +5 -0
  64. hutool/crypto/digest_util.py +167 -0
  65. hutool/crypto/secure_util.py +311 -0
  66. hutool/crypto/sign_util.py +74 -0
  67. hutool/dfa/__init__.py +3 -0
  68. hutool/dfa/sensitive_util.py +114 -0
  69. hutool/extra/__init__.py +6 -0
  70. hutool/extra/emoji_util.py +90 -0
  71. hutool/extra/pinyin_util.py +44 -0
  72. hutool/extra/qr_code_util.py +58 -0
  73. hutool/extra/template_util.py +41 -0
  74. hutool/http/__init__.py +6 -0
  75. hutool/http/html_util.py +88 -0
  76. hutool/http/http_request.py +188 -0
  77. hutool/http/http_response.py +139 -0
  78. hutool/http/http_util.py +237 -0
  79. hutool/json_util.py +251 -0
  80. hutool/jwt_util.py +57 -0
  81. hutool/setting/__init__.py +5 -0
  82. hutool/setting/props_util.py +79 -0
  83. hutool/setting/setting_util.py +80 -0
  84. hutool/setting/yaml_util.py +45 -0
  85. hutool_python-1.0.0.dist-info/LICENSE +127 -0
  86. hutool_python-1.0.0.dist-info/METADATA +438 -0
  87. hutool_python-1.0.0.dist-info/RECORD +89 -0
  88. hutool_python-1.0.0.dist-info/WHEEL +5 -0
  89. hutool_python-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,147 @@
1
+ import secrets
2
+ import threading
3
+ import time
4
+ import uuid
5
+
6
+
7
+ class _SnowflakeWorkerPool:
8
+ """模块级雪花算法工作池,跨调用维持状态"""
9
+
10
+ _workers: dict = {}
11
+ _lock = threading.Lock()
12
+
13
+ @classmethod
14
+ def get(cls, worker_id: int, datacenter_id: int) -> "_SnowflakeIdWorker":
15
+ key = (worker_id, datacenter_id)
16
+ with cls._lock:
17
+ if key not in cls._workers:
18
+ cls._workers[key] = _SnowflakeIdWorker(worker_id, datacenter_id)
19
+ return cls._workers[key]
20
+
21
+
22
+ class IdUtil:
23
+ """ID生成工具类,对应 Java cn.hutool.core.util.IdUtil"""
24
+
25
+ @staticmethod
26
+ def random_uuid() -> str:
27
+ """生成随机UUID(带横线)"""
28
+ return str(uuid.uuid4())
29
+
30
+ @staticmethod
31
+ def simple_uuid() -> str:
32
+ """生成简单UUID(不带横线)"""
33
+ return str(uuid.uuid4()).replace("-", "")
34
+
35
+ @staticmethod
36
+ def fast_uuid() -> str:
37
+ """快速生成UUID"""
38
+ return str(uuid.uuid4())
39
+
40
+ @staticmethod
41
+ def fast_simple_uuid() -> str:
42
+ """快速生成简单UUID"""
43
+ return str(uuid.uuid4()).replace("-", "")
44
+
45
+ @staticmethod
46
+ def nano_id(size: int = 21) -> str:
47
+ """生成NanoID(URL安全的短ID)
48
+
49
+ 使用 secrets 模块实现,类似 nanoid 算法。
50
+
51
+ :param size: 生成的ID长度,默认为21
52
+ :return: NanoID字符串
53
+ """
54
+ if size <= 0:
55
+ raise ValueError(f"size({size}) 必须为正数")
56
+ alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
57
+ return "".join(secrets.choice(alphabet) for _ in range(size))
58
+
59
+ @staticmethod
60
+ def snowflake_id(worker_id: int = 1, datacenter_id: int = 1) -> int:
61
+ """生成雪花算法ID
62
+
63
+ :param worker_id: 工作机器ID
64
+ :param datacenter_id: 数据中心ID
65
+ :return: 雪花算法生成的ID
66
+ """
67
+ return _SnowflakeWorkerPool.get(worker_id, datacenter_id).next_id()
68
+
69
+ @staticmethod
70
+ def object_id() -> str:
71
+ """生成MongoDB ObjectId格式的24位十六进制字符串"""
72
+ # 4字节时间戳 + 5字节随机值 + 3字节递增计数器
73
+ timestamp_part = format(int(time.time()), "08x")
74
+ random_part = secrets.token_hex(5)
75
+ counter_part = secrets.token_hex(3)
76
+ return timestamp_part + random_part + counter_part
77
+
78
+
79
+ class _SnowflakeIdWorker:
80
+ """雪花算法ID生成器内部实现"""
81
+
82
+ # 起始时间戳 (2020-01-01 00:00:00)
83
+ EPOCH = 1577808000000
84
+
85
+ # 各部分位数
86
+ WORKER_ID_BITS = 5
87
+ DATACENTER_ID_BITS = 5
88
+ SEQUENCE_BITS = 12
89
+
90
+ # 最大值
91
+ MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1 # 31
92
+ MAX_DATACENTER_ID = (1 << DATACENTER_ID_BITS) - 1 # 31
93
+
94
+ # 序列掩码
95
+ SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1 # 4095
96
+
97
+ # 左移位数
98
+ WORKER_ID_SHIFT = SEQUENCE_BITS # 12
99
+ DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS # 17
100
+ TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS # 22
101
+
102
+ def __init__(self, worker_id: int, datacenter_id: int):
103
+ """初始化雪花算法ID生成器
104
+
105
+ :param worker_id: 工作机器ID,范围 [0, 31]
106
+ :param datacenter_id: 数据中心ID,范围 [0, 31]
107
+ """
108
+ if worker_id < 0 or worker_id > self.MAX_WORKER_ID:
109
+ raise ValueError(f"worker_id({worker_id}) 必须在 [0, {self.MAX_WORKER_ID}] 范围内")
110
+ if datacenter_id < 0 or datacenter_id > self.MAX_DATACENTER_ID:
111
+ raise ValueError(f"datacenter_id({datacenter_id}) 必须在 [0, {self.MAX_DATACENTER_ID}] 范围内")
112
+ self._worker_id = worker_id
113
+ self._datacenter_id = datacenter_id
114
+ self._sequence = 0
115
+ self._last_timestamp = -1
116
+ self._lock = threading.Lock()
117
+
118
+ def next_id(self) -> int:
119
+ """生成下一个ID(线程安全)"""
120
+ with self._lock:
121
+ timestamp = self._current_millis()
122
+ if timestamp < self._last_timestamp:
123
+ raise RuntimeError(f"时钟回拨,拒绝生成ID,回拨毫秒数: {self._last_timestamp - timestamp}")
124
+ if timestamp == self._last_timestamp:
125
+ self._sequence = (self._sequence + 1) & self.SEQUENCE_MASK
126
+ if self._sequence == 0:
127
+ timestamp = self._wait_next_millis()
128
+ else:
129
+ self._sequence = 0
130
+ self._last_timestamp = timestamp
131
+ return (
132
+ ((timestamp - self.EPOCH) << self.TIMESTAMP_LEFT_SHIFT)
133
+ | (self._datacenter_id << self.DATACENTER_ID_SHIFT)
134
+ | (self._worker_id << self.WORKER_ID_SHIFT)
135
+ | self._sequence
136
+ )
137
+
138
+ def _current_millis(self) -> int:
139
+ """获取当前时间戳(毫秒)"""
140
+ return int(time.time() * 1000)
141
+
142
+ def _wait_next_millis(self) -> int:
143
+ """自旋等待直到下一毫秒"""
144
+ ts = self._current_millis()
145
+ while ts <= self._last_timestamp:
146
+ ts = self._current_millis()
147
+ return ts
@@ -0,0 +1,300 @@
1
+ import re
2
+ from datetime import date
3
+
4
+
5
+ class IdcardUtil:
6
+ """身份证工具类"""
7
+
8
+ # 省份代码
9
+ _PROVINCE_CODES = {
10
+ "11": "北京",
11
+ "12": "天津",
12
+ "13": "河北",
13
+ "14": "山西",
14
+ "15": "内蒙古",
15
+ "21": "辽宁",
16
+ "22": "吉林",
17
+ "23": "黑龙江",
18
+ "31": "上海",
19
+ "32": "江苏",
20
+ "33": "浙江",
21
+ "34": "安徽",
22
+ "35": "福建",
23
+ "36": "江西",
24
+ "37": "山东",
25
+ "41": "河南",
26
+ "42": "湖北",
27
+ "43": "湖南",
28
+ "44": "广东",
29
+ "45": "广西",
30
+ "46": "海南",
31
+ "50": "重庆",
32
+ "51": "四川",
33
+ "52": "贵州",
34
+ "53": "云南",
35
+ "54": "西藏",
36
+ "61": "陕西",
37
+ "62": "甘肃",
38
+ "63": "青海",
39
+ "64": "宁夏",
40
+ "65": "新疆",
41
+ "71": "台湾",
42
+ "81": "香港",
43
+ "82": "澳门",
44
+ "91": "国外",
45
+ }
46
+
47
+ # 加权因子(前17位)
48
+ _WEIGHT_FACTORS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
49
+
50
+ # 校验码对应值(余数 -> 校验码)
51
+ _CHECK_CODES = "10X98765432"
52
+
53
+ # 18位身份证正则
54
+ _ID_CARD_18_PATTERN = re.compile(r"^\d{6}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$")
55
+
56
+ # 15位身份证正则
57
+ _ID_CARD_15_PATTERN = re.compile(r"^\d{15}$")
58
+
59
+ @staticmethod
60
+ def is_valid_idcard(idcard: str) -> bool:
61
+ """校验身份证号是否有效(支持15位和18位)
62
+
63
+ :param idcard: 身份证号
64
+ :return: 是否有效
65
+ """
66
+ if not idcard:
67
+ return False
68
+ idcard = idcard.strip()
69
+ if len(idcard) == 18:
70
+ return IdcardUtil.is_valid_card18(idcard)
71
+ elif len(idcard) == 15:
72
+ return IdcardUtil.is_valid_card15(idcard)
73
+ return False
74
+
75
+ @staticmethod
76
+ def is_valid_card18(idcard: str) -> bool:
77
+ """校验18位身份证
78
+
79
+ 校验规则:
80
+ 1. 格式校验(17位数字 + 第18位数字或X/x)
81
+ 2. 省份代码校验
82
+ 3. 出生日期校验(合法的YYYYMMDD)
83
+ 4. 校验码计算验证(加权求和 mod 11)
84
+
85
+ :param idcard: 18位身份证号
86
+ :return: 是否有效
87
+ """
88
+ if not idcard:
89
+ return False
90
+ idcard = idcard.strip().upper()
91
+
92
+ # 1. 格式校验
93
+ if not IdcardUtil._ID_CARD_18_PATTERN.match(idcard):
94
+ # 也接受大写X
95
+ if not re.match(r"^\d{17}[\dX]$", idcard):
96
+ return False
97
+
98
+ # 2. 省份代码校验
99
+ province = idcard[:2]
100
+ if province not in IdcardUtil._PROVINCE_CODES:
101
+ return False
102
+
103
+ # 3. 出生日期校验
104
+ birth_str = idcard[6:14]
105
+ try:
106
+ year = int(birth_str[:4])
107
+ month = int(birth_str[4:6])
108
+ day = int(birth_str[6:8])
109
+ birth_date = date(year, month, day)
110
+ # 日期不能在未来
111
+ if birth_date > date.today():
112
+ return False
113
+ except ValueError:
114
+ return False
115
+
116
+ # 4. 校验码计算验证
117
+ # 前17位数字乘以加权因子求和
118
+ total = 0
119
+ for i in range(17):
120
+ total += int(idcard[i]) * IdcardUtil._WEIGHT_FACTORS[i]
121
+ # 取模11得到校验码索引
122
+ check_index = total % 11
123
+ expected_check = IdcardUtil._CHECK_CODES[check_index]
124
+ return idcard[17] == expected_check
125
+
126
+ @staticmethod
127
+ def is_valid_card15(idcard: str) -> bool:
128
+ """校验15位身份证
129
+
130
+ :param idcard: 15位身份证号
131
+ :return: 是否有效
132
+ """
133
+ if not idcard:
134
+ return False
135
+ idcard = idcard.strip()
136
+
137
+ # 格式校验
138
+ if not IdcardUtil._ID_CARD_15_PATTERN.match(idcard):
139
+ return False
140
+
141
+ # 省份代码校验
142
+ province = idcard[:2]
143
+ if province not in IdcardUtil._PROVINCE_CODES:
144
+ return False
145
+
146
+ # 出生日期校验(15位:6位地区码 + 6位出生日期YYMMDD + 3位顺序码)
147
+ try:
148
+ year = int(idcard[6:8])
149
+ # 15位身份证的年份默认为19xx
150
+ year += 1900
151
+ month = int(idcard[8:10])
152
+ day = int(idcard[10:12])
153
+ birth_date = date(year, month, day)
154
+ if birth_date > date.today():
155
+ return False
156
+ except ValueError:
157
+ return False
158
+
159
+ return True
160
+
161
+ @staticmethod
162
+ def get_birth(idcard: str) -> str:
163
+ """获取出生日期字符串,如 "19900101"
164
+
165
+ :param idcard: 身份证号
166
+ :return: 出生日期字符串(YYYYMMDD格式),无效则返回空字符串
167
+ """
168
+ if not idcard:
169
+ return ""
170
+ idcard = idcard.strip()
171
+ if len(idcard) == 18:
172
+ return idcard[6:14]
173
+ elif len(idcard) == 15:
174
+ return "19" + idcard[6:12]
175
+ return ""
176
+
177
+ @staticmethod
178
+ def get_age(idcard: str) -> int:
179
+ """根据身份证号获取年龄
180
+
181
+ :param idcard: 身份证号
182
+ :return: 年龄,无效则返回-1
183
+ """
184
+ if not idcard:
185
+ return -1
186
+ idcard = idcard.strip()
187
+ birth_str = IdcardUtil.get_birth(idcard)
188
+ if not birth_str or len(birth_str) != 8:
189
+ return -1
190
+ try:
191
+ year = int(birth_str[:4])
192
+ month = int(birth_str[4:6])
193
+ day = int(birth_str[6:8])
194
+ birth_date = date(year, month, day)
195
+ return IdcardUtil.get_age_by_birth(birth_date)
196
+ except ValueError:
197
+ return -1
198
+
199
+ @staticmethod
200
+ def get_age_by_birth(birth_date: date) -> int:
201
+ """根据出生日期获取年龄
202
+
203
+ :param birth_date: 出生日期
204
+ :return: 年龄
205
+ """
206
+ today = date.today()
207
+ age = today.year - birth_date.year
208
+ # 如果今年的生日还没到,年龄减1
209
+ if (today.month, today.day) < (birth_date.month, birth_date.day):
210
+ age -= 1
211
+ return age
212
+
213
+ @staticmethod
214
+ def get_gender(idcard: str) -> str:
215
+ """获取性别
216
+
217
+ 身份证第17位(18位身份证)或第15位(15位身份证):
218
+ 奇数为男性,偶数为女性
219
+
220
+ :param idcard: 身份证号
221
+ :return: "M"(男)或 "F"(女),无效则返回空字符串
222
+ """
223
+ if not idcard:
224
+ return ""
225
+ idcard = idcard.strip()
226
+ if len(idcard) == 18:
227
+ gender_digit = int(idcard[16])
228
+ elif len(idcard) == 15:
229
+ gender_digit = int(idcard[14])
230
+ else:
231
+ return ""
232
+ return "M" if gender_digit % 2 == 1 else "F"
233
+
234
+ @staticmethod
235
+ def get_province(idcard: str) -> str:
236
+ """获取省份
237
+
238
+ :param idcard: 身份证号
239
+ :return: 省份名称,无效则返回空字符串
240
+ """
241
+ if not idcard:
242
+ return ""
243
+ idcard = idcard.strip()
244
+ if len(idcard) < 2:
245
+ return ""
246
+ province_code = idcard[:2]
247
+ return IdcardUtil._PROVINCE_CODES.get(province_code, "")
248
+
249
+ @staticmethod
250
+ def convert15to18(idcard15: str) -> str:
251
+ """15位身份证转18位
252
+
253
+ 转换规则:
254
+ 1. 在第6位后插入"19"(年份补全)
255
+ 2. 计算第18位校验码
256
+
257
+ :param idcard15: 15位身份证号
258
+ :return: 18位身份证号,无效输入返回空字符串
259
+ """
260
+ if not idcard15:
261
+ return ""
262
+ idcard15 = idcard15.strip()
263
+ if len(idcard15) != 15:
264
+ return ""
265
+ if not IdcardUtil._ID_CARD_15_PATTERN.match(idcard15):
266
+ return ""
267
+
268
+ # 在第6位后插入"19"
269
+ idcard17 = idcard15[:6] + "19" + idcard15[6:]
270
+
271
+ # 计算校验码
272
+ total = 0
273
+ for i in range(17):
274
+ total += int(idcard17[i]) * IdcardUtil._WEIGHT_FACTORS[i]
275
+ check_index = total % 11
276
+ check_code = IdcardUtil._CHECK_CODES[check_index]
277
+
278
+ return idcard17 + check_code
279
+
280
+ @staticmethod
281
+ def hide(idcard: str) -> str:
282
+ """隐藏身份证号中间部分
283
+
284
+ 18位身份证:保留前6位和后4位,中间用****代替
285
+ 15位身份证:保留前4位和后4位,中间用****代替
286
+
287
+ :param idcard: 身份证号
288
+ :return: 脱敏后的身份证号
289
+ """
290
+ if not idcard:
291
+ return ""
292
+ idcard = idcard.strip()
293
+ if len(idcard) == 18:
294
+ return idcard[:6] + "********" + idcard[14:]
295
+ elif len(idcard) == 15:
296
+ return idcard[:4] + "*******" + idcard[11:]
297
+ elif len(idcard) > 8:
298
+ half = (len(idcard) - 4) // 2
299
+ return idcard[:half] + "****" + idcard[half + 4 :]
300
+ return idcard