2019-12-27 10:28:25 +00:00
#!/usr/bin/env python3.7
2020-09-30 18:36:27 +00:00
import csv
2023-11-01 20:56:04 +00:00
import logging
2020-09-30 18:36:27 +00:00
from datetime import datetime
2023-07-08 12:13:07 +00:00
from itertools import chain
2019-12-27 10:28:25 +00:00
from pathlib import Path
2021-04-06 09:43:28 +00:00
import sys
import click
import requests
2023-10-28 22:40:27 +00:00
from peewee import fn , JOIN
2023-11-01 20:56:04 +00:00
from textual . logging import TextualHandler
2023-10-28 22:40:27 +00:00
2023-10-29 13:40:03 +00:00
from . db import (
db ,
HamsterCategory ,
HamsterActivity ,
HamsterFact ,
KimaiCustomer ,
KimaiProject ,
KimaiActivity ,
2023-11-01 19:28:55 +00:00
HamsterActivityKimaiMapping ,
2023-11-01 19:33:30 +00:00
HamsterFactKimaiImport ,
2023-10-29 13:40:03 +00:00
)
2023-11-17 23:22:21 +00:00
from . kimaiapi import KimaiAPI , Timesheet
2023-11-01 21:11:21 +00:00
from . sync import sync
2023-10-29 13:40:03 +00:00
HAMSTER_DIR = Path . home ( ) / " .local/share/hamster "
2023-11-03 23:42:11 +00:00
HAMSTER_FILE = HAMSTER_DIR / " hamster.db "
2019-12-27 10:28:25 +00:00
2023-10-28 22:40:27 +00:00
db . init ( HAMSTER_FILE )
2019-12-27 10:28:25 +00:00
@click.group ( )
2023-11-01 20:56:04 +00:00
@click.option ( " -d " , " --debug " , is_flag = True )
def cli ( debug ) :
if debug :
2023-11-01 21:11:21 +00:00
logging . basicConfig ( )
logging . getLogger ( ) . setLevel ( logging . DEBUG )
2023-11-01 20:56:04 +00:00
peewee_logger = logging . getLogger ( " peewee " )
peewee_logger . addHandler ( TextualHandler ( ) )
peewee_logger . setLevel ( logging . DEBUG )
2023-11-01 21:11:21 +00:00
2023-11-01 20:56:04 +00:00
requests_log = logging . getLogger ( " requests.packages.urllib3 " )
requests_log . setLevel ( logging . DEBUG )
requests_log . propagate = True
2019-12-27 10:28:25 +00:00
@cli.group ( )
def categories ( ) :
pass
2021-04-06 09:43:28 +00:00
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
@categories.command ( " list " )
@click.option ( " --search " , help = " Search string " )
2019-12-27 10:28:25 +00:00
def list_categories ( search ) :
2023-10-29 13:40:03 +00:00
""" List / search categories """
2023-10-28 22:40:27 +00:00
categories = HamsterCategory . select ( )
2019-12-27 10:28:25 +00:00
2023-10-28 22:40:27 +00:00
if search is not None :
categories = categories . where ( HamsterCategory . name . contains ( search ) )
for c in categories :
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { c . id } : { c . name } " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
@categories.command ( " delete " )
@click.argument ( " ids " , nargs = - 1 )
2019-12-28 09:33:35 +00:00
def delete_categories ( ids ) :
2023-10-29 13:40:03 +00:00
""" Delete categories specified by IDS """
click . secho ( " Deleting: " , fg = " red " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
categories = (
HamsterCategory . select (
HamsterCategory , fn . Count ( HamsterActivity . id ) . alias ( " activities_count " )
)
. join ( HamsterActivity , JOIN . LEFT_OUTER )
. group_by ( HamsterCategory )
. where ( HamsterCategory . id . in_ ( ids ) )
)
2019-12-27 10:28:25 +00:00
2023-10-28 22:40:27 +00:00
for c in categories :
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { c . id } : { c . name } ( { c . activities_count } activities) " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . confirm ( " Do you want to continue? " , abort = True )
2019-12-27 10:28:25 +00:00
2023-10-28 22:40:27 +00:00
count = HamsterCategory . delete ( ) . where ( HamsterCategory . id . in_ ( ids ) ) . execute ( )
2021-09-07 21:53:36 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( " Deleted {0} categories " . format ( count ) , fg = " green " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
@categories.command ( " rename " )
@click.argument ( " id_ " , metavar = " ID " )
@click.argument ( " name " )
2021-09-07 21:53:36 +00:00
def rename_category ( id_ , name ) :
2023-10-29 13:40:03 +00:00
""" Rename a category """
2021-09-07 21:53:36 +00:00
2023-10-29 09:28:25 +00:00
category = HamsterCategory . get ( id = id_ )
2021-09-07 21:53:36 +00:00
2023-10-29 09:28:25 +00:00
click . echo ( f ' Renaming @ { category . id } : { category . name } to " { name } " ' )
2021-09-07 21:53:36 +00:00
2023-10-29 09:28:25 +00:00
category . name = name
category . save ( )
2021-09-07 21:53:36 +00:00
2022-11-12 00:42:24 +00:00
2023-10-29 13:40:03 +00:00
@categories.command ( " activities " )
@click.argument ( " ids " , nargs = - 1 )
2019-12-27 10:28:25 +00:00
def list_category_activities ( ids ) :
2023-10-29 13:40:03 +00:00
""" Show activities for categories specified by ids """
2023-10-29 09:28:25 +00:00
2023-10-29 13:40:03 +00:00
activities = (
HamsterActivity . select ( HamsterActivity , HamsterCategory . name )
. join ( HamsterCategory , JOIN . LEFT_OUTER )
. where ( HamsterCategory . id . in_ ( ids ) )
)
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { a . id } : { a . category . name } » { a . name } " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
@categories.command ( " tidy " )
2022-11-12 01:18:23 +00:00
def tidy_categories ( ) :
2023-10-29 13:40:03 +00:00
""" Remove categories with no activities """
2022-11-12 01:18:23 +00:00
2023-10-29 09:28:25 +00:00
subquery = (
2023-10-29 13:40:03 +00:00
HamsterCategory . select (
HamsterCategory , fn . COUNT ( HamsterActivity . id ) . alias ( " activities_count " )
)
2023-10-29 09:28:25 +00:00
. join ( HamsterActivity , JOIN . LEFT_OUTER )
. group_by ( HamsterCategory )
2023-10-29 13:40:03 +00:00
. alias ( " subquery " )
2023-10-29 09:28:25 +00:00
)
categories = (
2023-10-29 13:40:03 +00:00
HamsterCategory . select ( )
2023-10-29 09:28:25 +00:00
. join ( subquery , on = ( HamsterCategory . id == subquery . c . id ) )
. where ( subquery . c . activities_count == 0 )
)
2022-11-12 01:18:23 +00:00
2023-10-29 13:40:03 +00:00
click . echo ( " Found {0} empty categories: " . format ( categories . count ( ) ) )
2022-11-12 01:18:23 +00:00
for cat in categories :
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { cat . id } : { cat . name } " )
2022-11-12 01:18:23 +00:00
2023-10-29 13:40:03 +00:00
click . confirm ( " Do you want to continue? " , abort = True )
2022-11-12 01:18:23 +00:00
2023-10-29 09:28:25 +00:00
[ cat . delete_instance ( ) for cat in categories ]
2022-11-12 01:18:23 +00:00
2019-12-27 10:28:25 +00:00
@cli.group ( )
def activities ( ) :
pass
2023-10-29 13:40:03 +00:00
@activities.command ( " list " )
@click.option ( " --search " , help = " Search string " )
@click.option ( " --csv/--no-csv " , " csv_output " , default = False , help = " CSV output " )
2020-12-18 15:11:39 +00:00
def list_activities ( search , csv_output ) :
2023-10-29 13:40:03 +00:00
""" List / search activities """
activities = (
HamsterActivity . select ( HamsterActivity , HamsterCategory )
. join ( HamsterCategory , JOIN . LEFT_OUTER )
. order_by ( HamsterCategory . name , HamsterActivity . name )
2023-10-29 09:28:25 +00:00
)
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
if search is not None :
activities = activities . where ( HamsterActivity . name . contains ( search ) )
2023-01-13 00:34:51 +00:00
2020-12-18 15:11:39 +00:00
if csv_output :
csv_writer = csv . writer ( sys . stdout )
2023-10-29 09:28:25 +00:00
for a in activities :
category_name = a . category . name if a . category_id != - 1 else " "
2020-12-18 15:11:39 +00:00
if csv_output :
2023-10-29 13:40:03 +00:00
csv_writer . writerow ( [ a . category_id , category_name , a . id , a . name ] )
2020-12-18 15:11:39 +00:00
else :
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { a . category_id } : { category_name } » { a . id } : { a . name } " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
@activities.command ( " delete " )
@click.argument ( " ids " , nargs = - 1 )
2019-12-27 10:28:25 +00:00
def delete_activities ( ids ) :
2023-10-29 13:40:03 +00:00
""" Delete activities specified by IDS """
activities = (
HamsterActivity . select (
HamsterActivity ,
HamsterCategory . name ,
fn . Count ( HamsterFact . id ) . alias ( " facts_count " ) ,
)
. join ( HamsterCategory , JOIN . LEFT_OUTER )
. switch ( HamsterActivity )
. join ( HamsterFact , JOIN . LEFT_OUTER )
. group_by ( HamsterActivity )
. where ( HamsterActivity . id . in_ ( ids ) )
2023-10-29 09:28:25 +00:00
)
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( " Deleting: " , fg = " red " )
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
category_name = a . category . name if a . category_id != - 1 else " "
2023-10-29 13:40:03 +00:00
click . echo ( f " @ { a . id } : { category_name } » { a . name } ( { a . facts_count } facts) " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . confirm ( " Do you want to continue? " , abort = True )
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
[ a . delete_instance ( ) for a in activities ]
2019-12-28 09:33:35 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( " Deleted {0} activities " . format ( len ( ids ) ) , fg = " green " )
2019-12-28 09:33:35 +00:00
2020-09-21 18:17:59 +00:00
2019-12-27 10:28:25 +00:00
@activities.command ( )
2023-10-29 13:40:03 +00:00
@click.argument ( " category_id " )
@click.argument ( " ids " , nargs = - 1 )
2019-12-27 10:28:25 +00:00
def move ( category_id , ids ) :
2023-10-29 13:40:03 +00:00
""" Move activities to another category """
2023-10-29 09:28:25 +00:00
category = HamsterCategory . get ( id = category_id )
activities = HamsterActivity . select ( ) . where ( HamsterActivity . id . in_ ( ids ) )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( f ' Moving to " @ { category . id } : { category . name } " : ' , fg = " green " )
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
category_name = a . category . name if a . category_id != - 1 else " "
2023-10-29 13:40:03 +00:00
click . secho ( f " @ { a . category_id } : { category_name } » @ { a . id } : { a . name } " , fg = " blue " )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . confirm ( " Do you want to continue? " , abort = True )
2019-12-27 10:28:25 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
a . category = category
a . save ( )
2019-12-27 10:28:25 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( " Moved {0} activities " . format ( len ( ids ) ) , fg = " green " )
2019-12-27 10:28:25 +00:00
2019-12-28 09:33:35 +00:00
@activities.command ( )
2023-10-29 13:40:03 +00:00
@click.argument ( " ids " , nargs = - 1 )
2019-12-28 09:33:35 +00:00
def list_facts ( ids ) :
2023-10-29 13:40:03 +00:00
""" Show facts for activities """
2023-10-29 09:28:25 +00:00
activities = HamsterActivity . select ( ) . where ( HamsterActivity . id . in_ ( ids ) )
2019-12-28 09:33:35 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
2023-10-29 13:40:03 +00:00
click . secho ( f " @ { a . id } : { a . name } " , fg = " green " )
2019-12-28 09:33:35 +00:00
2023-10-29 09:28:25 +00:00
for f in a . facts :
2023-10-29 13:40:03 +00:00
click . secho ( f " @ { f . id } , { f . start_time } " , fg = " blue " )
2022-11-12 00:43:30 +00:00
2019-12-28 09:33:35 +00:00
@activities.command ( )
2023-10-29 13:40:03 +00:00
@click.argument ( " from_id " )
@click.argument ( " to_id " )
2019-12-28 09:33:35 +00:00
def move_facts ( from_id , to_id ) :
2023-10-29 13:40:03 +00:00
""" Move facts from one activity to another """
2023-10-29 09:28:25 +00:00
from_activity = HamsterActivity . get ( id = from_id )
to_activity = HamsterActivity . get ( id = to_id )
2023-10-29 13:40:03 +00:00
from_category_name = (
from_activity . category . name if from_activity . category_id != - 1 else " "
)
to_category_name = (
to_activity . category . name if to_activity . category_id != - 1 else " "
)
2019-12-28 09:33:35 +00:00
click . secho (
2023-10-29 13:40:03 +00:00
f ' Moving facts from " { from_category_name } » @ { from_activity . id } : { from_activity . name } " to " @ { to_category_name } » @ { to_activity . id } : { to_activity . name } " ' ,
fg = " green " ,
2019-12-28 09:33:35 +00:00
)
2023-10-29 09:28:25 +00:00
for f in from_activity . facts :
2023-10-29 13:40:03 +00:00
click . secho ( f " @ { f . id } , { f . start_time } " , fg = " blue " )
2019-12-28 09:33:35 +00:00
2023-10-29 13:40:03 +00:00
click . confirm ( " Do you want to continue? " , abort = True )
2019-12-28 09:33:35 +00:00
2023-10-29 13:40:03 +00:00
count = (
HamsterFact . update ( activity_id = to_activity . id )
. where ( HamsterFact . activity == from_activity )
. execute ( )
)
2019-12-28 09:33:35 +00:00
2023-10-29 13:40:03 +00:00
click . secho ( " Moved {0} facts " . format ( count ) , fg = " green " )
2019-12-28 09:33:35 +00:00
2021-09-07 21:53:36 +00:00
click . confirm (
2023-10-29 09:28:25 +00:00
f ' Would you like to delete " { from_category_name } » @ { from_activity . id } : { from_activity . name } ? ' ,
2023-10-29 13:40:03 +00:00
abort = True ,
2021-09-07 21:53:36 +00:00
)
2023-10-29 09:28:25 +00:00
from_activity . delete_instance ( )
2021-09-07 21:53:36 +00:00
2019-12-28 09:33:35 +00:00
@activities.command ( )
def find_duplicates ( ) :
2023-10-29 13:40:03 +00:00
""" Show activities which are not unique in their categories """
2019-12-28 09:33:35 +00:00
2023-10-29 09:28:25 +00:00
non_unique_activities = (
2023-10-29 13:40:03 +00:00
HamsterActivity . select (
2023-10-29 09:28:25 +00:00
HamsterActivity ,
HamsterCategory . id ,
2023-10-29 13:40:03 +00:00
fn . COALESCE ( HamsterCategory . name , " None " ) . alias ( " category_name " ) ,
2023-10-29 09:28:25 +00:00
)
. join ( HamsterCategory , JOIN . LEFT_OUTER )
. group_by ( HamsterActivity . category_id , HamsterActivity . name )
. having ( fn . COUNT ( HamsterActivity . id ) > 1 )
)
for activity in non_unique_activities :
2022-11-12 00:43:30 +00:00
click . secho (
2023-10-29 13:40:03 +00:00
f " @ { activity . category_id } : { activity . category_name } » @ { activity . id } : { activity . name } " ,
fg = " blue " ,
)
2019-12-28 09:33:35 +00:00
2020-09-30 18:36:27 +00:00
@cli.group ( )
2021-04-06 09:43:28 +00:00
def kimai ( ) :
2020-09-30 18:36:27 +00:00
pass
2023-07-08 12:13:07 +00:00
def _get_kimai_mapping_file ( path , category_search = None ) :
2020-09-30 18:36:27 +00:00
try :
2021-04-06 09:43:28 +00:00
return open ( path )
2020-09-30 18:36:27 +00:00
except FileNotFoundError :
2023-10-29 13:40:03 +00:00
click . confirm ( " Mapping file {} not found, create it?: " . format ( path ) , abort = True )
mapping_file = open ( path , " w " )
2020-09-30 18:36:27 +00:00
mapping_writer = csv . writer ( mapping_file )
2023-10-29 13:40:03 +00:00
mapping_writer . writerow (
[
" FROM category " ,
" FROM activity " ,
" TO Customer " ,
" TO Project " ,
" TO Activity " ,
" TO Tag " ,
" TO Note " ,
]
)
activities = HamsterActivity . select ( HamsterActivity , HamsterCategory ) . join (
HamsterCategory , JOIN . LEFT_OUTER
)
2020-09-30 18:36:27 +00:00
2023-10-29 09:28:25 +00:00
for a in activities :
2023-10-29 13:40:03 +00:00
mapping_writer . writerow (
[ a . category . name if a . category_id != - 1 else " " , a . name ]
)
2020-09-30 18:36:27 +00:00
mapping_file . close ( )
2021-04-06 09:43:28 +00:00
return open ( path )
2023-11-01 21:56:08 +00:00
@kimai.command ( )
2023-10-29 13:40:03 +00:00
@click.option (
" --mapping-path " ,
help = " Mapping file (default ~/.local/share/hamster/mapping.kimai.csv) " ,
multiple = True ,
)
@click.argument ( " username " )
2023-11-01 21:56:08 +00:00
@click.argument ( " api_key " , envvar = " KIMAI_API_KEY " )
2023-10-29 13:40:03 +00:00
@click.option ( " --just-errors " , " just_errors " , is_flag = True , help = " Only display errors " )
@click.option ( " --ignore-activities " , is_flag = True , help = " Ignore missing activities " )
2023-11-01 21:11:21 +00:00
def check ( username , api_key , just_errors , ignore_activities , mapping_path = None ) :
2021-04-06 09:43:28 +00:00
"""
2023-11-01 21:11:21 +00:00
Check customer / project / activity data from Kimai
2021-04-06 09:43:28 +00:00
"""
2023-10-29 13:40:03 +00:00
kimai_api_url = " https://kimai.autonomic.zone/api "
2021-04-06 09:43:28 +00:00
2023-11-01 21:56:08 +00:00
if len ( mapping_path ) == 0 :
mapping_path = ( HAMSTER_DIR / " mapping.kimai.csv " , )
mapping_files = [ ]
for mapping_path_item in mapping_path :
if not Path ( mapping_path_item ) . exists ( ) :
2023-11-02 22:43:11 +00:00
raise click . UsageError ( f " { mapping_path_item } does not exist " )
2023-11-01 21:56:08 +00:00
mapping_file = _get_kimai_mapping_file ( mapping_path_item )
next ( mapping_file )
mapping_files . append ( mapping_file )
mapping_reader = csv . reader ( chain ( * mapping_files ) )
2021-04-06 09:43:28 +00:00
next ( mapping_reader )
2023-10-29 13:40:03 +00:00
mapping_data = [ [ row [ 2 ] , row [ 3 ] , row [ 4 ] ] for row in mapping_reader ]
2021-04-06 09:43:28 +00:00
mapping_file . close ( )
2023-10-29 13:40:03 +00:00
auth_headers = { " X-AUTH-USER " : username , " X-AUTH-TOKEN " : api_key }
2021-04-06 09:43:28 +00:00
2022-11-12 00:43:30 +00:00
customers = requests . get (
2023-10-29 13:40:03 +00:00
f " { kimai_api_url } /customers?visible=3 " , headers = auth_headers
) . json ( )
2022-11-12 00:43:30 +00:00
projects = requests . get (
2023-10-29 13:40:03 +00:00
f " { kimai_api_url } /projects?visible=3 " , headers = auth_headers
) . json ( )
2022-11-12 00:43:30 +00:00
activities = requests . get (
2023-10-29 13:40:03 +00:00
f " { kimai_api_url } /activities?visible=3 " , headers = auth_headers
) . json ( )
2021-04-06 09:43:28 +00:00
found_customers = [ ]
found_projects = [ ]
found_activities = [ ]
for row in mapping_data :
# Check if each mapping still exists in Kimai
2023-10-29 13:40:03 +00:00
matching_customers = list ( filter ( lambda x : x [ " name " ] == row [ 0 ] , customers ) )
2021-04-06 09:43:28 +00:00
if row [ 0 ] in found_customers :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Skipping existing customer ' {0} ' " . format ( row [ 0 ] ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
else :
if len ( matching_customers ) > 1 :
2022-11-12 00:43:30 +00:00
click . secho (
2023-10-29 13:40:03 +00:00
" More than one match for customer ' {0} ' " . format ( row [ 0 ] ) , fg = " red "
)
2021-04-06 09:43:28 +00:00
continue
elif len ( matching_customers ) < 1 :
2023-10-29 13:40:03 +00:00
click . secho ( " Missing customer ' {0} ' " . format ( row [ 0 ] ) , fg = " yellow " )
2021-04-06 09:43:28 +00:00
continue
else :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Found customer ' {0} ' " . format ( row [ 0 ] ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
found_customers . append ( row [ 0 ] )
2023-10-29 13:40:03 +00:00
project_str = " : " . join ( row [ 0 : 2 ] )
matching_projects = list (
filter (
lambda x : x [ " name " ] == row [ 1 ]
and x [ " customer " ] == matching_customers [ 0 ] [ " id " ] ,
projects ,
)
2021-04-06 09:43:28 +00:00
)
if project_str in found_projects :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Skipping existing project ' {0} ' " . format ( project_str ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
else :
if len ( matching_projects ) > 1 :
2023-10-29 13:40:03 +00:00
click . secho (
" More than one match for project ' {0} ' " . format ( project_str ) ,
fg = " red " ,
)
2021-04-06 09:43:28 +00:00
continue
elif len ( matching_projects ) < 1 :
2023-10-29 13:40:03 +00:00
click . secho ( " Missing project ' {0} ' " . format ( project_str ) , fg = " yellow " )
2021-04-06 09:43:28 +00:00
continue
else :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Found project ' {0} ' " . format ( project_str ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
found_projects . append ( project_str )
2021-10-11 13:50:09 +00:00
if ignore_activities :
continue
2023-10-29 13:40:03 +00:00
activity_str = " : " . join ( row )
2021-04-06 09:43:28 +00:00
if activity_str in found_activities :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Skipping existing activity ' {0} ' " . format ( activity_str ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
else :
2023-10-29 13:40:03 +00:00
matching_activities = list (
filter (
lambda x : x [ " name " ] == row [ 2 ]
and x [ " project " ] == matching_projects [ 0 ] [ " id " ] ,
activities ,
)
)
2021-04-06 09:43:28 +00:00
if len ( matching_activities ) > 1 :
2023-10-29 13:40:03 +00:00
click . secho (
" More than one match for activity ' {0} ' " . format ( activity_str ) ,
fg = " red " ,
)
2021-04-06 09:43:28 +00:00
elif len ( matching_activities ) < 1 :
2023-10-29 13:40:03 +00:00
click . secho ( " Missing activity ' {0} ' " . format ( activity_str ) , fg = " yellow " )
2021-04-06 09:43:28 +00:00
else :
2022-11-12 00:43:30 +00:00
just_errors or click . secho (
2023-10-29 13:40:03 +00:00
" Found activity ' {0} ' " . format ( activity_str ) , fg = " green "
)
2021-04-06 09:43:28 +00:00
found_activities . append ( activity_str )
2021-11-29 13:24:51 +00:00
2023-11-17 23:22:21 +00:00
@kimai.command ( " csv " )
2023-10-29 13:40:03 +00:00
@click.option (
" --mapping-path " ,
help = " Mapping file (default ~/.local/share/hamster/mapping.kimai.csv) " ,
multiple = True ,
)
@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 " )
2023-11-17 23:22:21 +00:00
def _csv (
2023-10-29 13:40:03 +00:00
username ,
mapping_path = None ,
output = None ,
category_search = None ,
after = None ,
show_missing = False ,
) :
2021-04-06 09:43:28 +00:00
"""
Export time tracking data in Kimai format
"""
if mapping_path is None :
2023-10-29 13:40:03 +00:00
mapping_path = HAMSTER_DIR / " mapping.kimai.csv "
2021-04-06 09:43:28 +00:00
if output is None :
2023-10-29 13:40:03 +00:00
timestamp = datetime . now ( ) . strftime ( " %F " )
output = f " kimai_ { timestamp } .csv "
2021-04-06 09:43:28 +00:00
2023-07-08 12:13:07 +00:00
if type ( mapping_path ) == tuple :
mapping_files = [ ]
for mapping_path_item in mapping_path :
mapping_file = _get_kimai_mapping_file ( mapping_path_item , category_search )
next ( mapping_file )
mapping_files . append ( mapping_file )
mapping_reader = csv . reader ( chain ( * mapping_files ) )
else :
mapping_file = _get_kimai_mapping_file ( mapping_path , category_search )
next ( mapping_file )
mapping_reader = csv . reader ( mapping_file )
2020-09-30 18:36:27 +00:00
mapping = {
2023-10-29 13:40:03 +00:00
" {0} : {1} " . format ( row [ 0 ] , row [ 1 ] ) : [ row [ 2 ] , row [ 3 ] , row [ 4 ] , row [ 5 ] ]
2020-09-30 18:36:27 +00:00
for row in mapping_reader
}
2023-07-08 12:13:07 +00:00
if type ( mapping_path ) == tuple :
for mapping_file in mapping_files :
mapping_file . close ( )
else :
mapping_file . close ( )
2020-09-30 18:36:27 +00:00
2023-10-29 13:40:03 +00:00
output_file = open ( output , " w " )
2020-09-30 18:36:27 +00:00
output_writer = csv . writer ( output_file )
args = [ ]
2023-10-29 13:40:03 +00:00
facts = (
HamsterFact . select ( HamsterFact , HamsterActivity , HamsterCategory )
. join ( HamsterActivity )
. join ( HamsterCategory , JOIN . LEFT_OUTER )
)
2023-10-29 09:28:25 +00:00
2023-10-29 13:40:03 +00:00
sql = """
2021-04-06 09:43:28 +00:00
SELECT
facts . id , facts . start_time , facts . end_time , facts . description ,
activities . id , activities . name ,
categories . name , categories . id
FROM
facts
LEFT JOIN
activities ON facts . activity_id = activities . id
LEFT JOIN
2023-10-29 13:40:03 +00:00
categories ON activities . category_id = categories . id """
2020-09-30 18:36:27 +00:00
if category_search is not None :
2023-10-29 09:28:25 +00:00
facts = facts . where ( HamsterCategory . name . contains ( category_search ) )
2020-09-30 18:36:27 +00:00
2021-04-06 09:43:28 +00:00
if after is not None :
2023-10-29 09:28:25 +00:00
facts = facts . where ( HamsterFact . start_time > = after )
2020-09-30 18:36:27 +00:00
2021-04-06 09:43:28 +00:00
if not show_missing :
2023-10-29 13:40:03 +00:00
output_writer . writerow (
[
" Date " ,
" From " ,
" To " ,
" Duration " ,
" Rate " ,
" User " ,
" Customer " ,
" Project " ,
" Activity " ,
" Description " ,
" Exported " ,
" Tags " ,
" HourlyRate " ,
" FixedRate " ,
" InternalRate " ,
]
)
2020-09-30 18:36:27 +00:00
2023-10-29 09:28:25 +00:00
for fact in facts :
2023-10-29 13:40:03 +00:00
k = f " { fact . activity . category . name } : { fact . activity . name } "
2020-09-30 18:36:27 +00:00
try :
mapping_ = mapping [ k ]
except KeyError :
2021-04-06 09:43:28 +00:00
if show_missing :
2023-10-29 13:40:03 +00:00
output_writer . writerow (
[ fact . activity . category . name , fact . activity . name ]
)
click . secho ( " Can ' t find mapping for ' {0} ' , skipping " . format ( k ) , fg = " yellow " )
2020-09-30 18:36:27 +00:00
continue
2021-04-06 09:43:28 +00:00
if show_missing :
continue
2023-10-29 09:28:25 +00:00
if fact . start_time is None or fact . end_time is None :
2023-10-29 13:40:03 +00:00
click . secho (
f " Missing duration data ' { fact . start_time } - { fact . end_time } ' , skipping " ,
fg = " yellow " ,
)
2020-09-30 18:36:27 +00:00
continue
2023-01-13 00:34:51 +00:00
if len ( mapping_ ) < 5 :
mapping_ . append ( None )
2020-09-30 18:36:27 +00:00
date_start , date_end = (
2023-10-29 13:40:03 +00:00
datetime . strptime ( fact . start_time . split ( " . " ) [ 0 ] , " % Y- % m- %d % H: % M: % S " ) ,
datetime . strptime ( fact . end_time . split ( " . " ) [ 0 ] , " % Y- % m- %d % H: % M: % S " ) ,
)
duration = ( date_start - date_end ) . seconds / 3600
output_writer . writerow (
[
date_start . strftime ( " % Y- % m- %d " ) ,
date_start . strftime ( " % H: % M " ) ,
" " , # To (time)
duration ,
" " , # Rate
username ,
mapping_ [ 0 ] ,
mapping_ [ 1 ] ,
mapping_ [ 2 ] ,
fact . description or mapping_ [ 4 ] or " " ,
" 0 " , # Exported
mapping_ [ 3 ] ,
" " , # Hourly rate
" " , # Fixed rate
]
2020-09-30 18:36:27 +00:00
)
output_file . close ( )
2023-11-17 23:22:21 +00:00
@kimai.command ( " import " )
@click.argument ( " search " )
@click.argument ( " after " )
@click.argument ( " before " )
def _import ( search , after , before ) :
api = KimaiAPI ( )
SEARCH = " auto "
facts = (
HamsterFact . select ( HamsterFact , HamsterActivity , HamsterCategory )
. join ( HamsterActivity , JOIN . LEFT_OUTER )
. join ( HamsterCategory , JOIN . LEFT_OUTER )
. where (
( HamsterFact . start_time > datetime . strptime ( after , " % Y- % m- %d " ) )
& ( HamsterFact . start_time < datetime . strptime ( before , " % Y- % m- %d " ) )
& HamsterCategory . name . contains ( SEARCH )
)
)
has_errors = False
# check data
for f in facts :
mappings = f . activity . mappings
if len ( mappings ) == 0 :
print (
f " fact { f . id } : @ { f . activity . category . id } { f . activity . category . name } » @ { f . activity . id } { f . activity . name } has no mapping "
)
has_errors = True
continue
if len ( mappings ) > 1 :
print (
f " fact { f . id } : activity @ { f . activity . id } { f . activity . name } has multiple mappings "
)
has_errors = True
continue
if (
mappings [ 0 ] . kimai_activity . project is None
and not mappings [ 0 ] . kimai_project . allow_global_activities
) :
click . secho (
2024-02-10 06:35:00 +00:00
f " fact { f . id } : project @ { mappings [ 0 ] . kimai_project . id } { mappings [ 0 ] . kimai_project . name } does not allow global activity { mappings [ 0 ] . kimai_activity } ( { mappings [ 0 ] . hamster_activity . name } ) " , fg = " red " ,
2023-11-17 23:22:21 +00:00
)
has_errors = True
continue
if f . imports . count ( ) > 0 :
click . secho (
f " fact { f . id } : activity @ { f . activity . id } { f . activity . name } was already imported { f . imports . count ( ) } time(s) " ,
2023-11-18 11:23:54 +00:00
fg = " yellow " ,
2023-11-17 23:22:21 +00:00
)
continue
if has_errors :
sys . exit ( 1 )
# upload data
for f in facts :
try :
mapping = f . activity . mappings [ 0 ]
except IndexError :
print (
f " no mapping, skipping { f . id } ( { f . activity . category . name } » { f . activity . name } ) "
)
continue
if f . imports . count ( ) > 0 :
print (
f " already imported, skipping { f . id } ( { f . activity . category . name } » { f . activity . name } ) "
)
continue
t = Timesheet (
api ,
activity = mapping . kimai_activity ,
project = mapping . kimai_project ,
begin = f . start_time ,
end = f . end_time ,
2023-11-18 11:23:54 +00:00
description = f . description
if f . description != " "
else mapping . kimai_description ,
2023-11-17 23:22:21 +00:00
# tags=f.tags if f.tags != '' else mapping.kimai_tags
)
r = t . upload ( ) . json ( )
if r . get ( " code " , 200 ) != 200 :
print ( r )
print ( f " { f . id } ( { f . activity . category . name } » { f . activity . name } ) " )
from pdb import set_trace
2023-11-18 11:23:54 +00:00
2023-11-17 23:22:21 +00:00
set_trace ( )
else :
HamsterFactKimaiImport . create ( hamster_fact = f , kimai_id = r [ " id " ] ) . save ( )
print ( f ' Created Kimai timesheet { r [ " id " ] } ' )
2023-11-18 11:23:54 +00:00
2023-10-29 13:40:03 +00:00
@kimai.group ( " db " )
2023-10-29 09:28:25 +00:00
def db_ ( ) :
pass
@db_.command ( )
def init ( ) :
2023-11-01 19:33:30 +00:00
db . create_tables (
[
KimaiCustomer ,
KimaiProject ,
KimaiActivity ,
HamsterActivityKimaiMapping ,
HamsterFactKimaiImport ,
]
)
2023-10-28 22:40:27 +00:00
2023-10-29 09:28:25 +00:00
@db_.command ( )
def reset ( ) :
2023-11-01 19:28:55 +00:00
HamsterActivityKimaiMapping . delete ( ) . execute ( )
2023-10-28 22:40:27 +00:00
2023-11-01 21:11:21 +00:00
@db_.command ( " sync " )
def kimai_db_sync ( ) :
sync ( )
2023-10-29 09:28:25 +00:00
@db_.command ( )
2023-10-29 13:40:03 +00:00
@click.option (
" -g " ,
" --global " ,
" global_ " ,
help = " Does this file contain mappings to global activties " ,
is_flag = True ,
)
@click.option ( " --mapping-path " , help = " Mapping file " )
2023-10-29 13:37:40 +00:00
def mapping2db ( mapping_path = None , global_ = False ) :
2023-10-28 22:40:27 +00:00
mapping_file = _get_kimai_mapping_file ( mapping_path , None )
next ( mapping_file )
mapping_reader = csv . reader ( mapping_file )
2023-10-29 13:40:03 +00:00
2023-10-28 22:40:27 +00:00
for row in mapping_reader :
hamster_category = HamsterCategory . get ( name = row [ 0 ] )
2023-10-29 13:40:03 +00:00
hamster_activity = HamsterActivity . get (
name = row [ 1 ] , category_id = hamster_category . id
)
2023-10-28 22:40:27 +00:00
kimai_customer = KimaiCustomer . get ( name = row [ 2 ] )
2023-10-29 13:40:03 +00:00
kimai_project = KimaiProject . get ( name = row [ 3 ] , customer_id = kimai_customer . id )
2023-10-29 13:37:40 +00:00
2023-10-28 22:40:27 +00:00
try :
2023-10-29 13:40:03 +00:00
kimai_activity = KimaiActivity . get ( name = row [ 4 ] , project_id = kimai_project . id )
2023-10-28 22:40:27 +00:00
except KimaiActivity . DoesNotExist :
2023-11-01 19:33:30 +00:00
kimai_activity = KimaiActivity . get ( name = row [ 4 ] , project_id = None )
2023-10-28 22:40:27 +00:00
2023-11-01 19:28:55 +00:00
HamsterActivityKimaiMapping . create (
2023-10-28 22:40:27 +00:00
hamster_activity = hamster_activity ,
kimai_customer = kimai_customer ,
kimai_project = kimai_project ,
kimai_activity = kimai_activity ,
kimai_description = row [ 6 ] ,
2023-10-29 13:40:03 +00:00
kimai_tags = row [ 5 ] ,
2023-10-28 22:40:27 +00:00
)
2023-10-29 09:28:25 +00:00
2023-10-27 00:13:08 +00:00
@cli.command ( )
def app ( ) :
from . app import HamsterToolsApp
2023-10-29 13:40:03 +00:00
2023-10-27 03:04:30 +00:00
app = HamsterToolsApp ( )
2023-10-27 00:13:08 +00:00
app . run ( )
2020-09-21 18:17:59 +00:00
@cli.command ( )
def hamster ( ) :
2023-10-29 13:40:03 +00:00
click . echo ( " 🐹 " )
2020-09-21 18:17:59 +00:00
2019-12-27 10:28:25 +00:00
if __name__ == " __main__ " :
cli ( )