Skip to content

Commit

Permalink
Merge branch 'junk'
Browse files Browse the repository at this point in the history
  • Loading branch information
Shmakov committed May 15, 2020
2 parents 24bcbb0 + 5aed1b4 commit 8d506a1
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.idea
venv
build
dist
*.pyc
*.spec
config.ini
118 changes: 118 additions & 0 deletions KrogerAPI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import asyncio
import json
import KrogerCLI
from pyppeteer import launch


class KrogerAPI:
browser_options = {
'headless': True,
'args': ['--blink-settings=imagesEnabled=false'] # Disable images for hopefully faster load-time
}
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/81.0.4044.129 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9'
}

def __init__(self, cli: KrogerCLI):
self.cli = cli

def get_account_info(self):
return asyncio.run(self._get_account_info())

def clip_coupons(self):
return asyncio.run(self._clip_coupons())

async def _get_account_info(self):
signed_in = await self.sign_in_routine()
if not signed_in:
await self.destroy()
return None

self.cli.console.print('Loading profile info..')
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/accountmanagement/api/profile')
try:
plain_text = await self.page.plainText()
profile = json.loads(plain_text)
user_id = profile['userId']
except Exception:
profile = None
await self.destroy()

return profile

async def _clip_coupons(self):
signed_in = await self.sign_in_routine(redirect_url='/cl/coupons/', contains=['Coupons Clipped'])
if not signed_in:
await self.destroy()
return None

js = """
window.scrollTo(0, document.body.scrollHeight);
for (let i = 0; i < 150; i++) {
let el = document.getElementsByClassName('kds-Button--favorable')[i];
if (el !== undefined) {
el.scrollIntoView();
el.click();
}
}
"""

self.cli.console.print('[italic]Applying the coupons, please wait..[/italic]')
await self.page.keyboard.press('Escape')
for i in range(6):
await self.page.evaluate(js)
await self.page.keyboard.press('End')
await self.page.waitFor(1000)
await self.page.waitFor(3000)
await self.destroy()
self.cli.console.print('[bold]Coupons successfully clipped to your account! :thumbs_up:[/bold]')

async def init(self):
self.browser = await launch(self.browser_options)
self.page = await self.browser.newPage()
await self.page.setExtraHTTPHeaders(self.headers)
await self.page.setViewport({'width': 700, 'height': 0})

async def destroy(self):
await self.browser.close()

async def sign_in_routine(self, redirect_url='/account/update', contains=None):
await self.init()
self.cli.console.print('[italic]Signing in.. (please wait, it might take awhile)[/italic]')
signed_in = await self.sign_in(redirect_url, contains)

if not signed_in and self.browser_options['headless']:
self.cli.console.print('[red]Sign in failed. Trying one more time..[/red]')
self.browser_options['headless'] = False
await self.destroy()
await self.init()
signed_in = await self.sign_in(redirect_url, contains)

if not signed_in:
self.cli.console.print('[bold red]Sign in failed. Please make sure the username/password is correct.'
'[/bold red]')

return signed_in

async def sign_in(self, redirect_url, contains):
timeout = 20000
if not self.browser_options['headless']:
timeout = 60000
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/signin?redirectUrl=' + redirect_url)
await self.page.type('#SignIn-emailInput', self.cli.username)
await self.page.type('#SignIn-passwordInput', self.cli.password)
await self.page.keyboard.press('Enter')
try:
await self.page.waitForNavigation(timeout=timeout)
except Exception:
return False

if contains is not None:
html = await self.page.content()
for item in contains:
if item not in html:
return False

return True
114 changes: 114 additions & 0 deletions KrogerCLI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import configparser
import os
import click
import time
from rich.console import Console
from rich.panel import Panel
from rich import box
from KrogerAPI import *


class KrogerCLI:
config_file = 'config.ini'

def __init__(self):
self.config = configparser.ConfigParser()
self.username = None
self.password = None
self.console = Console()
self.api = KrogerAPI(self)
if not os.path.exists(self.config_file):
self._init_config_file()
self.config.read(self.config_file)
self.init()

def init(self):
if self.config['profile']['first_name'] != '':
self.console.print(Panel('[bold]Welcome Back, ' + self.config['profile']['first_name'] + '! :smiley:\n'
'[dark_blue]Kroger[/dark_blue] CLI[/bold]', box=box.ASCII))
else:
self.console.print(Panel('[bold]Welcome to [dark_blue]Kroger[/dark_blue] CLI[/bold] (unofficial command '
'line interface)', box=box.ASCII))

self.prompt_store_selection()

if self.username is None and self.config['main']['username'] != '':
self.username = self.config['main']['username']
self.password = self.config['main']['password']
else:
self.prompt_credentials()

self.prompt_options()

def prompt_store_selection(self):
pass
# TODO:
# self.console.print('Please select preferred store')

def prompt_credentials(self):
self.console.print('In order to continue, please enter your username (email) and password for kroger.com '
'(also works with Ralphs, Dillons, Smith’s and other Kroger’s Chains)')
username = click.prompt('Username (email)')
password = click.prompt('Password')
self._set_credentials(username, password)

def prompt_options(self):
while True:
self.console.print('[bold]1[/bold] - Display account info')
self.console.print('[bold]2[/bold] - Clip all digital coupons')
self.console.print('[bold]8[/bold] - Re-Enter username/password')
self.console.print('[bold]9[/bold] - Exit')
option = click.prompt('Please select from one of the options', type=int)

if option == 1:
self._option_account_info()
elif option == 2:
self._option_clip_coupons()
elif option == 8:
self.prompt_credentials()
elif option == 9:
return

self.console.rule()
time.sleep(2)

def _write_config_file(self):
with open(self.config_file, 'w') as f:
self.config.write(f)

def _init_config_file(self):
self.config.add_section('main')
self.config['main']['username'] = ''
self.config['main']['password'] = ''
self.config['main']['domain'] = 'kroger.com'
self.config.add_section('profile')
self.config['profile']['first_name'] = ''
self._write_config_file()

def _set_credentials(self, username, password):
self.username = username
self.password = password
self.config['main']['username'] = self.username
self.config['main']['password'] = self.password
self._write_config_file()

def _option_account_info(self):
info = self.api.get_account_info()
if info is None:
self.console.print('[bold red]Couldn\'t retrieve the account info.[/bold red]')
else:
self.config['profile']['first_name'] = info['firstName']
self.config['profile']['last_name'] = info['lastName']
self.config['profile']['email_address'] = info['emailAddress']
self.config['profile']['loyalty_card_number'] = info['loyaltyCardNumber']
self.config['profile']['mobile_phone'] = info['mobilePhoneNumber']
self.config['profile']['address_line1'] = info['address']['addressLine1']
self.config['profile']['address_line2'] = info['address']['addressLine2']
self.config['profile']['city'] = info['address']['city']
self.config['profile']['state'] = info['address']['stateCode']
self.config['profile']['zip'] = info['address']['zip']
self._write_config_file()
self.console.print(self.config.items(section='profile'))

def _option_clip_coupons(self):
self.api.clip_coupons()
13 changes: 13 additions & 0 deletions create_executable.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
call venv\scripts\activate

pyinstaller -n kroger-cli ^
--onefile ^
--exclude-module tkinter ^
--hidden-import=six ^
--hidden-import=packaging ^
--hidden-import=packaging.version ^
--hidden-import=packaging.requirements ^
--hidden-import=packaging.specifiers ^
--hidden-import=pkg_resources ^
--hidden-import pkg_resources.py2_warn ^
main.py
4 changes: 4 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from KrogerCLI import *

if __name__ == '__main__':
cli = KrogerCLI()
21 changes: 21 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
altgraph==0.17
appdirs==1.4.4
click==7.1.2
colorama==0.4.3
commonmark==0.9.1
future==0.18.2
packaging==20.3
pefile==2019.4.18
pprintpp==0.4.0
pyee==7.0.2
Pygments==2.6.1
pyinstaller @ https://github.com/pyinstaller/pyinstaller/archive/develop.zip
pyparsing==2.4.7
pyppeteer==0.2.2
pywin32-ctypes==0.2.0
rich==1.1.3
six==1.14.0
tqdm==4.46.0
typing-extensions==3.7.4.2
urllib3==1.25.9
websockets==8.1

0 comments on commit 8d506a1

Please sign in to comment.