Compare commits

...

5 Commits

Author SHA1 Message Date
3wc 4b85921b3e Reasonably-working Kimai API data fetch'n'display 2023-10-27 22:00:03 +01:00
3wc a5eca9960e Kimai API caching and nicer architecture 2023-10-27 21:11:29 +01:00
3wc d88098dd30 Initial Kimai API 2023-10-27 21:02:17 +01:00
3wc 8908290c4d Simplify db ORM method names 2023-10-27 21:01:45 +01:00
3wc 23e90a4413 Reformat db too 2023-10-27 19:32:16 +01:00
4 changed files with 236 additions and 20 deletions

View File

@ -704,7 +704,6 @@ def _import(username, mapping_path=None, output=None, category_search=None, afte
@cli.command() @cli.command()
def app(): def app():
from .app import HamsterToolsApp from .app import HamsterToolsApp
#app = HamsterToolsApp(db_cursor=c, db_connection=conn)
app = HamsterToolsApp() app = HamsterToolsApp()
app.run() app.run()

View File

@ -5,7 +5,8 @@ from textual.containers import Horizontal, Vertical
from textual.coordinate import Coordinate from textual.coordinate import Coordinate
from textual.screen import Screen from textual.screen import Screen
from .db import DatabaseManager, Category, Activity from .db import DatabaseManager, Category, Activity, KimaiProject, KimaiCustomer
from .kimai import KimaiAPI, Customer as KimaiAPICustomer, Project as KimaiAPIProject, Activity as KimaiAPIActivity
class ListScreen(Screen): class ListScreen(Screen):
@ -55,7 +56,6 @@ class ListScreen(Screen):
class ActivitiesScreen(ListScreen): class ActivitiesScreen(ListScreen):
BINDINGS = [ BINDINGS = [
("q", "quit", "Quit"),
("s", "sort", "Sort"), ("s", "sort", "Sort"),
("r", "refresh", "Refresh"), ("r", "refresh", "Refresh"),
("/", "filter", "Search"), ("/", "filter", "Search"),
@ -68,7 +68,7 @@ class ActivitiesScreen(ListScreen):
self.table.clear() self.table.clear()
# List activities with the count of facts # List activities with the count of facts
activities = Activity.list_activities(self.db_manager, filter_query) activities = Activity.list(self.db_manager, filter_query)
self.table.add_rows( self.table.add_rows(
[ [
@ -133,7 +133,6 @@ class ActivitiesScreen(ListScreen):
class CategoriesScreen(ListScreen): class CategoriesScreen(ListScreen):
BINDINGS = [ BINDINGS = [
("q", "quit", "Quit"),
("s", "sort", "Sort"), ("s", "sort", "Sort"),
("r", "refresh", "Refresh"), ("r", "refresh", "Refresh"),
("/", "filter", "Search"), ("/", "filter", "Search"),
@ -144,7 +143,7 @@ class CategoriesScreen(ListScreen):
def _refresh(self, filter_query=None): def _refresh(self, filter_query=None):
self.table.clear() self.table.clear()
categories = Category.list_categories( categories = Category.list(
self.db_manager, filter_query=filter_query self.db_manager, filter_query=filter_query
) )
@ -182,11 +181,66 @@ class CategoriesScreen(ListScreen):
self.table.remove_row(row_key) self.table.remove_row(row_key)
class KimaiScreen(ListScreen):
BINDINGS = [
("s", "sort", "Sort"),
("r", "refresh", "Refresh"),
("g", "get", "Get data"),
("/", "filter", "Search"),
Binding(key="escape", action="cancelfilter", show=False),
]
def _refresh(self, filter_query=None):
self.table.clear()
projects = KimaiProject.list(
self.db_manager
)
self.table.add_rows(
[
[
project.customer_id,
project.customer_name,
project.id,
project.name,
]
for project in projects
]
)
self.table.sort(self.sort)
def action_get(self) -> None:
api = KimaiAPI()
customers = KimaiAPICustomer.list(api)
for customer in customers:
KimaiCustomer(self.db_manager, id=customer.id, name=customer.name).save()
projects = KimaiAPIProject.list(api)
for project in projects:
KimaiProject(self.db_manager, id=project.id, name=project.name,
customer_id=project.customer.id, customer_name="").save()
self._refresh()
def on_mount(self) -> None:
self.table = self.query_one(DataTable)
self.table.cursor_type = "row"
self.columns = self.table.add_columns("customer id", "customer",
"project id", "project")
# self.sort = (self.columns[1], self.columns[3])
self.sort = self.columns[1]
self._refresh()
class HamsterToolsApp(App): class HamsterToolsApp(App):
CSS_PATH = "app.tcss" CSS_PATH = "app.tcss"
BINDINGS = [ BINDINGS = [
("a", "switch_mode('activities')", "Activities"), ("a", "switch_mode('activities')", "Activities"),
("c", "switch_mode('categories')", "Categories"), ("c", "switch_mode('categories')", "Categories"),
("k", "switch_mode('kimai')", "Kimai"),
("q", "quit", "Quit"), ("q", "quit", "Quit"),
] ]
@ -195,7 +249,8 @@ class HamsterToolsApp(App):
self.MODES = { self.MODES = {
"categories": CategoriesScreen(self.db_manager), "categories": CategoriesScreen(self.db_manager),
"activities": ActivitiesScreen(self.db_manager) "activities": ActivitiesScreen(self.db_manager),
"kimai": KimaiScreen(self.db_manager)
} }
super().__init__() super().__init__()

View File

@ -1,5 +1,6 @@
import sqlite3 import sqlite3
class DatabaseManager: class DatabaseManager:
def __init__(self, database_name): def __init__(self, database_name):
self.conn = sqlite3.connect(database_name) self.conn = sqlite3.connect(database_name)
@ -14,6 +15,7 @@ class DatabaseManager:
def close(self): def close(self):
self.conn.close() self.conn.close()
class BaseORM: class BaseORM:
def __init__(self, db_manager, table_name, id, **kwargs): def __init__(self, db_manager, table_name, id, **kwargs):
self.db_manager = db_manager self.db_manager = db_manager
@ -31,11 +33,12 @@ class BaseORM:
class Category(BaseORM): class Category(BaseORM):
def __init__(self, db_manager, id, name, activity_count): def __init__(self, db_manager, id, name, activity_count):
super().__init__(db_manager, "categories", id, name=name, super().__init__(
activity_count=activity_count) db_manager, "categories", id, name=name, activity_count=activity_count
)
@staticmethod @staticmethod
def list_categories(db_manager, filter_query=None): def list(db_manager, filter_query=None):
cursor = db_manager.get_cursor() cursor = db_manager.get_cursor()
where = "" where = ""
if filter_query is not None: if filter_query is not None:
@ -65,7 +68,8 @@ class Category(BaseORM):
@staticmethod @staticmethod
def get_by_id(db_manager, category_id): def get_by_id(db_manager, category_id):
cursor = db_manager.get_cursor() cursor = db_manager.get_cursor()
cursor.execute(""" cursor.execute(
"""
SELECT SELECT
categories.id, categories.id,
categories.name, categories.name,
@ -78,7 +82,9 @@ class Category(BaseORM):
categories.id = activities.category_id categories.id = activities.category_id
WHERE WHERE
categories.id = ? categories.id = ?
""", (category_id,)) """,
(category_id,),
)
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -88,7 +94,9 @@ class Category(BaseORM):
class Activity(BaseORM): class Activity(BaseORM):
def __init__(self, db_manager, id, name, category_id, category_name, facts_count): def __init__(self, db_manager, id, name, category_id, category_name, facts_count):
super().__init__(db_manager, "activities", id, name=name, category_id=category_id) super().__init__(
db_manager, "activities", id, name=name, category_id=category_id
)
self.category_name = category_name self.category_name = category_name
self.facts_count = facts_count self.facts_count = facts_count
@ -97,19 +105,22 @@ class Activity(BaseORM):
print(f"moving from {self.id} to {to_activity.id}") print(f"moving from {self.id} to {to_activity.id}")
cursor.execute(""" cursor.execute(
"""
UPDATE UPDATE
facts facts
SET SET
activity_id = ? activity_id = ?
WHERE WHERE
activity_id = ? activity_id = ?
""", (to_activity.id, self.id)) """,
(to_activity.id, self.id),
)
self.conn.commit() self.conn.commit()
@staticmethod @staticmethod
def list_activities(db_manager, filter_query=None): def list(db_manager, filter_query=None):
cursor = db_manager.get_cursor() cursor = db_manager.get_cursor()
where = "" where = ""
if filter_query is not None: if filter_query is not None:
@ -142,12 +153,15 @@ class Activity(BaseORM):
cursor.execute(sql) cursor.execute(sql)
rows = cursor.fetchall() rows = cursor.fetchall()
return [Activity(db_manager, row[0], row[1], row[2], row[3], row[4]) for row in rows] return [
Activity(db_manager, row[0], row[1], row[2], row[3], row[4]) for row in rows
]
@staticmethod @staticmethod
def get_by_id(db_manager, activity_id): def get_by_id(db_manager, activity_id):
cursor = db_manager.get_cursor() cursor = db_manager.get_cursor()
cursor.execute(""" cursor.execute(
"""
SELECT SELECT
activities.id, activities.id,
activities.name, activities.name,
@ -166,7 +180,9 @@ class Activity(BaseORM):
activities.id = facts.activity_id activities.id = facts.activity_id
WHERE WHERE
activities.id = ? activities.id = ?
""", (activity_id,)) """,
(activity_id,),
)
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -179,8 +195,74 @@ class Fact(BaseORM):
super().__init__(db_manager, "facts", id, activity_id=activity_id) super().__init__(db_manager, "facts", id, activity_id=activity_id)
@staticmethod @staticmethod
def list_facts(db_manager): def list(db_manager):
cursor = db_manager.get_cursor() cursor = db_manager.get_cursor()
cursor.execute("SELECT * FROM facts") cursor.execute("SELECT * FROM facts")
rows = cursor.fetchall() rows = cursor.fetchall()
return [Fact(db_manager, row[0], row[1]) for row in rows] return [Fact(db_manager, row[0], row[1]) for row in rows]
class KimaiCustomer(BaseORM):
def __init__(self, db_manager, id, name):
super().__init__(db_manager, "kimai_customers", id, name=name)
def save(self):
cursor = self.db_manager.get_cursor()
cursor.execute("SELECT id FROM kimai_customers WHERE id = ?", (self.id,))
row = cursor.fetchone()
if row:
cursor.execute("""
UPDATE kimai_customers SET name = ? WHERE id = ?
""", (self.name, self.id))
else:
cursor.execute("""
INSERT INTO kimai_customers (id, name) VALUES (?, ?)
""", (self.id, self.name))
self.db_manager.get_conn().commit()
class KimaiProject(BaseORM):
def __init__(self, db_manager, id, name, customer_id, customer_name):
super().__init__(db_manager, "kimai_projects", id, name=name,
customer_id=customer_id, customer_name=customer_name)
def save(self):
cursor = self.db_manager.get_cursor()
cursor.execute("SELECT id FROM kimai_projects WHERE id = ?", (self.id,))
row = cursor.fetchone()
if row:
cursor.execute("""
UPDATE kimai_projects SET name = ?, customer_id = ? WHERE id = ?
""", (self.name, self.customer_id, self.id))
else:
cursor.execute("""
INSERT INTO kimai_projects (id, name, customer_id) VALUES (?, ?, ?)
""", (self.id, self.name, self.customer_id))
self.db_manager.get_conn().commit()
@staticmethod
def list(db_manager):
cursor = db_manager.get_cursor()
cursor.execute("""
SELECT
kimai_projects.id,
COALESCE(kimai_projects.name, ""),
kimai_customers.id,
COALESCE(kimai_customers.name, "")
FROM
kimai_projects
LEFT JOIN
kimai_customers
ON
kimai_customers.id = kimai_projects.customer_id
GROUP BY
kimai_customers.id
""")
rows = cursor.fetchall()
return [KimaiProject(db_manager, row[0], row[1], row[2], row[3]) for row in rows]
class KimaiACtivity(BaseORM):
pass

80
hamstertools/kimai.py Normal file
View File

@ -0,0 +1,80 @@
import requests
import requests_cache
import os
class NotFound(Exception):
pass
class KimaiAPI(object):
# temporary hardcoded config
KIMAI_API_URL = 'https://kimai.autonomic.zone/api'
KIMAI_USERNAME = '3wordchant'
KIMAI_API_KEY = os.environ['KIMAI_API_KEY']
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()
class BaseAPI(object):
def __init__(self, api, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class Customer(BaseAPI):
def __init__(self, api, id, name):
super().__init__(api, id=id, name=name)
@staticmethod
def list(api):
return [
Customer(api, c['id'], c['name']) for c in api.customers_json
]
@staticmethod
def get_by_id(api, id):
for value in api.customers_json:
if value['id'] == id:
return Customer(api, value['id'], value['name'])
raise NotFound()
def __repr__(self):
return f'Customer (id={self.id}, name={self.name})'
class Project(BaseAPI):
def __init__(self, api, id, name, customer):
super().__init__(api, id=id, name=name, customer=customer)
@staticmethod
def list(api):
return [
Project(api, p['id'], p['name'], Customer.get_by_id(api, p['customer'])) for p in api.projects_json
]
@staticmethod
def get_by_id(api, id):
for value in api.projects_json:
if value['id'] == id:
return Project(api, value['id'], value['name'],
Customer.get_by_id(api, value['customer']))
raise NotFound()
def __repr__(self):
return f'Project (id={self.id}, name={self.name}, customer={self.customer})'
class Activity():
pass