Skip to content

Commit

Permalink
model models and added simple entity cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Webster committed Sep 8, 2020
1 parent 5b916f8 commit a1f94c7
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 134 deletions.
4 changes: 4 additions & 0 deletions server/api/code/lacity_data_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
# check whether running in legacy mode
API_LEGACY_MODE = config('API_LEGACY_MODE', cast=bool, default=True)

# TODO: figure out how to remove dependency on DATABASE_URL from services
# the legacy code needs these created as environment settings
if API_LEGACY_MODE:
environ['DATABASE_URL'] = str(DB_DSN)
Expand All @@ -60,3 +61,6 @@
for k, v in sorted(os.environ.items()):
print(f'\033[92m{k}\033[0m: {v}')
print(f"\n\033[93mDatabase\033[0m: {DB_DSN}\n")

# create empty cache object to populate at runtime
cache = {}
99 changes: 78 additions & 21 deletions server/api/code/lacity_data_api/models/clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from sqlalchemy import and_

from .service_request import ServiceRequest
from .request_type import get_types_dict
from .council import Council
from .region import Region
from . import db
from ..config import cache


DEFAULT_CITY_ZOOM = 12 # a click on a city point zooms from 10 to 12
DEFAULT_CITY_ZOOM = 11 # a click on a city point zooms from 10 to 12
DEFAULT_REGION_ZOOM = 12 # a click on a city point zooms from 10 to 12
DEFAULT_COUNCIL_ZOOM = 13 # a click on a council point zooms to 14
DEFAULT_LATITUDE = 34.0522
DEFAULT_LONGITUDE = -118.2437
Expand Down Expand Up @@ -71,15 +72,60 @@ async def get_clusters_for_city(
return cluster_list


# TODO: same as above by group by region of each council
def get_clusters_for_regions(pins, zoom, bounds, options):
async def get_clusters_for_regions(
start_date: datetime.date,
end_date: datetime.date,
type_ids: List[int],
council_ids: List[int],
zoom_current: int
) -> List[Cluster]:
"""
Cluster pins by region
Cluster service request pins by council regions
Args:
start_date (date): beginning of date range service was requested
end_date (date): end of date range service was requested
type_ids (List[int]): the request type ids to match on
council_ids (List[int]): the council ids to match on
Returns:
cluster: a list of cluster objects
"""
print(zoom)

# TODO: CACHE 'region-reqs:start-end-types-councils'
result = await (
db.select(
[
ServiceRequest.region_id,
db.func.count()
]
).where(
and_(
ServiceRequest.created_date >= start_date,
ServiceRequest.created_date <= end_date,
ServiceRequest.type_id.in_(type_ids),
ServiceRequest.council_id.in_(council_ids),
)
).group_by(
ServiceRequest.region_id
).gino.all()
)

cluster_list = []

for row in result:
region = await Region.get(row[0])
cluster_list.append(Cluster(
count=row[1],
expansion_zoom=DEFAULT_REGION_ZOOM,
id=region.region_id,
latitude=region.latitude,
longitude=region.longitude
))

return cluster_list


# TODO: same as above by group by council
async def get_clusters_for_councils(
start_date: datetime.date,
end_date: datetime.date,
Expand All @@ -88,17 +134,19 @@ async def get_clusters_for_councils(
zoom_current: int
) -> List[Cluster]:
"""
Cluster pins for the entire city
Cluster service request pins by council
Args:
start_date (date): beginning of date range service was requested
end_date (date): end of date range service was requested
type_ids (List[int]): the request type ids to match on
council_ids (List[int]): the council ids to match on
Returns:
cluster: a cluster object
cluster: a list of cluster objects
"""

# TODO: CACHE 'council-reqs:start-end-types-councils'
result = await (
db.select(
[
Expand All @@ -120,14 +168,23 @@ async def get_clusters_for_councils(
# zoom_next = (zoom_current + 1) or DEFAULT_COUNCIL_ZOOM
cluster_list = []

# TODO: replace this with a caching solution
# returns dictionary with council id as key and name, lat, long
# council_result = await db.all(Council.query)
# councils = [
# (i.council_id, [i.council_name, i.latitude, i.longitude])
# for i in council_result
# ]
councils_dict = cache.get("councils_dict")

for row in result:
council = await Council.get(row[0])
council = councils_dict.get(row[0])
cluster_list.append(Cluster(
count=row[1],
expansion_zoom=DEFAULT_COUNCIL_ZOOM,
id=council.council_id,
latitude=council.latitude,
longitude=council.longitude
id=row[0],
latitude=council[1],
longitude=council[2]
))

return cluster_list
Expand All @@ -149,7 +206,7 @@ async def get_points(
council_ids: (List[int]): the council ids to match
Returns:
a list of latitude and logitude pairs of service locations
a list of latitude and logitude pairs of service request locations
"""

result = await (
Expand All @@ -170,12 +227,11 @@ async def get_points(

point_list = []
for row in result:
point_list.append([row[0], row[1]])
point_list.append([row.latitude, row.longitude])

return point_list


# TODO: same as above by group by council
async def get_clusters_for_bounds(
start_date: datetime.date,
end_date: datetime.date,
Expand Down Expand Up @@ -219,7 +275,7 @@ async def get_clusters_for_bounds(
)

# TODO: clean this up. goes in [longitude, latitude] format
points = [[i[2], i[1]] for i in result]
points = [[row.longitude, row.latitude] for row in result]

index = pysupercluster.SuperCluster(
numpy.array(points),
Expand All @@ -235,14 +291,15 @@ async def get_clusters_for_bounds(
zoom=zoom_current
)

types_dict = await get_types_dict()
# TODO: replace this with a proper caching solution
types_dict = cache.get("types_dict")

for item in cluster_list:
# change single item clusters into points
if item['count'] == 1:
pin = result[item['id']] # cluster id matches the result row
item['srnumber'] = "1-" + str(pin[0])
item['requesttype'] = types_dict[pin[3]]
item['srnumber'] = "1-" + str(pin.request_id)
item['requesttype'] = types_dict[pin.type_id]
del item['expansion_zoom']

return cluster_list
9 changes: 9 additions & 0 deletions server/api/code/lacity_data_api/models/council.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ class Council(db.Model):
region_id = db.Column(db.SmallInteger)
latitude = db.Column(db.Float)
longitude = db.Column(db.Float)


async def get_councils_dict():
result = await db.all(Council.query)
councils_dict = [
(i.council_id, (i.council_name, i.latitude, i.longitude))
for i in result
]
return dict(councils_dict)
9 changes: 9 additions & 0 deletions server/api/code/lacity_data_api/models/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ class Region(db.Model):
region_name = db.Column(db.String)
latitude = db.Column(db.Float)
longitude = db.Column(db.Float)


async def get_regions_dict():
result = await db.all(Region.query)
regions_dict = [
(i.region_id, (i.region_name, i.latitude, i.longitude))
for i in result
]
return dict(regions_dict)
13 changes: 13 additions & 0 deletions server/api/code/lacity_data_api/models/service_request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

from . import db


Expand All @@ -9,6 +11,17 @@ class ServiceRequest(db.Model):
closed_date = db.Column(db.Date)
type_id = db.Column(db.SmallInteger)
council_id = db.Column(db.SmallInteger)
region_id = db.Column(db.SmallInteger)
address = db.Column(db.String)
latitude = db.Column(db.Float)
longitude = db.Column(db.Float)


async def get_open_requests() -> List[ServiceRequest]:
'''Get a list of RequestTypes from their type_names'''
result = await db.all(
ServiceRequest.query.where(
ServiceRequest.closed_date == None # noqa
)
)
return result
86 changes: 86 additions & 0 deletions server/api/code/lacity_data_api/routers/api_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import List, Optional
import datetime
from enum import Enum

from pydantic import BaseModel, validator
from pydantic.dataclasses import dataclass


class Bounds(BaseModel):
north: float
south: float
east: float
west: float


class Filter(BaseModel):
startDate: str
endDate: str
ncList: List[int]
requestTypes: List[str]
zoom: Optional[int] = None
bounds: Optional[Bounds] = None

@validator('startDate', 'endDate')
def parse_date(cls, v):
if isinstance(v, str):
try:
v = datetime.datetime.strptime(v, '%m/%d/%Y')
except ValueError:
try:
v = datetime.datetime.strptime(v, '%Y-%m-%d')
except ValueError:
pass
return v


class Pin(BaseModel):
srnumber: str
requesttype: str
latitude: float
longitude: float


class Cluster(BaseModel):
count: int
expansion_zoom: Optional[int]
id: int
latitude: float
longitude: float


@dataclass
class Set:
district: str
list: List[int]

@validator('district')
def district_is_valid(cls, v):
assert v in ['cc', 'nc'], 'district must be either "nc" or "cc".'
return v

def __getitem__(cls, item):
return getattr(cls, item)


class Comparison(BaseModel):
startDate: str
endDate: str
requestTypes: List[str]
set1: Set
set2: Set


class Feedback(BaseModel):
title: str
body: str


class StatusTypes(str, Enum):
api = "api"
database = "db"
system = "sys"


Pins = List[Pin]
Clusters = List[Cluster]
6 changes: 5 additions & 1 deletion server/api/code/lacity_data_api/routers/index.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from fastapi import APIRouter

from .utilities import build_cache
from ..config import cache

router = APIRouter()


@router.get("/")
async def index():
return {"message": "Hello, new index!"}
await build_cache()
return cache
Loading

0 comments on commit a1f94c7

Please sign in to comment.