Many changes to Kimai stuff:

- Move `import kimai` to `kimai import`
 - Add `kimai sync` to check which Customers / Projects / Activities are
   missing, using the API
 - Switch to duration in hours instead of seconds
 - Add `--after` option to `kimai import` to limit time records by date
This commit is contained in:
3wc 2021-04-06 11:43:28 +02:00
parent b345f27583
commit 78c243fc8b
1 changed files with 179 additions and 63 deletions

View File

@ -1,10 +1,11 @@
#!/usr/bin/env python3.7 #!/usr/bin/env python3.7
import sys
import click
import csv import csv
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import sys
import click
import requests
import sqlite3 import sqlite3
HAMSTER_DIR = Path.home() / '.local/share/hamster-applet' HAMSTER_DIR = Path.home() / '.local/share/hamster-applet'
@ -337,34 +338,19 @@ def find_duplicates():
@cli.group() @cli.group()
def export(): def kimai():
pass pass
@export.command() def _get_kimai_mapping_file(path):
@click.option('--mapping', help='Mapping file (default ~/.local/share/hamster/mapping.kimai.csv)')
@click.option('--output', help='Output file (default kimai.csv)')
@click.option('--category-search', help='Category search string')
@click.argument('username')
def kimai(username, mapping=None, output=None, category_search=None):
"""
Export time tracking data in Kimai format
"""
if mapping is None:
mapping = HAMSTER_DIR / 'mapping.kimai.csv'
if output is None:
output = 'kimai.csv'
try: try:
mapping_file = open(mapping) return open(path)
mapping_reader = csv.reader(mapping_file)
except FileNotFoundError: except FileNotFoundError:
click.confirm( click.confirm(
'Mapping file {} not found, create it?:'.format(mapping), 'Mapping file {} not found, create it?:'.format(path),
abort=True abort=True
) )
mapping_file = open(mapping, 'w') mapping_file = open(path, 'w')
mapping_writer = csv.writer(mapping_file) mapping_writer = csv.writer(mapping_file)
mapping_writer.writerow([ mapping_writer.writerow([
@ -383,7 +369,123 @@ def kimai(username, mapping=None, output=None, category_search=None):
]) ])
mapping_file.close() mapping_file.close()
return return open(path)
@kimai.command()
@click.option('--mapping-path', help='Mapping file (default ~/.local/share/hamster/mapping.kimai.csv)')
@click.argument('username')
@click.argument('api_key')
def sync(username, api_key, mapping_path=None):
"""
Download customer / project / activity data from Kimai
"""
kimai_api_url = 'https://kimai.autonomic.zone/api'
if mapping_path is None:
mapping_path = HAMSTER_DIR / 'mapping.kimai.csv'
mapping_file = _get_kimai_mapping_file(mapping_path)
mapping_reader = csv.reader(mapping_file)
next(mapping_reader)
mapping_data = [
[row[2], row[3], row[4]]
for row in mapping_reader
]
mapping_file.close()
auth_headers = {
'X-AUTH-USER': username,
'X-AUTH-TOKEN': api_key
}
customers = requests.get(f'{kimai_api_url}/customers', headers=auth_headers).json()
projects = requests.get(f'{kimai_api_url}/projects', headers=auth_headers).json()
activities = requests.get(f'{kimai_api_url}/activities', headers=auth_headers).json()
found_customers = []
found_projects = []
found_activities = []
for row in mapping_data:
# Check if each mapping still exists in Kimai
matching_customers = list(filter(lambda x: x['name'] == row[0], customers))
if row[0] in found_customers:
click.secho("Skipping existing customer '{0}'".format(row[0]), fg='green')
else:
if len(matching_customers) > 1:
click.secho("More than one match for customer '{0}'".format(row[0]), fg='red')
continue
elif len(matching_customers) < 1:
click.secho("Missing customer '{0}'".format(row[0]), fg='yellow')
continue
else:
click.secho("Found customer '{0}'".format(row[0]), fg='green')
found_customers.append(row[0])
project_str = ':'.join(row[0:2])
matching_projects = list(filter(
lambda x: x['name'] == row[1] and
x['customer'] == matching_customers[0]['id'],
projects)
)
if project_str in found_projects:
click.secho("Skipping existing project '{0}'".format(project_str), fg='green')
else:
if len(matching_projects) > 1:
click.secho("More than one match for project '{0}'".format(project_str), fg='red')
continue
elif len(matching_projects) < 1:
click.secho("Missing project '{0}'".format(project_str), fg='yellow')
continue
else:
click.secho("Found project '{0}'".format(project_str), fg='green')
found_projects.append(project_str)
activity_str = ':'.join(row)
if activity_str in found_activities:
click.secho("Skipping existing activity '{0}'".format(activity_str), fg='green')
else:
matching_activities = list(filter(
lambda x: x['name'] == row[2]
and x['project'] == matching_projects[0]['id'],
activities
))
if len(matching_activities) > 1:
click.secho("More than one match for activity '{0}'".format(activity_str), fg='red')
elif len(matching_activities) < 1:
click.secho("Missing activity '{0}'".format(activity_str), fg='yellow')
else:
click.secho("Found activity '{0}'".format(activity_str), fg='green')
found_activities.append(activity_str)
@kimai.command('import')
@click.option('--mapping-path', help='Mapping file (default ~/.local/share/hamster/mapping.kimai.csv)')
@click.option('--output', help='Output file (default kimai.csv)')
@click.option('--category-search', help='Category search string')
@click.option('--after', help='Only show time entries after this date')
@click.option('--show-missing', help='Just report on the missing entries', is_flag=True)
@click.argument('username')
def _import(username, mapping_path=None, output=None, category_search=None, after=None, show_missing=False):
"""
Export time tracking data in Kimai format
"""
if mapping_path is None:
mapping_path = HAMSTER_DIR / 'mapping.kimai.csv'
if output is None:
timestamp = datetime.now().strftime('%F')
output = f'kimai_{timestamp}.csv'
mapping_file = _get_kimai_mapping_file(mapping_path)
mapping_reader = csv.reader(mapping_file)
next(mapping_reader) next(mapping_reader)
@ -416,9 +518,18 @@ def kimai(username, mapping=None, output=None, category_search=None):
category_search = '%{0}%'.format(category_search) category_search = '%{0}%'.format(category_search)
args.append(category_search) args.append(category_search)
if after is not None:
if category_search is not None:
sql = sql + ' AND '
else:
sql = sql + ' WHERE '
sql = sql + f"DATE(facts.start_time) > DATE(?)"
args.append(after)
results = c.execute(sql, args) results = c.execute(sql, args)
results = c.fetchall() results = c.fetchall()
if not show_missing:
output_writer.writerow([ output_writer.writerow([
"Date", "Date",
"From", "From",
@ -441,9 +552,14 @@ def kimai(username, mapping=None, output=None, category_search=None):
try: try:
mapping_ = mapping[k] mapping_ = mapping[k]
except KeyError: except KeyError:
if show_missing:
output_writer.writerow([fact[6], fact[5]])
click.secho("Can't find mapping for '{0}', skipping".format(k), fg='yellow') click.secho("Can't find mapping for '{0}', skipping".format(k), fg='yellow')
continue continue
if show_missing:
continue
if fact[1] is None or fact[2] is None: if fact[1] is None or fact[2] is None:
click.secho("Missing duration data '{0}-{1}', skipping".format( click.secho("Missing duration data '{0}-{1}', skipping".format(
fact[1], fact[1],
@ -457,7 +573,7 @@ def kimai(username, mapping=None, output=None, category_search=None):
) )
duration = ( duration = (
date_start - date_end date_start - date_end
).seconds ).seconds / 3600
output_writer.writerow([ output_writer.writerow([
date_start.strftime('%Y-%m-%d'), date_start.strftime('%Y-%m-%d'),