Skip to content

Commit a90f200

Browse files
committed
apply smtp server to workflow and fix bugs
1 parent 506f118 commit a90f200

File tree

10 files changed

+186
-80
lines changed

10 files changed

+186
-80
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from datetime import datetime
2+
from ..ticketscraping.models.pick import Pick
3+
4+
def price_formatter(price):
5+
price_str = ''
6+
if type(price) is float or int:
7+
price_str = "$" + "{:.2f}".format(price)
8+
elif type(price) is str:
9+
price_str = price
10+
return price_str
11+
12+
def decimal_to_percent(num: float):
13+
return "{:.2f}".format(num*100) + "%"
14+
15+
def format_date(date: datetime):
16+
return date.isoformat()
17+
18+
def default_formatter(s: str):
19+
return s
20+
21+
def format_seat_columns(cols):
22+
if type(cols) is str:
23+
return cols
24+
elif type(cols) is list:
25+
return "(" + ",".join(cols) + ")"
26+
return '-'
27+
28+
def apply_format(s, formatter)->str:
29+
return formatter(s)
30+
31+
def apply(values: list, formatters: list, delimiter="\t"):
32+
if len(values) != len(formatters):
33+
raise Exception('values and formatters must have the same length')
34+
s = []
35+
for i in range(len(values)):
36+
s.append(apply_format(values[i], formatters[i]))
37+
return delimiter.join(s)
38+
39+
def format_full_seat(seat: dict, delimiter="\t"):
40+
price = seat.get("price", "n/a")
41+
section = seat.get("section", "n/a")
42+
row = seat.get("row", "n/a")
43+
seat_columns = seat.get("seat_columns", "n/a")
44+
last_modified = seat.get("last_modified", "n/a")
45+
return apply(
46+
[price, section, row, seat_columns, last_modified],
47+
[price_formatter, default_formatter, default_formatter,
48+
format_seat_columns, format_date],
49+
delimiter)
50+
51+
def format_price_only_seat(seat: dict, delimiter="\t"):
52+
price = seat.get("price", "n/a")
53+
last_modified = seat.get("last_modified", "n/a")
54+
return apply([price, last_modified], [price_formatter, format_date], delimiter)
55+
56+
def format_seat(seat: dict, price_only=False, delimiter="\t"):
57+
if price_only:
58+
return format_price_only_seat(seat, delimiter)
59+
else:
60+
return format_full_seat(seat, delimiter)
61+
62+
def format_seats(seats: list, price_only=False, delimiter="\t"):
63+
return "\n".join([format_seat(seat, price_only, delimiter) for seat in seats])
64+
65+
66+
def format_entire_mail(pick: Pick, target_price: int, percentile: float, rank: int, num_total: int, top_history_seats: list, same_seats: list):
67+
"""
68+
structure of message:
69+
1. greetings
70+
2. attributes of new seats
71+
3. top 3 comparable history seats
72+
4. exact same seats if possible
73+
5. signature
74+
"""
75+
p1 = (
76+
f"Hi!"
77+
)
78+
p2 = (
79+
f"Congratulations! Ticket tracker reminds you that your ticket subscription request with target price {price_formatter(target_price)} "
80+
f"found better budget seats (price, section, row, seats) at ({format_full_seat(vars(pick), delimiter=', ')}). "
81+
f"{decimal_to_percent(percentile)} of all comparable seats in the history are better than the newly found seats, that is, "
82+
f"they rank no.{rank} out of {num_total} comparable seats in the history."
83+
)
84+
p3 = (
85+
f"You can compare to history seats that are better than the newly found seats:"
86+
f"{chr(10)}"
87+
f"{format_seats(top_history_seats, price_only=False)}"
88+
) if len(top_history_seats) > 0 else ""
89+
p4 = (
90+
f"The newly found seats have history prices:"
91+
f"{chr(10)}"
92+
f"{format_seats(same_seats, price_only=True)}"
93+
) if len(same_seats) > 0 else ""
94+
p5 = (
95+
f"Bests,"
96+
f"{chr(10)}"
97+
f"Ticketmaster Ticket Tracker"
98+
)
99+
paras = list(filter(lambda p: len(p) > 0, [p1, p2, p3, p4, p5]))
100+
return "\n\n".join(paras)

apps/pushnotification/smtp.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,6 @@ def server_login(server: SMTP_SSL, password: str):
1414
return server.login(constants.sender_email, password)
1515

1616

17-
def auth_server(server: SMTP_SSL):
18-
server_login(server, constants.app_password)
19-
20-
21-
server = init_server()
22-
# auth_server(server)
23-
24-
2517
def server_send_email(server: SMTP_SSL, receiver_emails: list[str], message: str):
2618
em = EmailMessage()
2719
em['From'] = constants.sender_email
@@ -32,10 +24,21 @@ def server_send_email(server: SMTP_SSL, receiver_emails: list[str], message: str
3224
return server.sendmail(constants.sender_email, receiver_emails, em.as_string())
3325

3426

35-
def send_email(receiver_emails: list[str], message: str):
27+
def send_email(receiver_emails: list[str], messages: list[str]):
28+
if len(messages) == 0:
29+
return
30+
# print(messages[0])
3631
try:
37-
err = server_send_email(server, receiver_emails, message)
32+
err = server_send_email(server, receiver_emails, messages[0])
3833
if err is not None:
3934
raise Exception('could not send email to the receiver')
4035
except Exception as ex:
4136
print(ex)
37+
38+
39+
server = init_server()
40+
41+
42+
def auth_server():
43+
global server
44+
server_login(server, constants.app_password)

apps/startup/apps.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ def run():
2929
from apps.ticketscraping.connection.asyn_tasks_receiver import run
3030
conn_process = Process(target=run)
3131
conn_process.start()
32-
conn_process.join()
3332

3433

3534
class MyAppConfig(AppConfig):

apps/ticketscraping/connection/mail_receiver.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from apps.ticketscraping.connection.receiver_process import ReceiverProcess
2-
# from ..tasks.asynchronous import run_async_tasks
2+
from apps.pushnotification.smtp import send_email, auth_server
33
from apps.ticketscraping.constants import SERVICE_LOCALHOST, MAIL_RECEIVER_PORT
44

55
def run():
66
# start itself
7-
receiver = ReceiverProcess(lambda x: print(
8-
x), SERVICE_LOCALHOST, MAIL_RECEIVER_PORT)
7+
auth_server()
8+
receiver = ReceiverProcess(send_email, SERVICE_LOCALHOST, MAIL_RECEIVER_PORT)
99
receiver.connect()
1010
receiver.serve_forever()
1111

apps/ticketscraping/constants.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def get_top_picks_url(
2525

2626
SUBSCRIBE_REQUEST_PROPS = {
2727
'NAME': 'name',
28+
'CLIENT_NAME': 'client_name',
29+
'CLIENT_EMAILS': 'client_emails',
2830
'TARGET_PRICE': 'target_price',
2931
'TOLERANCE': 'tolerance',
3032
'TICKET_NUM': 'ticket_num',
@@ -36,14 +38,19 @@ def filter_obj_from_attrs(obj, atts: dict[str,str]):
3638
for key in atts.values():
3739
if key in obj:
3840
res[key] = obj[key]
41+
if len(res) != len(atts):
42+
raise Exception('lack of attributes')
3943
return res
4044

4145

4246
# metric thresholds
4347
MINIMUM_HISTORY_DATA = 3
4448
PERCENT_OF_CHANGE = 0.5
4549
PERCENTILE_HISTORY_PRICES = 0.25
50+
51+
# alert content constants
4652
ALERT_SEATS_MAX_COUNT = 3
53+
TOP_COMPARED_HISTORY_SEATS = 3
4754

4855
def get_top_picks_header(): return {
4956
**BASIC_REQ_HEADER,
@@ -71,7 +78,7 @@ def get_top_picks_query_params(qty: int, target_price: int, tolerance: int): ret
7178
TOKEN_RENEW_PRIORITY = 1
7279
TICKET_SCRAPING_PRIORITY = 3
7380
TICKET_SCRAPING_INTERVAL = 60
74-
TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL = 5
81+
TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL = 10
7582

7683
INJECTOR_LOCATION = "js/injector.js"
7784
INJECTOR_HEADER_LOCATION = "js/injector-header.js"

apps/ticketscraping/scraping.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,35 @@
1010
from ..storage.storage import *
1111
from .seat_analysis import format_seats
1212
from .tasks.periodic import run_periodic_task
13+
import traceback
1314

1415
class Reese84TokenUpdating():
1516
def __init__(self):
1617
self.is_running = False
17-
self.reese84_token = ''
18+
self._reese84_token = ''
1819
self.reese84_renewInSec = 0
20+
self.token_access_semaphore = Semaphore(1) # one can access at a time
1921
self.token_semaphore = Semaphore(0)
2022
self.scheduler = sched.scheduler(time.time, time.sleep)
2123

24+
@property
25+
def reese84_token(self):
26+
self.token_semaphore.acquire()
27+
self.token_access_semaphore.acquire()
28+
token = self._reese84_token
29+
self.token_semaphore.release()
30+
self.token_access_semaphore.release()
31+
32+
return token
33+
2234
def initialize_reese84_token(self):
2335
"""
2436
This method should not be called directly.
2537
"""
26-
self.reese84_token, self.reese84_renewInSec = getReese84Token()
27-
self.token_semaphore.release() # produce a new token
38+
self.token_access_semaphore.acquire()
39+
self._reese84_token, self.reese84_renewInSec = getReese84Token()
40+
self.token_semaphore.release() # produce a new token
41+
self.token_access_semaphore.release()
2842
self.scheduler.enter(self.reese84_renewInSec -
2943
constants.TOKEN_RENEW_SEC_OFFSET, constants.TOKEN_RENEW_PRIORITY, self.renew_reese84_token)
3044

@@ -34,8 +48,10 @@ def renew_reese84_token(self):
3448
"""
3549
print("renewing token")
3650
self.token_semaphore.acquire() # invalidate a token
37-
self.reese84_token = getReese84Token()
38-
self.token_semaphore.release()
51+
self.token_access_semaphore.acquire()
52+
self._reese84_token, self.reese84_renewInSec = getReese84Token()
53+
self.token_semaphore.release() # produce a token
54+
self.token_access_semaphore.release()
3955
self.scheduler.enter(self.reese84_renewInSec -
4056
constants.TOKEN_RENEW_SEC_OFFSET, constants.TOKEN_RENEW_PRIORITY, self.renew_reese84_token)
4157

@@ -49,7 +65,7 @@ def start(self):
4965

5066

5167
class TicketScraping(threading.Thread):
52-
def __init__(self, token_generator: Reese84TokenUpdating, event_id: str, subscribe_id: str, num_seats: int, target_price: int, tolerance: int):
68+
def __init__(self, token_generator: Reese84TokenUpdating, event_id: str, subscribe_id: str, num_seats: int, target_price: int, tolerance: int, emails: list[str]):
5369
threading.Thread.__init__(self)
5470
self.is_running = False
5571
self.is_stopping = False
@@ -58,6 +74,7 @@ def __init__(self, token_generator: Reese84TokenUpdating, event_id: str, subscri
5874
self.num_seats = num_seats
5975
self.target_price = target_price
6076
self.tolerance = tolerance
77+
self.emails = emails
6178
self.token_gen = token_generator
6279
self.scheduler = sched.scheduler(time.time, time.sleep)
6380
self.initialDelay = random.randint(
@@ -77,25 +94,26 @@ def flag_for_termination(self):
7794
print(f'Failed to terminate the thread with id={thread_id}')
7895

7996
def ticket_scraping(self):
80-
if self.token_gen.token_semaphore._value <= 0:
81-
# phase: retry after a delay
82-
self.scheduler.enter(constants.TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL,
83-
constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping)
84-
return
8597
# scrape the top-picks from ticketmaster
8698
top_picks_url = constants.get_top_picks_url(self.event_id)
8799
top_picks_q_params = constants.get_top_picks_query_params(
88100
self.num_seats, self.target_price, self.tolerance)
89101
top_picks_header = constants.get_top_picks_header()
90-
res = requests.get(top_picks_url, headers=top_picks_header, params=top_picks_q_params,
91-
cookies=dict(reese84=self.token_gen.reese84_token)) # type: ignore
92-
102+
res = None
103+
try:
104+
res = requests.get(top_picks_url, headers=top_picks_header, params=top_picks_q_params,
105+
cookies=dict(reese84=self.token_gen.reese84_token)) # type: ignore
106+
except Exception:
107+
# retry after a delay
108+
self.scheduler.enter(constants.TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL,
109+
constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping)
110+
return
93111
# prune and format the received picks
94112
picks_obj = format_seats(res.json(), self.subscribe_id)
95113

96114
# periodic task: update collections best_available_seats and best_history_seats
97115
# and automatically spawn async tasks
98-
run_periodic_task(picks_obj, self.subscribe_id, self.target_price)
116+
run_periodic_task(picks_obj, self.subscribe_id, self.target_price, self.emails)
99117

100118
print("Got the ticket info from TM. /", res.status_code)
101119
self.scheduler.enter(constants.TICKET_SCRAPING_INTERVAL,
@@ -116,9 +134,9 @@ def run(self):
116134
return
117135
self.is_running = True
118136
self.is_stopping = False
119-
self.ticket_scraping()
120137
# randomize start time to scatter out event of API fetching
121138
time.sleep(self.initialDelay)
139+
self.ticket_scraping()
122140
self.scheduler.run()
123141
finally:
124142
print(
@@ -137,7 +155,7 @@ def start():
137155
'$or': [{'markPaused': {'$exists': False}}, {'markPaused': False}]})
138156
for evt in events:
139157
ticket_scraping = TicketScraping(
140-
reese_token_gen, evt["tm_event_id"], evt["_id"], evt["ticket_num"], evt["target_price"], evt["tolerance"])
158+
reese_token_gen, evt["tm_event_id"], evt["_id"], evt["ticket_num"], evt["target_price"], evt["tolerance"], evt["client_emails"])
141159
print(ticket_scraping.initialDelay, "s")
142160
scraping_list[ticket_scraping.subscribe_id] = ticket_scraping
143161
for scraping_thread in scraping_list.values():
@@ -158,7 +176,7 @@ def start():
158176
# spawn a thread to do scraping operations
159177
full_doc = change['fullDocument']
160178
ticket_scraping = TicketScraping(
161-
reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"])
179+
reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"], full_doc["client_emails"])
162180
print(ticket_scraping.initialDelay, "s")
163181
scraping_list[ticket_scraping.subscribe_id] = ticket_scraping
164182
ticket_scraping.start()
@@ -179,7 +197,7 @@ def start():
179197
# resume scraping if currently paused
180198
if doc_id not in scraping_list:
181199
ticket_scraping = TicketScraping(
182-
reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"])
200+
reese_token_gen, full_doc["tm_event_id"], full_doc["_id"], full_doc["ticket_num"], full_doc["target_price"], full_doc["tolerance"], full_doc["client_emails"])
183201
print(ticket_scraping.initialDelay, "s")
184202
scraping_list[ticket_scraping.subscribe_id] = ticket_scraping
185203
ticket_scraping.start()

apps/ticketscraping/tasks/asynchronous.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..schedulers.mail_scheduler import mail_scheduler
77

88

9-
def run_async_tasks(picks: typing.Iterable[Pick], scraping_id: str, target_price: int):
9+
def run_async_tasks(picks: typing.Iterable[Pick], scraping_id: str, target_price: int, emails: list[str]):
1010
picks_size = len(list(picks))
1111
if picks_size == 0: return
1212

@@ -30,7 +30,7 @@ def fn(arg: tuple[int, Pick]):
3030
# get the alert information
3131
alert_contents = list(map(lambda qs: qs.get_alert_content(), alert_seats)) # type: ignore
3232
# send the alert to user
33-
mail_scheduler.send(alert_contents)
33+
mail_scheduler.send(emails, alert_contents)
3434

3535

3636
def run_async_task(pick: Pick, scraping_id: str, target_price: int):

apps/ticketscraping/tasks/periodic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def insert_history_seats(seats: set[Pick]):
3939
return insert_many(constants.DATABASE['BEST_HISTORY_SEATS'], list(map(lambda seat: vars(seat), seats)))
4040

4141

42-
def run_periodic_task(picks, scraping_id: str, target_price: int):
42+
def run_periodic_task(picks, scraping_id: str, target_price: int, emails: list[str]):
4343
# B the list of new best available seats
4444
new_best_avail = generate_picks_set_from_picks(picks)
4545
# A be the list of current best available seats
@@ -66,4 +66,4 @@ def run_periodic_task(picks, scraping_id: str, target_price: int):
6666
insert_history_seats(overwritten_seats)
6767

6868
# Use D to invoke a handler to analyze them against the best_history_seats asynchronously.
69-
async_tasks_scheduler.send(new_seats, scraping_id, target_price)
69+
async_tasks_scheduler.send(new_seats, scraping_id, target_price, emails)

0 commit comments

Comments
 (0)