Guest User

Untitled

a guest
Dec 29th, 2019
293
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.33 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. """
  4. Checks stock on specified items at Microcenter store locations,
  5. and sends email notifications when changes are detected.
  6. Applicably, it helps the user obtain rare items during shortages.
  7. """
  8.  
  9. from aiohttp import ClientSession
  10. from async_timeout import timeout
  11. from getpass import getpass
  12. from re import search
  13. from smtplib import SMTP
  14. import asyncio
  15.  
  16. import base64
  17. from email.mime.audio import MIMEAudio
  18. from email.mime.base import MIMEBase
  19. from email.mime.image import MIMEImage
  20. from email.mime.multipart import MIMEMultipart
  21. from email.mime.text import MIMEText
  22. import mimetypes
  23. import os
  24.  
  25. import pickle
  26. import os.path
  27. from googleapiclient.discovery import build
  28. from google_auth_oauthlib.flow import InstalledAppFlow
  29. from google.auth.transport.requests import Request
  30.  
  31.  
  32.  
  33. class Item:
  34.     """
  35.    Class for containing state of individual items; methods update state
  36.    by awaiting update().
  37.  
  38.    Item does not need to be directly instantiated; Store will create one
  39.    per provided url.
  40.    """
  41.     def __init__(self, storeNum, url):
  42.         self.storeNum, self.url = storeNum, url
  43.         self.sku = self.price = self.stock = None
  44.         self.stockChanged = self.priceChanged = False
  45.         self.loop = asyncio.get_event_loop()
  46.  
  47.     def __str__(self):
  48.         stock = 'in' if self.stock else 'out of'
  49.         return f'SKU {self.sku} is {stock} stock for {self.price} at Microcenter {self.storeNum}\n{self.url}\n'
  50.  
  51.     async def pull(self):
  52.         async with ClientSession() as session:
  53.             async with timeout(10):
  54.                 async with session.get(self.url, params={'storeSelected': self.storeNum}) as response:
  55.                     return await response.text()
  56.  
  57.     @staticmethod
  58.     def parse_lines(page):
  59.         for var in ['SKU', 'inStock', 'productPrice']:
  60.             reply = search(f"(?<='{var}':').*?(?=',)", page)
  61.             if reply:
  62.                 yield reply.group()
  63.  
  64.     @staticmethod
  65.     def compare(new, old):
  66.         return (new != old and old is not None)
  67.  
  68.     async def update(self):
  69.         data = tuple(self.parse_lines(await self.pull()))
  70.         if not data or any(data) is None:
  71.             raise ValueError('Data missing from request or store number invalid')
  72.         self.sku, stock, price = int(data[0]), data[1] is 'True', float(data[2])
  73.         self.stockChanged, self.priceChanged = self.compare(stock, self.stock), self.compare(price, self.price)
  74.         self.stock, self.price = stock, price
  75.  
  76.  
  77. class Store:
  78.     """
  79.    Periodically checks a given list of urls for stock changes
  80.  
  81.    A store number is required to get accurate stock numbers.
  82.    The default store number is set to the North Dallas/Richardson, TX location.
  83.  
  84.    Also required is valid email account information for notifications.
  85.    If a recipient address is not provided, the user will be prompted for one.
  86.    If the prompt is empty, notifications are sent from the sender
  87.    address to itself.  Providing an empty string for recipient is a valid
  88.    argument to enable loopback operation, as only a value of None
  89.    will trigger a prompt.
  90.  
  91.    The default time between checks is 15 minutes.  This value should
  92.    be at least a few minutes, to avoid being blacklisted by the
  93.    server, though this class enforces no such limit.  To change the
  94.    time period, provide a value in minutes to self.run(minutes).
  95.  
  96.    Setting debug to True enables false positives for testing
  97.    """
  98.  
  99.     def __init__(
  100.             self, storeNum=131, sender=None,
  101.             recipient=None, debug=True, service=None
  102.         ):
  103.         self.storeNum = storeNum
  104.         self.items, self.newInStock, self.totalInStock = set(), 0, 0
  105.         self.debug = debug
  106.         if not sender:
  107.             self.sender = input('Enter sender email address: ').lstrip().rstrip()
  108.         else:
  109.             self.sender = sender
  110.         if recipient is None:
  111.             prompted = input('Enter recipient email address (leave blank for loopback): ').lstrip().rstrip()
  112.             if not prompted:
  113.                 self.recipient = self.sender
  114.             else:
  115.                 self.recipient = prompted
  116.         else:
  117.             self.recipient = self.sender
  118.            
  119.         #Google API BULLSHIT
  120.         SCOPES = ['https://www.googleapis.com/auth/gmail.compose','https://www.googleapis.com/auth/gmail.readonly']
  121.         creds = None
  122.         # The file token.pickle stores the user's access and refresh tokens, and is
  123.         # created automatically when the authorization flow completes for the first
  124.         # time.
  125.         if os.path.exists('token.pickle'):
  126.             with open('token.pickle', 'rb') as token:
  127.                 creds = pickle.load(token)
  128.         # If there are no (valid) credentials available, let the user log in.
  129.         if not creds or not creds.valid:
  130.             if creds and creds.expired and creds.refresh_token:
  131.                 creds.refresh(Request())
  132.             else:
  133.                 flow = InstalledAppFlow.from_client_secrets_file(
  134.                     'credentials.json', SCOPES)
  135.                 creds = flow.run_local_server(port=0)
  136.             # Save the credentials for the next run
  137.             with open('token.pickle', 'wb') as token:
  138.                 pickle.dump(creds, token)
  139.  
  140.         self.service = build('gmail', 'v1', credentials=creds)
  141.  
  142.         # Call the Gmail API
  143.         results = self.service.users().labels().list(userId='me').execute()
  144.         labels = results.get('labels', [])
  145.  
  146.         if not labels:
  147.             print('No labels found.')
  148.         else:
  149.             print('Labels:')
  150.             for label in labels:
  151.                 print((label['name']))
  152.        
  153.         self.loop = asyncio.get_event_loop()
  154.  
  155.     def __str__(self):
  156.         return '\n'.join(item.__str__() for item in self.items)
  157.  
  158.     def __enter__(self):
  159.         return self
  160.  
  161.     def __exit__(self, exc_type, exc_val, exc_tb):
  162.         self.loop.close()
  163.  
  164.     @property
  165.     def storeNum(self):
  166.         return self._storeNum
  167.  
  168.     @storeNum.setter
  169.     def storeNum(self, val):
  170.         """
  171.        Check to see if value is formatted properly
  172.        storeNum must be sent as a string, but should contain an integer.
  173.        """
  174.         assert isinstance(val, (int, str)), 'Store number must be an integer or string of integer'
  175.         try:
  176.             num = int(val)
  177.         except:
  178.             raise
  179.         else:
  180.             self._storeNum = str(num)
  181.  
  182.     @property
  183.     def sender(self):
  184.         return self._sender
  185.  
  186.     @sender.setter
  187.     def sender(self, val):
  188.         assert val is not None, 'Sender address cannot be empty'
  189.         assert isinstance(val, str), 'Must be str'
  190.         self._sender = val
  191.  
  192.     def run(self, minutes=5):
  193.         run = asyncio.ensure_future(self.check(minutes))
  194.         self.loop.run_forever()
  195.  
  196.     async def check(self, minutes=5):
  197.         assert isinstance(minutes, (int, float)), 'Minutes must be an integer or float'
  198.         seconds = minutes * 60
  199.         while True:
  200.             print('Checking stock...')
  201.             await self.update()
  202.             if self.newInStock:
  203.                 print('New items available')
  204.                 msg = email_message()
  205.                 print("message created")
  206.                 self.send_email(msg)
  207.                 print("email send attempted")
  208.                 #if sent:
  209.                     #print('Recipient notified of stock changes')
  210.             else:
  211.                 print('Stock unchanged')
  212.             await asyncio.sleep(seconds)
  213.  
  214.     def add_interactive(self):
  215.         entry = True
  216.         while entry:
  217.             entry = eval(input('Add one or more URLs separated by spaces, or leave blank to complete: '))
  218.             try:
  219.                 urls = entry.split()
  220.             except:
  221.                 if entry and 'http' in entry:
  222.                     self.add(entry.lstrip().rstrip())
  223.             else:
  224.                 self.add(*urls)
  225.  
  226.  
  227.     def add(self, *urls):
  228.         for url in urls:
  229.             assert isinstance(url, str), 'URL must be a string'
  230.             if url not in (item.url for item in self.items):
  231.                 new = Item(self.storeNum, url)
  232.                 self.loop.run_until_complete(new.update())
  233.                 self.items.add(new)
  234.  
  235.     def remove(self, *urls):
  236.         for url in urls:
  237.             assert isinstance(url, str), 'URL must be a string'
  238.         self.items = set([item for item in self.items if item.url not in urls])
  239.  
  240.     def email_message(self):
  241.         if self.debug:
  242.             new = self.items
  243.         else:
  244.             new = tuple([item for item in self.items if item.stockChanged])
  245.         message_text = '\n'.join(item.__str__() for item in new)
  246.         print(message_text)
  247.         #Create message container
  248.         message = MIMEMultipart('alternative') # needed for both plain & HTML (the MIME type is multipart/alternative)
  249.         message['Subject'] = self.email_subject()
  250.         print("set Subject")
  251.         message['From'] = self.sender
  252.         print("set sender")
  253.         message['To'] = self.recipient
  254.         print("set recipient")
  255.  
  256.         #Create the body of the message (a plain-text and an HTML version)
  257.         message.attach(MIMEText(message_text, 'plain'))
  258.         print("attached plaintext")
  259.         message.attach(MIMEText(message_text, 'html'))
  260.         print("attached html")
  261.  
  262.         raw_message_no_attachment = base64.urlsafe_b64encode(message.as_bytes())
  263.         print("encoded b64")
  264.         raw_message_no_attachment = raw_message_no_attachment.decode()
  265.         print("decoded raw")
  266.         body  = {'raw': raw_message_no_attachment}
  267.         print("set body")
  268.         return body
  269.  
  270.     def email_subject(self):
  271.         return f'({self.newInStock} new, {self.totalInStock} total) items in stock at Microcenter {self.storeNum}'
  272.  
  273.     def send_email(self, msgOBJ):
  274.         message = msgOBJ
  275.         print("message encoded")
  276.  
  277.         try:
  278.             message_sent = (self.service.users().messages().send(userId='me', body=message).execute())
  279.             message_id = message_sent['id']
  280.             # print(attached_file)
  281.             print (f'Message sent (without attachment) \n\n Message Id: {message_id}\n\n Message:\n\n {message_text_plain}')
  282.             # return body
  283.             return True
  284.         except errors.HttpError as error:
  285.             print (f'An error occurred: {error}')
  286.             return False
  287.  
  288.     async def update(self):
  289.         for item in self.items:
  290.             await item.update()
  291.         if self.debug:
  292.             self.newInStock = self.totalInStock = len(self.items)
  293.         else:
  294.             self.newInStock = sum(item.stockChanged for item in self.items)
  295.             self.totalInStock = sum(item.stock for item in self.items)
  296.  
  297.  
  298. class Clerk(Store):
  299.     """
  300.    Further abstraction and automation of Store
  301.  
  302.    Instantiate Clerk with a list of urls as arguments
  303.    and an optional store number as a keyword argument.
  304.  
  305.    Clerk exists to be able to start and run a Store in one line.
  306.  
  307.    The user will be prompted for email account information.
  308.    """
  309.  
  310.     def __init__(self, *urls, storeNum=131):
  311.         super().__init__(storeNum=storeNum)
  312.         if urls:
  313.             super().add(*urls)
  314.         else:
  315.             super().add_interactive()
  316.         super().run()
  317.  
  318. Clerk("https://www.microcenter.com/product/616858/amd-ryzen-9-3950x-35ghz-16-core-am4-boxed-processor", storeNum=155)
Add Comment
Please, Sign In to add comment