Skip to content

Commit

Permalink
Cleanup of daemon package:
Browse files Browse the repository at this point in the history
* remove unuused imports other `pyflakes` fixes
* better logging: streamhandler, syslog
* neater handling of "global" data. no more global `DIR_PATH`
* don't bail out on non-200 status. log and return empty results
* use config directory `~/.coincharts` storing api auth and history symbols
  • Loading branch information
stnbu committed Sep 1, 2018
1 parent 3991d60 commit 94026e1
Showing 1 changed file with 79 additions and 33 deletions.
112 changes: 79 additions & 33 deletions coincharts/daemon/base.py
Expand Up @@ -5,29 +5,33 @@
import os import os
import sys import sys


# FIXME
# beautiful HACK until I get some stuff figured out.
sys.path.insert(0, '../mutils')

import datetime import datetime
import pytz import pytz
from dateutil.parser import parse as parse_dt from dateutil.parser import parse as parse_dt
import json
import urllib import urllib
import requests import requests
import logging import logging
import logging.handlers
import time import time


import yaml
from sqlalchemy import Integer, String, Float from sqlalchemy import Integer, String, Float
import daemon import daemon
import daemon.pidfile import daemon.pidfile


from mutil import simple_alchemy from mutils import simple_alchemy


logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)


DIR_PATH = None ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)


CONFIG_DIR = os.path.expanduser('~/.coincharts')
API_KEY_FILE = os.path.join(CONFIG_DIR, 'API_KEY')
CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yaml')




class PriceSeries(object): class PriceSeries(object):
Expand All @@ -40,7 +44,7 @@ class PriceSeries(object):
) )
date_format_template = '%Y-%m-%dT%H:%M:%S.%f0Z' # magic date_format_template = '%Y-%m-%dT%H:%M:%S.%f0Z' # magic
headers = {'X-CoinAPI-Key': headers = {'X-CoinAPI-Key':
open('API_KEY').read().strip()} open(API_KEY_FILE).read().strip()}
schema = [ schema = [
('time_period_start', 'TEXT'), ('time_period_start', 'TEXT'),
('time_period_end', 'TEXT'), ('time_period_end', 'TEXT'),
Expand All @@ -63,11 +67,12 @@ class PriceSeries(object):


_session = None _session = None


# the sqlalchemy is a class singleton, so many symbols can share a connection.
@classmethod @classmethod
def get_db_session(cls): def get_db_session(cls, dir_path):
if cls._session is not None: if cls._session is not None:
return cls._session return cls._session
db_path = os.path.join(os.path.abspath(DIR_PATH), 'db.sqlite3') db_path = os.path.join(os.path.abspath(dir_path), 'db.sqlite3')
cls._session = simple_alchemy.get_session(db_path) cls._session = simple_alchemy.get_session(db_path)
return cls._session return cls._session


Expand All @@ -92,9 +97,10 @@ def round_up_hour(cls, dt):
dt = dt.replace(minute=0, second=0, microsecond=0) dt = dt.replace(minute=0, second=0, microsecond=0)
return dt return dt


def __init__(self, symbol_id): def __init__(self, symbol_id, dir_path):
logger.debug('creating PriceSeries object for {}'.format(symbol_id)) logger.debug('creating PriceSeries object for {}'.format(symbol_id))
self.symbol_id = symbol_id self.symbol_id = symbol_id
self.dir_path = dir_path
schema = [ schema = [
('time_period_start', String), ('time_period_start', String),
('time_period_end', String), ('time_period_end', String),
Expand All @@ -118,11 +124,11 @@ def get_prices_since(self, start_dt):
start_dt = parse_dt(start_dt) start_dt = parse_dt(start_dt)
except TypeError: except TypeError:
pass pass
start_dt = self.get_normalized_datetime(self.round_up(start_dt)) start_dt = self.get_normalized_datetime(self.round_up_hour(start_dt))
kwargs = {self.TIME: start_dt} kwargs = {self.TIME: start_dt}
session = self.get_db_session() session = self.get_db_session(self.dir_path)
first = session.query(self.data).filter_by(**kwargs).first() first = session.query(self.data).filter_by(**kwargs).first()
results = session.query(self.data).filter(id >= start_dt).first() results = session.query(self.data).filter(id >= first).first()
return results return results


def get_url(self, query_data): def get_url(self, query_data):
Expand Down Expand Up @@ -162,12 +168,15 @@ def fetch(self):
url = self.get_url(query_data) url = self.get_url(query_data)
logger.debug('getting url {}'.format(url)) logger.debug('getting url {}'.format(url))
response = requests.get(url, headers=self.headers) response = requests.get(url, headers=self.headers)
if response.status_code != 200:
logger.error('request {} failed: {}'.format(url, response.reason))
return {}
logger.info('account has {} more API requests for this time period'.format( logger.info('account has {} more API requests for this time period'.format(
response.headers['X-RateLimit-Remaining'])) response.headers['X-RateLimit-Remaining']))
return response.json() return response.json()


def get_last_date_from_store(self): def get_last_date_from_store(self):
session = self.get_db_session() session = self.get_db_session(self.dir_path)
obj = session.query(self.data).order_by(self.data.id.desc()).first() obj = session.query(self.data).order_by(self.data.id.desc()).first()
if obj is None: if obj is None:
return parse_dt(self.first_date) return parse_dt(self.first_date)
Expand All @@ -179,7 +188,7 @@ def insert(self, data):
insertions = [] insertions = []
for row in data: for row in data:
insertions.append(self.data(**row)) insertions.append(self.data(**row))
session = self.get_db_session() session = self.get_db_session(self.dir_path)
session.add_all(insertions) session.add_all(insertions)
session.commit() session.commit()


Expand All @@ -188,29 +197,24 @@ def update(self):
self.insert(data) self.insert(data)




def main(dir_path, daemon=True): def worker(dir_path, daemonize=True):

global DIR_PATH
DIR_PATH = dir_path


fh = logging.FileHandler(os.path.join(dir_path, 'logs')) fh = logging.FileHandler(os.path.join(dir_path, 'logs'))
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter) fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
logger.addHandler(fh) logger.addHandler(fh)


symbols = [ sh = logging.handlers.SysLogHandler(address='/var/run/syslog')
'BITSTAMP_SPOT_BTC_USD', sh.setLevel(logging.DEBUG)
'BITSTAMP_SPOT_XRP_USD', logger.addHandler(sh)
'BITSTAMP_SPOT_ETH_USD',
'BITSTAMP_SPOT_LTC_USD', with open(CONFIG_FILE) as f:
'BITSTAMP_SPOT_EUR_USD', config = yaml.load(f)
'BITSTAMP_SPOT_BCH_USD'
]


series = [] series = []
for symbol_id in symbols: for symbol_id in config['history_symbols']:
series.append(PriceSeries(symbol_id)) series.append(PriceSeries(symbol_id, dir_path))


while True: while True:
for ps in series: for ps in series:
Expand All @@ -219,8 +223,50 @@ def main(dir_path, daemon=True):
time.sleep(3600) time.sleep(3600)




if __name__ == '__main__': def main():

script_basename, _ = os.path.splitext(os.path.basename(__file__))

# no need to involve argparse for something this simple
if len(sys.argv) == 1:
print('usage: {} [--daemon] <directory>'.format(script_basename))
sys.exit(1)


from mutils.simple_daemon import daemonize daemonize = True
if '--daemon' in sys.argv:
script_name, _, dir_path = sys.argv
else:
script_name, dir_path = sys.argv
daemonize = False

dir_path = os.path.abspath(dir_path)

if daemonize:
# when I'm a daemon, log all exceptions
def exception_handler(type_, value, tb):
logger.exception('uncaught exception on line {}; {}: {}'.format(
tb.tb_lineno,
type_.__name__,
value,
))
sys.__excepthook__(type_, value, tb)
sys.excepthook = exception_handler

logger.debug('starting daemon {} using path {}'.format(script_name, dir_path))

if daemonize:
pid_file = os.path.join(dir_path, script_basename + '.pid')

with daemon.DaemonContext(
working_directory=dir_path,
pidfile=daemon.pidfile.PIDLockFile(pid_file),

):
worker(dir_path, daemonize=daemonize)
else:
worker(dir_path, daemonize=daemonize)


if __name__ == '__main__':


daemonize(main) main()

0 comments on commit 94026e1

Please sign in to comment.