Exemplo n.º 1
0
def parking_panda_welcome_email(uname, uemail):
    """
    Send a welcome email to users that have just signed up for Parking Panda features.
    """
    CONFIG = create_app().config
    c = boto.ses.connect_to_region("us-west-2",
        aws_access_key_id=CONFIG["AWS_ACCESS_KEY"],
        aws_secret_access_key=CONFIG["AWS_SECRET_KEY"])
    # don't bother with flask app context
    with open(os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates/welcome_parking_panda.html"), "r") as f:
        c.send_email("*****@*****.**", "Reserved Parking? You're on your way!",
            "", uemail, html_body=f.read().replace("{{ uname }}", uname))
Exemplo n.º 2
0
def push_deneigement_scheduled():
    """
    Push messages to users when snow removal is initially scheduled for their checkin location.
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    # grab the appropriate checkins to send pushes to by slot ID
    start = datetime.datetime.now()
    finish = start - datetime.timedelta(minutes=5)
    res = db.query("""
        SELECT DISTINCT x.start, u.lang, u.sns_id
        FROM temporary_restrictions x
        JOIN checkins c ON c.slot_id = ANY(x.slot_ids)
        JOIN users u ON c.user_id = u.id
        WHERE (x.meta = '2' OR x.meta = '3') AND x.active = true AND x.type = 'snow'
            AND x.modified > '{}' AND x.modified < '{}'
            AND c.active = true AND c.checkout_time IS NULL
            AND u.push_on_temp = true AND u.sns_id IS NOT NULL
            AND c.checkin_time > (NOW() - INTERVAL '14 DAYS')
    """.format(finish.strftime('%Y-%m-%d %H:%M:%S'),
               start.strftime('%Y-%m-%d %H:%M:%S')))

    # group device IDs by start time, then send messages
    lang_en, lang_fr = filter(lambda x: x[1] == 'en',
                              res), filter(lambda x: x[1] == 'fr', res)
    data = {
        "en": {x: []
               for x in set([z[0].isoformat() for z in lang_en])},
        "fr": {x: []
               for x in set([z[0].isoformat() for z in lang_fr])}
    }
    for x in lang_en:
        data["en"][x[0].isoformat()].append(x[2])
    for x in lang_fr:
        data["fr"][x[0].isoformat()].append(x[2])
    for x in data["en"].keys():
        dt = format_datetime(aniso8601.parse_datetime(x),
                             u"h:mm a 'on' EEEE d MMM")
        notifications.schedule_notifications(
            data["en"][x],
            "❄️ Snow removal scheduled! Move your car before {}".format(dt))
    for x in data["fr"].keys():
        dt = format_datetime(aniso8601.parse_datetime(x),
                             u"H'h'mm', 'EEEE 'le 'd MMM",
                             locale='fr_FR')
        notifications.schedule_notifications(
            data["fr"][x],
            "❄️ Déneigement annoncé ! Déplacez votre véhicule avant {}".format(
                dt))
Exemplo n.º 3
0
def push_deneigement_8hr():
    """
    Push messages to users when the snow removal period for their checkin location is exactly eight hours away
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    # grab the appropriate checkins to send pushes to by slot ID
    start = (datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(
        pytz.timezone('US/Eastern')) + datetime.timedelta(hours=8))
    finish = start - datetime.timedelta(minutes=5)
    res = db.query("""
        SELECT DISTINCT x.start, u.lang, u.sns_id
        FROM temporary_restrictions x
        JOIN checkins c ON c.slot_id = ANY(x.slot_ids)
        JOIN users u ON c.user_id = u.id
        WHERE (x.meta = '2' OR x.meta = '3') AND x.active = true AND x.type = 'snow'
            AND x.start > '{}' AND x.start < '{}'
            AND c.active = true AND c.checkout_time IS NULL
            AND u.push_on_temp = true AND u.sns_id IS NOT NULL
            AND c.checkin_time > (NOW() - INTERVAL '14 DAYS')
    """.format(finish.strftime('%Y-%m-%d %H:%M:%S'),
               start.strftime('%Y-%m-%d %H:%M:%S')))

    # group device IDs by start time, then send messages
    lang_en, lang_fr = filter(lambda x: x[1] == 'en',
                              res), filter(lambda x: x[1] == 'fr', res)
    data = {
        "en": {x: []
               for x in set([z[0].isoformat() for z in lang_en])},
        "fr": {x: []
               for x in set([z[0].isoformat() for z in lang_fr])}
    }
    for x in lang_en:
        data["en"][x[0].isoformat()].append(x[2])
    for x in lang_fr:
        data["fr"][x[0].isoformat()].append(x[2])
    for x in data["en"].keys():
        dt = format_datetime(aniso8601.parse_datetime(x), u"h:mm a")
        notifications.schedule_notifications(
            data["en"][x],
            u"❄️ Attention, snow removal starts in 8 hours, at {}!".format(dt))
    for x in data["fr"].keys():
        dt = format_datetime(aniso8601.parse_datetime(x),
                             u"H'h'mm",
                             locale='fr_FR')
        notifications.schedule_notifications(
            data["fr"][x],
            u"❄️ Attention, le déneigement commence dans 8h, à {} !".format(
                dt))
Exemplo n.º 4
0
def update_analytics():
    """
    Task to push analytics submissions from Redis to DB
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))
    r = Redis(db=1)

    data = r.lrange('prkng:analytics:pos', 0, -1)
    r.delete('prkng:analytics:pos')

    # insert Map Location Data (positions)
    values = ["({}, {}, {}, '{}'::timestamp, '{}')".format(x["user_id"], x["lat"], x["long"],
        x["created"], x["search_type"]) for x in map(lambda y: json.loads(y), data)]
    if values:
        # create MultiPoint with positions that occur within five-minute increments
        pos_query = """
            WITH tmp AS (
                SELECT
                    user_id,
                    search_type,
                    count(*),
                    date_trunc('hour', created) AS hour_stump,
                    (extract(minute FROM created)::int / 5) AS min_by5,
                    ST_Collect(ST_Transform(ST_SetSRID(ST_MakePoint(long, lat), 4326), 3857)) AS geom
                FROM (VALUES {}) AS d(user_id, lat, long, created, search_type)
                GROUP BY 1, 2, 4, 5
                ORDER BY 1, 2, 4, 5
            )
            INSERT INTO analytics_pos (user_id, geom, centerpoint, count, created, search_type)
                SELECT user_id, geom, ST_Centroid(geom), count, hour_stump + (INTERVAL '5 MINUTES' * min_by5),
                    search_type
                FROM tmp
        """.format(",".join(values))
        db.query(pos_query)

    data = r.lrange('prkng:analytics:event', 0, -1)
    r.delete('prkng:analytics:event')

    # insert buffered Event data
    if data:
        event_query = "INSERT INTO analytics_event (user_id, lat, long, created, event) VALUES "
        event_query += ",".join(["({}, {}, {}, '{}', '{}')".format(x["user_id"], x["lat"] or "NULL",
            x["long"] or "NULL", x["created"], x["event"]) for x in map(lambda y: json.loads(y), data)])
        db.query(event_query)
Exemplo n.º 5
0
def update_seattle_lots():
    """
    Fetch Seattle parking lot data and real-time availability from City of Seattle GIS
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    # grab data from city of seattle DOT
    data = requests.get("http://web6.seattle.gov/sdot/wsvcEparkGarageOccupancy/Occupancy.asmx/GetGarageList",
        params={"prmGarageID": "G", "prmMyCallbackFunctionName": ""})
    data = json.loads(data.text.lstrip("(").rstrip(");"))

    if data:
        db.query("""
            UPDATE parking_lots l SET available = d.available
            FROM (VALUES {}) AS d(pid, available)
            WHERE l.partner_name = 'Seattle ePark'
                AND l.partner_id = d.pid
        """.format(",".join(["('{}',{})".format(x["Id"], x["VacantSpaces"]) for x in data])))
Exemplo n.º 6
0
def update_free_spaces():
    """
    Task to check recently departed carshare spaces and record
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    start = datetime.datetime.now()
    finish = start - datetime.timedelta(minutes=5)

    db.query("""
        INSERT INTO free_spaces (slot_ids)
          SELECT array_agg(s.id) FROM slots s
            JOIN carshares c ON c.slot_id = s.id
            WHERE c.lot_id IS NULL
              AND c.parked = false
              AND c.since  > '{}'
              AND c.since  < '{}'
    """.format(finish.strftime('%Y-%m-%d %H:%M:%S'),
               start.strftime('%Y-%m-%d %H:%M:%S')))
Exemplo n.º 7
0
def run_backup():
    """
    Backs up our local database to an encrypted bucket on Amazon S3.

    :returns: Path to database backup in S3 (str)
    """
    CONFIG = create_app().config
    file_name = 'prkng-{}.sql.gz'.format(datetime.datetime.now().strftime('%Y%m%d-%H%M%S'))
    c = S3Connection(CONFIG["AWS_ACCESS_KEY"], CONFIG["AWS_SECRET_KEY"])

    # dump the DB to compressed temporary file
    subprocess.check_call('pg_dump -c -U {PG_USERNAME} {PG_DATABASE} | gzip > {file_name}'.format(
        file_name=os.path.join('/tmp', file_name), **CONFIG),
        shell=True)

    # upload and encrypt database dump and remove the temp file
    b = c.get_bucket('prkng-bak')
    k = b.initiate_multipart_upload(file_name, encrypt_key=True)
    with open(os.path.join('/tmp', file_name), 'rb') as f:
        k.upload_part_from_file(f, 1)
    k.complete_upload()
    os.unlink(os.path.join('/tmp', file_name))
    return os.path.join('prkng-bak/', file_name)
Exemplo n.º 8
0
def hello_amazon():
    """
    Fetch newly-registered users' device IDs and register with Amazon SNS for push notifications.
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))
    r = Redis(db=1)
    amz = boto.sns.connect_to_region("us-west-2",
        aws_access_key_id=CONFIG["AWS_ACCESS_KEY"],
        aws_secret_access_key=CONFIG["AWS_SECRET_KEY"])
    values = []

    # register the user's device ID with Amazon, and add to the associated notification topics
    for d in ["ios", "ios-sbx", "android"]:
        for x in r.hkeys('prkng:hello-amazon:'+d):
            try:
                # create SNS platform endpoint with saved user device ID, and save endpoint to DB
                device_id = r.hget('prkng:hello-amazon:'+d, x)
                arn = amz.create_platform_endpoint(CONFIG["AWS_SNS_APPS"][d], device_id, x.encode('utf-8'))
                arn = arn['CreatePlatformEndpointResponse']['CreatePlatformEndpointResult']['EndpointArn']
                values.append("({},'{}')".format(x, arn))
                r.hdel('prkng:hello-amazon:'+d, x)
                if not CONFIG["DEBUG"]:
                    # add the user to associated mass-push topics
                    amz.subscribe(CONFIG["AWS_SNS_TOPICS"]["all_users"], "application", arn)
                    amz.subscribe(CONFIG["AWS_SNS_TOPICS"][d+"_users"], "application", arn)
            except Exception, e:
                # if the token already exists, grab and save the existing one instead
                if "already exists with the same Token" in e.message:
                    arn = re.search("Endpoint (arn:aws:sns\S*)\s.?", e.message)
                    if not arn:
                        continue
                    values.append("({},'{}')".format(x, arn.group(1)))
                    r.hdel('prkng:hello-amazon:'+d, x)
Exemplo n.º 9
0
def app(request):
    app = create_app(env='Testing')

    init_model(app)
    init_api(app)
    init_login(app)

    # Establish an application context before running the tests.
    ctx = app.app_context()
    ctx.push()

    def teardown():
        metadata.drop_all()
        db.engine.execute("drop table if exists slots")
        ctx.pop()

    request.addfinalizer(teardown)

    # hack to cause a new request context to be pushed and have a current user logged in
    @app.route('/test_auto_login')
    def auto_login():
        User.get(1)
        return "ok"

    # create slots table
    db.engine.execute("""
        CREATE TABLE IF NOT EXISTS slots
        (
          id serial PRIMARY KEY,
          city varchar,
          rid integer,
          signposts integer[],
          rules jsonb,
          way_name varchar,
          geom geometry(LineString,3857),
          geojson jsonb,
          button_location jsonb,
          button_locations jsonb
        )
    """)

    with app.test_client() as client:
        # add a user in order to honor foreign key of checkins
        g.user = User.add_user(email='test@test', name='test_user')
        client.get("/test_auto_login")

    db.engine.execute("""
        DROP TABLE IF EXISTS cities;
        CREATE TABLE cities AS
            SELECT 'montreal' AS name,
             'POLYGON((4.28302949061661931 42.34791957104560112,
                4.28302949061661931 44.39724932975872917,
                1.43423592493298013 44.44228954423594047,
                1.31037533512064774 42.483040214477235,
                4.28302949061661931 42.34791957104560112))'::GEOMETRY AS geom
    """)

    # add some checkins
    db.engine.execute("truncate table checkins")
    for num in range(3):
        db.engine.execute("""
            INSERT INTO slots (geom)
            VALUES('SRID=3857;LINESTRING(2.765 42.988, 2.865 43.988)'::geometry)
            """.format(num))
        db.engine.execute("""
            INSERT INTO checkins (user_id, slot_id, long, lat)
            VALUES(1, {}, 2.765, 42.988)""".format(num))

    return app
Exemplo n.º 10
0
def update_zipcar():
    """
    Task to check with the Zipcar API and update parking lot data
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    lots, cars, vids = [], [], []
    raw = requests.get("https://api.zipcar.com/partner-api/directory",
                       params={
                           "country": "us",
                           "embed": "vehicles",
                           "apikey": CONFIG["ZIPCAR_KEY"]
                       })
    data = raw.json()["locations"]
    for x in data:
        if not x["address"]["city"] or not x["address"]["city"] \
                in ["Seattle", "New York", "Brooklyn", "Queens", "Staten Island",
                    "Boston", "Cambridge"]:
            continue
        city = x["address"]["city"].encode("utf-8").lower()
        if x["address"]["city"] in [
                "New York", "Brooklyn", "Queens", "Staten Island"
        ]:
            city = "newyork"
        if x["address"]["city"] in ["Boston", "Cambridge"]:
            city = "boston"
        lots.append(
            "('{}','{}','{}',{},'SRID=4326;POINT({} {})'::geometry)".format(
                x["location_id"], city,
                x["display_name"].replace("'", "''").encode("utf-8"),
                len(x["vehicles"]), x["coordinates"]["lng"],
                x["coordinates"]["lat"]))
        for y in x["vehicles"]:
            cars.append(
                "('{}','{}','{}','{}','{}','SRID=4326;POINT({} {})'::geometry)"
                .format(
                    y["vehicle_id"],
                    y["vehicle_name"].replace("'", "''").encode("utf-8"), city,
                    x["address"]["street"].replace("'", "''").encode("utf-8"),
                    x["location_id"], x["coordinates"]["lng"],
                    x["coordinates"]["lat"]))
            vids.append(y["vehicle_id"])

    if lots:
        db.query("""
            UPDATE carshare_lots l SET name = d.name, capacity = d.capacity, available = d.capacity
            FROM (VALUES {}) AS d(pid, city, name, capacity, geom)
            WHERE l.company = 'zipcar'
                AND l.partner_id = d.pid
                AND (l.available != d.capacity OR l.capacity != d.capacity OR l.name != d.name)
        """.format(",".join(lots)))
        db.query("""
            INSERT INTO carshare_lots (company, partner_id, city, name, capacity, available, geom, geojson)
            SELECT 'zipcar', d.pid, d.city, d.name, d.capacity, d.capacity,
                    ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
            FROM (VALUES {}) AS d(pid, city, name, capacity, geom)
            WHERE (SELECT 1 FROM carshare_lots l WHERE l.city = d.city AND l.partner_id = d.pid LIMIT 1) IS NULL
        """.format(",".join(lots)))
    if cars:
        db.query("""
            INSERT INTO carshares (company, city, partner_id, name, address, lot_id, parked, geom, geojson)
                SELECT 'zipcar', d.city, d.pid, d.name, d.address, l.id, true,
                        ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                FROM (VALUES {}) AS d(pid, name, city, address, lot_pid, geom)
                JOIN carshare_lots l ON l.company = 'zipcar' AND l.city = d.city
                    AND l.partner_id = d.lot_pid
                WHERE (SELECT 1 FROM carshares c WHERE c.partner_id = d.pid LIMIT 1) IS NULL
        """.format(",".join(cars)))
    db.query("""
        DELETE FROM carshare_lots l
        WHERE l.company = 'zipcar'
            AND (SELECT 1 FROM (VALUES {}) AS d(pid) WHERE l.company = 'zipcar' AND l.partner_id = d.pid) IS NULL
    """.format(",".join(["('{}')".format(z["location_id"]) for z in data])))
    db.query("""
        DELETE FROM carshares l
        WHERE l.company = 'zipcar'
            AND (SELECT 1 FROM (VALUES {}) AS d(pid) WHERE l.company = 'zipcar' AND l.partner_id = d.pid) IS NULL
    """.format(",".join(["('{}')".format(z) for z in vids])))
Exemplo n.º 11
0
def update_car2go():
    """
    Task to check with the car2go API, find moved cars and update their positions/slots
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    for city in ["montreal", "newyork", "seattle"]:
        # grab data from car2go api
        c2city = city
        if c2city == "newyork":
            c2city = "newyorkcity"
        raw = requests.get("https://www.car2go.com/api/v2.1/vehicles",
                           params={
                               "loc": c2city,
                               "format": "json",
                               "oauth_consumer_key": CONFIG["CAR2GO_CONSUMER"]
                           })
        data = raw.json()["placemarks"]

        raw = requests.get("https://www.car2go.com/api/v2.1/parkingspots",
                           params={
                               "loc": c2city,
                               "format": "json",
                               "oauth_consumer_key": CONFIG["CAR2GO_CONSUMER"]
                           })
        lot_data = raw.json()["placemarks"]

        # create or update car2go parking lots
        values = [
            "('{}','{}',{},{})".format(
                city, x["name"].replace("'", "''").encode("utf-8"),
                x["totalCapacity"], (x["totalCapacity"] - x["usedCapacity"]))
            for x in lot_data
        ]
        if values:
            db.query("""
                UPDATE carshare_lots l SET capacity = d.capacity, available = d.available
                FROM (VALUES {}) AS d(city, name, capacity, available)
                WHERE l.company = 'car2go' AND l.city = d.city AND l.name = d.name
                    AND l.available != d.available
            """.format(",".join(values)))

        values = [
            "('{}','{}',{},{},'SRID=4326;POINT({} {})'::geometry)".format(
                city, x["name"].replace("'", "''").encode("utf-8"),
                x["totalCapacity"], (x["totalCapacity"] - x["usedCapacity"]),
                x["coordinates"][0], x["coordinates"][1]) for x in lot_data
        ]
        if values:
            db.query("""
                INSERT INTO carshare_lots (company, city, name, capacity, available, geom, geojson)
                    SELECT 'car2go', d.city, d.name, d.capacity, d.available,
                            ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                    FROM (VALUES {}) AS d(city, name, capacity, available, geom)
                    WHERE (SELECT 1 FROM carshare_lots l WHERE l.city = d.city AND l.name = d.name LIMIT 1) IS NULL
            """.format(",".join(values)))

        # unpark stale entries in our database
        db.query("""
            UPDATE carshares c SET since = NOW(), parked = false
            WHERE c.company = 'car2go'
                AND c.city = '{city}'
                AND c.parked = true
                AND (SELECT 1 FROM (VALUES {data}) AS d(pid) WHERE c.vin = d.pid LIMIT 1) IS NULL
        """.format(city=city,
                   data=",".join(["('{}')".format(x["vin"]) for x in data])))

        # create or update car2go tracking with new data
        values = [
            "('{}','{}','{}','{}',{},'SRID=4326;POINT({} {})'::geometry)".
            format(city, x["vin"], x["name"].encode('utf-8'),
                   x["address"].replace("'", "''").encode("utf-8"),
                   x.get("fuel", 0), x["coordinates"][0], x["coordinates"][1])
            for x in data
        ]
        db.query("""
            WITH tmp AS (
                SELECT DISTINCT ON (d.vin) d.vin, d.name, d.fuel, d.address, d.geom,
                    s.id AS slot_id, l.id AS lot_id
                FROM (VALUES {}) AS d(city, vin, name, address, fuel, geom)
                LEFT JOIN carshare_lots l ON d.city = l.city AND l.name = d.address
                LEFT JOIN slots s ON l.id IS NULL AND d.city = s.city
                    AND ST_DWithin(ST_Transform(d.geom, 3857), s.geom, 5)
                ORDER BY d.vin, ST_Distance(ST_Transform(d.geom, 3857), s.geom)
            )
            UPDATE carshares c SET since = NOW(), name = t.name, address = t.address,
                parked = true, slot_id = t.slot_id, lot_id = t.lot_id, fuel = t.fuel,
                geom = ST_Transform(t.geom, 3857), geojson = ST_AsGeoJSON(t.geom)::jsonb
            FROM tmp t
            WHERE c.company = 'car2go'
                AND c.vin = t.vin
                AND c.parked = false
        """.format(",".join(values)))
        db.query("""
            INSERT INTO carshares (company, city, vin, name, address, slot_id, lot_id, parked, fuel, geom, geojson)
                SELECT DISTINCT ON (d.vin) 'car2go', d.city, d.vin, d.name, d.address, s.id, l.id,
                    true, d.fuel, ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                FROM (VALUES {}) AS d(city, vin, name, address, fuel, geom)
                LEFT JOIN carshare_lots l ON d.city = l.city AND l.name = d.address
                LEFT JOIN slots s ON l.id IS NULL AND s.city = d.city
                    AND ST_DWithin(ST_Transform(d.geom, 3857), s.geom, 5)
                WHERE (SELECT 1 FROM carshares c WHERE c.vin = d.vin LIMIT 1) IS NULL
                ORDER BY d.vin, ST_Distance(ST_Transform(d.geom, 3857), s.geom)
        """.format(",".join(values)))
Exemplo n.º 12
0
def update_communauto():
    """
    Task to check with the Communuauto API, find moved cars and update their positions/slots
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    for city in ["montreal", "quebec"]:
        # grab data from communauto api
        if city == "montreal":
            cacity = 59
        elif city == "quebec":
            cacity = 90
        start = datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(
            pytz.timezone('US/Eastern'))
        finish = (start + datetime.timedelta(minutes=30))
        data = requests.post(
            "https://www.reservauto.net/Scripts/Client/Ajax/PublicCall/Get_Car_DisponibilityJSON.asp",
            data={
                "CityID": cacity,
                "StartDate": start.strftime("%d/%m/%Y %H:%M"),
                "EndDate": finish.strftime("%d/%m/%Y %H:%M"),
                "FeeType": 80
            })
        # must use demjson here because returning format is non-standard JSON
        try:
            data = demjson.decode(data.text.lstrip("(").rstrip(")"))["data"]
        except:
            return

        # create or update communauto parking spaces
        values = [
            "('{}',{})".format(x["StationID"], (1 if x["NbrRes"] == 0 else 0))
            for x in data
        ]
        db.query("""
            UPDATE carshare_lots l SET capacity = 1, available = d.available
            FROM (VALUES {}) AS d(pid, available)
            WHERE l.company = 'communauto'
                AND l.partner_id = d.pid
                AND l.available != d.available
        """.format(",".join(values)))

        values = [
            "('{}','{}',{},'{}','SRID=4326;POINT({} {})'::geometry)".format(
                city, x["strNomStation"].replace("'", "''").encode("utf-8"),
                (1 if x["NbrRes"] == 0 else 0), x["StationID"], x["Longitude"],
                x["Latitude"]) for x in data
        ]
        db.query("""
            INSERT INTO carshare_lots (company, city, name, capacity, available, partner_id, geom, geojson)
                SELECT 'communauto', d.city, d.name, 1, d.available, d.partner_id,
                        ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                FROM (VALUES {}) AS d(city, name, available, partner_id, geom)
                WHERE (SELECT 1 FROM carshare_lots l WHERE l.partner_id = d.partner_id LIMIT 1) IS NULL
        """.format(",".join(values)))

        # unpark stale entries in our database
        db.query("""
            UPDATE carshares c SET since = NOW(), parked = false
            FROM (VALUES {data}) AS d(pid, lot_id, numres)
            WHERE c.parked = true
                AND c.city = '{city}'
                AND d.numres = 1
                AND c.company = 'communauto'
                AND c.partner_id = d.pid;

            UPDATE carshares c SET since = NOW(), parked = false
            WHERE c.parked = true
                AND c.company = 'communauto'
                AND c.city = '{city}'
                AND (SELECT 1 FROM (VALUES {data}) AS d(pid, lot_id, numres) WHERE d.pid != c.partner_id
                     AND d.lot_id = c.lot_id LIMIT 1) IS NOT NULL
        """.format(city=city,
                   data=",".join([
                       "('{}',{},{})".format(x["CarID"], x["StationID"],
                                             x["NbrRes"]) for x in data
                   ])))

        # create or update communauto tracking with newly parked vehicles
        values = [
            "('{}',{},'{}','{}','{}'::timestamp,'SRID=4326;POINT({} {})'::geometry)"
            .format(x["CarID"], x["NbrRes"], x["Model"].encode("utf-8"),
                    x["strNomStation"].replace("'", "''").encode("utf-8"),
                    x["AvailableUntilDate"] or "NOW", x["Longitude"],
                    x["Latitude"]) for x in data
        ]
        db.query("""
            UPDATE carshares c SET since = NOW(), until = d.until, name = d.name, address = d.address,
                parked = true, geom = ST_Transform(d.geom, 3857), geojson = ST_AsGeoJSON(d.geom)::jsonb
            FROM (VALUES {}) AS d(pid, numres, name, address, until, geom)
            WHERE c.company = 'communauto'
                AND c.partner_id = d.pid
                AND d.numres = 0
        """.format(",".join(values)))

        values = [
            "('{}','{}','{}','{}','{}',{},'{}'::timestamp,'SRID=4326;POINT({} {})'::geometry)"
            .format(city, x["StationID"], x["CarID"],
                    x["Model"].encode("utf-8"),
                    x["strNomStation"].replace("'", "''").encode("utf-8"),
                    x["NbrRes"], x["AvailableUntilDate"] or "NOW",
                    x["Longitude"], x["Latitude"]) for x in data
        ]
        db.query("""
            INSERT INTO carshares (company, city, partner_id, name, address, lot_id, parked, until, geom, geojson)
                SELECT 'communauto', d.city, d.partner_id, d.name, d.address, l.id, d.numres = 0,
                        d.until, ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                FROM (VALUES {}) AS d(city, lot_pid, partner_id, name, address, numres, until, geom)
                JOIN carshare_lots l ON l.company = 'communauto' AND l.city = d.city
                    AND l.partner_id = d.lot_pid
                WHERE (SELECT 1 FROM carshares c WHERE c.partner_id = d.partner_id LIMIT 1) IS NULL
        """.format(",".join(values)))
Exemplo n.º 13
0
def update_automobile():
    """
    Task to check with the Auto-mobile API, find moved cars and update their positions/slots
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    # grab data from Auto-mobile api
    data = requests.get(
        "https://www.reservauto.net/WCF/LSI/LSIBookingService.asmx/GetVehicleProposals",
        params={
            "Longitude": "-73.56307727766432",
            "Latitude": "45.48420949674474",
            "CustomerID": '""'
        })
    data = demjson.decode(data.text.lstrip("(").rstrip(");"))["Vehicules"]

    # unpark stale entries in our database
    if data:
        db.query("""
            UPDATE carshares c SET since = NOW(), parked = false
            WHERE c.company = 'auto-mobile'
                AND c.parked = true
                AND (SELECT 1 FROM (VALUES {data}) AS d(pid) WHERE c.vin = d.pid LIMIT 1) IS NULL
        """.format(data=",".join(["('{}')".format(x["Id"]) for x in data])))

        # create or update Auto-mobile tracking with newly parked vehicles
        values = [
            "('{}','{}',{},{},'{}','SRID=4326;POINT({} {})'::geometry)".format(
                x["Id"], x["Immat"].encode('utf-8'), x["EnergyLevel"],
                ("true" if x["Name"].endswith("-R") else "false"),
                x["Name"].encode('utf-8'), x["Position"]["Lon"],
                x["Position"]["Lat"]) for x in data
        ]
        db.query("""
            WITH tmp AS (
                SELECT DISTINCT ON (d.vin) d.vin, d.name, d.fuel, d.id, s.id AS slot_id, s.way_name, d.geom
                FROM (VALUES {}) AS d(vin, name, fuel, electric, id, geom)
                JOIN cities c ON ST_Intersects(ST_Transform(d.geom, 3857), c.geom)
                LEFT JOIN slots s ON s.city = c.name
                    AND ST_DWithin(ST_Transform(d.geom, 3857), s.geom, 5)
                ORDER BY d.vin, ST_Distance(ST_Transform(d.geom, 3857), s.geom)
            )
            UPDATE carshares c SET partner_id = t.id, since = NOW(), name = t.name, address = t.way_name,
                parked = true, slot_id = t.slot_id, fuel = t.fuel, geom = ST_Transform(t.geom, 3857),
                geojson = ST_AsGeoJSON(t.geom)::jsonb
            FROM tmp t
            WHERE c.company = 'auto-mobile'
                AND c.vin = t.vin
                AND c.parked = false
        """.format(",".join(values)))
        db.query("""
            INSERT INTO carshares (company, city, partner_id, vin, name, address, slot_id, parked, fuel, electric, geom, geojson)
                SELECT DISTINCT ON (d.vin) 'auto-mobile', c.name, d.id, d.vin, d.name, s.way_name, s.id,
                    true, d.fuel, d.electric, ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb
                FROM (VALUES {}) AS d(vin, name, fuel, electric, id, geom)
                JOIN cities c ON ST_Intersects(ST_Transform(d.geom, 3857), c.geom)
                LEFT JOIN slots s ON s.city = c.name
                    AND ST_DWithin(ST_Transform(d.geom, 3857), s.geom, 5)
                WHERE (SELECT 1 FROM carshares c WHERE c.vin = d.vin LIMIT 1) IS NULL
                ORDER BY d.vin, ST_Distance(ST_Transform(d.geom, 3857), s.geom)
        """.format(",".join(values)))
Exemplo n.º 14
0
def update_deneigement():
    """
    Task to check with Montreal Planif-Neige API and note snow-clearing operations
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))
    r = Redis(db=1)
    logfile = os.path.join(os.path.expanduser('~'), 'log', 'deneigement.log')
    if not CONFIG["DEBUG"]:
        logfile = '/home/parkng/log/deneigement.log'

    # get snow removal API changes that have occurred since our last known successful check
    now = int(time.time())
    since = r.get("prkng:snowdt")
    if since:
        since = datetime.datetime.fromtimestamp(
            int(since)).replace(tzinfo=pytz.utc)
    else:
        since = (datetime.datetime.utcnow().replace(tzinfo=pytz.utc) -
                 datetime.timedelta(minutes=30))
    with open(logfile, 'a') as f:
        f.write("Snow removal API check: {} ===\n".format(
            datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')))
    client = Client(
        "https://servicesenligne2.ville.montreal.qc.ca/api/infoneige/InfoneigeWebService?WSDL"
    )
    planification_request = client.factory.create('getPlanificationsForDate')
    planification_request.fromDate = since.astimezone(
        pytz.timezone('US/Eastern')).strftime('%Y-%m-%dT%H:%M:%S')
    planification_request.tokenString = CONFIG["PLANIFNEIGE_API_KEY"]
    response = client.service.GetPlanificationsForDate(planification_request)
    with open(logfile, 'a') as f:
        f.write(" > API contacted successfully.\n")

    if response['responseStatus'] == 8:
        # No new data
        r.set("prkng:snowdt", now)
        with open(logfile, 'a') as f:
            f.write(" > No new data.\n\n")
        return
    elif response['responseStatus'] != 0:
        # An error occurred
        with open(logfile, 'a') as f:
            f.write(" > CALL FAILED: code {}, message: {}\n\n".format(
                response['responseStatus'],
                response['responseDesc'].encode('utf-8')))
        raise Exception("Info-Neige call failed: code {}, message: {}".format(
            response['responseStatus'],
            response['responseDesc'].encode('utf-8')))
    r.set("prkng:snowdt", now)
    with open(logfile, 'a') as f:
        f.write(" > Contains {} changed objects.\n".format(
            len(response['planifications']['planification'])))

    db.query("""
        CREATE TABLE IF NOT EXISTS temporary_restrictions (
            id serial primary key,
            city varchar,
            partner_id varchar,
            slot_ids integer[],
            modified timestamp default NOW(),
            start timestamp,
            finish timestamp,
            type varchar,
            meta varchar,
            rule jsonb,
            active boolean
        )
    """)
    values, record = [], "({},'{}'::timestamp,'{}'::timestamp,{},'{}'::jsonb,{})"
    for x in response['planifications']['planification']:
        # if snow removal scheduled or rescheduled and we have a start time...
        if x['etatDeneig'] in [2, 3] and hasattr(x, 'dateDebutPlanif'):
            debut, fin = x['dateDebutPlanif'], x['dateFinPlanif']
            if hasattr(x, 'dateDebutReplanif'):
                debut, fin = x['dateDebutReplanif'], x['dateFinReplanif']
            # translate start/end times into a rule agenda object
            agenda = {str(z): [] for z in range(1, 8)}
            debutJour, finJour = debut.isoweekday(), fin.isoweekday()
            debutHeure = float(debut.hour) + (float(debut.minute) / 60.0)
            finHeure = float(fin.hour) + (float(fin.minute) / 60.0)
            if debutJour == finJour:
                agenda[str(debutJour)] = [[debutHeure, finHeure]]
            else:
                # split multi-day restrictions over the midnight divide
                agenda[str(debutJour)] = [[debutHeure, 24.0]]
                agenda[str(finJour)] = [[0.0, finHeure]]
                if (fin.day - debut.day) > 1:
                    if debutJour > finJour:
                        for z in range(debutJour, 8):
                            agenda[str(z)] = [[0.0, 24.0]]
                        for z in range(1, finJour + 1):
                            agenda[str(z)] = [[0.0, 24.0]]
                    else:
                        for z in range(debutJour + 1, finJour + 1):
                            agenda[str(z)] = [[0.0, 24.0]]
            # create the rule object with associated dates/times agenda
            rule = {
                "code": "MTL-NEIGE",
                "description": "DÉNEIGEMENT PRÉVU DANS CE SECTEUR",
                "periods": [],
                "agenda": agenda,
                "time_max_parking": None,
                "special_days": None,
                "restrict_types": ["snow"],
                "permit_no": None
            }
            values.append(
                record.format(x['coteRueId'],
                              debut.strftime('%Y-%m-%d %H:%M:%S'),
                              fin.strftime('%Y-%m-%d %H:%M:%S'), 'true',
                              json.dumps(rule), x['etatDeneig']))
        # if snow removal is done or unscheduled, make sure the restriction is deactivated
        elif x['etatDeneig'] in [0, 1, 4, 10]:
            values.append(
                record.format(
                    x['coteRueId'],
                    datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    'false', '{}', x['etatDeneig']))
    with open(logfile, 'a') as f:
        f.write(" > Parsed into {} values to update.\n".format(len(values)))

    if values:
        # update temporary restrictions item when we are already tracking the blockface
        db.query("""
            WITH tmp AS (
                SELECT x.*, g.name
                FROM (VALUES {}) AS x(geobase_id, start, finish, active, rule, state)
                JOIN montreal_geobase_double d ON x.geobase_id = d.cote_rue_i
                JOIN montreal_roads_geobase g ON d.id_trc = g.id_trc
            )
            UPDATE temporary_restrictions d SET start = x.start, finish = x.finish,
                active = x.active, rule = x.rule, modified = NOW(), meta = x.state::text
            FROM tmp x
            WHERE d.city = 'montreal' AND d.type = 'snow' AND x.geobase_id::text = d.partner_id
              AND (x.start != d.start OR x.finish != d.finish OR x.active != d.active
                OR x.state::text != d.meta)
        """.format(",".join(values)))
        with open(logfile, 'a') as f:
            f.write(" > Updated values.\n")

        # insert temporary restrictions for newly-mentioned blockfaces, and link with current slot IDs
        db.query("""
            WITH tmp AS (
                SELECT DISTINCT ON (d.cote_rue_i) d.cote_rue_i AS id,
                    array_agg(s.id) AS slot_ids
                FROM montreal_geobase_double d
                JOIN montreal_roads_geobase g ON d.id_trc = g.id_trc
                JOIN montreal_geobase r ON g.id_trc = r.id_trc
                JOIN slots s ON city = 'montreal' AND s.rid = g.id
                    AND ST_isLeft(ST_LineMerge(r.geom), ST_LineInterpolatePoint(ST_LineMerge(d.geom), 0.5))
                      = ST_isLeft(g.geom, ST_LineInterpolatePoint(s.geom, 0.5))
                WHERE ST_GeometryType(ST_LineMerge(d.geom)) = 'ST_LineString'
                GROUP BY d.cote_rue_i
            )
            INSERT INTO temporary_restrictions (city, partner_id, slot_ids, start, finish,
                    rule, type, active, meta)
                SELECT 'montreal', x.geobase_id::text, t.slot_ids, x.start, x.finish,
                    x.rule, 'snow', x.active, x.state::text
                FROM (VALUES {}) AS x(geobase_id, start, finish, active, rule, state)
                JOIN tmp t ON t.id = x.geobase_id
                WHERE (SELECT 1 FROM temporary_restrictions l WHERE l.type = 'snow'
                            AND l.partner_id = x.geobase_id::text LIMIT 1) IS NULL
        """.format(",".join(values)))
        with open(logfile, 'a') as f:
            f.write(" > Inserted values.\n\n")
Exemplo n.º 15
0
def send_notifications():
    """
    Send a push notification to specified user IDs via Amazon SNS
    """
    CONFIG = create_app().config
    r = Redis(db=1)
    amz = boto.sns.connect_to_region("us-west-2",
        aws_access_key_id=CONFIG["AWS_ACCESS_KEY"],
        aws_secret_access_key=CONFIG["AWS_SECRET_KEY"])

    keys = r.hkeys('prkng:push')
    if not keys:
        return

    # for each message to push...
    for pid in keys:
        message = r.hget('prkng:push', pid)
        r.hdel('prkng:push', pid)
        device_ids = r.lrange('prkng:push:'+pid, 0, -1)
        r.delete('prkng:push:'+pid)

        # if the message looks like a JSON, structure it accordingly
        message_structure = None
        if message.startswith("{") and message.endswith("}"):
            message_structure = "json"
        mg_title = "message-group-{}".format(datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
        mg_arn = None

        if device_ids == ["all"]:
            # Automatically publish messages destined for "all" via our All Users notification topic
            amz.publish(message=message, message_structure=message_structure,
                target_arn=CONFIG["AWS_SNS_TOPICS"]["all_users"])
        elif device_ids == ["ios"]:
            # Automatically publish messages destined for all iOS users
            amz.publish(message=message, message_structure=message_structure,
                target_arn=CONFIG["AWS_SNS_TOPICS"]["ios_users"])
        elif device_ids == ["android"]:
            # Automatically publish messages destined for all Android users
            amz.publish(message=message, message_structure=message_structure,
                target_arn=CONFIG["AWS_SNS_TOPICS"]["android_users"])
        elif device_ids == ["en"]:
            # Automatically publish messages destined for all English-language users
            amz.publish(message=message, message_structure=message_structure,
                target_arn=CONFIG["AWS_SNS_TOPICS"]["en_users"])
        elif device_ids == ["fr"]:
            # Automatically publish messages destined for all French-language users
            amz.publish(message=message, message_structure=message_structure,
                target_arn=CONFIG["AWS_SNS_TOPICS"]["fr_users"])

        if len(device_ids) >= 10:
            # If more than 10 real device IDs at once:
            for id in device_ids:
                if id.startswith("arn:aws:sns") and "endpoint" in id:
                    # this is a user device ID
                    # Create a temporary topic for a manually specified list of users
                    if not mg_arn:
                        mg_arn = amz.create_topic(mg_title)
                        mg_arn = mg_arn["CreateTopicResponse"]["CreateTopicResult"]["TopicArn"]
                    try:
                        amz.subscribe(mg_arn, "application", id)
                    except:
                        continue
                elif id.startswith("arn:aws:sns"):
                    # this must be a topic ARN, send to it immediately
                    amz.publish(message=message, message_structure=message_structure, target_arn=id)
            if mg_arn:
                # send to all user device IDs that we queued up in the prior loop
                amz.publish(message=message, message_structure=message_structure, target_arn=mg_arn)
        else:
            # Less than 10 device IDs or topic ARNs. Send to them immediately
            for id in [x for x in device_ids if x.startswith("arn:aws:sns")]:
                try:
                    amz.publish(message=message, message_structure=message_structure, target_arn=id)
                except BotoServerError:
                    continue
Exemplo n.º 16
0
# -*- coding: utf-8 -*-
"""
:author: [email protected]

This module contains the WSGI application used by Flask development server
and any production WSGI deployments
"""
from prkng import create_app
from prkng.api.admin import init_admin
from prkng.api.explorer import init_explorer
from prkng.api.public import init_api, v0, v1
from prkng.api.partners.car2go import init_car2go
from prkng.api.partners.communauto import init_communauto
from prkng.logger import Logger
from prkng.login import init_login
from prkng.models import init_model

app = create_app()
init_model(app)
init_api(app)
init_login(app)
init_admin(app)
init_car2go(app)
init_communauto(app)
init_explorer(app)

Logger.debug(app.config)
Exemplo n.º 17
0
def db(scope="module"):
    CONFIG = create_app(env='Testing').config
    return PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))
Exemplo n.º 18
0
def update_parkingpanda():
    """
    Task to check with the Parking Panda API, update data on associated parking lots
    """
    CONFIG = create_app().config
    db = PostgresWrapper(
        "host='{PG_HOST}' port={PG_PORT} dbname={PG_DATABASE} "
        "user={PG_USERNAME} password={PG_PASSWORD} ".format(**CONFIG))

    parkingpanda_url = "https://www.parkingpanda.com/api/v2/locations" if not CONFIG["DEBUG"] else "http://dev.parkingpanda.com/api/v2/locations"

    # for each city we serve...
    for city, addr in [("boston", "Faneuil Hall, Boston, MA"),
            ("newyork", "4 Pennsylvania Plaza, New York, NY")]:
        # grab data from parkingpanda api
        start = datetime.datetime.utcnow().replace(tzinfo=pytz.utc).astimezone(pytz.timezone('US/Eastern'))
        finish = (start + datetime.timedelta(hours=23, minutes=59))
        data = requests.get(parkingpanda_url, params={"search": addr, "miles": 20.0,
                "startDate": start.strftime("%m/%d/%Y"), "startTime": start.strftime("%H:%M"),
                "endDate": finish.strftime("%m/%d/%Y"), "endTime": finish.strftime("%H:%M"),
                "onlyavailable": False, "showSoldOut": True, "peer": False})
        data = data.json()["data"]["locations"]

        # converts times like "8:00AM" to float-based times like 8.0
        hourToFloat = lambda x: float(x.split(":")[0]) + (float(x.split(":")[1][0:2]) / 60) + (12 if "PM" in x and x.split(":")[0] != "12" else 0)

        values = []
        # for each lot received from their API...
        for x in data:
            x["displayName"] = x["displayName"].replace("'","''").encode("utf-8")
            x["displayAddress"] = x["displayAddress"].replace("'","''").encode("utf-8")
            x["description"] = x["description"].replace("'","''").encode("utf-8")
            basic = "('{}','{}','{}',{},{},'{}','{}','SRID=4326;POINT({} {})'::geometry,'{}'::jsonb,'{}'::jsonb)"
            # if it's open 24/7, give it a standard all-open agenda
            if x["isOpen247"]:
                agenda = {str(y): [{"max": None, "hourly": None, "daily": x["price"],
                    "hours": [0.0,24.0]}] for y in range(1,8)}
            else:
                # create an agenda based on opening days/times as reported, with given price
                agenda = {str(y): [] for y in range(1,8)}
                for y in x["hoursOfOperation"]:
                    if not y["isOpen"]:
                        continue
                    hours = [hourToFloat(y["timeOfDayOpen"]), hourToFloat(y["timeOfDayClose"])]
                    if y["timeOfDayClose"] == "11:59 PM":
                        hours[1] = 24.0
                    # if the closing time is the next day, handle the time overlap between days
                    if hours != [0.0, 24.0] and hours[0] > hours[1]:
                        nextday = str(y["dayOfWeek"]+2) if (y["dayOfWeek"] < 6) else "1"
                        agenda[nextday].append({"max": None, "hourly": None,
                            "daily": x["price"], "hours": [0.0, hours[1]]})
                        hours = [hours[0], 24.0]
                    agenda[str(y["dayOfWeek"]+1)].append({"max": None, "hourly": None,
                        "daily": x["price"], "hours": hours})
            # Create "closed" rules for periods not covered by an open rule
            for j in agenda:
                hours = sorted([y["hours"] for y in agenda[j]], key=lambda z: z[0])
                for i, y in enumerate(hours):
                    starts = [z[0] for z in hours]
                    if y[0] == 0.0:
                        continue
                    last_end = hours[i-1][1] if not i == 0 else 0.0
                    next_start = hours[i+1][0] if not i == (len(hours) - 1) else 24.0
                    if not last_end in starts:
                        agenda[j].append({"hours": [last_end, y[0]], "hourly": None, "max": None,
                            "daily": None})
                    if not next_start in starts and y[1] != 24.0:
                        agenda[j].append({"hours": [y[1], next_start], "hourly": None, "max": None,
                            "daily": None})
                if agenda[j] == []:
                    agenda[j].append({"hours": [0.0,24.0], "hourly": None, "max": None, "daily": None})
            attrs = {"card": True, "indoor": "covered" in [y["name"] for y in x["amenities"]],
                "handicap": "accessible" in [y["name"] for y in x["amenities"]],
                "valet": "valet" in [y["name"] for y in x["amenities"]]}
            values.append(basic.format(x["id"], city, x["displayName"], json.dumps(x["isLive"]),
                x["availableSpaces"], x["displayAddress"], x["description"],
                x["longitude"], x["latitude"], json.dumps(agenda), json.dumps(attrs)))

        # persist the new lots or updated ones to the database
        if values:
            db.query("""
                UPDATE parking_lots l SET available = d.available, agenda = d.agenda, attrs = d.attrs,
                    active = d.active
                FROM (VALUES {}) AS d(pid, city, name, active, available, address, description,
                    geom, agenda, attrs)
                WHERE l.partner_name = 'Parking Panda'
                    AND l.partner_id = d.pid
            """.format(",".join(values)))
            db.query("""
                INSERT INTO parking_lots (partner_id, partner_name, city, name, active,
                    available, address, description, geom, geojson, agenda, attrs, street_view)
                SELECT d.pid, 'Parking Panda', d.city, d.name, d.active, d.available, d.address,
                    d.description, ST_Transform(d.geom, 3857), ST_AsGeoJSON(d.geom)::jsonb,
                    d.agenda, d.attrs, json_build_object('head', p.street_view_head, 'id', p.street_view_id)::jsonb
                FROM (VALUES {}) AS d(pid, city, name, active, available, address, description,
                    geom, agenda, attrs)
                LEFT JOIN parking_lots_streetview p ON p.partner_name = 'Parking Panda' AND p.partner_id = d.pid
                WHERE (SELECT 1 FROM parking_lots l WHERE l.partner_id = d.pid LIMIT 1) IS NULL
            """.format(",".join(values)))