jwf-common-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 (60) hide show
  1. jwf_common_python/__init__.py +14 -0
  2. jwf_common_python/db_utils/__init__.py +6 -0
  3. jwf_common_python/db_utils/clickhouse.py +124 -0
  4. jwf_common_python/db_utils/elasticsearch.py +196 -0
  5. jwf_common_python/db_utils/mysql.py +64 -0
  6. jwf_common_python/eudic/__init__.py +5 -0
  7. jwf_common_python/eudic/wordbook.py +211 -0
  8. jwf_common_python/file_storage/__init__.py +2 -0
  9. jwf_common_python/file_storage/file_interface.py +29 -0
  10. jwf_common_python/file_storage/local.py +50 -0
  11. jwf_common_python/file_storage/tencent_cos.py +65 -0
  12. jwf_common_python/geography/__init__.py +8 -0
  13. jwf_common_python/geography/district_amap.py +181 -0
  14. jwf_common_python/geography/search_amap.py +84 -0
  15. jwf_common_python/github/__init__.py +5 -0
  16. jwf_common_python/github/webhooks.py +87 -0
  17. jwf_common_python/language_utils/__init__.py +34 -0
  18. jwf_common_python/language_utils/core.py +203 -0
  19. jwf_common_python/language_utils/files.py +52 -0
  20. jwf_common_python/language_utils/web.py +117 -0
  21. jwf_common_python/llm/__init__.py +1 -0
  22. jwf_common_python/llm/aliyun_qwen.py +10 -0
  23. jwf_common_python/llm/azure_gpt.py +66 -0
  24. jwf_common_python/llm/baidu_ernie.py +112 -0
  25. jwf_common_python/message_queue/__init__.py +7 -0
  26. jwf_common_python/message_queue/interface.py +49 -0
  27. jwf_common_python/message_queue/kafka.py +145 -0
  28. jwf_common_python/message_queue/rabbitmq.py +113 -0
  29. jwf_common_python/oauth2/__init__.py +0 -0
  30. jwf_common_python/oauth2/google.py +158 -0
  31. jwf_common_python/parse_subtitle/__init__.py +6 -0
  32. jwf_common_python/parse_subtitle/aa.vtt +2364 -0
  33. jwf_common_python/parse_subtitle/parse_srt_file.py +60 -0
  34. jwf_common_python/pdf/__init__.py +0 -0
  35. jwf_common_python/pdf/aaa.py +38 -0
  36. jwf_common_python/pdf/local_parse.py +259 -0
  37. jwf_common_python/sms/__init__.py +1 -0
  38. jwf_common_python/sms/interface.py +7 -0
  39. jwf_common_python/sms/tencent_sms.py +117 -0
  40. jwf_common_python/translate/__init__.py +152 -0
  41. jwf_common_python/translate/_youdao.py +234 -0
  42. jwf_common_python/translate/microsoft.py +696 -0
  43. jwf_common_python/translate/tencent.py +194 -0
  44. jwf_common_python/tts/__init__.py +0 -0
  45. jwf_common_python/tts/microsoft.py +87 -0
  46. jwf_common_python/utils/__init__.py +0 -0
  47. jwf_common_python/utils/image_utils.py +18 -0
  48. jwf_common_python/utils/tencent_utils.py +51 -0
  49. jwf_common_python/utils/utils.py +46 -0
  50. jwf_common_python/video/__init__.py +5 -0
  51. jwf_common_python/video/merge.py +276 -0
  52. jwf_common_python/weather/__init__.py +0 -0
  53. jwf_common_python/weather/amap.py +127 -0
  54. jwf_common_python/weather/wanwei.py +58 -0
  55. jwf_common_python/youdao/__init__.py +13 -0
  56. jwf_common_python/youdao/wordbook.py +88 -0
  57. jwf_common_python-1.0.0.dist-info/METADATA +669 -0
  58. jwf_common_python-1.0.0.dist-info/RECORD +60 -0
  59. jwf_common_python-1.0.0.dist-info/WHEEL +4 -0
  60. jwf_common_python-1.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,145 @@
1
+ """基于 kafka-python 的同步生产和消费。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from .interface import MessageCallback, MessageQueue
9
+
10
+
11
+ class KafkaQueue(MessageQueue):
12
+ def __init__(
13
+ self,
14
+ bootstrap_servers: list[str],
15
+ topic: str,
16
+ *,
17
+ group_id: str | None = None,
18
+ timeout: float = 10,
19
+ producer_config: dict[str, Any] | None = None,
20
+ consumer_config: dict[str, Any] | None = None,
21
+ ) -> None:
22
+ """连接按需创建,消费时必须提供 group_id。
23
+
24
+ config 参数透传 SDK 配置,例如 SSL/SASL;连接地址、消费组和
25
+ 手动确认配置由本类管理,不能通过 config 覆盖。
26
+ """
27
+ super().__init__()
28
+ if not isinstance(bootstrap_servers, list) or not bootstrap_servers:
29
+ raise ValueError("bootstrap_servers 必须为非空 list[str]")
30
+ if not all(
31
+ isinstance(server, str) and server.strip() for server in bootstrap_servers
32
+ ):
33
+ raise ValueError("bootstrap_servers 必须为非空 list[str]")
34
+ if not isinstance(topic, str) or not topic.strip():
35
+ raise ValueError("topic 不能为空")
36
+ if group_id is not None and (
37
+ not isinstance(group_id, str) or not group_id.strip()
38
+ ):
39
+ raise ValueError("group_id 不能为空")
40
+ if timeout <= 0:
41
+ raise ValueError("timeout 必须大于 0")
42
+ self._producer_config = dict(producer_config or {})
43
+ self._consumer_config = dict(consumer_config or {})
44
+ if "bootstrap_servers" in self._producer_config:
45
+ raise ValueError("请通过 bootstrap_servers 参数设置连接地址")
46
+ if {
47
+ "bootstrap_servers",
48
+ "group_id",
49
+ "enable_auto_commit",
50
+ } & self._consumer_config.keys():
51
+ raise ValueError("consumer_config 不能覆盖连接地址、消费组或手动确认配置")
52
+
53
+ try:
54
+ import kafka
55
+ from kafka.structs import OffsetAndMetadata, TopicPartition
56
+ except ImportError as exc:
57
+ raise ImportError("请先安装 Kafka 依赖:pip install kafka-python") from exc
58
+
59
+ self._kafka = kafka
60
+ self._offset_metadata = OffsetAndMetadata
61
+ self._topic_partition = TopicPartition
62
+ self.bootstrap_servers = list(bootstrap_servers)
63
+ self.topic = topic
64
+ self.group_id = group_id
65
+ self.timeout = timeout
66
+ self._producer = None
67
+
68
+ @property
69
+ def producer(self):
70
+ """首次发送或访问此属性时创建生产者。"""
71
+ self._ensure_open()
72
+ if self._producer is None:
73
+ config = {
74
+ "acks": "all",
75
+ "retries": 5,
76
+ "max_in_flight_requests_per_connection": 1,
77
+ **self._producer_config,
78
+ }
79
+ self._producer = self._kafka.KafkaProducer(
80
+ bootstrap_servers=self.bootstrap_servers, **config
81
+ )
82
+ return self._producer
83
+
84
+ def enqueue(self, item: bytes, *, key: bytes | None = None) -> dict[str, Any]:
85
+ """同步发送并返回 topic、partition、offset;支持二进制内容和空 key。"""
86
+ self._ensure_open()
87
+ self._validate_item(item)
88
+ if key is not None and not isinstance(key, bytes):
89
+ raise TypeError("key 必须为 bytes 或 None")
90
+ metadata = self.producer.send(self.topic, value=item, key=key).get(
91
+ timeout=self.timeout
92
+ )
93
+ return {
94
+ "topic": metadata.topic,
95
+ "partition": metadata.partition,
96
+ "offset": metadata.offset,
97
+ }
98
+
99
+ def enqueue_json(self, message: Any, *, key: bytes | None = None) -> dict[str, Any]:
100
+ """发送 UTF-8 JSON,等待确认并返回 topic、partition、offset。
101
+
102
+ 不可序列化的内容或 NaN/Infinity 在连接前报错;不能同时配置
103
+ value_serializer,避免对已编码 JSON 二次序列化。None 发送为 JSON null。
104
+ """
105
+ self._ensure_open()
106
+ if self._producer_config.get("value_serializer") is not None:
107
+ raise ValueError("enqueue_json 负责序列化,不能再配置 value_serializer")
108
+ payload = json.dumps(
109
+ message, ensure_ascii=False, allow_nan=False, separators=(",", ":")
110
+ ).encode("utf-8")
111
+ return self.enqueue(payload, key=key)
112
+
113
+ def consumer(self, callback: MessageCallback) -> None:
114
+ self._ensure_open()
115
+ if not callable(callback):
116
+ raise TypeError("callback 必须可调用")
117
+ if self.group_id is None:
118
+ raise ValueError("消费时必须提供 group_id,用于提交消费进度")
119
+ client = self._kafka.KafkaConsumer(
120
+ self.topic,
121
+ bootstrap_servers=self.bootstrap_servers,
122
+ group_id=self.group_id,
123
+ enable_auto_commit=False,
124
+ **self._consumer_config,
125
+ )
126
+ try:
127
+ for message in client:
128
+ callback(message.value)
129
+ # 只提交已处理消息所在分区的下一条 offset,避免确认预取的其他消息。
130
+ partition = self._topic_partition(message.topic, message.partition)
131
+ offset = {"offset": message.offset + 1, "metadata": ""}
132
+ # kafka-python 2.1+ 的 OffsetAndMetadata 新增了必填 leader_epoch。
133
+ if "leader_epoch" in self._offset_metadata._fields:
134
+ offset["leader_epoch"] = getattr(message, "leader_epoch", -1)
135
+ client.commit(offsets={partition: self._offset_metadata(**offset)})
136
+ finally:
137
+ client.close(autocommit=False)
138
+
139
+ def close(self) -> None:
140
+ if not self._closed:
141
+ try:
142
+ if self._producer is not None:
143
+ self._producer.close(timeout=self.timeout)
144
+ finally:
145
+ self._closed = True
@@ -0,0 +1,113 @@
1
+ """基于 pika BlockingConnection 的同步生产和消费。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .interface import MessageCallback, MessageQueue
8
+
9
+
10
+ class RabbitmqQueue(MessageQueue):
11
+ def __init__(
12
+ self,
13
+ queue_name: str,
14
+ host: str,
15
+ port: int | str = 5672,
16
+ *,
17
+ username: str,
18
+ password: str,
19
+ durable: bool = True,
20
+ prefetch_count: int = 1,
21
+ requeue_on_error: bool = True,
22
+ connection_config: dict[str, Any] | None = None,
23
+ ) -> None:
24
+ """默认持久化队列和消息,并启用发布确认。
25
+
26
+ 若已有同名非持久化队列,应传 durable=False,与服务端保持一致。
27
+ connection_config 可传 virtual_host、SSL 等 pika 连接配置。
28
+ """
29
+ super().__init__()
30
+ if not isinstance(queue_name, str) or not queue_name.strip():
31
+ raise ValueError("queue_name 不能为空")
32
+ if not isinstance(host, str) or not host.strip():
33
+ raise ValueError("host 不能为空")
34
+ port = int(port)
35
+ if not 1 <= port <= 65535:
36
+ raise ValueError("port 必须在 1 到 65535 之间")
37
+ if not isinstance(prefetch_count, int) or prefetch_count <= 0:
38
+ raise ValueError("prefetch_count 必须为正整数")
39
+ config = dict(connection_config or {})
40
+ if {"host", "port", "credentials"} & config.keys():
41
+ raise ValueError("请通过独立参数设置 host、port、username 和 password")
42
+ try:
43
+ import pika
44
+ except ImportError as exc:
45
+ raise ImportError("请先安装 RabbitMQ 依赖:pip install pika") from exc
46
+
47
+ self.pika = pika
48
+ self.queue_name = queue_name
49
+ self.durable = durable
50
+ self.prefetch_count = prefetch_count
51
+ self.requeue_on_error = requeue_on_error
52
+ self.connect = pika.BlockingConnection(
53
+ pika.ConnectionParameters(
54
+ host=host,
55
+ port=port,
56
+ credentials=pika.PlainCredentials(username, password),
57
+ **{"blocked_connection_timeout": 30, **config},
58
+ )
59
+ )
60
+ try:
61
+ self.channel = self.connect.channel()
62
+ self.channel.queue_declare(queue=queue_name, durable=durable)
63
+ self.channel.confirm_delivery()
64
+ except BaseException:
65
+ self.close()
66
+ raise
67
+
68
+ def enqueue(self, item: bytes) -> None:
69
+ self._ensure_open()
70
+ self._validate_item(item)
71
+ self.channel.basic_publish(
72
+ exchange="",
73
+ routing_key=self.queue_name,
74
+ body=item,
75
+ properties=self.pika.BasicProperties(
76
+ delivery_mode=2 if self.durable else 1
77
+ ),
78
+ mandatory=True,
79
+ )
80
+
81
+ def consumer(self, callback: MessageCallback) -> None:
82
+ self._ensure_open()
83
+ if not callable(callback):
84
+ raise TypeError("callback 必须可调用")
85
+
86
+ def on_message(channel, method, properties, body):
87
+ try:
88
+ callback(body)
89
+ except Exception:
90
+ channel.basic_nack(
91
+ delivery_tag=method.delivery_tag, requeue=self.requeue_on_error
92
+ )
93
+ raise
94
+ else:
95
+ channel.basic_ack(delivery_tag=method.delivery_tag)
96
+
97
+ try:
98
+ self.channel.basic_qos(prefetch_count=self.prefetch_count)
99
+ self.channel.basic_consume(
100
+ queue=self.queue_name, on_message_callback=on_message, auto_ack=False
101
+ )
102
+ self.channel.start_consuming()
103
+ finally:
104
+ # 关闭连接也会使尚未确认的消息重新可消费,避免遗留活动消费者。
105
+ self.close()
106
+
107
+ def close(self) -> None:
108
+ if not self._closed:
109
+ try:
110
+ if self.connect.is_open:
111
+ self.connect.close()
112
+ finally:
113
+ self._closed = True
File without changes
@@ -0,0 +1,158 @@
1
+ import requests
2
+
3
+
4
+ class Oauth2Google:
5
+ def __init__(
6
+ self, client_id: str, client_secret: str, redirect_uri: str, scope: str
7
+ ):
8
+ self.client_id = client_id
9
+ self.client_secret = client_secret
10
+ self.response_type = "code"
11
+ self.access_type = "online"
12
+ self.grant_type = "authorization_code"
13
+ # 认证完成之后的回调地址,必须与google console中配置的一致
14
+ self.redirect_uri = redirect_uri
15
+ # 请求的权限
16
+ self.scope = scope
17
+
18
+ def get_web_auth_url(self) -> str:
19
+ """获取web页面上认证地址"""
20
+ url = f"https://accounts.google.com/o/oauth2/v2/auth?\
21
+ client_id={self.client_id}&response_type={self.response_type}&access_type={self.access_type}\
22
+ &redirect_uri={self.redirect_uri}&scope={self.scope}"
23
+ return url
24
+
25
+ def get_access_token(self, code: str) -> str:
26
+ """根据code获取access_token"""
27
+ url = "https://oauth2.googleapis.com/token"
28
+ data = {
29
+ "client_id": self.client_id,
30
+ "client_secret": self.client_secret,
31
+ "code": code,
32
+ "grant_type": self.grant_type,
33
+ "redirect_uri": self.redirect_uri,
34
+ }
35
+ response = requests.post(
36
+ url,
37
+ data=data,
38
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
39
+ )
40
+ if response.status_code != 200:
41
+ raise Exception(f"获取access_token失败 {response.status_code} {response.text}")
42
+ res = response.json()
43
+ access_token = res["access_token"]
44
+ return access_token
45
+
46
+ def get_raw_user_info(self, access_token: str) -> dict:
47
+ """获取原生的用户信息
48
+
49
+ email: 用户邮箱
50
+ name: 用户名字
51
+ avatar_content: 用户头像字节流
52
+ """
53
+ url = "https://people.googleapis.com/v1/people/me?personFields=\
54
+ names,emailAddresses,genders,nicknames,phoneNumbers,photos"
55
+ response = requests.get(
56
+ url, headers={"Authorization": f"Bearer {access_token}"}
57
+ )
58
+ if response.status_code != 200:
59
+ raise Exception(f"获取用户信息失败 {response.status_code} {response.text}")
60
+
61
+ res = response.json()
62
+ return res
63
+
64
+ def format_user_info(self, raw_user_info: dict) -> dict:
65
+ """格式化用户信息"""
66
+ _ = {
67
+ "resourceName": "people/110311852157832716656",
68
+ "etag": "%EgwBAxxxPj8axxxxBQc=",
69
+ "names": [
70
+ {
71
+ "metadata": {
72
+ "primary": True,
73
+ "source": {"type": "PROFILE", "id": "1185215783"},
74
+ "sourcePrimary": True,
75
+ },
76
+ "displayName": "姜xx",
77
+ "familyName": "姜",
78
+ "givenName": "xx",
79
+ "displayNameLastFirst": "姜xx",
80
+ "unstructuredName": "姜xx",
81
+ }
82
+ ],
83
+ "photos": [
84
+ {
85
+ "metadata": {
86
+ "primary": True,
87
+ "source": {"type": "PROFILE", "id": "1215783xx6"},
88
+ },
89
+ "url": "https://lh3.googleusercontent.com/a/\
90
+ ACg8ocLGxxxxkIpZx7vEfeD31o=s100",
91
+ }
92
+ ],
93
+ "emailAddresses": [
94
+ {
95
+ "metadata": {
96
+ "primary": True,
97
+ "verified": True,
98
+ "source": {"type": "ACCOUNT", "id": "11xxxxxx656"},
99
+ "sourcePrimary": True,
100
+ },
101
+ "value": "xxxx@gmail.com",
102
+ },
103
+ {
104
+ "metadata": {
105
+ "verified": True,
106
+ "source": {"type": "ACCOUNT", "id": "1103xxxxxx56"},
107
+ },
108
+ "value": "xxxx@xxx.com",
109
+ },
110
+ ],
111
+ }
112
+ # 1. 获取用户邮箱
113
+ email = None
114
+ emailAddresses = raw_user_info["emailAddresses"]
115
+ for email_info in emailAddresses:
116
+ # 获取主邮箱
117
+ metadata = email_info["metadata"]
118
+ if metadata.get("primary"):
119
+ # 当primary存在,且为True时,表示为主邮箱
120
+ email = email_info["value"]
121
+ break
122
+
123
+ # 2. 获取用户名字
124
+ name = None
125
+ names = raw_user_info["names"]
126
+ for name_info in names:
127
+ if name_info.get("displayName"):
128
+ # 当displayName存在时,表示为用户名
129
+ name = name_info["displayName"]
130
+ break
131
+
132
+ # 3. 获取用户头像
133
+ photo_url = None
134
+ photos = raw_user_info["photos"]
135
+ for photo_info in photos:
136
+ if photo_info.get("url"):
137
+ # 当url存在时,表示为用户头像
138
+ photo_url = photo_info["url"]
139
+ break
140
+ # 3.1 如果有头像,获取图片字节流
141
+ avatar_content = None
142
+ if photo_url:
143
+ response = requests.get(photo_url)
144
+ if response.status_code == 200:
145
+ avatar_content = response.content
146
+
147
+ return {
148
+ "email": email,
149
+ "name": name,
150
+ "avatar_content": avatar_content,
151
+ }
152
+
153
+ def get_user_info(self, code: str) -> dict:
154
+ """获取用户信息,格式化后返回"""
155
+ access_token = self.get_access_token(code)
156
+ raw_user_info = self.get_raw_user_info(access_token)
157
+ user_info = self.format_user_info(raw_user_info)
158
+ return user_info
@@ -0,0 +1,6 @@
1
+ # [参考](https://github.com/cdown/srt)
2
+ # [参考](https://srt.readthedocs.io/en/latest/quickstart.html#compose-an-srt-from-python-objects)
3
+
4
+ # 1. 清理非标准字符
5
+ # 2. 使用srt解析
6
+ # 3. 清理每个字幕中的非标准字符