pyscriptbase 1.1.6__tar.gz → 1.2.0__tar.gz

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 (24) hide show
  1. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/PKG-INFO +8 -2
  2. pyscriptbase-1.2.0/pyscriptbase/ocr.py +109 -0
  3. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase.egg-info/PKG-INFO +8 -2
  4. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase.egg-info/SOURCES.txt +3 -1
  5. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase.egg-info/requires.txt +1 -0
  6. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/setup.py +2 -1
  7. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/test/test.py +1 -1
  8. pyscriptbase-1.2.0/test/test_ocr.py +92 -0
  9. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/README.md +0 -0
  10. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/__init__.py +0 -0
  11. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/app.py +0 -0
  12. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/cipher.py +0 -0
  13. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/database.py +0 -0
  14. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/env.py +0 -0
  15. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/net.py +0 -0
  16. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/panel.py +0 -0
  17. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/pusher.py +0 -0
  18. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/script.py +0 -0
  19. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/sms.py +0 -0
  20. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/util.py +0 -0
  21. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase/webview.py +0 -0
  22. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase.egg-info/dependency_links.txt +0 -0
  23. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/pyscriptbase.egg-info/top_level.txt +0 -0
  24. {pyscriptbase-1.1.6 → pyscriptbase-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyscriptbase
3
- Version: 1.1.6
3
+ Version: 1.2.0
4
4
  Summary: python script base library
5
5
  Home-page:
6
6
  Author: ASMan
@@ -19,6 +19,12 @@ Requires-Dist: beautifulsoup4
19
19
  Requires-Dist: requests
20
20
  Requires-Dist: lxml
21
21
  Requires-Dist: brotli
22
+ Requires-Dist: pyperclip
23
+ Dynamic: author
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: requires-dist
27
+ Dynamic: summary
22
28
 
23
29
  # Instructions
24
30
 
@@ -0,0 +1,109 @@
1
+ # encoding=utf8
2
+
3
+ from urllib.parse import urlsplit
4
+
5
+ import requests
6
+
7
+ from . import env as Env
8
+
9
+
10
+ class OCRError(RuntimeError):
11
+ """
12
+ OCR 服务返回异常或无效响应
13
+ """
14
+
15
+
16
+ class OCRClient:
17
+ def __init__(
18
+ self, host: str = None, ocrHost: str = None, timeout: float = 30
19
+ ) -> None:
20
+ """
21
+ @host:pyai 服务地址,默认读取 PYAI_HOST,未配置时使用 http://192.168.8.103:8000
22
+ @ocrHost:pyocr 服务地址,默认读取 PYOCR_HOST,调用文字识别前需配置
23
+ @timeout:请求超时时间,单位为秒
24
+ """
25
+ if host is None:
26
+ host = Env.get("PYAI_HOST", "http://192.168.8.103:8000")
27
+ if ocrHost is None:
28
+ ocrHost = Env.get("PYOCR_HOST")
29
+ self.host = self.__validAddress__(host)
30
+ self.ocrHost = self.__validAddress__(ocrHost) if ocrHost else None
31
+ self.timeout = timeout
32
+
33
+ def __validAddress__(self, address: str) -> str:
34
+ address = address.strip()
35
+ if not address:
36
+ raise ValueError("OCR 服务地址不能为空")
37
+ if "://" not in address:
38
+ address = f"http://{address}"
39
+ parsed = urlsplit(address)
40
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
41
+ raise ValueError("OCR 服务地址必须为有效的 HTTP 或 HTTPS 地址")
42
+ if parsed.query or parsed.fragment:
43
+ raise ValueError("OCR 服务地址不能包含查询参数或片段")
44
+ return address.rstrip("/")
45
+
46
+ def __request__(self, host: str, path: str, data: dict):
47
+ response = requests.post(
48
+ url=f"{host}{path}", json=data, timeout=self.timeout
49
+ )
50
+ response.raise_for_status()
51
+ try:
52
+ result = response.json()
53
+ except ValueError as e:
54
+ raise OCRError("OCR 服务返回的不是有效 JSON") from e
55
+ if not isinstance(result, dict):
56
+ raise OCRError("OCR 服务返回格式错误")
57
+ return result
58
+
59
+ def classification(self, b64Data: str, model: str = "tencent") -> dict:
60
+ """
61
+ 验证码识别,model 支持 tencent(腾讯数据模型)和 origin(原始模型)
62
+ """
63
+ if model not in ("tencent", "origin"):
64
+ raise ValueError("验证码模型仅支持 tencent 和 origin")
65
+ return self.__request__(
66
+ self.host, f"/api/recognize/classification/{model}", {"data": b64Data}
67
+ )
68
+
69
+ def getPredictCode(self, b64Data: str) -> dict:
70
+ """
71
+ 腾讯模型验证码识别,返回完整 JSON 响应
72
+ """
73
+ return self.classification(b64Data)
74
+
75
+ def rotate(self, b64Data: str) -> dict:
76
+ """
77
+ 识别图片旋转角度,返回完整 JSON 响应
78
+ """
79
+ return self.__request__(
80
+ self.host, "/api/recognize/rotate", {"data": b64Data}
81
+ )
82
+
83
+ def match(self, target: str, background: str) -> dict:
84
+ """
85
+ 背景缺口匹配,target 和 background 均为 Base64 图片,返回完整 JSON 响应
86
+ """
87
+ return self.__request__(
88
+ self.host,
89
+ "/api/recognize/match/origin",
90
+ {"target": target, "background": background},
91
+ )
92
+
93
+ def detect(self, b64Data: str) -> dict:
94
+ """
95
+ 检测图片中的文字区域,返回完整 JSON 响应
96
+ """
97
+ return self.__request__(
98
+ self.host, "/api/recognize/detect/origin", {"data": b64Data}
99
+ )
100
+
101
+ def ocr(self, b64Data: str) -> dict:
102
+ """
103
+ pyocr 文字识别,返回完整 JSON 响应,保留 cost 等字段
104
+ """
105
+ if not self.ocrHost:
106
+ raise ValueError("文字识别需要配置 pyocr 服务地址 ocrHost")
107
+ return self.__request__(
108
+ self.ocrHost, "/api/paddlex/orc", {"data": b64Data}
109
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyscriptbase
3
- Version: 1.1.6
3
+ Version: 1.2.0
4
4
  Summary: python script base library
5
5
  Home-page:
6
6
  Author: ASMan
@@ -19,6 +19,12 @@ Requires-Dist: beautifulsoup4
19
19
  Requires-Dist: requests
20
20
  Requires-Dist: lxml
21
21
  Requires-Dist: brotli
22
+ Requires-Dist: pyperclip
23
+ Dynamic: author
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: requires-dist
27
+ Dynamic: summary
22
28
 
23
29
  # Instructions
24
30
 
@@ -6,6 +6,7 @@ pyscriptbase/cipher.py
6
6
  pyscriptbase/database.py
7
7
  pyscriptbase/env.py
8
8
  pyscriptbase/net.py
9
+ pyscriptbase/ocr.py
9
10
  pyscriptbase/panel.py
10
11
  pyscriptbase/pusher.py
11
12
  pyscriptbase/script.py
@@ -17,4 +18,5 @@ pyscriptbase.egg-info/SOURCES.txt
17
18
  pyscriptbase.egg-info/dependency_links.txt
18
19
  pyscriptbase.egg-info/requires.txt
19
20
  pyscriptbase.egg-info/top_level.txt
20
- test/test.py
21
+ test/test.py
22
+ test/test_ocr.py
@@ -11,3 +11,4 @@ beautifulsoup4
11
11
  requests
12
12
  lxml
13
13
  brotli
14
+ pyperclip
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyscriptbase", # 包名
5
- version="1.1.6", # 版本号
5
+ version="1.2.0", # 版本号
6
6
  packages=find_packages(), # 自动查找包
7
7
  author="ASMan",
8
8
  author_email="",
@@ -24,5 +24,6 @@ setup(
24
24
  "requests",
25
25
  "lxml",
26
26
  "brotli",
27
+ "pyperclip",
27
28
  ], # 依赖包列表
28
29
  )
@@ -1,7 +1,7 @@
1
1
  from pyscriptbase.pusher import SMTPParams, sendMail, HtmlTableEmail
2
2
 
3
3
  if __name__ == "__main__":
4
- params = SMTPParams("smtp.qq.com", 587, "2328697997@qq.com", "tbivvqolbinoecij")
4
+ params = SMTPParams("smtp.qq.com", 587, "dotoolsender@qq.com", "swecxgotxpfxdahe")
5
5
  table = HtmlTableEmail("Pupu", ["备注", "朴分", "红包"])
6
6
  table.rows.append(["测试", "100", "3.54"])
7
7
  table.rows.append(["测试", "100", "3.54"])
@@ -0,0 +1,92 @@
1
+ import os
2
+ import unittest
3
+ from unittest.mock import Mock, patch
4
+
5
+ import requests
6
+
7
+ from pyscriptbase.ocr import OCRClient, OCRError
8
+
9
+
10
+ class OCRClientTest(unittest.TestCase):
11
+ @patch.dict(os.environ, {}, clear=True)
12
+ def test_default_host(self):
13
+ self.assertEqual(OCRClient().host, "http://192.168.8.103:8000")
14
+
15
+ @patch.dict(os.environ, {"PYAI_HOST": "https://ai.example/", "PYOCR_HOST": "https://ocr.example/"})
16
+ def test_environment_and_explicit_hosts(self):
17
+ client = OCRClient()
18
+ self.assertEqual(client.host, "https://ai.example")
19
+ self.assertEqual(client.ocrHost, "https://ocr.example")
20
+ client = OCRClient("localhost:8000/", "https://custom.example/")
21
+ self.assertEqual(client.host, "http://localhost:8000")
22
+ self.assertEqual(client.ocrHost, "https://custom.example")
23
+
24
+ @patch("pyscriptbase.ocr.requests.post")
25
+ def test_routes(self, post):
26
+ client = OCRClient("https://ai.example/", "https://ocr.example/", timeout=12)
27
+ cases = [
28
+ (lambda: client.classification("image"), "https://ai.example/api/recognize/classification/tencent", {"data": "image"}, "abcd"),
29
+ (lambda: client.classification("image", "origin"), "https://ai.example/api/recognize/classification/origin", {"data": "image"}, "1234"),
30
+ (lambda: client.getPredictCode("image"), "https://ai.example/api/recognize/classification/tencent", {"data": "image"}, "abcd"),
31
+ (lambda: client.rotate("image"), "https://ai.example/api/recognize/rotate", {"data": "image"}, 0),
32
+ (lambda: client.match("target", "background"), "https://ai.example/api/recognize/match/origin", {"target": "target", "background": "background"}, {"target": [1, 2, 3, 4]}),
33
+ (lambda: client.detect("image"), "https://ai.example/api/recognize/detect/origin", {"data": "image"}, []),
34
+ (lambda: client.ocr("image"), "https://ocr.example/api/paddlex/orc", {"data": "image"}, [{"text": "文字", "score": 0.99, "x": 1, "y": 2, "w": 3, "h": 4}]),
35
+ ]
36
+ for call, url, data, expected in cases:
37
+ with self.subTest(url=url):
38
+ post.return_value = Mock()
39
+ result = {"code": 0, "data": expected, "msg": "", "cost": 0.25}
40
+ post.return_value.json.return_value = result
41
+ self.assertIs(call(), result)
42
+ post.assert_called_with(url=url, json=data, timeout=12)
43
+ post.return_value.raise_for_status.assert_called_once_with()
44
+
45
+ @patch("pyscriptbase.ocr.requests.post")
46
+ def test_invalid_responses(self, post):
47
+ client = OCRClient()
48
+ for result in [{"code": 200, "data": None, "msg": "not ready"}, {"code": 201}, {"data": "abcd"}, {"code": 0}, {"code": 0, "data": None}]:
49
+ with self.subTest(result=result):
50
+ post.return_value.json.return_value = result
51
+ self.assertIs(client.classification("image"), result)
52
+ self.assertIs(client.getPredictCode("image"), result)
53
+ post.return_value.json.return_value = []
54
+ with self.assertRaises(OCRError):
55
+ client.classification("image")
56
+ post.return_value.json.side_effect = ValueError("无效 JSON")
57
+ with self.assertRaises(OCRError):
58
+ client.classification("image")
59
+ with self.assertRaises(OCRError):
60
+ client.getPredictCode("image")
61
+
62
+ @patch("pyscriptbase.ocr.requests.post")
63
+ def test_request_failures(self, post):
64
+ client = OCRClient()
65
+ post.side_effect = requests.Timeout()
66
+ with self.assertRaises(requests.Timeout):
67
+ client.rotate("image")
68
+ with self.assertRaises(requests.Timeout):
69
+ client.getPredictCode("image")
70
+ post.side_effect = None
71
+ post.return_value.raise_for_status.side_effect = requests.HTTPError()
72
+ with self.assertRaises(requests.HTTPError):
73
+ client.classification("image")
74
+ with self.assertRaises(requests.HTTPError):
75
+ client.getPredictCode("image")
76
+
77
+ @patch.dict(os.environ, {}, clear=True)
78
+ @patch("pyscriptbase.ocr.requests.post")
79
+ def test_invalid_configuration(self, post):
80
+ for host in ["", " ", "ftp://example", "https://", "https://example?x=1"]:
81
+ with self.subTest(host=host), self.assertRaises(ValueError):
82
+ OCRClient(host)
83
+ client = OCRClient()
84
+ with self.assertRaises(ValueError):
85
+ client.ocr("image")
86
+ with self.assertRaises(ValueError):
87
+ client.classification("image", "unknown")
88
+ post.assert_not_called()
89
+
90
+
91
+ if __name__ == "__main__":
92
+ unittest.main()
File without changes
File without changes