def autologin(requester: nsapi.NSRequester, nations: Mapping[str, str], isAutologin: bool) -> Mapping[str, Optional[Result]]: """Autologins a list of nations, from the nations dict of {nation: password/autologin}. If isAutologin is true, the passwords are interpreted as autologins. Returns a dict mapping from nations to autologins. If the autologin value of the returned mapping is None, that means the login failed. """ output: Dict[str, Optional[Result]] = {} for nation, password in nations.items(): # Check how to interpret password if isAutologin: nationAPI = requester.nation(nation, nsapi.Auth(autologin=password)) else: nationAPI = requester.nation(nation, nsapi.Auth(password=password)) # Try making the shard request try: shards = nationAPI.shards("region", "ping", "wa") except nsapi.APIError: output[nation] = None else: output[nation] = Result( autologin=nationAPI.get_autologin(), region=shards["region"], wa=shards["unstatus"].startswith("WA"), ) return output
def unendorsed_nations_v2( requester: nsapi.NSRequester, endorser: str ) -> Tuple[str, Iterable[str]]: """Finds all WA members of the nation's region who have not been endorsed Returns a tuple containing the region, and a iterable of unendorsed nations """ # Retrieve endorser region and endorsement list logging.info("Collecting WA Members") info = requester.nation(endorser).shards("region", "endorsements") region = info["region"] endorsements = set(info["endorsements"].split(",")) # Retrieve regional citizens by cross referencing residents and wa members citizens = set(requester.wa().shard("members").split(",")) & set( requester.region(region).shard("nations").split(":") ) # Optionally only check nations who havent endorsed the endorser nations = citizens - endorsements # Check each nation's endorsments logging.info("Checking WA members for endorsement") nonendorsed = [ nation for nation in nations if endorser not in requester.nation(nation).shard("endorsements") ] return (region, nonendorsed)
def unendorsed_nations( requester: nsapi.NSRequester, endorser: str ) -> Tuple[str, Iterable[str]]: """Finds all WA members of the nation's region who have not been endorsed Returns a tuple containing the region, and a iterable of unendorsed nations """ # Collect region region = requester.nation(endorser).shard("region") # Load downloaded nation file # Pack into conversion generator to simplify transformations nationDump = requester.dumpManager().nations() # Pull all nations in the region that are WA members # Use generator because we dont need to generate a list that is never used logging.info("Collecting %s WA Members", region) waMembers = [ nation for nation in nationDump if nation.region == region and nation.WAStatus.startswith("WA") ] # Pull nations who are not endorsed logging.info("Collecting WA members who have not been endorsed") nonendorsed = [ # Save name string, converting to lowercase, underscore format clean_format(nation.name) for nation in waMembers # Check if unendorsed by checking endorsements if endorser not in nation.endorsements ] return (region, nonendorsed)
def check_region(requester: nsapi.NSRequester, region: str, previous: Container[str] = None) -> Sequence[str]: """Checks all nations in the specified region for trading card activity. Makes at most 1 + region population requests (1 for each nation). A nation does not qualify if it is in the `previous` container. """ # If a previous list is not provided, use an empty set if not previous: previous = set() participants = [] # Grabs all residents of the region residents = requester.region(region).shard("nations").split(":") # Make a request for each resident for nation in residents: if nation not in previous: info = requester.nation(nation).deck_info() # Save the nation if it meets any of the requirments if (info.numCards > 0 or info.bank > 0 or info.lastValued or info.lastPackOpened): participants.append(nation) return participants
def count_change( requester: nsapi.NSRequester, nations: t.Collection[str], start: datetime.date, end: datetime.date, ) -> t.Tuple[t.Mapping[str, int], t.Iterable[str]]: """For each nation provided, retrieves the number of issues they have answered between the two dates. The returned mapping contains .clean_format version of nation names. """ starting = {} ending = {} invalid = [] # Get the starting number of issues for nation in requester.dumpManager().nations(date=start): # Check if either normal or clean name was provided if nation.name in nations or nsapi.clean_format( nation.name) in nations: starting[nsapi.clean_format(nation.name)] = nation.issuesAnswered # Get the ending number of issues for nation in requester.dumpManager().nations(date=end): # Check if either normal or clean name was provided if nation.name in nations or nsapi.clean_format( nation.name) in nations: ending[nsapi.clean_format(nation.name)] = nation.issuesAnswered # Check/make sure both starting/ending have all the nations # (e.g. differences may occur when a nation ceased to exist, was founded, etc) # If a nation didnt exist on the start date, it started with 0 answered. # If a nation didnt exist on the end date, it must have CTE # there is not really an easy way of finding the ending number of issues answered, # so we cant really calculate. for nationName in nations: if nationName not in starting: starting[nationName] = 0 if nationName not in ending: starting.pop(nationName) # track the invalid nations invalid.append(nationName) # Calculate the difference between end and start for each nation in # starting, which should have identical keys to ending so it doesnt matter # which one we iterate through # Standardize nation name formatting delta = { nsapi.clean_format(nationName): ending[nationName] - starting[nationName] for nationName in starting } return delta, invalid
def low_endorsements(requester: nsapi.NSRequester, region: str, count: int = 20) -> Collection[str]: """ Finds nations with low endorsements. Searches the nation dump for WA nations in the specified region with less endorsements than the given count. """ filtered = [] # Squash casing on region lower_region = region.lower() # Search for matching nations for nation in requester.dumpManager().nations(): if ( # Compare regions, case insensitive nation.region.lower() == lower_region # Check that in WA and nation.WAStatus.startswith("WA") # Check that endorsements are under the specified level and len(nation.endorsements) <= count): # Save to return at end of search filtered.append(nation.name) return filtered
def login( requester: nsapi.NSRequester, nation: str, autologin: t.Optional[str], password: t.Optional[str], ) -> t.Optional[Result]: """Attempts to log in a nation via NS API. Returns None on failure. At least one of password or autologin must be provided; autologin is used if both are provided. """ # Create API object nationAPI = requester.nation( nation, auth=nsapi.Auth(autologin=autologin, password=password) ) # Try making the shard request try: shards = nationAPI.shards("region", "ping", "wa") except nsapi.APIError: # None indicates any failure return None else: return Result( autologin=nationAPI.get_autologin(), region=shards["region"], wa=shards["unstatus"].startswith("WA"), )
def founder_endings( requester: nsapi.NSRequester, since: t.Optional[int] = None, before: t.Optional[int] = None, referenceDate: datetime.date = None, ) -> t.Iterable[Ending]: """Returns the founder endings in the given timestamp frame. `before` defaults to now, and `since` defaults to 24 hours ago. Uses the given referenceDate to retrive a region dump to cross-reference with endings with region founders (defaults to most recent dump). """ endings = retrieve_endings(requester, since, before) # convert endings to nation: region map endingRegions = { nsapi.clean_format(ending.nation): ending.region for ending in endings } # cross reference with region dump founderEndings = ( Ending( nation=nsapi.clean_format(region.founder), region=endingRegions[nsapi.clean_format(region.founder)], ) for region in requester.dumpManager().regions(date=referenceDate) if nsapi.clean_format(region.founder) in endingRegions.keys()) return founderEndings
def answer_all( requester: nsapi.NSRequester, name: str, autologin: str, optionNum: Optional[int] = None, ) -> Iterable[Tuple[int, int, str]]: """Answers all available issues on the nation. If optionNum is provided, attempts to answer all issues with that option (starting at 0, so 0 is the first option). If an issue does not have that many options, answers with the last. Counts by available options, not all options. Returns tuples encoding (id, option, description). """ # Create and add authentication to the nation API object nation = requester.nation(name) nation.login(autologin) # Prepare output list output: List[Tuple[int, int, str]] = [] # Iterate through all available issues for issue in nation.issues(): # Use the option requested if optionNum: option = list(issue.options.keys())[optionNum] # If none, answer randomly else: option = random.choice(list(issue.options.keys())) info = nation.answer_issue(issue=issue.id, option=option) # info[1] should be the DESC node output.append((issue.id, option, info[1].text if info[1].text else "")) return output
def uncrossed(requester: nsapi.NSRequester, nation: str, lead: str, duration: int = 3600) -> Iterable[str]: """Returns a iterable of non-recently endorsed nations sourced from lead endorsements `duration` is the number of seconds to check back in the nation's endo happenings """ # Source endorsement list from lead logging.info("Retrieving lead endorser list") nations: Set[str] = set( requester.nation(nation).shard("endorsements").split(",")) # Retrieve recent endo happenings in text form timestamp = int(time.time()) - duration logging.info("Retrieving endo happenings of nation since %s", timestamp) endos: List[str] = [ happening.text for happening in requester.world().happenings( view=f"nation.{nation}", filter="endo", sincetime=str(timestamp), ) ] # Parse endo happening texts # A text can be one of the following two 'distinct' forms: # based on 'hn67' == nation # '@@hn67@@ endorsed @@hn67_ii@@.' (outgoing) # '@@hn67_ii@@ endorsed @@hn67@@.' (incoming) # we only care about outgoing endorsements # the endorsed nation is retrieved using the following process: # .split() transforms the above outgoing example to ['', 'hn67', 'endorsed', 'hn67_ii', ''] # index [3] retrieves the endorsed nation, index [1] retrieves the endorser # [1] is used to verify outgoing ( by checking == nation), and [3] retrieves the endorsee # Note: Splitting twice, could be optimized with pre-comprehension, for loop, or walrus operator logging.info("Filtering for outgoing endorsements") outgoing: Set[str] = set( text.split("@@")[3] for text in endos if text.split("@@")[1] == nation) # Unendorsed is obtained by subtracting the outgoing endorsements from the lead endorsement list logging.info("Obtaining uncross endorsed nations") unendorsed = nations - outgoing return unendorsed
def collect(requester: nsapi.NSRequester, region: str) -> t.Mapping[str, t.Iterable[str]]: """Compiles the endorsees of every WA nation in the region.""" # Cross-reference region nations and WA members region_nations = set(requester.region(region).nations()) wa_nations = set(requester.wa().shard("members").split(",")) members = region_nations & wa_nations # A dict mapping each WA member of this region to the nations they endorse endorsees: t.Dict[str, t.List[str]] = {member: [] for member in members} # Parse nation dump for nation in requester.dumpManager().nations(): # Search for nations that are WA in this region if nation in members: for endorser in nation.endorsements: endorsees[endorser].append(nation.name) return endorsees
def factbook_searcher( requester: nsapi.NSRequester, *keywords: str, populationMinimum: int = 0) -> Sequence[nsapi.RegionStandard]: """Returns Region's which have at least one of the given keywords in their WFE (.factbook)""" return [ region # Maps from XML to RegionStandard, should be single pass since its a generator wrapping for region in requester.dumpManager().regions() # Probably not the most efficient (probably O(n^2)) if any( keyword in region.factbook for keyword in keywords) and region.numnations > populationMinimum ]
def filter_regions( requester: nsapi.NSRequester, exclude: t.Iterable[str], *tags: str ) -> t.Collection[str]: """Returns a collection of regions retrieved from the world region tag api, after filtering is applied. """ excludeSet = set(nsapi.clean_format(exclusion) for exclusion in exclude) return [ region for region in requester.world().regions_by_tag(*tags) if nsapi.clean_format(region) not in excludeSet ]
def find_rarities(requester: nsapi.NSRequester, rarity: str) -> None: """Searches both card dumps for cards of the specified rarity and saves the id/season to a file of the same name""" # Goes through each dump, grabbing any CardStandard that has the right rarity # store season with it, since the CardStandard does not have season cards = [] for card in requester.dumpManager().cards(season="1"): if card.rarity == rarity: cards.append((card, "1")) for card in requester.dumpManager().cards(season="2"): if card.rarity == rarity: cards.append((card, "2")) # Dump everything as strings so its easier to use with open(resolve_path(rarity), "w") as file: json.dump( [{ "cardid": str(card.id), "season": season, "name": card.name } for card, season in cards], file, )
def named_cards( requester: nsapi.NSRequester, cards: Iterable[nsapi.CardIdentifier], ) -> Mapping[int, str]: """Maps each CardIdentifier to the nation name, using the card dumps. Returns a mapping from id to name """ cardMap = {card.id: "" for card in cards} for standard in itertools.chain( requester.dumpManager().cards(season="1"), requester.dumpManager().cards(season="2"), ): if standard.id in cardMap: cardMap[standard.id] = standard.name return cardMap
def get_nations_by_region( requester: nsapi.NSRequester, regions: t.Iterable[str] ) -> t.Mapping[str, t.Iterable[str]]: """Returns an mapping from region to all residents of that region. """ regionSet = set(nsapi.clean_format(string) for string in regions) output: t.Dict[str, t.List[str]] = {} for region in requester.dumpManager().regions(): if nsapi.clean_format(region.name) in regionSet: output[nsapi.clean_format(region.name)] = [ nsapi.clean_format(name) for name in region.nations ] return output
def deployed( requester: nsapi.NSRequester, lead: str, roster: t.Mapping[str, t.Iterable[str]], ) -> t.Collection[str]: """Determine who are deployed and endorsing the lead.""" # Obtain endorsement list of lead endos = map(nsapi.clean_format, requester.nation(lead).endorsements()) # Invert roster into puppet -> main form owners = { nsapi.clean_format(switcher): main for main, switchers in roster.items() for switcher in switchers } # Also include main nations owners.update({main: main for main in roster.keys()}) # Return owners who have a nation endoing lead return [owners[nation] for nation in endos if nation in owners]
def retrieve_endings( requester: nsapi.NSRequester, since: t.Optional[int] = None, before: t.Optional[int] = None, ) -> t.Iterable[Ending]: """Returns the endings in the given timestamp frame. `before` defaults to now, and `since` defaults to 24 hours before `before`. """ # Define default window in seconds DEFAULT_WINDOW = 24 * 3600 # Check for before default if before: beforetime = before else: beforetime = int(time.time()) # Check for since default if since: sincetime = since else: sincetime = beforetime - DEFAULT_WINDOW # Retrieve endings happening data happenings = requester.world().happenings(safe=False, filter="cte", sincetime=str(sincetime), beforetime=str(beforetime)) output = [] for happ in happenings: # Try to match the two patterns nationMatch = re.search(r"@@(.*)@@", happ.text) regionMatch = re.search(r"%%(.*)%%", happ.text) if nationMatch and regionMatch: # Pull the first group of each match, i.e. omit the @@/%% delimiters output.append(Ending(nation=nationMatch[1], region=regionMatch[1])) else: logging.warning( "Found CTE happening with no nation or no region: %s", happ.text) return output
def sorted_cards( requester: nsapi.NSRequester, nations: Iterable[str] ) -> Mapping[str, Mapping[str, Mapping[nsapi.CardIdentifier, int]]]: """Searchs the trading card decks of the given nations (by name), and sorts by rarity Returns a mapping with {rarity: {nation: {card: quantity}}} structure. Always includes exactly `common`, `uncommon`, `rare`, `ultra-rare`, `epic`, and `legendary` """ # Prepare output structure with rarities # empty string key should catch any Card objects that dont have a category for some reason out: MutableMapping[str, MutableMapping[str, MutableMapping[nsapi.CardIdentifier, int]]] = { "common": {}, "uncommon": {}, "rare": {}, "ultra-rare": {}, "epic": {}, "legendary": {}, "": {}, } # Sort the deck of each nation for nation in nations: # Retrieve the deck of the nation (1 request) deck = requester.nation(nation).deck() # Parse each card in the deck, sorting into appropriate rarity bin for card in deck: # We dont wrap the out[] indexing in a try, since there should never be a error: # if for some reason the card doesnt have a category, it should have "" and # get placed in that bin rarityDict = out[card.rarity] # ensure this nation exists in this rarity bin try: nationDict = rarityDict[nation] except KeyError: rarityDict[nation] = {} nationDict = rarityDict[nation] # creates the card: num key-value pair if it doesnt exist # otherwise, increment the count try: nationDict[card] += 1 except KeyError: nationDict[card] = 1 return out
def retrieve_status(requester: nsapi.NSRequester, nation: str) -> Status: """Retrieves info on the nation provided.""" # Check region, wa, and cte try: shard_info = requester.nation(nation).shards("region", "wa") except nsapi.ResourceError: return Status( nsapi.clean_format(nation), True, None, None, ) else: return Status( nsapi.clean_format(nation), False, shard_info["region"], shard_info["unstatus"], )
def delegacy( requester: nsapi.NSRequester, startTime: int = None, endTime: int = None, duration: int = None, ) -> Iterable[nsapi.Happening]: """Obtain all delegacy changes in the requested time period. Up to two of startTime, endTime, and duration should be provided. End time defaults to now, unless overridden by a combination of start and duration. Start time defaults to duration before end time, or 12 hours before end time otherwise. If all three arguments are given, duration is ignored. """ # Determine start and end if endTime is None: if startTime is not None and duration is not None: endTime = startTime + duration else: # Time.time produces a float to represent milli, we just want seconds endTime = int(time.time()) if startTime is None: if duration is not None: startTime = endTime - duration else: # 12 hours startTime = endTime - 12 * 3600 # Filter happenings return [ happening for happening in requester.world().happenings( safe=False, sincetime=str(startTime), beforetime=str(endTime), filter="member", ) if "WA Delegate" in happening.text ]
def delegacy( requester: nsapi.NSRequester, startTime: int = None, endTime: int = None ) -> Iterable[nsapi.Happening]: """Returns all delegacy changes between start and end times (as epoch timestamps), endTime defaults to now, and startTime defaults to 12 hours before endTime """ # Set start/end defaults if not endTime: # Time.time produces a float to represent milli, we just want seconds endTime = int(time.time()) if not startTime: # 12 hours, 3600 seconds an hour, 12 hours previous startTime = endTime - 12 * 3600 # Filter happenings return [ happening for happening in requester.world().happenings( safe=False, sincetime=str(startTime), beforetime=str(endTime), filter="member", ) if "WA Delegate" in happening.text ]
def residents(requester: nsapi.NSRequester, region: str) -> Collection[str]: """Retrieves the WA residents of the given region.""" # Retrieve residents return set(requester.region(region).nations()) & set( requester.wa().shard("members").split(","))
def is_wa(requester: nsapi.NSRequester, nation: str) -> bool: """Checks if the nation is in the WA""" try: return requester.nation(nation).wa().startswith("WA") except nsapi.ResourceError: return False