Advertisement
Guest User

Untitled

a guest
Dec 17th, 2016
111
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 89.80 KB | None | 0 0
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import logging
  5. import itertools
  6. import calendar
  7. import sys
  8. import traceback
  9. import gc
  10. import time
  11. import geopy
  12. import math
  13. import cluster
  14.  
  15.  
  16. from peewee import SqliteDatabase, InsertQuery, \
  17.     Check, CompositeKey, \
  18.     IntegerField, CharField, DoubleField, BooleanField, \
  19.     DateTimeField, fn, DeleteQuery, FloatField, SQL, TextField, JOIN
  20. from playhouse.flask_utils import FlaskDB
  21. from playhouse.pool import PooledMySQLDatabase
  22. from playhouse.shortcuts import RetryOperationalError
  23. from playhouse.migrate import migrate, MySQLMigrator, SqliteMigrator
  24. from datetime import datetime, timedelta
  25. from base64 import b64encode
  26. from cachetools import TTLCache
  27. from cachetools import cached
  28. from math import asin, atan, cos, exp, log, pi, sin, sqrt, tan
  29.  
  30. from . import config
  31. from .utils import get_pokemon_name, get_pokemon_rarity, get_pokemon_types, get_args, \
  32.     cellid, in_radius, date_secs, clock_between, secs_between, get_move_name, get_move_damage, \
  33.     get_move_energy, get_move_type
  34. from .transform import transform_from_wgs_to_gcj, get_new_coords
  35. from .customLog import printPokemon
  36. log = logging.getLogger(__name__)
  37.  
  38. args = get_args()
  39. flaskDb = FlaskDB()
  40. cache = TTLCache(maxsize=100, ttl=60 * 5)
  41.  
  42. db_schema_version = 11
  43.  
  44.  
  45. class MyRetryDB(RetryOperationalError, PooledMySQLDatabase):
  46.     pass
  47.  
  48.  
  49. def init_database(app):
  50.     if args.db_type == 'mysql':
  51.         log.info('Connecting to MySQL database on %s:%i', args.db_host, args.db_port)
  52.         connections = args.db_max_connections
  53.         if hasattr(args, 'accounts'):
  54.             connections *= len(args.accounts)
  55.         db = MyRetryDB(
  56.             args.db_name,
  57.             user=args.db_user,
  58.             password=args.db_pass,
  59.             host=args.db_host,
  60.             port=args.db_port,
  61.             max_connections=connections,
  62.             stale_timeout=300)
  63.     else:
  64.         log.info('Connecting to local SQLite database')
  65.         db = SqliteDatabase(args.db)
  66.  
  67.     app.config['DATABASE'] = db
  68.     flaskDb.init_app(app)
  69.  
  70.     return db
  71.  
  72.  
  73. class BaseModel(flaskDb.Model):
  74.  
  75.     @classmethod
  76.     def get_all(cls):
  77.         results = [m for m in cls.select().dicts()]
  78.         if args.china:
  79.             for result in results:
  80.                 result['latitude'], result['longitude'] = \
  81.                     transform_from_wgs_to_gcj(
  82.                         result['latitude'], result['longitude'])
  83.         return results
  84.  
  85.  
  86. class Pokemon(BaseModel):
  87.     # We are base64 encoding the ids delivered by the api,
  88.     # because they are too big for sqlite to handle.
  89.     encounter_id = CharField(primary_key=True, max_length=50)
  90.     spawnpoint_id = CharField(index=True)
  91.     pokemon_id = IntegerField(index=True)
  92.     latitude = DoubleField()
  93.     longitude = DoubleField()
  94.     disappear_time = DateTimeField(index=True)
  95.     individual_attack = IntegerField(null=True)
  96.     individual_defense = IntegerField(null=True)
  97.     individual_stamina = IntegerField(null=True)
  98.     move_1 = IntegerField(null=True)
  99.     move_2 = IntegerField(null=True)
  100.     last_modified = DateTimeField(null=True, index=True, default=datetime.utcnow)
  101.  
  102.     class Meta:
  103.         indexes = ((('latitude', 'longitude'), False),)
  104.  
  105.     @staticmethod
  106.     def get_active(swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, oSwLng=None, oNeLat=None, oNeLng=None):
  107.         now_date = datetime.utcnow()
  108.         # now_secs = date_secs(now_date)
  109.         query = Pokemon.select()
  110.         if not (swLat and swLng and neLat and neLng):
  111.             query = (query
  112.                      .where(Pokemon.disappear_time > now_date)
  113.                      .dicts())
  114.         elif timestamp > 0:
  115.             # If timestamp is known only load modified pokemon.
  116.             query = (query
  117.                      .where(((Pokemon.last_modified > datetime.utcfromtimestamp(timestamp / 1000)) &
  118.                              (Pokemon.disappear_time > now_date)) &
  119.                             ((Pokemon.latitude >= swLat) &
  120.                              (Pokemon.longitude >= swLng) &
  121.                              (Pokemon.latitude <= neLat) &
  122.                              (Pokemon.longitude <= neLng)))
  123.                      .dicts())
  124.         elif oSwLat and oSwLng and oNeLat and oNeLng:
  125.             # Send Pokemon in view but exclude those within old boundaries. Only send newly uncovered Pokemon.
  126.             query = (query
  127.                      .where(((Pokemon.disappear_time > now_date) &
  128.                             (((Pokemon.latitude >= swLat) &
  129.                               (Pokemon.longitude >= swLng) &
  130.                               (Pokemon.latitude <= neLat) &
  131.                               (Pokemon.longitude <= neLng))) &
  132.                             ~((Pokemon.disappear_time > now_date) &
  133.                               (Pokemon.latitude >= oSwLat) &
  134.                               (Pokemon.longitude >= oSwLng) &
  135.                               (Pokemon.latitude <= oNeLat) &
  136.                               (Pokemon.longitude <= oNeLng))))
  137.                      .dicts())
  138.         else:
  139.             query = (Pokemon
  140.                      .select()
  141.                      # add 1 hour buffer to include spawnpoints that persist after tth, like shsh
  142.                      .where((Pokemon.disappear_time > now_date) &
  143.                             (((Pokemon.latitude >= swLat) &
  144.                               (Pokemon.longitude >= swLng) &
  145.                               (Pokemon.latitude <= neLat) &
  146.                               (Pokemon.longitude <= neLng))))
  147.                      .dicts())
  148.  
  149.         # Performance: Disable the garbage collector prior to creating a (potentially) large dict with append().
  150.         gc.disable()
  151.  
  152.         pokemons = []
  153.         for p in list(query):
  154.  
  155.             p['pokemon_name'] = get_pokemon_name(p['pokemon_id'])
  156.             p['pokemon_rarity'] = get_pokemon_rarity(p['pokemon_id'])
  157.             p['pokemon_types'] = get_pokemon_types(p['pokemon_id'])
  158.             if args.china:
  159.                 p['latitude'], p['longitude'] = \
  160.                     transform_from_wgs_to_gcj(p['latitude'], p['longitude'])
  161.             pokemons.append(p)
  162.  
  163.         # Re-enable the GC.
  164.         gc.enable()
  165.  
  166.         return pokemons
  167.  
  168.     @staticmethod
  169.     def get_active_by_id(ids, swLat, swLng, neLat, neLng):
  170.         if not (swLat and swLng and neLat and neLng):
  171.             query = (Pokemon
  172.                      .select()
  173.                      .where((Pokemon.pokemon_id << ids) &
  174.                             (Pokemon.disappear_time > datetime.utcnow()))
  175.                      .dicts())
  176.         else:
  177.             query = (Pokemon
  178.                      .select()
  179.                      .where((Pokemon.pokemon_id << ids) &
  180.                             (Pokemon.disappear_time > datetime.utcnow()) &
  181.                             (Pokemon.latitude >= swLat) &
  182.                             (Pokemon.longitude >= swLng) &
  183.                             (Pokemon.latitude <= neLat) &
  184.                             (Pokemon.longitude <= neLng))
  185.                      .dicts())
  186.  
  187.         # Performance: Disable the garbage collector prior to creating a (potentially) large dict with append().
  188.         gc.disable()
  189.  
  190.         pokemons = []
  191.         for p in query:
  192.             p['pokemon_name'] = get_pokemon_name(p['pokemon_id'])
  193.             p['pokemon_rarity'] = get_pokemon_rarity(p['pokemon_id'])
  194.             p['pokemon_types'] = get_pokemon_types(p['pokemon_id'])
  195.             if args.china:
  196.                 p['latitude'], p['longitude'] = \
  197.                     transform_from_wgs_to_gcj(p['latitude'], p['longitude'])
  198.             pokemons.append(p)
  199.  
  200.         # Re-enable the GC.
  201.         gc.enable()
  202.  
  203.         return pokemons
  204.  
  205.     @classmethod
  206.     @cached(cache)
  207.     def get_seen(cls, timediff):
  208.         if timediff:
  209.             timediff = datetime.utcnow() - timediff
  210.         pokemon_count_query = (Pokemon
  211.                                .select(Pokemon.pokemon_id,
  212.                                        fn.COUNT(Pokemon.pokemon_id).alias('count'),
  213.                                        fn.MAX(Pokemon.disappear_time).alias('lastappeared')
  214.                                        )
  215.                                .where(Pokemon.disappear_time > timediff)
  216.                                .group_by(Pokemon.pokemon_id)
  217.                                .alias('counttable')
  218.                                )
  219.         query = (Pokemon
  220.                  .select(Pokemon.pokemon_id,
  221.                          Pokemon.disappear_time,
  222.                          Pokemon.latitude,
  223.                          Pokemon.longitude,
  224.                          pokemon_count_query.c.count)
  225.                  .join(pokemon_count_query, on=(Pokemon.pokemon_id == pokemon_count_query.c.pokemon_id))
  226.                  .distinct()
  227.                  .where(Pokemon.disappear_time == pokemon_count_query.c.lastappeared)
  228.                  .dicts()
  229.                  )
  230.  
  231.         # Performance: Disable the garbage collector prior to creating a (potentially) large dict with append().
  232.         gc.disable()
  233.  
  234.         pokemons = []
  235.         total = 0
  236.         for p in query:
  237.             p['pokemon_name'] = get_pokemon_name(p['pokemon_id'])
  238.             pokemons.append(p)
  239.             total += p['count']
  240.  
  241.         # Re-enable the GC.
  242.         gc.enable()
  243.  
  244.         return {'pokemon': pokemons, 'total': total}
  245.  
  246.     @classmethod
  247.     def get_appearances(cls, pokemon_id, timediff):
  248.         '''
  249.        :param pokemon_id: id of pokemon that we need appearances for
  250.        :param timediff: limiting period of the selection
  251.        :return: list of  pokemon  appearances over a selected period
  252.        '''
  253.         if timediff:
  254.             timediff = datetime.utcnow() - timediff
  255.         query = (Pokemon
  256.                  .select(Pokemon.latitude, Pokemon.longitude, Pokemon.pokemon_id, fn.Count(Pokemon.spawnpoint_id).alias('count'), Pokemon.spawnpoint_id)
  257.                  .where((Pokemon.pokemon_id == pokemon_id) &
  258.                         (Pokemon.disappear_time > timediff)
  259.                         )
  260.                  .group_by(Pokemon.latitude, Pokemon.longitude, Pokemon.pokemon_id, Pokemon.spawnpoint_id)
  261.                  .dicts()
  262.                  )
  263.  
  264.         return list(query)
  265.  
  266.     @classmethod
  267.     def get_appearances_times_by_spawnpoint(cls, pokemon_id, spawnpoint_id, timediff):
  268.         '''
  269.        :param pokemon_id: id of pokemon that we need appearances times for
  270.        :param spawnpoint_id: spawnpoing id we need appearances times for
  271.        :param timediff: limiting period of the selection
  272.        :return: list of time appearances over a selected period
  273.        '''
  274.         if timediff:
  275.             timediff = datetime.utcnow() - timediff
  276.         query = (Pokemon
  277.                  .select(Pokemon.disappear_time)
  278.                  .where((Pokemon.pokemon_id == pokemon_id) &
  279.                         (Pokemon.spawnpoint_id == spawnpoint_id) &
  280.                         (Pokemon.disappear_time > timediff)
  281.                         )
  282.                  .order_by(Pokemon.disappear_time.asc())
  283.                  .tuples()
  284.                  )
  285.  
  286.         return list(itertools.chain(*query))
  287.  
  288.     @classmethod
  289.     def get_spawn_time(cls, disappear_time):
  290.         return (disappear_time + 2700) % 3600
  291.  
  292.     @classmethod
  293.     def get_spawnpoints(cls, swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, oSwLng=None, oNeLat=None, oNeLng=None):
  294.         query = Pokemon.select(Pokemon.latitude, Pokemon.longitude, Pokemon.spawnpoint_id, (date_secs(Pokemon.disappear_time)).alias('time'), fn.Count(Pokemon.spawnpoint_id).alias('count'))
  295.  
  296.         if timestamp > 0:
  297.             query = (query
  298.                      .where(((Pokemon.last_modified > datetime.utcfromtimestamp(timestamp / 1000))) &
  299.                             ((Pokemon.latitude >= swLat) &
  300.                              (Pokemon.longitude >= swLng) &
  301.                              (Pokemon.latitude <= neLat) &
  302.                              (Pokemon.longitude <= neLng)))
  303.                      .dicts())
  304.         elif oSwLat and oSwLng and oNeLat and oNeLng:
  305.             # Send spawnpoints in view but exclude those within old boundaries. Only send newly uncovered spawnpoints.
  306.             query = (query
  307.                      .where((((Pokemon.latitude >= swLat) &
  308.                               (Pokemon.longitude >= swLng) &
  309.                               (Pokemon.latitude <= neLat) &
  310.                               (Pokemon.longitude <= neLng))) &
  311.                               ((Pokemon.latitude >= oSwLat) &
  312.                               (Pokemon.longitude >= oSwLng) &
  313.                               (Pokemon.latitude <= oNeLat) &
  314.                               (Pokemon.longitude <= oNeLng)))
  315.                      .dicts())
  316.         elif swLat and swLng and neLat and neLng:
  317.             query = (query
  318.                      .where((Pokemon.latitude <= neLat) &
  319.                             (Pokemon.latitude >= swLat) &
  320.                             (Pokemon.longitude >= swLng) &
  321.                             (Pokemon.longitude <= neLng)
  322.                             ))
  323.  
  324.         query = query.group_by(Pokemon.latitude, Pokemon.longitude, Pokemon.spawnpoint_id, SQL('time'))
  325.  
  326.         queryDict = query.dicts()
  327.         spawnpoints = {}
  328.  
  329.         for sp in queryDict:
  330.             key = sp['spawnpoint_id']
  331.             disappear_time = cls.get_spawn_time(sp.pop('time'))
  332.             count = int(sp['count'])
  333.  
  334.             if key not in spawnpoints:
  335.                 spawnpoints[key] = sp
  336.             else:
  337.                 spawnpoints[key]['special'] = True
  338.  
  339.             if 'time' not in spawnpoints[key] or count >= spawnpoints[key]['count']:
  340.                 spawnpoints[key]['time'] = disappear_time
  341.                 spawnpoints[key]['count'] = count
  342.  
  343.         for sp in spawnpoints.values():
  344.             del sp['count']
  345.  
  346.         return list(spawnpoints.values())
  347.  
  348.     @classmethod
  349.     def get_spawnpoints_in_hex(cls, center, steps):
  350.         log.info('Finding spawn points {} steps away'.format(steps))
  351.  
  352.         n, e, s, w = hex_bounds(center, steps)
  353.  
  354.         query = (Pokemon
  355.                  .select(Pokemon.latitude.alias('lat'),
  356.                          Pokemon.longitude.alias('lng'),
  357.                          (date_secs(Pokemon.disappear_time)).alias('time'),
  358.                          Pokemon.spawnpoint_id
  359.                          ))
  360.         query = (query.where((Pokemon.latitude <= n) &
  361.                              (Pokemon.latitude >= s) &
  362.                              (Pokemon.longitude >= w) &
  363.                              (Pokemon.longitude <= e)
  364.                              ))
  365.         # Sqlite doesn't support distinct on columns. (distinct has no effect on mysql either)
  366.         query = query.group_by(Pokemon.spawnpoint_id)
  367.  
  368.         s = list(query.dicts())
  369.  
  370.         # The distance between scan circles of radius 70 in a hex is 121.2436
  371.         # steps - 1 to account for the center circle then add 70 for the edge.
  372.         step_distance = ((steps - 1) * 121.2436) + 70
  373.         # Compare spawnpoint list to a circle with radius steps * 120.
  374.         # Uses the direct geopy distance between the center and the spawnpoint.
  375.         filtered = []
  376.  
  377.         for idx, sp in enumerate(s):
  378.             if geopy.distance.distance(center, (sp['lat'], sp['lng'])).meters <= step_distance:
  379.                 filtered.append(s[idx])
  380.  
  381.         # At this point, 'time' is DISAPPEARANCE time, we're going to morph it to APPEARANCE time.
  382.         for location in filtered:
  383.             # examples: time    shifted
  384.             #           0       (   0 + 2700) = 2700 % 3600 = 2700 (0th minute to 45th minute, 15 minutes prior to appearance as time wraps around the hour.)
  385.             #           1800    (1800 + 2700) = 4500 % 3600 =  900 (30th minute, moved to arrive at 15th minute.)
  386.             # todo: this DOES NOT ACCOUNT for pokemons that appear sooner and live longer, but you'll _always_ have at least 15 minutes, so it works well enough.
  387.             location['time'] = cls.get_spawn_time(location['time'])
  388.  
  389.         if args.sscluster:
  390.             filtered = cluster.main(filtered)
  391.  
  392.         return filtered
  393.  
  394.  
  395. class Pokestop(BaseModel):
  396.     pokestop_id = CharField(primary_key=True, max_length=50)
  397.     enabled = BooleanField()
  398.     latitude = DoubleField()
  399.     longitude = DoubleField()
  400.     last_modified = DateTimeField(index=True)
  401.     lure_expiration = DateTimeField(null=True, index=True)
  402.     active_fort_modifier = CharField(max_length=50, null=True)
  403.     last_updated = DateTimeField(null=True, index=True, default=datetime.utcnow)
  404.  
  405.     class Meta:
  406.         indexes = ((('latitude', 'longitude'), False),)
  407.  
  408.     @staticmethod
  409.     def get_stops(api, swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, oSwLng=None, oNeLat=None, oNeLng=None, lured=False):
  410.  
  411.         query = Pokestop.select(Pokestop.active_fort_modifier, Pokestop.enabled, Pokestop.latitude, Pokestop.longitude, Pokestop.last_modified, Pokestop.lure_expiration, Pokestop.pokestop_id)
  412.  
  413.         if not (swLat and swLng and neLat and neLng):
  414.             query = (query
  415.                      .dicts())
  416.         elif timestamp > 0:
  417.             query = (query
  418.                      .where(((Pokestop.last_updated > datetime.utcfromtimestamp(timestamp / 1000))) &
  419.                             (Pokestop.latitude >= swLat) &
  420.                             (Pokestop.longitude >= swLng) &
  421.                             (Pokestop.latitude <= neLat) &
  422.                             (Pokestop.longitude <= neLng))
  423.                      .dicts())
  424.         elif oSwLat and oSwLng and oNeLat and oNeLng and lured:
  425.             query = (query
  426.                      .where((((Pokestop.latitude >= swLat) &
  427.                               (Pokestop.longitude >= swLng) &
  428.                               (Pokestop.latitude <= neLat) &
  429.                               (Pokestop.longitude <= neLng)) &
  430.                              (Pokestop.active_fort_modifier.is_null(False))) &
  431.                             ~((Pokestop.latitude >= oSwLat) &
  432.                               (Pokestop.longitude >= oSwLng) &
  433.                               (Pokestop.latitude <= oNeLat) &
  434.                               (Pokestop.longitude <= oNeLng)) &
  435.                              (Pokestop.active_fort_modifier.is_null(False)))
  436.                      .dicts())
  437.         elif oSwLat and oSwLng and oNeLat and oNeLng:
  438.             # Send stops in view but exclude those within old boundaries. Only send newly uncovered stops.
  439.             query = (query
  440.                      .where(((Pokestop.latitude >= swLat) &
  441.                              (Pokestop.longitude >= swLng) &
  442.                              (Pokestop.latitude <= neLat) &
  443.                              (Pokestop.longitude <= neLng)) &
  444.                             ~((Pokestop.latitude >= oSwLat) &
  445.                               (Pokestop.longitude >= oSwLng) &
  446.                               (Pokestop.latitude <= oNeLat) &
  447.                               (Pokestop.longitude <= oNeLng)))
  448.                      .dicts())
  449.         elif lured:
  450.             query = (query
  451.                      .where(((Pokestop.last_updated > datetime.utcfromtimestamp(timestamp / 1000))) &
  452.                             ((Pokestop.latitude >= swLat) &
  453.                              (Pokestop.longitude >= swLng) &
  454.                              (Pokestop.latitude <= neLat) &
  455.                              (Pokestop.longitude <= neLng)) &
  456.                             (Pokestop.active_fort_modifier.is_null(False)))
  457.                      .dicts())
  458.  
  459.         else:
  460.             query = (query
  461.                      .where((Pokestop.latitude >= swLat) &
  462.                             (Pokestop.longitude >= swLng) &
  463.                             (Pokestop.latitude <= neLat) &
  464.                             (Pokestop.longitude <= neLng))
  465.                      .dicts())
  466.  
  467.         # Performance: Disable the garbage collector prior to creating a (potentially) large dict with append().
  468.         gc.disable()
  469.  
  470.         pokestops = []
  471.         for p in query:
  472.             if args.china:
  473.                 p['latitude'], p['longitude'] = \
  474.                     transform_from_wgs_to_gcj(p['latitude'], p['longitude'])
  475.             pokestops.append(p)
  476.  
  477.         # Re-enable the GC.
  478.         gc.enable()
  479.  
  480.         return pokestops
  481.  
  482.  
  483. class Gym(BaseModel):
  484.     UNCONTESTED = 0
  485.     TEAM_MYSTIC = 1
  486.     TEAM_VALOR = 2
  487.     TEAM_INSTINCT = 3
  488.  
  489.     gym_id = CharField(primary_key=True, max_length=50)
  490.     team_id = IntegerField()
  491.     guard_pokemon_id = IntegerField()
  492.     gym_points = IntegerField()
  493.     enabled = BooleanField()
  494.     latitude = DoubleField()
  495.     longitude = DoubleField()
  496.     last_modified = DateTimeField(index=True)
  497.     last_scanned = DateTimeField(default=datetime.utcnow)
  498.  
  499.     class Meta:
  500.         indexes = ((('latitude', 'longitude'), False),)
  501.  
  502.     @staticmethod
  503.     def get_gyms(swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, oSwLng=None, oNeLat=None, oNeLng=None):
  504.         if not (swLat and swLng and neLat and neLng):
  505.             results = (Gym
  506.                        .select()
  507.                        .dicts())
  508.         elif timestamp > 0:
  509.             # If timestamp is known only send last scanned Gyms.
  510.             results = (Gym
  511.                        .select()
  512.                        .where(((Gym.last_scanned > datetime.utcfromtimestamp(timestamp / 1000)) &
  513.                               (Gym.latitude >= swLat) &
  514.                               (Gym.longitude >= swLng) &
  515.                               (Gym.latitude <= neLat) &
  516.                               (Gym.longitude <= neLng)))
  517.                        .dicts())
  518.         elif oSwLat and oSwLng and oNeLat and oNeLng:
  519.             # Send gyms in view but exclude those within old boundaries. Only send newly uncovered gyms.
  520.             results = (Gym
  521.                        .select()
  522.                        .where(((Gym.latitude >= swLat) &
  523.                                (Gym.longitude >= swLng) &
  524.                                (Gym.latitude <= neLat) &
  525.                                (Gym.longitude <= neLng)) &
  526.                               ~((Gym.latitude >= oSwLat) &
  527.                                 (Gym.longitude >= oSwLng) &
  528.                                 (Gym.latitude <= oNeLat) &
  529.                                 (Gym.longitude <= oNeLng)))
  530.                        .dicts())
  531.  
  532.         else:
  533.             results = (Gym
  534.                        .select()
  535.                        .where((Gym.latitude >= swLat) &
  536.                               (Gym.longitude >= swLng) &
  537.                               (Gym.latitude <= neLat) &
  538.                               (Gym.longitude <= neLng))
  539.                        .dicts())
  540.  
  541.         # Performance: Disable the garbage collector prior to creating a (potentially) large dict with append().
  542.         gc.disable()
  543.  
  544.         gyms = {}
  545.         gym_ids = []
  546.         for g in results:
  547.             g['name'] = None
  548.             g['pokemon'] = []
  549.             gyms[g['gym_id']] = g
  550.             gym_ids.append(g['gym_id'])
  551.  
  552.         if len(gym_ids) > 0:
  553.             pokemon = (GymMember
  554.                        .select(
  555.                            GymMember.gym_id,
  556.                            GymPokemon.cp.alias('pokemon_cp'),
  557.                            GymPokemon.pokemon_id,
  558.                            Trainer.name.alias('trainer_name'),
  559.                            Trainer.level.alias('trainer_level'))
  560.                        .join(Gym, on=(GymMember.gym_id == Gym.gym_id))
  561.                        .join(GymPokemon, on=(GymMember.pokemon_uid == GymPokemon.pokemon_uid))
  562.                        .join(Trainer, on=(GymPokemon.trainer_name == Trainer.name))
  563.                        .where(GymMember.gym_id << gym_ids)
  564.                        .where(GymMember.last_scanned > Gym.last_modified)
  565.                        .order_by(GymMember.gym_id, GymPokemon.cp)
  566.                        .dicts())
  567.  
  568.             for p in pokemon:
  569.                 p['pokemon_name'] = get_pokemon_name(p['pokemon_id'])
  570.                 gyms[p['gym_id']]['pokemon'].append(p)
  571.  
  572.             details = (GymDetails
  573.                        .select(
  574.                            GymDetails.gym_id,
  575.                            GymDetails.name)
  576.                        .where(GymDetails.gym_id << gym_ids)
  577.                        .dicts())
  578.  
  579.             for d in details:
  580.                 gyms[d['gym_id']]['name'] = d['name']
  581.  
  582.         # Re-enable the GC.
  583.         gc.enable()
  584.  
  585.         return gyms
  586.  
  587.     @staticmethod
  588.     def get_gym(id):
  589.         result = (Gym
  590.                   .select(Gym.gym_id,
  591.                           Gym.team_id,
  592.                           GymDetails.name,
  593.                           GymDetails.description,
  594.                           Gym.guard_pokemon_id,
  595.                           Gym.gym_points,
  596.                           Gym.latitude,
  597.                           Gym.longitude,
  598.                           Gym.last_modified,
  599.                           Gym.last_scanned)
  600.                   .join(GymDetails, JOIN.LEFT_OUTER, on=(Gym.gym_id == GymDetails.gym_id))
  601.                   .where(Gym.gym_id == id)
  602.                   .dicts()
  603.                   .get())
  604.  
  605.         result['guard_pokemon_name'] = get_pokemon_name(result['guard_pokemon_id']) if result['guard_pokemon_id'] else ''
  606.         result['pokemon'] = []
  607.  
  608.         pokemon = (GymMember
  609.                    .select(GymPokemon.cp.alias('pokemon_cp'),
  610.                            GymPokemon.pokemon_id,
  611.                            GymPokemon.pokemon_uid,
  612.                            GymPokemon.move_1,
  613.                            GymPokemon.move_2,
  614.                            GymPokemon.iv_attack,
  615.                            GymPokemon.iv_defense,
  616.                            GymPokemon.iv_stamina,
  617.                            Trainer.name.alias('trainer_name'),
  618.                            Trainer.level.alias('trainer_level'))
  619.                    .join(Gym, on=(GymMember.gym_id == Gym.gym_id))
  620.                    .join(GymPokemon, on=(GymMember.pokemon_uid == GymPokemon.pokemon_uid))
  621.                    .join(Trainer, on=(GymPokemon.trainer_name == Trainer.name))
  622.                    .where(GymMember.gym_id == id)
  623.                    .where(GymMember.last_scanned > Gym.last_modified)
  624.                    .order_by(GymPokemon.cp.desc())
  625.                    .dicts())
  626.  
  627.         for p in pokemon:
  628.             p['pokemon_name'] = get_pokemon_name(p['pokemon_id'])
  629.  
  630.             p['move_1_name'] = get_move_name(p['move_1'])
  631.             p['move_1_damage'] = get_move_damage(p['move_1'])
  632.             p['move_1_energy'] = get_move_energy(p['move_1'])
  633.             p['move_1_type'] = get_move_type(p['move_1'])
  634.  
  635.             p['move_2_name'] = get_move_name(p['move_2'])
  636.             p['move_2_damage'] = get_move_damage(p['move_2'])
  637.             p['move_2_energy'] = get_move_energy(p['move_2'])
  638.             p['move_2_type'] = get_move_type(p['move_2'])
  639.  
  640.             result['pokemon'].append(p)
  641.  
  642.         return result
  643.  
  644.  
  645. class ScannedLocation(BaseModel):
  646.     cellid = CharField(primary_key=True, max_length=50)
  647.     latitude = DoubleField()
  648.     longitude = DoubleField()
  649.     last_modified = DateTimeField(index=True, default=datetime.utcnow, null=True)
  650.     # marked true when all five bands have been completed
  651.     done = BooleanField(default=False)
  652.  
  653.     # Five scans/hour is required to catch all spawns
  654.     # Each scan must be at least 12 minutes from the previous check,
  655.     # with a 2 minute window during which the scan can be done
  656.  
  657.     # default of -1 is for bands not yet scanned
  658.     band1 = IntegerField(default=-1)
  659.     band2 = IntegerField(default=-1)
  660.     band3 = IntegerField(default=-1)
  661.     band4 = IntegerField(default=-1)
  662.     band5 = IntegerField(default=-1)
  663.  
  664.     # midpoint is the center of the bands relative to band 1
  665.     # e.g., if band 1 is 10.4 min, and band 4 is 34.0 min, midpoint is -0.2 min in minsec
  666.     # extra 10 seconds in case of delay in recording now time
  667.     midpoint = IntegerField(default=0)
  668.  
  669.     # width is how wide the valid window is. Default is 0, max is 2 min
  670.     # e.g., if band 1 is 10.4 min, and band 4 is 34.0 min, midpoint is 0.4 min in minsec
  671.     width = IntegerField(default=0)
  672.  
  673.     class Meta:
  674.         indexes = ((('latitude', 'longitude'), False),)
  675.         constraints = [Check('band1 >= -1'), Check('band1 < 3600'),
  676.                        Check('band2 >= -1'), Check('band2 < 3600'),
  677.                        Check('band3 >= -1'), Check('band3 < 3600'),
  678.                        Check('band4 >= -1'), Check('band4 < 3600'),
  679.                        Check('band5 >= -1'), Check('band5 < 3600'),
  680.                        Check('midpoint >= -130'), Check('midpoint <= 130'),
  681.                        Check('width >= 0'), Check('width <= 130')]
  682.  
  683.     @staticmethod
  684.     def get_recent(swLat, swLng, neLat, neLng, timestamp=0, oSwLat=None, oSwLng=None, oNeLat=None, oNeLng=None):
  685.         activeTime = (datetime.utcnow() - timedelta(minutes=15))
  686.         if timestamp > 0:
  687.             query = (ScannedLocation
  688.                      .select()
  689.                      .where(((ScannedLocation.last_modified >= datetime.utcfromtimestamp(timestamp / 1000))) &
  690.                             (ScannedLocation.latitude >= swLat) &
  691.                             (ScannedLocation.longitude >= swLng) &
  692.                             (ScannedLocation.latitude <= neLat) &
  693.                             (ScannedLocation.longitude <= neLng))
  694.                      .dicts())
  695.         elif oSwLat and oSwLng and oNeLat and oNeLng:
  696.             # Send scannedlocations in view but exclude those within old boundaries. Only send newly uncovered scannedlocations.
  697.             query = (ScannedLocation
  698.                      .select()
  699.                      .where((((ScannedLocation.last_modified >= activeTime)) &
  700.                              (ScannedLocation.latitude >= swLat) &
  701.                              (ScannedLocation.longitude >= swLng) &
  702.                              (ScannedLocation.latitude <= neLat) &
  703.                              (ScannedLocation.longitude <= neLng)) &
  704.                             ~(((ScannedLocation.last_modified >= activeTime)) &
  705.                               (ScannedLocation.latitude >= oSwLat) &
  706.                               (ScannedLocation.longitude >= oSwLng) &
  707.                               (ScannedLocation.latitude <= oNeLat) &
  708.                               (ScannedLocation.longitude <= oNeLng)))
  709.                      .dicts())
  710.         else:
  711.             query = (ScannedLocation
  712.                      .select()
  713.                      .where((ScannedLocation.last_modified >= activeTime) &
  714.                             (ScannedLocation.latitude >= swLat) &
  715.                             (ScannedLocation.longitude >= swLng) &
  716.                             (ScannedLocation.latitude <= neLat) &
  717.                             (ScannedLocation.longitude <= neLng))
  718.                      .order_by(ScannedLocation.last_modified.asc())
  719.                      .dicts())
  720.  
  721.         return list(query)
  722.  
  723.     # DB format of a new location
  724.     @staticmethod
  725.     def new_loc(loc):
  726.         return {'cellid': cellid(loc),
  727.                 'latitude': loc[0],
  728.                 'longitude': loc[1],
  729.                 'done': False,
  730.                 'band1': -1,
  731.                 'band2': -1,
  732.                 'band3': -1,
  733.                 'band4': -1,
  734.                 'band5': -1,
  735.                 'width': 0,
  736.                 'midpoint': 0,
  737.                 'last_modified': None}
  738.  
  739.     # Used to update bands
  740.     @staticmethod
  741.     def db_format(scan, band, nowms):
  742.         scan.update({'band' + str(band): nowms})
  743.         scan['done'] = reduce(lambda x, y: x and (scan['band' + str(y)] > -1), range(1, 6), True)
  744.         return scan
  745.  
  746.     # Shorthand helper for DB dict
  747.     @staticmethod
  748.     def _q_init(scan, start, end, kind, sp_id=None):
  749.         return {'loc': scan['loc'], 'kind': kind, 'start': start, 'end': end, 'step': scan['step'], 'sp': sp_id}
  750.  
  751.     # return value of a particular scan from loc, or default dict if not found
  752.     @classmethod
  753.     def get_by_loc(cls, loc):
  754.         query = (cls
  755.                  .select()
  756.                  .where((ScannedLocation.latitude == loc[0]) &
  757.                         (ScannedLocation.longitude == loc[1]))
  758.                  .dicts())
  759.  
  760.         return query[0] if len(list(query)) else cls.new_loc(loc)
  761.  
  762.     # Check if spawn points in a list are in any of the existing spannedlocation records
  763.     # Otherwise, search through the spawn point list, and update scan_spawn_point dict for DB bulk upserting
  764.     @classmethod
  765.     def link_spawn_points(cls, scans, initial, spawn_points, distance, scan_spawn_point):
  766.         for cell, scan in scans.iteritems():
  767.             if initial[cell]['done']:
  768.                 continue
  769.  
  770.             for sp in spawn_points:
  771.                 if in_radius((sp['latitude'], sp['longitude']), scan['loc'], distance):
  772.                     scan_spawn_point[cell + sp['id']] = {'spawnpoint': sp['id'],
  773.                                                          'scannedlocation': cell}
  774.  
  775.     # return list of dicts for upcoming valid band times
  776.     @classmethod
  777.     def linked_spawn_points(cls, cell):
  778.  
  779.         # unable to use a normal join, since MySQL produces foreignkey constraint errors when
  780.         # trying to upsert fields that are foreignkeys on another table
  781.  
  782.         ''''        query = (SpawnPoint
  783.                        .select()
  784.                        .join(ScanSpawnPoint)
  785.                        .join(cls)
  786.                        .where(cls.cellid == cell).dicts())
  787.        '''
  788.         query = (ScanSpawnPoint
  789.                  .select(ScanSpawnPoint.spawnpoint)
  790.                  .where(ScanSpawnPoint.scannedlocation == cell).dicts())
  791.  
  792.         return [i['spawnpoint'] for i in list(query)]
  793.  
  794.     # return list of dicts for upcoming valid band times
  795.     @staticmethod
  796.     def visible_forts(step_location):
  797.         distance = 0.9
  798.         n, e, s, w = hex_bounds(step_location, radius=distance * 1000)
  799.         for g in Gym.get_gyms(s, w, n, e).values():
  800.             if in_radius((g['latitude'], g['longitude']), step_location, distance):
  801.                 return True
  802.  
  803.         for g in Pokestop.get_stops(api, s, w, n, e):
  804.             if in_radius((g['latitude'], g['longitude']), step_location, distance):
  805.                 return True
  806.  
  807.         return False
  808.  
  809.     # return list of dicts for upcoming valid band times
  810.     @classmethod
  811.     def get_times(cls, scan, now_date):
  812.         s = cls.get_by_loc(scan['loc'])
  813.         if s['done']:
  814.             return []
  815.  
  816.         max = 3600 * 2 + 250  # greater than maximum possible value
  817.         min = {'end': max}
  818.  
  819.         nowms = date_secs(now_date)
  820.         if s['band1'] == -1:
  821.             return [cls._q_init(scan, nowms, nowms + 3599, 'band')]
  822.  
  823.         # Find next window
  824.         basems = s['band1']
  825.         for i in range(2, 6):
  826.             ms = s['band' + str(i)]
  827.  
  828.             # skip bands already done
  829.             if ms > -1:
  830.                 continue
  831.  
  832.             radius = 120 - s['width'] / 2
  833.             end = (basems + s['midpoint'] + radius + (i - 1) * 720 - 10) % 3600
  834.             end = end if end >= nowms else end + 3600
  835.  
  836.             if end < min['end']:
  837.                 min = cls._q_init(scan, end - radius * 2 + 10, end, 'band')
  838.  
  839.         return [min] if min['end'] < max else []
  840.  
  841.     # Checks if now falls within an unfilled band for a scanned location
  842.     # Returns the updated scan location dict
  843.     @classmethod
  844.     def update_band(cls, scan):
  845.         now_date = datetime.utcnow()
  846.         scan['last_modified'] = now_date
  847.  
  848.         if scan['done']:
  849.             return scan
  850.  
  851.         now_secs = date_secs(now_date)
  852.         if scan['band1'] == -1:
  853.             return cls.db_format(scan, 1, now_secs)
  854.  
  855.         # calc if number falls in band with remaining points
  856.         basems = scan['band1']
  857.         delta = (now_secs - basems - scan['midpoint']) % 3600
  858.         band = int(round(delta / 12 / 60.0) % 5) + 1
  859.  
  860.         # Check if that band already filled
  861.         if scan['band' + str(band)] > -1:
  862.             return scan
  863.  
  864.         # Check if this result falls within the band 2 min window
  865.         offset = (delta + 1080) % 720 - 360
  866.         if abs(offset) > 120 - scan['width'] / 2:
  867.             return scan
  868.  
  869.         # find band midpoint/width
  870.         scan = cls.db_format(scan, band, now_secs)
  871.         bts = [scan['band' + str(i)] for i in range(1, 6)]
  872.         bts = filter(lambda ms: ms > -1, bts)
  873.         bts_delta = map(lambda ms: (ms - basems) % 3600, bts)
  874.         bts_offsets = map(lambda ms: (ms + 1080) % 720 - 360, bts_delta)
  875.         min_scan = min(bts_offsets)
  876.         max_scan = max(bts_offsets)
  877.         scan['width'] = max_scan - min_scan
  878.         scan['midpoint'] = (max_scan + min_scan) / 2
  879.  
  880.         return scan
  881.  
  882.     @classmethod
  883.     def bands_filled(cls, locations):
  884.         filled = 0
  885.         for e in locations:
  886.             sl = cls.get_by_loc(e[1])
  887.             bands = [sl['band' + str(i)] for i in range(1, 6)]
  888.             filled += reduce(lambda x, y: x + (y > -1), bands, 0)
  889.  
  890.         return filled
  891.  
  892.     @classmethod
  893.     def reset_bands(cls, scan_loc):
  894.         scan_loc['done'] = False
  895.         scan_loc['last_modified'] = datetime.utcnow()
  896.         for i in range(1, 6):
  897.             scan_loc['band' + str(i)] = -1
  898.  
  899.     @classmethod
  900.     def select_in_hex(cls, center, steps):
  901.         # should be a way to delegate this to SpawnPoint.select_in_hex, but w/e
  902.  
  903.         R = 6378.1  # km radius of the earth
  904.         hdist = ((steps * 120.0) - 50.0) / 1000.0
  905.         n, e, s, w = hex_bounds(center, steps)
  906.  
  907.         # get all spawns in that box
  908.         sp = list(cls
  909.                   .select()
  910.                   .where((cls.latitude <= n) &
  911.                          (cls.latitude >= s) &
  912.                          (cls.longitude >= w) &
  913.                          (cls.longitude <= e))
  914.                   .dicts())
  915.  
  916.         # for each spawn work out if it is in the hex (clipping the diagonals)
  917.         in_hex = []
  918.         for spawn in sp:
  919.             # get the offset from the center of each spawn in km
  920.             offset = [math.radians(spawn['latitude'] - center[0]) * R,
  921.                       math.radians(spawn['longitude'] - center[1]) * (R * math.cos(math.radians(center[0])))]
  922.             # check agains the 4 lines that make up the diagonals
  923.             if (offset[1] + (offset[0] * 0.5)) > hdist:  # too far ne
  924.                 continue
  925.             if (offset[1] - (offset[0] * 0.5)) > hdist:  # too far se
  926.                 continue
  927.             if ((offset[0] * 0.5) - offset[1]) > hdist:  # too far nw
  928.                 continue
  929.             if ((0 - offset[1]) - (offset[0] * 0.5)) > hdist:  # too far sw
  930.                 continue
  931.             # if it gets to here its  a good spawn
  932.             in_hex.append(spawn)
  933.         return in_hex
  934.  
  935.  
  936. class MainWorker(BaseModel):
  937.     worker_name = CharField(primary_key=True, max_length=50)
  938.     message = CharField()
  939.     method = CharField(max_length=50)
  940.     last_modified = DateTimeField(index=True)
  941.  
  942.  
  943. class WorkerStatus(BaseModel):
  944.     username = CharField(primary_key=True, max_length=50)
  945.     worker_name = CharField(index=True, max_length=50)
  946.     success = IntegerField()
  947.     fail = IntegerField()
  948.     no_items = IntegerField()
  949.     skip = IntegerField()
  950.     captchas = IntegerField(default=0)
  951.     last_modified = DateTimeField(index=True)
  952.     message = CharField(max_length=255)
  953.     last_scan_date = DateTimeField(index=True)
  954.     latitude = DoubleField(null=True)
  955.     longitude = DoubleField(null=True)
  956.  
  957.     @staticmethod
  958.     def db_format(status, name='status_worker_db'):
  959.         status['worker_name'] = status.get('worker_name', name)
  960.         return {'username': status['username'],
  961.                 'worker_name': status['worker_name'],
  962.                 'success': status['success'],
  963.                 'fail': status['fail'],
  964.                 'no_items': status['noitems'],
  965.                 'skip': status['skip'],
  966.                 'captchas': status['captchas'],
  967.                 'last_modified': datetime.utcnow(),
  968.                 'message': status['message'],
  969.                 'last_scan_date': status.get('last_scan_date', datetime.utcnow()),
  970.                 'latitude': status.get('latitude', None),
  971.                 'longitude': status.get('longitude', None)}
  972.  
  973.     @staticmethod
  974.     def get_recent():
  975.         query = (WorkerStatus
  976.                  .select()
  977.                  .where((WorkerStatus.last_modified >=
  978.                         (datetime.utcnow() - timedelta(minutes=5))))
  979.                  .order_by(WorkerStatus.username)
  980.                  .dicts())
  981.  
  982.         status = []
  983.         for s in query:
  984.             status.append(s)
  985.  
  986.         return status
  987.  
  988.     @staticmethod
  989.     def get_worker(username, loc=False):
  990.         query = (WorkerStatus
  991.                  .select()
  992.                  .where((WorkerStatus.username == username))
  993.                  .dicts())
  994.  
  995.         # Sometimes is appears peewee is slow to load, and and this produces an Exception
  996.         # Retry after a second to give peewee time to load
  997.         while True:
  998.             try:
  999.                 result = query[0] if len(query) else {
  1000.                     'username': username,
  1001.                     'success': 0,
  1002.                     'fail': 0,
  1003.                     'no_items': 0,
  1004.                     'skip': 0,
  1005.                     'last_modified': datetime.utcnow(),
  1006.                     'message': 'New account {} loaded'.format(username),
  1007.                     'last_scan_date': datetime.utcnow(),
  1008.                     'latitude': loc[0] if loc else None,
  1009.                     'longitude': loc[1] if loc else None
  1010.                 }
  1011.                 break
  1012.             except Exception as e:
  1013.                 log.error('Exception in get_worker under account {} Exception message: {}'.format(username, e))
  1014.                 traceback.print_exc(file=sys.stdout)
  1015.                 time.sleep(1)
  1016.  
  1017.         return result
  1018.  
  1019.  
  1020. class SpawnPoint(BaseModel):
  1021.     id = CharField(primary_key=True, max_length=50)
  1022.     latitude = DoubleField()
  1023.     longitude = DoubleField()
  1024.     last_scanned = DateTimeField(index=True)
  1025.     # kind gives the four quartiles of the spawn, as 's' for seen or 'h' for hidden
  1026.     # for example, a 30 minute spawn is 'hhss'
  1027.     kind = CharField(max_length=4, default='hhhs')
  1028.  
  1029.     # links shows whether a pokemon encounter id changes between quartiles or stays the same
  1030.     # both 1x45 and 1x60h3 have the kind of 'sssh', but the different links shows when the
  1031.     # encounter id changes
  1032.     # same encounter id is shared between two quartiles, links shows a '+',
  1033.     # a different encounter id between two quartiles is a '-'
  1034.     # For the hidden times, an 'h' is used. Until determined, '?' is used.
  1035.     # Note index is shifted by a half. links[0] is the link between kind[0] and kind[1],
  1036.     # and so on. links[3] is the link between kind[3] and kind[0]
  1037.     links = CharField(max_length=4, default='????')
  1038.  
  1039.     # count consecutive times spawn should have been seen, but wasn't
  1040.     # if too high, will not be scheduled for review, and treated as inactive
  1041.     missed_count = IntegerField(default=0)
  1042.  
  1043.     # next 2 fields are to narrow down on the valid TTH window
  1044.     # seconds after the hour of the latest pokemon seen time within the hour
  1045.     latest_seen = IntegerField()
  1046.  
  1047.     # seconds after the hour of the earliest time wasn't seen after an appearance
  1048.     earliest_unseen = IntegerField()
  1049.  
  1050.     class Meta:
  1051.         indexes = ((('latitude', 'longitude'), False),)
  1052.         constraints = [Check('earliest_unseen >= 0'), Check('earliest_unseen < 3600'),
  1053.                        Check('latest_seen >= 0'), Check('latest_seen < 3600')]
  1054.  
  1055.     # Returns the spawn point dict from ID, or a new dict if not found
  1056.     @classmethod
  1057.     def get_by_id(cls, id, latitude=0, longitude=0):
  1058.         query = (cls
  1059.                  .select()
  1060.                  .where(cls.id == id)
  1061.                  .dicts())
  1062.  
  1063.         return query[0] if query else {
  1064.             'id': id,
  1065.             'latitude': latitude,
  1066.             'longitude': longitude,
  1067.             'last_scanned': None,  # Null value used as new flag
  1068.             'kind': 'hhhs',
  1069.             'links': '????',
  1070.             'missed_count': 0,
  1071.             'latest_seen': None,
  1072.             'earliest_unseen': None
  1073.  
  1074.         }
  1075.  
  1076.     # Confirm if tth has been found
  1077.     @staticmethod
  1078.     def tth_found(sp):
  1079.         # fully indentified if no '?' in links and latest seen == earliest seen
  1080.         return sp['latest_seen'] == sp['earliest_unseen']
  1081.  
  1082.     # return [start, end] in seconds after the hour for the spawn, despawn time of a spawnpoint
  1083.     @classmethod
  1084.     def start_end(cls, sp, spawn_delay=0, links=False):
  1085.         links_arg = links
  1086.         links = links if links else str(sp['links'])
  1087.  
  1088.         if links == '????':  # clean up for old data
  1089.             links = str(sp['kind'].replace('s', '?'))
  1090.  
  1091.         # make some assumptions if link not fully identified
  1092.         if links.count('-') == 0:
  1093.             links = links[:-1] + '-'
  1094.  
  1095.         links = links.replace('?', '+')
  1096.  
  1097.         links = links[:-1] + '-'
  1098.         plus_or_minus = links.index('+') if links.count('+') else links.index('-')
  1099.         start = sp['earliest_unseen'] - (4 - plus_or_minus) * 900 + spawn_delay
  1100.         no_tth_adjust = 60 if not links_arg and not cls.tth_found(sp) else 0
  1101.         end = sp['latest_seen'] - (3 - links.index('-')) * 900 + no_tth_adjust
  1102.         return [start % 3600, end % 3600]
  1103.  
  1104.     # Return a list of dicts with the next spawn times
  1105.     @classmethod
  1106.     def get_times(cls, cell, scan, now_date, scan_delay):
  1107.         l = []
  1108.         now_secs = date_secs(now_date)
  1109.         for sp_id in ScannedLocation.linked_spawn_points(cell):
  1110.  
  1111.             sp = SpawnPoint.get_by_id(sp_id)
  1112.  
  1113.             if sp['missed_count'] > 5:
  1114.                 continue
  1115.  
  1116.             endpoints = SpawnPoint.start_end(sp, scan_delay)
  1117.             cls.add_if_not_scanned('spawn', l, sp, scan, endpoints[0], endpoints[1], now_date, now_secs)
  1118.  
  1119.             # check to see if still searching for valid TTH
  1120.             if cls.tth_found(sp):
  1121.                 continue
  1122.  
  1123.             # add a spawnpoint check between latest seen and earliest seen
  1124.             start = sp['latest_seen'] + scan_delay
  1125.             end = sp['earliest_unseen']
  1126.  
  1127.             cls.add_if_not_scanned('TTH', l, sp, scan, start, end, now_date, now_secs)
  1128.  
  1129.         return l
  1130.  
  1131.     @classmethod
  1132.     def add_if_not_scanned(cls, kind, l, sp, scan, start, end, now_date, now_secs):
  1133.         # make sure later than now_secs
  1134.         while end < now_secs:
  1135.             start, end = start + 3600, end + 3600
  1136.  
  1137.         # ensure start before end
  1138.         while start > end:
  1139.             start -= 3600
  1140.  
  1141.         if (now_date - cls.get_by_id(sp['id'])['last_scanned']).total_seconds() > now_secs - start:
  1142.             l.append(ScannedLocation._q_init(scan, start, end, kind, sp['id']))
  1143.  
  1144.     # given seconds after the hour and a spawnpoint dict, return which quartile of the
  1145.     # spawnpoint the secs falls in
  1146.     @staticmethod
  1147.     def get_quartile(secs, sp):
  1148.         return int(((secs - sp['earliest_unseen'] + 15 * 60 + 3600 - 1) % 3600) / 15 / 60)
  1149.  
  1150.     @classmethod
  1151.     def select_in_hex(cls, center, steps):
  1152.         R = 6378.1  # km radius of the earth
  1153.         hdist = ((steps * 120.0) - 50.0) / 1000.0
  1154.         n, e, s, w = hex_bounds(center, steps)
  1155.  
  1156.         # get all spawns in that box
  1157.         sp = list(cls
  1158.                   .select()
  1159.                   .where((cls.latitude <= n) &
  1160.                          (cls.latitude >= s) &
  1161.                          (cls.longitude >= w) &
  1162.                          (cls.longitude <= e))
  1163.                   .dicts())
  1164.  
  1165.         # for each spawn work out if it is in the hex (clipping the diagonals)
  1166.         in_hex = []
  1167.         for spawn in sp:
  1168.             # get the offset from the center of each spawn in km
  1169.             offset = [math.radians(spawn['latitude'] - center[0]) * R,
  1170.                       math.radians(spawn['longitude'] - center[1]) * (R * math.cos(math.radians(center[0])))]
  1171.             # check agains the 4 lines that make up the diagonals
  1172.             if (offset[1] + (offset[0] * 0.5)) > hdist:  # too far ne
  1173.                 continue
  1174.             if (offset[1] - (offset[0] * 0.5)) > hdist:  # too far se
  1175.                 continue
  1176.             if ((offset[0] * 0.5) - offset[1]) > hdist:  # too far nw
  1177.                 continue
  1178.             if ((0 - offset[1]) - (offset[0] * 0.5)) > hdist:  # too far sw
  1179.                 continue
  1180.             # if it gets to here its  a good spawn
  1181.             in_hex.append(spawn)
  1182.         return in_hex
  1183.  
  1184.  
  1185. class ScanSpawnPoint(BaseModel):
  1186.     # removing ForeignKeyField due to MSQL issues with upserting rows that are foreignkeys for other tables
  1187.     # scannedlocation = ForeignKeyField(ScannedLocation)
  1188.     # spawnpoint = ForeignKeyField(SpawnPoint)
  1189.  
  1190.     scannedlocation = CharField(max_length=54)
  1191.     spawnpoint = CharField(max_length=54)
  1192.  
  1193.     class Meta:
  1194.         primary_key = CompositeKey('spawnpoint', 'scannedlocation')
  1195.  
  1196.  
  1197. class SpawnpointDetectionData(BaseModel):
  1198.     id = CharField(primary_key=True, max_length=54)
  1199.     encounter_id = CharField(max_length=54)  # removed ForeignKeyField since it caused MySQL issues
  1200.     spawnpoint_id = CharField(max_length=54)  # removed ForeignKeyField since it caused MySQL issues
  1201.     scan_time = DateTimeField()
  1202.     tth_secs = IntegerField(null=True)
  1203.  
  1204.     @staticmethod
  1205.     def set_default_earliest_unseen(sp):
  1206.         sp['earliest_unseen'] = (sp['latest_seen'] + 14 * 60) % 3600
  1207.  
  1208.     @classmethod
  1209.     def classify(cls, sp, scan_loc, now_secs, sighting=None):
  1210.  
  1211.         # to reduce CPU usage, give an intial reading of 15 min spawns if not done with initial scan of location
  1212.         if not scan_loc['done']:
  1213.             sp['kind'] = 'hhhs'
  1214.             if not sp['earliest_unseen']:
  1215.                 sp['latest_seen'] = now_secs
  1216.                 cls.set_default_earliest_unseen(sp)
  1217.  
  1218.             elif clock_between(sp['latest_seen'], now_secs, sp['earliest_unseen']):
  1219.                 sp['latest_seen'] = now_secs
  1220.  
  1221.             return
  1222.  
  1223.         # get past sightings
  1224.         query = list(cls.select()
  1225.                         .where(cls.spawnpoint_id == sp['id'])
  1226.                         .dicts())
  1227.  
  1228.         if sighting:
  1229.             query.append(sighting)
  1230.  
  1231.         # make a record of links, so we can reset earliest_unseen if it changes
  1232.         old_kind = str(sp['kind'])
  1233.  
  1234.         # make a sorted list of the seconds after the hour
  1235.         seen_secs = sorted(map(lambda x: date_secs(x['scan_time']), query))
  1236.  
  1237.         # add the first seen_secs to the end as a clock wrap around
  1238.         if seen_secs:
  1239.             seen_secs.append(seen_secs[0] + 3600)
  1240.  
  1241.         # make a list of gaps between sightings
  1242.         gap_list = [seen_secs[i + 1] - seen_secs[i] for i in range(len(seen_secs) - 1)]
  1243.  
  1244.         max_gap = max(gap_list)
  1245.  
  1246.         # an hour (60 min) minus the largest gap in minutes gives us the duration the spawn was there
  1247.         # round up to the nearest 15 min interval for our current best duration guess
  1248.         duration = (int((59 - max_gap / 60.0) / 15) + 1) * 15
  1249.  
  1250.         # if the second largest gap is larger than 15 minutes, then there are two gaps that are
  1251.         # greater than 15 min, so it must be a double-spawn
  1252.         if len(gap_list) > 4 and sorted(gap_list)[-2] > 900:
  1253.             sp['kind'] = 'hshs'
  1254.             sp['links'] = 'h?h?'
  1255.  
  1256.         else:
  1257.             # convert the duration into a 'hhhs', 'hhss', 'hsss', 'ssss' string accordingly
  1258.             # 's' is for seen, 'h' is for hidden
  1259.             sp['kind'] = ''.join(['s' if i > (3 - duration / 15) else 'h' for i in range(0, 4)])
  1260.  
  1261.         # assume no hidden times
  1262.         sp['links'] = sp['kind'].replace('s', '?')
  1263.  
  1264.         if sp['kind'] != 'ssss':
  1265.  
  1266.             if not sp['earliest_unseen'] or sp['earliest_unseen'] != sp['latest_seen']:
  1267.  
  1268.                 # new latest seen will be just before max_gap
  1269.                 sp['latest_seen'] = seen_secs[gap_list.index(max_gap)]
  1270.  
  1271.                 # if we don't have a earliest_unseen yet or the kind of spawn has changed, reset
  1272.                 # set to latest_seen + 14 min
  1273.                 if not sp['earliest_unseen'] or sp['kind'] != old_kind:
  1274.                     cls.set_default_earliest_unseen(sp)
  1275.  
  1276.             return
  1277.  
  1278.         # only ssss spawns from here below
  1279.  
  1280.         sp['links'] = '+++-'
  1281.         if sp['earliest_unseen'] == sp['latest_seen']:
  1282.             return
  1283.  
  1284.         # make a sight_list of dicts like
  1285.         # {date: first seen time,
  1286.         # delta: duration of sighting,
  1287.         # same: whether encounter ID was same or different over that time}
  1288.  
  1289.         # for 60 min spawns ('ssss'), the largest gap doesn't give the earliest spawn point,
  1290.         # because a pokemon is always there
  1291.         # use the union of all intervals where the same encounter ID was seen to find the latest_seen
  1292.         # If a different encounter ID was seen, then the complement of that interval was the same
  1293.         # ID, so union that complement as well
  1294.  
  1295.         sight_list = [{'date': query[i]['scan_time'],
  1296.                        'delta': query[i + 1]['scan_time'] - query[i]['scan_time'],
  1297.                        'same': query[i + 1]['encounter_id'] == query[i]['encounter_id']}
  1298.                       for i in range(len(query) - 1)
  1299.                       if query[i + 1]['scan_time'] - query[i]['scan_time'] < timedelta(hours=1)]
  1300.  
  1301.         start_end_list = []
  1302.         for s in sight_list:
  1303.             if s['same']:
  1304.                 # get the seconds past the hour for start and end times
  1305.                 start = date_secs(s['date'])
  1306.                 end = (start + int(s['delta'].total_seconds())) % 3600
  1307.  
  1308.             else:
  1309.                 # convert diff range to same range by taking the clock complement
  1310.                 start = date_secs(s['date'] + s['delta']) % 3600
  1311.                 end = date_secs(s['date'])
  1312.  
  1313.             start_end_list.append([start, end])
  1314.  
  1315.         # Take the union of all the ranges
  1316.         while True:
  1317.             # union is list of unions of ranges with the same encounter id
  1318.             union = []
  1319.             for start, end in start_end_list:
  1320.                 if not union:
  1321.                     union.append([start, end])
  1322.                     continue
  1323.                 # cycle through all ranges in union, since it might overlap with any of them
  1324.                 for u in union:
  1325.                     if clock_between(u[0], start, u[1]):
  1326.                         u[1] = end if not(clock_between(u[0], end, u[1])) else u[1]
  1327.                     elif clock_between(u[0], end, u[1]):
  1328.                         u[0] = start if not(clock_between(u[0], start, u[1])) else u[0]
  1329.                     elif union.count([start, end]) == 0:
  1330.                         union.append([start, end])
  1331.  
  1332.             # Are no more unions possible?
  1333.             if union == start_end_list:
  1334.                 break
  1335.             else:
  1336.                 start_end_list = union  # Make another pass looking for unions
  1337.  
  1338.         # if more than one disparate union, take the largest as our starting point
  1339.         union = reduce(lambda x, y: x if (x[1] - x[0]) % 3600 > (y[1] - y[0]) % 3600 else y,
  1340.                        union, [0, 3600])
  1341.         sp['latest_seen'] = union[1]
  1342.         sp['earliest_unseen'] = union[0]
  1343.         log.info('1x60: appear %d, despawn %d, duration: %d min', union[0], union[1], ((union[1] - union[0]) % 3600) / 60)
  1344.  
  1345.     # expand the seen times for 30 minute spawnpoints based on scans when spawn wasn't there
  1346.     # return true if spawnpoint dict changed
  1347.     @classmethod
  1348.     def unseen(cls, sp, now_secs):
  1349.  
  1350.         # return if we already have a tth
  1351.         if sp['latest_seen'] == sp['earliest_unseen']:
  1352.             return False
  1353.  
  1354.         # if now_secs is later than the latest seen return
  1355.         if not clock_between(sp['latest_seen'], now_secs, sp['earliest_unseen']):
  1356.             return False
  1357.  
  1358.         sp['earliest_unseen'] = now_secs
  1359.  
  1360.         return True
  1361.  
  1362.     # expand a 30 minute spawn with a new seen point based on which endpoint it is closer to
  1363.     # return true if sp changed
  1364.     @classmethod
  1365.     def clock_extend(cls, sp, new_secs):
  1366.         # check if this is a new earliest time
  1367.         if clock_between(sp['earliest_seen'], new_secs, sp['latest_seen']):
  1368.             return False
  1369.  
  1370.         # extend earliest or latest seen depending on which is closer to the new point
  1371.         if secs_between(new_secs, sp['earliest_seen']) < secs_between(new_secs, sp['latest_seen']):
  1372.             sp['earliest_seen'] = new_secs
  1373.         else:
  1374.             sp['latest_seen'] = new_secs
  1375.  
  1376.         return True
  1377.  
  1378.  
  1379. class Versions(flaskDb.Model):
  1380.     key = CharField()
  1381.     val = IntegerField()
  1382.  
  1383.     class Meta:
  1384.         primary_key = False
  1385.  
  1386.  
  1387. class GymMember(BaseModel):
  1388.     gym_id = CharField(index=True)
  1389.     pokemon_uid = CharField()
  1390.     last_scanned = DateTimeField(default=datetime.utcnow)
  1391.  
  1392.     class Meta:
  1393.         primary_key = False
  1394.  
  1395.  
  1396. class GymPokemon(BaseModel):
  1397.     pokemon_uid = CharField(primary_key=True, max_length=50)
  1398.     pokemon_id = IntegerField()
  1399.     cp = IntegerField()
  1400.     trainer_name = CharField()
  1401.     num_upgrades = IntegerField(null=True)
  1402.     move_1 = IntegerField(null=True)
  1403.     move_2 = IntegerField(null=True)
  1404.     height = FloatField(null=True)
  1405.     weight = FloatField(null=True)
  1406.     stamina = IntegerField(null=True)
  1407.     stamina_max = IntegerField(null=True)
  1408.     cp_multiplier = FloatField(null=True)
  1409.     additional_cp_multiplier = FloatField(null=True)
  1410.     iv_defense = IntegerField(null=True)
  1411.     iv_stamina = IntegerField(null=True)
  1412.     iv_attack = IntegerField(null=True)
  1413.     last_seen = DateTimeField(default=datetime.utcnow)
  1414.  
  1415.  
  1416. class Trainer(BaseModel):
  1417.     name = CharField(primary_key=True, max_length=50)
  1418.     team = IntegerField()
  1419.     level = IntegerField()
  1420.     last_seen = DateTimeField(default=datetime.utcnow)
  1421.  
  1422.  
  1423. class GymDetails(BaseModel):
  1424.     gym_id = CharField(primary_key=True, max_length=50)
  1425.     name = CharField()
  1426.     description = TextField(null=True, default="")
  1427.     url = CharField()
  1428.     last_scanned = DateTimeField(default=datetime.utcnow)
  1429.  
  1430.  
  1431. def hex_bounds(center, steps=None, radius=None):
  1432.     # Make a box that is (70m * step_limit * 2) + 70m away from the center point
  1433.     # Rationale is that you need to travel
  1434.     sp_dist = 0.07 * (2 * steps + 1) if steps else radius
  1435.     n = get_new_coords(center, sp_dist, 0)[0]
  1436.     e = get_new_coords(center, sp_dist, 90)[1]
  1437.     s = get_new_coords(center, sp_dist, 180)[0]
  1438.     w = get_new_coords(center, sp_dist, 270)[1]
  1439.     return (n, e, s, w)
  1440.  
  1441.  
  1442. # todo: this probably shouldn't _really_ be in "models" anymore, but w/e
  1443. def parse_map(args, map_dict, step_location, db_update_queue, wh_update_queue, api, now_date):
  1444.     pokemons = {}
  1445.     pokestops = {}
  1446.     gyms = {}
  1447.     skipped = 0
  1448.     stopsskipped = 0
  1449.     forts = []
  1450.     wild_pokemon = []
  1451.     nearby_pokemons = []
  1452.     spawn_points = {}
  1453.     scan_spawn_points = {}
  1454.     sightings = {}
  1455.     new_spawn_points = []
  1456.     sp_id_list = []
  1457.     now_secs = date_secs(now_date)
  1458.    
  1459.     # consolidate the individual lists in each cell into one list of pokemon and a list of forts
  1460.     cells = map_dict['responses']['GET_MAP_OBJECTS']['map_cells']
  1461.     for cell in cells:
  1462.         nearby_pokemons += cell.get('nearby_pokemons', [])
  1463.         if config['parse_pokemon']:
  1464.             wild_pokemon += cell.get('wild_pokemons', [])
  1465.  
  1466.         if config['parse_pokestops'] or config['parse_gyms']:
  1467.             forts += cell.get('forts', [])
  1468.  
  1469.     # Check for a 0/0/0 bad scan
  1470.     # If we saw nothing and there should be visible forts, it's bad
  1471.     if not len(wild_pokemon) and not len(forts) and ScannedLocation.visible_forts(step_location):
  1472.         log.warning('Bad scan. Parsing found 0/0/0 pokemons/pokestops/gyms')
  1473.         log.info('Common causes: captchas, IP bans, or using -ng and -nk arguments')
  1474.         return {
  1475.             'count': 0,
  1476.             'gyms': gyms,
  1477.             'spawn_points': spawn_points,
  1478.             'bad_scan': True
  1479.         }
  1480.  
  1481.     if not len(nearby_pokemons) and not len(wild_pokemon):
  1482.         log.warning('Nothing on nearby_pokemons or wild. Speed violation?')
  1483.         log.info("Common causes: not using -speed, deleting or dropping the WorkerStatus table without waiting before restarting, or there really aren't any pokemon in 200m")
  1484.  
  1485.     scan_loc = ScannedLocation.get_by_loc(step_location)
  1486.    
  1487.     if len(wild_pokemon):
  1488.         encounter_ids = [b64encode(str(p['encounter_id'])) for p in wild_pokemon]
  1489.         # For all the wild Pokemon we found check if an active Pokemon is in the database.
  1490.         query = (Pokemon
  1491.                  .select(Pokemon.encounter_id, Pokemon.spawnpoint_id)
  1492.                  .where((Pokemon.disappear_time > datetime.utcnow()) & (Pokemon.encounter_id << encounter_ids))
  1493.                  .dicts())
  1494.  
  1495.         # Store all encounter_ids and spawnpoint_id for the pokemon in query (all thats needed to make sure its unique).
  1496.         encountered_pokemon = [(p['encounter_id'], p['spawnpoint_id']) for p in query]
  1497.  
  1498.         for p in wild_pokemon:
  1499.  
  1500.             sp = SpawnPoint.get_by_id(p['spawn_point_id'], p['latitude'], p['longitude'])
  1501.             spawn_points[p['spawn_point_id']] = sp
  1502.             sp['missed_count'] = 0
  1503.  
  1504.             sighting = {
  1505.                 'id': b64encode(str(p['encounter_id'])) + '_' + str(now_secs),
  1506.                 'encounter_id': b64encode(str(p['encounter_id'])),
  1507.                 'spawnpoint_id': p['spawn_point_id'],
  1508.                 'scan_time': now_date,
  1509.                 'tth_secs': None
  1510.             }
  1511.  
  1512.             sp_id_list.append(p['spawn_point_id'])  # keep a list of sp_ids to return
  1513.  
  1514.             # time_till_hidden_ms was overflowing causing a negative integer.
  1515.             # It was also returning a value above 3.6M ms.
  1516.             if 0 < p['time_till_hidden_ms'] < 3600000:
  1517.                 d_t_secs = date_secs(datetime.utcfromtimestamp((p['last_modified_timestamp_ms'] + p['time_till_hidden_ms']) / 1000.0))
  1518.                 if sp['latest_seen'] != sp['earliest_unseen']:
  1519.                     log.info('TTH found for spawnpoint %s', sp['id'])
  1520.                     sighting['tth_secs'] = d_t_secs
  1521.                 sp['latest_seen'] = d_t_secs
  1522.                 sp['earliest_unseen'] = d_t_secs
  1523.  
  1524.             scan_spawn_points[scan_loc['cellid'] + sp['id']] = {'spawnpoint': sp['id'],
  1525.                                                                 'scannedlocation': scan_loc['cellid']}
  1526.             if not sp['last_scanned']:
  1527.                 log.info('New Spawn Point found!')
  1528.                 new_spawn_points.append(sp)
  1529.  
  1530.                 # if we found a new spawnpoint after the location was already fully scanned
  1531.                 # either it's new, or we had a bad scan. Either way, rescan the loc
  1532.                 if scan_loc['done']:
  1533.                     log.warning('Location was fully scanned, and yet a brand new spawnpoint found.')
  1534.                     log.warning('Redoing scan of this location to identify new spawnpoint.')
  1535.                     ScannedLocation.reset_bands(scan_loc)
  1536.  
  1537.             if (not SpawnPoint.tth_found(sp) or sighting['tth_secs'] or not scan_loc['done']):
  1538.                 SpawnpointDetectionData.classify(sp, scan_loc, now_secs, sighting)
  1539.                 sightings[p['encounter_id']] = sighting
  1540.  
  1541.             sp['last_scanned'] = datetime.utcfromtimestamp(p['last_modified_timestamp_ms'] / 1000.0)
  1542.  
  1543.             if (b64encode(str(p['encounter_id'])), p['spawn_point_id']) in encountered_pokemon:
  1544.                 # If pokemon has been encountered before dont process it.
  1545.                 skipped += 1
  1546.                 continue
  1547.  
  1548.             seconds_until_despawn = (SpawnPoint.start_end(sp)[1] - now_secs) % 3600
  1549.             disappear_time = now_date + timedelta(seconds=seconds_until_despawn)
  1550.  
  1551.             printPokemon(p['pokemon_data']['pokemon_id'], p['latitude'], p['longitude'], disappear_time)
  1552.  
  1553.             # Scan for IVs and moves.
  1554.             encounter_result = None
  1555.             if (args.encounter and (p['pokemon_data']['pokemon_id'] in args.encounter_whitelist or
  1556.                                     p['pokemon_data']['pokemon_id'] not in args.encounter_blacklist and not args.encounter_whitelist)):
  1557.                 time.sleep(args.encounter_delay)
  1558.                 # Set up encounter request envelope
  1559.                 req = api.create_request()
  1560.                 encounter_result = req.encounter(encounter_id=p['encounter_id'],
  1561.                                                  spawn_point_id=p['spawn_point_id'],
  1562.                                                  player_latitude=step_location[0],
  1563.                                                  player_longitude=step_location[1])
  1564.                 encounter_result = req.check_challenge()
  1565.                 encounter_result = req.get_hatched_eggs()
  1566.                 encounter_result = req.get_inventory()
  1567.                 encounter_result = req.check_awarded_badges()
  1568.                 encounter_result = req.download_settings()
  1569.                 encounter_result = req.get_buddy_walked()
  1570.                 encounter_result = req.call()
  1571.  
  1572.             pokemons[p['encounter_id']] = {
  1573.                 'encounter_id': b64encode(str(p['encounter_id'])),
  1574.                 'spawnpoint_id': p['spawn_point_id'],
  1575.                 'pokemon_id': p['pokemon_data']['pokemon_id'],
  1576.                 'latitude': p['latitude'],
  1577.                 'longitude': p['longitude'],
  1578.                 'disappear_time': disappear_time,
  1579.                 'individual_attack': None,
  1580.                 'individual_defense': None,
  1581.                 'individual_stamina': None,
  1582.                 'move_1': None,
  1583.                 'move_2': None
  1584.             }
  1585.  
  1586.             if encounter_result is not None and 'wild_pokemon' in encounter_result['responses']['ENCOUNTER']:
  1587.                 pokemon_info = encounter_result['responses']['ENCOUNTER']['wild_pokemon']['pokemon_data']
  1588.                 pokemons[p['encounter_id']].update({
  1589.                     'individual_attack': pokemon_info.get('individual_attack', 0),
  1590.                     'individual_defense': pokemon_info.get('individual_defense', 0),
  1591.                     'individual_stamina': pokemon_info.get('individual_stamina', 0),
  1592.                     'move_1': pokemon_info['move_1'],
  1593.                     'move_2': pokemon_info['move_2'],
  1594.                 })
  1595.  
  1596.             if args.webhooks:
  1597.  
  1598.                 wh_poke = pokemons[p['encounter_id']].copy()
  1599.                 wh_poke.update({
  1600.                     'disappear_time': calendar.timegm(disappear_time.timetuple()),
  1601.                     'last_modified_time': p['last_modified_timestamp_ms'],
  1602.                     'time_until_hidden_ms': p['time_till_hidden_ms']
  1603.                 })
  1604.                 wh_update_queue.put(('pokemon', wh_poke))
  1605.  
  1606.     if len(forts):
  1607.         if config['parse_pokestops']:
  1608.             stop_ids = [f['id'] for f in forts if f.get('type') == 1]
  1609.             if len(stop_ids) > 0:
  1610.                 query = (Pokestop
  1611.                          .select(Pokestop.pokestop_id, Pokestop.last_modified)
  1612.                          .where((Pokestop.pokestop_id << stop_ids))
  1613.                          .dicts())
  1614.                 encountered_pokestops = [(f['pokestop_id'], int((f['last_modified'] - datetime(1970, 1, 1)).total_seconds())) for f in query]
  1615.                
  1616.         #spin
  1617.         spin_pokestop(api, (step_location[0], step_location[1]), forts)
  1618.                
  1619.         for f in forts:
  1620.             if config['parse_pokestops'] and f.get('type') == 1:  # Pokestops.
  1621.                 if 'active_fort_modifier' in f:
  1622.                     lure_expiration = datetime.utcfromtimestamp(
  1623.                         f['last_modified_timestamp_ms'] / 1000.0) + timedelta(minutes=30)
  1624.                     active_fort_modifier = f['active_fort_modifier']
  1625.                     if args.webhooks and args.webhook_updates_only:
  1626.                         wh_update_queue.put(('pokestop', {
  1627.                             'pokestop_id': b64encode(str(f['id'])),
  1628.                             'enabled': f['enabled'],
  1629.                             'latitude': f['latitude'],
  1630.                             'longitude': f['longitude'],
  1631.                             'last_modified_time': f['last_modified_timestamp_ms'],
  1632.                             'lure_expiration': calendar.timegm(lure_expiration.timetuple()),
  1633.                             'active_fort_modifier': active_fort_modifier
  1634.                         }))
  1635.                 else:
  1636.                     lure_expiration, active_fort_modifier = None, None
  1637.  
  1638.                 # Send all pokestops to webhooks.
  1639.                 if args.webhooks and not args.webhook_updates_only:
  1640.                     # Explicitly set 'webhook_data', in case we want to change the information pushed to webhooks,
  1641.                     # similar to above and previous commits.
  1642.                     l_e = None
  1643.  
  1644.                     if lure_expiration is not None:
  1645.                         l_e = calendar.timegm(lure_expiration.timetuple())
  1646.  
  1647.                     wh_update_queue.put(('pokestop', {
  1648.                         'pokestop_id': b64encode(str(f['id'])),
  1649.                         'enabled': f['enabled'],
  1650.                         'latitude': f['latitude'],
  1651.                         'longitude': f['longitude'],
  1652.                         'last_modified': f['last_modified_timestamp_ms'],
  1653.                         'lure_expiration': l_e,
  1654.                         'active_fort_modifier': active_fort_modifier
  1655.                     }))
  1656.  
  1657.                 if (f['id'], int(f['last_modified_timestamp_ms'] / 1000.0)) in encountered_pokestops:
  1658.                     # If pokestop has been encountered before and hasn't changed dont process it.
  1659.                     stopsskipped += 1
  1660.                     continue
  1661.  
  1662.                 pokestops[f['id']] = {
  1663.                     'pokestop_id': f['id'],
  1664.                     'enabled': f['enabled'],
  1665.                     'latitude': f['latitude'],
  1666.                     'longitude': f['longitude'],
  1667.                     'last_modified': datetime.utcfromtimestamp(
  1668.                         f['last_modified_timestamp_ms'] / 1000.0),
  1669.                     'lure_expiration': lure_expiration,
  1670.                     'active_fort_modifier': active_fort_modifier
  1671.                 }
  1672.  
  1673.             elif config['parse_gyms'] and f.get('type') is None:  # Currently, there are only stops and gyms
  1674.                 # Send gyms to webhooks.
  1675.                 if args.webhooks and not args.webhook_updates_only:
  1676.                     # Explicitly set 'webhook_data', in case we want to change the information pushed to webhooks,
  1677.                     # similar to above and previous commits.
  1678.                     wh_update_queue.put(('gym', {
  1679.                         'gym_id': b64encode(str(f['id'])),
  1680.                         'team_id': f.get('owned_by_team', 0),
  1681.                         'guard_pokemon_id': f.get('guard_pokemon_id', 0),
  1682.                         'gym_points': f.get('gym_points', 0),
  1683.                         'enabled': f['enabled'],
  1684.                         'latitude': f['latitude'],
  1685.                         'longitude': f['longitude'],
  1686.                         'last_modified': f['last_modified_timestamp_ms']
  1687.                     }))
  1688.  
  1689.                 gyms[f['id']] = {
  1690.                     'gym_id': f['id'],
  1691.                     'team_id': f.get('owned_by_team', 0),
  1692.                     'guard_pokemon_id': f.get('guard_pokemon_id', 0),
  1693.                     'gym_points': f.get('gym_points', 0),
  1694.                     'enabled': f['enabled'],
  1695.                     'latitude': f['latitude'],
  1696.                     'longitude': f['longitude'],
  1697.                     'last_modified': datetime.utcfromtimestamp(
  1698.                         f['last_modified_timestamp_ms'] / 1000.0),
  1699.                 }
  1700.  
  1701.     log.info('Parsing found %d pokemons, %d pokestops, and %d gyms.',
  1702.              len(pokemons) + skipped,
  1703.              len(pokestops) + stopsskipped,
  1704.              len(gyms))
  1705.  
  1706.     log.debug('Skipped %d Pokemons and %d pokestops.', skipped, stopsskipped)
  1707.  
  1708.     # look for spawnpoints within scan_loc that are not here to see if can narrow down tth window
  1709.     for sp_id in ScannedLocation.linked_spawn_points(scan_loc['cellid']):
  1710.         if sp_id in sp_id_list:
  1711.             sp = spawn_points[sp_id]
  1712.         # not seen and not a speed violation
  1713.         else:
  1714.             sp = SpawnPoint.get_by_id(sp_id)
  1715.             if SpawnpointDetectionData.unseen(sp, now_secs):
  1716.                 spawn_points[sp['id']] = sp
  1717.             endpoints = SpawnPoint.start_end(sp, args.spawn_delay)
  1718.             if clock_between(endpoints[0], now_secs, endpoints[1]):
  1719.                 sp['missed_count'] += 1
  1720.                 spawn_points[sp['id']] = sp
  1721.                 log.warning('%s kind spawnpoint %s has no pokemon %d times in a row',
  1722.                             sp['kind'], sp['id'], sp['missed_count'])
  1723.                 log.info('Possible causes: Still doing initial scan, or super rare double spawnpoint during hidden period, or Niantic has removed spawnpoint')
  1724.  
  1725.         if (not SpawnPoint.tth_found(sp) and scan_loc['done'] and
  1726.                 (sp['earliest_unseen'] - sp['latest_seen'] - args.spawn_delay) % 3600 < 60):
  1727.             log.warning('Spawnpoint %s was unable to locate a TTH, with only %ss after pokemon last seen',
  1728.                         sp['id'], (sp['earliest_unseen'] - sp['latest_seen']) % 3600)
  1729.             log.info('Embiggening search for TTH by 15 minutes to try again')
  1730.             if sp_id not in sp_id_list:
  1731.                 SpawnpointDetectionData.classify(sp, scan_loc, now_secs)
  1732.             sp['latest_seen'] = (sp['latest_seen'] - 60) % 3600
  1733.             sp['earliest_unseen'] = (sp['earliest_unseen'] + 14 * 60) % 3600
  1734.             spawn_points[sp_id] = sp
  1735.  
  1736.     ScannedLocation.update_band(scan_loc)  # updating here so the last scan data isn't ignored by 'done'
  1737.  
  1738.     db_update_queue.put((ScannedLocation, {0: scan_loc}))
  1739.  
  1740.     if len(pokemons):
  1741.         db_update_queue.put((Pokemon, pokemons))
  1742.     if len(pokestops):
  1743.         db_update_queue.put((Pokestop, pokestops))
  1744.     if len(gyms):
  1745.         db_update_queue.put((Gym, gyms))
  1746.     if len(spawn_points):
  1747.         db_update_queue.put((SpawnPoint, spawn_points))
  1748.         db_update_queue.put((ScanSpawnPoint, scan_spawn_points))
  1749.         if len(sightings):
  1750.             db_update_queue.put((SpawnpointDetectionData, sightings))
  1751.  
  1752.     return {
  1753.         'count': len(wild_pokemon) + len(forts),
  1754.         'gyms': gyms,
  1755.         'sp_id_list': sp_id_list,
  1756.         'bad_scan': False
  1757.     }
  1758.  
  1759.  
  1760. def parse_gyms(args, gym_responses, wh_update_queue):
  1761.     gym_details = {}
  1762.     gym_members = {}
  1763.     gym_pokemon = {}
  1764.     trainers = {}
  1765.  
  1766.     i = 0
  1767.     for g in gym_responses.values():
  1768.         gym_state = g['gym_state']
  1769.         gym_id = gym_state['fort_data']['id']
  1770.  
  1771.         gym_details[gym_id] = {
  1772.             'gym_id': gym_id,
  1773.             'name': g['name'],
  1774.             'description': g.get('description'),
  1775.             'url': g['urls'][0],
  1776.         }
  1777.  
  1778.         if args.webhooks:
  1779.             webhook_data = {
  1780.                 'id': gym_id,
  1781.                 'latitude': gym_state['fort_data']['latitude'],
  1782.                 'longitude': gym_state['fort_data']['longitude'],
  1783.                 'team': gym_state['fort_data'].get('owned_by_team', 0),
  1784.                 'name': g['name'],
  1785.                 'description': g.get('description'),
  1786.                 'url': g['urls'][0],
  1787.                 'pokemon': [],
  1788.             }
  1789.  
  1790.         for member in gym_state.get('memberships', []):
  1791.             gym_members[i] = {
  1792.                 'gym_id': gym_id,
  1793.                 'pokemon_uid': member['pokemon_data']['id'],
  1794.             }
  1795.  
  1796.             gym_pokemon[i] = {
  1797.                 'pokemon_uid': member['pokemon_data']['id'],
  1798.                 'pokemon_id': member['pokemon_data']['pokemon_id'],
  1799.                 'cp': member['pokemon_data']['cp'],
  1800.                 'trainer_name': member['trainer_public_profile']['name'],
  1801.                 'num_upgrades': member['pokemon_data'].get('num_upgrades', 0),
  1802.                 'move_1': member['pokemon_data'].get('move_1'),
  1803.                 'move_2': member['pokemon_data'].get('move_2'),
  1804.                 'height': member['pokemon_data'].get('height_m'),
  1805.                 'weight': member['pokemon_data'].get('weight_kg'),
  1806.                 'stamina': member['pokemon_data'].get('stamina'),
  1807.                 'stamina_max': member['pokemon_data'].get('stamina_max'),
  1808.                 'cp_multiplier': member['pokemon_data'].get('cp_multiplier'),
  1809.                 'additional_cp_multiplier': member['pokemon_data'].get('additional_cp_multiplier', 0),
  1810.                 'iv_defense': member['pokemon_data'].get('individual_defense', 0),
  1811.                 'iv_stamina': member['pokemon_data'].get('individual_stamina', 0),
  1812.                 'iv_attack': member['pokemon_data'].get('individual_attack', 0),
  1813.                 'last_seen': datetime.utcnow(),
  1814.             }
  1815.  
  1816.             trainers[i] = {
  1817.                 'name': member['trainer_public_profile']['name'],
  1818.                 'team': gym_state['fort_data']['owned_by_team'],
  1819.                 'level': member['trainer_public_profile']['level'],
  1820.                 'last_seen': datetime.utcnow(),
  1821.             }
  1822.  
  1823.             if args.webhooks:
  1824.                 webhook_data['pokemon'].append({
  1825.                     'pokemon_uid': member['pokemon_data']['id'],
  1826.                     'pokemon_id': member['pokemon_data']['pokemon_id'],
  1827.                     'cp': member['pokemon_data']['cp'],
  1828.                     'num_upgrades': member['pokemon_data'].get('num_upgrades', 0),
  1829.                     'move_1': member['pokemon_data'].get('move_1'),
  1830.                     'move_2': member['pokemon_data'].get('move_2'),
  1831.                     'height': member['pokemon_data'].get('height_m'),
  1832.                     'weight': member['pokemon_data'].get('weight_kg'),
  1833.                     'stamina': member['pokemon_data'].get('stamina'),
  1834.                     'stamina_max': member['pokemon_data'].get('stamina_max'),
  1835.                     'cp_multiplier': member['pokemon_data'].get('cp_multiplier'),
  1836.                     'additional_cp_multiplier': member['pokemon_data'].get('additional_cp_multiplier', 0),
  1837.                     'iv_defense': member['pokemon_data'].get('individual_defense', 0),
  1838.                     'iv_stamina': member['pokemon_data'].get('individual_stamina', 0),
  1839.                     'iv_attack': member['pokemon_data'].get('individual_attack', 0),
  1840.                     'trainer_name': member['trainer_public_profile']['name'],
  1841.                     'trainer_level': member['trainer_public_profile']['level'],
  1842.                 })
  1843.  
  1844.             i += 1
  1845.         if args.webhooks:
  1846.             wh_update_queue.put(('gym_details', webhook_data))
  1847.  
  1848.     # All this database stuff is synchronous (not using the upsert queue) on purpose.
  1849.     # Since the search workers load the GymDetails model from the database to determine if a gym
  1850.     # needs rescanned, we need to be sure the GymDetails get fully committed to the database before moving on.
  1851.     #
  1852.     # We _could_ synchronously upsert GymDetails, then queue the other tables for
  1853.     # upsert, but that would put that Gym's overall information in a weird non-atomic state.
  1854.  
  1855.     # Upsert all the models.
  1856.     if len(gym_details):
  1857.         bulk_upsert(GymDetails, gym_details)
  1858.     if len(gym_pokemon):
  1859.         bulk_upsert(GymPokemon, gym_pokemon)
  1860.     if len(trainers):
  1861.         bulk_upsert(Trainer, trainers)
  1862.  
  1863.     # This needs to be completed in a transaction, because we don't wany any other thread or process
  1864.     # to mess with the GymMembers for the gyms we're updating while we're updating the bridge table.
  1865.     with flaskDb.database.transaction():
  1866.         # Get rid of all the gym members, we're going to insert new records.
  1867.         if len(gym_details):
  1868.             DeleteQuery(GymMember).where(GymMember.gym_id << gym_details.keys()).execute()
  1869.  
  1870.         # Insert new gym members.
  1871.         if len(gym_members):
  1872.             bulk_upsert(GymMember, gym_members)
  1873.  
  1874.     log.info('Upserted %d gyms and %d gym members',
  1875.              len(gym_details),
  1876.              len(gym_members))
  1877.  
  1878.  
  1879. def db_updater(args, q):
  1880.     # The forever loop.
  1881.     while True:
  1882.         try:
  1883.  
  1884.             while True:
  1885.                 try:
  1886.                     flaskDb.connect_db()
  1887.                     break
  1888.                 except Exception as e:
  1889.                     log.warning('%s... Retrying', e)
  1890.  
  1891.             # Loop the queue.
  1892.             while True:
  1893.                 model, data = q.get()
  1894.                 bulk_upsert(model, data)
  1895.                 q.task_done()
  1896.                 log.debug('Upserted to %s, %d records (upsert queue remaining: %d)',
  1897.                           model.__name__,
  1898.                           len(data),
  1899.                           q.qsize())
  1900.                 if q.qsize() > 50:
  1901.                     log.warning("DB queue is > 50 (@%d); try increasing --db-threads", q.qsize())
  1902.  
  1903.         except Exception as e:
  1904.             log.exception('Exception in db_updater: %s', e)
  1905.  
  1906.  
  1907. def clean_db_loop(args):
  1908.     while True:
  1909.         try:
  1910.             query = (MainWorker
  1911.                      .delete()
  1912.                      .where((ScannedLocation.last_modified <
  1913.                              (datetime.utcnow() - timedelta(minutes=30)))))
  1914.             query.execute()
  1915.  
  1916.             query = (WorkerStatus
  1917.                      .delete()
  1918.                      .where((ScannedLocation.last_modified <
  1919.                              (datetime.utcnow() - timedelta(minutes=30)))))
  1920.             query.execute()
  1921.  
  1922.             # Remove active modifier from expired lured pokestops.
  1923.             query = (Pokestop
  1924.                      .update(lure_expiration=None, active_fort_modifier=None)
  1925.                      .where(Pokestop.lure_expiration < datetime.utcnow()))
  1926.             query.execute()
  1927.  
  1928.             # If desired, clear old pokemon spawns.
  1929.             if args.purge_data > 0:
  1930.                 query = (Pokemon
  1931.                          .delete()
  1932.                          .where((Pokemon.disappear_time <
  1933.                                 (datetime.utcnow() - timedelta(hours=args.purge_data)))))
  1934.                 query.execute()
  1935.  
  1936.             log.info('Regular database cleaning complete')
  1937.             time.sleep(60)
  1938.         except Exception as e:
  1939.             log.exception('Exception in clean_db_loop: %s', e)
  1940.  
  1941.  
  1942. def bulk_upsert(cls, data):
  1943.     num_rows = len(data.values())
  1944.     i = 0
  1945.  
  1946.     if args.db_type == 'mysql':
  1947.         step = 120
  1948.     else:
  1949.         # SQLite has a default max number of parameters of 999,
  1950.         # so we need to limit how many rows we insert for it.
  1951.         step = 50
  1952.  
  1953.     while i < num_rows:
  1954.         log.debug('Inserting items %d to %d', i, min(i + step, num_rows))
  1955.         try:
  1956.             InsertQuery(cls, rows=data.values()[i:min(i + step, num_rows)]).upsert().execute()
  1957.         except Exception as e:
  1958.             # if there is a DB table constraint error, dump the data and don't retry
  1959.             # unrecoverable error strings:
  1960.             unrecoverable = ['constraint', 'has no attribute', 'peewee.IntegerField object at']
  1961.             has_unrecoverable = filter(lambda x: x in str(e), unrecoverable)
  1962.             if has_unrecoverable:
  1963.                 log.warning('%s. Data is:', e)
  1964.                 log.warning(data.items())
  1965.             else:
  1966.                 log.warning('%s... Retrying', e)
  1967.                 time.sleep(1)
  1968.                 continue
  1969.  
  1970.         i += step
  1971.  
  1972.  
  1973. def create_tables(db):
  1974.     db.connect()
  1975.     verify_database_schema(db)
  1976.     db.create_tables([Pokemon, Pokestop, Gym, ScannedLocation, GymDetails, GymMember, GymPokemon,
  1977.                       Trainer, MainWorker, WorkerStatus, SpawnPoint, ScanSpawnPoint, SpawnpointDetectionData], safe=True)
  1978.     db.close()
  1979.  
  1980.  
  1981. def drop_tables(db):
  1982.     db.connect()
  1983.     db.drop_tables([Pokemon, Pokestop, Gym, ScannedLocation, Versions, GymDetails, GymMember, GymPokemon,
  1984.                     Trainer, MainWorker, WorkerStatus, SpawnPoint, ScanSpawnPoint, SpawnpointDetectionData, Versions], safe=True)
  1985.     db.close()
  1986.  
  1987.  
  1988. def verify_database_schema(db):
  1989.     if not Versions.table_exists():
  1990.         db.create_tables([Versions])
  1991.  
  1992.         if ScannedLocation.table_exists():
  1993.             # Versions table didn't exist, but there were tables. This must mean the user
  1994.             # is coming from a database that existed before we started tracking the schema
  1995.             # version. Perform a full upgrade.
  1996.             InsertQuery(Versions, {Versions.key: 'schema_version', Versions.val: 0}).execute()
  1997.             database_migrate(db, 0)
  1998.         else:
  1999.             InsertQuery(Versions, {Versions.key: 'schema_version', Versions.val: db_schema_version}).execute()
  2000.  
  2001.     else:
  2002.         db_ver = Versions.get(Versions.key == 'schema_version').val
  2003.  
  2004.         if db_ver < db_schema_version:
  2005.             database_migrate(db, db_ver)
  2006.  
  2007.         elif db_ver > db_schema_version:
  2008.             log.error("Your database version (%i) appears to be newer than the code supports (%i).",
  2009.                       db_ver, db_schema_version)
  2010.             log.error("Please upgrade your code base or drop all tables in your database.")
  2011.             sys.exit(1)
  2012.  
  2013. def spin_pokestop(api, position, pokestops):
  2014.     pokestops_in_range = get_forts_in_range(pokestops, position)
  2015.     SPIN_REQUEST_RESULT_SUCCESS = 1
  2016.     SPIN_REQUEST_RESULT_OUT_OF_RANGE = 2
  2017.     SPIN_REQUEST_RESULT_IN_COOLDOWN_PERIOD = 3
  2018.     SPIN_REQUEST_RESULT_INVENTORY_FULL = 4
  2019.     for pokestop in pokestops_in_range:
  2020.         if pokestop.get('type') != 1:
  2021.             log.info("That was a gym")
  2022.             continue
  2023.         log.info("Found a pokestop in spin range")
  2024.         time.sleep(15)
  2025.         req = api.create_request()
  2026.         response_dict = req.fort_search(fort_id=pokestop['id'],
  2027.                         fort_latitude=pokestop['latitude'],
  2028.                         fort_longitude=pokestop['longitude'],
  2029.                         player_latitude=pokestop['latitude'],
  2030.                         player_longitude=pokestop['longitude']
  2031.                         )  
  2032.         response_dict = req.call()
  2033.         #log.info(response_dict)
  2034.         if ('responses' in response_dict) and ('FORT_SEARCH' in response_dict['responses']):
  2035.                 spin_details = response_dict['responses']['FORT_SEARCH']
  2036.                 spin_result = spin_details.get('result', -1)
  2037.                 if (spin_result == SPIN_REQUEST_RESULT_SUCCESS) or (spin_result == SPIN_REQUEST_RESULT_INVENTORY_FULL):
  2038.                     experience_awarded = spin_details.get('experience_awarded', 0)
  2039.                     if experience_awarded:
  2040.                         log.info("Spun pokestop got response data!")
  2041.                     else:
  2042.                         log.info('Found nothing in pokestop')
  2043.                 elif spin_result == SPIN_REQUEST_RESULT_OUT_OF_RANGE:
  2044.                     log.info("Pokestop out of range.")
  2045.                 elif spin_result == SPIN_REQUEST_RESULT_IN_COOLDOWN_PERIOD:
  2046.                     log.info("Pokestop in cooldown")
  2047.                 else:
  2048.                     log.info("unknown pokestop return")              
  2049.     return  
  2050.  
  2051.  
  2052. def get_forts_in_range(pokestops, scan_location):
  2053.     log.info("Checking for pokestops in spin range")
  2054.     MAX_DISTANCE_FORT_IS_REACHABLE = 38 # meters
  2055.     MAX_DISTANCE_POKEMON_IS_REACHABLE = 48
  2056.     forts = pokestops
  2057.     forts = filter(lambda fort: fort["id"], forts)
  2058.     forts = filter(lambda fort: distance(
  2059.         scan_location[0],
  2060.         scan_location[1],
  2061.         fort['latitude'],
  2062.         fort['longitude']
  2063.     ) <= MAX_DISTANCE_FORT_IS_REACHABLE, forts)
  2064.  
  2065.     return forts    
  2066.  
  2067. def distance(lat1, lon1, lat2, lon2):
  2068.     p = 0.017453292519943295
  2069.     a = 0.5 - cos((lat2 - lat1) * p) / 2 + cos(lat1 * p) * \
  2070.         cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2
  2071.     return 12742 * asin(sqrt(a)) * 1000
  2072.    
  2073. def database_migrate(db, old_ver):
  2074.     # Update database schema version.
  2075.     Versions.update(val=db_schema_version).where(Versions.key == 'schema_version').execute()
  2076.  
  2077.     log.info("Detected database version %i, updating to %i", old_ver, db_schema_version)
  2078.  
  2079.     # Perform migrations here.
  2080.     migrator = None
  2081.     if args.db_type == 'mysql':
  2082.         migrator = MySQLMigrator(db)
  2083.     else:
  2084.         migrator = SqliteMigrator(db)
  2085.  
  2086. #   No longer necessary, we're doing this at schema 4 as well.
  2087. #    if old_ver < 1:
  2088. #        db.drop_tables([ScannedLocation])
  2089.  
  2090.     if old_ver < 2:
  2091.         migrate(migrator.add_column('pokestop', 'encounter_id', CharField(max_length=50, null=True)))
  2092.  
  2093.     if old_ver < 3:
  2094.         migrate(
  2095.             migrator.add_column('pokestop', 'active_fort_modifier', CharField(max_length=50, null=True)),
  2096.             migrator.drop_column('pokestop', 'encounter_id'),
  2097.             migrator.drop_column('pokestop', 'active_pokemon_id')
  2098.         )
  2099.  
  2100.     if old_ver < 4:
  2101.         db.drop_tables([ScannedLocation])
  2102.  
  2103.     if old_ver < 5:
  2104.         # Some pokemon were added before the 595 bug was "fixed".
  2105.         # Clean those up for a better UX.
  2106.         query = (Pokemon
  2107.                  .delete()
  2108.                  .where(Pokemon.disappear_time >
  2109.                         (datetime.utcnow() - timedelta(hours=24))))
  2110.         query.execute()
  2111.  
  2112.     if old_ver < 6:
  2113.         migrate(
  2114.             migrator.add_column('gym', 'last_scanned', DateTimeField(null=True)),
  2115.         )
  2116.  
  2117.     if old_ver < 7:
  2118.         migrate(
  2119.             migrator.drop_column('gymdetails', 'description'),
  2120.             migrator.add_column('gymdetails', 'description', TextField(null=True, default=""))
  2121.         )
  2122.  
  2123.     if old_ver < 8:
  2124.         migrate(
  2125.             migrator.add_column('pokemon', 'individual_attack', IntegerField(null=True, default=0)),
  2126.             migrator.add_column('pokemon', 'individual_defense', IntegerField(null=True, default=0)),
  2127.             migrator.add_column('pokemon', 'individual_stamina', IntegerField(null=True, default=0)),
  2128.             migrator.add_column('pokemon', 'move_1', IntegerField(null=True, default=0)),
  2129.             migrator.add_column('pokemon', 'move_2', IntegerField(null=True, default=0))
  2130.         )
  2131.  
  2132.     if old_ver < 9:
  2133.         migrate(
  2134.             migrator.add_column('pokemon', 'last_modified', DateTimeField(null=True, index=True)),
  2135.             migrator.add_column('pokestop', 'last_updated', DateTimeField(null=True, index=True))
  2136.         )
  2137.  
  2138.     if old_ver < 10:
  2139.         # Information in ScannedLocation and Member Status probably out of date,
  2140.         # so drop and re-create with new schema
  2141.  
  2142.         db.drop_tables([ScannedLocation])
  2143.         db.drop_tables([WorkerStatus])
  2144.  
  2145.     if old_ver < 11:
  2146.         migrate(
  2147.             migrator.add_column('workerstatus', 'captchas', IntegerField(default=0))
  2148.         )
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement