hvbrowser 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/PKG-INFO +5 -5
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/pyproject.toml +6 -6
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/equipment_repair.py +247 -37
- hvbrowser-0.2.2/tests/test_equipment_repair.py +773 -0
- hvbrowser-0.2.0/tests/test_equipment_repair.py +0 -314
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/LICENSE +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/README.md +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/scripts/live_readonly_smoke.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/__init__.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/hv.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/live_probe.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/lottery.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/maintenance_navigation.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/market.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/monster_lab.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/player.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/py.typed +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/realm.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/runtime.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/session.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/src/hvbrowser/urls.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_architecture.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_hv.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_live_probe.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_lottery.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_maintenance_navigation.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_market.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_monster_lab.py +0 -0
- {hvbrowser-0.2.0 → hvbrowser-0.2.2}/tests/test_player.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hvbrowser
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Browser automation APIs for HentaiVerse.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Kuan-Lun/hvbrowser
|
|
6
6
|
Project-URL: Tracker, https://github.com/Kuan-Lun/hvbrowser/issues
|
|
@@ -13,13 +13,13 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Requires-Python: <3.15,>=3.14
|
|
16
|
-
Requires-Dist: hbrowser<0.37,>=0.36.
|
|
16
|
+
Requires-Dist: hbrowser<0.37,>=0.36.4
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: black>=26.5.1; extra == 'dev'
|
|
19
|
-
Requires-Dist: mypy>=2.
|
|
20
|
-
Requires-Dist: pymarkdownlnt>=0.9.
|
|
19
|
+
Requires-Dist: mypy>=2.3.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pymarkdownlnt>=0.9.39; extra == 'dev'
|
|
21
21
|
Requires-Dist: pytest>=9.1.1; extra == 'dev'
|
|
22
|
-
Requires-Dist: ruff>=0.
|
|
22
|
+
Requires-Dist: ruff>=0.16.3; extra == 'dev'
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# HVBrowser
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling>=1.32.0"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hvbrowser"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Browser automation APIs for HentaiVerse."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Kuan-Lun Wang" }]
|
|
11
11
|
license = "AGPL-3.0-or-later"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
requires-python = ">=3.14,<3.15"
|
|
14
|
-
dependencies = ["hbrowser>=0.36.
|
|
14
|
+
dependencies = ["hbrowser>=0.36.4,<0.37"]
|
|
15
15
|
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 3 - Alpha",
|
|
@@ -24,9 +24,9 @@ classifiers = [
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
dev = [
|
|
26
26
|
"black>=26.5.1",
|
|
27
|
-
"ruff>=0.
|
|
28
|
-
"mypy>=2.
|
|
29
|
-
"pymarkdownlnt>=0.9.
|
|
27
|
+
"ruff>=0.16.3",
|
|
28
|
+
"mypy>=2.3.0",
|
|
29
|
+
"pymarkdownlnt>=0.9.39",
|
|
30
30
|
"pytest>=9.1.1",
|
|
31
31
|
]
|
|
32
32
|
|
|
@@ -1,27 +1,81 @@
|
|
|
1
1
|
"""Typed inspection and repair operations for equipped gear."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import re
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
from enum import StrEnum
|
|
7
6
|
from typing import Any, Protocol
|
|
7
|
+
from urllib.parse import parse_qs, urlsplit
|
|
8
8
|
|
|
9
9
|
from .maintenance_navigation import (
|
|
10
10
|
MaintenanceNavigationBlockedError,
|
|
11
11
|
MaintenanceNavigator,
|
|
12
|
+
classify_maintenance_navigation_blocker,
|
|
12
13
|
)
|
|
13
|
-
from .realm import Realm, RealmNavigator
|
|
14
|
+
from .realm import Realm, RealmNavigator, realm_from_url
|
|
14
15
|
from .runtime import setup_logger
|
|
16
|
+
from .urls import HENTAIVERSE_ISEKAI_ROOT_URL, HENTAIVERSE_ROOT_URL
|
|
15
17
|
|
|
16
18
|
logger = setup_logger(__name__)
|
|
17
19
|
|
|
18
|
-
_EQUIPMENT_COUNT_XPATH = "//label[@id='equipcount']"
|
|
19
|
-
_REPAIR_SUBMIT_XPATH = "//input[@id='equipsubmit']"
|
|
20
|
+
_EQUIPMENT_COUNT_XPATH = "//*[@id='equipform']//label[@id='equipcount']"
|
|
21
|
+
_REPAIR_SUBMIT_XPATH = "//*[@id='equipform']//input[@id='equipsubmit']"
|
|
22
|
+
_ARMORY_MENU_XPATH = (
|
|
23
|
+
"//*[@id='child_Bazaar']"
|
|
24
|
+
"//*[@onclick and contains(@onclick, 's=Bazaar') "
|
|
25
|
+
"and contains(@onclick, 'ss=am')]"
|
|
26
|
+
" | //*[@id='child_Bazaar']//a[contains(@href, 's=Bazaar') "
|
|
27
|
+
"and contains(@href, 'ss=am')]"
|
|
28
|
+
)
|
|
20
29
|
_REPAIR_TAB_XPATH = (
|
|
21
|
-
"
|
|
22
|
-
"and contains(normalize-space(
|
|
30
|
+
"//*[@id='armory_left']/a[contains(@href, 'screen=repair') "
|
|
31
|
+
"and ./*[contains(concat(' ', normalize-space(@class), ' '), "
|
|
32
|
+
"' armory_tab ')]]"
|
|
33
|
+
)
|
|
34
|
+
_REPAIR_SELECTED_PAGE_XPATH = (
|
|
35
|
+
"//*[@id='armory_outer']["
|
|
36
|
+
".//*[@id='armory_left']/a[contains(@href, 'screen=repair')]"
|
|
37
|
+
"/*[contains(concat(' ', normalize-space(@class), ' '), ' armory_tab ') "
|
|
38
|
+
"and contains(concat(' ', normalize-space(@class), ' '), ' armory_cur ')] "
|
|
39
|
+
"and .//*[@id='filterbar']/a[contains(@href, 'filter=equipped')]"
|
|
40
|
+
"/*[contains(concat(' ', normalize-space(@class), ' '), ' cfbs ')]"
|
|
41
|
+
"]"
|
|
23
42
|
)
|
|
24
|
-
|
|
43
|
+
_EQUIPMENT_STATE_SCRIPT = """
|
|
44
|
+
(() => {
|
|
45
|
+
const equipForm = document.getElementById("equipform");
|
|
46
|
+
const equipList = document.getElementById("equiplist");
|
|
47
|
+
const selectableCount = (
|
|
48
|
+
typeof selectable_count !== "number"
|
|
49
|
+
|| !Number.isInteger(selectable_count)
|
|
50
|
+
|| selectable_count < 0
|
|
51
|
+
) ? null : selectable_count;
|
|
52
|
+
return {
|
|
53
|
+
hasEquipForm: Boolean(equipForm),
|
|
54
|
+
hasEquipList: Boolean(equipList),
|
|
55
|
+
selectableCount,
|
|
56
|
+
empty: Boolean(
|
|
57
|
+
equipList
|
|
58
|
+
&& (
|
|
59
|
+
equipList.querySelector(".eqempty")
|
|
60
|
+
|| equipList.querySelector("tr.eqselall > th > p")
|
|
61
|
+
)
|
|
62
|
+
),
|
|
63
|
+
rowCount: equipList
|
|
64
|
+
? equipList.querySelectorAll('tr[onmouseover*="hover_equip"]').length
|
|
65
|
+
: null,
|
|
66
|
+
};
|
|
67
|
+
})()
|
|
68
|
+
"""
|
|
69
|
+
_ARMORY_MENU_VISIBILITY_ATTEMPTS = 20
|
|
70
|
+
_ARMORY_MENU_VISIBILITY_INTERVAL_SECONDS = 0.1
|
|
71
|
+
_ARMORY_URLS = {
|
|
72
|
+
Realm.PERSISTENT: (
|
|
73
|
+
f"{HENTAIVERSE_ROOT_URL}/" "?s=Bazaar&ss=am&screen=repair&filter=equipped"
|
|
74
|
+
),
|
|
75
|
+
Realm.ISEKAI: (
|
|
76
|
+
f"{HENTAIVERSE_ISEKAI_ROOT_URL}" "?s=Bazaar&ss=am&screen=repair&filter=equipped"
|
|
77
|
+
),
|
|
78
|
+
}
|
|
25
79
|
|
|
26
80
|
|
|
27
81
|
class EquipmentRepairOutcome(StrEnum):
|
|
@@ -60,6 +114,10 @@ class EquipmentRepairPageError(RuntimeError):
|
|
|
60
114
|
"""The Armory Repair page did not expose the expected structure."""
|
|
61
115
|
|
|
62
116
|
|
|
117
|
+
class _EquipmentRepairNavigationSafetyError(EquipmentRepairPageError):
|
|
118
|
+
"""The current battle state, origin, or realm could not be trusted."""
|
|
119
|
+
|
|
120
|
+
|
|
63
121
|
class EquipmentRepairSubmissionError(RuntimeError):
|
|
64
122
|
"""A repair was submitted but its outcome is unknown or unconfirmed."""
|
|
65
123
|
|
|
@@ -71,6 +129,8 @@ class EquipmentRepairStateChangedError(RuntimeError):
|
|
|
71
129
|
class _EquipmentRepairDriver(Protocol):
|
|
72
130
|
page: Any
|
|
73
131
|
|
|
132
|
+
async def get(self, url: str) -> None: ...
|
|
133
|
+
|
|
74
134
|
async def wait(
|
|
75
135
|
self,
|
|
76
136
|
fun: Any,
|
|
@@ -193,6 +253,44 @@ class EquipmentRepairClient:
|
|
|
193
253
|
)
|
|
194
254
|
|
|
195
255
|
async def _navigate(self, realm: Realm) -> None:
|
|
256
|
+
try:
|
|
257
|
+
await self._open_repair_from_menu(realm)
|
|
258
|
+
return
|
|
259
|
+
except MaintenanceNavigationBlockedError:
|
|
260
|
+
raise
|
|
261
|
+
except _EquipmentRepairNavigationSafetyError:
|
|
262
|
+
raise
|
|
263
|
+
except EquipmentRepairPageError as error:
|
|
264
|
+
logger.warning(
|
|
265
|
+
"Armory menu navigation did not open Repair; retrying through "
|
|
266
|
+
"the realm-scoped direct URL: realm=%s error_type=%s",
|
|
267
|
+
realm.value,
|
|
268
|
+
type(error).__name__,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
await self._open_repair_directly(realm)
|
|
272
|
+
|
|
273
|
+
async def _open_repair_from_menu(self, realm: Realm) -> None:
|
|
274
|
+
await self._open_armory_from_menu(realm)
|
|
275
|
+
repair = await self._find_repair_tab()
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
await self.driver.wait(
|
|
279
|
+
repair.click,
|
|
280
|
+
ischangeurl=True,
|
|
281
|
+
)
|
|
282
|
+
except Exception as error:
|
|
283
|
+
try:
|
|
284
|
+
await self._ensure_navigation_is_safe("after opening Repair")
|
|
285
|
+
except MaintenanceNavigationBlockedError as blocked:
|
|
286
|
+
raise blocked from error
|
|
287
|
+
except _EquipmentRepairNavigationSafetyError as safety_error:
|
|
288
|
+
raise safety_error from error
|
|
289
|
+
raise EquipmentRepairPageError("Unable to open Repair tab") from error
|
|
290
|
+
|
|
291
|
+
await self._verify_repair_destination(realm)
|
|
292
|
+
|
|
293
|
+
async def _open_armory_from_menu(self, realm: Realm) -> None:
|
|
196
294
|
try:
|
|
197
295
|
bazaar = await self.maintenance.select_bazaar(
|
|
198
296
|
realm,
|
|
@@ -204,9 +302,7 @@ class EquipmentRepairClient:
|
|
|
204
302
|
raise EquipmentRepairPageError("Unable to open Bazaar") from error
|
|
205
303
|
|
|
206
304
|
try:
|
|
207
|
-
armory_elements = await self.page.xpath(
|
|
208
|
-
"//div[contains(text(), 'The Armory')]", timeout=5
|
|
209
|
-
)
|
|
305
|
+
armory_elements = await self.page.xpath(_ARMORY_MENU_XPATH, timeout=5)
|
|
210
306
|
except Exception as error:
|
|
211
307
|
raise EquipmentRepairPageError(
|
|
212
308
|
"Unable to find The Armory menu entry"
|
|
@@ -214,16 +310,112 @@ class EquipmentRepairClient:
|
|
|
214
310
|
if not armory_elements:
|
|
215
311
|
raise EquipmentRepairPageError("Unable to find The Armory menu entry")
|
|
216
312
|
|
|
313
|
+
armory = armory_elements[0]
|
|
217
314
|
try:
|
|
218
315
|
await bazaar.mouse_move()
|
|
219
|
-
await
|
|
316
|
+
await self._wait_for_armory_menu(armory)
|
|
317
|
+
await armory.mouse_move()
|
|
220
318
|
await self.driver.wait(
|
|
221
|
-
|
|
319
|
+
armory.mouse_click,
|
|
222
320
|
ischangeurl=True,
|
|
223
321
|
)
|
|
322
|
+
except EquipmentRepairPageError:
|
|
323
|
+
raise
|
|
224
324
|
except Exception as error:
|
|
225
325
|
raise EquipmentRepairPageError("Unable to open The Armory") from error
|
|
226
326
|
|
|
327
|
+
async def _wait_for_armory_menu(self, armory: Any) -> None:
|
|
328
|
+
last_error: Exception | None = None
|
|
329
|
+
for attempt in range(_ARMORY_MENU_VISIBILITY_ATTEMPTS):
|
|
330
|
+
try:
|
|
331
|
+
position = await armory.get_position()
|
|
332
|
+
except Exception as error:
|
|
333
|
+
last_error = error
|
|
334
|
+
else:
|
|
335
|
+
if position is not None:
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if attempt + 1 < _ARMORY_MENU_VISIBILITY_ATTEMPTS:
|
|
339
|
+
try:
|
|
340
|
+
await self.page.wait(_ARMORY_MENU_VISIBILITY_INTERVAL_SECONDS)
|
|
341
|
+
except Exception as error:
|
|
342
|
+
raise EquipmentRepairPageError(
|
|
343
|
+
"Unable to wait for The Armory menu entry"
|
|
344
|
+
) from error
|
|
345
|
+
|
|
346
|
+
page_error = EquipmentRepairPageError(
|
|
347
|
+
"The Armory menu entry did not become visible"
|
|
348
|
+
)
|
|
349
|
+
if last_error is None:
|
|
350
|
+
raise page_error
|
|
351
|
+
raise page_error from last_error
|
|
352
|
+
|
|
353
|
+
async def _open_repair_directly(self, realm: Realm) -> None:
|
|
354
|
+
await self._ensure_navigation_is_safe("before direct Armory navigation")
|
|
355
|
+
try:
|
|
356
|
+
await self.driver.get(_ARMORY_URLS[realm])
|
|
357
|
+
except Exception as error:
|
|
358
|
+
try:
|
|
359
|
+
await self._ensure_navigation_is_safe("after direct Armory navigation")
|
|
360
|
+
except MaintenanceNavigationBlockedError as blocked:
|
|
361
|
+
raise blocked from error
|
|
362
|
+
except _EquipmentRepairNavigationSafetyError as safety_error:
|
|
363
|
+
raise safety_error from error
|
|
364
|
+
raise EquipmentRepairPageError(
|
|
365
|
+
"Unable to open The Armory through its direct URL"
|
|
366
|
+
) from error
|
|
367
|
+
|
|
368
|
+
await self._verify_repair_destination(realm)
|
|
369
|
+
|
|
370
|
+
async def _ensure_navigation_is_safe(self, context: str) -> None:
|
|
371
|
+
try:
|
|
372
|
+
blocker = await classify_maintenance_navigation_blocker(self.page)
|
|
373
|
+
except Exception as error:
|
|
374
|
+
raise _EquipmentRepairNavigationSafetyError(
|
|
375
|
+
f"Unable to verify battle state {context}"
|
|
376
|
+
) from error
|
|
377
|
+
if blocker is not None:
|
|
378
|
+
raise MaintenanceNavigationBlockedError(blocker)
|
|
379
|
+
|
|
380
|
+
async def _verify_repair_destination(self, realm: Realm) -> None:
|
|
381
|
+
await self._ensure_navigation_is_safe("after opening Repair")
|
|
382
|
+
try:
|
|
383
|
+
current_url = await self.page.evaluate("window.location.href")
|
|
384
|
+
landed_realm = realm_from_url(current_url)
|
|
385
|
+
except Exception as error:
|
|
386
|
+
raise _EquipmentRepairNavigationSafetyError(
|
|
387
|
+
"Unable to verify the Repair URL"
|
|
388
|
+
) from error
|
|
389
|
+
if landed_realm is not realm:
|
|
390
|
+
raise _EquipmentRepairNavigationSafetyError(
|
|
391
|
+
"Repair navigation landed in the wrong realm"
|
|
392
|
+
)
|
|
393
|
+
if not isinstance(current_url, str):
|
|
394
|
+
raise _EquipmentRepairNavigationSafetyError("Repair URL is invalid")
|
|
395
|
+
parsed_url = urlsplit(current_url)
|
|
396
|
+
expected_path = "/isekai/" if realm is Realm.ISEKAI else "/"
|
|
397
|
+
if parsed_url.path != expected_path:
|
|
398
|
+
raise _EquipmentRepairNavigationSafetyError(
|
|
399
|
+
"Repair navigation landed on an unexpected path"
|
|
400
|
+
)
|
|
401
|
+
query = parse_qs(parsed_url.query, keep_blank_values=True)
|
|
402
|
+
expected_query = {
|
|
403
|
+
"s": ["Bazaar"],
|
|
404
|
+
"ss": ["am"],
|
|
405
|
+
"screen": ["repair"],
|
|
406
|
+
}
|
|
407
|
+
if any(query.get(key) != value for key, value in expected_query.items()):
|
|
408
|
+
raise EquipmentRepairPageError(
|
|
409
|
+
"Repair navigation did not land on the Repair route"
|
|
410
|
+
)
|
|
411
|
+
filter_values = query.get("filter")
|
|
412
|
+
if filter_values is not None and filter_values != ["equipped"]:
|
|
413
|
+
raise EquipmentRepairPageError(
|
|
414
|
+
"Repair navigation did not land on the Equipped filter"
|
|
415
|
+
)
|
|
416
|
+
await self._verify_repair_selected()
|
|
417
|
+
|
|
418
|
+
async def _find_repair_tab(self) -> Any:
|
|
227
419
|
try:
|
|
228
420
|
repair_elements = await self.page.xpath(
|
|
229
421
|
_REPAIR_TAB_XPATH,
|
|
@@ -233,53 +425,71 @@ class EquipmentRepairClient:
|
|
|
233
425
|
raise EquipmentRepairPageError("Unable to find Repair tab") from error
|
|
234
426
|
if not repair_elements:
|
|
235
427
|
raise EquipmentRepairPageError("Unable to find Repair tab")
|
|
428
|
+
return repair_elements[0]
|
|
236
429
|
|
|
430
|
+
async def _verify_repair_selected(self) -> None:
|
|
237
431
|
try:
|
|
238
|
-
await self.
|
|
239
|
-
|
|
240
|
-
|
|
432
|
+
selected_tabs = await self.page.xpath(
|
|
433
|
+
_REPAIR_SELECTED_PAGE_XPATH,
|
|
434
|
+
timeout=5,
|
|
241
435
|
)
|
|
242
436
|
except Exception as error:
|
|
243
|
-
raise EquipmentRepairPageError(
|
|
437
|
+
raise EquipmentRepairPageError(
|
|
438
|
+
"Unable to verify the Armory Repair page"
|
|
439
|
+
) from error
|
|
440
|
+
if not selected_tabs:
|
|
441
|
+
raise EquipmentRepairPageError(
|
|
442
|
+
"Armory Repair selected-tab marker or Equipped filter marker is missing"
|
|
443
|
+
)
|
|
244
444
|
|
|
245
445
|
async def _inspect_current(
|
|
246
446
|
self,
|
|
247
447
|
realm: Realm,
|
|
248
448
|
) -> tuple[EquipmentRepairSnapshot, Any | None]:
|
|
449
|
+
await self._verify_repair_destination(realm)
|
|
450
|
+
|
|
249
451
|
try:
|
|
250
|
-
|
|
251
|
-
|
|
452
|
+
equipcount_elements = await self.page.xpath(
|
|
453
|
+
_EQUIPMENT_COUNT_XPATH,
|
|
252
454
|
timeout=5,
|
|
253
455
|
)
|
|
254
456
|
except Exception as error:
|
|
255
457
|
raise EquipmentRepairPageError(
|
|
256
|
-
"Unable to
|
|
458
|
+
"Unable to inspect equipment repair count"
|
|
257
459
|
) from error
|
|
258
|
-
if not repair_page_markers:
|
|
259
|
-
raise EquipmentRepairPageError("Armory Repair page marker is missing")
|
|
260
460
|
|
|
261
461
|
try:
|
|
262
|
-
|
|
263
|
-
_EQUIPMENT_COUNT_XPATH,
|
|
264
|
-
timeout=5,
|
|
265
|
-
)
|
|
462
|
+
raw_state = await self.page.evaluate(_EQUIPMENT_STATE_SCRIPT)
|
|
266
463
|
except Exception as error:
|
|
267
464
|
raise EquipmentRepairPageError(
|
|
268
465
|
"Unable to inspect equipment repair count"
|
|
269
466
|
) from error
|
|
270
|
-
if not
|
|
467
|
+
if not isinstance(raw_state, dict):
|
|
468
|
+
raise EquipmentRepairPageError("Equipment repair count state is invalid")
|
|
469
|
+
repair_count = raw_state.get("selectableCount")
|
|
470
|
+
empty = raw_state.get("empty")
|
|
471
|
+
row_count = raw_state.get("rowCount")
|
|
472
|
+
has_equip_form = raw_state.get("hasEquipForm")
|
|
473
|
+
has_equip_list = raw_state.get("hasEquipList")
|
|
474
|
+
if (
|
|
475
|
+
(repair_count is not None and type(repair_count) is not int)
|
|
476
|
+
or type(empty) is not bool
|
|
477
|
+
or (row_count is not None and (type(row_count) is not int or row_count < 0))
|
|
478
|
+
or has_equip_form is not True
|
|
479
|
+
or has_equip_list is not True
|
|
480
|
+
):
|
|
481
|
+
raise EquipmentRepairPageError("Equipment repair count state is invalid")
|
|
482
|
+
if equipcount_elements:
|
|
483
|
+
if repair_count is None or repair_count < 0:
|
|
484
|
+
raise EquipmentRepairPageError(
|
|
485
|
+
"Equipment repair count state is invalid"
|
|
486
|
+
)
|
|
487
|
+
return EquipmentRepairSnapshot(realm, repair_count), equipcount_elements[0]
|
|
488
|
+
if empty and row_count == 0 and repair_count in {None, 0}:
|
|
271
489
|
return EquipmentRepairSnapshot(realm, 0), None
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if not isinstance(text, str):
|
|
276
|
-
raise EquipmentRepairPageError("Equipment repair count text is missing")
|
|
277
|
-
match = _EQUIPMENT_COUNT_PATTERN.search(text)
|
|
278
|
-
if match is None:
|
|
279
|
-
raise EquipmentRepairPageError(
|
|
280
|
-
f"Unable to parse equipment repair count from: {text!r}"
|
|
281
|
-
)
|
|
282
|
-
return EquipmentRepairSnapshot(realm, int(match.group(1))), equipcount
|
|
490
|
+
raise EquipmentRepairPageError(
|
|
491
|
+
"Equipment repair count is missing without an empty equipment list"
|
|
492
|
+
)
|
|
283
493
|
|
|
284
494
|
async def _select_all_and_inspect_submit(self, equipcount: Any) -> tuple[bool, Any]:
|
|
285
495
|
logger.debug("Before select-all click: %r", getattr(equipcount, "text", None))
|