def write_ride_efforts(strava_activity, ride): """ Writes out all effort associated with a ride to the database. :param strava_activity: The :class:`stravalib.model.Activity` that is associated with this effort. :type strava_activity: :class:`stravalib.model.Activity` :param ride: The db model object for ride. :type ride: :class:`bafs.model.Ride` """ assert isinstance(strava_activity, strava_model.Activity) assert isinstance(ride, Ride) try: # Start by removing any existing segments for the ride. db.engine.execute(RideEffort.__table__.delete().where(RideEffort.ride_id == strava_activity.id)) # Then add them back in for se in strava_activity.segment_efforts: effort = RideEffort(id=se.id, ride_id=strava_activity.id, elapsed_time=timedelta_to_seconds(se.elapsed_time), segment_name=se.segment.name, segment_id=se.segment.id) log.debug("Writing ride effort: {se_id}: {effort!r}".format(se_id=se.id, effort=effort.segment_name)) db.session.add(effort) ride.efforts_fetched = True except: log.exception("Error adding effort for ride: {0}".format(ride)) raise
def write_ride_photo_primary(strava_activity, ride): """ Store primary photo for activity from the main detail-level activity. :param strava_activity: The Strava :class:`stravalib.model.Activity` object. :type strava_activity: :class:`stravalib.model.Activity` :param ride: The db model object for ride. :type ride: bafs.model.Ride """ # If we have > 1 instagram photo, then we don't do anything. if strava_activity.photo_count > 1: log.debug("Ignoring basic sync for {} since there are > 1 instagram photos.") return # Start by removing any priamry photos for this ride. db.engine.execute(RidePhoto.__table__.delete().where(and_(RidePhoto.ride_id == strava_activity.id, RidePhoto.primary == True))) primary_photo = strava_activity.photos.primary if primary_photo: if primary_photo.source == 1: _write_strava_photo_primary(primary_photo, ride) else: _write_instagram_photo_primary(primary_photo, ride)
def _write_instagram_photo_primary(photo, ride): """ Writes an instagram primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.model.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.model.Ride :return: The newly added ride photo object. :rtype: bafs.model.RidePhoto """ # Here is when we have an Instagram photo as primary: # u'photos': {u'count': 1, # u'primary': {u'id': 106409096, # u'source': 2, # u'unique_id': None, # u'urls': {u'100': u'https://instagram.com/p/88qaqZvrBI/media?size=t', # u'600': u'https://instagram.com/p/88qaqZvrBI/media?size=l'}}, # u'use_prima ry_photo': False}, insta_client = insta.configured_instagram_client() shortcode = re.search(r'/p/([^/]+)/', photo.urls['100']).group(1) media = None try: #log.debug("Fetching Instagram media for shortcode: {}".format(shortcode)) media = insta_client.media_shortcode(shortcode) except (InstagramAPIError, InstagramClientError) as e: if e.status_code == 400: log.warning("Instagram photo {} for ride {}; user is set to private".format(shortcode, ride)) elif e.status_code == 404: log.warning("Photo {} for ride {}; shortcode not found".format(shortcode, ride)) else: log.exception("Error fetching instagram photo {}".format(photo)) p = RidePhoto() if media: p.id = media.id p.ref = media.link p.img_l = media.get_standard_resolution_url() p.img_t = media.get_thumbnail_url() if media.caption: p.caption = media.caption.text else: p.id = photo.id p.ref = re.match(r'(.+/)media\?size=.$', photo.urls['100']).group(1) p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id p.primary = True p.source = photo.source log.debug("Writing (primary) Instagram ride photo: {!r}".format(p)) db.session.add(p) db.session.flush() return p
def index(): page = int(request.args.get('page', 1)) if page < 1: page = 1 page_size = 60 offset = page_size * (page - 1) limit = page_size log.debug("Page = {0}, offset={1}, limit={2}".format(page, offset, limit)) total_q = db.session.query(RidePhoto).join(Ride).order_by(Ride.start_date.desc()) num_photos = total_q.count() page_q = total_q.limit(limit).offset(offset) if num_photos < offset: page = 1 total_pages = int(math.ceil( (1.0 * num_photos) / page_size)) if page > total_pages: page = total_pages return render_template('photos.html', photos=page_q, page=page, total_pages=total_pages)
def disambiguate_athlete_display_names(): q = db.session.query(model.Athlete) q = q.filter(model.Athlete.access_token != None) athletes = q.all() # Ok, here is the plan; bin these things together based on firstname and last initial. # Then iterate over each bin and if there are multiple entries, find the least number # of letters to make them all different. (we should be able to use set intersection # to check for differences within the bins?) def firstlast(name): name_parts = a.name.split(' ') fname = name_parts[0] if len(name_parts) < 2: lname = None else: lname = name_parts[-1] return (fname, lname) athletes_bin = {} for a in athletes: (fname, lname) = firstlast(a.name) if lname is None: # We only care about people with first and last names for this exercise # key = fname continue else: key = '{0} {1}'.format(fname, lname[0]) athletes_bin.setdefault(key, []).append(a) for (name_key, athletes) in athletes_bin.items(): shortest_lname = min([firstlast(a.name)[1] for a in athletes], key=len) required_length = None for i in range(len(shortest_lname)): # Calculate fname + lname-of-x-chars for each athlete. # If unique, then use this number and update the model objects candidate_short_lasts = [firstlast(a.name)[1][:i + 1] for a in athletes] if len(set(candidate_short_lasts)) == len(candidate_short_lasts): required_length = i + 1 break if required_length is not None: for a in athletes: fname, lname = firstlast(a.name) log.debug("Converting '{fname} {lname}' -> '{fname} {minlname}".format(fname=fname, lname=lname, minlname=lname[ :required_length])) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) else: log.debug("Unable to find a minimum lastname; using full lastname.") # Just use full names for a in athletes: fname, lname = firstlast(a.name) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) # Update the database with new values db.session.commit()
def update_ride_from_activity(strava_activity, ride): """ Refactoring to just set ride properties from the Strava Activity object. :param strava_activity: The Strava Activyt :type strava_activity: stravalib.model.Activity :param ride: The ride model object. :type ride: Ride """ # Should apply to both new and preexisting rides ... # If there are multiple instagram photos, then request syncing of non-primary photos too. if strava_activity.photo_count > 1 and ride.photos_fetched is None: log.debug("Scheduling non-primary photos sync for {!r}".format(ride)) ride.photos_fetched = False ride.private = bool(strava_activity.private) ride.name = strava_activity.name ride.start_date = strava_activity.start_date_local # We need to round so that "1.0" miles in strava is "1.0" miles when we convert back from meters. ride.distance = round(float(unithelper.miles(strava_activity.distance)), 3) ride.average_speed = float(unithelper.mph(strava_activity.average_speed)) ride.maximum_speed = float(unithelper.mph(strava_activity.max_speed)) ride.elapsed_time = timedelta_to_seconds(strava_activity.elapsed_time) ride.moving_time = timedelta_to_seconds(strava_activity.moving_time) location_parts = [] if strava_activity.location_city: location_parts.append(strava_activity.location_city) if strava_activity.location_state: location_parts.append(strava_activity.location_state) location_str = ', '.join(location_parts) ride.location = location_str ride.commute = strava_activity.commute ride.trainer = strava_activity.trainer ride.manual = strava_activity.manual ride.elevation_gain = float(unithelper.feet(strava_activity.total_elevation_gain)) ride.timezone = str(strava_activity.timezone) # # Short-circuit things that might result in more obscure db errors later. if ride.elapsed_time is None: raise DataEntryError("Activities cannot have null elapsed time.") if ride.moving_time is None: raise DataEntryError("Activities cannot have null moving time.") if ride.distance is None: raise DataEntryError("Activities cannot have null distance.") log.debug("Writing ride for {athlete!r}: \"{ride!r}\" on {date}".format(athlete=ride.athlete.name, ride=ride.name, date=ride.start_date.strftime('%m/%d/%y')))
def register_athlete_team(strava_athlete, athlete_model): """ Updates db with configured team that matches the athlete's teams. Updates the passed-in Athlete model object with created/updated team. :param strava_athlete: The Strava model object for the athlete. :type strava_athlete: :class:`stravalib.model.Athlete` :param athlete_model: The athlete model object. :type athlete_model: :class:`bafs.model.Athlete` :return: The :class:`bafs.model.Team` object will be returned which matches configured teams. :rtype: :class:`bafs.model.Team` :raise MultipleTeamsError: If this athlete is registered for multiple of the configured teams. That won't work. :raise NoTeamsError: If no teams match. """ assert isinstance(strava_athlete, strava_model.Athlete) assert isinstance(athlete_model, Athlete) all_teams = app.config['BAFS_TEAMS'] log.info("Checking {0!r} against {1!r}".format(strava_athlete.clubs, all_teams)) try: matches = [c for c in strava_athlete.clubs if c.id in all_teams] log.debug("Matched: {0!r}".format(matches)) athlete_model.team = None if len(matches) > 1: log.info("Multiple teams matcheed.") raise MultipleTeamsError(matches) elif len(matches) == 0: raise NoTeamsError() else: club = matches[0] # create the team row if it does not exist team = db.session.query(Team).get(club.id) if team is None: team = Team() team.id = club.id team.name = club.name team.leaderboard_exclude = club.id in app.config['BAFS_OBSERVER_TEAMS'] athlete_model.team = team db.session.add(team) return team finally: db.session.commit()
def _write_strava_photo_primary(photo, ride): """ Writes a strava native (source=1) primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.model.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.model.Ride :return: The newly added ride photo object. :rtype: bafs.model.RidePhoto """ # 'photos': {u'count': 1, # u'primary': {u'id': None, # u'source': 1, # u'unique_id': u'35453b4b-0fc1-46fd-a824-a4548426b57d', # u'urls': {u'100': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-128x96.jpg', # u'600': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-768x576.jpg'}}, # u'use_primary_photo': False}, if not photo.urls: log.warning("Photo {} present, but has no URLs (skipping)".format(photo)) return None p = RidePhoto() p.id = photo.unique_id p.primary = True p.source = photo.source p.ref = None p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id log.debug("Writing (primary) Strava ride photo: {}".format(p)) db.session.add(p) db.session.flush() return p
def write_ride_photos_nonprimary(activity_photos, ride): """ Writes out non-primary photos (currently only instagram) associated with a ride to the database. :param activity_photos: Photos for an activity. :type activity_photos: list[stravalib.model.ActivityPhoto] :param ride: The db model object for ride. :type ride: bafs.model.Ride """ # [{u'activity_id': 414980300, # u'activity_name': u'Pimmit Run CX', # u'caption': u'Pimmit Run cx', # u'created_at': u'2015-10-17T20:51:02Z', # u'created_at_local': u'2015-10-17T16:51:02Z', # u'id': 106409096, # u'ref': u'https://instagram.com/p/88qaqZvrBI/', # u'resource_state': 2, # u'sizes': {u'0': [150, 150]}, # u'source': 2, # u'type': u'InstagramPhoto', # u'uid': u'1097938959360503880_297644011', # u'unique_id': None, # u'uploaded_at': u'2015-10-17T17:55:45Z', # u'urls': {u'0': u'https://instagram.com/p/88qaqZvrBI/media?size=t'}}] db.engine.execute(RidePhoto.__table__.delete().where(and_(RidePhoto.ride_id == ride.id, RidePhoto.primary == False))) insta_client = insta.configured_instagram_client() for activity_photo in activity_photos: # If it's already in the db, then skip it. existing = db.session.query(RidePhoto).get(activity_photo.uid) if existing: log.info("Skipping photo {} because it's already in database: {}".format(activity_photo, existing)) continue try: media = insta_client.media(activity_photo.uid) photo = RidePhoto(id=activity_photo.uid, ride_id=ride.id, ref=activity_photo.ref, caption=activity_photo.caption) photo.img_l = media.get_standard_resolution_url() photo.img_t = media.get_thumbnail_url() db.session.add(photo) log.debug("Writing (non-primary) ride photo: {p_id}: {photo!r}".format(p_id=photo.id, photo=photo)) db.session.flush() except (InstagramAPIError, InstagramClientError) as e: if e.status_code == 400: log.warning("Skipping photo {0} for ride {1}; user is set to private".format(activity_photo, ride)) elif e.status_code == 404: log.warning("Skipping photo {0} for ride {1}; not found".format(activity_photo, ride)) else: log.exception("Error fetching instagram photo {0} (skipping)".format(activity_photo)) ride.photos_fetched = True
def _write_instagram_photo_primary(photo, ride): """ Writes an instagram primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.model.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.model.Ride :return: The newly added ride photo object. :rtype: bafs.model.RidePhoto """ # Here is when we have an Instagram photo as primary: # u'photos': {u'count': 1, # u'primary': {u'id': 106409096, # u'source': 2, # u'unique_id': None, # u'urls': {u'100': u'https://instagram.com/p/88qaqZvrBI/media?size=t', # u'600': u'https://instagram.com/p/88qaqZvrBI/media?size=l'}}, # u'use_prima ry_photo': False}, media = None # This doesn't work any more; Instagram changed their API to use OAuth. #insta_client = insta.configured_instagram_client() #shortcode = re.search(r'/p/([^/]+)/', photo.urls['100']).group(1) # try: # #log.debug("Fetching Instagram media for shortcode: {}".format(shortcode)) # media = insta_client.media_shortcode(shortcode) # except (InstagramAPIError, InstagramClientError) as e: # if e.status_code == 400: # log.warning("Instagram photo {} for ride {}; user is set to private".format(shortcode, ride)) # elif e.status_code == 404: # log.warning("Photo {} for ride {}; shortcode not found".format(shortcode, ride)) # else: # log.exception("Error fetching instagram photo {}".format(photo)) p = RidePhoto() if media: p.id = media.id p.ref = media.link p.img_l = media.get_standard_resolution_url() p.img_t = media.get_thumbnail_url() if media.caption: p.caption = media.caption.text else: p.id = photo.id p.ref = re.match(r'(.+/)media\?size=.$', photo.urls['100']).group(1) p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id p.primary = True p.source = photo.source log.debug("Writing (primary) Instagram ride photo: {!r}".format(p)) db.session.add(p) db.session.flush() return p