1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 23:43:02 +08:00

Compare commits

...

13 Commits

Author SHA1 Message Date
KenanZhu 5c393595d7 fix(ALWebDriverDownloadDialog): 重命名信号避免与 QThread 内置信号冲突并改进线程生命周期管理 2026-03-21 01:53:22 +08:00
KenanZhu 4924f4b031 fix(WebDriverDownloader): 优化下载速度计算逻辑并改用时间间隔触发回调
- 将回调触发条件由进度变化量改为固定时间间隔(0.5s), 避免突发数据导致速度虚高

- 修正 total_size == 0 为 total_size <= 0, 完善边界判断

- 重命名变量提升可读性(last_time/last_size -> last_callback_time/last_callback_size)
2026-03-21 01:52:20 +08:00
KenanZhu 62c1ecdb07 fix(LogManager): 修复 CallerInfoFormatter 中 lineno 类型转换异常 2026-03-21 00:55:17 +08:00
KenanZhu aef28b6d5e feat(ALConfigWidget): 集成浏览器驱动自动下载功能到配置界面 2026-03-21 00:55:09 +08:00
KenanZhu afa1d39051 feat(gui): 新增 ALStatusLabel 状态标签组件和浏览器驱动下载对话框 2026-03-21 00:55:02 +08:00
KenanZhu 84cff6acc3 feat(WebDriverManager): 支持下载取消操作并完善异常处理 2026-03-21 00:54:49 +08:00
KenanZhu e40c7f4f3e chore(*): 降低 ddddocr 版本以避免不必要的打包体积,同时回滚工作流 2026-03-20 20:57:24 +08:00
KenanZhu c8e202dc8c ci(workflows): 修复构建工作流中的模型文件复制问题 2026-03-20 20:09:30 +08:00
KenanZhu 9a3abc365c fix(requirement.txt): 添加缺失的依赖项 pyinstaller 2026-03-20 19:26:17 +08:00
KenanZhu 6b2bf4863e chore(*): 更新项目依赖,并由此修改 CI/CD 工作流配置
- 更新项目依赖
2026-03-20 19:21:56 +08:00
KenanZhu 95aa2bb518 feat(WebDriverManager): 新增浏览器管理类 WebDriverManager
- 新增浏览器管理类,支持下载和管理浏览器驱动
2026-03-20 19:20:43 +08:00
KenanZhu 571af554d2 chore(Main.py): 使用 exec() 替换 exec_() 方法
- chore(Main.py): 使用 exec() 替换 exec_() 方法
2026-03-20 19:20:01 +08:00
KenanZhu 706fc889f9 chore(*): 重构项目结构
- 新增 src/boot 目录,用于存放启动时需要初始化的模块
- 新增 src/managers 目录,用于存放项目中的管理模块
- 新增 src/managers/config 目录,用于存放配置管理模块
- 新增 src/managers/log 目录,用于存放日志管理模块
- 新增 src/managers/driver 目录,用于存放浏览器驱动管理模块
- 修改对应文件中 import 导入路径
2026-03-20 19:19:34 +08:00
25 changed files with 2181 additions and 827 deletions
-15
View File
@@ -1,15 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ddddocr = "*"
selenium = "*"
pyinstaller = "*"
pyside6 = "*"
[dev-packages]
[requires]
python_version = "3.13"
Generated
-630
View File
@@ -1,630 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "26dffc26812d5328611959b95713a7ed65e20c08c60089b54283b0f406dd08e4"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.13"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"altgraph": {
"hashes": [
"sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406",
"sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"
],
"version": "==0.17.4"
},
"attrs": {
"hashes": [
"sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11",
"sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"
],
"markers": "python_version >= '3.9'",
"version": "==25.4.0"
},
"certifi": {
"hashes": [
"sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de",
"sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"
],
"markers": "python_version >= '3.7'",
"version": "==2025.10.5"
},
"cffi": {
"hashes": [
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
],
"markers": "python_version >= '3.9'",
"version": "==2.0.0"
},
"coloredlogs": {
"hashes": [
"sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934",
"sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==15.0.1"
},
"ddddocr": {
"hashes": [
"sha256:5991594d481d33ba0b136022e910f578d6d5b0ca536b44886591359622ab0c70",
"sha256:7c44b58ba7d7566d785c65b8526ec5b78efacd121e993dea4fda5f7966897428"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.0.6"
},
"flatbuffers": {
"hashes": [
"sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2",
"sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12"
],
"version": "==25.9.23"
},
"h11": {
"hashes": [
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
],
"markers": "python_version >= '3.8'",
"version": "==0.16.0"
},
"humanfriendly": {
"hashes": [
"sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477",
"sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==10.0"
},
"idna": {
"hashes": [
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
],
"markers": "python_version >= '3.8'",
"version": "==3.11"
},
"mpmath": {
"hashes": [
"sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
"sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
],
"version": "==1.3.0"
},
"numpy": {
"hashes": [
"sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64",
"sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e",
"sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0",
"sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365",
"sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d",
"sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c",
"sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52",
"sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36",
"sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec",
"sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f",
"sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197",
"sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7",
"sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9",
"sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37",
"sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a",
"sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db",
"sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c",
"sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7",
"sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d",
"sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e",
"sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f",
"sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a",
"sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16",
"sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e",
"sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868",
"sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05",
"sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e",
"sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff",
"sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f",
"sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7",
"sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f",
"sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e",
"sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562",
"sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6",
"sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0",
"sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26",
"sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0",
"sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d",
"sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879",
"sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef",
"sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29",
"sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252",
"sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847",
"sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6",
"sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32",
"sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0",
"sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3",
"sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b",
"sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3",
"sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc",
"sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc",
"sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda",
"sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a",
"sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40",
"sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032",
"sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7",
"sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966",
"sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9",
"sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346",
"sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2",
"sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a",
"sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786",
"sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f",
"sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc",
"sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb",
"sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646",
"sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd",
"sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1",
"sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11",
"sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667",
"sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996",
"sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953",
"sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b",
"sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"
],
"markers": "python_version >= '3.11'",
"version": "==2.3.4"
},
"onnxruntime": {
"hashes": [
"sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc",
"sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77",
"sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95",
"sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088",
"sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b",
"sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435",
"sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f",
"sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b",
"sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7",
"sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c",
"sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c",
"sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612",
"sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872",
"sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2",
"sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e",
"sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466",
"sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3",
"sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36",
"sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321",
"sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6",
"sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e",
"sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145"
],
"markers": "python_version >= '3.10'",
"version": "==1.23.2"
},
"outcome": {
"hashes": [
"sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8",
"sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.0.post0"
},
"packaging": {
"hashes": [
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
],
"markers": "python_version >= '3.8'",
"version": "==25.0"
},
"pefile": {
"hashes": [
"sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc",
"sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2023.2.7"
},
"pillow": {
"hashes": [
"sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643",
"sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e",
"sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e",
"sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc",
"sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642",
"sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6",
"sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1",
"sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b",
"sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399",
"sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba",
"sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad",
"sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47",
"sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739",
"sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b",
"sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f",
"sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10",
"sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52",
"sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d",
"sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b",
"sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a",
"sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9",
"sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d",
"sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098",
"sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905",
"sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b",
"sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3",
"sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371",
"sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953",
"sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01",
"sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca",
"sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e",
"sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7",
"sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27",
"sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082",
"sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e",
"sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d",
"sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8",
"sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a",
"sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad",
"sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3",
"sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a",
"sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d",
"sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353",
"sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee",
"sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b",
"sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b",
"sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a",
"sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7",
"sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef",
"sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a",
"sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a",
"sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257",
"sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07",
"sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4",
"sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c",
"sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c",
"sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4",
"sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe",
"sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8",
"sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5",
"sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6",
"sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e",
"sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8",
"sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e",
"sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275",
"sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3",
"sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76",
"sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227",
"sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9",
"sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5",
"sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79",
"sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca",
"sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa",
"sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b",
"sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e",
"sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197",
"sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab",
"sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79",
"sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2",
"sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363",
"sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0",
"sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e",
"sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782",
"sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925",
"sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0",
"sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b",
"sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced",
"sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c",
"sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344",
"sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9",
"sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"
],
"markers": "python_version >= '3.10'",
"version": "==12.0.0"
},
"protobuf": {
"hashes": [
"sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954",
"sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995",
"sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef",
"sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455",
"sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee",
"sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9",
"sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3",
"sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035",
"sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90",
"sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"
],
"markers": "python_version >= '3.9'",
"version": "==6.33.0"
},
"pycparser": {
"hashes": [
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
],
"markers": "python_version >= '3.8'",
"version": "==2.23"
},
"pyinstaller": {
"hashes": [
"sha256:0a48f55b85ff60f83169e10050f2759019cf1d06773ad1c4da3a411cd8751058",
"sha256:53559fe1e041a234f2b4dcc3288ea8bdd57f7cad8a6644e422c27bb407f3edef",
"sha256:6d5f8617f3650ff9ef893e2ab4ddbf3c0d23d0c602ef74b5df8fbef4607840c8",
"sha256:73ba72e04fcece92e32518bbb1e1fb5ac2892677943dfdff38e01a06e8742851",
"sha256:7fd1c785219a87ca747c21fa92f561b0d2926a7edc06d0a0fe37f3736e00bd7a",
"sha256:b1752488248f7899281b17ca3238eefb5410521291371a686a4f5830f29f52b3",
"sha256:b756ddb9007b8141c5476b553351f9d97559b8af5d07f9460869bfae02be26b0",
"sha256:ba618a61627ee674d6d68e5de084ba17c707b59a4f2a856084b3999bdffbd3f0",
"sha256:bc10eb1a787f99fea613509f55b902fbd2d8b73ff5f51ff245ea29a481d97d41",
"sha256:c8b7ef536711617e12fef4673806198872033fa06fa92326ad7fd1d84a9fa454",
"sha256:d0af8a401de792c233c32c44b16d065ca9ab8262ee0c906835c12bdebc992a64",
"sha256:d1ebf84d02c51fed19b82a8abb4df536923abd55bb684d694e1356e4ae2a0ce5"
],
"index": "pypi",
"markers": "python_version < '3.15' and python_version >= '3.8'",
"version": "==6.16.0"
},
"pyinstaller-hooks-contrib": {
"hashes": [
"sha256:56e972bdaad4e9af767ed47d132362d162112260cbe488c9da7fee01f228a5a6",
"sha256:ccbfaa49399ef6b18486a165810155e5a8d4c59b41f20dc5da81af7482aaf038"
],
"markers": "python_version >= '3.8'",
"version": "==2025.9"
},
"pyreadline3": {
"hashes": [
"sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7",
"sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"
],
"markers": "python_version >= '3.8'",
"version": "==3.5.4"
},
"pyside6": {
"hashes": [
"sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339",
"sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f",
"sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820",
"sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01",
"sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"
],
"index": "pypi",
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pyside6-addons": {
"hashes": [
"sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464",
"sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0",
"sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c",
"sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1",
"sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pyside6-essentials": {
"hashes": [
"sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a",
"sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8",
"sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1",
"sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998",
"sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"pysocks": {
"hashes": [
"sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
"sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
"sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.7.1"
},
"pywin32-ctypes": {
"hashes": [
"sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8",
"sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"
],
"markers": "python_version >= '3.6'",
"version": "==0.2.3"
},
"selenium": {
"hashes": [
"sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c",
"sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"
],
"index": "pypi",
"markers": "python_version >= '3.10'",
"version": "==4.38.0"
},
"setuptools": {
"hashes": [
"sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922",
"sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"
],
"markers": "python_version >= '3.9'",
"version": "==80.9.0"
},
"shiboken6": {
"hashes": [
"sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717",
"sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543",
"sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148",
"sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61",
"sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"
],
"markers": "python_version < '3.14' and python_version >= '3.9'",
"version": "==6.10.0"
},
"sniffio": {
"hashes": [
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
],
"markers": "python_version >= '3.7'",
"version": "==1.3.1"
},
"sortedcontainers": {
"hashes": [
"sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
"sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
],
"version": "==2.4.0"
},
"sympy": {
"hashes": [
"sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517",
"sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"
],
"markers": "python_version >= '3.9'",
"version": "==1.14.0"
},
"trio": {
"hashes": [
"sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b",
"sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"
],
"markers": "python_version >= '3.10'",
"version": "==0.32.0"
},
"trio-websocket": {
"hashes": [
"sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae",
"sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"
],
"markers": "python_version >= '3.8'",
"version": "==0.12.2"
},
"typing-extensions": {
"hashes": [
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
],
"markers": "python_version >= '3.9'",
"version": "==4.15.0"
},
"urllib3": {
"extras": [
"socks"
],
"hashes": [
"sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760",
"sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"
],
"markers": "python_version >= '3.9'",
"version": "==2.5.0"
},
"websocket-client": {
"hashes": [
"sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98",
"sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"
],
"markers": "python_version >= '3.9'",
"version": "==1.9.0"
},
"wsproto": {
"hashes": [
"sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
"sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==1.2.0"
}
},
"develop": {}
}
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -15,7 +15,7 @@ from PySide6.QtWidgets import QApplication
from gui.ALMainWindow import ALMainWindow
from gui.resources import ALResource
from utils.AppInitializer import initializeApp
from boot.AppInitializer import initializeApp
def main():
@@ -30,7 +30,7 @@ def main():
sys.exit(-1)
window = ALMainWindow()
window.show()
sys.exit(app.exec_())
sys.exit(app.exec())
if __name__ == "__main__":
+1 -1
View File
@@ -11,7 +11,7 @@ import logging
import queue
import datetime
from utils.LogManager import getLogger
from managers.log.LogManager import getLogger
class MsgBase:
@@ -11,10 +11,22 @@ import os
from PySide6.QtCore import QStandardPaths, QDir
from utils.ConfigManager import instance as configInstance
from utils.LogManager import instance as logInstance
from managers.log.LogManager import instance as logInstance
from managers.config.ConfigManager import instance as configInstance
from managers.driver.WebDriverManager import instance as webdriverInstance
def initializeLogManager(
) -> bool:
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
log_dir = os.path.join(app_dir, "logs")
if not QDir(log_dir).exists():
if not QDir().mkpath(log_dir):
return False
logInstance(log_dir)
return True
def initializeConfigManager(
) -> bool:
@@ -37,15 +49,20 @@ def initializeConfigManager(
configInstance(new_config_dir)
return True
def initializeLogManager(
def initializeWebDriverManager(
) -> bool:
logger = logInstance().getLogger("AppInitializer")
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
log_dir = os.path.join(app_dir, "logs")
if not QDir(log_dir).exists():
if not QDir().mkpath(log_dir):
driver_dir = os.path.join(app_dir, "drivers")
logger.info("初始化驱动目录 %s", driver_dir)
if not QDir(driver_dir).exists():
logger.error("创建驱动目录 %s 失败", driver_dir)
if not QDir().mkpath(driver_dir):
logger.error("创建驱动目录 %s 失败", driver_dir)
return False
logInstance(log_dir)
webdriverInstance(driver_dir)
return True
def initializeApp(
@@ -55,4 +72,6 @@ def initializeApp(
return False
if not initializeConfigManager():
return False
if not initializeWebDriverManager():
return False
return True
+6
View File
@@ -0,0 +1,6 @@
"""
Boot module for the AutoLibrary project.
Here are the classes and modules in this package:
- AppInitializer: Application initializer class.
"""
+18 -1
View File
@@ -20,7 +20,7 @@ from PySide6.QtGui import (
QCloseEvent, QAction
)
import utils.ConfigManager as ConfigManager
import managers.config.ConfigManager as ConfigManager
from utils.JSONReader import JSONReader
from utils.JSONWriter import JSONWriter
@@ -29,6 +29,7 @@ from gui.resources.ui.Ui_ALConfigWidget import Ui_ALConfigWidget
from gui.ALSeatMapSelectDialog import ALSeatMapSelectDialog
from gui.ALSeatMapTable import ALSeatMapTable
from gui.ALUserTreeWidget import ALUserTreeWidget, ALUserTreeItemType
from gui.ALWebDriverDownloadDialog import ALWebDriverDownloadDialog
class ALConfigWidget(QWidget, Ui_ALConfigWidget):
@@ -80,6 +81,7 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
self.AddUserButton.clicked.connect(self.onAddUserButtonClicked)
self.DelUserButton.clicked.connect(self.onDelUserButtonClicked)
self.BrowseBrowserDriverButton.clicked.connect(self.onBrowseBrowserDriverButtonClicked)
self.AutoDownloadWebDriverButton.clicked.connect(self.onAutoDownloadWebDriverButtonClicked)
self.BrowseCurrentRunConfigButton.clicked.connect(self.onBrowseCurrentRunConfigButtonClicked)
self.BrowseCurrentUserConfigButton.clicked.connect(self.onBrowseCurrentUserConfigButtonClicked)
self.BrowseExportRunConfigButton.clicked.connect(self.onBrowseExportRunConfigButtonClicked)
@@ -948,6 +950,21 @@ class ALConfigWidget(QWidget, Ui_ALConfigWidget):
if browser_driver_path:
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(browser_driver_path))
@Slot()
def onAutoDownloadWebDriverButtonClicked(
self
):
dialog = ALWebDriverDownloadDialog(self)
dialog.show()
dialog.exec_()
selected_driver_info = dialog.getSelectedDriverInfo()
if selected_driver_info and selected_driver_info.driver_path:
self.BrowserTypeComboBox.setCurrentText(selected_driver_info.driver_type.value)
self.BrowseBrowserDriverEdit.setText(QDir.toNativeSeparators(str(selected_driver_info.driver_path)))
@Slot()
def onBrowseCurrentRunConfigButtonClicked(
self
+1 -1
View File
@@ -19,7 +19,7 @@ from PySide6.QtGui import (
QTextCursor, QCloseEvent, QFont, QIcon, QDesktopServices
)
import utils.ConfigManager as ConfigManager
import managers.config.ConfigManager as ConfigManager
from base.MsgBase import MsgBase
+246
View File
@@ -0,0 +1,246 @@
from enum import Enum
from PySide6.QtWidgets import (
QLabel
)
from PySide6.QtCore import (
Qt, Property, QPropertyAnimation, QEasingCurve
)
from PySide6.QtGui import (
QPainter, QColor, QConicalGradient, QPalette
)
class ALStatusLabel(QLabel):
class Status(Enum):
"""
Enum class for representing the status of ALStatusLabel.
"""
WAITING = 0
RUNNING = 1
SUCCESS = 2
WARNING = 3
FAILURE = 4
def __init__(
self,
parent = None
):
super().__init__(parent)
self.__status = self.Status.WAITING
self.__icon_angle = 0
self.setupUi()
def setupUi(
self
):
self.setFixedSize(36, 36)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.RunningAnimation = QPropertyAnimation(self, b"iconAngle")
self.RunningAnimation.setDuration(1000)
self.RunningAnimation.setStartValue(0)
self.RunningAnimation.setEndValue(-360)
self.RunningAnimation.setLoopCount(-1)
self.RunningAnimation.setEasingCurve(QEasingCurve.Type.Linear)
def isDarkMode(
self
) -> bool:
return self.palette().color(QPalette.ColorRole.Window).value() < 128
def getMarkColor(
self
) -> QColor:
return QColor("#FFFFFF") if self.isDarkMode() else QColor("#454545")
@Property(Status)
def status(
self
) -> Status:
return self.__status
@Property(int)
def iconAngle(
self
) -> int:
return self.__icon_angle
@status.setter
def status(
self,
status: Status
):
if status not in self.Status:
raise ValueError(f"Invalid (class)Status[enum.Enum] value: {status}")
self.__status = status
if self.__status == self.Status.RUNNING:
self.RunningAnimation.start()
else:
self.RunningAnimation.stop()
self.update()
@iconAngle.setter
def iconAngle(
self,
value: int
):
self.__icon_angle = value
self.update()
def paintEvent(
self,
event
):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
center_x = self.width()/2
center_y = self.height()/2
radius = min(center_x, center_y) - 3
match self.__status:
case self.Status.WAITING:
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#969696")) # grey
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.RUNNING:
gradient = QConicalGradient(center_x, center_y, self.__icon_angle)
gradient.setColorAt(0.0, QColor("#2294FF" if self.isDarkMode() else "#0094FF"))
gradient.setColorAt(1.0, QColor("#2294FF00"))
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(gradient)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
case self.Status.SUCCESS:
# draw the success green circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#4CAF50" if self.isDarkMode() else "#00AF50")) # green
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the success check mark '✓'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/2
mark_path = [
(center_x - mark_size, center_y),
(center_x - mark_size/3, center_y + mark_size/2),
(center_x + mark_size, center_y - mark_size/2)
]
painter.drawLine(
int(mark_path[0][0]),int(mark_path[0][1]),
int(mark_path[1][0]),int(mark_path[1][1])
)
painter.drawLine(
int(mark_path[1][0]),int(mark_path[1][1]),
int(mark_path[2][0]),int(mark_path[2][1])
)
case self.Status.WARNING:
# draw the warning orange circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#FF9800")) # orange
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the warning exclamation mark '!'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
painter.drawLine(
int(center_x), int(center_y - radius/2),
int(center_x), int(center_y + radius/6)
)
painter.drawPoint(
int(center_x), int(center_y + radius/2)
)
case self.Status.FAILURE:
# draw the failure red circle
pen = painter.pen()
pen.setWidth(2)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setColor(QColor("#DC0000")) # red
painter.setPen(pen)
painter.drawEllipse(
int(center_x - radius),
int(center_y - radius),
int(radius*2),
int(radius*2)
)
# draw the failure cross mark '✗'
painter.setPen(Qt.PenStyle.SolidLine)
pen = painter.pen()
pen.setWidth(3)
pen.setBrush(Qt.BrushStyle.NoBrush)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
# white when dark mode, black when light mode
pen.setColor(self.getMarkColor())
painter.setPen(pen)
mark_size = radius/3
painter.drawLine(
int(center_x - mark_size), int(center_y - mark_size),
int(center_x + mark_size), int(center_y + mark_size)
)
painter.drawLine(
int(center_x + mark_size), int(center_y - mark_size),
int(center_x - mark_size), int(center_y + mark_size)
)
painter.end()
super().paintEvent(event)
+1 -1
View File
@@ -25,7 +25,7 @@ from PySide6.QtGui import (
QCloseEvent
)
import utils.ConfigManager as ConfigManager
import managers.config.ConfigManager as ConfigManager
import utils.TimerUtils as TimerUtils
from gui.resources.ui.Ui_ALTimerTaskManageWidget import Ui_ALTimerTaskManageWidget
+536
View File
@@ -0,0 +1,536 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import threading
from typing import Optional
from PySide6.QtCore import (
Qt, Slot, QThread, Signal
)
from PySide6.QtWidgets import (
QDialog, QLabel, QComboBox, QProgressBar,
QPushButton, QVBoxLayout, QHBoxLayout,
QMessageBox, QFrame, QLineEdit
)
from PySide6.QtGui import (
QCloseEvent
)
from managers.driver.WebDriverManager import (
instance as webdriver_manager_instance,
WebDriverManager, WebDriverInfo, WebDriverType
)
from gui.ALStatusLabel import ALStatusLabel
class DownloadWorker(QThread):
"""
Worker thread for downloading web drivers.
"""
progress = Signal(float, int, float, str)
downloadFinished = Signal(object, str)
downloadError = Signal(str)
downloadCancelled = Signal()
def __init__(
self,
driver_manager: WebDriverManager,
driver_info: WebDriverInfo
):
super().__init__()
self.__driver_manager = driver_manager
self.__driver_info = driver_info
self.__driver_path = None
self.__cancelled = False
self.__cancel_event = threading.Event()
def cancel(
self
):
"""
Cancel the download operation.
"""
self.__cancelled = True
self.__cancel_event.set()
def run(
self
):
try:
if self.__cancelled:
self.downloadCancelled.emit()
return
self.__driver_path = self.__driver_manager.installDriver(
self.__driver_info,
progress_callback=self.onProgress,
cancel_event=self.__cancel_event
)
if self.__cancelled:
self.downloadCancelled.emit()
return
if self.__driver_path:
self.downloadFinished.emit(self.__driver_path, "")
else:
self.downloadError.emit("下载失败: 未返回有效路径")
except Exception as e:
if not self.__cancelled:
self.downloadError.emit(f"下载失败: {str(e)}")
def onProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
if self.__cancel_event.is_set():
self.__cancelled = True
if not self.__cancelled:
self.progress.emit(downloaded, total, speed, message)
def stop(
self
):
"""
Cancel and wait for the thread to finish.
Must only be called from the main thread.
"""
self.cancel()
if not self.isFinished():
if not self.wait(5000):
self.terminate()
self.wait()
class ALWebDriverDownloadDialog(QDialog):
def __init__(
self,
parent: Optional[QDialog] = None,
driver_dir: str = ""
):
"""
Web driver download dialog.
Args:
parent: Parent widget.
driver_dir: Driver directory path.
"""
super().__init__(parent)
self.__driver_dir = driver_dir
self.__driver_manager: Optional[WebDriverManager] = None
self.__confirmed = False
self.__selected_driver_info: Optional[WebDriverInfo] = None
self.__driver_infos: list[WebDriverInfo] = []
self.__download_thread: Optional[DownloadWorker] = None
self.setupUi()
self.connectSignals()
self.initializeDriverManager()
self.refreshDriverList()
def showEvent(
self,
event
):
result = super().showEvent(event)
if self.parent():
screen_rect = self.screen().geometry()
target_pos = self.parent().geometry().center()
target_pos.setX(target_pos.x() - self.width()//2)
target_pos.setY(target_pos.y() - self.height()//2)
if target_pos.x() < 0:
target_pos.setX(0)
if target_pos.x() + self.width() > screen_rect.width():
target_pos.setX(screen_rect.width() - self.width())
if target_pos.y() < 0:
target_pos.setY(0)
if target_pos.y() + self.height() > screen_rect.height():
target_pos.setY(screen_rect.height() - self.height())
self.move(target_pos)
return result
def setupUi(
self
):
self.setModal(True)
self.setMaximumHeight(240)
self.setMinimumHeight(240)
self.setWindowTitle("浏览器驱动下载 - AutoLibrary")
self.MainLayout = QVBoxLayout(self)
self.MainLayout.setContentsMargins(5, 5, 5, 5)
self.MainLayout.setSpacing(5)
self.BrowserCountLabel = QLabel("检测到 0 个可用浏览器:")
self.MainLayout.addWidget(self.BrowserCountLabel)
self.DriverInfoLayout = QHBoxLayout()
self.DriverInfoLayout.setSpacing(5)
self.DriverComboBox = QComboBox()
self.DriverInfoLayout.addWidget(self.DriverComboBox)
self.StatusLabel = ALStatusLabel()
self.StatusLabel.setFixedSize(32, 32)
self.DriverInfoLayout.addWidget(self.StatusLabel)
self.MainLayout.addLayout(self.DriverInfoLayout)
self.DetailLayout = QVBoxLayout()
self.DetailLayout.setSpacing(5)
self.DetailLayout.setContentsMargins(5, 5, 5, 5)
self.BrowserTypeLabel = QLabel("类型:")
self.DetailLayout.addWidget(self.BrowserTypeLabel)
self.VersionLabel = QLabel("版本:")
self.DetailLayout.addWidget(self.VersionLabel)
self.PathLabel = QLineEdit()
self.PathLabel.setReadOnly(True)
self.PathLabel.setText("路径:未安装")
self.DetailLayout.addWidget(self.PathLabel)
self.MainLayout.addLayout(self.DetailLayout)
self.Line = QFrame()
self.Line.setFrameShape(QFrame.Shape.HLine)
self.Line.setFrameShadow(QFrame.Shadow.Sunken)
self.MainLayout.addWidget(self.Line)
self.ProgressBar = QProgressBar()
self.ProgressBar.setValue(0)
self.ProgressBar.setTextVisible(False)
self.MainLayout.addWidget(self.ProgressBar)
self.ProgressText = QLabel("")
self.ProgressText.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.MainLayout.addWidget(self.ProgressText)
self.ControlLayout = QHBoxLayout()
self.ControlLayout.setSpacing(8)
self.ControlLayout.setAlignment(Qt.AlignmentFlag.AlignRight)
self.RefreshButton = QPushButton("刷新")
self.RefreshButton.setFixedSize(80, 25)
self.DownloadButton = QPushButton("下载驱动")
self.DownloadButton.setFixedSize(80, 25)
self.DeleteButton = QPushButton("删除驱动")
self.DeleteButton.setFixedSize(80, 25)
self.CancelButton = QPushButton("取消")
self.CancelButton.setFixedSize(80, 25)
self.ConfirmButton = QPushButton("确认")
self.ConfirmButton.setFixedSize(80, 25)
self.ConfirmButton.setEnabled(False)
self.ControlLayout.addWidget(self.RefreshButton)
self.ControlLayout.addWidget(self.DownloadButton)
self.ControlLayout.addWidget(self.DeleteButton)
self.ControlLayout.addWidget(self.CancelButton)
self.ControlLayout.addWidget(self.ConfirmButton)
self.MainLayout.addLayout(self.ControlLayout)
def connectSignals(
self
):
self.RefreshButton.clicked.connect(self.onRefreshButtonClicked)
self.DownloadButton.clicked.connect(self.onDownloadButtonClicked)
self.DeleteButton.clicked.connect(self.onDeleteButtonClicked)
self.CancelButton.clicked.connect(self.onCancelButtonClicked)
self.ConfirmButton.clicked.connect(self.onConfirmButtonClicked)
self.DriverComboBox.currentIndexChanged.connect(self.onDriverComboBoxChanged)
def initializeDriverManager(
self
):
try:
self.__driver_manager = webdriver_manager_instance(self.__driver_dir)
except ValueError as e:
QMessageBox.warning(self, "初始化失败", f"WebDriverManager 初始化失败:\n{str(e)}")
self.reject()
def refreshDriverList(
self
):
if not self.__driver_manager:
return
self.__driver_manager.refresh()
self.__driver_infos = self.__driver_manager.getDriverInfos()
self.DriverComboBox.clear()
for driver_info in self.__driver_infos:
display_text = f"{driver_info.driver_type.value} - {driver_info.browser_version}"
self.DriverComboBox.addItem(display_text)
count = len(self.__driver_infos)
self.BrowserCountLabel.setText(f"检测到 {count} 个可用浏览器:")
if self.__driver_infos:
self.onDriverComboBoxChanged(0)
def onDriverComboBoxChanged(
self,
index: int
):
if not self.__driver_infos or index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
self.updateDriverInfoDisplay(driver_info)
self.updateButtonStates(driver_info)
@Slot()
def onRefreshButtonClicked(
self
):
self.refreshDriverList()
@Slot()
def onDeleteButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name != "INSTALLED":
QMessageBox.information(self, "提示 - AutoLibrary", "该驱动未安装, 无需删除")
return
reply = QMessageBox.question(
self,
"确认删除 - AutoLibrary",
f"确定要删除 {driver_info.driver_type.value} 驱动吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
try:
self.__driver_manager.uninstallDriver(driver_info)
self.refreshDriverList()
QMessageBox.information(self, "删除成功 - AutoLibrary", "驱动已成功删除")
except Exception as e:
QMessageBox.critical(self, "删除失败 - AutoLibrary", f"删除驱动时出错:\n{str(e)}")
@Slot()
def onDownloadButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name == "INSTALLED":
return
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
self.DownloadButton.setEnabled(False)
self.RefreshButton.setEnabled(False)
self.DriverComboBox.setEnabled(False)
self.ProgressBar.setValue(0)
self.ProgressText.setText("正在下载驱动...")
self.__download_thread = DownloadWorker(self.__driver_manager, driver_info)
self.__download_thread.progress.connect(self.onDownloadProgress)
self.__download_thread.downloadFinished.connect(self.onDownloadFinished)
self.__download_thread.downloadError.connect(self.onDownloadError)
self.__download_thread.downloadCancelled.connect(self.onDownloadCancelled)
self.__download_thread.finished.connect(self.__onThreadFinished)
self.__download_thread.start()
@Slot()
def onDownloadProgress(
self,
downloaded: float,
total: int,
speed: float,
message: str
):
progress = downloaded
self.ProgressBar.setValue(progress)
if speed >= 1024:
speed_text = f"{speed/1024:.1f} MB/s"
else:
speed_text = f"{speed:.1f} KB/s"
progress_text = f"{message}... {downloaded:.1f}% - {speed_text}"
self.ProgressText.setText(progress_text)
@Slot()
def onDownloadFinished(
self
):
self.ProgressBar.setValue(100)
self.ProgressText.setText("下载完成 !")
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
self.updateDriverInfoDisplay(driver_info)
self.ConfirmButton.setEnabled(True)
self.DownloadButton.setEnabled(False)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.DeleteButton.setEnabled(True)
@Slot()
def onDownloadError(
self,
error_message: str
):
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
self.DownloadButton.setEnabled(True)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.CancelButton.setEnabled(True)
@Slot()
def onDownloadCancelled(
self
):
index = self.DriverComboBox.currentIndex()
if 0 <= index < len(self.__driver_infos):
driver_info = self.__driver_infos[index]
self.__driver_manager.cancelDriverDownload(driver_info)
self.updateDriverInfoDisplay(driver_info)
self.ProgressText.setText("下载已取消")
self.ProgressBar.setValue(0)
self.StatusLabel.status = ALStatusLabel.Status.WAITING
self.DownloadButton.setEnabled(True)
self.RefreshButton.setEnabled(True)
self.DriverComboBox.setEnabled(True)
self.CancelButton.setEnabled(True)
self.DeleteButton.setEnabled(False)
@Slot()
def onConfirmButtonClicked(
self
):
index = self.DriverComboBox.currentIndex()
if index < 0 or index >= len(self.__driver_infos):
return
driver_info = self.__driver_infos[index]
if driver_info.driver_status.name != "INSTALLED":
return
self.__selected_driver_info = driver_info
self.__confirmed = True
self.accept()
@Slot()
def onCancelButtonClicked(
self
):
if self.__download_thread:
reply = QMessageBox.question(
self,
"确认取消 - AutoLibrary",
"正在下载中, 确定要取消下载吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.__download_thread.cancel()
else:
self.__confirmed = False
self.__selected_driver_info = None
self.reject()
def closeEvent(
self,
event: QCloseEvent
):
if self.__download_thread and self.__download_thread.isRunning():
reply = QMessageBox.question(
self,
"确认关闭 - AutoLibrary",
"驱动正在下载中, 确定要取消并关闭对话框吗 ?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
self.__download_thread.stop()
if not self.__confirmed:
self.__selected_driver_info = None
event.accept()
super().closeEvent(event)
def __onThreadFinished(
self
):
if self.__download_thread:
self.__download_thread.deleteLater()
self.__download_thread = None
def getSelectedDriverInfo(
self
) -> Optional[WebDriverInfo]:
return self.__selected_driver_info
def updateDriverInfoDisplay(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_type == WebDriverType.CHROME:
driver_type = "Google Chrome"
elif driver_info.driver_type == WebDriverType.FIREFOX:
driver_type = "Mozilla Firefox"
elif driver_info.driver_type == WebDriverType.EDGE:
driver_type = "Microsoft Edge"
else:
driver_type = "未知"
self.BrowserTypeLabel.setText(f"类型:{driver_type}")
self.VersionLabel.setText(f"版本:{driver_info.driver_version}")
if driver_info.driver_path:
self.PathLabel.setText(str(driver_info.driver_path))
else:
self.PathLabel.setText("未安装")
match driver_info.driver_status.name:
case "NOT_INSTALLED":
self.StatusLabel.status = ALStatusLabel.Status.WAITING
case "INSTALLED":
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
case "DOWNLOADING":
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
case "ERROR":
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
def updateButtonStates(
self,
driver_info: WebDriverInfo
):
if driver_info.driver_status.name == "INSTALLED":
self.DownloadButton.setEnabled(False)
self.DeleteButton.setEnabled(True)
self.ConfirmButton.setEnabled(True)
else:
self.DeleteButton.setEnabled(False)
self.DownloadButton.setEnabled(True)
self.ConfirmButton.setEnabled(False)
+18
View File
@@ -0,0 +1,18 @@
"""
GUI module for the AutoLibrary project.
Here are the classes and modules in this package:
- ALMainWindow: Main window class.
- ALAboutDialog: About dialog class.
- ALConfigWidget: Configuration widget class.
- ALSeatFrame: Seat frame class.
- ALSeatMapView: Seat map view class.
- ALSeatMapTable: Seat map table class.
- ALSeatMapSelectDialog: Seat map select dialog class.
- ALTimerTaskAddDialog: Timer task add dialog class.
- ALTimerTaskHistoryDialog: Timer task history dialog class.
- ALTimerTaskManageWidget: Timer task manage widget class.
- ALUserTreeWidget: User tree widget class.
- ALMainWorkers: Main workers class.
- ALVersionInfo: Version info class.
"""
+3
View File
@@ -0,0 +1,3 @@
"""
GUI resources module for the AutoLibrary project.
"""
+192 -161
View File
@@ -1233,12 +1233,31 @@
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="BrowserConfigGroupBox">
<property name="title">
<string>浏览器设置</string>
<item row="2" column="0">
<widget class="QFrame" name="SystemConfigSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>270</height>
</size>
</property>
<layout class="QVBoxLayout" name="BrowserConfigLayout">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QGroupBox" name="RunModeConfigGroupBox">
<property name="title">
<string>运行模式</string>
</property>
<layout class="QVBoxLayout" name="RunModeConfigLayout">
<property name="spacing">
<number>5</number>
</property>
@@ -1255,162 +1274,59 @@
<number>3</number>
</property>
<item>
<widget class="QLabel" name="BrowserTypeLabel">
<widget class="QCheckBox" name="AutoReserveCheckBox">
<property name="minimumSize">
<size>
<width>80</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>浏览器类型:</string>
<string>自动预约</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="BrowserTypeComboBox">
<widget class="QCheckBox" name="AutoCheckinCheckBox">
<property name="minimumSize">
<size>
<width>80</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;脚本运行使用的浏览器类型&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="currentText">
<string>edge</string>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="maxVisibleItems">
<number>3</number>
</property>
<property name="maxCount">
<number>3</number>
</property>
<item>
<property name="text">
<string>edge</string>
</property>
</item>
<item>
<property name="text">
<string>chrome</string>
</property>
</item>
<item>
<property name="text">
<string>firefox</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="BrowserDriverLabel">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>驱动路径:</string>
<string>自动签到</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BrowserDriverLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
<property name="minimumSize">
<size>
<width>250</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.kenanzhu.com/manuals&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="BrowseBrowserDriverButton">
<property name="minimumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="HeadlessCheckBox">
<widget class="QCheckBox" name="AutoRenewalCheckBox">
<property name="minimumSize">
<size>
<width>0</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<width>100</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;运行时不显示浏览器&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>无头模式</string>
<string>自动续约</string>
</property>
</widget>
</item>
@@ -1529,15 +1445,12 @@
</layout>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QGroupBox" name="RunModeConfigGroupBox">
<item row="1" column="0">
<widget class="QGroupBox" name="BrowserConfigGroupBox">
<property name="title">
<string>运行模式</string>
<string>浏览器设置</string>
</property>
<layout class="QVBoxLayout" name="RunModeConfigLayout">
<property name="spacing">
<number>5</number>
</property>
<layout class="QGridLayout" name="BrowserConfigLayout">
<property name="leftMargin">
<number>3</number>
</property>
@@ -1550,85 +1463,203 @@
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QCheckBox" name="AutoReserveCheckBox">
<property name="spacing">
<number>5</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="BrowserTypeLabel">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>自动预约</string>
<string>浏览器类型:</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="AutoCheckinCheckBox">
<item row="1" column="0">
<widget class="QComboBox" name="BrowserTypeComboBox">
<property name="minimumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;脚本运行使用的浏览器类型&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="currentText">
<string>edge</string>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="maxVisibleItems">
<number>3</number>
</property>
<property name="maxCount">
<number>3</number>
</property>
<item>
<property name="text">
<string>edge</string>
</property>
</item>
<item>
<property name="text">
<string>chrome</string>
</property>
</item>
<item>
<property name="text">
<string>firefox</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="BrowserDriverLabel">
<property name="minimumSize">
<size>
<width>175</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>175</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>自动签到</string>
<string>驱动路径:</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="AutoRenewalCheckBox">
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="BrowserDriverLayout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="BrowseBrowserDriverEdit">
<property name="minimumSize">
<size>
<width>250</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;详情请参阅 &lt;a href=&quot;https://www.autolibrary.kenanzhu.com/manuals&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#69fcff;&quot;&gt;用户手册&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="BrowseBrowserDriverButton">
<property name="minimumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>35</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="HeadlessCheckBox">
<property name="minimumSize">
<size>
<width>100</width>
<width>0</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<width>16777215</width>
<height>25</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;运行时不显示浏览器&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>自动续约</string>
<string>无头模式</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="AutoDownloadWebDriverButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>120</width>
<height>25</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>自动下载驱动</string>
</property>
<property name="icon">
<iconset theme="document-properties"/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QFrame" name="SystemConfigSpaceFrame">
<property name="minimumSize">
<size>
<width>0</width>
<height>270</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="OtherConfigWidget">
+8
View File
@@ -0,0 +1,8 @@
"""
Managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
- LogManager: Log manager for logging.
- WebDriverManager: Web driver manager for managing web drivers.
"""
@@ -234,11 +234,12 @@ def instance(
global _config_manager_instance
with _instance_lock:
if _config_manager_instance is None:
if not config_dir:
raise ValueError("ConfigManager 需要配置目录参数")
_config_manager_instance = ConfigManager(config_dir)
else:
if config_dir == "":
return _config_manager_instance
if getBaseConfigDir() != config_dir:
raise ValueError(
"ConfigManager 的实例已初始化,不能使用不同的配置目录。")
raise ValueError("ConfigManager 的实例已初始化,不能使用不同的配置目录。")
return _config_manager_instance
+6
View File
@@ -0,0 +1,6 @@
"""
Config managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Config manager for managing configuration files.
"""
+166
View File
@@ -0,0 +1,166 @@
import platform
import installed_browsers
from pathlib import Path
from enum import Enum
from dataclasses import dataclass
class WebBrowserType(Enum):
"""
Web browser type
"""
CHROME = "chrome"
FIREFOX = "firefox"
EDGE = "edge"
class WebBrowserArch(Enum):
"""
Web browser architecture
"""
WINX86_32 = 0
WINX86_64 = 1
WINARM = 2
LINUXX86_32 = 3
LINUXX86_64 = 4
LINUXARM = 5
MACX86_64 = 6
MACARM = 7
@dataclass
class WebBrowserInfo:
"""
Web browser information
Attributes:
browser_arch (WebBrowserArch): Web browser architecture
browser_type (WebBrowserType): Web browser type
browser_version (str): Web browser version
browser_path (Path): Web browser executable file path
"""
browser_arch: WebBrowserArch
browser_type: WebBrowserType
browser_version: str
browser_path: Path
class WebBrowserArchDetector:
"""
Web browser architecture detector
"""
def __init__(
self
):
pass
def detect(
self
) -> WebBrowserArch:
"""
Detect system architecture
Returns:
WebBrowserArch: System architecture
"""
system = platform.system()
machine = platform.machine().lower()
if system == "Windows":
if machine in ["amd64", "x86_64"]:
return WebBrowserArch.WINX86_64
elif machine in ["i386", "i686", "x86"]:
return WebBrowserArch.WINX86_32
elif machine in ["arm64", "aarch64"]:
return WebBrowserArch.WINARM
else:
return WebBrowserArch.WINX86_64
elif system == "Darwin":
if machine in ["arm64", "aarch64"]:
return WebBrowserArch.MACARM
else:
return WebBrowserArch.MACX86_64
elif system == "Linux":
if machine in ["amd64", "x86_64"]:
return WebBrowserArch.LINUXX86_64
elif machine in ["i386", "i686", "x86"]:
return WebBrowserArch.LINUXX86_32
elif machine in ["arm64", "aarch64"]:
return WebBrowserArch.LINUXARM
elif machine.startswith("arm"):
return WebBrowserArch.LINUXARM
else:
return WebBrowserArch.LINUXX86_64
raise ValueError(f"不支持的系统架构 : {system} {machine}")
class WebBrowserDetector:
"""
Web browser detector
"""
def __init__(
self
):
self.browser_arch = WebBrowserArchDetector().detect()
self.browser_infos : list[WebBrowserInfo] = []
def detect(
self
) -> list[WebBrowserInfo]:
"""
Detect installed web browsers on the system.
Returns:
list[WebBrowserInfo]: List of detected browser information objects.
"""
self.browser_infos = []
try:
all_browsers = installed_browsers.browsers()
except Exception as e:
self.browser_infos = []
return self.browser_infos
# Mapping from internal library name to our enum
type_map = {
'chrome': WebBrowserType.CHROME,
'firefox': WebBrowserType.FIREFOX,
'msedge': WebBrowserType.EDGE,
}
for browser in all_browsers:
internal_name = browser.get('name', '').lower()
if internal_name not in type_map:
continue # Not one of the browsers we care about
version = browser.get('version')
if not version:
# Skip browsers with no version info (unlikely, but defensive)
continue
exe_path = browser.get('location')
if not exe_path:
continue
try:
path = Path(exe_path)
if not path.is_file():
continue
except Exception:
continue # Invalid path
info = WebBrowserInfo(
browser_arch=self.browser_arch, # Use system architecture as fallback
browser_type=type_map[internal_name],
browser_version=version,
browser_path=path,
)
self.browser_infos.append(info)
return self.browser_infos
+452
View File
@@ -0,0 +1,452 @@
import os
import time
import shutil
import threading
import requests
import zipfile
import tarfile
from enum import Enum
from pathlib import Path
from typing import Optional, Callable
class WebDriverType(Enum):
"""
Web driver type
"""
CHROME = "chrome"
FIREFOX = "firefox"
EDGE = "edge"
class WebDriverArch(Enum):
"""
Web driver architecture
"""
class Chrome(Enum):
"""
Chrome web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
# LINUX86_32 : no support for linux 32bit
LINUX86_64 = "linux64"
# LINUXARM : no support for linux arm64
MACX86_64 = "mac-x64"
MACARM = "mac-arm64"
class Firefox(Enum):
"""
Firefox web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
WINARM = "win-aarch64"
LINUXX86_32 = "linux32"
LINUXX86_64 = "linux64"
LINUXARM = "linux-aarch64"
MACX86_64 = "macos"
MACARM = "macos-aarch64"
class Edge(Enum):
"""
Edge web driver architecture
"""
WINX86_32 = "win32"
WINX86_64 = "win64"
WINARM = "arm64"
# LINUX86_32 : no support for linux 32bit
LINUXX86_64 = "linux64"
# LINUXARM : no support for linux arm64
MACX86_64 = "mac64"
MACARM = "mac64_m1"
class WebDriverName:
"""
Web driver name
"""
def __init__(
self,
driver_type: WebDriverType
):
self.driver_type = driver_type
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return "chromedriver"
case WebDriverType.FIREFOX:
return "geckodriver"
case WebDriverType.EDGE:
return "msedgedriver"
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverExecName:
"""
Web driver executable file name
"""
def __init__(
self,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.driver_type = driver_type
self.arch = arch
def __str__(
self
) -> str:
is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\
self.arch is WebDriverArch.Chrome.WINX86_64 or\
self.arch is WebDriverArch.Firefox.WINX86_32 or\
self.arch is WebDriverArch.Firefox.WINX86_64 or\
self.arch is WebDriverArch.Edge.WINX86_32 or\
self.arch is WebDriverArch.Edge.WINX86_64 else False
match self.driver_type:
case WebDriverType.CHROME:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case WebDriverType.FIREFOX:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case WebDriverType.EDGE:
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverFileName:
"""\
Web driver compressed file name
"""
def __init__(
self,
version: str,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.version = version
self.driver_type = driver_type
self.arch = arch
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip"
case WebDriverType.FIREFOX:
if self.arch is WebDriverArch.Firefox.WINX86_32 or\
self.arch is WebDriverArch.Firefox.WINX86_64:
suffix = "zip"
else:
suffix = "tar.gz"
return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}"
case WebDriverType.EDGE:
return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverURL:
"""
Web driver download URL
"""
def __init__(
self,
version: str,
driver_type: WebDriverType,
arch: WebDriverArch
):
self.version = version
self.driver_type = driver_type
self.arch = arch
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
def __str__(
self
) -> str:
match self.driver_type:
case WebDriverType.CHROME:
return f"https://storage.googleapis.com/chrome-for-testing-public/"\
f"{self.version}/"\
f"{self.arch.value}/"\
f"{self.file_name}"
case WebDriverType.FIREFOX:
return f"https://github.com/mozilla/geckodriver/releases/download/"\
f"v{self.version}/"\
f"{self.file_name}"
case WebDriverType.EDGE:
return f"https://msedgedriver.microsoft.com/"\
f"{self.version}/"\
f"{self.file_name}"
case _:
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
class WebDriverDownloader:
"""
Base class for WebDriver downloaders
Args:
driver_type (WebDriverType): Web driver type
version (str): WebDriver version
arch (WebDriverArch): WebDriver architecture
download_dir (str): Download directory
"""
def __init__(
self,
driver_type: WebDriverType,
driver_version: str,
driver_arch: WebDriverArch,
download_dir: str
):
self.driver_type = driver_type
self.arch = driver_arch
self.version = driver_version
self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch))
self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
def download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
try:
# downlaod file : 0% - 98%
if not self._download(progress_callback, cancel_event=cancel_event):
return None
# verify file : 98% - 99%
if not self._verify(progress_callback):
progress_callback(0, 100, 0.0, "验证失败")
return None
# extract file : 99% - 100%
driver_path = self._extract(progress_callback)
if not driver_path:
progress_callback(0, 100, 0.0, "解压失败")
return None
return driver_path
except Exception as e:
raise e
def _download(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
max_retries: int = 3,
cancel_event: Optional[threading.Event] = None
) -> bool:
CHUNK_SIZE = 8192*8 # 64KB chunk
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Encoding': 'gzip, deflate'
}
for attempt in range(max_retries):
try:
if cancel_event and cancel_event.is_set():
return False
# resume download if file exists
if self.download_path.exists():
downloaded_size = self.download_path.stat().st_size
headers_ = headers.copy()
headers_['Range'] = f"bytes={downloaded_size}-"
mode = 'ab'
else:
downloaded_size = 0
headers_ = headers
mode = 'wb'
# get response
response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=10)
if response.status_code not in [200, 206]:
if self.download_path.exists():
self.download_path.unlink()
downloaded_size = 0
mode = 'wb'
response = requests.get(str(self.download_url), headers=headers, stream=True)
response.raise_for_status()
# get total size
total_size = int(response.headers.get('Content-Length', 0))
if response.status_code == 206: # Partial Content - server supports Range
total_size += downloaded_size
last_callback_time = time.time()
last_callback_size = downloaded_size
callback_interval = 0.1
with open(self.download_path, mode) as f:
for chunk in response.iter_content(CHUNK_SIZE):
current_time = time.time()
if cancel_event and cancel_event.is_set():
response.close()
return False
if not chunk:
continue
f.write(chunk)
downloaded_size += len(chunk)
if not progress_callback or total_size <= 0:
continue
current_progress = (downloaded_size/total_size)*98.0
if current_time - last_callback_time >= callback_interval or current_progress >= 98.0:
elapsed = current_time - last_callback_time
if elapsed > 0:
speed = (downloaded_size - last_callback_size)/(elapsed*1024.0)
else:
speed = 0.0
progress_callback(current_progress, 100, speed, "下载中...")
last_callback_time = current_time
last_callback_size = downloaded_size
if total_size > 0 and self.download_path.stat().st_size < total_size:
raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节")
return True
except Exception as e:
if cancel_event and cancel_event.is_set():
return False
if attempt < max_retries - 1:
progress_callback(0, 100, 0.0, f"{attempt+1} 次重试...")
time.sleep(1)
continue
raise e
def _verify(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
) -> bool:
progress_callback(98, 100, 0.0, "验证完成")
return True
def _extract(
self,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None
) -> Optional[Path]:
try:
progress_callback(98, 100, 0.0, "解压中...")
file_path_str = str(self.download_path)
if file_path_str.endswith('.tar.gz'):
with tarfile.open(self.download_path, 'r:gz') as tar_ref:
tar_ref.extractall(self.download_dir)
else:
with zipfile.ZipFile(self.download_path, 'r') as zip_ref:
zip_ref.extractall(self.download_dir)
driver_file = None
for root, _, files in os.walk(self.download_dir):
for file in files:
expected_name = str(WebDriverExecName(self.driver_type, self.arch))
if file == str(expected_name):
src_path = Path(root, file)
dst_path = self.download_dir/file
src_path.rename(dst_path)
driver_file = dst_path
break
if driver_file:
break
if not driver_file:
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
progress_callback(100, 100, 0.0, "解压完成")
self.download_path.unlink()
self._cleanup(driver_file)
return driver_file
except Exception:
return None
def _cleanup(
self,
driver_file: Path
) -> None:
for item in self.download_dir.iterdir():
if item != driver_file:
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
class ChromeDriverDownloader(WebDriverDownloader):
"""
Chrome web driver downloader
Only support version higher than 114
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.CHROME, version, arch, download_dir)
class FirefoxDriverDownloader(WebDriverDownloader):
"""
Firefox web driver downloader
This class do not resolve version mapping,
only support driver version higher than 0.17.0
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.FIREFOX, version, arch, download_dir)
class EdgeDriverDownloader(WebDriverDownloader):
"""
Edge web driver downloader
"""
def __init__(
self,
version: str,
arch: WebDriverArch,
download_dir: str
):
super().__init__(WebDriverType.EDGE, version, arch, download_dir)
+471
View File
@@ -0,0 +1,471 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
import os
import threading
import packaging.version as ver
from enum import Enum
from pathlib import Path
from typing import Optional, Callable
from managers.driver.WebBrowserDetector import (
WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector
)
from managers.driver.WebDriverDownloader import (
WebDriverArch, WebDriverType,
ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader
)
class DriverStatus(Enum):
"""
Web driver status.
"""
NOT_INSTALLED = 0
INSTALLED = 1
DOWNLOADING = 2
ERROR = 3
class WebDriverInfo:
"""
Web driver information.
Attributes:
driver_type (WebDriverType): Web driver type
driver_arch (WebDriverArch): Web driver architecture
driver_version (str): Web driver version
browser_version (str): Web browser version
driver_path (Optional[Path]): Web driver executable file path
driver_status (DriverStatus): Web driver status
"""
def __init__(
self
):
self.driver_type = None
self.driver_arch = None
self.driver_version = ""
self.browser_version = ""
self.driver_path: Optional[Path] = None
self.driver_status = DriverStatus.NOT_INSTALLED
class WebDriverManager:
"""
Web Driver Manager Singleton Class
Args:
driver_dir (str): The directory to store web drivers.
"""
def __init__(
self,
driver_dir: str
):
self.__driver_dir = os.path.abspath(driver_dir)
self.__browser_detector = WebBrowserDetector()
self.__driver_infos: list[WebDriverInfo] = []
self.__initialized = False
self.__lock = threading.Lock()
self.initialize()
def initialize(
self
):
if self.__initialized:
return
os.makedirs(self.__driver_dir, exist_ok=True)
self._detectBrowsers()
self._checkDriverStatus()
self.__initialized = True
def _detectBrowsers(
self
):
with self.__lock:
browser_infos = self.__browser_detector.detect()
self.__driver_infos = [
self._getDriverInfo(info)
for info in browser_infos
]
def _checkDriverStatus(
self
):
with self.__lock:
for driver_info in self.__driver_infos:
driver_path = self._getDriverPath(driver_info)
if driver_path and driver_path.exists() and driver_path.is_file():
driver_info.driver_path = driver_path
driver_info.driver_status = DriverStatus.INSTALLED
def _mapWebBrowserTypeToDriver(
self,
browser_type: WebBrowserType
) -> WebDriverType:
if browser_type == WebBrowserType.CHROME:
return WebDriverType.CHROME
elif browser_type == WebBrowserType.FIREFOX:
return WebDriverType.FIREFOX
elif browser_type == WebBrowserType.EDGE:
return WebDriverType.EDGE
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapWebBrowserArchToDriver(
self,
browser_type: WebBrowserType,
browser_arch: WebBrowserArch
) -> WebDriverArch:
if browser_type == WebBrowserType.CHROME:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Chrome.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Chrome.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
raise ValueError("Chrome 不支持 Windows ARM 架构")
elif browser_arch == WebBrowserArch.LINUXX86_32:
raise ValueError("Chrome 不支持 Linux x86_32 架构")
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Chrome.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
raise ValueError("Chrome 不支持 Linux ARM 架构")
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Chrome.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Chrome.MACARM
else:
raise ValueError(f"不支持的 Chrome 浏览器架构 : {browser_arch}")
elif browser_type == WebBrowserType.FIREFOX:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Firefox.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Firefox.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
return WebDriverArch.Firefox.WINARM
elif browser_arch == WebBrowserArch.LINUXX86_32:
return WebDriverArch.Firefox.LINUXX86_32
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Firefox.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
return WebDriverArch.Firefox.LINUXARM
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Firefox.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Firefox.MACARM
else:
raise ValueError(f"不支持的 Firefox 浏览器架构 : {browser_arch}")
elif browser_type == WebBrowserType.EDGE:
if browser_arch == WebBrowserArch.WINX86_32:
return WebDriverArch.Edge.WINX86_32
elif browser_arch == WebBrowserArch.WINX86_64:
return WebDriverArch.Edge.WINX86_64
elif browser_arch == WebBrowserArch.WINARM:
return WebDriverArch.Edge.WINARM
elif browser_arch == WebBrowserArch.LINUXX86_32:
raise ValueError("Edge 不支持 Linux x86_32 架构")
elif browser_arch == WebBrowserArch.LINUXX86_64:
return WebDriverArch.Edge.LINUXX86_64
elif browser_arch == WebBrowserArch.LINUXARM:
raise ValueError("Edge 不支持 Linux ARM 架构")
elif browser_arch == WebBrowserArch.MACX86_64:
return WebDriverArch.Edge.MACX86_64
elif browser_arch == WebBrowserArch.MACARM:
return WebDriverArch.Edge.MACARM
else:
raise ValueError(f"不支持的 Edge 浏览器架构 : {browser_arch}")
else:
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
def _mapFirefoxDriverVersion(
self,
version: str
) -> str:
version_mapping = [
(ver.Version("128.0"), ver.Version("999.0"), "0.36.0"),
(ver.Version("115.0"), ver.Version("127.0"), "0.35.0"),
(ver.Version("91.0"), ver.Version("114.0"), "0.34.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.33.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.32.0"),
(ver.Version("91.0"), ver.Version("120.0"), "0.31.0"),
(ver.Version("78.0"), ver.Version("90.0"), "0.30.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.29.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.28.0"),
(ver.Version("60.0"), ver.Version("90.0"), "0.27.0"),
(ver.Version("57.0"), ver.Version("90.0"), "0.26.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.25.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.24.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.23.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.22.0"),
(ver.Version("57.0"), ver.Version("79.0"), "0.21.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.20.0"),
(ver.Version("55.0"), ver.Version("62.0"), "0.19.0"),
(ver.Version("53.0"), ver.Version("62.0"), "0.18.0"),
(ver.Version("52.0"), ver.Version("62.0"), "0.17.0"),
]
try:
firefox_version = ver.Version(version)
for min_ver, max_ver, gecko_ver in version_mapping:
if min_ver <= firefox_version <= max_ver:
return gecko_ver
raise ValueError(
f"不支持的 Firefox 版本 : {version}"
f"Firefox 版本 52 及以上受支持"
)
except Exception as e:
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
def _getDriverInfo(
self,
browser_info: WebBrowserInfo
) -> WebDriverInfo:
driver_info = WebDriverInfo()
driver_info.driver_type = self._mapWebBrowserTypeToDriver(browser_info.browser_type)
driver_info.driver_arch = self._mapWebBrowserArchToDriver(browser_info.browser_type, browser_info.browser_arch)
if browser_info.browser_type == WebBrowserType.FIREFOX:
driver_info.driver_version = self._mapFirefoxDriverVersion(browser_info.browser_version)
else:
driver_info.driver_version = browser_info.browser_version
driver_info.browser_version = browser_info.browser_version
return driver_info
def _getDriverPath(
self,
driver_info: WebDriverInfo
) -> Optional[Path]:
driver_type = driver_info.driver_type
driver_arch = driver_info.driver_arch
driver_version = driver_info.driver_version
if driver_type == WebDriverType.CHROME:
driver_name = "chromedriver"
elif driver_type == WebDriverType.FIREFOX:
driver_name = "geckodriver"
elif driver_type == WebDriverType.EDGE:
driver_name = "msedgedriver"
else:
return None
is_win = driver_arch in [
WebDriverArch.Chrome.WINX86_32,
WebDriverArch.Chrome.WINX86_64,
WebDriverArch.Firefox.WINX86_32,
WebDriverArch.Firefox.WINX86_64,
WebDriverArch.Edge.WINX86_32,
WebDriverArch.Edge.WINX86_64,
]
exe_name = f"{driver_name}.exe" if is_win else driver_name
driver_dir = Path(self.__driver_dir)/driver_type.value/driver_version/driver_arch.value
driver_path = driver_dir/exe_name
return driver_path
def refresh(
self
):
self._detectBrowsers()
self._checkDriverStatus()
def getDriverInfos(
self
) -> list[WebDriverInfo]:
with self.__lock:
return self.__driver_infos.copy()
def getDriverInfo(
self,
driver_type: WebDriverType
) -> list[WebDriverInfo]:
with self.__lock:
return [
info
for info in self.__driver_infos
if info.driver_type == driver_type
]
def getDriverPath(
self,
driver_info: WebDriverInfo
) -> Optional[Path]:
if driver_info and driver_info.driver_status == DriverStatus.INSTALLED:
return driver_info.driver_path
return None
def installDriver(
self,
driver_info: WebDriverInfo,
progress_callback: Optional[Callable[[float, int, float, str], None]] = None,
cancel_event: Optional[threading.Event] = None
) -> Optional[Path]:
with self.__lock:
if not driver_info:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
if driver_info and driver_info.driver_status == DriverStatus.DOWNLOADING:
if progress_callback:
progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动正在下载中")
else:
raise ValueError(f"{driver_info.driver_type} 驱动正在下载中")
try:
if not driver_info:
raise ValueError("未找到浏览器信息")
driver_arch = driver_info.driver_arch
driver_type = driver_info.driver_type
driver_version = driver_info.driver_version
downloader = None
if driver_type == WebDriverType.CHROME:
downloader = ChromeDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
elif driver_type == WebDriverType.FIREFOX:
downloader = FirefoxDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
elif driver_type == WebDriverType.EDGE:
downloader = EdgeDriverDownloader(
version=driver_version,
arch=driver_arch,
download_dir=self.__driver_dir
)
if downloader is None:
if progress_callback:
progress_callback(0, 0, 0, f"不支持的 Web Driver 类型")
else:
raise ValueError(f"不支持的 Web Driver 类型")
with self.__lock:
driver_info.driver_status = DriverStatus.DOWNLOADING
driver_path = downloader.download(progress_callback=progress_callback, cancel_event=cancel_event)
with self.__lock:
if driver_path:
driver_info.driver_path = driver_path
driver_info.driver_version = driver_version
driver_info.driver_status = DriverStatus.INSTALLED
else:
driver_info.driver_status = DriverStatus.ERROR
return driver_path
except Exception as e:
with self.__lock:
driver_info.driver_status = DriverStatus.ERROR
raise e
def cancelDriverDownload(
self,
driver_info: WebDriverInfo
) -> bool:
import shutil
try:
driver_path = self._getDriverPath(driver_info)
if driver_path:
download_dir = driver_path.parent
if download_dir.exists():
shutil.rmtree(download_dir, ignore_errors=True)
with self.__lock:
driver_info.driver_path = None
driver_info.driver_status = DriverStatus.NOT_INSTALLED
return True
except Exception:
return False
def uninstallDriver(
self,
driver_info: WebDriverInfo,
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
) -> bool:
with self.__lock:
if not driver_info:
if progress_callback:
progress_callback(0, 0, 0, "未找到浏览器信息")
else:
raise ValueError("未找到浏览器信息")
if driver_info.driver_status != DriverStatus.INSTALLED:
if progress_callback:
progress_callback(0, 0, 0, f"{driver_info.driver_type} 驱动未安装")
else:
raise ValueError(f"{driver_info.driver_type} 驱动未安装")
try:
driver_path = driver_info.driver_path
driver_path.unlink()
with self.__lock:
driver_info.driver_path = None
driver_info.driver_status = DriverStatus.NOT_INSTALLED
return True
except Exception:
with self.__lock:
driver_info.driver_status = DriverStatus.ERROR
raise
def driverDir(
self
) -> str:
return self.__driver_dir
# WebDriverManager singleton instance.
_webdriver_manager_instance = None
# Singleton instance lock.
_instance_lock = threading.Lock()
def instance(
driver_dir: str = ""
) -> WebDriverManager:
global _webdriver_manager_instance
with _instance_lock:
if _webdriver_manager_instance is None:
if not driver_dir:
raise ValueError("WebDriverManager 需要驱动目录参数")
_webdriver_manager_instance = WebDriverManager(driver_dir)
else:
if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir):
raise ValueError("WebDriverManager 的实例已初始化, 不能使用不同的驱动目录")
return _webdriver_manager_instance
+8
View File
@@ -0,0 +1,8 @@
"""
Driver managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- WebBrowserDetector: Web browser detector class.
- WebDriverDownloader: Web driver downloader class.
- WebDriverManager: Web driver manager class.
"""
@@ -55,12 +55,17 @@ class CallerInfoFormatter(logging.Formatter):
if depth < len(record.stack_list):
frame = record.stack_list[-depth-1]
record.filename = os.path.basename(frame.filename)
record.lineno = frame.lineno
record.lineno = int(frame.lineno)
record.funcName = frame.name
record.name = record.name[-15:].ljust(15)
record.levelname = record.levelname.ljust(8)
record.filename = record.filename[-20:].ljust(20)
record.lineno = f"{record.lineno:04d}"
# Ensure lineno is always integer before formatting
try:
lineno_int = int(record.lineno)
except (ValueError, TypeError):
lineno_int = 0
record.lineno = f"{lineno_int:04d}"
return super().format(record)
@@ -174,11 +179,11 @@ def instance(
with _instance_lock:
if _log_manager_instance is None:
if not log_dir:
raise ValueError("LogManager initialization requires log_dir parameter")
raise ValueError("LogManager 需要日志目录参数")
_log_manager_instance = LogManager(log_dir)
else:
if log_dir and _log_manager_instance.logDir() != os.path.abspath(log_dir):
raise ValueError("LogManager instance already initialized with a different log directory")
raise ValueError("LogManager 的实例已初始化, 不能使用不同的日志目录")
return _log_manager_instance
@@ -187,5 +192,5 @@ def getLogger(
) -> logging.Logger:
if _log_manager_instance is None:
raise RuntimeError("LogManager not initialized, please call LogManager.instance(log_dir) first")
raise RuntimeError("LogManager 未初始化, 请先调用 LogManager.instance(log_dir) 初始化")
return _log_manager_instance.getLogger(name)
+6
View File
@@ -0,0 +1,6 @@
"""
Log managers module for the AutoLibrary project.
Here are the classes and modules in this package:
- LogManager: Log manager for logging.
"""
+1 -1
View File
@@ -2,7 +2,7 @@
Utils module for the AutoLibrary project.
Here are the classes and modules in this package:
- ConfigManager: Configuration manager class for the AutoLibrary project.
- TimerUtils: Timer utils class for the AutoLibrary project.
- JSONReader: JSON reader class for the AutoLibrary project.
- JSONWriter: JSON writer class for the AutoLibrary project.
"""