Skip to content

Commit 68eac40

Browse files
committed
feat: /new_stock command to search for new arrivals + paginated responses
1 parent c4eaf31 commit 68eac40

5 files changed

Lines changed: 237 additions & 40 deletions

File tree

src/extensions/commands.py

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
from interactions import Extension, slash_command, SlashContext, OptionType, slash_option
2-
import requests
1+
from interactions import Extension, slash_command, SlashContext, OptionType, slash_option, Client
2+
from interactions.ext.paginators import Paginator
3+
import logging
4+
import datetime as dt
35
import utils.html_parser as html_parser
6+
import utils.embeds as embeds
7+
import utils.pullnsave as pullnsave
48

59

610
class Commands(Extension):
11+
# Extension init
12+
def __init__(self, client: Client):
13+
self.client = client
714

815

916
# User-invoked command to manually search for make
@@ -20,46 +27,94 @@ class Commands(Extension):
2027
required=False,
2128
opt_type=OptionType.STRING
2229
)
23-
async def search(self, ctx: SlashContext, make: str, model: str=""):
30+
@slash_option(
31+
name="year",
32+
description="Model year of vehicle to search",
33+
required=False,
34+
opt_type=OptionType.INTEGER
35+
)
36+
async def search(self, ctx: SlashContext, make: str, model: str=None, year: int=None):
37+
logging.info(f'COMMANDS-SEARCH: Command Invoked! args: {make} {model} {year}')
38+
2439
# Defer response to give time for request
2540
await ctx.defer()
2641

27-
if model == "":
28-
model = "0"
42+
# Query pullnsave site
43+
data = pullnsave.search_vehicles(make, model, year)
44+
if not data:
45+
logging.error('COMMANDS-SEARCH: pullnsave returned no data, exiting...')
46+
await ctx.send('pullnsave returned no data')
47+
return
48+
49+
# Parse site data
50+
all_listings = html_parser.parse_listings(data)
51+
if not all_listings:
52+
logging.info('COMMANDS-SEARCH: no results, exiting...')
53+
await ctx.send('No results found')
54+
return
55+
56+
# Filter listings
57+
filtered_listings = html_parser.filter_listings(all_listings)
2958

30-
# Make request
31-
r = requests.post(
32-
url='https://pullnsave.com/wp-admin/admin-ajax.php',
33-
headers={
34-
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
35-
'host': 'pullnsave.com',
36-
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0'
37-
},
38-
data=f"makes={make}&models={model}&years=0&store=0&beginDate=&endDate=&action=getVehicles"
39-
)
59+
# Build embeds
60+
listing_embeds = []
61+
for listing in filtered_listings:
62+
listing_embeds.append(embeds.vehicle_listing(listing))
63+
64+
# Send paginated resposne
65+
paginator = Paginator.create_from_embeds(self.client, *listing_embeds)
66+
await paginator.send(ctx)
67+
logging.info('COMMANDS-SEARCH: paginated results sent!')
68+
69+
70+
# User-invoked command to get new stock
71+
@slash_command(name="new_stock", description="Get all latest arrivals")
72+
async def new_stock(self, ctx: SlashContext):
73+
logging.info('COMMANDS-NEW_STOCK: Command Invoked!')
74+
75+
# Defer response to give time for requests
76+
await ctx.defer()
4077

41-
if r.status_code == 200:
42-
all_listings = html_parser.parse_listings(r.text)
43-
if all_listings:
44-
filtered_listings = html_parser.filter_listings(all_listings)
45-
message = ""
46-
for listing in filtered_listings:
47-
message += f"""
48-
**Model Year:** {listing['model_year']}
49-
**Model:** {listing['model']}
50-
**Color:** {listing['color']}
51-
**Location:** {listing['location']}
52-
**Row:** {listing['row']}
53-
**Date Recieved:** {listing['date_recieved'].strftime("%A %B %m %Y")}
54-
**Image:** {listing['image']}
78+
# Query pullnsave for all makes
79+
make_data = pullnsave.get_makes()
80+
if not make_data:
81+
logging.error('COMMANDS-NEW_STOCK: pullnsave returned no data, exiting...')
82+
await ctx.send("Couldn't grab make data")
83+
return
84+
85+
# Parse make data
86+
all_makes = html_parser.parse_makes(make_data)
87+
if not all_makes:
88+
logging.error('COMMANDS-NEW_STOCK: make parse error, exiting...')
89+
await ctx.send("Couldn't parse make data")
90+
return
91+
92+
# Grab all recent listings
93+
all_listings = []
94+
for make in all_makes:
95+
data = pullnsave.search_vehicles(make, begin_date=(dt.datetime.now() - dt.timedelta(days=3)).strftime('%Y-%m-%d'))
96+
if not data:
97+
logging.error('COMMANDS-NEW_STOCK: pullnsave returned no data, skipping...')
98+
continue
99+
listings = html_parser.parse_listings(data)
100+
if not listings:
101+
logging.error('COMMANDS-NEW_STOCK: pullnsave returned no data, skipping...')
102+
continue
103+
for listing in listings:
104+
all_listings.append(listing)
105+
106+
# Filter and sort listings
107+
filtered_listings = html_parser.filter_listings(all_listings)
55108

56-
"""
57-
await ctx.send(message)
58-
else:
59-
await ctx.send(f"No listings found for Make: {make} Model: {model}")
60-
else:
61-
await ctx.send(f"Unhandled request exception: {r.status_code}")
62-
109+
# Build embeds
110+
listing_embeds = []
111+
for listing in filtered_listings:
112+
listing_embeds.append(embeds.vehicle_listing(listing))
113+
114+
# Send paginated resposne
115+
paginator = Paginator.create_from_embeds(self.client, *listing_embeds)
116+
await paginator.send(ctx)
117+
logging.info('COMMANDS-SEARCH: paginated results sent!')
63118

64119

65120
def setup(bot):

src/main.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import interactions
2-
from interactions import slash_command, SlashContext
32
import os
3+
import logging
44

55

6+
# Init Logging
7+
logging.basicConfig(
8+
level=logging.INFO,
9+
format="%(asctime)s [%(levelname)s] %(message)s",
10+
handlers=[
11+
logging.StreamHandler()
12+
]
13+
)
14+
logging.info('MAIN: Logging started!')
15+
16+
# Init Client
617
client = interactions.Client()
718

819

20+
# On Load
921
@interactions.listen()
1022
async def on_startup():
1123
print('Logged in!')
1224

1325

26+
# Load Extensions
1427
client.load_extension('extensions.commands')
1528

29+
# Start bot
1630
client.start(os.environ['DISCORD_BOT_TOKEN'])

src/utils/embeds.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from interactions import Embed, EmbedField, EmbedFooter
2+
3+
4+
def vehicle_listing(listing_data):
5+
embed = Embed(
6+
title=f"{listing_data['model_year']} {listing_data['model']}",
7+
fields=[
8+
EmbedField(
9+
name="Color",
10+
value=listing_data['color'],
11+
inline=True
12+
),
13+
EmbedField(
14+
name="VIN",
15+
value=listing_data['VIN'],
16+
inline=True
17+
),
18+
EmbedField(
19+
name="Stock #",
20+
value=listing_data['stock_number'],
21+
inline=True
22+
),
23+
EmbedField(
24+
name="Location",
25+
value=listing_data['location'],
26+
inline=True
27+
),
28+
EmbedField(
29+
name="Row",
30+
value=listing_data['row'],
31+
inline=True
32+
),
33+
EmbedField(
34+
name="Date Revieved",
35+
value=listing_data['date_recieved'].strftime("%A %B %d, %Y"),
36+
inline=True
37+
)
38+
],
39+
footer=EmbedFooter(
40+
text="Benz-Finder | Powered by Pull-N-Save",
41+
icon_url="https://cdn.discordapp.com/embed/avatars/0.png"
42+
)
43+
)
44+
embed.set_image(listing_data['image'])
45+
46+
return embed

src/utils/html_parser.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from bs4 import BeautifulSoup
22
import datetime as dt
3+
import logging
34

45

56
def filter_listings(listings):
@@ -9,7 +10,6 @@ def filter_listings(listings):
910
return sorted_listings
1011

1112

12-
1313
def parse_listings(html):
1414
# Initiate bs4 parser
1515
soup = BeautifulSoup(html, 'html.parser')
@@ -27,10 +27,31 @@ def parse_listings(html):
2727
"date_recieved": dt.datetime.strptime(tr.find_all('td')[3].get_text().strip(), "%m-%d-%y"),
2828
"row": tr.find_all('td')[4].get_text().strip(),
2929
"location": tr.find_all('td')[5].get_text().strip(),
30-
"color": tr.find_all('td')[6].get_text().strip()
30+
"color": tr.find_all('td')[6].get_text().strip(),
31+
"stock_number": tr.find_all('td')[7].get_text().strip(),
32+
"VIN": tr.find_all('td')[8].get_text().strip()
3133
})
3234

3335
return listings
3436
except Exception as e:
35-
print('PARSING ERROR: {e}')
37+
logging.error(f'HTML_PARSER-PARSE_LISTINGS: Parsing Error: {e}')
38+
return False
39+
40+
41+
def parse_makes(html):
42+
# Initiate bs4 parser
43+
soup = BeautifulSoup(html, 'html.parser')
44+
45+
try:
46+
# Find all span elements with the class 'notranslate'
47+
spans = soup.find_all('span', {'class': 'notranslate'})
48+
49+
# Extract span text
50+
makes = []
51+
for span in spans:
52+
makes.append(span.text)
53+
54+
return makes
55+
except Exception as e:
56+
logging.error(f'HTML_PARSER-PARSE_MAKES: Parsing Error: {e}')
3657
return False

src/utils/pullnsave.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import requests
2+
import logging
3+
4+
5+
def search_vehicles(make, model=None, year=None, store=None, begin_date=None, end_date=None):
6+
# Build request body
7+
data = f'makes={make}'
8+
if model:
9+
data += f'&models={model}'
10+
if year:
11+
data += f'&years={year}'
12+
if store:
13+
data += f'&store={store}'
14+
if begin_date:
15+
data += f'&beginDate={begin_date}'
16+
if end_date:
17+
data += f'&endDate={end_date}'
18+
data += '&action=getVehicles'
19+
20+
# Send request
21+
logging.info(f'PULLNSAVE: Sending Request: {data}')
22+
r = requests.post(
23+
url='https://pullnsave.com/wp-admin/admin-ajax.php',
24+
headers={
25+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
26+
'host': 'pullnsave.com',
27+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0'
28+
},
29+
data=data
30+
)
31+
32+
# Return response
33+
if r.status_code == 200:
34+
return r.text
35+
else:
36+
logging.error(f'PULLNSAVE: Unhandled Response Status: {r.status_code}')
37+
return False
38+
39+
40+
def get_makes():
41+
# Build request body
42+
data = 'action=getMakes'
43+
44+
# Send request
45+
logging.info(f'PULLNSAVE: Sending Request: {data}')
46+
r = requests.post(
47+
url='https://pullnsave.com/wp-admin/admin-ajax.php',
48+
headers={
49+
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
50+
'host': 'pullnsave.com',
51+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/112.0'
52+
},
53+
data=data
54+
)
55+
56+
# Return response
57+
if r.status_code == 200:
58+
return r.text
59+
else:
60+
logging.error(f'PULLNSAVE: Unhandled Response Status: {r.status_code}')
61+
return False

0 commit comments

Comments
 (0)