mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
@@ -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
@@ -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": {}
|
||||
}
|
||||
Binary file not shown.
+5
-14
@@ -7,26 +7,17 @@ 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 sys
|
||||
|
||||
from PySide6.QtCore import QTranslator, QStandardPaths, QDir
|
||||
from PySide6.QtCore import QTranslator
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from gui.ALMainWindow import ALMainWindow
|
||||
from gui.resources import ALResource
|
||||
|
||||
from utils.ConfigManager import instance
|
||||
from boot.AppInitializer import initializeApp
|
||||
|
||||
|
||||
def initializeConfigManager():
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
config_dir = os.path.join(app_dir, "config")
|
||||
if not QDir(config_dir).exists():
|
||||
QDir().mkpath(config_dir)
|
||||
instance(config_dir)
|
||||
|
||||
def main():
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
@@ -35,11 +26,11 @@ def main():
|
||||
app.installTranslator(translator)
|
||||
app.setStyle('Fusion')
|
||||
app.setApplicationName("AutoLibrary")
|
||||
initializeConfigManager()
|
||||
if not initializeApp():
|
||||
sys.exit(-1)
|
||||
window = ALMainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
+16
-10
@@ -9,6 +9,8 @@ See the LICENSE file for details.
|
||||
"""
|
||||
import queue
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from base.LibOperator import LibOperator
|
||||
|
||||
|
||||
@@ -29,25 +31,33 @@ class LibTimeSelector(LibOperator):
|
||||
super().__init__(input_queue, output_queue)
|
||||
|
||||
@staticmethod
|
||||
def _timeToMins(
|
||||
def _timeStrToMins(
|
||||
time_str: str
|
||||
) -> int:
|
||||
|
||||
"""
|
||||
Convert time string "HH:MM" to minutes since midnight.
|
||||
|
||||
Example:
|
||||
"10:00" -> 600
|
||||
"13:30" -> 810
|
||||
"""
|
||||
hour, minute = map(int, time_str.split(":"))
|
||||
return hour*60 + minute
|
||||
|
||||
@staticmethod
|
||||
def _minsToTime(
|
||||
def _minsToTimeStr(
|
||||
mins: int
|
||||
) -> str:
|
||||
|
||||
"""
|
||||
Convert minutes since midnight to time string "HH:MM".
|
||||
|
||||
Example:
|
||||
600 -> "10:00"
|
||||
810 -> "13:30"
|
||||
"""
|
||||
hour, minute = divmod(mins, 60)
|
||||
hour, minute = divmod(int(mins), 60)
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
|
||||
@@ -99,11 +109,11 @@ class LibTimeSelector(LibOperator):
|
||||
for time_opt in time_options:
|
||||
# Parse time value based on context
|
||||
if is_reserve:
|
||||
# Reservation context: parse 'time' attribute
|
||||
time_attr = time_opt.get_attribute("time")
|
||||
if time_attr == "now":
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
time_val = now.hour * 60 + now.minute
|
||||
time_val = now.hour*60 + now.minute
|
||||
elif time_attr and time_attr.isdigit():
|
||||
time_val = int(time_attr)
|
||||
else:
|
||||
@@ -114,9 +124,7 @@ class LibTimeSelector(LibOperator):
|
||||
if not (time_attr and time_attr.isdigit()):
|
||||
continue
|
||||
time_val = int(time_attr)
|
||||
|
||||
free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTime(time_val))
|
||||
|
||||
free_times.append(time_opt.text.strip() if not is_reserve else self._minsToTimeStr(time_val))
|
||||
actual_diff = time_val - target_time
|
||||
abs_diff = abs(actual_diff)
|
||||
|
||||
@@ -125,11 +133,9 @@ class LibTimeSelector(LibOperator):
|
||||
(abs_diff == best_time_diff and
|
||||
((prefer_earlier and actual_diff <= 0) or
|
||||
(not prefer_earlier and actual_diff >= 0)))):
|
||||
|
||||
best_time_diff = abs_diff
|
||||
best_actual_diff = actual_diff
|
||||
best_time_opt = time_opt
|
||||
|
||||
if best_time_opt is not None:
|
||||
return (best_time_opt, best_time_opt.text.strip(), best_actual_diff, free_times)
|
||||
return (None, None, None, free_times)
|
||||
|
||||
+34
-1
@@ -7,9 +7,12 @@ 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 logging
|
||||
import queue
|
||||
import datetime
|
||||
|
||||
from managers.log.LogManager import getLogger
|
||||
|
||||
|
||||
class MsgBase:
|
||||
"""
|
||||
@@ -29,6 +32,18 @@ class MsgBase:
|
||||
implement queue polling to retrieve and process messages.
|
||||
"""
|
||||
|
||||
class TraceLevel:
|
||||
"""
|
||||
Enum class for trace levels.
|
||||
|
||||
This class provides the trace levels for the logger.
|
||||
"""
|
||||
DEBUG = logging.DEBUG
|
||||
INFO = logging.INFO
|
||||
WARNING = logging.WARNING
|
||||
ERROR = logging.ERROR
|
||||
CRITICAL = logging.CRITICAL
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_queue: queue.Queue,
|
||||
@@ -38,6 +53,10 @@ class MsgBase:
|
||||
self._class_name = self.__class__.__name__
|
||||
self._input_queue = input_queue
|
||||
self._output_queue = output_queue
|
||||
try:
|
||||
self._logger = getLogger(self._class_name)
|
||||
except RuntimeError:
|
||||
self._logger = None
|
||||
|
||||
|
||||
def _showMsg(
|
||||
@@ -50,11 +69,25 @@ class MsgBase:
|
||||
|
||||
def _showTrace(
|
||||
self,
|
||||
msg: str
|
||||
msg: str,
|
||||
level: int = logging.INFO,
|
||||
no_log: bool = False
|
||||
):
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
self._output_queue.put(f"{timestamp}-[{self._class_name:<15}] : {msg}")
|
||||
if self._logger and not no_log:
|
||||
self._logger.log(level, msg)
|
||||
|
||||
|
||||
def _showLog(
|
||||
self,
|
||||
msg: str,
|
||||
level: int = logging.INFO
|
||||
):
|
||||
|
||||
if self._logger:
|
||||
self._logger.log(level, msg)
|
||||
|
||||
|
||||
def _waitMsg(
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2025 - 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
|
||||
|
||||
from PySide6.QtCore import QStandardPaths, QDir
|
||||
|
||||
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:
|
||||
|
||||
logger = logInstance().getLogger("AppInitializer")
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
old_config_dir = os.path.join(app_dir, "config")
|
||||
new_config_dir = os.path.join(app_dir, "configs")
|
||||
if QDir(old_config_dir).exists(): # old config dir exists
|
||||
#we rename it to compatible with new version
|
||||
logger.info("存在旧配置目录 %s,将其重命名为 %s", old_config_dir, new_config_dir)
|
||||
if not QDir().rename(old_config_dir, new_config_dir):
|
||||
logger.error("重命名旧配置目录 %s 到 %s 失败", old_config_dir, new_config_dir)
|
||||
return False
|
||||
elif not QDir(new_config_dir).exists():
|
||||
logger.info("初始化配置目录 %s", new_config_dir)
|
||||
if not QDir().mkpath(new_config_dir):
|
||||
logger.error("创建配置目录 %s 失败", new_config_dir)
|
||||
return False
|
||||
configInstance(new_config_dir)
|
||||
return True
|
||||
|
||||
def initializeWebDriverManager(
|
||||
) -> bool:
|
||||
|
||||
logger = logInstance().getLogger("AppInitializer")
|
||||
|
||||
app_dir = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation)
|
||||
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
|
||||
webdriverInstance(driver_dir)
|
||||
return True
|
||||
|
||||
def initializeApp(
|
||||
) -> bool:
|
||||
|
||||
if not initializeLogManager():
|
||||
return False
|
||||
if not initializeConfigManager():
|
||||
return False
|
||||
if not initializeWebDriverManager():
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Boot module for the AutoLibrary project.
|
||||
|
||||
Here are the classes and modules in this package:
|
||||
- AppInitializer: Application initializer class.
|
||||
"""
|
||||
@@ -8,7 +8,6 @@ You may use, modify, and distribute this file under the terms of the MIT License
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import (
|
||||
Qt, Signal, Slot, QTime, QDate, QDir, QFileInfo
|
||||
@@ -21,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
|
||||
@@ -30,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):
|
||||
@@ -81,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)
|
||||
@@ -949,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
|
||||
|
||||
+13
-5
@@ -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
|
||||
|
||||
@@ -59,6 +59,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.connectSignals()
|
||||
self.startMsgPolling()
|
||||
self.startTimerTaskPolling()
|
||||
self._showLog("主窗口初始化完成")
|
||||
|
||||
|
||||
def modifyUi(
|
||||
@@ -113,7 +114,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
):
|
||||
|
||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||
self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标")
|
||||
self._showTrace("操作系统不支持系统托盘功能, 无法创建系统托盘图标", self.TraceLevel.WARNING)
|
||||
return
|
||||
self.TrayIcon = QSystemTrayIcon(self.icon, self)
|
||||
self.TrayIcon.setToolTip("AutoLibrary")
|
||||
@@ -186,6 +187,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
if self.__alConfigWidget:
|
||||
self.__alConfigWidget.close()
|
||||
# the config widget is already deleted in the 'self.onConfigWidgetClosed'
|
||||
self._showLog("主窗口关闭")
|
||||
QMainWindow.closeEvent(self, event)
|
||||
|
||||
|
||||
@@ -298,7 +300,9 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.__alConfigWidget.configWidgetIsClosed.disconnect(self.onConfigWidgetClosed)
|
||||
self.__alConfigWidget.deleteLater()
|
||||
self.__alConfigWidget = None
|
||||
self.__config_paths = ConfigManager.getValidateAutomationConfigPaths()
|
||||
self.setControlButtons(True, None, None)
|
||||
self._showLog("配置窗口已关闭,配置文件路径已更新")
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsReady(
|
||||
@@ -330,7 +334,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
1000
|
||||
)
|
||||
self._showTrace(
|
||||
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['task_uuid']}"
|
||||
f"定时任务 {timer_task['name']} 执行{'失败' if is_error else '完成'}, uuid: {timer_task['uuid']}"
|
||||
)
|
||||
if not is_error:
|
||||
self.timerTaskIsExecuted.emit(timer_task)
|
||||
@@ -346,6 +350,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.__alTimerTaskManageWidget.raise_()
|
||||
self.__alTimerTaskManageWidget.activateWindow()
|
||||
self.TimerTaskManageWidgetButton.setEnabled(False)
|
||||
self._showLog("打开定时任务管理窗口")
|
||||
|
||||
@Slot()
|
||||
def onConfigButtonClicked(
|
||||
@@ -359,6 +364,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.__alConfigWidget.raise_()
|
||||
self.__alConfigWidget.activateWindow()
|
||||
self.ConfigButton.setEnabled(False)
|
||||
self._showLog("打开配置窗口")
|
||||
|
||||
@Slot()
|
||||
def onStartButtonClicked(
|
||||
@@ -375,6 +381,7 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
self.__auto_lib_thread.autoLibWorkerIsFinished.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.autoLibWorkerFinishedWithError.connect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.start()
|
||||
self._showLog("开始手动执行任务")
|
||||
|
||||
@Slot()
|
||||
def onStopButtonClicked(
|
||||
@@ -382,14 +389,15 @@ class ALMainWindow(MsgBase, QMainWindow, Ui_ALMainWindow):
|
||||
):
|
||||
|
||||
if self.__auto_lib_thread:
|
||||
self._showTrace("正在停止操作......")
|
||||
self._showTrace("正在停止操作......", no_log=True)
|
||||
self.__auto_lib_thread.wait(2000)
|
||||
self._showTrace("操作已停止")
|
||||
self._showTrace("操作已停止", no_log=True)
|
||||
self.__auto_lib_thread.autoLibWorkerIsFinished.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.autoLibWorkerFinishedWithError.disconnect(self.onStopButtonClicked)
|
||||
self.__auto_lib_thread.deleteLater()
|
||||
self.__auto_lib_thread = None
|
||||
self.setControlButtons(None, False, True)
|
||||
self._showLog("任务已停止")
|
||||
|
||||
@Slot()
|
||||
def onSendButtonClicked(
|
||||
|
||||
+29
-11
@@ -44,9 +44,11 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
current_time = time.strftime("%H:%M", time.localtime())
|
||||
if current_time >= "23:30" or current_time <= "07:30":
|
||||
self._showTrace(
|
||||
"当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试"
|
||||
"当前时间不在图书馆开放时间内, 请在 07:30 - 23:30 之间尝试",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return False
|
||||
self._showLog(f"时间检查通过, 当前时间: {current_time}", self.TraceLevel.INFO)
|
||||
return True
|
||||
|
||||
|
||||
@@ -57,8 +59,12 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
if not all(
|
||||
os.path.exists(path) for path in self.__config_paths.values()
|
||||
):
|
||||
self._showTrace("配置文件路径不存在, 请检查配置文件路径是否正确")
|
||||
self._showTrace(
|
||||
"配置文件路径不存在, 请检查配置文件路径是否正确",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
return False
|
||||
self._showLog(f"配置文件路径检查通过, 路径: {self.__config_paths}", self.TraceLevel.INFO)
|
||||
return True
|
||||
|
||||
|
||||
@@ -67,22 +73,28 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
) -> bool:
|
||||
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}"
|
||||
f"正在加载配置文件, 运行配置文件路径: {self.__config_paths["run"]}",
|
||||
no_log=True
|
||||
)
|
||||
self.__run_config = JSONReader(self.__config_paths["run"]).data()
|
||||
self._showTrace(
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}"
|
||||
f"正在加载配置文件, 用户配置文件路径: {self.__config_paths["user"]}",
|
||||
no_log=True
|
||||
)
|
||||
self.__user_config = JSONReader(self.__config_paths["user"]).data()
|
||||
if self.__run_config is None or self.__user_config is None:
|
||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||
self._showTrace("配置文件加载失败, 请检查配置文件是否正确")
|
||||
self._showTrace(
|
||||
"配置文件加载失败, 请检查配置文件是否正确",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
return False
|
||||
if not self.__user_config.get("groups"):
|
||||
self._showTrace(
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确"
|
||||
"用户配置文件中无有效任务组, 请检查用户配置文件是否正确",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return False
|
||||
self._showLog(f"配置文件加载成功, 任务组数量: {len(self.__user_config.get('groups', []))}", self.TraceLevel.INFO)
|
||||
return True
|
||||
|
||||
|
||||
@@ -108,14 +120,17 @@ class AutoLibWorker(MsgBase, QThread):
|
||||
groups = self.__user_config.get("groups")
|
||||
for group in groups:
|
||||
if not group["enabled"]:
|
||||
self._showTrace(f"任务组 {group["name"]} 已跳过")
|
||||
self._showTrace(f"任务组 {group["name"]} 已跳过", no_log=True)
|
||||
continue
|
||||
self._showTrace(f"正在运行任务组 {group["name"]}")
|
||||
self._showTrace(f"正在运行任务组 {group["name"]}", no_log=True)
|
||||
auto_lib.run(
|
||||
{ "users": group.get("users", []) }
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"AutoLibrary 运行时发生异常 : {e}")
|
||||
self._showTrace(
|
||||
f"AutoLibrary 运行时发生异常 : {e}",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
self.autoLibWorkerFinishedWithError.emit()
|
||||
return
|
||||
if auto_lib:
|
||||
@@ -154,7 +169,10 @@ class TimerTaskWorker(AutoLibWorker):
|
||||
self
|
||||
):
|
||||
|
||||
self._showTrace(f"定时任务 {self.__timer_task['name']} 运行时发生异常")
|
||||
self._showTrace(
|
||||
f"定时任务 {self.__timer_task['name']} 运行时发生异常",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
self.timerTaskWorkerIsFinished.emit(True, self.__timer_task)
|
||||
|
||||
@Slot()
|
||||
|
||||
@@ -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)
|
||||
@@ -121,11 +121,11 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
)
|
||||
task_data = {
|
||||
"name": name,
|
||||
"task_uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
|
||||
"uuid": uuid.uuid4().hex.upper() + f"-{added_time.strftime("%Y%m%d%H%M%S")}",
|
||||
"time_type": self.TimerTypeComboBox.currentText(),
|
||||
"execute_time": execute_time,
|
||||
"silent": silent,
|
||||
"add_time": added_time,
|
||||
"added_time": added_time,
|
||||
"status": ALTimerTaskStatus.PENDING,
|
||||
"executed": False,
|
||||
"repeat": self.RepeatCheckBox.isChecked(),
|
||||
@@ -158,7 +158,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
|
||||
task_data["repeat_minute"],
|
||||
task_data["repeat_second"]
|
||||
)
|
||||
|
||||
return task_data
|
||||
|
||||
@Slot(int)
|
||||
|
||||
@@ -47,16 +47,16 @@ class ALTimerTaskHistoryDialog(QDialog):
|
||||
MainLayout = QVBoxLayout(self)
|
||||
InfoLayout = QGridLayout()
|
||||
TaskNameLabel = QLabel(f"任务: {self.__task_data.get('name', '未命名')}")
|
||||
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
TaskNameLabel.setStyleSheet("font-weight: bold; font-size: 12px;")
|
||||
InfoLayout.addWidget(TaskNameLabel, 0, 0)
|
||||
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('task_uuid', '未命名')}")
|
||||
TaskUUIDLabel.setStyleSheet("font-size: 10px;")
|
||||
TaskUUIDLabel = QLabel(f"UUID: {self.__task_data.get('uuid', '未命名')}")
|
||||
TaskUUIDLabel.setStyleSheet("color: #969696; font-size: 11px;")
|
||||
InfoLayout.addWidget(TaskUUIDLabel, 1, 0)
|
||||
InfoLayout.setColumnStretch(0, 1)
|
||||
|
||||
if self.__task_data.get("repeat", False):
|
||||
RepeatLabel = QLabel("重复任务")
|
||||
RepeatLabel.setStyleSheet("color: #2294FF; font-weight: bold; font-size: 12px;")
|
||||
RepeatLabel = QLabel("可重复性任务")
|
||||
RepeatLabel.setStyleSheet("color: #2294FF; font-size: 12px;")
|
||||
InfoLayout.addWidget(RepeatLabel, 0, 1)
|
||||
MainLayout.addLayout(InfoLayout)
|
||||
self.HistoryTableWidget = QTableWidget()
|
||||
|
||||
@@ -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
|
||||
@@ -221,7 +221,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
timer_tasks = self.__cfg_mgr.get(ConfigManager.ConfigType.TIMERTASK)
|
||||
if timer_tasks and "timer_tasks" in timer_tasks:
|
||||
for task in timer_tasks["timer_tasks"]:
|
||||
task["add_time"] = datetime.strptime(task["add_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["added_time"] = datetime.strptime(task["added_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = datetime.strptime(task["execute_time"], "%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = ALTimerTaskStatus(task["status"])
|
||||
if "history" in task:
|
||||
@@ -245,7 +245,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
|
||||
try:
|
||||
for task in timer_tasks:
|
||||
task["add_time"] = task["add_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["added_time"] = task["added_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["execute_time"] = task["execute_time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
task["status"] = task["status"].value
|
||||
if "history" in task:
|
||||
@@ -309,7 +309,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
)
|
||||
elif policy == self.SortPolicy.BY_ADD_TIME:
|
||||
self.__timer_tasks.sort(
|
||||
key = lambda x: x["add_time"],
|
||||
key = lambda x: x["added_time"],
|
||||
reverse = order is Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
elif policy == self.SortPolicy.BY_EXECUTE_TIME:
|
||||
@@ -378,7 +378,6 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
self.__timer_tasks.append(timer_task)
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def getTimerTaskDetailMessage(
|
||||
timer_task: dict
|
||||
@@ -386,10 +385,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
|
||||
return (
|
||||
f"任务名称:{timer_task["name"]}\n"
|
||||
f"添加时间:{timer_task["add_time"]}\n"
|
||||
f"添加时间:{timer_task["added_time"]}\n"
|
||||
f"当前状态:{timer_task["status"].value}\n"
|
||||
f"下次执行时间:{datetime.strftime(timer_task["execute_time"], "%Y-%m-%d %H:%M:%S")}\n"
|
||||
f"已执行次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
|
||||
f"已记录次数:{len(timer_task['history'] if 'history' in timer_task else 0)}"
|
||||
)
|
||||
|
||||
|
||||
@@ -414,10 +413,10 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
result = msgbox.exec()
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
task_uuid = timer_task["task_uuid"]
|
||||
task_uuid = timer_task["uuid"]
|
||||
self.__timer_tasks = [
|
||||
x for x in self.__timer_tasks
|
||||
if x["task_uuid"] != task_uuid
|
||||
if x["uuid"] != task_uuid
|
||||
]
|
||||
self.timerTasksChanged.emit()
|
||||
|
||||
@@ -447,7 +446,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"警告 - AutoLibrary",
|
||||
f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
|
||||
f"存在 {in_queue_count} 个正在执行或已就绪的队列任务,无法清除所有定时任务 !"
|
||||
)
|
||||
return
|
||||
# repeat tasks ask before clear
|
||||
@@ -464,7 +463,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
msgbox.setText(
|
||||
f"存在 {repeat_tasks_count} 个可重复性任务,\n"
|
||||
f"存在 {repeat_tasks_count} 个可重复性任务,\n"
|
||||
"删除可重复性任务将同时删除所有已执行的记录 !\n"
|
||||
"是否继续 ?"
|
||||
)
|
||||
@@ -563,7 +562,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
task["status"] = ALTimerTaskStatus.RUNNING
|
||||
break
|
||||
self.timerTasksChanged.emit()
|
||||
@@ -575,17 +574,37 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
timer_task: dict
|
||||
) -> dict:
|
||||
|
||||
# only these status are valid
|
||||
valid_statuses = {ALTimerTaskStatus.EXECUTED, ALTimerTaskStatus.ERROR,
|
||||
ALTimerTaskStatus.OUTDATED}
|
||||
if status not in valid_statuses:
|
||||
return timer_task
|
||||
if "history" not in timer_task:
|
||||
timer_task["history"] = []
|
||||
executed_time = datetime.now()
|
||||
duration = (executed_time - timer_task["execute_time"]).total_seconds()
|
||||
timer_task["history"].append({
|
||||
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"result": status,
|
||||
"duration": duration if status is ALTimerTaskStatus.EXECUTED else 0,
|
||||
"uuid": timer_task["task_uuid"]
|
||||
})
|
||||
if status != ALTimerTaskStatus.OUTDATED:
|
||||
executed_time = datetime.now()
|
||||
duration = (executed_time - timer_task["execute_time"]).total_seconds()
|
||||
timer_task["history"].append({
|
||||
"execute_time": timer_task["execute_time"].strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"executed_time": executed_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"result": status,
|
||||
"duration": duration,
|
||||
"uuid": timer_task["uuid"]
|
||||
})
|
||||
else:
|
||||
current_time = datetime.now()
|
||||
execute_time = timer_task["execute_time"]
|
||||
execute_weekday = execute_time.weekday()
|
||||
delta_days = (current_time - execute_time).days
|
||||
for i in range(delta_days + 1):
|
||||
if (execute_weekday + i)%7 in timer_task["repeat_days"]:
|
||||
timer_task["history"].append({
|
||||
"execute_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"executed_time": (execute_time + timedelta(days=i)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"result": status,
|
||||
"duration": 0,
|
||||
"uuid": timer_task["uuid"]
|
||||
})
|
||||
next_time = TimerUtils.calculateNextRepeatTime(
|
||||
timer_task["repeat_days"],
|
||||
timer_task["repeat_hour"],
|
||||
@@ -598,6 +617,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
timer_task["executed"] = False
|
||||
else:
|
||||
timer_task["status"] = status
|
||||
return timer_task
|
||||
|
||||
@Slot(dict)
|
||||
def onTimerTaskIsExecuted(
|
||||
@@ -606,7 +626,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
if task.get("repeat", False):
|
||||
self.onRepeatTimerTaskIs(ALTimerTaskStatus.EXECUTED, task)
|
||||
else:
|
||||
@@ -621,7 +641,7 @@ class ALTimerTaskManageWidget(QWidget, Ui_ALTimerTaskManageWidget):
|
||||
):
|
||||
|
||||
for task in self.__timer_tasks:
|
||||
if task["task_uuid"] == timer_task["task_uuid"]:
|
||||
if task["uuid"] == timer_task["uuid"]:
|
||||
if task.get("repeat", False):
|
||||
self.onRepeatTimerTaskIs(ALTimerTaskStatus.ERROR, task)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
# -*- 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,
|
||||
WebDriverStatus
|
||||
)
|
||||
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()
|
||||
installed_idx = 0
|
||||
for i, driver_info in enumerate(self.__driver_infos):
|
||||
if driver_info.driver_status == WebDriverStatus.INSTALLED:
|
||||
installed_idx = i # get the installed driver index
|
||||
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.DriverComboBox.setCurrentIndex(installed_idx)
|
||||
|
||||
|
||||
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.updateProgressBarStates(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
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(False)
|
||||
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 == WebDriverStatus.INSTALLED:
|
||||
return
|
||||
driver_info.driver_status = WebDriverStatus.DOWNLOADING # we set this only to update
|
||||
# the display, and we will set to not installed in the download thread
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.ProgressText.setText("准备开始下载...")
|
||||
self.updateButtonStates(driver_info)
|
||||
# set to not installed
|
||||
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
|
||||
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.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_infos):
|
||||
driver_info = self.__driver_infos[index]
|
||||
driver_info.driver_status = WebDriverStatus.INSTALLED
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
|
||||
@Slot()
|
||||
def onDownloadError(
|
||||
self,
|
||||
error_message: str
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_infos):
|
||||
driver_info = self.__driver_infos[index]
|
||||
driver_info.driver_status = WebDriverStatus.ERROR
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
QMessageBox.critical(self, "下载失败 - AutoLibrary", error_message)
|
||||
|
||||
|
||||
@Slot()
|
||||
def onDownloadCancelled(
|
||||
self
|
||||
):
|
||||
|
||||
self.DriverComboBox.setEnabled(True)
|
||||
index = self.DriverComboBox.currentIndex()
|
||||
if 0 <= index < len(self.__driver_infos):
|
||||
driver_info = self.__driver_infos[index]
|
||||
self.__driver_manager.cancelDriverDownload(driver_info)
|
||||
driver_info.driver_status = WebDriverStatus.NOT_INSTALLED
|
||||
self.updateDriverInfoDisplay(driver_info)
|
||||
self.updateProgressBarStates(driver_info)
|
||||
self.updateButtonStates(driver_info)
|
||||
self.ProgressText.setText("下载已取消")
|
||||
|
||||
|
||||
@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 != WebDriverStatus.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:
|
||||
case WebDriverStatus.NOT_INSTALLED:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.WAITING
|
||||
case WebDriverStatus.INSTALLED:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.SUCCESS
|
||||
case WebDriverStatus.DOWNLOADING:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.RUNNING
|
||||
case WebDriverStatus.ERROR:
|
||||
self.StatusLabel.status = ALStatusLabel.Status.FAILURE
|
||||
|
||||
|
||||
def updateProgressBarStates(
|
||||
self,
|
||||
driver_info: WebDriverInfo
|
||||
):
|
||||
|
||||
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
|
||||
self.ProgressBar.setValue(0)
|
||||
self.ProgressText.setText("未安装")
|
||||
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
|
||||
self.ProgressBar.setValue(100)
|
||||
self.ProgressText.setText("已安装")
|
||||
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
|
||||
pass # update by worker thread
|
||||
elif driver_info.driver_status == WebDriverStatus.ERROR:
|
||||
self.ProgressBar.setValue(0)
|
||||
self.ProgressText.setText("下载失败")
|
||||
|
||||
|
||||
def updateButtonStates(
|
||||
self,
|
||||
driver_info: WebDriverInfo
|
||||
):
|
||||
|
||||
if driver_info.driver_status == WebDriverStatus.NOT_INSTALLED:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.DownloadButton.setEnabled(True)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
elif driver_info.driver_status == WebDriverStatus.INSTALLED:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DownloadButton.setEnabled(False)
|
||||
self.DeleteButton.setEnabled(True)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(True)
|
||||
elif driver_info.driver_status == WebDriverStatus.DOWNLOADING:
|
||||
self.RefreshButton.setEnabled(False)
|
||||
self.DownloadButton.setEnabled(False)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
elif driver_info.driver_status == WebDriverStatus.ERROR:
|
||||
self.RefreshButton.setEnabled(True)
|
||||
self.DownloadButton.setEnabled(True)
|
||||
self.DeleteButton.setEnabled(False)
|
||||
self.CancelButton.setEnabled(True)
|
||||
self.ConfirmButton.setEnabled(False)
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
GUI resources module for the AutoLibrary project.
|
||||
"""
|
||||
@@ -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><html><head/><body><p>脚本运行使用的浏览器类型</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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><html><head/><body><p>运行时不显示浏览器</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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><html><head/><body><p>脚本运行使用的浏览器类型</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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><html><head/><body><p>详情请参阅 <a href="https://www.autolibrary.kenanzhu.com/manuals"><span style=" text-decoration: underline; color:#69fcff;">用户手册</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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><html><head/><body><p>运行时不显示浏览器</p></body></html></string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p><br/></p></body></html></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">
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 WebDriverStatus(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 = WebDriverStatus.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 = WebDriverStatus.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 == WebDriverStatus.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 == WebDriverStatus.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 = WebDriverStatus.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 = WebDriverStatus.INSTALLED
|
||||
else:
|
||||
driver_info.driver_status = WebDriverStatus.ERROR
|
||||
return driver_path
|
||||
except Exception as e:
|
||||
with self.__lock:
|
||||
driver_info.driver_status = WebDriverStatus.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 = WebDriverStatus.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 != WebDriverStatus.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 = WebDriverStatus.NOT_INSTALLED
|
||||
return True
|
||||
except Exception:
|
||||
with self.__lock:
|
||||
driver_info.driver_status = WebDriverStatus.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
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- 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 logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CallerInfoFormatter(logging.Formatter):
|
||||
"""
|
||||
Custom formatter to extract real caller information.
|
||||
Skips MsgBase._showTrace to show the actual calling location.
|
||||
|
||||
Format:
|
||||
- Logger name: left-aligned, max 15 chars
|
||||
- Level name: left-aligned, max 8 chars
|
||||
- Filename: left-aligned, max 20 chars
|
||||
- Line number: left-aligned, max 4 digits
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fmt=None,
|
||||
datefmt=None,
|
||||
style='%'
|
||||
):
|
||||
|
||||
super().__init__(fmt, datefmt, style)
|
||||
self.basefmt = fmt
|
||||
|
||||
def format(
|
||||
self,
|
||||
record
|
||||
):
|
||||
|
||||
depth = 0
|
||||
while depth < 10:
|
||||
record.filename = os.path.basename(record.pathname)
|
||||
if 'MsgBase.py' not in record.filename and record.funcName != '_showTrace':
|
||||
break
|
||||
if not hasattr(record, 'stack'):
|
||||
record.stack = True
|
||||
import traceback
|
||||
record.stack_list = traceback.extract_stack()
|
||||
depth += 1
|
||||
if depth < len(record.stack_list):
|
||||
frame = record.stack_list[-depth-1]
|
||||
record.filename = os.path.basename(frame.filename)
|
||||
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)
|
||||
# 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)
|
||||
|
||||
|
||||
class LogManager:
|
||||
"""
|
||||
Log Manager Singleton Class
|
||||
|
||||
Args:
|
||||
log_dir (str): The directory to store log files.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_dir: str
|
||||
):
|
||||
|
||||
self.__log_dir = os.path.abspath(log_dir)
|
||||
self.__logger = None
|
||||
self.__initialized = False
|
||||
|
||||
self.initialize()
|
||||
|
||||
|
||||
def initialize(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__initialized:
|
||||
return
|
||||
os.makedirs(self.__log_dir, exist_ok=True)
|
||||
self.__logger = logging.getLogger("AutoLibrary")
|
||||
self.__logger.setLevel(logging.DEBUG)
|
||||
self.__logger.handlers.clear()
|
||||
|
||||
formatter = CallerInfoFormatter(
|
||||
'[%(asctime)s] - [%(name)s] - [%(levelname)s] - [%(filename)s:%(lineno)s] - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
self.__logger.addHandler(console_handler)
|
||||
|
||||
all_log_file = os.path.join(self.__log_dir, "all.log")
|
||||
file_handler_all = TimedRotatingFileHandler(
|
||||
all_log_file,
|
||||
when='midnight',
|
||||
interval=1,
|
||||
backupCount=7,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler_all.suffix = "%Y-%m-%d.log"
|
||||
file_handler_all.setLevel(logging.DEBUG)
|
||||
file_handler_all.setFormatter(formatter)
|
||||
self.__logger.addHandler(file_handler_all)
|
||||
|
||||
error_log_file = os.path.join(self.__log_dir, "error.log")
|
||||
file_handler_error = TimedRotatingFileHandler(
|
||||
error_log_file,
|
||||
when='midnight',
|
||||
interval=1,
|
||||
backupCount=14,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler_error.suffix = "%Y-%m-%d.log"
|
||||
file_handler_error.setLevel(logging.ERROR)
|
||||
file_handler_error.setFormatter(formatter)
|
||||
self.__logger.addHandler(file_handler_error)
|
||||
|
||||
self.__initialized = True
|
||||
|
||||
|
||||
def getLogger(
|
||||
self,
|
||||
name: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
|
||||
if name:
|
||||
return self.__logger.getChild(name)
|
||||
return self.__logger
|
||||
|
||||
|
||||
def setLevel(
|
||||
self,
|
||||
level: int
|
||||
):
|
||||
|
||||
if self.__logger:
|
||||
self.__logger.setLevel(level)
|
||||
|
||||
|
||||
def logDir(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.__log_dir
|
||||
|
||||
|
||||
# LogManager singleton instance.
|
||||
_log_manager_instance = None
|
||||
|
||||
# Singleton instance lock.
|
||||
_instance_lock = threading.Lock()
|
||||
def instance(
|
||||
log_dir: str = ""
|
||||
) -> LogManager:
|
||||
|
||||
global _log_manager_instance
|
||||
with _instance_lock:
|
||||
if _log_manager_instance is None:
|
||||
if not log_dir:
|
||||
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 的实例已初始化, 不能使用不同的日志目录")
|
||||
return _log_manager_instance
|
||||
|
||||
|
||||
def getLogger(
|
||||
name: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
|
||||
if _log_manager_instance is None:
|
||||
raise RuntimeError("LogManager 未初始化, 请先调用 LogManager.instance(log_dir) 初始化")
|
||||
return _log_manager_instance.getLogger(name)
|
||||
@@ -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.
|
||||
"""
|
||||
+44
-21
@@ -54,7 +54,7 @@ class AutoLib(MsgBase):
|
||||
self
|
||||
) -> bool:
|
||||
|
||||
self._showTrace("正在初始化浏览器驱动......")
|
||||
self._showTrace("正在初始化浏览器驱动......", no_log=True)
|
||||
|
||||
web_driver_config = self.__run_config.get("web_driver", None)
|
||||
self.__driver_type = web_driver_config.get("driver_type")
|
||||
@@ -66,11 +66,14 @@ class AutoLib(MsgBase):
|
||||
case "firefox":
|
||||
driver_options = webdriver.FirefoxOptions()
|
||||
case _:
|
||||
self._showTrace(f"不支持的浏览器驱动类型: {self.__driver_type} !")
|
||||
self._showTrace(
|
||||
f"不支持的浏览器驱动类型: {self.__driver_type} !",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return False
|
||||
|
||||
if not web_driver_config:
|
||||
self._showTrace("未配置浏览器驱动参数 !")
|
||||
self._showTrace("未配置浏览器驱动参数 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
if web_driver_config.get("headless"):
|
||||
driver_options.add_argument("--headless")
|
||||
@@ -110,7 +113,7 @@ class AutoLib(MsgBase):
|
||||
# init browser driver
|
||||
self.__driver_path = web_driver_config.get("driver_path")
|
||||
if not self.__driver_path:
|
||||
self._showTrace("未配置浏览器驱动路径 !")
|
||||
self._showTrace("未配置浏览器驱动路径 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
self.__driver_path = os.path.abspath(self.__driver_path)
|
||||
try:
|
||||
@@ -123,18 +126,18 @@ class AutoLib(MsgBase):
|
||||
service = ChromeService(executable_path=self.__driver_path)
|
||||
self.__driver = webdriver.Chrome(service=service, options=driver_options)
|
||||
case "firefox":
|
||||
self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...")
|
||||
self._showTrace(f"Firefox 浏览器驱动初始化略慢, 请耐心等待...", no_log=True)
|
||||
service = FirefoxService(executable_path=self.__driver_path)
|
||||
self.__driver = webdriver.Firefox(service=service, options=driver_options)
|
||||
case _: # actually will not happen, beacuse we have checked it at the initlization
|
||||
# of 'driver_options'
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type}")
|
||||
raise Exception(f"不支持的浏览器驱动类型: {self.__driver_type} !")
|
||||
self.__driver.implicitly_wait(1)
|
||||
self.__driver.execute_script(
|
||||
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
||||
)
|
||||
except Exception as e:
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}")
|
||||
self._showTrace(f"浏览器驱动初始化失败: {e}", self.TraceLevel.ERROR)
|
||||
return False
|
||||
self._showTrace(f"浏览器驱动已初始化, 类型: {self.__driver_type}, 路径: {self.__driver_path}")
|
||||
return True
|
||||
@@ -145,7 +148,7 @@ class AutoLib(MsgBase):
|
||||
):
|
||||
|
||||
if not self.__driver:
|
||||
self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !")
|
||||
self._showTrace(f"浏览器驱动未初始化, 请先初始化浏览器驱动 !", self.TraceLevel.WARNING)
|
||||
return
|
||||
self.__lib_checker = LibChecker(self._input_queue, self._output_queue, self.__driver)
|
||||
self.__lib_login = LibLogin(self._input_queue, self._output_queue, self.__driver)
|
||||
@@ -178,7 +181,7 @@ class AutoLib(MsgBase):
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 !")
|
||||
self._showTrace(f"登录页面加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -188,7 +191,7 @@ class AutoLib(MsgBase):
|
||||
|
||||
lib_config = self.__run_config.get("library", None)
|
||||
if not lib_config:
|
||||
self._showTrace("未配置图书馆参数 !")
|
||||
self._showTrace("未配置图书馆参数 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
url = lib_config.get("host_url") + lib_config.get("login_url")
|
||||
self.__driver.set_page_load_timeout(5)
|
||||
@@ -196,7 +199,9 @@ class AutoLib(MsgBase):
|
||||
self.__driver.get(url)
|
||||
except TimeoutException:
|
||||
self.__driver.execute_script("window.stop();")
|
||||
self._showTrace(f"图书馆登录页面加载超时 ! 请检查网络环境是否正常")
|
||||
self._showTrace(
|
||||
f"图书馆登录页面加载超时 ! 请检查网络环境是否正常", self.TraceLevel.ERROR
|
||||
)
|
||||
return False
|
||||
if not self.__waitResponseLoad():
|
||||
return False
|
||||
@@ -238,32 +243,45 @@ class AutoLib(MsgBase):
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法预约,已跳过")
|
||||
self._showTrace(f"用户 {username} 无法预约, 已跳过")
|
||||
result = 2
|
||||
|
||||
# checkin
|
||||
if run_mode["auto_checkin"] and result != 1:
|
||||
last_result = result
|
||||
if run_mode["auto_checkin"] and last_result != 1:
|
||||
if self.__lib_checker.canCheckin():
|
||||
if self.__lib_checkin.checkin(username):
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法签到,已跳过")
|
||||
self._showTrace(f"用户 {username} 无法签到, 已跳过")
|
||||
result = 2
|
||||
if last_result == 0: # partly success
|
||||
result = 0
|
||||
|
||||
# renewal
|
||||
if run_mode["auto_renewal"] and result != 1:
|
||||
last_result = result
|
||||
if run_mode["auto_renewal"] and last_result != 1:
|
||||
can_renew, record = self.__lib_checker.canRenew()
|
||||
if can_renew:
|
||||
if self.__lib_renew.renew(username, record, reserve_info):
|
||||
if self.__lib_checker.postRenewCheck(record):
|
||||
self._showTrace(f"用户 {username} 续约成功 !")
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
if result != 1: # partly success
|
||||
result = 0
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
result = 1
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 无法续约,已跳过")
|
||||
self._showTrace(f"用户 {username} 无法续约, 已跳过")
|
||||
result = 2
|
||||
if last_result == 0: # partly success
|
||||
result = 0
|
||||
|
||||
# logout
|
||||
if not self.__lib_logout.logout(
|
||||
username
|
||||
@@ -288,7 +306,8 @@ class AutoLib(MsgBase):
|
||||
for user in users:
|
||||
user_counter["current"] += 1
|
||||
self._showTrace(
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......"
|
||||
f"正在处理第 {user_counter["current"]}/{len(users)} 个用户: {user["username"]}......",
|
||||
no_log=True
|
||||
)
|
||||
if not user["enabled"]:
|
||||
self._showTrace(f"用户 {user["username"]} 已跳过")
|
||||
@@ -303,7 +322,8 @@ class AutoLib(MsgBase):
|
||||
)
|
||||
if r == -1:
|
||||
self._showTrace(
|
||||
f"用户 {user["username"]} 处理过程中页面发生异常,无法继续操作, 任务已终止 !"
|
||||
f"用户 {user["username"]} 处理过程中页面发生异常, 无法继续操作, 任务已终止 !",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
break
|
||||
elif r == 0:
|
||||
@@ -326,11 +346,14 @@ class AutoLib(MsgBase):
|
||||
|
||||
if self.__driver:
|
||||
if self.__driver_type.lower() == "firefox":
|
||||
self._showTrace(f"Firefox 浏览器驱动关闭略慢, 请耐心等待...")
|
||||
self._showTrace(
|
||||
f"Firefox 浏览器驱动关闭略慢, 请耐心等待...",
|
||||
no_log=True
|
||||
)
|
||||
self.__driver.quit()
|
||||
self.__driver = None
|
||||
self._showTrace(f"浏览器驱动已关闭")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"浏览器驱动未初始化, 无需关闭")
|
||||
self._showTrace(f"浏览器驱动未初始化, 无需关闭", no_log=True)
|
||||
return False
|
||||
@@ -63,7 +63,7 @@ class LibChecker(LibOperator):
|
||||
EC.presence_of_element_located((By.CLASS_NAME, "myReserveList"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("加载预约记录页面失败 !")
|
||||
self._showTrace("加载预约记录页面失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -174,7 +174,7 @@ class LibChecker(LibOperator):
|
||||
)
|
||||
return reservations
|
||||
except:
|
||||
self._showTrace("加载预约记录失败 !")
|
||||
self._showTrace("加载预约记录失败 !", self.TraceLevel.ERROR)
|
||||
return None
|
||||
|
||||
|
||||
@@ -197,10 +197,10 @@ class LibChecker(LibOperator):
|
||||
self.__driver.execute_script("arguments[0].click();", more_btn)
|
||||
return True
|
||||
else:
|
||||
self._showTrace("用户无法加载更多预约记录")
|
||||
self._showTrace("用户无法加载更多预约记录", self.TraceLevel.WARNING)
|
||||
return False
|
||||
except:
|
||||
self._showTrace("加载更多预约记录失败 !")
|
||||
self._showTrace("加载更多预约记录失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -211,9 +211,9 @@ class LibChecker(LibOperator):
|
||||
) -> dict:
|
||||
|
||||
if wanted_date is None:
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态")
|
||||
self._showTrace("日期未指定, 无法检查当前预约状态", self.TraceLevel.WARNING)
|
||||
return None
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......")
|
||||
self._showTrace(f"正在检查用户在 {wanted_date} 是否有预约状态为 {wanted_status} 的预约记录......", no_log=True)
|
||||
|
||||
checked_count = 0
|
||||
max_check_times = 6 # we only check (4*(6-1)=)20 reservations, the last time cant be checked
|
||||
@@ -245,7 +245,8 @@ class LibChecker(LibOperator):
|
||||
self._showTrace(
|
||||
f"寻找到用户第 {checked_count} 条状态为 {wanted_status} 的预约记录, "
|
||||
f"详细信息: {record["date"]} "
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}"
|
||||
f"{record["time"]["begin"]} - {record["time"]["end"]} {record["info"]["location"]}",
|
||||
no_log=True
|
||||
)
|
||||
return record
|
||||
if not self.__showMoreReserveRecords():
|
||||
@@ -369,7 +370,7 @@ class LibChecker(LibOperator):
|
||||
else:
|
||||
self._showTrace(f"\n"\
|
||||
f" 续约失败 !\n"\
|
||||
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
|
||||
f" 续约后结束时间为 {act_record["time"]["end"]},与预期结束时间 {record["time"]["end"]} 不符 !"
|
||||
)
|
||||
return False
|
||||
self._showTrace(f"用户在 {date} 没有有效预约记录, 无法检查续约结果")
|
||||
|
||||
@@ -51,7 +51,7 @@ class LibCheckin(LibOperator):
|
||||
)
|
||||
ok_btn = self.__driver.find_element(By.CLASS_NAME, "btnOK")
|
||||
except:
|
||||
self._showTrace("签到时发生未知错误 !")
|
||||
self._showTrace("签到时发生未知错误 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
result_message = result_message_element.text
|
||||
if "签到成功" in result_message:
|
||||
@@ -107,9 +107,9 @@ class LibCheckin(LibOperator):
|
||||
result = self.__driver.execute_script(script)
|
||||
time.sleep(0.1)
|
||||
if result:
|
||||
self._showTrace("签到按钮已启用")
|
||||
self._showTrace("签到按钮已启用", no_log=True)
|
||||
else:
|
||||
self._showTrace("签到按钮启用失败")
|
||||
self._showTrace("签到按钮启用失败", self.TraceLevel.WARNING)
|
||||
return result
|
||||
|
||||
|
||||
@@ -119,24 +119,24 @@ class LibCheckin(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
try:
|
||||
checkin_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnCheckIn"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 签到界面加载失败 !")
|
||||
self._showTrace(f"用户 {username} 签到界面加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
if "disabled" in checkin_btn.get_attribute("class"):
|
||||
self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......")
|
||||
self._showTrace("签到按钮不可用, 可能不在场馆内, 正在尝试启用......", no_log=True)
|
||||
if not self.__enableCheckinBtn():
|
||||
self._showTrace(f"签到按钮启用失败 !")
|
||||
self._showTrace(f"签到按钮启用失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
checkin_btn.click()
|
||||
if self._waitResponseLoad():
|
||||
self._showTrace(f"用户 {username} 签到成功 !")
|
||||
self._showTrace(f"用户 {username} 签到成功 !", no_log=True)
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 签到失败 !")
|
||||
self._showTrace(f"用户 {username} 签到失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
+23
-15
@@ -52,7 +52,10 @@ class LibLogin(LibOperator):
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准")
|
||||
self._showTrace(
|
||||
f"登录页面加载失败 ! : 用户账号或者密码错误/验证码错误, 具体以页面提示为准",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@@ -71,7 +74,7 @@ class LibLogin(LibOperator):
|
||||
password_element.clear()
|
||||
password_element.send_keys(password)
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户名或密码填写失败 ! : {e}")
|
||||
self._showTrace(f"用户名或密码填写失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -88,12 +91,13 @@ class LibLogin(LibOperator):
|
||||
captcha_img = base64.b64decode(base64_str)
|
||||
captcha_text = self.__ddddocr.classification(captcha_img)
|
||||
captcha_text = ''.join(filter(str.isalnum, captcha_text)).lower()
|
||||
self._showTrace(f"识别到验证码为 : '{captcha_text}'")
|
||||
self._showTrace(f"识别到验证码为 : '{captcha_text}'", no_log=True)
|
||||
if len(captcha_text) != 4:
|
||||
self._showLog("识别到的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
|
||||
raise Exception("识别到的验证码长度不等于 4 个字符 !")
|
||||
return captcha_text
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码识别失败 ! : {e}")
|
||||
self._showTrace(f"验证码识别失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -105,12 +109,13 @@ class LibLogin(LibOperator):
|
||||
try:
|
||||
self._showMsg("请输入验证码:")
|
||||
captcha_text = self._waitMsg(timeout=15)
|
||||
self._showTrace(f"输入的验证码为 : '{captcha_text}'")
|
||||
self._showTrace(f"输入的验证码为 : '{captcha_text}'", no_log=True)
|
||||
if len(captcha_text) != 4:
|
||||
self._showLog("输入的验证码长度不等于 4 个字符 !", self.TraceLevel.WARNING)
|
||||
raise Exception("输入的验证码长度不等于 4 个字符 !")
|
||||
return captcha_text
|
||||
except Exception as e:
|
||||
self._showTrace(f"输入验证码失败 ! : {e}")
|
||||
self._showTrace(f"输入验证码失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -120,13 +125,13 @@ class LibLogin(LibOperator):
|
||||
|
||||
# refresh captcha
|
||||
try:
|
||||
self._showTrace("刷新验证码......")
|
||||
self._showTrace("刷新验证码......", no_log=True)
|
||||
self.__driver.find_element(
|
||||
By.ID, "loadImgId"
|
||||
).click()
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"刷新验证码失败 ! : {e}")
|
||||
self._showTrace(f"刷新验证码失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -140,14 +145,17 @@ class LibLogin(LibOperator):
|
||||
if auto_captcha:
|
||||
captcha_text = self.__autoRecognizeCaptcha()
|
||||
else:
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !")
|
||||
self._showTrace(f"用户未配置自动识别验证码, 请手动输入验证码 !", no_log=True)
|
||||
captcha_text = self.__manualRecognizeCaptcha()
|
||||
if captcha_text:
|
||||
return captcha_text
|
||||
else:
|
||||
if not self.__refreshCaptcha():
|
||||
return ""
|
||||
self._showTrace(f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !")
|
||||
self._showTrace(
|
||||
f"验证码识别失败 {max_attempts} 次, 达到最大尝试次数 !",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -162,7 +170,7 @@ class LibLogin(LibOperator):
|
||||
captcha_element.send_keys(captcha_text)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"验证码填写失败 ! : {e}")
|
||||
self._showTrace(f"验证码填写失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -175,11 +183,11 @@ class LibLogin(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
# begin login process
|
||||
for attempt in range(max_attempts):
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......")
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次尝试登录......", no_log=True)
|
||||
if not self.__fillLogInElements(
|
||||
username,
|
||||
password,
|
||||
@@ -190,7 +198,7 @@ class LibLogin(LibOperator):
|
||||
continue
|
||||
if not self.__fillCaptchaElement(captcha_text):
|
||||
continue
|
||||
self._showTrace("尝试登录...")
|
||||
self._showTrace("尝试登录...", no_log=True)
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
By.XPATH,
|
||||
@@ -203,5 +211,5 @@ class LibLogin(LibOperator):
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录成功 !")
|
||||
return True
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !")
|
||||
self._showTrace(f"用户 {username} 第 {attempt + 1} 次登录失败 !",self.TraceLevel.WARNING)
|
||||
return False
|
||||
|
||||
@@ -42,7 +42,7 @@ class LibLogout(LibOperator):
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
try:
|
||||
self.__driver.find_element(
|
||||
@@ -51,5 +51,5 @@ class LibLogout(LibOperator):
|
||||
self._showTrace(f"用户 {username} 注销成功 !")
|
||||
return True
|
||||
except Exception as e:
|
||||
self._showTrace(f"用户 {username} 注销失败 ! : {e}")
|
||||
self._showTrace(f"用户 {username} 注销失败 ! : {e}", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
+17
-15
@@ -54,14 +54,14 @@ class LibRenew(LibTimeSelector):
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv div.resultMessage"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("续约时间选择界面加载失败 !")
|
||||
self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
head_message = head_message.text.strip()
|
||||
if "警告" in head_message:
|
||||
result_message = result_message.text.strip()
|
||||
self._showTrace(f"\n"\
|
||||
f" 续约失败 !\n"\
|
||||
f" {result_message}")
|
||||
f" {result_message}", no_log=True)
|
||||
return False
|
||||
try:
|
||||
WebDriverWait(self.__driver, 2).until(
|
||||
@@ -73,7 +73,7 @@ class LibRenew(LibTimeSelector):
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, "#extendDiv .btnOK"))
|
||||
)
|
||||
except:
|
||||
self._showTrace("续约时间选择界面加载失败 !")
|
||||
self._showTrace("续约时间选择界面加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -91,7 +91,7 @@ class LibRenew(LibTimeSelector):
|
||||
renew_info = reserve_info["renew_time"]
|
||||
max_diff = renew_info["max_diff"]
|
||||
prefer_earlier = renew_info["prefer_early"]
|
||||
target_renew_mins = self._timeToMins(end_time) + renew_info["expect_duration"]*60
|
||||
target_renew_mins = self._timeStrToMins(end_time) + renew_info["expect_duration"]*60
|
||||
|
||||
# Validate and adjust target renew time to library closing time
|
||||
if not self.__validateAndAdjustRenewTime(end_time, target_renew_mins):
|
||||
@@ -99,7 +99,7 @@ class LibRenew(LibTimeSelector):
|
||||
renew_ok_btn = self.__driver.find_element(By.CSS_SELECTOR, "#extendDiv .btnOK")
|
||||
renew_time_opts = self.__driver.find_elements(By.CSS_SELECTOR, "#extendDiv .renewal_List li")
|
||||
if not renew_time_opts:
|
||||
self._showTrace("当前未查询到可用续约时间 !")
|
||||
self._showTrace("当前未查询到可用续约时间 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
|
||||
# Find best renewal time option
|
||||
@@ -110,7 +110,8 @@ class LibRenew(LibTimeSelector):
|
||||
return self.__confirmRenewal(best_opt, best_text, actual_diff, record, renew_ok_btn)
|
||||
self._showTrace(
|
||||
"无法选择最近的可用续约时间 ! "
|
||||
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !"
|
||||
f"所有可选时间与目标时间相差都超过了 {max_diff} 分钟 !",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
self._showTrace(f"当前可供续约的时间有: {free_times}")
|
||||
return False
|
||||
@@ -127,12 +128,12 @@ class LibRenew(LibTimeSelector):
|
||||
"""
|
||||
LIBRARY_CLOSE_TIME = 1410 # 23:30 in minutes
|
||||
if target_renew_mins > LIBRARY_CLOSE_TIME:
|
||||
actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeToMins(end_time)
|
||||
actual_renew_duration = LIBRARY_CLOSE_TIME - self._timeStrToMins(end_time)
|
||||
if actual_renew_duration <= 0:
|
||||
self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !")
|
||||
self._showTrace(f"当前结束时间 {end_time} 已接近闭馆时间,无法续约 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
self._showTrace(
|
||||
f"续约时间已调整至闭馆时间 {self._minsToTime(LIBRARY_CLOSE_TIME)},"
|
||||
f"续约时间已调整至闭馆时间 {self._minsToTimeStr(LIBRARY_CLOSE_TIME)},"
|
||||
f"实际续约时长为 {actual_renew_duration//60} 小时 {actual_renew_duration%60} 分钟"
|
||||
)
|
||||
return True
|
||||
@@ -163,7 +164,7 @@ class LibRenew(LibTimeSelector):
|
||||
ok_btn.click()
|
||||
return True
|
||||
except:
|
||||
self._showTrace("确认续约时发生错误 !")
|
||||
self._showTrace("确认续约时发生错误 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -175,28 +176,29 @@ class LibRenew(LibTimeSelector):
|
||||
) -> bool:
|
||||
|
||||
if self.__driver is None:
|
||||
self._showTrace("未提供有效 WebDriver 实例 !")
|
||||
self._showTrace("未提供有效 WebDriver 实例 !", self.TraceLevel.WARNING)
|
||||
return False
|
||||
try:
|
||||
renew_btn = WebDriverWait(self.__driver, 2).until(
|
||||
EC.element_to_be_clickable((By.ID, "btnExtend"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"用户 {username} 续约界面加载失败 !")
|
||||
self._showTrace(f"用户 {username} 续约界面加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
if "disabled" in renew_btn.get_attribute("class"):
|
||||
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试")
|
||||
self._showLog(f"用户 {username} 续约按钮不可用, 可能不在场馆内")
|
||||
self._showTrace(f"用户 {username} 续约按钮不可用, 可能不在场馆内, 请连接图书馆网络后重试", no_log=True)
|
||||
return False
|
||||
renew_btn.click()
|
||||
if not self.__waitRenewDialog():
|
||||
self._showTrace(f"用户 {username} 续约失败 !")
|
||||
self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR)
|
||||
|
||||
# After the renewal, the webpage will display a mask overlay,
|
||||
# so we need to refresh the page for subsequent operations.
|
||||
self.__driver.refresh()
|
||||
return False
|
||||
if not self.__selectNearestTime(record, reserve_info):
|
||||
self._showTrace(f"用户 {username} 续约失败 !")
|
||||
self._showTrace(f"用户 {username} 续约失败 !", self.TraceLevel.ERROR)
|
||||
self.__driver.refresh()
|
||||
return False
|
||||
if self._waitResponseLoad():
|
||||
|
||||
+101
-67
@@ -72,13 +72,13 @@ class LibReserve(LibTimeSelector):
|
||||
By.CSS_SELECTOR, ".layoutSeat dd"
|
||||
)
|
||||
if not content_elements:
|
||||
self._showTrace("未找到预约结果")
|
||||
self._showTrace("未找到预约结果", self.TraceLevel.WARNING)
|
||||
raise
|
||||
title = title_elements[0].text if title_elements else ""
|
||||
contents = [element.text for element in content_elements if element.text.strip()]
|
||||
for message in contents:
|
||||
if "预约失败" in message or "已有1个有效预约" in message:
|
||||
self._showTrace(f"预约失败 - {"".join(contents)}")
|
||||
self._showTrace(f"预约失败 - {"".join(contents)}", self.TraceLevel.ERROR)
|
||||
raise
|
||||
if "预定好了" in title or "预约成功" in title or "操作成功" in title:
|
||||
if len(contents) >= 6:
|
||||
@@ -96,7 +96,7 @@ class LibReserve(LibTimeSelector):
|
||||
)
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"预约结果加载失败 !")
|
||||
self._showTrace(f"预约结果加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ class LibReserve(LibTimeSelector):
|
||||
|
||||
try:
|
||||
# must contain the required infomation
|
||||
# key 'place' is no need to check
|
||||
# because 'place' is only has one possible value '1' or '图书馆'
|
||||
if reserve_info.get("floor") is None: # if existence ?
|
||||
raise ValueError("未指定楼层")
|
||||
if reserve_info["floor"] not in self.__floor_map: # if in the mao ?
|
||||
@@ -123,7 +125,13 @@ class LibReserve(LibTimeSelector):
|
||||
except ValueError as e:
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整"
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程",
|
||||
self.TraceLevel.ERROR
|
||||
)
|
||||
self._showTrace(
|
||||
f"预约信息错误 ! : {e}, "\
|
||||
f"由于缺少必要的预约信息, 无法开始预约流程, 请检查预约信息是否完整",
|
||||
no_log=True
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -133,17 +141,20 @@ class LibReserve(LibTimeSelector):
|
||||
reserve_info: dict
|
||||
) -> bool:
|
||||
|
||||
cur_date = time.strftime("%Y-%m-%d", time.localtime())
|
||||
cur_date_str = time.strftime("%Y-%m-%d", time.localtime())
|
||||
cur_timestamp = time.mktime(time.strptime(cur_date_str, "%Y-%m-%d"))
|
||||
if reserve_info.get("date") is None:
|
||||
reserve_info["date"] = cur_date
|
||||
self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date}")
|
||||
reserve_info["date"] = cur_date_str
|
||||
self._showTrace(f"预约日期未指定, 自动设置为当前日期: {cur_date_str}")
|
||||
else:
|
||||
if reserve_info["date"] < cur_date:
|
||||
res_timestamp = time.mktime(time.strptime(reserve_info["date"], "%Y-%m-%d"))
|
||||
if res_timestamp < cur_timestamp:
|
||||
self._showTrace(
|
||||
f"预约日期错误 ! :"\
|
||||
f"{reserve_info['date']} 早于当前日期 {cur_date}, 自动设置为当前日期"
|
||||
f"{reserve_info['date']} 早于当前日期 {cur_date_str}, 自动设置为当前日期",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
reserve_info["date"] = cur_date
|
||||
reserve_info["date"] = cur_date_str
|
||||
return True
|
||||
|
||||
|
||||
@@ -190,10 +201,13 @@ class LibReserve(LibTimeSelector):
|
||||
if reserve_info.get("end_time") is None:
|
||||
reserve_info["end_time"] = {}
|
||||
if "time" not in reserve_info["end_time"]:
|
||||
end_mins = self._timeToMins(reserve_info["begin_time"]["time"])
|
||||
# here we add the expect duration to the begin time first,
|
||||
# the edge case that the end time is later than 23:30 will
|
||||
# be handled in __finalCheck. so no need to concern about it.
|
||||
end_mins = self._timeStrToMins(reserve_info["begin_time"]["time"])
|
||||
end_mins = end_mins + int(reserve_info["expect_duration"]*60)
|
||||
reserve_info["end_time"] = {
|
||||
"time": self._minsToTime(end_mins),
|
||||
"time": self._minsToTimeStr(end_mins),
|
||||
"max_diff": 30,
|
||||
"prefer_early": False
|
||||
}
|
||||
@@ -215,32 +229,39 @@ class LibReserve(LibTimeSelector):
|
||||
):
|
||||
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self._timeToMins(begin_time["time"])
|
||||
end_mins = self._timeToMins(end_time["time"])
|
||||
begin_mins = self._timeStrToMins(begin_time["time"])
|
||||
end_mins = self._timeStrToMins(end_time["time"])
|
||||
|
||||
# if end time is earlier than begin_time, exchange them
|
||||
if end_mins < begin_mins:
|
||||
# except that the user has set the satisfy_duration to True
|
||||
if end_mins < begin_mins and reserve_info["satisfy_duration"] is False:
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间"
|
||||
f"结束时间 {end_time['time']} 早于开始时间 {begin_time['time']}, 尝试交换时间",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
reserve_info["end_time"] = begin_time
|
||||
reserve_info["begin_time"] = end_time
|
||||
begin_time, end_time = reserve_info["begin_time"], reserve_info["end_time"]
|
||||
begin_mins = self._timeToMins(begin_time["time"])
|
||||
end_mins = self._timeToMins(end_time["time"])
|
||||
reserve_info["end_time"], reserve_info["begin_time"] = begin_time, end_time
|
||||
begin_time, end_time = end_time, begin_time
|
||||
begin_mins = self._timeStrToMins(begin_time["time"])
|
||||
end_mins = self._timeStrToMins(end_time["time"])
|
||||
|
||||
# ensure the end time is not later than 23:30
|
||||
if end_mins > self._timeToMins("23:30"):
|
||||
max_end_mins = self._timeStrToMins("23:30")
|
||||
if end_mins > max_end_mins:
|
||||
self._showTrace(
|
||||
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30"
|
||||
f"结束时间 {end_time['time']} 晚于 23:30, 自动设置为 23:30",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
reserve_info["end_time"]["time"] = "23:30"
|
||||
end_mins = self._timeToMins("23:30")
|
||||
end_mins = max_end_mins
|
||||
|
||||
# ensure the duration is not longer than 8 hours
|
||||
if reserve_info["satisfy_duration"]:
|
||||
if reserve_info["expect_duration"] > 8:
|
||||
self._showTrace(
|
||||
f"该用户设置了优先满足时长要求, 但是预约期望持续时间 "
|
||||
f"{reserve_info['expect_duration']} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
reserve_info["expect_duration"] = 8
|
||||
else:
|
||||
@@ -248,9 +269,10 @@ class LibReserve(LibTimeSelector):
|
||||
self._showTrace(
|
||||
f"该用户未设置优先满足时长要求, 但是检查到预约持续时间 "
|
||||
f"{float((end_mins - begin_mins)/60)} 小时 "
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时"
|
||||
f"超出最大时长 8 小时, 自动设置为 8 小时",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
reserve_info["end_time"]["time"] = self._minsToTime(begin_mins + 8*60)
|
||||
reserve_info["end_time"]["time"] = self._minsToTimeStr(begin_mins + 8*60)
|
||||
return True
|
||||
|
||||
|
||||
@@ -274,8 +296,8 @@ class LibReserve(LibTimeSelector):
|
||||
self._showTrace(
|
||||
f"预约信息检查完成, 准备预约 "
|
||||
f"{reserve_info['date']} "
|
||||
f"{reserve_info['begin_time']["time"]} - "
|
||||
f"{reserve_info['end_time']["time"]} "
|
||||
f"{reserve_info['begin_time']['time']} - "
|
||||
f"{reserve_info['end_time']['time']} "
|
||||
f"图书馆 "
|
||||
f"{self.__floor_map[reserve_info['floor']]} "
|
||||
f"{self.__room_map[reserve_info['room']]} "
|
||||
@@ -418,7 +440,7 @@ class LibReserve(LibTimeSelector):
|
||||
EC.element_to_be_clickable((By.ID, "findRoom"))
|
||||
).click()
|
||||
except:
|
||||
self._showTrace("加载房间/区域失败 !")
|
||||
self._showTrace("加载房间/区域失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
# select room
|
||||
try:
|
||||
@@ -428,7 +450,7 @@ class LibReserve(LibTimeSelector):
|
||||
self._showTrace(f"房间 {display_room} 选择成功 !")
|
||||
return True
|
||||
except:
|
||||
self._showTrace(f"选择房间失败 ! : {display_room} 不可用")
|
||||
self._showTrace(f"选择房间失败 ! : {display_room} 不可用", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -446,7 +468,7 @@ class LibReserve(LibTimeSelector):
|
||||
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "li[id^='seat_']"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"座位加载失败 !")
|
||||
self._showTrace(f"座位加载失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
try:
|
||||
all_seats = self.__driver.find_elements(
|
||||
@@ -464,9 +486,10 @@ class LibReserve(LibTimeSelector):
|
||||
seat_status = seat_link.get_attribute("title")
|
||||
self._showTrace(f"座位 {seat_id} 选择成功 ! : 当前状态 - '{seat_status}'")
|
||||
return True
|
||||
self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确")
|
||||
self._showLog(f"座位 {seat_id} 在该楼层区域中不存在", self.TraceLevel.WARNING)
|
||||
self._showTrace(f"座位 {seat_id} 在该楼层区域中不存在, 请检查座位号是否正确", no_log=True)
|
||||
except:
|
||||
self._showTrace(f"座位选择失败 !")
|
||||
self._showTrace(f"座位选择失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
|
||||
|
||||
@@ -481,6 +504,9 @@ class LibReserve(LibTimeSelector):
|
||||
|
||||
"""
|
||||
Select the nearest available time option.
|
||||
|
||||
Returns:
|
||||
int: The actual selected time value in minutes.
|
||||
"""
|
||||
# Wait for time options to load
|
||||
try:
|
||||
@@ -490,7 +516,7 @@ class LibReserve(LibTimeSelector):
|
||||
)
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR)
|
||||
return -1
|
||||
|
||||
# Find best time option
|
||||
@@ -499,7 +525,7 @@ class LibReserve(LibTimeSelector):
|
||||
f"#{time_id} ul li a"
|
||||
)
|
||||
if not all_time_opts:
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间")
|
||||
self._showTrace(f"{time_type} 选择失败 ! : 当前未查询到可用时间", self.TraceLevel.ERROR)
|
||||
return -1
|
||||
best_opt, best_text, actual_diff, free_times = self._findBestTimeOption(
|
||||
all_time_opts, target_time, max_time_diff, prefer_earlier, is_reserve=True
|
||||
@@ -515,8 +541,8 @@ class LibReserve(LibTimeSelector):
|
||||
)
|
||||
return target_time
|
||||
self._showTrace(
|
||||
f"无法选择最近的 {time_type} {self._minsToTime(target_time)}, "
|
||||
f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟"
|
||||
f"无法选择最近的 {time_type} {self._minsToTimeStr(target_time)}, "
|
||||
f"所有可选时间与目标时间相差都超过 {max_time_diff} 分钟", self.TraceLevel.WARNING
|
||||
)
|
||||
self._showTrace(f"当前可供预约的 {time_type} 有: {free_times}")
|
||||
return -1
|
||||
@@ -526,51 +552,58 @@ class LibReserve(LibTimeSelector):
|
||||
self,
|
||||
begin_time: dict,
|
||||
end_time: dict,
|
||||
expct_duration: int = 4,
|
||||
expect_duration: int = 4,
|
||||
satisfy_duration: bool = True
|
||||
) -> bool:
|
||||
|
||||
"""Select seat begin and end time."""
|
||||
expect_begin_time = actual_begin_time = begin_time["time"]
|
||||
expect_end_time = actual_end_time = end_time["time"]
|
||||
expect_begin_mins = self._timeToMins(expect_begin_time)
|
||||
actual_begin_mins = expect_begin_mins
|
||||
expect_end_mins = self._timeToMins(expect_end_time)
|
||||
"""
|
||||
Select seat begin and end time.
|
||||
"""
|
||||
exp_beg_tm_str = begin_time["time"]
|
||||
exp_end_tm_str = end_time["time"]
|
||||
# Initialize actual time strings for logging
|
||||
act_beg_tm_str = exp_beg_tm_str
|
||||
act_end_tm_str = exp_end_tm_str
|
||||
exp_beg_mins = self._timeStrToMins(exp_beg_tm_str)
|
||||
act_beg_mins = exp_beg_mins
|
||||
exp_end_mins = self._timeStrToMins(exp_end_tm_str)
|
||||
act_end_mins = exp_end_mins
|
||||
|
||||
# Select begin time
|
||||
if self.__selectNearestTime(
|
||||
act_beg_mins = self.__selectNearestTime(
|
||||
time_id="startTime",
|
||||
time_type="开始时间",
|
||||
target_time=expect_begin_mins,
|
||||
target_time=exp_beg_mins,
|
||||
max_time_diff=begin_time["max_diff"],
|
||||
prefer_earlier=begin_time["prefer_early"]
|
||||
) == -1:
|
||||
)
|
||||
if act_beg_mins == -1:
|
||||
return False
|
||||
actual_begin_time = self._minsToTime(expect_begin_mins)
|
||||
actual_begin_mins = self._timeToMins(actual_begin_time)
|
||||
act_beg_tm_str = self._minsToTimeStr(act_beg_mins)
|
||||
|
||||
# If 'satisfy_duration' is True, select end time based on actual begin time
|
||||
if satisfy_duration:
|
||||
expect_end_mins = self.validateAndAdjustEndTime(actual_begin_mins, expct_duration)
|
||||
expect_end_time = self._minsToTime(expect_end_mins)
|
||||
exp_end_mins = int(self.validateAndAdjustEndTime(act_beg_mins, expect_duration))
|
||||
exp_end_tm_str = self._minsToTimeStr(exp_end_mins)
|
||||
self._showTrace(
|
||||
f"需要满足期望预约持续时间: {expct_duration} 小时, "
|
||||
f"根据开始时间 {actual_begin_time} 计算结束时间: {expect_end_time}"
|
||||
f"需要满足期望预约持续时间: {expect_duration} 小时, "
|
||||
f"根据开始时间 {act_beg_tm_str} 计算结束时间: {exp_end_tm_str}"
|
||||
)
|
||||
|
||||
# Select end time
|
||||
if self.__selectNearestTime(
|
||||
act_end_mins = self.__selectNearestTime(
|
||||
time_id="endTime",
|
||||
time_type="结束时间",
|
||||
target_time=expect_end_mins,
|
||||
target_time=exp_end_mins,
|
||||
max_time_diff=end_time["max_diff"],
|
||||
prefer_earlier=end_time["prefer_early"]
|
||||
) == -1:
|
||||
)
|
||||
if act_end_mins == -1:
|
||||
return False
|
||||
actual_end_time = self._minsToTime(expect_end_mins)
|
||||
act_end_tm_str = self._minsToTimeStr(act_end_mins)
|
||||
self._showTrace(
|
||||
f"期望预约时间段: {expect_begin_time} - {expect_end_time}, "
|
||||
f"实际预约时间段: {actual_begin_time} - {actual_end_time}"
|
||||
f"期望预约时间段: {exp_beg_tm_str} - {exp_end_tm_str}, "
|
||||
f"实际预约时间段: {act_beg_tm_str} - {act_end_tm_str}"
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -584,12 +617,13 @@ class LibReserve(LibTimeSelector):
|
||||
"""
|
||||
Validate and adjust reserve end time to library closing time if needed.
|
||||
"""
|
||||
LIBRARY_CLOSE_TIME = self._timeToMins("23:30")
|
||||
expect_end_mins = begin_mins + duration * 60
|
||||
LIBRARY_CLOSE_TIME = self._timeStrToMins("23:30")
|
||||
expect_end_mins = int(begin_mins + duration*60)
|
||||
if expect_end_mins > LIBRARY_CLOSE_TIME:
|
||||
expect_end_mins = LIBRARY_CLOSE_TIME
|
||||
self._showTrace(
|
||||
f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30"
|
||||
f"预约持续时间 {duration} 小时, 超过最大预约时间 23:30, 自动调整为 23:30",
|
||||
self.TraceLevel.WARNING
|
||||
)
|
||||
return expect_end_mins
|
||||
|
||||
@@ -616,7 +650,7 @@ class LibReserve(LibTimeSelector):
|
||||
EC.presence_of_element_located((By.ID, "seatLayout"))
|
||||
)
|
||||
except:
|
||||
self._showTrace(f"加载预约选座页面失败 !")
|
||||
self._showTrace(f"加载预约选座页面失败 !", self.TraceLevel.ERROR)
|
||||
return False
|
||||
# date, place, floor, room
|
||||
if not self.__selectDate(reserve_info["date"]):
|
||||
@@ -635,7 +669,7 @@ class LibReserve(LibTimeSelector):
|
||||
elif not self.__selectSeatTime(
|
||||
begin_time=reserve_info["begin_time"],
|
||||
end_time=reserve_info["end_time"],
|
||||
expct_duration=reserve_info["expect_duration"],
|
||||
expect_duration=reserve_info["expect_duration"],
|
||||
satisfy_duration=reserve_info["satisfy_duration"]
|
||||
):
|
||||
pass
|
||||
@@ -649,11 +683,11 @@ class LibReserve(LibTimeSelector):
|
||||
raise
|
||||
reserve_success = True
|
||||
except:
|
||||
self._showTrace(f"预约提交失败 !")
|
||||
self._showTrace(f"预约提交失败 !", self.TraceLevel.ERROR)
|
||||
if not submit_reserve and have_hover_on_page:
|
||||
self.__driver.refresh()
|
||||
if reserve_success:
|
||||
self._showTrace(f"用户 {username} 预约成功 !")
|
||||
else:
|
||||
self._showTrace(f"用户 {username} 预约失败 !")
|
||||
self._showTrace(f"用户 {username} 预约失败 !", self.TraceLevel.ERROR)
|
||||
return reserve_success
|
||||
@@ -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.
|
||||
"""
|
||||
Reference in New Issue
Block a user