197 lines
6.4 KiB
Python
Executable file
197 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import datetime
|
|
import yaml
|
|
from schema import Schema, And, Or, Use, Optional, Regex
|
|
|
|
import click
|
|
|
|
import adapters
|
|
from adapters import *
|
|
|
|
logging.VERBOSE = (logging.DEBUG + logging.INFO) // 2
|
|
|
|
logger = logging.getLogger(f'wp_cal')
|
|
|
|
def range_str_to_timedelta(value):
|
|
valid_units = 'smhdw'
|
|
current_int = 0
|
|
values = {}
|
|
for c in value:
|
|
if c in '0123456789':
|
|
current_int *= 10
|
|
current_int += int(c)
|
|
elif c in valid_units:
|
|
if c in values:
|
|
logger.warning('unit %s already in values, overwriting', c)
|
|
values[c] = current_int
|
|
current_int = 0
|
|
c = 's'
|
|
if current_int != 0:
|
|
if c in values:
|
|
logger.warning('unit %s already in values, overwriting', c)
|
|
values['s'] = current_int
|
|
for valid_unit in valid_units:
|
|
values.setdefault(valid_unit, 0)
|
|
return datetime.timedelta(
|
|
seconds=values['s'],
|
|
minutes=values['m'],
|
|
hours=values['h'],
|
|
days=values['d'],
|
|
weeks=values['w']
|
|
)
|
|
|
|
def multiple_timedeltas(value):
|
|
mappings = {}
|
|
for v in value.split(','):
|
|
kv = v.split(':')
|
|
if len(kv) == 1:
|
|
mappings[-1] = range_str_to_timedelta(kv[0])
|
|
continue
|
|
mappings[int(kv[0])] = range_str_to_timedelta(kv[1])
|
|
return mappings
|
|
|
|
config_schema = Schema({
|
|
'sources': [dict],
|
|
'sinks': [dict],
|
|
Optional('logging', default={'level': ':WARNING,wp_cal:INFO'}): {
|
|
Optional('level', default=':WARNING,wp_cal:INFO'): Use(str),
|
|
},
|
|
Optional('range', default='365d'): Or(
|
|
dict,
|
|
And(
|
|
Use(str),
|
|
Regex(r'(([0-9]+:)?[0-9]+[smhdw],?)*'),
|
|
Use(multiple_timedeltas),
|
|
),
|
|
),
|
|
})
|
|
|
|
def load_config(file):
|
|
if file == '-':
|
|
config = yaml.safe_load(sys.stdin)
|
|
else:
|
|
if os.stat(file).st_mode & 0o777 & ~0o600:
|
|
raise Exception('refusing to load insecure configuration file, file must have permission 0o600')
|
|
with open(file) as fp:
|
|
config = yaml.safe_load(fp)
|
|
return config_schema.validate(config)
|
|
|
|
def init_logging():
|
|
logging.getLogger().addHandler(
|
|
logging.StreamHandler(),
|
|
)
|
|
|
|
def set_logging_level(level: str):
|
|
levels = {
|
|
'NOTSET': logging.NOTSET,
|
|
'DEBUG': logging.DEBUG,
|
|
'VERBOSE': logging.VERBOSE,
|
|
'INFO': logging.INFO,
|
|
'WARNING': logging.WARNING,
|
|
'WARN': logging.WARNING,
|
|
'ERROR': logging.ERROR,
|
|
'CRITICAL': logging.CRITICAL,
|
|
}
|
|
|
|
log_levels = [x.split(':') for x in level.split(',')]
|
|
for module, level in log_levels:
|
|
module = module.strip() if module else None
|
|
level = level.strip().upper()
|
|
if level not in levels:
|
|
raise ValueError(f'invalid log level, allowed values: {repr(set(levels.keys()))}')
|
|
logging.getLogger(module).setLevel(levels[level])
|
|
|
|
@click.command()
|
|
@click.option('--config', '-c', envvar='WP_CAL_CONFIG', default='-', help='The configuration file')
|
|
@click.option('--dryrun', '-d', envvar='WP_CAL_DRYRUN', is_flag=True, help="Don't actually post any data, just show it")
|
|
@click.option('--level', '-l', envvar='WP_CAL_LEVEL', default=None, help='The log level for the application')
|
|
@click.option('--range', '-r', envvar='WP_CAL_RANGE', default=None, help='The time range from start to start + range to synchronize events')
|
|
def main(config, dryrun, level, range):
|
|
init_logging()
|
|
set_logging_level(':DEBUG')
|
|
|
|
config = load_config(config)
|
|
if level:
|
|
config['logging']['level'] = level
|
|
if range:
|
|
config['range'] = range
|
|
config = config_schema.validate(config)
|
|
set_logging_level(config['logging']['level'])
|
|
|
|
sources = set()
|
|
sinks = set()
|
|
for source in config['sources']:
|
|
assert len(source.keys()) == 1
|
|
for aname, a in adapters.ADAPTERS.items():
|
|
if aname in source:
|
|
sources |= {a.new(source[aname])}
|
|
break
|
|
else:
|
|
logger.error('couldn\'t find valid adapter for source configuration %s', source)
|
|
sys.exit(1)
|
|
for sink in config['sinks']:
|
|
assert len(sink.keys()) == 1
|
|
for aname, a in adapters.ADAPTERS.items():
|
|
if aname in sink:
|
|
sinks |= {a.new(sink[aname])}
|
|
break
|
|
else:
|
|
logger.error('couldn\'t find valid adapter for sink configuration %s', sink)
|
|
sys.exit(1)
|
|
|
|
if not all([isinstance(x, adapters.Source) for x in sources]):
|
|
logger.error('one or more source configurations do not implement being a source')
|
|
sys.exit(1)
|
|
if not all([isinstance(x, adapters.Sink) for x in sinks]):
|
|
logger.error('one or more sink configurations do not implement being a sink')
|
|
sys.exit(1)
|
|
|
|
# log in
|
|
if not all([x.login() for x in sources | sinks]):
|
|
logger.error('failed to log into one or more sinks or sources')
|
|
sys.exit(1)
|
|
|
|
# gather events
|
|
events = []
|
|
source_results = []
|
|
for i, source in enumerate(sources):
|
|
try:
|
|
events += source.get_events_resolved(until=config['range'].get(i, config['range'][-1]))
|
|
source_results += [True]
|
|
except Exception:
|
|
logger.exception('failed to get events from source %s', source)
|
|
source_results += [False]
|
|
if not any(source_results):
|
|
logger.error('event get failed for all sources')
|
|
sys.exit(1)
|
|
# filter cancelled events
|
|
logger.info('found %d events', len(events))
|
|
logger.info('not syncing cancelled events')
|
|
events = [e for e in events if e['status'] != 'cancelled']
|
|
logger.info('syncing %d events', len(events))
|
|
logger.log(logging.VERBOSE, 'events are: %s', [f"{e.get('summary', '<NO SUMMARY PROVIDED>')} ({e.get('start')})" for e in events])
|
|
|
|
# post events
|
|
if not dryrun:
|
|
sink_results = []
|
|
for sink in sinks:
|
|
try:
|
|
sink.post_events(events, until=max(config['range'].values()))
|
|
sink_results += [True]
|
|
except Exception:
|
|
logger.exception('failed to post to sink %s', sink)
|
|
sink_results += [False]
|
|
if not any(sink_results):
|
|
logger.error('event post failed for all sinks')
|
|
sys.exit(1)
|
|
|
|
logger.info("done")
|
|
sys.exit(0)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|