Esempio n. 1
0
def write_ride_streams(streams, ride):
    """
    Store GPS track for activity as LINESTRING in db.

    :param streams: The Strava :class:`stravalib.model.Activity` object.
    :type streams: list[stravalib.model.Stream]

    :param ride: The db model object for ride.
    :type ride: :class:`bafs.model.Ride`
    """
    try:
        streams_dict = {s.type: s for s in streams}
        """ :type: dict[str,stravalib.model.Stream] """
        lonlat_points = [(lon,lat) for (lat,lon) in streams_dict['latlng'].data]

        if not lonlat_points:
            raise ValueError("No data points in latlng streams.")
    except (KeyError, ValueError) as x:
        log.info("No GPS track for activity {} (skipping): {}".format(ride, x), exc_info=log.isEnabledFor(logging.DEBUG))
        ride.track_fetched = None
    else:
        # Start by removing any existing segments for the ride.
        db.engine.execute(RideTrack.__table__.delete().where(RideTrack.ride_id == ride.id))

        gps_track = WKTSpatialElement(wktutils.linestring_wkt(lonlat_points))

        ride_track = RideTrack()
        ride_track.gps_track = gps_track
        ride_track.ride_id = ride.id
        ride_track.elevation_stream = streams_dict['altitude'].data
        ride_track.time_stream = streams_dict['time'].data
        db.session.add(ride_track)

    ride.track_fetched = True
Esempio n. 2
0
def write_ride_streams(streams, ride):
    """
    Store GPS track for activity as LINESTRING in db.

    :param streams: The Strava :class:`stravalib.model.Activity` object.
    :type streams: list[stravalib.model.Stream]

    :param ride: The db model object for ride.
    :type ride: :class:`bafs.model.Ride`
    """
    try:
        streams_dict = {s.type: s for s in streams}
        """ :type: dict[str,stravalib.model.Stream] """
        lonlat_points = [(lon,lat) for (lat,lon) in streams_dict['latlng'].data]

        if not lonlat_points:
            raise ValueError("No data points in latlng streams.")
    except (KeyError, ValueError) as x:
        log.info("No GPS track for activity {} (skipping): {}".format(ride, x), exc_info=log.isEnabledFor(logging.DEBUG))
        ride.track_fetched = None
    else:
        # Start by removing any existing segments for the ride.
        db.engine.execute(RideTrack.__table__.delete().where(RideTrack.ride_id == ride.id))

        gps_track = WKTSpatialElement(wktutils.linestring_wkt(lonlat_points))

        ride_track = RideTrack()
        ride_track.gps_track = gps_track
        ride_track.ride_id = ride.id
        ride_track.elevation_stream = streams_dict['altitude'].data
        ride_track.time_stream = streams_dict['time'].data
        db.session.add(ride_track)

    ride.track_fetched = True
Esempio n. 3
0
def _geo_tracks(start_date=None, end_date=None, team_id=None):

    # These dates  must be made naive, since we don't have TZ info stored in our ride columns.
    if start_date:
        start_date = arrow.get(start_date).datetime.replace(tzinfo=None)

    if end_date:
        end_date = arrow.get(end_date).datetime.replace(tzinfo=None)

    log.info("Filtering on start_date: {}".format(start_date))
    log.info("Filtering on end_date: {}".format(end_date))

    sess = db.session

    q = sess.query(RideTrack).join(Ride).join(Athlete)
    q = q.filter(Ride.private==False)

    if team_id:
        q = q.filter(Athlete.team_id==team_id)

    if start_date:
        q = q.filter(Ride.start_date >= start_date)
    if end_date:
        q = q.filter(Ride.start_date < end_date)

    linestrings = []
    for ride_track in q:
        assert isinstance(ride_track, RideTrack)
        ride_tz = pytz.timezone(ride_track.ride.timezone)
        wkt = sess.scalar(ride_track.gps_track.wkt)

        coordinates = []
        for (i, (lon, lat)) in enumerate(parse_linestring(wkt)):
            elapsed_time = ride_track.ride.start_date + timedelta(seconds=ride_track.time_stream[i])

            point = (
                float(Decimal(lon)),
                float(Decimal(lat)),
                float(Decimal(ride_track.elevation_stream[i])),
                ride_tz.localize(elapsed_time).isoformat()
            )

            coordinates.append(point)

        linestrings.append(coordinates)

    geojson_structure = {"type": "MultiLineString", "coordinates": linestrings}

    #return geojson.dumps(geojson.MultiLineString(linestrings))
    return json.dumps(geojson_structure)
Esempio n. 4
0
def _geo_tracks(start_date=None, end_date=None, team_id=None):

    # These dates  must be made naive, since we don't have TZ info stored in our ride columns.
    if start_date:
        start_date = arrow.get(start_date).datetime.replace(tzinfo=None)

    if end_date:
        end_date = arrow.get(end_date).datetime.replace(tzinfo=None)

    log.info("Filtering on start_date: {}".format(start_date))
    log.info("Filtering on end_date: {}".format(end_date))

    sess = db.session

    q = sess.query(RideTrack).join(Ride).join(Athlete)
    if team_id:
        q = q.filter(Athlete.team_id==team_id)

    if start_date:
        q = q.filter(Ride.start_date >= start_date)
    if end_date:
        q = q.filter(Ride.start_date < end_date)

    linestrings = []
    for ride_track in q:
        assert isinstance(ride_track, RideTrack)
        ride_tz = pytz.timezone(ride_track.ride.timezone)
        wkt = sess.scalar(ride_track.gps_track.wkt)

        coordinates = []
        for (i, (lon, lat)) in enumerate(parse_linestring(wkt)):
            elapsed_time = ride_track.ride.start_date + timedelta(seconds=ride_track.time_stream[i])

            point = (
                float(Decimal(lon)),
                float(Decimal(lat)),
                float(Decimal(ride_track.elevation_stream[i])),
                ride_tz.localize(elapsed_time).isoformat()
            )

            coordinates.append(point)

        linestrings.append(coordinates)

    geojson_structure = {"type": "MultiLineString", "coordinates": linestrings}

    #return geojson.dumps(geojson.MultiLineString(linestrings))
    return json.dumps(geojson_structure)
Esempio n. 5
0
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()
Esempio n. 6
0
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()
Esempio n. 7
0
    def is_excluded(activity):
        activity_end_date = (activity.start_date_local + activity.elapsed_time)
        if end_date and activity_end_date > end_date:
            log.info(
                "Skipping ride {0} ({1!r}) because date ({2}) is after competition end date ({3})".format(activity.id,
                                                                                                          activity.name,
                                                                                                          activity_end_date,
                                                                                                          end_date))
            return True

        for keyword in exclude_keywords:
            if keyword.lower() in activity.name.lower():
                log.info("Skipping ride {0} ({1!r}) due to presence of exlusion keyword: {2!r}".format(activity.id,
                                                                                                       activity.name,
                                                                                                       keyword))
                return True
        else:
            return False
Esempio n. 8
0
    def is_excluded(activity):
        activity_end_date = (activity.start_date_local + activity.elapsed_time)
        if end_date and activity_end_date > end_date:
            log.info(
                "Skipping ride {0} ({1!r}) because date ({2}) is after competition end date ({3})".format(activity.id,
                                                                                                          activity.name,
                                                                                                          activity_end_date,
                                                                                                          end_date))
            return True

        for keyword in exclude_keywords:
            if keyword.lower() in activity.name.lower():
                log.info("Skipping ride {0} ({1!r}) due to presence of exlusion keyword: {2!r}".format(activity.id,
                                                                                                       activity.name,
                                                                                                       keyword))
                return True
        else:
            return False
Esempio n. 9
0
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
Esempio n. 10
0
def write_ride(activity):
    """
    Takes the specified activity and writes it to the database.

    :param activity: The Strava :class:`stravalib.model.Activity` object.
    :type activity: stravalib.model.Activity

    :return: A tuple including the written Ride model object, whether to resync segment efforts, and whether to resync photos.
    :rtype: bafs.model.Ride
    """

    if activity.start_latlng:
        start_geo = WKTSpatialElement('POINT({lon} {lat})'.format(lat=activity.start_latlng.lat,
                                                                  lon=activity.start_latlng.lon))
    else:
        start_geo = None

    if activity.end_latlng:
        end_geo = WKTSpatialElement('POINT({lon} {lat})'.format(lat=activity.end_latlng.lat,
                                                                lon=activity.end_latlng.lon))
    else:
        end_geo = None

    athlete_id = activity.athlete.id

    # Fail fast for invalid data (this can happen with manual-entry rides)
    assert activity.elapsed_time is not None
    assert activity.moving_time is not None
    assert activity.distance is not None

    # Find the model object for that athlete (or create if doesn't exist)
    athlete = db.session.query(Athlete).get(athlete_id)
    if not athlete:
        # The athlete has to exist since otherwise we wouldn't be able to query their rides
        raise ValueError("Somehow you are attempting to write rides for an athlete not found in the database.")

    if start_geo is not None or end_geo is not None:
        ride_geo = RideGeo()
        ride_geo.start_geo = start_geo
        ride_geo.end_geo = end_geo
        ride_geo.ride_id = activity.id
        db.session.merge(ride_geo)

    ride = db.session.query(Ride).get(activity.id)
    new_ride = (ride is None)
    if ride is None:
        ride = Ride(activity.id)

    # Check to see if we need to pull down efforts for this ride
    if new_ride:
        ride.detail_fetched = False  # Just to be explicit

        if not activity.manual:
            ride.track_fetched = False

        # photo_count refers to instagram photos
        if activity.photo_count > 1:
            ride.photos_fetched = False
        else:
            ride.photos_fetched = None

    else:
        if round(ride.distance, 2) != round(float(unithelper.miles(activity.distance)), 2):
            log.info("Queing resync of details for activity {0!r}: distance mismatch ({1} != {2})".format(activity,
                                                                                                          ride.distance,
                                                                                                          unithelper.miles(activity.distance)))
            ride.detail_fetched = False
            ride.track_fetched = False

    ride.athlete = athlete


    update_ride_from_activity(strava_activity=activity, ride=ride)


    db.session.add(ride)

    return ride
Esempio n. 11
0
def webhook():
    log.info("Received a webhook.")
    log.info("Request JSON payload: {}".format(request.json))
    return jsonify()
Esempio n. 12
0
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
Esempio n. 13
0
def write_ride(activity):
    """
    Takes the specified activity and writes it to the database.

    :param activity: The Strava :class:`stravalib.model.Activity` object.
    :type activity: stravalib.model.Activity

    :return: A tuple including the written Ride model object, whether to resync segment efforts, and whether to resync photos.
    :rtype: bafs.model.Ride
    """

    if activity.start_latlng:
        start_geo = WKTSpatialElement('POINT({lon} {lat})'.format(lat=activity.start_latlng.lat,
                                                                  lon=activity.start_latlng.lon))
    else:
        start_geo = None

    if activity.end_latlng:
        end_geo = WKTSpatialElement('POINT({lon} {lat})'.format(lat=activity.end_latlng.lat,
                                                                lon=activity.end_latlng.lon))
    else:
        end_geo = None

    athlete_id = activity.athlete.id

    # Fail fast for invalid data (this can happen with manual-entry rides)
    assert activity.elapsed_time is not None
    assert activity.moving_time is not None
    assert activity.distance is not None

    # Find the model object for that athlete (or create if doesn't exist)
    athlete = db.session.query(Athlete).get(athlete_id)
    if not athlete:
        # The athlete has to exist since otherwise we wouldn't be able to query their rides
        raise ValueError("Somehow you are attempting to write rides for an athlete not found in the database.")

    if start_geo is not None or end_geo is not None:
        ride_geo = RideGeo()
        ride_geo.start_geo = start_geo
        ride_geo.end_geo = end_geo
        ride_geo.ride_id = activity.id
        db.session.merge(ride_geo)

    ride = db.session.query(Ride).get(activity.id)
    new_ride = (ride is None)
    if ride is None:
        ride = Ride(activity.id)

    # Check to see if we need to pull down efforts for this ride
    if new_ride:
        ride.detail_fetched = False  # Just to be explicit

        if not activity.manual:
            ride.track_fetched = False

        # photo_count refers to instagram photos
        if activity.photo_count > 1:
            ride.photos_fetched = False
        else:
            ride.photos_fetched = None

    else:
        if round(ride.distance, 2) != round(float(unithelper.miles(activity.distance)), 2):
            log.info("Queing resync of details for activity {0!r}: distance mismatch ({1} != {2})".format(activity,
                                                                                                          ride.distance,
                                                                                                          unithelper.miles(activity.distance)))
            ride.detail_fetched = False
            ride.track_fetched = False

    ride.athlete = athlete


    update_ride_from_activity(strava_activity=activity, ride=ride)


    db.session.add(ride)

    return ride