Skip to content

Commit 8d506a1

Browse files
committed
Merge branch 'junk'
2 parents 24bcbb0 + 5aed1b4 commit 8d506a1

File tree

6 files changed

+277
-0
lines changed

6 files changed

+277
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea
2+
venv
3+
build
4+
dist
5+
*.pyc
6+
*.spec
7+
config.ini

KrogerAPI.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import asyncio
2+
import json
3+
import KrogerCLI
4+
from pyppeteer import launch
5+
6+
7+
class KrogerAPI:
8+
browser_options = {
9+
'headless': True,
10+
'args': ['--blink-settings=imagesEnabled=false'] # Disable images for hopefully faster load-time
11+
}
12+
headers = {
13+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
14+
'Chrome/81.0.4044.129 Safari/537.36',
15+
'Accept-Language': 'en-US,en;q=0.9'
16+
}
17+
18+
def __init__(self, cli: KrogerCLI):
19+
self.cli = cli
20+
21+
def get_account_info(self):
22+
return asyncio.run(self._get_account_info())
23+
24+
def clip_coupons(self):
25+
return asyncio.run(self._clip_coupons())
26+
27+
async def _get_account_info(self):
28+
signed_in = await self.sign_in_routine()
29+
if not signed_in:
30+
await self.destroy()
31+
return None
32+
33+
self.cli.console.print('Loading profile info..')
34+
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/accountmanagement/api/profile')
35+
try:
36+
plain_text = await self.page.plainText()
37+
profile = json.loads(plain_text)
38+
user_id = profile['userId']
39+
except Exception:
40+
profile = None
41+
await self.destroy()
42+
43+
return profile
44+
45+
async def _clip_coupons(self):
46+
signed_in = await self.sign_in_routine(redirect_url='/cl/coupons/', contains=['Coupons Clipped'])
47+
if not signed_in:
48+
await self.destroy()
49+
return None
50+
51+
js = """
52+
window.scrollTo(0, document.body.scrollHeight);
53+
for (let i = 0; i < 150; i++) {
54+
let el = document.getElementsByClassName('kds-Button--favorable')[i];
55+
if (el !== undefined) {
56+
el.scrollIntoView();
57+
el.click();
58+
}
59+
}
60+
"""
61+
62+
self.cli.console.print('[italic]Applying the coupons, please wait..[/italic]')
63+
await self.page.keyboard.press('Escape')
64+
for i in range(6):
65+
await self.page.evaluate(js)
66+
await self.page.keyboard.press('End')
67+
await self.page.waitFor(1000)
68+
await self.page.waitFor(3000)
69+
await self.destroy()
70+
self.cli.console.print('[bold]Coupons successfully clipped to your account! :thumbs_up:[/bold]')
71+
72+
async def init(self):
73+
self.browser = await launch(self.browser_options)
74+
self.page = await self.browser.newPage()
75+
await self.page.setExtraHTTPHeaders(self.headers)
76+
await self.page.setViewport({'width': 700, 'height': 0})
77+
78+
async def destroy(self):
79+
await self.browser.close()
80+
81+
async def sign_in_routine(self, redirect_url='/account/update', contains=None):
82+
await self.init()
83+
self.cli.console.print('[italic]Signing in.. (please wait, it might take awhile)[/italic]')
84+
signed_in = await self.sign_in(redirect_url, contains)
85+
86+
if not signed_in and self.browser_options['headless']:
87+
self.cli.console.print('[red]Sign in failed. Trying one more time..[/red]')
88+
self.browser_options['headless'] = False
89+
await self.destroy()
90+
await self.init()
91+
signed_in = await self.sign_in(redirect_url, contains)
92+
93+
if not signed_in:
94+
self.cli.console.print('[bold red]Sign in failed. Please make sure the username/password is correct.'
95+
'[/bold red]')
96+
97+
return signed_in
98+
99+
async def sign_in(self, redirect_url, contains):
100+
timeout = 20000
101+
if not self.browser_options['headless']:
102+
timeout = 60000
103+
await self.page.goto('https://www.' + self.cli.config['main']['domain'] + '/signin?redirectUrl=' + redirect_url)
104+
await self.page.type('#SignIn-emailInput', self.cli.username)
105+
await self.page.type('#SignIn-passwordInput', self.cli.password)
106+
await self.page.keyboard.press('Enter')
107+
try:
108+
await self.page.waitForNavigation(timeout=timeout)
109+
except Exception:
110+
return False
111+
112+
if contains is not None:
113+
html = await self.page.content()
114+
for item in contains:
115+
if item not in html:
116+
return False
117+
118+
return True

KrogerCLI.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import configparser
2+
import os
3+
import click
4+
import time
5+
from rich.console import Console
6+
from rich.panel import Panel
7+
from rich import box
8+
from KrogerAPI import *
9+
10+
11+
class KrogerCLI:
12+
config_file = 'config.ini'
13+
14+
def __init__(self):
15+
self.config = configparser.ConfigParser()
16+
self.username = None
17+
self.password = None
18+
self.console = Console()
19+
self.api = KrogerAPI(self)
20+
if not os.path.exists(self.config_file):
21+
self._init_config_file()
22+
self.config.read(self.config_file)
23+
self.init()
24+
25+
def init(self):
26+
if self.config['profile']['first_name'] != '':
27+
self.console.print(Panel('[bold]Welcome Back, ' + self.config['profile']['first_name'] + '! :smiley:\n'
28+
'[dark_blue]Kroger[/dark_blue] CLI[/bold]', box=box.ASCII))
29+
else:
30+
self.console.print(Panel('[bold]Welcome to [dark_blue]Kroger[/dark_blue] CLI[/bold] (unofficial command '
31+
'line interface)', box=box.ASCII))
32+
33+
self.prompt_store_selection()
34+
35+
if self.username is None and self.config['main']['username'] != '':
36+
self.username = self.config['main']['username']
37+
self.password = self.config['main']['password']
38+
else:
39+
self.prompt_credentials()
40+
41+
self.prompt_options()
42+
43+
def prompt_store_selection(self):
44+
pass
45+
# TODO:
46+
# self.console.print('Please select preferred store')
47+
48+
def prompt_credentials(self):
49+
self.console.print('In order to continue, please enter your username (email) and password for kroger.com '
50+
'(also works with Ralphs, Dillons, Smith’s and other Kroger’s Chains)')
51+
username = click.prompt('Username (email)')
52+
password = click.prompt('Password')
53+
self._set_credentials(username, password)
54+
55+
def prompt_options(self):
56+
while True:
57+
self.console.print('[bold]1[/bold] - Display account info')
58+
self.console.print('[bold]2[/bold] - Clip all digital coupons')
59+
self.console.print('[bold]8[/bold] - Re-Enter username/password')
60+
self.console.print('[bold]9[/bold] - Exit')
61+
option = click.prompt('Please select from one of the options', type=int)
62+
63+
if option == 1:
64+
self._option_account_info()
65+
elif option == 2:
66+
self._option_clip_coupons()
67+
elif option == 8:
68+
self.prompt_credentials()
69+
elif option == 9:
70+
return
71+
72+
self.console.rule()
73+
time.sleep(2)
74+
75+
def _write_config_file(self):
76+
with open(self.config_file, 'w') as f:
77+
self.config.write(f)
78+
79+
def _init_config_file(self):
80+
self.config.add_section('main')
81+
self.config['main']['username'] = ''
82+
self.config['main']['password'] = ''
83+
self.config['main']['domain'] = 'kroger.com'
84+
self.config.add_section('profile')
85+
self.config['profile']['first_name'] = ''
86+
self._write_config_file()
87+
88+
def _set_credentials(self, username, password):
89+
self.username = username
90+
self.password = password
91+
self.config['main']['username'] = self.username
92+
self.config['main']['password'] = self.password
93+
self._write_config_file()
94+
95+
def _option_account_info(self):
96+
info = self.api.get_account_info()
97+
if info is None:
98+
self.console.print('[bold red]Couldn\'t retrieve the account info.[/bold red]')
99+
else:
100+
self.config['profile']['first_name'] = info['firstName']
101+
self.config['profile']['last_name'] = info['lastName']
102+
self.config['profile']['email_address'] = info['emailAddress']
103+
self.config['profile']['loyalty_card_number'] = info['loyaltyCardNumber']
104+
self.config['profile']['mobile_phone'] = info['mobilePhoneNumber']
105+
self.config['profile']['address_line1'] = info['address']['addressLine1']
106+
self.config['profile']['address_line2'] = info['address']['addressLine2']
107+
self.config['profile']['city'] = info['address']['city']
108+
self.config['profile']['state'] = info['address']['stateCode']
109+
self.config['profile']['zip'] = info['address']['zip']
110+
self._write_config_file()
111+
self.console.print(self.config.items(section='profile'))
112+
113+
def _option_clip_coupons(self):
114+
self.api.clip_coupons()

create_executable.bat

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
call venv\scripts\activate
2+
3+
pyinstaller -n kroger-cli ^
4+
--onefile ^
5+
--exclude-module tkinter ^
6+
--hidden-import=six ^
7+
--hidden-import=packaging ^
8+
--hidden-import=packaging.version ^
9+
--hidden-import=packaging.requirements ^
10+
--hidden-import=packaging.specifiers ^
11+
--hidden-import=pkg_resources ^
12+
--hidden-import pkg_resources.py2_warn ^
13+
main.py

main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from KrogerCLI import *
2+
3+
if __name__ == '__main__':
4+
cli = KrogerCLI()

requirements.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
altgraph==0.17
2+
appdirs==1.4.4
3+
click==7.1.2
4+
colorama==0.4.3
5+
commonmark==0.9.1
6+
future==0.18.2
7+
packaging==20.3
8+
pefile==2019.4.18
9+
pprintpp==0.4.0
10+
pyee==7.0.2
11+
Pygments==2.6.1
12+
pyinstaller @ https://github.com/pyinstaller/pyinstaller/archive/develop.zip
13+
pyparsing==2.4.7
14+
pyppeteer==0.2.2
15+
pywin32-ctypes==0.2.0
16+
rich==1.1.3
17+
six==1.14.0
18+
tqdm==4.46.0
19+
typing-extensions==3.7.4.2
20+
urllib3==1.25.9
21+
websockets==8.1

0 commit comments

Comments
 (0)