python-library-ff14-the-hunt 0.0.2__tar.gz → 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/.cursor/skills/ff14_the_hunt-package-changelog/SKILL.md +6 -0
  2. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/PKG-INFO +1 -1
  3. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/README.md +1 -1
  4. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/ff14_the_hunt.py +2 -0
  5. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/cn.py +1 -0
  6. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/en.py +1 -0
  7. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/models.py +1 -0
  8. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/loop.py +12 -1
  9. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/region_fetch.py +3 -1
  10. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/pyproject.toml +1 -4
  11. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_ff14_the_hunt_service.py +14 -0
  12. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_locale_cn.py +1 -0
  13. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_spawn_map_data.py +12 -0
  14. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/.cursor/skills/ff14_the_hunt-package-design/SKILL.md +0 -0
  15. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/.cursor/skills/ff14_the_hunt-package-preload/SKILL.md +0 -0
  16. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/.gitignore +0 -0
  17. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/example/__main__.py +0 -0
  18. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/example/spawn_map_io.py +0 -0
  19. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/example.bat +0 -0
  20. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/__init__.py +0 -0
  21. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/__init__.py +0 -0
  22. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/client.py +0 -0
  23. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/enrich.py +0 -0
  24. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/fate_timer.py +0 -0
  25. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/resources.py +0 -0
  26. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/spawn_points.py +0 -0
  27. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/spawn_window.py +0 -0
  28. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/bear_tracker/timer_theme.py +0 -0
  29. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/__init__.py +0 -0
  30. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/data/__init__.py +0 -0
  31. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/data/hunts_zh.json +0 -0
  32. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/data/regions_zh.json +0 -0
  33. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/detect.py +0 -0
  34. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/display.py +0 -0
  35. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/names.py +0 -0
  36. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/spawn_map_display.py +0 -0
  37. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/locale/tag.py +0 -0
  38. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/__init__.py +0 -0
  39. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/monitor_mode.py +0 -0
  40. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/scheduler.py +0 -0
  41. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/sleep_plan.py +0 -0
  42. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/sleep_settings.py +0 -0
  43. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/spawn_dedup.py +0 -0
  44. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/poll/window_remaining.py +0 -0
  45. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/__init__.py +0 -0
  46. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/attach.py +0 -0
  47. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/coordinates.py +0 -0
  48. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/layout.py +0 -0
  49. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/png_meta.py +0 -0
  50. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/points.py +0 -0
  51. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/ff14_the_hunt/spawn_map/region_image.py +0 -0
  52. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/test.bat +0 -0
  53. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/__init__.py +0 -0
  54. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_fate_timer.py +0 -0
  55. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_monitor_mode.py +0 -0
  56. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_poll_scheduler.py +0 -0
  57. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_poll_sleep.py +0 -0
  58. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_spawn_dedup.py +0 -0
  59. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_spawn_map_points.py +0 -0
  60. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_spawn_window.py +0 -0
  61. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tests/test_untimed_marks_filter.py +0 -0
  62. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/tools/build_locale_data.py +0 -0
  63. {python_library_ff14_the_hunt-0.0.2 → python_library_ff14_the_hunt-0.0.4}/update.bat +0 -0
@@ -7,8 +7,14 @@ description: ff14_the_hunt 包:要求与决议;最新在上。
7
7
 
8
8
  (规则见 `~/.cursor/skills/agent-project-changelog/SKILL.md`。)
9
9
 
10
+ ## 2026-06-09
11
+
12
+ - **决议**:``region_map_image_url`` 对区域名做 URL 编码,修复含空格等区域名(如 ``Yak T'el``)拉取狩猎地图时 ``InvalidURL`` 导致 ff14hub 崩溃。发布 ``0.0.4``。
13
+
10
14
  ## 2026-06-08
11
15
 
16
+ - **决议**:``wait_or_stop`` 改为分段 ``Event.wait``(默认 1 秒一片),便于 ``stop()`` / Ctrl+C 尽快打断长睡。
17
+ - **决议**:发布 ``0.0.3``:``HuntCrawlPacket`` 增加 ``next_fetch_at``(按计划下次爬取的 Unix 秒);locale 导出同步。
12
18
  - **决议**:发布 ``0.0.2``:包级导出 ``HuntCrawlPacket``、locale、poll 等;与 ff14hub 首版所需 API 对齐。
13
19
 
14
20
  ## 2026-06-07
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-ff14-the-hunt
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Requires-Python: >=3.10
5
5
  Requires-Dist: pydantic>=2.0
6
6
  Provides-Extra: dev
@@ -50,7 +50,7 @@ hunt.run()
50
50
 
51
51
  也可用实例作装饰器:`@hunt`。
52
52
 
53
- 单次爬取结果在 ``HuntCrawlPacket`` 里,含 ``crawled_at``、``marks``、``query``。
53
+ 单次爬取结果在 ``HuntCrawlPacket`` 里,含 ``crawled_at``、``next_fetch_at``、``marks``、``query``。
54
54
 
55
55
  ## 示例
56
56
 
@@ -333,8 +333,10 @@ class FF14TheHunt:
333
333
  crawled_at = self._scheduler.last_crawl_at
334
334
  if crawled_at is None:
335
335
  crawled_at = time.time()
336
+ wait_seconds = self._scheduler.seconds_until_next_fetch(now=crawled_at)
336
337
  return HuntCrawlPacket(
337
338
  crawled_at=crawled_at,
339
+ next_fetch_at=crawled_at + wait_seconds,
338
340
  marks=marks,
339
341
  query=self._query,
340
342
  )
@@ -186,6 +186,7 @@ def crawl_packet_to_display_dict(
186
186
  payload: dict[str, object] = {
187
187
  "配置": query_to_display_dict(packet.query),
188
188
  "爬取时间": packet.crawled_at,
189
+ "下次爬取时间": packet.next_fetch_at,
189
190
  "怪物": [_mark_dict(mark) for mark in packet.marks],
190
191
  }
191
192
  if recently_spawned is not None:
@@ -172,6 +172,7 @@ def crawl_packet_to_display_dict(
172
172
  payload: dict[str, object] = {
173
173
  "query": query_to_display_dict(packet.query),
174
174
  "crawled_at": packet.crawled_at,
175
+ "next_fetch_at": packet.next_fetch_at,
175
176
  "marks": [_mark_dict(mark) for mark in packet.marks],
176
177
  }
177
178
  if recently_spawned is not None:
@@ -189,6 +189,7 @@ class HuntCrawlPacket(BaseModel):
189
189
  model_config = ConfigDict(extra="ignore")
190
190
 
191
191
  crawled_at: float = Field(description="爬取完成时的 Unix 秒")
192
+ next_fetch_at: float = Field(description="按计划下次爬取的 Unix 秒")
192
193
  marks: list[HuntMarkRecord] = Field(default_factory=list, description="解析后的狩猎记录")
193
194
  query: HuntQueryFilter = Field(description="本次爬取使用的筛选条件")
194
195
 
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import threading
4
+ import time
5
+
6
+ _WAIT_SLICE_SECONDS = 1.0
4
7
 
5
8
 
6
9
  def wait_or_stop(stop_event: threading.Event, seconds: float) -> bool:
@@ -15,4 +18,12 @@ def wait_or_stop(stop_event: threading.Event, seconds: float) -> bool:
15
18
  """
16
19
  if seconds <= 0:
17
20
  return stop_event.is_set()
18
- return stop_event.wait(timeout=seconds)
21
+ deadline = time.monotonic() + seconds
22
+ while not stop_event.is_set():
23
+ remaining = deadline - time.monotonic()
24
+ if remaining <= 0:
25
+ return False
26
+ slice_seconds = min(remaining, _WAIT_SLICE_SECONDS)
27
+ if stop_event.wait(timeout=slice_seconds):
28
+ return True
29
+ return True
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import urllib.error
4
+ import urllib.parse
4
5
  import urllib.request
5
6
 
6
7
 
@@ -15,7 +16,8 @@ def site_root_from_api_base(base_url: str) -> str:
15
16
  def region_map_image_url(site_root: str, region: str) -> str:
16
17
  """区域狩猎地图 PNG 地址(与站点 ``HuntRegions`` 目录一致)。"""
17
18
  root = site_root.rstrip("/")
18
- return f"{root}/static/images/HuntRegions/{region}.png"
19
+ encoded_region = urllib.parse.quote(region, safe="")
20
+ return f"{root}/static/images/HuntRegions/{encoded_region}.png"
19
21
 
20
22
 
21
23
  class RegionMapFetcher:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-library-ff14-the-hunt"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
10
  "pydantic>=2.0",
@@ -15,6 +15,3 @@ dev = ["pytest>=8.0"]
15
15
 
16
16
  [tool.hatch.build.targets.wheel]
17
17
  packages = ["ff14_the_hunt"]
18
-
19
- [tool.hatch.build.targets.wheel.force-include]
20
- "ff14_the_hunt/locale/data" = "ff14_the_hunt/locale/data"
@@ -73,6 +73,7 @@ def test_crawl_once_returns_packet() -> None:
73
73
  assert isinstance(packet, HuntCrawlPacket)
74
74
  assert packet.query is hunt.query
75
75
  assert len(packet.marks) == 1
76
+ assert packet.next_fetch_at >= packet.crawled_at
76
77
 
77
78
 
78
79
  def test_start_stop_poll_service() -> None:
@@ -107,3 +108,16 @@ def test_wait_or_stop() -> None:
107
108
  assert wait_or_stop(stop_event, 0.1) is False
108
109
  stop_event.set()
109
110
  assert wait_or_stop(stop_event, 1.0) is True
111
+
112
+
113
+ def test_wait_or_stop_wakes_early_on_stop() -> None:
114
+ stop_event = threading.Event()
115
+
116
+ def _set_stop() -> None:
117
+ time.sleep(0.05)
118
+ stop_event.set()
119
+
120
+ threading.Thread(target=_set_stop, daemon=True).start()
121
+ started = time.monotonic()
122
+ assert wait_or_stop(stop_event, 30.0) is True
123
+ assert time.monotonic() - started < 2.0
@@ -64,6 +64,7 @@ def test_crawl_packet_to_display_dict_lists_all_marks() -> None:
64
64
  )
65
65
  packet = HuntCrawlPacket(
66
66
  crawled_at=1.0,
67
+ next_fetch_at=601.0,
67
68
  marks=[
68
69
  HuntMarkRecord(
69
70
  hunt_key="Arch Aethereater Kozama'uka",
@@ -48,6 +48,18 @@ def test_with_pixel_coordinates() -> None:
48
48
  assert enriched[0].pixel_y == 250.0
49
49
 
50
50
 
51
+ def test_region_map_image_url_encodes_special_characters() -> None:
52
+ from ff14_the_hunt.spawn_map.region_fetch import region_map_image_url
53
+
54
+ url = region_map_image_url("https://tracker.beartoolkit.com", "Yak T'el")
55
+ assert (
56
+ url
57
+ == "https://tracker.beartoolkit.com/static/images/HuntRegions/Yak%20T%27el.png"
58
+ )
59
+ plain = region_map_image_url("https://tracker.beartoolkit.com", "Shaaloani")
60
+ assert plain == "https://tracker.beartoolkit.com/static/images/HuntRegions/Shaaloani.png"
61
+
62
+
51
63
  def test_build_region_map_image_round_trip() -> None:
52
64
  from ff14_the_hunt.spawn_map.region_fetch import RegionMapFetcher
53
65
  from ff14_the_hunt.spawn_map.region_image import build_region_map_image