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 check_roster( requester: nsapi.NSRequester, nations: t.Mapping[str, t.Collection[str]]) -> t.Mapping[str, Result]: """Checks for the WA of each nation. Checks the main nation, and the list of puppets. If none are in the WA, prompts for a new nation. """ output = {} for nation, puppets in nations.items(): # Find current WA nation(s): SHOULD NOT BE PLURAL, WARN IF SO WAs = [] if is_wa(requester, nation): WAs.append(nation) WAs.extend(puppet for puppet in puppets if is_wa(requester, puppet)) # Warn if len > 1 if len(WAs) > 1: print(f"WARNING: {nation} has multiple WA nations: {WAs}") # Construct Result object if WAs: result = Result(nsapi.clean_format(WAs[0])) else: result = Result(None) output[nsapi.clean_format(nation)] = result return output
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 read_plain(lines: t.Iterable[str]) -> t.Mapping[str, str]: """Converts each line into a nation: wa pair""" output = {} for line in lines: # Try to find the two matches waMatch = re.search( r'\[a href="https://www.nationstates.net/nation=(.*?)"\]', line ) nationMatch = re.search(r"\](.*?)\[", line) # If both succeed add the pair if waMatch and nationMatch: output[nsapi.clean_format(nationMatch[1])] = nsapi.clean_format(waMatch[1]) return output
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 report_status( requester: nsapi.NSRequester, table: t.Iterable[t.Sequence[str]]) -> t.Iterable[t.Sequence[str]]: """Parses a table (such as sourced from csv), and returns columns reporting nation status. The returned iterable represents a iterable of rows in the same order as input. """ iterator = iter(table) header = next(iterator) # Expected headers: # username, forum, nation # use username as primary key # usernameIndex = header.index("username") # forumIndex = header.index("forum") nationIndex = header.index("nation") # define output headers output = [("link", "cte", "region", "wa", "label")] link = "https://www.nationstates.net/nation={}" for row in iterator: nation = row[nationIndex] status = retrieve_status(requester, nation) output.append(( link.format(nsapi.clean_format(nation)), str(status.cte), str(status.region), str(status.wa), xki_label(status), )) return output
def _get_forum_names() -> t.Mapping[str, str]: """Returns a mapping of main nation names to forum usernames. If the main nation does not have a forum account, it will not be in the map. This method only provides nations part of the XKI Card Coop program. Guarenteed to return cleaned format of nation names. """ source = ( "https://docs.google.com/spreadsheets/d/e/2PACX-1vSem15AVLXgdjxWBZOnWRFnF6NwkY0gVKPYI8" "aWuHJzlbyILBL3o1F5GK1hSK3iiBlXLIZBI5jdpkVr/pub?gid=1835258219&single=true&output=tsv" ) logging.info("Retrieving nation-forum mappings") text = requests.get(source).text # 2D table: each first-level element is a row, # each row contains 1st element main nation and # optionally 2nd forum username. # If there is no username, the second element should be "", which is falsy table = [line.split("\t") for line in text.split("\r\n")] usernames = {nsapi.clean_format(row[0]): row[1] for row in table if row[1]} logging.info("Retrieved nation-forum mappings") return usernames
def _get_forum_names() -> t.Mapping[str, str]: """Returns a mapping of main nation names to forum usernames. If the main nation does not have a forum account, it will not be in the map. This method only provides nations part of the XKI Card Coop program. Guarenteed to return cleaned format of nation names. """ source = FORUM_SPREADSHEET logger.info("Retrieving nation-forum mappings") text = requests.get(source).text # 2D table: each first-level element is a row, # each row contains 1st element main nation and # optionally 2nd forum username. # If there is no username, the second element should be "", which is falsy table = [line.split("\t") for line in text.split("\r\n")] usernames = {nsapi.clean_format(row[0]): row[1] for row in table if row[1]} logger.info("Retrieved nation-forum mappings") return usernames
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 logger.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 logger.info("Collecting WA members who have not been endorsed") nonendorsed = ( # Save name string, converting to lowercase, underscore format nsapi.clean_format(nation.name) for nation in waMembers # Check if unendorsed by checking endorsements if endorser not in nation.endorsements) return (region, nonendorsed)
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 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 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 main() -> None: """Main function""" # Create requester requester = nsapi.NSRequester(config.userAgent) print("Provide a data file to work through.") print( "Ideally, the file should be a csv-type text format, with each row in this format:" ) print("<card_link>,<sender>,<receiver>,<sender_password>") print( "e.g.: 'https://www.nationstates.net/page=deck/card=926511/season=1,hn67,hn67_ii,aPassword" ) print( "Technically, only the 'card=num/season=num' part of the link is required." ) print( "Also, the password is only required for the first occurence of a nation." ) print( "If your password contains a comma (','), it should be enclosed with double quotes (\")." ) print( "The path to the file is relative to this script. If the file is in the same directory," ) print("just enter the name (e.g. 'gifts.csv')") dataPath = input("Data file: ") print("\nInstead of saving your password in the file," " you can alternatively provide an API autologin,") print("which is a encrypted by NationStates version.") print("Enter 'yes'/'y' to interpret the passwords as autologins," " or 'no' (or anything else) to not.") autologinInput = input("Interpret password fields as autologin? ").lower() if autologinInput in ("yes", "y"): autologin = True print("Interpreting password field as autologins.") else: autologin = False # We save Nation objects so that they can all use the same Auth, i.e. pin nations: MutableMapping[str, nsapi.Nation] = {} with open(nsapi.absolute_path(dataPath), "r", newline="") as file: csvReader = csv.reader(file) for row in csvReader: try: # We need to prepare or find the nation object first (of the sender) nation = nsapi.clean_format(row[1]) if nation not in nations: nations[nation] = requester.nation(nation) # Update the nation auth using given password (or autologin) # Autologin is True if the passwords are actually autologins if autologin: nations[nation].login(row[3]) else: nations[nation].auth = nsapi.Auth(password=row[3]) # Now we can delegate to the function send_card(link=row[0], sender=nations[nation], receiver=row[2]) print(f"Sent {row[0]} from {nation} to {row[2]}") # Broad error cause we want to catch anything, so the whole # program doesnt crash. logs the message except Exception as exception: # pylint: disable=broad-except print("Failed to send a card. Error:", exception)
def main() -> None: """Main method""" # TODO add type checking into the arguments? # would possibly provide better error messages parser = argparse.ArgumentParser( description="Count the issues answered by a set of nations.") subparsers = parser.add_subparsers( help="The possible modes, rerun with [mode] -h for more info.", dest="sub", ) datesParser = subparsers.add_parser("dates") datesParser.add_argument("start", help="The start date, in YYYY-MM-DD format.") datesParser.add_argument("end", help="The end date, in YYYY-MM-DD format.") datesParser.add_argument( "-m", "--month", help="The month to use for a report. Defaults to month of end date.", default=None, ) monthParser = subparsers.add_parser("month") monthParser.add_argument( "month", help=("The month to count across, in YYYY-MM format. " "Compares issue counts from YYYY-MM-01 to (MM+1)-01."), ) # monthParser.add_argument( parser.add_argument( "-r", "--report", action="store_true", help=("Create output in report mode. Produces a formatted file in " "Issue Payout Reports folder for the given month. " "The report is labelled using the month of the starting date, " "unless specified by month parameter."), ) # XKI Puppet List default_source = ( "https://docs.google.com/spreadsheets/d/e/2PACX-1vSem15AVLXgdjxWBZOnWRFnF6NwkY0gVKPYI8" "aWuHJzlbyILBL3o1F5GK1hSK3iiBlXLIZBI5jdpkVr/pub?gid=1588413756&single=true&output=tsv" ) parser.add_argument( "-s", "--source", help=("""URL or file name to retrieve nation list from. Expects either one or two nations seperated by a tab per line, where the second is interpreted as the puppetmaster. Defaults to the XKI puppets URL.""" ), default=default_source, ) parser.add_argument( "-f", "--file", action="store_true", help="Makes the script source from a file instead of URL.", ) parser.add_argument( "-o", "--output", help= "File name to write (raw) output to. Outputs to stdout if not provided.", default=None, ) if len(sys.argv) > 1: args = parser.parse_args() else: inputString = input("Arguments (One line, -h for help): ") args = parser.parse_args(shlex.split(inputString)) requester = nsapi.NSRequester(config.userAgent) # Get input data if args.file: with open(nsapi.absolute_path(args.source)) as file: nations = [line.split("\t") for line in file.readlines()] else: text = requests.get(args.source).text nations = [line.split("\t") for line in text.split("\r\n")] # Convert to puppetmaster dicts # nation[1] is "" if no master, which is falsy # Clean the format of all the names puppets = { nsapi.clean_format(nation[0]): nsapi.clean_format(nation[1]) if nation[1] else nsapi.clean_format(nation[0]) for nation in nations } # Convert dates to start and end date objects if args.sub == "dates": start = datetime.date.fromisoformat(args.start) end = datetime.date.fromisoformat(args.end) # Usually makes sense to have the 'month' (for reports) be the ending date if not provided if args.month: month = datetime.date.fromisoformat(args.month + "-01") else: month = end.replace(day=1) elif args.sub == "month": month = datetime.date.fromisoformat(args.month + "-01") # Last day of previous month, i.e. 08 -> 07-31 start = month - datetime.timedelta(days=1) # Last day of this month, i.e. 08 -> 08-31 end = month.replace( day=calendar.monthrange(month.year, month.month)[1]) logging.info("month: %s start: %s end: %s", month, start, end) counts = count_change(requester, puppets.keys(), start, end) changes = counts[0] # Collect puppetmaster counts collected_count = {puppetmaster: 0 for puppetmaster in puppets.values()} for puppet, change in changes.items(): collected_count[puppets[puppet]] += change def write_output(file: t.TextIO) -> None: for puppetmaster, count in collected_count.items(): print(f"{puppetmaster},{count}", file=file) # Write output if args.output is not None: with open(args.output, "w") as file: write_output(file) else: write_output(sys.stdout) # Generate report if chosen. # Check the subcommand first, because if month # wasnt chosen, the .report attribute wont exist # this avoids any exception due to lazy eval # if args.sub == "month" and args.report: if args.report: report = generate_report(month, collected_count) # Check for output directory if not os.path.isdir(nsapi.absolute_path("IssuePayoutReports")): os.mkdir(nsapi.absolute_path("IssuePayoutReports")) # Write the report with open( nsapi.absolute_path( # Format the month to always be 2-digit f"IssuePayoutReports/issuePayoutReport_{month.year}-{month.month:0>2d}.txt" ), "w", ) as f: f.write(report)
logging.basicConfig(level=level) # Name logger logger = logging.getLogger() # Change nsapi logging level nsapi.logger.setLevel(level=level) requester = nsapi.NSRequester(config.userAgent) # load legendary cards rarityfinder.verify_rarity_data(requester, "legendary") cards = rarityfinder.load_rarity_data("legendary") # load member nations response = requests.get(PUPPET_SPREADSHEET) members = { nsapi.clean_format(line.split("\t")[0]) for line in response.text.split("\r\n") } print("Enter the timestamp to check trading since,") print("or press enter to default to the first day of the previous month.") boundInput = input("Timestamp: ") if boundInput: bound = int(boundInput) else: # first day, first second (12:00 AM) (UTC) of last month lastMonth: int = int( ( # Gets a datetime in the previous month # by subtracting 1 day from the first day of this month
def rule(nation: str) -> str: """Produce a ContainerRise rule for a nation.""" clean = nsapi.clean_format(nation) return fr"@^.*\.nationstates\.net/(.*/)?nation={clean}(/.*)?$ , {nation}"