add server
This commit is contained in:
parent
5620b9caa2
commit
8c2c090794
4 changed files with 139 additions and 118 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
json/*
|
||||||
|
__pycache__/
|
118
re.py
118
re.py
|
@ -1,118 +0,0 @@
|
||||||
'''
|
|
||||||
Reccomendation Engine Example
|
|
||||||
'''
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
|
|
||||||
import ox
|
|
||||||
|
|
||||||
class Pandora:
|
|
||||||
|
|
||||||
def __init__(self, url, username, password):
|
|
||||||
self.api = ox.API(url)
|
|
||||||
self.api.signin(username=username, password=password)
|
|
||||||
|
|
||||||
def find_annotations(self, query, keys):
|
|
||||||
# print('FIND ANNOTATIONS', query, keys)
|
|
||||||
return self.api.findAnnotations({
|
|
||||||
'keys': keys,
|
|
||||||
'query': query,
|
|
||||||
'range': [0, 1000000]
|
|
||||||
})['data']['items']
|
|
||||||
|
|
||||||
def find_entities(self, query, keys):
|
|
||||||
# print('FIND ENTITIES', query, keys)
|
|
||||||
return self.api.findEntities({
|
|
||||||
'keys': keys,
|
|
||||||
'query': query,
|
|
||||||
'range': [0, 1000000]
|
|
||||||
})['data']['items']
|
|
||||||
|
|
||||||
def get(self, id, keys):
|
|
||||||
# print('GET', id, keys)
|
|
||||||
return self.api.get({
|
|
||||||
'id': id,
|
|
||||||
'keys': keys
|
|
||||||
})['data']
|
|
||||||
|
|
||||||
class Engine:
|
|
||||||
|
|
||||||
def __init__(self, path):
|
|
||||||
self.path = path
|
|
||||||
self.pandora = Pandora(
|
|
||||||
url='http://pandora.dmp/api/',
|
|
||||||
username='dd.re',
|
|
||||||
password='dd.re'
|
|
||||||
)
|
|
||||||
|
|
||||||
def _shift_clips(self, clips):
|
|
||||||
index = random.randrange(len(clips))
|
|
||||||
return clips[index:] + clips[:index - 1]
|
|
||||||
|
|
||||||
def get_videos(self, user):
|
|
||||||
products = []
|
|
||||||
for event in user['events']:
|
|
||||||
if 'product' in event['data']:
|
|
||||||
products.append(event['data']['product'])
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
# Get all storylines with tags
|
|
||||||
storylines = [{
|
|
||||||
'name': entity['name'],
|
|
||||||
'tags': entity['tags']
|
|
||||||
} for entity in self.pandora.find_entities({
|
|
||||||
'conditions': [
|
|
||||||
{'key': 'type', 'operator': '==', 'value': 'storylines'},
|
|
||||||
],
|
|
||||||
'operator': '&'
|
|
||||||
}, ['id', 'name', 'tags']) if entity.get('tags', [])]
|
|
||||||
# Get list of storyline names
|
|
||||||
names = list(set([storyline['name'] for storyline in storylines]))
|
|
||||||
# Get all clips annotated with storyline references
|
|
||||||
clips = [clip for clip in self.pandora.find_annotations({
|
|
||||||
'conditions': [
|
|
||||||
{'key': 'layer', 'operator': '==', 'value': 'storylines'}
|
|
||||||
],
|
|
||||||
'operator': '&'
|
|
||||||
}, ['id', 'in', 'out', 'value']) if clip['value'] in names]
|
|
||||||
# Get list of ids for videos with clips
|
|
||||||
ids = list(set([clip['id'].split('/')[0] for clip in clips]))
|
|
||||||
# Get (and cache) order (and code + name) for each video
|
|
||||||
filename = os.path.join(self.path, 'videos.json')
|
|
||||||
if os.path.exists(filename):
|
|
||||||
with open(filename) as f:
|
|
||||||
videos_ = json.loads(f.read())
|
|
||||||
ids_ = [video['id'] for video in videos_]
|
|
||||||
else:
|
|
||||||
videos_, ids_ = [], []
|
|
||||||
videos = sorted(videos_ + [
|
|
||||||
self.pandora.get(id, ['code', 'id', 'order', 'title'])
|
|
||||||
for id in ids if not id in ids_
|
|
||||||
], key=lambda video: video['order'])
|
|
||||||
with open(filename, 'w') as f:
|
|
||||||
f.write(json.dumps(videos, indent=4, sort_keys=True))
|
|
||||||
order = {video['id']: video['order'] for video in videos}
|
|
||||||
# Sort clips
|
|
||||||
clips = sorted(
|
|
||||||
clips,
|
|
||||||
key=lambda clip: order[clip['id'].split('/')[0]] * 1000000 + clip['in']
|
|
||||||
)
|
|
||||||
# Get playlists
|
|
||||||
playlists = [playlist for playlist in [{
|
|
||||||
'name': storyline['name'],
|
|
||||||
'tags': storyline['tags'],
|
|
||||||
'clips': [
|
|
||||||
'{}_{:.3f}-{:.3f}'.format(
|
|
||||||
clip['id'].split('/')[0], clip['in'], clip['out']
|
|
||||||
) for clip in clips if clip['value'] == storyline['name']
|
|
||||||
]
|
|
||||||
} for storyline in storylines] if playlist['clips']]
|
|
||||||
with open(os.path.join(self.path, 'playlists.json'), 'w') as f:
|
|
||||||
f.write(json.dumps(playlists, indent=4, sort_keys=True))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
engine = Engine('json')
|
|
||||||
engine.update()
|
|
||||||
|
|
113
server.py
Executable file
113
server.py
Executable file
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from tornado.httpserver import HTTPServer
|
||||||
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
|
|
||||||
|
from tornado.web import StaticFileHandler, Application, HTTPError
|
||||||
|
import tornado.gen
|
||||||
|
import tornado.web
|
||||||
|
|
||||||
|
|
||||||
|
from recommendation_engine import Engine
|
||||||
|
from utils import json_dumps, run_async
|
||||||
|
|
||||||
|
|
||||||
|
BANNER_PUBLIC = 'DD recommondation engine'
|
||||||
|
BANNER = '%s - use POST at / to access JSON-RPC 2.0 endpoint' % BANNER_PUBLIC
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@run_async
|
||||||
|
def api_task(request, engine, callback):
|
||||||
|
try:
|
||||||
|
if request['method'] == 'getVideos':
|
||||||
|
result = engine.get_videos(request['params'])
|
||||||
|
else:
|
||||||
|
result = {}
|
||||||
|
response = {
|
||||||
|
'result': result
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
logger.error('api failed: %s', request, exc_info=True)
|
||||||
|
response = {'error': {'code': -32000, 'message': 'Server error'}}
|
||||||
|
callback(response)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
def initialize(self, engine):
|
||||||
|
self.engine = engine
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.write(BANNER)
|
||||||
|
|
||||||
|
@tornado.gen.coroutine
|
||||||
|
def post(self):
|
||||||
|
error = None
|
||||||
|
request = None
|
||||||
|
try:
|
||||||
|
request = json.loads(self.request.body.decode())
|
||||||
|
if request['method'] not in ('getVideos', ):
|
||||||
|
raise Exception('unknown method')
|
||||||
|
except:
|
||||||
|
error = {'error': {'code': -32700, 'message': 'Parse error'}}
|
||||||
|
if not error:
|
||||||
|
try:
|
||||||
|
response = yield tornado.gen.Task(api_task, request, self.engine)
|
||||||
|
except:
|
||||||
|
logger.error("ERROR: %s", request, exc_info=True)
|
||||||
|
error = {'error': {'code': -32000, 'message': 'Server error'}}
|
||||||
|
if error:
|
||||||
|
response = error
|
||||||
|
if request and 'id' in request:
|
||||||
|
response['id'] = request['id']
|
||||||
|
response['jsonrpc'] = '2.0'
|
||||||
|
response = json_dumps(response)
|
||||||
|
self.write(response)
|
||||||
|
|
||||||
|
|
||||||
|
def main(prefix='json/'):
|
||||||
|
settings = {
|
||||||
|
'debug': False,
|
||||||
|
'port': 8081,
|
||||||
|
'address': ''
|
||||||
|
}
|
||||||
|
engine = Engine(prefix)
|
||||||
|
handlers = [
|
||||||
|
(r'/', RPCHandler, dict(engine=engine)),
|
||||||
|
]
|
||||||
|
options = {
|
||||||
|
'debug': settings['debug'],
|
||||||
|
'gzip': True,
|
||||||
|
}
|
||||||
|
if settings['debug']:
|
||||||
|
log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format=log_format)
|
||||||
|
|
||||||
|
app = Application(handlers, **options)
|
||||||
|
app.listen(settings['port'], settings['address'])
|
||||||
|
main = IOLoop.instance()
|
||||||
|
|
||||||
|
@run_async
|
||||||
|
def update():
|
||||||
|
engine.update()
|
||||||
|
|
||||||
|
update_cb = PeriodicCallback(update, 60000)
|
||||||
|
|
||||||
|
#main.spawn_callback(update, engine)
|
||||||
|
|
||||||
|
#fixme run periodically
|
||||||
|
try:
|
||||||
|
main.start()
|
||||||
|
except:
|
||||||
|
print('shutting down...')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
24
utils.py
Normal file
24
utils.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import json
|
||||||
|
from functools import wraps
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
def json_dumps(obj):
|
||||||
|
return json.dumps(obj, indent=4, default=_to_json, ensure_ascii=False, sort_keys=True).encode()
|
||||||
|
|
||||||
|
def _to_json(python_object):
|
||||||
|
if isinstance(python_object, datetime.datetime):
|
||||||
|
if python_object.year < 1900:
|
||||||
|
tt = python_object.timetuple()
|
||||||
|
return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6])
|
||||||
|
return python_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
raise TypeError('%s %s is not JSON serializable' % (repr(python_object), type(python_object)))
|
||||||
|
|
||||||
|
def run_async(func):
|
||||||
|
@wraps(func)
|
||||||
|
def async_func(*args, **kwargs):
|
||||||
|
func_hl = Thread(target=func, args=args, kwargs=kwargs)
|
||||||
|
func_hl.start()
|
||||||
|
return func_hl
|
||||||
|
|
||||||
|
return async_func
|
||||||
|
|
Loading…
Reference in a new issue