Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added requestcounts endpoint #393

Merged
merged 3 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions server/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from services.time_to_close import time_to_close
from services.frequency import frequency
from services.pinService import PinService
from services.requestCountsService import RequestCountsService
from services.requestDetailService import RequestDetailService
from services.ingress_service import ingress_service
from services.sqlIngest import DataHandler
Expand Down Expand Up @@ -140,6 +141,25 @@ async def pinMap(request):
return json(return_data)


@app.route('/requestcounts', methods=["POST"])
@compress.compress()
async def requestCounts(request):
counts_worker = RequestCountsService(app.config['Settings'])
postArgs = request.json
start = postArgs.get('startDate', None)
end = postArgs.get('endDate', None)
ncs = postArgs.get('ncList', [])
requests = postArgs.get('requestTypes', [])
countFields = postArgs.get('countFields', [])

return_data = await counts_worker.get_req_counts(startDate=start,
endDate=end,
ncList=ncs,
requestTypes=requests,
countFields=countFields)
return json(return_data)


@app.route('/servicerequest/<srnumber>', methods=["GET"])
async def requestDetails(request, srnumber):
detail_worker = RequestDetailService(app.config['Settings'])
Expand Down
14 changes: 9 additions & 5 deletions server/src/services/dataService.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import datetime
import pandas as pd
import sqlalchemy as db
from sqlalchemy.orm import sessionmaker


class DataService(object):
def withMeta(dataResponse):
# Will represent last time the ingest pipeline ran
lastPulledTimestamp = datetime.datetime.utcnow()
return {'lastPulled': lastPulledTimestamp,
'data': dataResponse}

def includeMeta(func):
def innerFunc(*args, **kwargs):
dataResponse = func(*args, **kwargs)
if 'Error' in dataResponse:
return dataResponse

# Will represent last time the ingest pipeline ran
lastPulledTimestamp = datetime.datetime.utcnow()
withMeta = {'lastPulled': lastPulledTimestamp,
'data': dataResponse}
return withMeta
return DataService.withMeta(dataResponse)

return innerFunc

Expand All @@ -26,6 +29,7 @@ def __init__(self, config=None, tableName="ingest_staging_table"):
self.table = tableName
self.data = None
self.engine = db.create_engine(self.dbString)
self.session = sessionmaker(bind=self.engine)()

@includeMeta
def query(self, queryItems=None, queryfilters=[], limit=None):
Expand Down
70 changes: 70 additions & 0 deletions server/src/services/requestCountsService.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from sqlalchemy import func
from .dataService import DataService
from .databaseOrm import Ingest as Request


class RequestCountsService(object):
def __init__(self, config=None, tableName="ingest_staging_table"):
self.dataAccess = DataService(config, tableName)

async def get_req_counts(self,
startDate=None,
endDate=None,
ncList=[],
requestTypes=[],
countFields=[]):
"""
For each countField, returns the counts of each distinct value
in that field, given times, ncs, and request filters.
E.g. if countsFields is ['requesttype', 'requestsource'], returns:
{
'lastPulled': 'Timestamp',
'data': [
{
'field': 'requesttype',
'counts': {
'Graffiti Removal': 'Int',
'Bulky Items': 'Int',
...
}
},
{
'field': 'requestsource',
'counts': {
'Mobile App': 'Int',
'Driver Self Report': 'Int',
...
}
}
]
}
"""

# filter by date, nc, and requestType (if provided)
filters = [
Request.createddate > startDate if startDate else True,
Request.createddate < endDate if endDate else True,
Request.ncname.in_(ncList) if ncList else True,
Request.requesttype.in_(requestTypes) if requestTypes else True
]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im thinking that we might want to adjust this to hit data service with countFields and filters and create an aggregateQuery method in dataservice....so functionality will look like

select [countFields] from table where [filters]

Then do a pandas groupby(countFields).size()
Should follow something like this https://stackoverflow.com/a/38933130

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason behind this is to keep the database logic separate from any of the services which will make testing/validation much easier(when we get around to it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that makes sense, I can move the database logic into the dataservice. We might want to get away from constructing SQL queries by string concatenation though, it's kind of error-prone and hard to read. The ORM in sqlalchemy basically generates (and executes) SQL queries in a cleaner way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i agree, we can def tweak the existing query method to use the orm instead of creating a 💩 query string

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #398 in the meantime so whoever takes it can use your code as an example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok cool, yeah I'll take that one. I can work on it while I'm creating the aggregationQuery in the dataservice. I like the aggregationQuery concept, could be useful for other endpoints down the line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks!!
Im hoping that with query and aggregationQuery itll keep the backend to easy categorization of services. Either getting a single thing, a lot of stuff, or a summary of groupings
That way there wont be weird complex querying per endpoint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I gave the aggregationQuery a shot, also removed SQL string concatenation and used ORM instead in the pins service and request detail service.

data = []
for field in countFields:
# make sure the field exists in the Request model
if not getattr(Request, field, None):
continue

# run count/groupby query
results = self.dataAccess.session \
.query(field, func.count()) \
.filter(*filters) \
.group_by(field) \
.all()

# add results to data set
data.append({
'field': field,
'counts': dict(results)
})

return DataService.withMeta(data)