diff --git a/.github/workflows/data.yml b/.github/workflows/data.yml index f0d8b83..6a64278 100644 --- a/.github/workflows/data.yml +++ b/.github/workflows/data.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - league: [challenge] + league: [challenge, challengehc, standard, hardcore] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/data/__main__.py b/data/__main__.py index 9b2370e..df61f00 100644 --- a/data/__main__.py +++ b/data/__main__.py @@ -4,6 +4,11 @@ import sys from pathlib import Path +from .types import LeagueType +from .utils import EnumAction + + +DEFAULT_DATA_FILES = [f'data-{lt.value}.json' for lt in LeagueType] logging.basicConfig(level=logging.INFO) @@ -12,25 +17,17 @@ gen = subparsers.add_parser('gen') gen.add_argument( - 'league', - choices=[ - 'challenge', - 'challenge-hc', - 'standard', - 'hc', - ], + 'league_type', + type=LeagueType, + action=EnumAction, ) pub = subparsers.add_parser('pub') pub.add_argument( 'filename', nargs='+', - default=[ - 'data-challenge.json', - # 'data-challenge-hc.json', - # 'data-standard.json', - # 'data-hc.json', - ], + default=DEFAULT_DATA_FILES, + choices=DEFAULT_DATA_FILES, ) args = parser.parse_args() @@ -46,25 +43,35 @@ from dataclasses import asdict from .beasts import get_beasts + from .leagues import get_leagues from .tools import get_tools from .wiki import get_items + league = get_leagues()[args.league_type] + print(f'Aggregating data for {league.title} ({league.slug})...') + data = [] - for num, entry in enumerate(itertools.chain(get_items(), get_beasts(), get_tools())): + for num, entry in enumerate( + itertools.chain( + get_items(league), + get_beasts(league), + get_tools(league), + ), + ): data.append({'id': num, **asdict(entry)}) now = datetime.datetime.now(tz=datetime.UTC) doc = { 'meta': { - 'league': args.league, + 'league': args.league_type.value, 'timestamp': datetime.datetime.timestamp(now), 'timestamp_human': now.isoformat(), }, 'data': data, } - with pathlib.Path(f'data-{args.league}.json').open('w') as file: + with pathlib.Path(f'data-{args.league_type.value}.json').open('w') as file: json.dump(doc, file) diff --git a/data/beasts.py b/data/beasts.py index 108ecb1..e82c78b 100644 --- a/data/beasts.py +++ b/data/beasts.py @@ -1,18 +1,19 @@ from collections.abc import Generator +from .leagues import League from .ninja import get_ninja_index, make_ninja_url from .trade import make_trade_url from .types import NinjaCategory from .utils import Entry, make_wiki_url -def get_beasts() -> Generator[Entry, None, None]: - index = get_ninja_index() +def get_beasts(league: League) -> Generator[Entry, None, None]: + index = get_ninja_index(league) for beast in index.raw[NinjaCategory.BEASTS]: yield Entry( display_text=beast, wiki_url=make_wiki_url(beast), - ninja_url=make_ninja_url(beast, None, NinjaCategory.BEASTS), - trade_url=make_trade_url(beast), + ninja_url=make_ninja_url(league, beast, None, NinjaCategory.BEASTS), + trade_url=make_trade_url(league, beast), ) diff --git a/data/leagues.py b/data/leagues.py new file mode 100644 index 0000000..1b8259c --- /dev/null +++ b/data/leagues.py @@ -0,0 +1,71 @@ +import dataclasses +import functools +import http + +from tabulate import tabulate + +from .types import LeagueType +from .utils import LoggedRequestsSession, slugify + + +@dataclasses.dataclass(frozen=True) +class League: + type_: LeagueType + title: str # e.g. "Ancestor" + slug: str # e.g. "ancestor" + is_hardcore: bool + + +@functools.cache +def get_leagues() -> dict[LeagueType, League]: + session = LoggedRequestsSession() + + res = session.get('https://poe.ninja/api/data/getindexstate') + assert res.status_code == http.HTTPStatus.OK + + res_parsed = res.json() + + leagues = { + LeagueType.STANDARD: League( + type_=LeagueType.STANDARD, + title='Standard', + slug='standard', + is_hardcore=False, + ), + LeagueType.HARDCORE: League( + type_=LeagueType.HARDCORE, + title='Hardcore', + slug='hardcore', + is_hardcore=True, + ), + } + + # challenge league usually is the first + challenge_league_name = res_parsed['economyLeagues'][0]['name'] + assert challenge_league_name != 'Standard', challenge_league_name + assert 'Hardcore' not in challenge_league_name, challenge_league_name + assert 'Ruthless' not in challenge_league_name, challenge_league_name + assert 'HC' not in challenge_league_name, challenge_league_name + + leagues[LeagueType.CHALLENGE] = League( + type_=LeagueType.CHALLENGE, + title=challenge_league_name, + slug=slugify(challenge_league_name), + is_hardcore=False, + ) + leagues[LeagueType.CHALLENGE_HARDCORE] = League( + type_=LeagueType.CHALLENGE_HARDCORE, + title=f'Hardcore {challenge_league_name}', + slug=f'{leagues[LeagueType.CHALLENGE].slug}hc', + is_hardcore=True, + ) + + print( + tabulate( + [[league_type, league.title, league.slug, league.is_hardcore] for league_type, league in leagues.items()], + headers=('league type', 'human', 'slug', 'is hardcore'), + tablefmt='outline', + ), + ) + + return leagues diff --git a/data/ninja.py b/data/ninja.py index 6bfdf42..03854e3 100644 --- a/data/ninja.py +++ b/data/ninja.py @@ -4,86 +4,87 @@ from tabulate import tabulate +from .leagues import League from .types import URL, NinjaCategory from .utils import LoggedRequestsSession -ninja_api_endpoint_for_category: dict[NinjaCategory, tuple[str, str]] = { +ninja_api_endpoint_for_category: dict[NinjaCategory, tuple[str, str]] = { # (url_template, response_key) # General - NinjaCategory.CURRENCY: ('https://poe.ninja/api/data/currencyoverview?league=Ancestor&type=Currency', 'currencyTypeName'), # noqa: E501 - NinjaCategory.FRAGMENTS: ('https://poe.ninja/api/data/currencyoverview?league=Ancestor&type=Fragment', 'currencyTypeName'), # noqa: E501 - NinjaCategory.TATTOOS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Tattoo', 'name'), - NinjaCategory.OMENS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Omen', 'name'), - NinjaCategory.DIVINATION_CARDS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=DivinationCard', 'name'), # noqa: E501 - NinjaCategory.ARTIFACTS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Artifact', 'name'), - NinjaCategory.OILS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Oil', 'name'), - NinjaCategory.INCUBATORS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Incubator', 'name'), + NinjaCategory.CURRENCY: ('https://poe.ninja/api/data/currencyoverview?league={league}&type=Currency', 'currencyTypeName'), # noqa: E501 + NinjaCategory.FRAGMENTS: ('https://poe.ninja/api/data/currencyoverview?league={league}&type=Fragment', 'currencyTypeName'), # noqa: E501 + NinjaCategory.TATTOOS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Tattoo', 'name'), + NinjaCategory.OMENS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Omen', 'name'), + NinjaCategory.DIVINATION_CARDS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=DivinationCard', 'name'), # noqa: E501 + NinjaCategory.ARTIFACTS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Artifact', 'name'), + NinjaCategory.OILS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Oil', 'name'), + NinjaCategory.INCUBATORS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Incubator', 'name'), # Equipment & Gems - NinjaCategory.UNIQUE_WEAPONS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueWeapon', 'name'), - NinjaCategory.UNIQUE_ARMOURS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueArmour', 'name'), - NinjaCategory.UNIQUE_ACCESSORIES: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueAccessory', 'name'), # noqa: E501 - NinjaCategory.UNIQUE_FLASKS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueFlask', 'name'), - NinjaCategory.UNIQUE_JEWELS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueJewel', 'name'), - NinjaCategory.UNIQUE_RELICS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueRelic', 'name'), - NinjaCategory.SKILL_GEMS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=SkillGem', 'name'), - NinjaCategory.CLUSTER_JEWELS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=ClusterJewel', 'name'), + NinjaCategory.UNIQUE_WEAPONS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueWeapon', 'name'), + NinjaCategory.UNIQUE_ARMOURS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueArmour', 'name'), + NinjaCategory.UNIQUE_ACCESSORIES: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueAccessory', 'name'), # noqa: E501 + NinjaCategory.UNIQUE_FLASKS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueFlask', 'name'), + NinjaCategory.UNIQUE_JEWELS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueJewel', 'name'), + NinjaCategory.UNIQUE_RELICS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueRelic', 'name'), + NinjaCategory.SKILL_GEMS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=SkillGem', 'name'), + NinjaCategory.CLUSTER_JEWELS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=ClusterJewel', 'name'), # Atlas - NinjaCategory.MAPS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Map', 'name'), - NinjaCategory.BLIGHTED_MAPS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=BlightedMap', 'name'), - NinjaCategory.BLIGHT_RAVAGED_MAPS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=BlightRavagedMap', 'name'), # noqa: E501 - NinjaCategory.SCOURGED_MAPS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=ScourgedMap', 'name'), - NinjaCategory.UNIQUE_MAPS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=UniqueMap', 'name'), - NinjaCategory.DELIRIUM_ORBS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=DeliriumOrb', 'name'), - NinjaCategory.INVITATIONS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Invitation', 'name'), - NinjaCategory.SCARABS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Scarab', 'name'), - NinjaCategory.MEMORIES: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Memory', 'name'), + NinjaCategory.MAPS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Map', 'name'), + NinjaCategory.BLIGHTED_MAPS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=BlightedMap', 'name'), + NinjaCategory.BLIGHT_RAVAGED_MAPS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=BlightRavagedMap', 'name'), # noqa: E501 + NinjaCategory.SCOURGED_MAPS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=ScourgedMap', 'name'), + NinjaCategory.UNIQUE_MAPS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=UniqueMap', 'name'), + NinjaCategory.DELIRIUM_ORBS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=DeliriumOrb', 'name'), + NinjaCategory.INVITATIONS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Invitation', 'name'), + NinjaCategory.SCARABS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Scarab', 'name'), + NinjaCategory.MEMORIES: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Memory', 'name'), # Crafting - NinjaCategory.BASE_TYPES: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=BaseType', 'name'), - NinjaCategory.FOSSILS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Fossil', 'name'), - NinjaCategory.RESONATORS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Resonator', 'name'), - NinjaCategory.HELMET_ENCHANTS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=HelmetEnchant', 'name'), # noqa: E501 - NinjaCategory.BEASTS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Beast', 'name'), - NinjaCategory.ESSENCES: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Essence', 'name'), - NinjaCategory.VIALS: ('https://poe.ninja/api/data/itemoverview?league=Ancestor&type=Vial', 'name'), + NinjaCategory.BASE_TYPES: ('https://poe.ninja/api/data/itemoverview?league={league}&type=BaseType', 'name'), + NinjaCategory.FOSSILS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Fossil', 'name'), + NinjaCategory.RESONATORS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Resonator', 'name'), + NinjaCategory.HELMET_ENCHANTS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=HelmetEnchant', 'name'), # noqa: E501 + NinjaCategory.BEASTS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Beast', 'name'), + NinjaCategory.ESSENCES: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Essence', 'name'), + NinjaCategory.VIALS: ('https://poe.ninja/api/data/itemoverview?league={league}&type=Vial', 'name'), } -ninja_url_for_category: dict[NinjaCategory, tuple[str, bool]] = { # (url, item_name_as_param) +ninja_url_for_category: dict[NinjaCategory, tuple[str, bool]] = { # (url_template, item_name_as_param) # General - NinjaCategory.CURRENCY: ('https://poe.ninja/economy/ancestor/currency', False), - NinjaCategory.FRAGMENTS: ('https://poe.ninja/economy/ancestor/fragments', False), - NinjaCategory.TATTOOS: ('https://poe.ninja/economy/ancestor/tattoos', False), - NinjaCategory.OMENS: ('https://poe.ninja/economy/ancestor/omens', False), - NinjaCategory.DIVINATION_CARDS: ('https://poe.ninja/economy/ancestor/divination-cards', False), - NinjaCategory.ARTIFACTS: ('https://poe.ninja/economy/ancestor/artifacts', False), - NinjaCategory.OILS: ('https://poe.ninja/economy/ancestor/oils', False), - NinjaCategory.INCUBATORS: ('https://poe.ninja/economy/ancestor/incubators', False), + NinjaCategory.CURRENCY: ('https://poe.ninja/economy/{league}/currency', False), + NinjaCategory.FRAGMENTS: ('https://poe.ninja/economy/{league}/fragments', False), + NinjaCategory.TATTOOS: ('https://poe.ninja/economy/{league}/tattoos', False), + NinjaCategory.OMENS: ('https://poe.ninja/economy/{league}/omens', False), + NinjaCategory.DIVINATION_CARDS: ('https://poe.ninja/economy/{league}/divination-cards', False), + NinjaCategory.ARTIFACTS: ('https://poe.ninja/economy/{league}/artifacts', False), + NinjaCategory.OILS: ('https://poe.ninja/economy/{league}/oils', False), + NinjaCategory.INCUBATORS: ('https://poe.ninja/economy/{league}/incubators', False), # Equipment & Gems - NinjaCategory.UNIQUE_WEAPONS: ('https://poe.ninja/economy/ancestor/unique-weapons', False), - NinjaCategory.UNIQUE_ARMOURS: ('https://poe.ninja/economy/ancestor/unique-armours', False), - NinjaCategory.UNIQUE_ACCESSORIES: ('https://poe.ninja/economy/ancestor/unique-accessories', False), - NinjaCategory.UNIQUE_FLASKS: ('https://poe.ninja/economy/ancestor/unique-flasks', False), - NinjaCategory.UNIQUE_JEWELS: ('https://poe.ninja/economy/ancestor/unique-jewels', False), - NinjaCategory.UNIQUE_RELICS: ('https://poe.ninja/economy/ancestor/unique-relics', False), - NinjaCategory.SKILL_GEMS: ('https://poe.ninja/economy/ancestor/skill-gems', True), - NinjaCategory.CLUSTER_JEWELS: ('https://poe.ninja/economy/ancestor/cluster-jewels', True), + NinjaCategory.UNIQUE_WEAPONS: ('https://poe.ninja/economy/{league}/unique-weapons', False), + NinjaCategory.UNIQUE_ARMOURS: ('https://poe.ninja/economy/{league}/unique-armours', False), + NinjaCategory.UNIQUE_ACCESSORIES: ('https://poe.ninja/economy/{league}/unique-accessories', False), + NinjaCategory.UNIQUE_FLASKS: ('https://poe.ninja/economy/{league}/unique-flasks', False), + NinjaCategory.UNIQUE_JEWELS: ('https://poe.ninja/economy/{league}/unique-jewels', False), + NinjaCategory.UNIQUE_RELICS: ('https://poe.ninja/economy/{league}/unique-relics', False), + NinjaCategory.SKILL_GEMS: ('https://poe.ninja/economy/{league}/skill-gems', True), + NinjaCategory.CLUSTER_JEWELS: ('https://poe.ninja/economy/{league}/cluster-jewels', True), # Atlas - NinjaCategory.MAPS: ('https://poe.ninja/economy/ancestor/maps', True), - NinjaCategory.BLIGHTED_MAPS: ('https://poe.ninja/economy/ancestor/blighted-maps', True), - NinjaCategory.BLIGHT_RAVAGED_MAPS: ('https://poe.ninja/economy/ancestor/blight-ravaged-maps', True), - NinjaCategory.SCOURGED_MAPS: ('https://poe.ninja/economy/ancestor/scourged-maps', True), - NinjaCategory.UNIQUE_MAPS: ('https://poe.ninja/economy/ancestor/unique-maps', True), - NinjaCategory.DELIRIUM_ORBS: ('https://poe.ninja/economy/ancestor/delirium-orbs', False), - NinjaCategory.INVITATIONS: ('https://poe.ninja/economy/ancestor/invitations', False), - NinjaCategory.SCARABS: ('https://poe.ninja/economy/ancestor/scarabs', False), - NinjaCategory.MEMORIES: ('https://poe.ninja/economy/ancestor/memories', True), + NinjaCategory.MAPS: ('https://poe.ninja/economy/{league}/maps', True), + NinjaCategory.BLIGHTED_MAPS: ('https://poe.ninja/economy/{league}/blighted-maps', True), + NinjaCategory.BLIGHT_RAVAGED_MAPS: ('https://poe.ninja/economy/{league}/blight-ravaged-maps', True), + NinjaCategory.SCOURGED_MAPS: ('https://poe.ninja/economy/{league}/scourged-maps', True), + NinjaCategory.UNIQUE_MAPS: ('https://poe.ninja/economy/{league}/unique-maps', True), + NinjaCategory.DELIRIUM_ORBS: ('https://poe.ninja/economy/{league}/delirium-orbs', False), + NinjaCategory.INVITATIONS: ('https://poe.ninja/economy/{league}/invitations', False), + NinjaCategory.SCARABS: ('https://poe.ninja/economy/{league}/scarabs', False), + NinjaCategory.MEMORIES: ('https://poe.ninja/economy/{league}/memories', True), # Crafting - NinjaCategory.BASE_TYPES: ('https://poe.ninja/economy/ancestor/base-types', True), - NinjaCategory.FOSSILS: ('https://poe.ninja/economy/ancestor/fossils', False), - NinjaCategory.RESONATORS: ('https://poe.ninja/economy/ancestor/resonators', False), - NinjaCategory.HELMET_ENCHANTS: ('https://poe.ninja/economy/ancestor/helmet-enchants', True), - NinjaCategory.BEASTS: ('https://poe.ninja/economy/ancestor/beasts', False), - NinjaCategory.ESSENCES: ('https://poe.ninja/economy/ancestor/essences', False), - NinjaCategory.VIALS: ('https://poe.ninja/economy/ancestor/vials', False), + NinjaCategory.BASE_TYPES: ('https://poe.ninja/economy/{league}/base-types', True), + NinjaCategory.FOSSILS: ('https://poe.ninja/economy/{league}/fossils', False), + NinjaCategory.RESONATORS: ('https://poe.ninja/economy/{league}/resonators', False), + NinjaCategory.HELMET_ENCHANTS: ('https://poe.ninja/economy/{league}/helmet-enchants', True), + NinjaCategory.BEASTS: ('https://poe.ninja/economy/{league}/beasts', False), + NinjaCategory.ESSENCES: ('https://poe.ninja/economy/{league}/essences', False), + NinjaCategory.VIALS: ('https://poe.ninja/economy/{league}/vials', False), } @@ -112,7 +113,7 @@ def print_stats(self) -> None: @functools.cache -def get_ninja_index() -> NinjaIndex: +def get_ninja_index(league: League) -> NinjaIndex: """ Downloads current data from ninja and makes it available as a sort-of index. """ @@ -121,7 +122,8 @@ def get_ninja_index() -> NinjaIndex: index = {} for category, endpoint_info in ninja_api_endpoint_for_category.items(): - url, response_attr = endpoint_info + url_template, response_attr = endpoint_info + url = url_template.format(league=league.title) res = session.get(url) assert res.status_code == http.HTTPStatus.OK @@ -135,8 +137,9 @@ def get_ninja_index() -> NinjaIndex: return index -def make_ninja_url(item_name: str, base_name: str | None, category: NinjaCategory) -> URL | None: - base_url, name_as_param = ninja_url_for_category[category] +def make_ninja_url(league: League, item_name: str, base_name: str | None, category: NinjaCategory) -> URL | None: + base_url_template, name_as_param = ninja_url_for_category[category] + base_url = base_url_template.format(league=league.slug) if name_as_param: return f'{base_url}?name={item_name}' diff --git a/data/tools.py b/data/tools.py index 8748fce..c35d113 100644 --- a/data/tools.py +++ b/data/tools.py @@ -1,48 +1,54 @@ from collections.abc import Generator +from .leagues import League from .ninja import NinjaCategory -from .utils import Entry +from .utils import Entry, is_format_string -all_tools = [ +tool_data = [ # PoEStack - Entry(display_text='PoeStack', tool_url='https://poestack.com/'), - Entry(display_text='PoeStack - Stash View', tool_url='https://poestack.com/poe/stash-view?league=Ancestor'), - Entry(display_text='PoeStack - Pricing', tool_url='https://poestack.com/pricing'), - Entry(display_text='PoeStack - TFT Bulk Tool', tool_url='https://poestack.com/tft/bulk-tool?league=Ancestor'), - Entry(display_text='PoeStack - TFT Compasses', tool_url='https://poestack.com/tft/live-search?tag=compasses'), - Entry(display_text='PoeStack - TFT Five Ways', tool_url='https://poestack.com/tft/live-search?tag=five-ways'), + {'display_text': 'PoeStack', 'tool_url': 'https://poestack.com/'}, + {'display_text': 'PoeStack - Stash View', 'tool_url': 'https://poestack.com/poe/stash-view?league={league_title}'}, + {'display_text': 'PoeStack - Pricing', 'tool_url': 'https://poestack.com/pricing'}, + {'display_text': 'PoeStack - TFT Bulk Tool', 'tool_url': 'https://poestack.com/tft/bulk-tool?league={league_title}'}, + {'display_text': 'PoeStack - TFT Compasses', 'tool_url': 'https://poestack.com/tft/live-search?tag=compasses'}, + {'display_text': 'PoeStack - TFT Five Ways', 'tool_url': 'https://poestack.com/tft/live-search?tag=five-ways'}, # PoELab - Entry(display_text='PoELab - Normal', tool_url='https://www.poelab.com/gtgax'), - Entry(display_text='PoELab - Cruel', tool_url='https://www.poelab.com/r8aws'), - Entry(display_text='PoELab - Merciless', tool_url='https://www.poelab.com/riikv'), - Entry(display_text='PoELab - Eternal', tool_url='https://www.poelab.com/wfbra'), + {'display_text': 'PoELab - Normal', 'tool_url': 'https://www.poelab.com/gtgax'}, + {'display_text': 'PoELab - Cruel', 'tool_url': 'https://www.poelab.com/r8aws'}, + {'display_text': 'PoELab - Merciless', 'tool_url': 'https://www.poelab.com/riikv'}, + {'display_text': 'PoELab - Eternal', 'tool_url': 'https://www.poelab.com/wfbra'}, # PoENinja - Entry(display_text='PoE Ninja - Builds', tool_url='https://poe.ninja/builds/ancestor'), - Entry(display_text='PoE Ninja - Streamers', tool_url='https://poe.ninja/builds/streamers'), + {'display_text': 'PoE Ninja - Builds', 'tool_url': 'https://poe.ninja/builds/{league_slug}'}, + {'display_text': 'PoE Ninja - Streamers', 'tool_url': 'https://poe.ninja/builds/streamers'}, *[ - Entry( - display_text=f'PoE Ninja - {category.value}', - tool_url=f'https://poe.ninja/economy/ancestor/{category.value.lower().replace(" ", "-")}', - ) + { + 'display_text': f'PoE Ninja - {category.value}', + 'tool_url': f'https://poe.ninja/economy/{{league_slug}}/{category.value.lower().replace(" ", "-")}', + } for category in NinjaCategory ], # misc - Entry(display_text='Craft of Exile', tool_url='https://www.craftofexile.com/'), - Entry(display_text='PoEDB', tool_url='https://poedb.tw/us'), - Entry(display_text='PoEDB - Modifiers', tool_url='https://poedb.tw/us/Modifiers'), - Entry(display_text='PoE Antiquary', tool_url='https://poe-antiquary.xyz'), - Entry(display_text='Vorici Chromatic Calculator', tool_url='https://siveran.github.io/calc.html'), - Entry(display_text='Path of Exile Regex', tool_url='https://poe.re/'), - Entry(display_text='GGG Tracker', tool_url='https://gggtracker.com/'), - Entry(display_text='Official Trade Site', tool_url='https://www.pathofexile.com/trade/search/Ancestor'), - Entry(display_text='Patch Notes Archive', tool_url='https://www.pathofexile.com/forum/view-forum/patch-notes'), - Entry(display_text='Maxroll Passive Tree Planner', tool_url='https://maxroll.gg/poe/poe-passive-tree/'), - Entry(display_text='Maxroll Atlas Tree Planner', tool_url='https://maxroll.gg/poe/poe-atlas-tree/'), - Entry(display_text='Large Cluster Jewel Calculator', tool_url='https://theodorejbieber.github.io/PoEClusterJewelCalculator/'), - Entry(display_text='Timeless Jewel Finder', tool_url='https://vilsol.github.io/timeless-jewels/tree'), + {'display_text': 'Craft of Exile', 'tool_url': 'https://www.craftofexile.com/'}, + {'display_text': 'PoEDB', 'tool_url': 'https://poedb.tw/us'}, + {'display_text': 'PoEDB - Modifiers', 'tool_url': 'https://poedb.tw/us/Modifiers'}, + {'display_text': 'PoE Antiquary', 'tool_url': 'https://poe-antiquary.xyz'}, + {'display_text': 'Vorici Chromatic Calculator', 'tool_url': 'https://siveran.github.io/calc.html'}, + {'display_text': 'Path of Exile Regex', 'tool_url': 'https://poe.re/'}, + {'display_text': 'GGG Tracker', 'tool_url': 'https://gggtracker.com/'}, + {'display_text': 'Official Trade Site', 'tool_url': 'https://www.pathofexile.com/trade/search/{league_title}'}, + {'display_text': 'Patch Notes Archive', 'tool_url': 'https://www.pathofexile.com/forum/view-forum/patch-notes'}, + {'display_text': 'Maxroll Passive Tree Planner', 'tool_url': 'https://maxroll.gg/poe/poe-passive-tree/'}, + {'display_text': 'Maxroll Atlas Tree Planner', 'tool_url': 'https://maxroll.gg/poe/poe-atlas-tree/'}, + {'display_text': 'Large Cluster Jewel Calculator', 'tool_url': 'https://theodorejbieber.github.io/PoEClusterJewelCalculator/'}, + {'display_text': 'Timeless Jewel Finder', 'tool_url': 'https://vilsol.github.io/timeless-jewels/tree'}, ] -def get_tools() -> Generator[Entry, None, None]: - yield from all_tools +def get_tools(league: League) -> Generator[Entry, None, None]: + for data in tool_data: + url_template = data['tool_url'] + if is_format_string(url_template): + data['tool_url'] = url_template.format(league_title=league.title, league_slug=league.slug) + + yield Entry(**data) diff --git a/data/trade.py b/data/trade.py index dcf38dd..0669863 100644 --- a/data/trade.py +++ b/data/trade.py @@ -1,6 +1,7 @@ import json from urllib.parse import quote +from .leagues import League from .ninja import NinjaCategory from .types import URL from .utils import slugify @@ -119,7 +120,7 @@ } -def automake_trade_url(category: NinjaCategory, item_name: str, base_item: str | None = None) -> URL: +def automake_trade_url(league: League, category: NinjaCategory, item_name: str, base_item: str | None = None) -> URL: # NinjaCategory is a good (but not perfect) indicator of whether an item is bulk tradable. if category in bulk_tradable_ninja_categories: if category in { @@ -129,16 +130,16 @@ def automake_trade_url(category: NinjaCategory, item_name: str, base_item: str | NinjaCategory.SCOURGED_MAPS, }: item_name = f'{item_name} Tier 16' - return make_bulk_trade_url(item_name) + return make_bulk_trade_url(league, item_name) if base_item: trade_type, trade_name = base_item, item_name else: trade_type, trade_name = item_name, None - return make_trade_url(trade_type, name=trade_name) + return make_trade_url(league, trade_type, name=trade_name) -def make_trade_url(type_: str, name: str | None = None) -> URL: +def make_trade_url(league: League, type_: str, name: str | None = None) -> URL: # do not change the order of the keys carelessly! the trade site is very sensitive about them. query = { 'query': { @@ -153,10 +154,10 @@ def make_trade_url(type_: str, name: str | None = None) -> URL: query['query']['name'] = name query_quoted = quote(json.dumps(query)) - return f'https://www.pathofexile.com/trade/search/Ancestor?q={query_quoted}' + return f'https://www.pathofexile.com/trade/search/{league.title}?q={query_quoted}' -def make_bulk_trade_url(name: str) -> URL: +def make_bulk_trade_url(league: League, name: str) -> URL: c, div = bulk_trade_tokens['Chaos Orb'], bulk_trade_tokens['Divine Orb'] if name == 'Divine Orb': @@ -180,4 +181,4 @@ def make_bulk_trade_url(name: str) -> URL: } query_quoted = quote(json.dumps(query)) - return f'https://www.pathofexile.com/trade/exchange/Ancestor?q={query_quoted}' + return f'https://www.pathofexile.com/trade/exchange/{league.title}?q={query_quoted}' diff --git a/data/types.py b/data/types.py index ff5ceb2..28eb4fd 100644 --- a/data/types.py +++ b/data/types.py @@ -50,3 +50,11 @@ class NinjaCategory(Enum): BEASTS = 'Beasts' ESSENCES = 'Essences' VIALS = 'Vials' + + +@unique +class LeagueType(Enum): + CHALLENGE = 'challenge' + CHALLENGE_HARDCORE = 'challengehc' + STANDARD = 'standard' + HARDCORE = 'hardcore' diff --git a/data/utils.py b/data/utils.py index e6c771a..451537a 100644 --- a/data/utils.py +++ b/data/utils.py @@ -1,6 +1,10 @@ +import argparse import dataclasses +import enum import logging +import string import urllib.parse +from typing import Any import humanize import requests @@ -54,3 +58,49 @@ def make_poedb_url(item_name: str) -> URL: def slugify(text: str) -> str: text = text.replace("'", '').replace(' ', '-') return text.lower() + + +class EnumAction(argparse.Action): + """ + Argparse action for handling Enums. + + Courtesy by Tim on StackOverflow: https://stackoverflow.com/a/60750535 + Slight adaptations to satisfy the project's linter. + """ + def __init__(self, **kwargs) -> None: + # Pop off the type value + enum_type = kwargs.pop('type', None) + + # Ensure an Enum subclass is provided + if enum_type is None: + msg = 'type must be assigned an Enum when using EnumAction' + raise ValueError(msg) + if not issubclass(enum_type, enum.Enum): + msg = 'type must be an Enum when using EnumAction' + raise TypeError(msg) + + # Generate choices from the Enum + kwargs.setdefault('choices', tuple(e.value for e in enum_type)) + + super().__init__(**kwargs) + + self._enum = enum_type + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, # noqa: ANN401 + option_string: str | None = None, + ) -> None: + # Convert value back into an Enum + value = self._enum(values) + setattr(namespace, self.dest, value) + + +def is_format_string(str_: str) -> bool: + """ + Checks if a string is a format string, e.g. "Hello {name}". + """ + fmter = string.Formatter() + return next(fmter.parse(str_))[1] is not None diff --git a/data/wiki.py b/data/wiki.py index cb40461..5ed63b3 100644 --- a/data/wiki.py +++ b/data/wiki.py @@ -5,6 +5,7 @@ from tabulate import tabulate +from .leagues import League from .ninja import NinjaCategory, get_ninja_index, make_ninja_url from .trade import automake_trade_url from .types import Rarity @@ -224,10 +225,10 @@ def iter_wiki_query(**cargo_params: dict[str, str]) -> Generator[dict, None, Non } -def get_items() -> Generator[Entry, None, None]: +def get_items(league: League) -> Generator[Entry, None, None]: ninja_unknown = [] - ninja_index = get_ninja_index() + ninja_index = get_ninja_index(league) for item in iter_wiki_query( tables='items', @@ -264,10 +265,10 @@ def get_items() -> Generator[Entry, None, None]: ninja_category is not None and ninja_category not in {NinjaCategory.CLUSTER_JEWELS, NinjaCategory.HELMET_ENCHANTS} ): - entry_kwargs['ninja_url'] = make_ninja_url(name, base_item, ninja_category) + entry_kwargs['ninja_url'] = make_ninja_url(league, name, base_item, ninja_category) if tradable: - entry_kwargs['trade_url'] = automake_trade_url(ninja_category, name, base_item=base_item) + entry_kwargs['trade_url'] = automake_trade_url(league, ninja_category, name, base_item=base_item) yield Entry(**entry_kwargs) diff --git a/src/main/main.js b/src/main/main.js index 886c92b..e70f766 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -1,6 +1,7 @@ const { app, BrowserWindow, ipcMain, shell, globalShortcut, dialog } = require('electron') const path = require('node:path') const tray = require('./tray') +const { userSettings } = require('./storage') app.commandLine.appendSwitch('wm-window-animations-disabled') @@ -64,6 +65,7 @@ app.whenReady().then(() => { ipcMain.handle('externalUrlOpen', (event, url) => shell.openExternal(url)) ipcMain.handle('panic', (event, msg) => panic(window, msg)) ipcMain.handle('hideWindow', () => window.hide()) + ipcMain.handle('getSelectedLeague', () => userSettings.get('league')) const shortcut = 'CommandOrControl+Shift+P' const ret = globalShortcut.register(shortcut, () => toggleWindowVisibility(window)) diff --git a/src/main/preload.js b/src/main/preload.js index 4536515..2bd43ca 100644 --- a/src/main/preload.js +++ b/src/main/preload.js @@ -4,4 +4,5 @@ contextBridge.exposeInMainWorld('electronAPI', { externalUrlOpen: (url) => ipcRenderer.invoke('externalUrlOpen', url), panic: (msg) => ipcRenderer.invoke('panic', msg), hideWindow: () => ipcRenderer.invoke('hideWindow'), + getSelectedLeague: () => ipcRenderer.invoke('getSelectedLeague'), }) diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index c2d5dec..e9c07b5 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -29,4 +29,4 @@ const init_minisearch = async (league) => { console.log(`search index size: ${POEPALETTE_MINISEARCH.termCount}`) } -init_minisearch('challenge') +window.electronAPI.getSelectedLeague().then((league) => init_minisearch(league))