def load(self): """ Loads all bank account details """ # Get the index pg = self._get_page('index') # Check if the user has a bank account if 'I see you don\'t currently have an account with us' in pg.content: self._logger.warning('User ' + self._usr.username + ' does not have a bank account') return # Load the details try: rows = self._xpath('rows', pg) self.type = str(rows[0].xpath('./td[2]/text()')[0]) self.balance = self._format_nps(rows[1].xpath('./td[2]/text()')[0]) self.interest_rate = float( rows[2].xpath('./td[2]/b/text()')[0].replace('%', '')) self.yearly_interest = self._format_nps( rows[3].xpath('./td[2]/text()')[0]) # Some user's don't have enough for daily interest if 'you might want to deposit a few more Neopoints' not in pg.content: self.daily_interest = self._format_nps( self._xpath('daily', pg)[0]) self.interest_collected = True if 'You have already collected your interest today' in pg.content: self.interest_collected = True except Exception: self._logger.exception('Failed to parse user bank details', {'pg': pg}) raise ParseException('Failed to parse user bank details')
def load(self, owner, index, pages=False): """ Loads the shop contents Arguments: | **owner**: The owner of the shop | **index**: The index page of the shop | **pages**: Whether or not to fetch all shop pages """ # Determine if we're going to load multiple pages try: if pages: # Parse the index self._parse_page(index) # Keep going until we run out of pages i = 1 while True: lim = i * 80 pg = self._get_page('shop', (str(lim), owner)) self._parse_page(pg) if 'Next 80 Items' not in pg.content: break i += 1 else: self._parse_page(index) except Exception: self._logger.exception( 'Failed to parse user front shop with owner: ' + owner, {'pg': pg}) raise ParseException('Failed to parse user front shop')
def load(self): """ Loads the inventory for the :class:`User` instance of this class """ # Load the inventory pg = self._get_page('inventory') # Loops through all non-NC items self.data = [] try: for td in self._xpath('main_inventory', pg): id = str(self._xpath('item/id', td)).replace(');', '') item = InventoryItem(id, self._usr) item.img = str(self._xpath('item/img', td)[0]) item.desc = str(self._xpath('item/desc', td)[0]) item.name = str(self._xpath('item/name', td)[0]) if len(self._xpath('item/rarity', td)) > 0: item.rarity = str(self._xpath('item/rarity', td)[0]) item.rarity = item.rarity.replace('(', '').replace(')', '') self.data.append(item) except Exception: self._logger.exception('Unable to parse user\'s inventory', {'pg': pg}) raise ParseException('Unable to parse user\'s inventory')
def load(self): """ Loads the user's sales history """ # Load the history page pg = self._get_page('sales') try: rows = self._xpath('rows', pg) rows.pop(0) rows.pop() for row in rows: trans = Transaction() details = self._xpath('details', row) trans.date = details[0].text_content() trans.item = details[1].text_content() trans.buyer = details[2].text_content() trans.price = int( self._remove_multi(details[3].text_content(), [',', ' NP'])) self.data.append(trans) except Exception: self._logger.exception('Failed to parse shop history', {'pg': pg}) raise ParseException('Failed to parse shop history')
def till(self): pg = self._get_page('till') try: return int( self._remove_multi(self._xpath('till', pg)[0], [' NP', ','])) except Exception: self._logger.exception('Failed to parse user till', {'pg': pg}) raise ParseException('Failed to parse user till')
def buy(self, price=0): """ Attempts to buy the item from the main shop This function will take the given price (or use the price of the item from the shop if no price is given) and attempt to purchase the item. It automatically scans the generated OCR and attempts to find the x,y coordinate of the darkest pixel. Returns the result. Arguments: **price**: Optional price to buy the item at Returns: Boolean indicating if the purchase was successful """ # Goto the haggle page pg = self._get_page('haggle', (self.id, self.stock_id, self.brr)) # Did we get a price? if not price: price = self.price try: # Check to see if the item is still for sale if pg.form(action='haggle.phtml') and 'SOLD OUT' not in pg.content: form = pg.form(action='haggle.phtml')[0] # Download the image url = self._base_url + self._xpath('captcha', pg)[0] pg = self._usr.get_page(url) x, y = self._crack_OCR(io.BytesIO(pg.content)) form.update(current_offer=str(price), x=str(x), y=str(y)) # Attempt to buy the item pg = form.submit(self._usr) if "I accept" in pg.content: return True else: return False else: return False except Exception: self._logger.exception('Failed to handle haggle page', {'pg': pg}) raise ParseException('Failed to handle haggle page')
def load(self, id, index=None): """ Loads the main shop inventory Arguments: | **id**: The id of the shop to load | **index**: Optional index page of the shop to load from """ # Check if we already have the index page if not index: pg = self._get_page('shop', str(id)) else: pg = index # Clear any existing data self.data = [] try: for row in self._xpath('rows', pg): for td in self._xpath('tds', row): url = td.xpath('./a/@href')[0] onclick = td.xpath('./a/@onclick')[0] id = url.split('obj_info_id=')[1].split('&')[0] stock_id = url.split('stock_id=')[1].split('&')[0] brr = onclick.split('brr=')[1].split('\'')[0] item = MSItem(id, self._usr) item.name = str(td.xpath('./b/text()')[0]) item.img = td.xpath('./a/img/@src') item.stock = int( td.xpath('./text()')[0].replace(' in stock', '')) item.price = int( self._remove_multi( td.xpath('./text()')[1], ['Cost: ', ',', ' NP'])) item.stock_id = stock_id item.brr = brr self.data.append(item) except Exception: self._logger.exception( 'Failed to parse main shop inventory for id: ' + str(id), {'pg': pg}) raise ParseException('Failed to parse main shop inventory')
def get_details(self): """ Fills in the item details obtained off the item info page Most details can be obtained directly off of the user's inventory screen, however a few key details like weight and value cannot and this function should be called to retrieve these """ pg = self._get_page('item', self.id) if "not in your inventory" in pg.content: self._logger.error('No item with id ' + self.id + ' in inventory') raise InvalidItemID(self.id) try: self.name = str(self._xpath('name', pg)[0].replace(' : ', '')) self.img = str(self._xpath('image', pg)[0]) self.desc = str(self._xpath('desc', pg)[0]) self.type = str(self._xpath('type', pg)[0]) self.weight = str(self._xpath('weight', pg)[0]) self.rarity = str(self._xpath('rarity', pg)[0]) self.value = str(self._xpath('value', pg)[0]) except Exception: self._logger.exception('Failed to parse details for ' + self.name, {'pg': pg}) raise ParseException('Failed to parse details for ' + self.name)
def load(self, index=None): """ Loads the shop inventory for the :class:`User` instance of this class Arguments: **index**: Optional :class:`Page` instance with the user's shop index page loaded """ # Load the main index if not index: pg = self._get_page('index') else: pg = index # Reset items if already loaded self.data = [] # Determine how many pages there are. pages = len(self._xpath('pages', pg)) # The first link is not a page self.pages = pages - 1 # Always have at least one page if self.pages <= 0: self.pages = 1 try: # Load the items from the first page self._parse_page(pg, 1) # Grab from the remaining pages. By starting at 2 we ignore the index. for i in range(2, pages): pg = self._get_page('page', str(i * self._items_per_page)) self._parse_page(pg, i) except Exception: self._logger.exception('Failed to parse user shop', {'pg': pg}) raise ParseException('Failed to parse user shop')
def load(self): """Fetches the user profile data and populates the attributes Query's the user's profile on Neopets using the username of the given :class:`User` object when the profile was initialized. This method can be called multiple times to update the loaded profile details. Raises: **ParseException**: An error occured parsing the user's profile """ # Much of the user profile can change without much uniformity. This # means certain HTML elements may or may not be in different places. # It all depends on how much the user has filled their profile in. # Therefore, this class uses a combination of xpath and regular # expressions to parse all required information from the profile. # Since the pattern for parsing with regular expressions and xpath is # fairly similar across all the sections of a user profile, a # refactored function was implemented which takes an exception function # for dealing with any exceptional fields when running through a # profile section. # The age of an account is only displayed in the form of an image. This # image can represent either the number of weeks, months, or years. The # below exception function determines which type of image it is and # calculates the proper age. def general_exception(self, key, result, html): if key == 'age': if 'years' in result: self.age = int(result.split("_")[0]) * 12 elif 'mth' in result: self.age = int(result.split('mth')[0]) else: self.age = int(result.split('wk')[0]) / 4 return True else: return False # The number of neocards a user has is display differently depending on # if the user has an active deck. The below exception function # determines which case exists and parses accordingly. def collections2_exception(self, key, result, html): if key == 'neocards': # If the first match failed it will match an entire sentence. # So we just check the results length to see if it failed if len(result) > 4: result = self._search('colls2/neocards2', html, True)[0] self.neocards = int(self._remove_extra(result)) else: self.neocards = int(self._remove_extra(result)) return True elif key == 'neocards2': return True else: return False # Get the profile page pg = self._get_page('profile', self._usr.username) try: # Parse the general profile details self._set_attributes(pg, 'general', 'general', general_exception) # Parse the first set of collections self._set_attributes(pg, 'colls1', 'colls1') # Parse the second set of collections self._set_attributes(pg, 'colls2', 'colls2', collections2_exception) # The user might not have a shop or gallery if 'Shop & Gallery' in pg.content: self._set_attributes(pg, 'shop_gallery', 'shop_gallery') # The neopets are parsed slightly differently for td in pg.xpath(self._paths['neopets']): html = self._to_html(td) pet = Neopet() for key in self._regex['neopets'].keys(): result = self._search('neopets/' + key, html) if result: setattr(pet, key, self._remove_extra(result[0])) self.neopets.append(pet) except: self._logger.exception( 'Failed to parse user profile with name: ' + self._usr.username, {'pg': pg}) raise ParseException('Could not parse user profile')
def search(self, item, area='shop', criteria='exact', min=0, max=99999): """ Searches the shop wizard with the provided details Arguments: | **item**: The name of the item to search for | **area**: Optional area to search for the item | **criteria**: Method to search with | **min**: The minimum price of the item | **max**: The maximum price of the item Returns: Instance of :class:`WizardItemList` with the search results or None if the search returned zero results """ # Load the index pg = self._get_page('index') # Grab the form and set the values form = pg.form(action='market.phtml')[0] data = { 'type': 'process_wizard', 'feedset': '0', 'shopwizard': item, 'table': area, 'criteria': criteria, 'min_price': min, 'max_price': max, } form.update(data) # Submit the form and get the results pg = form.submit(self._usr) # Check for results if 'I did not find anything' in pg.content: return [] # Check for ban if 'too many searches' in pg.content: raise WizardBanned # Parse the results try: rows = self._xpath('rows', pg) rows.pop(0) items = [] for row in rows: details = self._xpath('details', row) url = details[0].xpath('./a/@href')[0] id = url.split('obj_info_id=')[1].split('&')[0] item = WizardItem(id, self._usr) item.owner = str(details[0].text_content()) item.name = str(details[1].text_content()) item.stock = int(details[2].text_content()) item.price = int( self._remove_multi(details[3].text_content(), [',', ' NP'])) items.append(item) except Exception: self._logger.log('Failed to parse shop wizard results', {'pg': pg}) raise ParseException('Could not parse shop wizard results') return WizardItemList(self._usr, items)
def move(self, location): """ Moves all items in the current list to the destination specified Arguments: | **location**: The destination to move the items to Returns: Boolean inicating if the move was successful Example: >>> snowballs = usr.inventory.find(name__contains='Snowball') >>> snowballs.move(snowballs.SHOP) True """ pg = self._get_page('quickstock') form = pg.form(action='process_quickstock.phtml')[0] # First we need to build a dictionary of positions and id's ids = {} try: for name, inp in form.items(): if 'id_arr' in name: pos = inp.name.split('[')[1].replace(']', '') ids[pos] = inp.value except Exception: self._logger.exception('Unable to parse item id\'s') raise ParseException('Unable to parse item id\'s') # Next we need to remove all positions that are not changing remove = [] for pos in ids.keys(): found = False for item in self.data: if item.id == ids[pos]: found = True if not found: remove.append(pos) for pos in remove: del ids[pos] # Now we need to remove all radio fields that are not changing try: for name, inp in list(form.items()): if 'radio_arr' in name: pos = inp.name.split('[')[1].replace(']', '') if pos not in list(ids.keys()): del form[name] except Exception: self._logger.exception('Unable to parse radio fields') raise ParseException('Unable to parse radio fields') # Finally we need to set the destination for remaining radio fields if location == self.SDB: value = self.SDB elif location == self.DONATE: value = self.DONATE elif location == self.DROP: value = self.DROP elif location == self.SHOP: value = self.SHOP elif location == self.GALLERY: value = self.GALLERY else: return False for name, inp in list(form.items()): if 'radio_arr' in name: inp.value = value # Now we submit the form and check the result pg = form.submit(self._usr) if 'red_oops.gif' in pg.content: return False else: return True
def __init__(self, usr, owner, item_id='', price=''): """ Loads the shops inventory and grabs main item if necessary The initialization of this class takes multiple parameters for different scenarios. If a shop needs to be loaded (including all shop pages) then the `usr` and `owner` arguments should only be supplied. If a specific item in the shop is being sought after (like from a shop wizard result) then the item id and price should also be supplied. In the latter case only the first page of the user shop is loaded and if the item is found it will be appended to the very beginning of the inventory list. Arguments: | **usr**: The :class:`User` instance to use | **owner**: The username of the owner of the shop | **item_id**: The item id to search for | **price**: The price of the item to search for """ super().__init__(usr) # Load the appropriate page if item_id and price: pg = self._get_page('shop_item', (owner, item_id, str(price))) else: pg = self._get_page('shop', owner) # Get the details try: self.name = str(self._xpath('name', pg)[0]) self.keeper_img = self._xpath('keeper_img', pg)[0] self.keeper_name = str( self._xpath('keeper_text', pg)[0].split(' says')[0]) self.keeper_message = str( self._xpath('keeper_text', pg)[0].split('says ')[1].replace('\'', '')) except Exception: self._logger.exception('Failed to parse user front shop details') raise ParseException('Failed to parse user front shop details') # Load the inventory self.inventory = USFrontInventory(self._usr) if item_id and price: self.inventory.load(owner, pg) else: self.inventory.load(owner, pg, True) # If searching for a specific item, find it if item_id and price: try: td = self._xpath('main_item', pg)[0] item = USFrontItem('', self._usr) item.url = td.xpath('./a/@href')[0] item.name = str(td.xpath('./b/text()')[0]) item.stock = int( td.xpath('./text()[3]')[0].replace(' in stock', '')) item.price = int( self._remove_multi( td.xpath('./text()[4]')[0], [',', ' NP', 'Cost : '])) # Add it to the beginning of the inventory self.inventory.data = [item] + self.inventory.data except Exception: # This most likely means the item has already been bought pass