oxy-py-sdk 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.
oxy/__init__.py ADDED
File without changes
oxy/py.typed ADDED
File without changes
oxy/utils/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,285 @@
1
+ import base64
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from cryptography.exceptions import InvalidSignature
8
+ from cryptography.hazmat.backends import default_backend
9
+ from cryptography.hazmat.primitives import hashes, serialization
10
+ from cryptography.hazmat.primitives.asymmetric import rsa, padding, types
11
+
12
+ from oxy.utils.codec.crypto_model import SignPadding
13
+ from oxy.utils.lang import logger
14
+
15
+ log = logger.of(__name__)
16
+
17
+
18
+ def load_user_keys(base_dir: Path = Path.home().joinpath('.ssh'), force_first: bool = False) \
19
+ -> tuple[types.PrivateKeyTypes, types.PublicKeyTypes]:
20
+ """
21
+ 加载用户秘钥:以.pem结尾,包含public和private名称的文件
22
+ :param base_dir: 秘钥存放目录,默认为用户目录.ssh下,即:~/.ssh/.
23
+ :param force_first: 强制选择第一个秘钥
24
+ :return: (私钥,公钥)
25
+ """
26
+ key_pairs = [(pub_key_file.replace('public', 'private'), pub_key_file) for pub_key_file in os.listdir(base_dir)
27
+ if re.search(r'public\S+\.pem$', pub_key_file)
28
+ and base_dir.joinpath(pub_key_file.replace('public', 'private')).is_file()]
29
+ if not key_pairs:
30
+ log.error(f'用户目录下<{base_dir.absolute()}>未找到秘钥文件!')
31
+ raise FileNotFoundError(f'用户目录下<{base_dir.absolute()}>未找到秘钥文件!')
32
+
33
+ pri_key_file, pub_key_file = key_pairs[0]
34
+ if len(key_pairs) > 1 and not force_first:
35
+ log.warning(f'用户目录下找到多个秘钥文件:')
36
+ for i, key_pair in enumerate(key_pairs):
37
+ log.warning(f'{i + 1}. {key_pair[0]} and {key_pair[1]}')
38
+ while True:
39
+ try:
40
+ select_idx = int(input(f'请选择要使用的秘钥文件[输入1至{len(key_pairs)}]:'))
41
+ if 1 <= select_idx <= len(key_pairs):
42
+ pri_key_file, pub_key_file = key_pairs[select_idx - 1]
43
+ break
44
+ except ValueError:
45
+ log.error(f'请正确输入序号!')
46
+ log.warning(f'选择使用的秘钥文件:\n - {pri_key_file} \n - {pub_key_file}')
47
+ return load_private_key(base_dir.joinpath(pri_key_file)), load_public_key(base_dir.joinpath(pub_key_file))
48
+
49
+
50
+ def load_user_public_key(base_dir: Path = Path.home().joinpath('.ssh'), force_first: bool = False) \
51
+ -> Optional[types.PublicKeyTypes]:
52
+ """
53
+ 加载用户公钥:以.pem结尾,包含public名称的文件
54
+ :param base_dir: 秘钥存放目录,默认为用户目录.ssh下,即:~/.ssh/.
55
+ :param force_first: 强制选择第一个秘钥
56
+ """
57
+ keys = [pub_key_file for pub_key_file in os.listdir(base_dir) if re.search(r'public\S+\.pem$', pub_key_file)]
58
+ if not keys:
59
+ log.error(f'用户目录下<{base_dir.absolute()}>未找到公钥文件!')
60
+ raise FileNotFoundError(f'用户目录下<{base_dir.absolute()}>未找到公钥文件!')
61
+
62
+ pub_key_file = keys[0]
63
+ if len(keys) > 1 and not force_first:
64
+ log.warning(f'用户目录下找到多个公钥文件:')
65
+ for i, key in enumerate(keys):
66
+ log.warning(f'{i + 1}. {key}')
67
+ while True:
68
+ try:
69
+ select_idx = int(input(f'请选择要使用的公钥文件[输入1至{len(keys)}]:'))
70
+ if 1 <= select_idx <= len(keys):
71
+ pub_key_file = keys[select_idx - 1]
72
+ break
73
+ except ValueError:
74
+ log.error(f'请正确输入序号!')
75
+ log.warning(f'选择使用的公钥文件:\n - {pub_key_file}')
76
+ return load_public_key(base_dir.joinpath(pub_key_file))
77
+
78
+
79
+ def load_user_private_key(base_dir: Path = Path.home().joinpath('.ssh'), force_first: bool = False) \
80
+ -> Optional[types.PrivateKeyTypes]:
81
+ """
82
+ 加载用户私钥:以.pem结尾,包含private名称的文件
83
+ :param base_dir: 秘钥存放目录,默认为用户目录.ssh下,即:~/.ssh/.
84
+ :param force_first: 强制选择第一个秘钥
85
+ """
86
+ keys = [pri_key_file for pri_key_file in os.listdir(base_dir) if re.search(r'private\S+\.pem$', pri_key_file)]
87
+ if not keys:
88
+ log.error(f'用户目录下<{base_dir.absolute()}>未找到私钥文件!')
89
+ raise FileNotFoundError(f'用户目录下<{base_dir.absolute()}>未找到私钥文件!')
90
+
91
+ pri_key_file = keys[0]
92
+ if len(keys) > 1 and not force_first:
93
+ log.warning(f'用户目录下找到多个私钥文件:')
94
+ for i, key in enumerate(keys):
95
+ log.warning(f'{i + 1}. {key}')
96
+ while True:
97
+ try:
98
+ select_idx = int(input(f'请选择要使用的私钥文件[输入1至{len(keys)}]:'))
99
+ if 1 <= select_idx <= len(keys):
100
+ pri_key_file = keys[select_idx - 1]
101
+ break
102
+ except ValueError:
103
+ log.error(f'请正确输入序号!')
104
+ log.warning(f'选择使用的私钥文件:\n - {pri_key_file}')
105
+ return load_private_key(base_dir.joinpath(pri_key_file))
106
+
107
+
108
+ def generate_rsa_keys(serialize: bool = False) -> tuple[types.PrivateKeyTypes | str, types.PublicKeyTypes | str]:
109
+ """
110
+ 生成一个RSA密钥对
111
+ :param serialize: 如果为True,则返回秘钥序列化的字符串
112
+ :return: (私钥, 公钥)
113
+ """
114
+ private_key = rsa.generate_private_key(
115
+ public_exponent=65537,
116
+ key_size=2048,
117
+ backend=default_backend()
118
+ )
119
+ public_key = private_key.public_key()
120
+ if not serialize:
121
+ return private_key, public_key
122
+ # 私钥序列化
123
+ private_pem = private_key.private_bytes(
124
+ encoding=serialization.Encoding.PEM,
125
+ format=serialization.PrivateFormat.PKCS8,
126
+ encryption_algorithm=serialization.NoEncryption()
127
+ )
128
+ # 公钥序列化
129
+ public_pem = public_key.public_bytes(
130
+ encoding=serialization.Encoding.PEM,
131
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
132
+ )
133
+ return str(private_pem, 'utf-8'), str(public_pem, 'utf-8')
134
+
135
+
136
+ def load_public_key(public_key_pem: Path | bytes | types.PublicKeyTypes) -> types.PublicKeyTypes:
137
+ """
138
+ 加载公钥
139
+ :param public_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
140
+ """
141
+ if isinstance(public_key_pem, Path):
142
+ with open(public_key_pem, 'rb') as file:
143
+ file_bytes = file.read()
144
+ # 递归处理
145
+ return load_public_key(file_bytes)
146
+ elif isinstance(public_key_pem, bytes):
147
+ return serialization.load_pem_public_key(public_key_pem, default_backend())
148
+ elif isinstance(public_key_pem, types.PublicKeyTypes):
149
+ return public_key_pem
150
+ else:
151
+ raise TypeError('公钥入参格式异常,请检查')
152
+
153
+
154
+ def load_private_key(private_key_pem: Path | bytes | types.PrivateKeyTypes) -> types.PrivateKeyTypes:
155
+ """
156
+ 加载私钥
157
+ :param private_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
158
+ """
159
+ if isinstance(private_key_pem, Path):
160
+ with open(private_key_pem, 'rb') as file:
161
+ file_bytes = file.read()
162
+ # 递归处理
163
+ return load_private_key(file_bytes)
164
+ elif isinstance(private_key_pem, bytes):
165
+ return serialization.load_pem_private_key(private_key_pem, password=None, backend=default_backend())
166
+ elif isinstance(private_key_pem, types.PrivateKeyTypes):
167
+ return private_key_pem
168
+ else:
169
+ raise TypeError('私钥入参格式异常,请检查')
170
+
171
+
172
+ def encrypt_message(public_key_pem: Path | bytes | types.PublicKeyTypes, message: str | bytes) \
173
+ -> tuple[types.PublicKeyTypes, str]:
174
+ """
175
+ 公钥加密
176
+ :param public_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
177
+ """
178
+ if isinstance(message, str):
179
+ message = message.encode('utf-8')
180
+ # 加载公钥
181
+ public_key = load_public_key(public_key_pem)
182
+ # 加密
183
+ chunk_size = 128
184
+ if len(message) > chunk_size:
185
+ # 超长分段加密
186
+ enc_messages = [encrypt_message(public_key, message[i:i+chunk_size])[1] for i in range(0, len(message), chunk_size)]
187
+ return public_key, '__'.join(enc_messages)
188
+ enc_message = public_key.encrypt(
189
+ message,
190
+ padding.OAEP(
191
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
192
+ algorithm=hashes.SHA256(),
193
+ label=None
194
+ )
195
+ )
196
+ return public_key, base64.b64encode(enc_message).decode('utf-8')
197
+
198
+
199
+ def decrypt_message(private_key_pem: Path | bytes | types.PrivateKeyTypes, encrypted_message: str | bytes) \
200
+ -> tuple[types.PrivateKeyTypes, str]:
201
+ """
202
+ 私钥解密
203
+ :param private_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
204
+ :param encrypted_message: 加密信息
205
+ """
206
+ # 加载私钥
207
+ private_key = load_private_key(private_key_pem)
208
+ if isinstance(encrypted_message, bytes):
209
+ encrypted_message = encrypted_message.decode('ascii')
210
+ # 分段边界可能位于 UTF-8 字符内部,合并全部明文字节后再解码。
211
+ decrypted_parts = [
212
+ private_key.decrypt(
213
+ base64.b64decode(part),
214
+ padding.OAEP(
215
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
216
+ algorithm=hashes.SHA256(),
217
+ label=None,
218
+ ),
219
+ )
220
+ for part in encrypted_message.split('__')
221
+ ]
222
+ return private_key, b''.join(decrypted_parts).decode('utf-8')
223
+
224
+
225
+ def sign_message(private_key_pem: Path | bytes | types.PrivateKeyTypes, message: str | bytes,
226
+ *, padding: SignPadding = SignPadding.PSS, digest_sign: bool = False) \
227
+ -> tuple[types.PrivateKeyTypes, str]:
228
+ """
229
+ 私钥加签
230
+ :param private_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
231
+ :param padding:签名加密标准
232
+ :param digest_sign: 是否对message先进行哈希得到摘要信息,然后再对摘要信息加签
233
+ """
234
+ if isinstance(message, str):
235
+ message = message.encode('utf-8')
236
+ if digest_sign:
237
+ # 消息摘要
238
+ digest_hasher = hashes.Hash(hashes.SHA256(), backend=default_backend())
239
+ digest_hasher.update(message)
240
+ message = digest_hasher.finalize()
241
+ # 加载私钥
242
+ private_key = load_private_key(private_key_pem)
243
+ # 使用私钥签名
244
+ # types.rsa.RSAPrivateKey.sign()
245
+ sign_message = private_key.sign(
246
+ data=message,
247
+ padding=padding.value,
248
+ algorithm=hashes.SHA256()
249
+ )
250
+ return private_key, base64.b64encode(sign_message).decode('utf-8')
251
+
252
+
253
+ def verify_message(public_key_pem: Path | bytes | types.PublicKeyTypes, message: str | bytes, sign: str | bytes,
254
+ *, padding: SignPadding = SignPadding.PSS, digest_sign: bool = False) \
255
+ -> tuple[types.PublicKeyTypes, bool]:
256
+ """
257
+ :param public_key_pem: 支持秘钥文件路径、秘钥字节流、秘钥对象
258
+ :param sign: 需要验证的签名
259
+ :param padding:签名加密标准
260
+ :param digest_sign: 是否对message先进行哈希得到摘要信息,然后再对摘要信息加签
261
+ """
262
+ if isinstance(message, str):
263
+ message = message.encode('utf-8')
264
+ # 可视签名解码
265
+ sign = base64.b64decode(sign)
266
+ if digest_sign:
267
+ # 消息摘要
268
+ digest_hasher = hashes.Hash(hashes.SHA256(), backend=default_backend())
269
+ digest_hasher.update(message)
270
+ message = digest_hasher.finalize()
271
+ # 加载公钥
272
+ public_key = load_public_key(public_key_pem)
273
+ # 使用公钥验签
274
+ try:
275
+ # types.rsa.RSAPublicKey.verify()
276
+ public_key.verify(
277
+ sign,
278
+ message,
279
+ padding.value,
280
+ hashes.SHA256()
281
+ )
282
+ verified = True
283
+ except InvalidSignature:
284
+ verified = False
285
+ return public_key, verified
@@ -0,0 +1,15 @@
1
+ from enum import Enum
2
+
3
+ from cryptography.hazmat.primitives import hashes
4
+ from cryptography.hazmat.primitives.asymmetric import padding
5
+
6
+
7
+ class SignPadding(Enum):
8
+ """
9
+ 签名加密标准
10
+ """
11
+ PSS = padding.PSS(
12
+ mgf=padding.MGF1(hashes.SHA256()),
13
+ salt_length=padding.PSS.MAX_LENGTH
14
+ )
15
+ PKCS1 = padding.PKCS1v15()
oxy/utils/codec/des.py ADDED
@@ -0,0 +1,66 @@
1
+ import codecs
2
+
3
+ from Crypto.Cipher import DES
4
+ from Crypto.Cipher._mode_cbc import CbcMode
5
+ from Crypto.Cipher._mode_cfb import CfbMode
6
+ from Crypto.Cipher._mode_ctr import CtrMode
7
+ from Crypto.Cipher._mode_eax import EaxMode
8
+ from Crypto.Cipher._mode_ecb import EcbMode
9
+ from Crypto.Cipher._mode_ofb import OfbMode
10
+ from Crypto.Cipher._mode_openpgp import OpenPgpMode
11
+ from Crypto.Util.Padding import pad, unpad
12
+
13
+
14
+ def encrypt(message: str | bytes, *, key: str | bytes, iv: str | bytes = None, mode: int = None) -> str:
15
+ """
16
+ DES加密
17
+ :param message: 加密数据
18
+ :param key: 加密key
19
+ :param iv: 加密iv
20
+ :param mode: 模式
21
+ :return:
22
+ """
23
+ if message is None:
24
+ raise Exception('请输入需要加密的报文')
25
+ if isinstance(message, str):
26
+ message = message.encode('utf-8')
27
+ # 创建DES对象
28
+ des = _instance(key=key, iv=iv, mode=mode)
29
+ # 填充
30
+ padded_message = pad(message, DES.block_size)
31
+ # 加密
32
+ return codecs.encode(des.encrypt(padded_message), 'hex').decode('utf-8')
33
+
34
+
35
+ def decrypt(encrypted_message: str | bytes, *, key: str | bytes, iv: str | bytes = None, mode: int = None) -> str:
36
+ """
37
+ DES解密
38
+ :param encrypted_message: 待解密数据
39
+ :param key: 加密key
40
+ :param iv: 加密iv
41
+ :param mode: 模式
42
+ :return:
43
+ """
44
+ if encrypted_message is None:
45
+ raise Exception('请输入需要解密的报文')
46
+ if isinstance(encrypted_message, str):
47
+ encrypted_message = encrypted_message.encode('utf-8')
48
+ # 创建DES对象
49
+ des = _instance(key=key, iv=iv, mode=mode)
50
+ # 解密
51
+ padded_message = des.decrypt(codecs.decode(encrypted_message, 'hex'))
52
+ # 去掉填充
53
+ message = unpad(padded_message, DES.block_size)
54
+ return message.decode('utf-8')
55
+
56
+
57
+ def _instance(*, key: str | bytes, iv: str | bytes = None, mode: int = None) \
58
+ -> EcbMode | CbcMode | CfbMode | OfbMode | CtrMode | OpenPgpMode | EaxMode:
59
+ if key is None:
60
+ raise Exception('请输入加密秘钥')
61
+ if isinstance(key, str):
62
+ key = bytes(key.encode('utf-8'))[:8]
63
+ if isinstance(iv, str):
64
+ iv = iv.encode('utf-8')
65
+ # 创建DES对象
66
+ return DES.new(key=key, iv=iv, mode=DES.MODE_CBC if mode is None else mode)
oxy/utils/codec/md5.py ADDED
@@ -0,0 +1,10 @@
1
+ import hashlib
2
+
3
+
4
+ def encrypt(message: str | bytes):
5
+ if isinstance(message, str):
6
+ message = message.encode('utf-8')
7
+ # 创建一个md5对象
8
+ md5 = hashlib.md5()
9
+ md5.update(message)
10
+ return md5.hexdigest()
@@ -0,0 +1,21 @@
1
+ import hashlib
2
+ from pathlib import Path
3
+
4
+
5
+ def encrypt(message: str | bytes):
6
+ if isinstance(message, str):
7
+ message = message.encode('utf-8')
8
+ sha256 = hashlib.sha256()
9
+ sha256.update(message)
10
+ return sha256.hexdigest()
11
+
12
+
13
+ def encrypt_file(file: Path):
14
+ if file is None or not file.exists():
15
+ raise Exception('文件不存在')
16
+ sha256 = hashlib.sha256()
17
+ # 打开文件并读取内容,同时更新哈希对象
18
+ with open(file, 'rb') as file: # 以二进制读取模式打开文件
19
+ while chunk := file.read(4096): # 每次读取4096字节,直到文件末尾
20
+ sha256.update(chunk)
21
+ return sha256.hexdigest()
File without changes
@@ -0,0 +1,31 @@
1
+ import pandas as pd
2
+
3
+
4
+ def split(
5
+ data: pd.DataFrame,
6
+ delta: int,
7
+ ) -> list[pd.DataFrame]:
8
+ """
9
+ 如果是集合:[e1, e2, e3, ..., en] ==> [[e1, e2], [e3, e4], ..., [en-1, en]] ;
10
+ 如果是范围:[开始,结束) ==> [开始,开始+30),[开始+30,开始+60),...,[开始+30*N,结束) ;
11
+ """
12
+ if data is None:
13
+ return []
14
+ if isinstance(data, pd.DataFrame):
15
+ return _split_data_frame(data, delta)
16
+ raise TypeError('暂不支持的操作类型')
17
+
18
+
19
+ def _split_data_frame(data: pd.DataFrame, group_size: int) -> list[pd.DataFrame]:
20
+ if data is None or data.empty:
21
+ return []
22
+ idx = 0
23
+ page = group_size
24
+ all_data = []
25
+ while True:
26
+ idx += 1
27
+ part = data.iloc[page * (idx - 1): page * idx]
28
+ if part.empty:
29
+ break
30
+ all_data.append(part)
31
+ return all_data
@@ -0,0 +1 @@
1
+ from oxy.utils.data.cache._cache import *
@@ -0,0 +1,30 @@
1
+ from functools import wraps
2
+ from os import PathLike
3
+ from pathlib import Path
4
+
5
+ import pandas as pd
6
+
7
+
8
+ def cached_excel(file: str | PathLike[str], *, home: bool = False):
9
+ """
10
+ 表格文件缓存
11
+ """
12
+ def decorator(func):
13
+ @wraps(func)
14
+ def wrapper(*args, **kwargs):
15
+ nonlocal file
16
+ nonlocal home
17
+
18
+ if home:
19
+ file = Path.home().joinpath(file)
20
+ if isinstance(file, str):
21
+ file = Path(file)
22
+ if not file.exists():
23
+ result = func(*args, **kwargs)
24
+ if isinstance(result, pd.DataFrame):
25
+ result.to_excel(file, index=False)
26
+ else:
27
+ result = pd.read_excel(file)
28
+ return result
29
+ return wrapper
30
+ return decorator
@@ -0,0 +1,37 @@
1
+ from oxy.utils.http._model import (
2
+ ContentType,
3
+ ContentTypeName,
4
+ HttpRequest,
5
+ HttpRequestExtra,
6
+ HttpResponse,
7
+ HttpUrl,
8
+ MediaType,
9
+ )
10
+ from oxy.utils.http.api import (
11
+ ResponseProcessor,
12
+ make_response_processors,
13
+ origin_response,
14
+ get,
15
+ get_json,
16
+ post,
17
+ post_json,
18
+ )
19
+ from oxy.utils.http import api
20
+
21
+ __all__ = [
22
+ 'api',
23
+ 'ResponseProcessor',
24
+ 'make_response_processors',
25
+ 'origin_response',
26
+ 'get',
27
+ 'get_json',
28
+ 'post',
29
+ 'post_json',
30
+ 'HttpResponse',
31
+ 'HttpRequest',
32
+ 'HttpRequestExtra',
33
+ 'HttpUrl',
34
+ 'ContentType',
35
+ 'ContentTypeName',
36
+ 'MediaType',
37
+ ]