Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store updated and last fetched timestamps for venues #347

Merged
merged 14 commits into from
Oct 26, 2020
Merged
1 change: 0 additions & 1 deletion beers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
"manufacturer": models.Manufacturer.objects.order_by("name"),
}
order_qs = fields.get(db_field.name)
print(db_field.name, order_qs is not None)
if order_qs:
kwargs["queryset"] = order_qs

Expand Down
25 changes: 22 additions & 3 deletions tap_list_providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from decimal import Decimal, InvalidOperation
from urllib.parse import urlparse, unquote
import logging
import datetime

from django.db.models import Prefetch, Q
from django.db.models.functions import Length
from django.db import transaction
from django.utils.timezone import now
from kombu.exceptions import OperationalError

from venues.models import Venue
Expand Down Expand Up @@ -51,12 +53,13 @@

class BaseTapListProvider:
def __init__(self):
self.check_timestamp = now()
self.styles = {}
if not hasattr(self, "provider_name"):
# Don't define this attribute if the child does for us
self.provider_name = None

def handle_venue(self, venue):
def handle_venue(self, venue: Venue) -> datetime.datetime:
raise NotImplementedError("You need to implement this yourself")

@classmethod
Expand Down Expand Up @@ -92,7 +95,23 @@ def get_venues(self):
def handle_venues(self, venues):
for venue in venues:
LOG.debug("Fetching beers at %s", venue)
self.handle_venue(venue)
update_time = self.handle_venue(venue)
self.update_venue_timestamps(venue, update_time)

def update_venue_timestamps(
self, venue: Venue, update_time: datetime.datetime = None
) -> None:
"""Update the venue last checked and last updated times"""
LOG.debug(
"Setting check time for %s to %s and update time for to %s",
venue,
self.check_timestamp,
update_time,
)
venue.tap_list_last_check_time = self.check_timestamp
if update_time:
venue.tap_list_last_update_time = update_time
venue.save()

def get_style(self, name):
name = name.strip()
Expand Down Expand Up @@ -410,7 +429,7 @@ def get_beer(self, name, manufacturer, pricing=None, venue=None, **defaults):
if str(exc).casefold() == "max number of clients reached".casefold():
LOG.error("Reached redis limit!")
# fall back to doing it synchronously
look_up_beer(beer.id)
look_up_beer(beer.id) # pylint: disable=no-value-for-parameter
else:
raise
return beer
Expand Down
4 changes: 3 additions & 1 deletion tap_list_providers/management/commands/parsebeermenus.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)

self.stdout.write(self.style.SUCCESS("Done!"))
5 changes: 3 additions & 2 deletions tap_list_providers/management/commands/parsedigitalpour.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class Command(BaseCommand):
help = "Populates any venues using the DigitalPour tap list provider with" " beers"
help = "Populates any venues using the DigitalPour tap list provider with beers"

def add_arguments(self, parser):
# does not take any arguments
Expand All @@ -18,5 +18,6 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)
self.stdout.write(self.style.SUCCESS("Done!"))
7 changes: 3 additions & 4 deletions tap_list_providers/management/commands/parsestemandstein.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@


class Command(BaseCommand):
help = (
"Populates any venues using the Stem and Stein tap list provider with" " beers"
)
help = "Populates any venues using the Stem and Stein tap list provider with beers"

def add_arguments(self, parser):
# does not take any arguments
Expand All @@ -20,5 +18,6 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)
self.stdout.write(self.style.SUCCESS("Done!"))
5 changes: 3 additions & 2 deletions tap_list_providers/management/commands/parsetaphunter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class Command(BaseCommand):
help = "Populates any venues using the TapHunter tap list provider with" " beers"
help = "Populates any venues using the TapHunter tap list provider with beers"

def add_arguments(self, parser):
# does not take any arguments
Expand All @@ -18,5 +18,6 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)
self.stdout.write(self.style.SUCCESS("Done!"))
3 changes: 2 additions & 1 deletion tap_list_providers/management/commands/parsetaplistio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)
self.stdout.write(self.style.SUCCESS("Done!"))
3 changes: 2 additions & 1 deletion tap_list_providers/management/commands/parseuntappd.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ def handle(self, *args, **options):
with transaction.atomic():
for venue in tap_list_provider.get_venues():
self.stdout.write("Processing %s" % venue.name)
tap_list_provider.handle_venue(venue)
timestamp = tap_list_provider.handle_venue(venue)
tap_list_provider.update_venue_timestamps(venue, timestamp)
self.stdout.write(self.style.SUCCESS("Done!"))
17 changes: 13 additions & 4 deletions tap_list_providers/parsers/beermenus.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Parser for beermenus dot com"""

from decimal import Decimal
import datetime
from dataclasses import dataclass
import logging
import os
Expand Down Expand Up @@ -71,6 +74,7 @@ def __init__(
self.categories = categories
self.soup = None
self.save_fetched_data = save_fetched_data
self.updated_date = None
super().__init__()

def fetch_data(self) -> str:
Expand Down Expand Up @@ -100,14 +104,14 @@ def parse_html(self, data: str) -> List[BeerData]:
)[0]
# they just give us a date. I'm going to arbitrarily declare that to be
# midnight UTC because who cares if we're off by a day
updated_date = UTC.localize(
self.updated_date = UTC.localize(
parse(
updated_span.text.split()[1],
dayfirst=False,
)
)
# TODO save this to the venue
LOG.debug("Last updated: %s", updated_date)
LOG.debug("Last updated: %s", self.updated_date)

# the beer lists are in <ul>s
beers = []
Expand Down Expand Up @@ -218,7 +222,7 @@ def parse_beers(self, beers: List[BeerData]) -> None:
beer.brewery_name = brewery_a.text
beer.brewery_location = brewery_p.text.split(MIDDOT)[1].strip()

def handle_venue(self, venue: Venue) -> None:
def handle_venue(self, venue: Venue) -> datetime.datetime:
self.categories = venue.api_configuration.beermenus_categories
self.location_url = self.URL.format(venue.api_configuration.beermenus_slug)
data = self.fetch_data()
Expand Down Expand Up @@ -261,6 +265,7 @@ def handle_venue(self, venue: Venue) -> None:
tap_number,
beer,
)
return self.updated_date


def parse_beer_tag(tag: Tag) -> BeerData:
Expand Down Expand Up @@ -289,7 +294,7 @@ def parse_beer_tag(tag: Tag) -> BeerData:
)


if __name__ == "__main__":
def main():
import argparse

LOCATIONS = {
Expand Down Expand Up @@ -319,3 +324,7 @@ def parse_beer_tag(tag: Tag) -> BeerData:

for beer in beers:
print(f"{beer.name} by {beer.brewery_name} ({beer.abv}%, {beer.style})")


if __name__ == "__main__":
main()
12 changes: 11 additions & 1 deletion tap_list_providers/parsers/digitalpour.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Parser for DigitalPour"""

import datetime
from decimal import Decimal
import logging
import os
Expand All @@ -8,6 +11,7 @@
import configurations
from django.utils.timezone import now
from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady
from pytz import UTC

# boilerplate code necessary for launching outside manage.py
try:
Expand All @@ -19,6 +23,7 @@
from ..base import BaseTapListProvider

from taps.models import Tap
from venues.models import Venue


LOG = logging.getLogger(__name__)
Expand All @@ -34,16 +39,18 @@ class DigitalPourParser(BaseTapListProvider):
def __init__(self, location=None):
"""Constructor."""
self.url = None
self.update_date = None
if location:
self.url = self.URL.format(location[0], location[1], self.APIKEY)
super().__init__()

def handle_venue(self, venue):
def handle_venue(self, venue: Venue) -> datetime.datetime:
venue_id = venue.api_configuration.digital_pour_venue_id
location_number = venue.api_configuration.digital_pour_location_number
self.url = self.URL.format(venue_id, location_number, self.APIKEY)
data = self.fetch()
taps = {tap.tap_number: tap for tap in venue.taps.all()}
self.update_date = UTC.localize(datetime.datetime(1970, 1, 1, 0, 0, 0))
manufacturers = {}
for entry in data:
if not entry["Active"]:
Expand All @@ -57,6 +64,8 @@ def handle_venue(self, venue):
tap = Tap(venue=venue, tap_number=tap_info["tap_number"])
tap.time_added = tap_info["added"]
tap.time_updated = tap_info["updated"]
if tap.time_updated and tap.time_updated > self.update_date:
self.update_date = tap.time_updated
tap.estimated_percent_remaining = tap_info["percent_full"]
if tap_info["gas_type"] in [i[0] for i in Tap.GAS_CHOICES]:
tap.gas_type = tap_info["gas_type"]
Expand Down Expand Up @@ -116,6 +125,7 @@ def handle_venue(self, venue):
# 4. assign the beer to the tap
tap.beer = beer
tap.save()
return self.update_date

def parse_beer(self, entry):
"""Parse beer info from JSON entry."""
Expand Down
19 changes: 18 additions & 1 deletion tap_list_providers/parsers/stemandstein.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""HTML scraper for The Stem & Stein"""
import datetime
from urllib.parse import parse_qsl
from html.parser import HTMLParser
from decimal import Decimal
Expand All @@ -8,6 +9,8 @@
from bs4 import BeautifulSoup
import requests
import configurations
import pytz
from dateutil.parser import parse
from django.db.models import Q
from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady

Expand All @@ -24,6 +27,7 @@
from taps.models import Tap


CENTRAL_TIME = pytz.timezone("America/Chicago")
LOG = logging.getLogger(__name__)


Expand Down Expand Up @@ -133,6 +137,8 @@ def fill_in_beer_details(self, beer):
).text
beer_parser = BeautifulSoup(beer_html, "html.parser")
jumbotron = beer_parser.find("div", {"class": "jumbotron"})
tap_table = beer_parser.find("table", {"id": "tapList"})
tap_body = tap_table.find("tbody")
image_div = jumbotron.find(
"div",
{"style": "display:table-cell;vertical-align:top;width:17px;"},
Expand Down Expand Up @@ -190,6 +196,12 @@ def fill_in_beer_details(self, beer):
beer=beer,
defaults={"price": price},
)
time_tapped = None
for row in tap_body.find_all("tr"):
cells = list(row.find_all("td"))
if cells[-1].text.endswith("(so far)"):
time_tapped = CENTRAL_TIME.localize(parse(cells[0].text))
return time_tapped

def handle_venue(self, venue):
self.venue = venue
Expand All @@ -199,8 +211,11 @@ def handle_venue(self, venue):
existing_taps = {i.tap_number: i for i in venue.taps.all()}
LOG.debug("existing taps %s", existing_taps)
taps_hit = []
latest_time = CENTRAL_TIME.localize(datetime.datetime(1970, 1, 1, 0))
for tap_number, beer in taps.items():
self.fill_in_beer_details(beer)
time_tapped = self.fill_in_beer_details(beer)
if time_tapped > latest_time:
latest_time = time_tapped
try:
tap = existing_taps[tap_number]
except KeyError:
Expand All @@ -209,9 +224,11 @@ def handle_venue(self, venue):
tap_number=tap_number,
)
tap.beer = beer
tap.time_added = time_tapped
tap.save()
taps_hit.append(tap.tap_number)
LOG.debug("Deleting all taps except %s", taps_hit)
Tap.objects.filter(venue=venue,).exclude(
tap_number__in=taps_hit,
).delete()
return latest_time
8 changes: 8 additions & 0 deletions tap_list_providers/parsers/taphunter.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""Parse beers from TapHunter"""
import argparse
import datetime
import decimal
import logging
import os
import json

import configurations
import requests
from dateutil.parser import parse
from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady
from pytz import UTC

# boilerplate code necessary for launching outside manage.py
try:
Expand Down Expand Up @@ -49,6 +52,7 @@ def handle_venue(self, venue):
use_sequential_taps = any(
(tap_info["serving_info"]["tap_number"] == "" for tap_info in data["taps"])
)
latest_timestamp = UTC.localize(datetime.datetime(1970, 1, 1, 12))
for index, entry in enumerate(data["taps"]):
# 1. parse the tap
tap_info = self.parse_tap(entry)
Expand All @@ -73,6 +77,9 @@ def handle_venue(self, venue):
tap = Tap(venue=venue, tap_number=tap_number)
tap.time_added = tap_info["added"]
tap.time_updated = tap_info["updated"]
parsed_time = parse(tap_info["updated"])
if parsed_time > latest_timestamp:
latest_timestamp = parsed_time
if "percent_full" in tap_info:
tap.estimated_percent_remaining = tap_info["percent_full"]
else:
Expand Down Expand Up @@ -123,6 +130,7 @@ def handle_venue(self, venue):
# 4. assign the beer to the tap
tap.beer = beer
tap.save()
return latest_timestamp

def parse_beer(self, tap):
beer = {
Expand Down
1 change: 1 addition & 0 deletions tap_list_providers/parsers/taplist_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def handle_venue(self, venue):
if current_tap.time_updated != timestamp:
current_tap.time_updated = timestamp
current_tap.save()
return timestamp

def parse_tap(self, tap_dict):
if tap_dict["current_keg"] is None:
Expand Down
Loading