From ebf2dca6951e5c13729a4618b76b661de3ccb14a Mon Sep 17 00:00:00 2001 From: 3wc <3wc@doesthisthing.work> Date: Wed, 1 Nov 2023 19:28:55 +0000 Subject: [PATCH] =?UTF-8?q?Fuckin=20yaldi,=20working=20kimai=20import=20?= =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hamstertools/__init__.py | 12 +++-- hamstertools/app.py | 13 ++--- hamstertools/db.py | 16 +++++- hamstertools/kimai.py | 111 +++++++++++++++++++++++++++++++-------- 4 files changed, 118 insertions(+), 34 deletions(-) diff --git a/hamstertools/__init__.py b/hamstertools/__init__.py index 338ed39..17f77dd 100755 --- a/hamstertools/__init__.py +++ b/hamstertools/__init__.py @@ -17,7 +17,8 @@ from .db import ( KimaiCustomer, KimaiProject, KimaiActivity, - HamsterKimaiMapping, + HamsterActivityKimaiMapping, + HamsterFactKimaiImport ) HAMSTER_DIR = Path.home() / ".local/share/hamster" @@ -630,12 +631,13 @@ def db_(): @db_.command() def init(): - db.create_tables([KimaiCustomer, KimaiProject, KimaiActivity, HamsterKimaiMapping]) + db.create_tables([KimaiCustomer, KimaiProject, KimaiActivity, + HamsterActivityKimaiMapping, HamsterFactKimaiImport]) @db_.command() def reset(): - HamsterKimaiMapping.delete().execute() + HamsterActivityKimaiMapping.delete().execute() @db_.command() @@ -653,7 +655,6 @@ def mapping2db(mapping_path=None, global_=False): mapping_reader = csv.reader(mapping_file) for row in mapping_reader: - hamster_category = HamsterCategory.get(name=row[0]) hamster_activity = HamsterActivity.get( name=row[1], category_id=hamster_category.id @@ -666,9 +667,10 @@ def mapping2db(mapping_path=None, global_=False): except KimaiActivity.DoesNotExist: kimai_activity = KimaiActivity.get( name=row[4], + project_id=None ) - HamsterKimaiMapping.create( + HamsterActivityKimaiMapping.create( hamster_activity=hamster_activity, kimai_customer=kimai_customer, kimai_project=kimai_project, diff --git a/hamstertools/app.py b/hamstertools/app.py index 25eba38..b7d3b30 100644 --- a/hamstertools/app.py +++ b/hamstertools/app.py @@ -22,7 +22,7 @@ from .db import ( KimaiProject, KimaiCustomer, KimaiActivity, - HamsterKimaiMapping, + HamsterActivityKimaiMapping, ) from .kimai import ( KimaiAPI, @@ -303,11 +303,11 @@ class ActivityListScreen(ListScreen): ) mappings_count_query = ( - HamsterKimaiMapping.select( - HamsterKimaiMapping.hamster_activity_id, - fn.COUNT(HamsterKimaiMapping.id).alias("mappings_count"), + HamsterActivityKimaiMapping.select( + HamsterActivityKimaiMapping.hamster_activity_id, + fn.COUNT(HamsterActivityKimaiMapping.id).alias("mappings_count"), ) - .group_by(HamsterKimaiMapping.hamster_activity_id) + .group_by(HamsterActivityKimaiMapping.hamster_activity_id) .alias("mappings_count_query") ) @@ -460,7 +460,7 @@ class ActivityListScreen(ListScreen): def handle_mapping(mapping): if mapping is None: return - m = HamsterKimaiMapping.create( + m = HamsterActivityKimaiMapping.create( hamster_activity=selected_activity, **mapping ) m.save() @@ -613,6 +613,7 @@ class KimaiProjectListScreen(ListScreen): "id": project.id, "name": project.name, "customer_id": project.customer.id, + "allow_global_activities": project.allow_global_activities } for project in projects ] diff --git a/hamstertools/db.py b/hamstertools/db.py index e628d9f..a47d8c2 100644 --- a/hamstertools/db.py +++ b/hamstertools/db.py @@ -1,5 +1,6 @@ +from datetime import datetime import logging -from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField +from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField, SmallIntegerField, BooleanField from textual.logging import TextualHandler @@ -49,6 +50,7 @@ class KimaiCustomer(Model): class KimaiProject(Model): name = CharField() customer = ForeignKeyField(KimaiCustomer, backref="projects") + allow_global_activities = BooleanField(default=True) class Meta: database = db @@ -64,7 +66,7 @@ class KimaiActivity(Model): table_name = "kimai_activities" -class HamsterKimaiMapping(Model): +class HamsterActivityKimaiMapping(Model): hamster_activity = ForeignKeyField(HamsterActivity, backref="mappings") kimai_customer = ForeignKeyField(KimaiCustomer, backref="mappings") kimai_project = ForeignKeyField(KimaiProject, backref="mappings") @@ -75,3 +77,13 @@ class HamsterKimaiMapping(Model): class Meta: database = db table_name = "hamster_kimai_mappings" + + +class HamsterFactKimaiImport(Model): + hamster_fact = ForeignKeyField(HamsterFact, backref="mappings") + kimai_id = SmallIntegerField() + imported = DateTimeField(default=datetime.now) + + class Meta: + database = db + table_name = "hamster_fact_kimai_imports" diff --git a/hamstertools/kimai.py b/hamstertools/kimai.py index d2b08da..caa9640 100644 --- a/hamstertools/kimai.py +++ b/hamstertools/kimai.py @@ -1,7 +1,10 @@ +from datetime import datetime import requests import requests_cache import os +from dataclasses import dataclass, field + class NotFound(Exception): pass @@ -9,7 +12,8 @@ class NotFound(Exception): class KimaiAPI(object): # temporary hardcoded config - KIMAI_API_URL = "https://kimai.autonomic.zone/api" + # KIMAI_API_URL = "https://kimai.autonomic.zone/api" + KIMAI_API_URL = "https://kimaitest.autonomic.zone/api" KIMAI_USERNAME = "3wordchant" KIMAI_API_KEY = os.environ["KIMAI_API_KEY"] @@ -17,16 +21,19 @@ class KimaiAPI(object): auth_headers = {"X-AUTH-USER": KIMAI_USERNAME, "X-AUTH-TOKEN": KIMAI_API_KEY} def __init__(self): - # requests_cache.install_cache("kimai", backend="sqlite", expire_after=1800) - self.customers_json = requests.get( - f"{self.KIMAI_API_URL}/customers?visible=3", headers=self.auth_headers - ).json() - self.projects_json = requests.get( - f"{self.KIMAI_API_URL}/projects?visible=3", headers=self.auth_headers - ).json() - self.activities_json = requests.get( - f"{self.KIMAI_API_URL}/activities?visible=3", headers=self.auth_headers - ).json() + requests_cache.install_cache("kimai", backend="sqlite", expire_after=1800) + self.customers_json = self.get("customers", {"visible": 3}) + self.projects_json = self.get("projects", {"visible": 3}) + self.activities_json = self.get("activities", {"visible": 3}) + self.user_json = self.get("users/me") + + def get(self, endpoint, params=None): + return requests.get(f"{self.KIMAI_API_URL}/{endpoint}", + params=params, headers=self.auth_headers).json() + + def post(self, endpoint, data): + return requests.post(f"{self.KIMAI_API_URL}/{endpoint}", + json=data, headers=self.auth_headers) class BaseAPI(object): @@ -54,14 +61,20 @@ class Customer(BaseAPI): return f"Customer (id={self.id}, name={self.name})" +@dataclass class Project(BaseAPI): - def __init__(self, api, id, name, customer): - super().__init__(api, id=id, name=name, customer=customer) + api: KimaiAPI = field(repr=False) + id: int + name: str + customer: Customer + allow_global_activities: bool = field(default=True) @staticmethod def list(api): return [ - Project(api, p["id"], p["name"], Customer.get_by_id(api, p["customer"])) + Project(api, p["id"], p["name"], Customer.get_by_id(api, + p["customer"]), + p["globalActivities"]) for p in api.projects_json ] @@ -74,17 +87,18 @@ class Project(BaseAPI): value["id"], value["name"], Customer.get_by_id(api, value["customer"]), + value["globalActivities"] ) if not none: raise NotFound() - def __repr__(self): - return f"Project (id={self.id}, name={self.name}, customer={self.customer})" - +@dataclass class Activity(BaseAPI): - def __init__(self, api, id, name, project): - super().__init__(api, id=id, name=name, project=project) + api: KimaiAPI = field(repr=False) + id: int + name: str + project: Project @staticmethod def list(api): @@ -108,5 +122,60 @@ class Activity(BaseAPI): if not none: raise NotFound() - def __repr__(self): - return f"Activity (id={self.id}, name={self.name}, project={self.project})" + +@dataclass +class Timesheet(BaseAPI): + api: KimaiAPI = field(repr=False) + activity: Activity + project: Project + begin: datetime + end: datetime + id: int = field(default=None) + description: str = field(default="") + tags: str = field(default="") + + @staticmethod + def list(api): + return [ + Timesheet( + api, + Activity.get_by_id(api, t["activity"], none=True), + Project.get_by_id(api, t["project"], none=True), + t["begin"], + t["end"], + t["id"], + t["description"], + t["tags"], + ) + for t in api.get( + 'timesheets', + ) + ] + + @staticmethod + def get_by_id(api, id, none=False): + t = api.get( + f'timesheets/{id}', + ) + return Timesheet( + api, + Activity.get_by_id(api, t["activity"], none=True), + Project.get_by_id(api, t["project"], none=True), + t["begin"], + t["end"], + t["id"], + t["description"], + t["tags"], + ) + + def upload(self): + return self.api.post('timesheets', { + "begin": self.begin.isoformat(), + "end": self.end.isoformat(), + "project": self.project.id, + "activity": self.activity.id, + "description": self.description, + # FIXME: support multiple users + # "user": self., + "tags": self.tags, + })