Skip to content

Commit ada5c45

Browse files
Merge pull request Jackiebibili#9 from Jackiebibili/algo-periodic-task
Add periodic task
2 parents 5efc86d + 016c2ce commit ada5c45

File tree

6 files changed

+213
-59
lines changed

6 files changed

+213
-59
lines changed

apps/storage/storage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ def insert_one(collection_name, doc: dict, db_name="tickets"):
1010
return insert_one__(coll, doc)
1111

1212
# insert many
13-
def insert_many(collection_name, docs: list, db_name="tickets"):
13+
def insert_many(collection_name, docs: list[dict], db_name="tickets"):
14+
if len(docs) == 0:
15+
return True
1416
db = get_db_handle(db_name)
1517
coll = db[collection_name]
1618
# additional attributes

apps/ticketscraping/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ def get_top_picks_url(
1313
"referer": "https://www.ticketmaster.com/"}
1414
DATABASE = {
1515
"EVENTS": "events",
16-
"TOP_PICKS": "top-picks"
16+
"TOP_PICKS": "top-picks",
17+
"BEST_AVAILABLE_SEATS": "best-available-seats",
18+
"BEST_HISTORY_SEATS": "best-history-seats"
1719
}
1820
def get_top_picks_header(): return {
1921
**BASIC_REQ_HEADER,
@@ -31,7 +33,7 @@ def get_top_picks_query_params(qty, priceInterval): return {
3133
'embed': ['area', 'offer', 'description'],
3234
'apikey': 'b462oi7fic6pehcdkzony5bxhe',
3335
'apisecret': 'pquzpfrfz7zd2ylvtz3w5dtyse',
34-
'limit': 25,
36+
'limit': 100,
3537
'offset': 0,
3638
'sort': '-quality',
3739
}

apps/ticketscraping/models/pick.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class Pick():
2+
def __init__(self, type, selection, quality, section, row, area, maxQuantity, offer, seat_columns, _id=None, scraping_id=None):
3+
self._id = _id
4+
self.scraping_id = scraping_id
5+
self.type = type
6+
self.selection = selection
7+
self.quality = quality
8+
self.section = section
9+
self.row = row
10+
self.area = area
11+
self.maxQuantity = maxQuantity
12+
self.offer = offer
13+
self.price = offer.get('listPrice')
14+
self.seat_columns = seat_columns
15+
16+
def setScrapingId(self, scraping_id: str):
17+
self.scraping_id = scraping_id
18+
19+
def __eq__(self, other):
20+
return (self.section == other.section and self.row == other.row and
21+
((type(self.seat_columns) is list and len(
22+
self.seat_columns) > 0 and type(other.seat_columns) is list and len(
23+
other.seat_columns) > 0 and self.seat_columns[0] == other.seat_columns[0]) or
24+
(self.seat_columns is None and other.seat_columns is None)) and
25+
self.price == other.price)
26+
27+
def __hash__(self):
28+
return hash((self.section,
29+
self.row,
30+
self.seat_columns[0] if type(self.seat_columns) is list and len(
31+
self.seat_columns) > 0 else None,
32+
self.price))

apps/ticketscraping/scraping.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from threading import Semaphore
99
from .prepare_reese84token import getReese84Token
1010
from ..storage.storage import *
11-
from .seat_analysis import store_seats
11+
from .seat_analysis import format_seats
12+
from .tasks.periodic import run_periodic_task
1213

1314
class Reese84TokenUpdating():
1415
def __init__(self):
@@ -75,19 +76,25 @@ def flag_for_termination(self):
7576

7677
def ticket_scraping(self):
7778
if self.token_gen.token_semaphore._value <= 0:
78-
# retry after a delay
79+
# phase: retry after a delay
7980
self.scheduler.enter(constants.TICKET_SCRAPING_TOKEN_AWAIT_MAX_INTERVAL,
8081
constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping)
8182
return
83+
# scrape the top-picks from ticketmaster
8284
top_picks_url = constants.get_top_picks_url(self.event_id)
8385
top_picks_q_params = constants.get_top_picks_query_params(
8486
self.num_seats, self.price_range)
8587
top_picks_header = constants.get_top_picks_header()
8688
res = requests.get(top_picks_url, headers=top_picks_header, params=top_picks_q_params,
8789
cookies=dict(reese84=self.token_gen.reese84_token['token']))
8890
# print(res.json())
89-
res_obj = res.json()
90-
store_seats(res_obj, {'subscribe_req_id': self.subscribe_id})
91+
92+
# prune and format the received picks
93+
picks_obj = format_seats(res.json(), self.subscribe_id)
94+
95+
# periodic task: update collections best_available_seats and best_history_seats
96+
run_periodic_task(picks_obj, self.subscribe_id)
97+
9198
print("Got the ticket info from TM. /", res.status_code)
9299
self.scheduler.enter(constants.TICKET_SCRAPING_INTERVAL,
93100
constants.TICKET_SCRAPING_PRIORITY, self.ticket_scraping)

apps/ticketscraping/seat_analysis.py

Lines changed: 92 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from dateutil import parser
2-
from ..storage.storage import insert_one
3-
from ..ticketscraping import constants
2+
# from ..storage.storage import insert_one
3+
# from ..ticketscraping import constants
44

55

6-
def store_seats(data, scheduler_config):
6+
def format_seats(data, subscriber_id):
77
# prune top-picks data structure
88
pruned_picks = prune_pick_attributes(data)
99

@@ -12,12 +12,12 @@ def store_seats(data, scheduler_config):
1212
append_scraping_config_ref,
1313
map_prices_to_seats,
1414
remove_embedded_field
15-
], pruned_picks, scheduler_config)
15+
], pruned_picks, subscriber_id)
1616

17-
# store in db
18-
# print(res)
19-
insert_one(constants.DATABASE['TOP_PICKS'], res)
20-
pass
17+
# # store in db
18+
# # print(res)
19+
# insert_one(constants.DATABASE['TOP_PICKS'], res)
20+
return res
2121

2222
def pipe(fns: list, *args):
2323
out = args
@@ -28,79 +28,119 @@ def pipe(fns: list, *args):
2828
out = fn(out)
2929
return out
3030

31+
def get_value_from_map(map: dict, *args, **kwargs):
32+
# input validation
33+
if type(map) is not dict:
34+
return kwargs.get('default', None)
35+
res = kwargs.get('default', None)
36+
for attr in args:
37+
res = map.get(attr)
38+
if res is not None:
39+
break
40+
return res
41+
42+
def get_value_from_nested_map(map: dict, *args, **kwargs):
43+
# input validation
44+
if type(map) is not dict:
45+
return kwargs.get('default', None)
46+
res = None
47+
m = map
48+
count = 0
49+
for attr in args:
50+
res = m.get(attr)
51+
count += 1
52+
if res is None:
53+
break
54+
elif type(res) is dict:
55+
m = res
56+
else:
57+
break
58+
return res if res is not None and count == len(args) else kwargs.get('default', None)
59+
60+
def get_fn_return(fn, *args, **kwargs):
61+
res = kwargs.get('default', None)
62+
try:
63+
res = fn(*args)
64+
except:
65+
pass
66+
finally:
67+
return res
3168

3269
def prune_pick_attributes(data):
33-
def prune_pick_offer_attributes(pick):
70+
def prune_pick_offer_attributes(pick: dict):
3471
return {
35-
'type': pick['type'],
36-
'selection': pick['selection'],
37-
'quality': pick['quality'],
38-
'section': pick['section'],
39-
'row': pick['row'],
40-
'offerGroups': pick['offerGroups'],
41-
'area': pick['area'],
42-
'maxQuantity': pick['maxQuantity'],
72+
'type': get_value_from_map(pick, 'type'),
73+
'selection': get_value_from_map(pick, 'selection'),
74+
'quality': get_value_from_map(pick, 'quality'),
75+
'section': get_value_from_map(pick, 'section'),
76+
'row': get_value_from_map(pick, 'row'),
77+
'offerGroups': get_value_from_map(pick, 'offerGroups', 'offers'),
78+
'area': get_value_from_map(pick, 'area'),
79+
'maxQuantity': get_value_from_map(pick, 'maxQuantity'),
4380
}
4481

45-
def prune_pick_embedded_attributes(embedded):
82+
def prune_pick_embedded_attributes(embedded: dict):
4683
def prune_pick_embedded_offer_attributes(item):
4784
return {
48-
'expired_date': parser.parse(item['meta']['expires']),
49-
'offerId': item['offerId'],
50-
'rank': item['rank'],
51-
'online': item['online'],
52-
'protected': item['protected'],
53-
'rollup': item['rollup'],
54-
'inventoryType': item['inventoryType'],
55-
'offerType': item['offerType'],
56-
'currency': item['currency'],
57-
'listPrice': item['listPrice'],
58-
'faceValue': item['faceValue'],
59-
'totalPrice': item['totalPrice'],
60-
'noChargesPrice': item['noChargesPrice'],
61-
# 'listingId': item['listingId'],
62-
# 'listingVersionId': item['listingVersionId'],
63-
# 'charges': item['charges'],
64-
# 'sellableQuantities': item['sellableQuantities'],
65-
# 'section': item['section'],
66-
# 'row': item['row'],
67-
# 'seatFrom': item['seatFrom'],
68-
# 'seatTo': item['seatTo'],
69-
# 'ticketTypeId': item['ticketTypeId']
85+
'expired_date': get_fn_return(parser.parse, get_value_from_nested_map(item, 'meta', 'expires'), default=None),
86+
'offerId': get_value_from_map(item, 'offerId'),
87+
'rank': get_value_from_map(item, 'rank'),
88+
'online': get_value_from_map(item, 'online'),
89+
'protected': get_value_from_map(item, 'protected'),
90+
'rollup': get_value_from_map(item, 'rollup'),
91+
'inventoryType': get_value_from_map(item, 'inventoryType'),
92+
'offerType': get_value_from_map(item, 'offerType'),
93+
'currency': get_value_from_map(item, 'currency'),
94+
'listPrice': get_value_from_map(item, 'listPrice'),
95+
'faceValue': get_value_from_map(item, 'faceValue'),
96+
'totalPrice': get_value_from_map(item, 'totalPrice'),
97+
'noChargesPrice': get_value_from_map(item, 'noChargesPrice'),
98+
# 'listingId': get_value_from_map(item, 'listingId'),
99+
# 'listingVersionId': get_value_from_map(item, 'listingVersionId'),
100+
# 'charges': get_value_from_map(item, 'charges'),
101+
# 'sellableQuantities': get_value_from_map(item, 'sellableQuantities'),
102+
# 'section': get_value_from_map(item, 'section'),
103+
# 'row': get_value_from_map(item, 'row'),
104+
# 'seatFrom': get_value_from_map(item, 'seatFrom'),
105+
# 'seatTo': get_value_from_map(item, 'seatTo'),
106+
# 'ticketTypeId': get_value_from_map(item, 'ticketTypeId')
70107
}
71108
return {
72-
'offer': list(map(prune_pick_embedded_offer_attributes, embedded['offer']))
109+
'offer': list(map(prune_pick_embedded_offer_attributes, get_value_from_map(embedded, 'offer', default=dict())))
73110
}
74111
return {
75-
'expired_date': parser.parse(data['meta']['expires']),
76-
'eventId': data['eventId'],
77-
'offset': data['offset'],
78-
'total': data['total'],
79-
'picks': list(map(prune_pick_offer_attributes, data['picks'])),
80-
'_embedded': prune_pick_embedded_attributes(data['_embedded'])
112+
'expired_date': get_fn_return(parser.parse, get_value_from_nested_map(data, 'meta', 'expires'), default=None),
113+
'eventId': get_value_from_map(data, 'eventId'),
114+
'offset': get_value_from_map(data, 'offset'),
115+
'total': get_value_from_map(data, 'total'),
116+
'picks': list(map(prune_pick_offer_attributes, get_value_from_map(data, 'picks', default=dict()))),
117+
'_embedded': prune_pick_embedded_attributes(get_value_from_map(data, '_embedded', default=dict()))
81118
}
82119

83120

84-
def append_scraping_config_ref(data, scheduler_config):
85-
data['scraping_config_ref'] = scheduler_config
121+
def append_scraping_config_ref(data, config_id):
122+
data['scraping_config_ref'] = config_id
86123
return data
87124

88125

89126
def map_prices_to_seats(data):
90127
def map_prices_to_seat_helper(offer_table: dict):
91128
def __map_prices_to_seat_helper(pick):
92129
offerGroups = pick['offerGroups']
130+
if offerGroups is None or len(offerGroups) == 0:
131+
return {'offer_available': False}
93132
offerGroup = offerGroups[0]
94-
offerIds = offerGroup['offers']
95-
offerSeatCols = offerGroup['seats']
96-
if len(offerGroups) == 0 or len(offerIds) == 0:
133+
offerIds = get_value_from_map(offerGroup, 'offers', default=[offerGroup])
134+
offerSeatCols = get_value_from_map(offerGroup, 'seats')
135+
if len(offerIds) == 0:
97136
return {'offer_available': False}
98137
offerId = offerIds[0]
99138
offerObj = offer_table.get(offerId)
100139
res = {**pick, 'offer': offerObj, 'seat_columns': offerSeatCols}
101140
del res['offerGroups']
102141
return res
103142
return __map_prices_to_seat_helper
143+
104144
offer_dict = {offer['offerId']: offer for offer in data['_embedded']['offer']}
105145
picks_list = list(
106146
map(map_prices_to_seat_helper(offer_dict), data['picks']))
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from ...storage.storage import find_many, insert_many, delete_many
2+
from ...ticketscraping import constants
3+
from ..models.pick import Pick
4+
5+
def generate_picks_set_from_picks(picks):
6+
def __helper(pick: dict):
7+
return Pick(_id=pick.get('_id'),
8+
scraping_id=pick.get('scraping_id'),
9+
type=pick['type'],
10+
selection=pick['selection'],
11+
quality=pick['quality'],
12+
section=pick['section'],
13+
row=pick['row'],
14+
area=pick['area'],
15+
maxQuantity=pick['maxQuantity'],
16+
offer=pick['offer'],
17+
seat_columns=pick['seat_columns'])
18+
19+
if type(picks) is dict:
20+
return set(map(__helper, picks['picks']))
21+
elif type(picks) is list:
22+
return set(map(__helper, picks))
23+
else:
24+
raise Exception('argument type error')
25+
26+
def get_current_best_available(scraping_id: str):
27+
return find_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], {"scraping_id": scraping_id})
28+
def remove_best_seats(seats: set[Pick]):
29+
ids = []
30+
for seat in seats:
31+
ids.append(seat._id)
32+
return delete_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], {"_id" : {"$in": ids}})
33+
def insert_best_seats(seats: set[Pick], scraping_id: str):
34+
for seat in seats:
35+
seat.setScrapingId(scraping_id)
36+
return insert_many(constants.DATABASE['BEST_AVAILABLE_SEATS'], list(map(lambda seat: vars(seat), seats)))
37+
def insert_history_seats(seats: set[Pick]):
38+
return insert_many(constants.DATABASE['BEST_HISTORY_SEATS'], list(map(lambda seat: vars(seat), seats)))
39+
40+
41+
42+
def run_periodic_task(picks: dict, scraping_id: str):
43+
# B the list of new best available seats
44+
new_best_avail = generate_picks_set_from_picks(picks)
45+
# A be the list of current best available seats
46+
cur_best_avail = generate_picks_set_from_picks(get_current_best_available(scraping_id))
47+
48+
# Compute C := A-B which is the seats
49+
overwritten_seats = cur_best_avail - new_best_avail
50+
51+
# Compute D := B-A which is the new seats
52+
new_seats = new_best_avail - cur_best_avail
53+
54+
print(f"size of B is {len(new_best_avail)}")
55+
print(f"size of A is {len(cur_best_avail)}")
56+
print(f"size of C is {len(overwritten_seats)}")
57+
print(f"size of D is {len(new_seats)}")
58+
59+
# Remove C from best_available_seats
60+
remove_best_seats(overwritten_seats)
61+
62+
# Insert D to best_available_seats
63+
insert_best_seats(new_seats, scraping_id)
64+
65+
# Save C to best_history_seats.
66+
insert_history_seats(overwritten_seats)
67+
68+
# TODO
69+
# Use D to invoke a handler to analyze them against the best_history_seats asynchronously.
70+
71+
pass

0 commit comments

Comments
 (0)