Compare commits

...

3 Commits

Author SHA1 Message Date
3wc
db6d7ac640 Merge branch 'api-conf' 2025-08-06 11:09:13 +01:00
3wc
69d471a859 Working config loading from TOML, env var, cli args 2025-08-06 11:07:45 +01:00
3wc
4fcf972e17 WIP: config file support 2024-12-06 21:30:34 -05:00
6 changed files with 66 additions and 51 deletions

View File

@ -1,16 +1,21 @@
#!/usr/bin/env python3.7 #!/usr/bin/env python3.7
import os
import csv import csv
import logging import logging
from datetime import datetime from datetime import datetime
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
import sys import sys
import tomllib
import click import click
import requests import requests
from peewee import fn, JOIN from peewee import fn, JOIN
from textual.logging import TextualHandler from textual.logging import TextualHandler
from xdg.BaseDirectory import xdg_config_home
from .db import ( from .db import (
KimaiTag, KimaiTag,
db, db,
@ -22,19 +27,31 @@ from .db import (
KimaiActivity, KimaiActivity,
HamsterActivityKimaiMapping, HamsterActivityKimaiMapping,
HamsterFactKimaiImport, HamsterFactKimaiImport,
HamsterFactTag,
) )
from .kimaiapi import KimaiAPI, Timesheet from .kimaiapi import KimaiAPI, Timesheet
from .sync import sync from .sync import sync
CONFIG_FILE = Path(xdg_config_home) / "hamstertools.toml"
HAMSTER_DIR = Path.home() / ".local/share/hamster" HAMSTER_DIR = Path.home() / ".local/share/hamster"
HAMSTER_FILE = HAMSTER_DIR / "hamster.db" HAMSTER_FILE = HAMSTER_DIR / "hamster.db"
db.init(HAMSTER_FILE) db.init(HAMSTER_FILE)
@click.group() @click.group(context_settings={"auto_envvar_prefix": "HAMSTERTOOL"})
@click.option("-d", "--debug", is_flag=True) @click.option("-d", "--debug", is_flag=True)
def cli(debug): @click.option("--config", default=CONFIG_FILE, type=click.Path())
@click.option("--kimai-api-url", envvar="KIMAI_API_URL")
@click.option("--kimai-username", envvar="KIMAI_USERNAME")
@click.option("--kimai-api-key", envvar="KIMAI_API_KEY")
@click.pass_context
def cli(ctx, config, debug, kimai_api_url=None, kimai_username=None, kimai_api_key=None):
file_config = {}
if os.path.exists(config):
with open(config, "rb") as f:
file_config = tomllib.load(f)
if debug: if debug:
logging.basicConfig() logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@ -47,6 +64,12 @@ def cli(debug):
requests_log.setLevel(logging.DEBUG) requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True requests_log.propagate = True
ctx.obj = KimaiAPI(
kimai_username if kimai_username is not None else file_config.get("kimai_username"),
kimai_api_key if kimai_api_key is not None else file_config.get("kimai_api_key"),
kimai_api_url if kimai_api_url is not None else file_config.get("kimai_api_url"),
)
@cli.group() @cli.group()
def categories(): def categories():
@ -355,7 +378,6 @@ def _get_kimai_mapping_file(path, category_search=None):
multiple=True, multiple=True,
) )
@click.argument("username") @click.argument("username")
@click.argument("api_key", envvar="KIMAI_API_KEY")
@click.option("--just-errors", "just_errors", is_flag=True, help="Only display errors") @click.option("--just-errors", "just_errors", is_flag=True, help="Only display errors")
@click.option("--ignore-activities", is_flag=True, help="Ignore missing activities") @click.option("--ignore-activities", is_flag=True, help="Ignore missing activities")
def check(username, api_key, just_errors, ignore_activities, mapping_path=None): def check(username, api_key, just_errors, ignore_activities, mapping_path=None):
@ -644,7 +666,7 @@ def _csv(
@click.argument("after") @click.argument("after")
@click.argument("before") @click.argument("before")
def _import(search, after, before): def _import(search, after, before):
api = KimaiAPI() api = KimaiAPI(username=KIMAI_USERNAME, api_key=KIMAI_API_KEY)
SEARCH = "auto" SEARCH = "auto"
@ -652,6 +674,8 @@ def _import(search, after, before):
HamsterFact.select(HamsterFact, HamsterActivity, HamsterCategory) HamsterFact.select(HamsterFact, HamsterActivity, HamsterCategory)
.join(HamsterActivity, JOIN.LEFT_OUTER) .join(HamsterActivity, JOIN.LEFT_OUTER)
.join(HamsterCategory, JOIN.LEFT_OUTER) .join(HamsterCategory, JOIN.LEFT_OUTER)
.switch(HamsterFact)
.join(HamsterFactTag, JOIN.LEFT_OUTER)
.where( .where(
(HamsterFact.start_time > datetime.strptime(after, "%Y-%m-%d")) (HamsterFact.start_time > datetime.strptime(after, "%Y-%m-%d"))
& (HamsterFact.start_time < datetime.strptime(before, "%Y-%m-%d")) & (HamsterFact.start_time < datetime.strptime(before, "%Y-%m-%d"))
@ -729,9 +753,6 @@ def _import(search, after, before):
if r.get("code", 200) != 200: if r.get("code", 200) != 200:
print(r) print(r)
print(f"{f.id} ({f.activity.category.name} » {f.activity.name})") print(f"{f.id} ({f.activity.category.name} » {f.activity.name})")
from pdb import set_trace
set_trace()
else: else:
HamsterFactKimaiImport.create(hamster_fact=f, kimai_id=r["id"]).save() HamsterFactKimaiImport.create(hamster_fact=f, kimai_id=r["id"]).save()
@ -763,8 +784,11 @@ def reset():
@db_.command("sync") @db_.command("sync")
def kimai_db_sync(): @click.pass_obj
sync() def kimai_db_sync(api):
sync(
api
)
@db_.command() @db_.command()
@ -805,10 +829,13 @@ def mapping2db(mapping_path=None, global_=False):
@cli.command() @cli.command()
def app(): @click.pass_obj
def app(kimai_api):
from .app import HamsterToolsApp from .app import HamsterToolsApp
app = HamsterToolsApp() app = HamsterToolsApp(
kimai_api
)
app.run() app.run()

View File

@ -16,25 +16,20 @@ class HamsterToolsApp(App):
("q", "quit", "Quit"), ("q", "quit", "Quit"),
] ]
api_ = None def __init__(self, kimai_api=None):
self.add_mode("hamster", HamsterScreen())
@property self.add_mode("kimai", KimaiScreen())
def api(self) -> KimaiAPI: # self.mode MODES = {
if self.api_ is None: # "hamster": HamsterScreen(),
self.api_ = KimaiAPI() # "kimai": KimaiScreen(),
return self.api_ # }
self.api = kimai_api
def __init__(self):
self.MODES = {
"hamster": HamsterScreen(),
"kimai": KimaiScreen(),
}
super().__init__() super().__init__()
def on_mount(self) -> None: def on_mount(self) -> None:
self.switch_mode("hamster") self.switch_mode("hamster")
def action_quit(self) -> None: async def action_quit(self) -> None:
db.close() db.close()
self.exit() self.exit()

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from peewee import ( from peewee import (
CompositeKey,
SqliteDatabase, SqliteDatabase,
Model, Model,
CharField, CharField,
@ -32,6 +33,14 @@ class HamsterActivity(Model):
table_name = "activities" table_name = "activities"
class HamsterTag(Model):
name = CharField()
class Meta:
database = db
table_name = "tags"
class HamsterFact(Model): class HamsterFact(Model):
activity = ForeignKeyField(HamsterActivity, backref="facts") activity = ForeignKeyField(HamsterActivity, backref="facts")
start_time = DateTimeField() start_time = DateTimeField()
@ -43,15 +52,6 @@ class HamsterFact(Model):
table_name = "facts" table_name = "facts"
class HamsterTag(Model):
name = CharField()
class Meta:
database = db
table_name = "tags"
class HamsterFactTag(Model): class HamsterFactTag(Model):
fact = ForeignKeyField(HamsterFact, backref="tags") fact = ForeignKeyField(HamsterFact, backref="tags")
tag = ForeignKeyField(HamsterTag, backref="facts") tag = ForeignKeyField(HamsterTag, backref="facts")

View File

@ -1,8 +1,6 @@
from datetime import datetime from datetime import datetime
import pdb import pdb
import requests import requests
import requests_cache
import os
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -12,16 +10,12 @@ class NotFound(Exception):
class KimaiAPI(object): class KimaiAPI(object):
# temporary hardcoded config def __init__(self, username=None, api_key=None, api_url=None):
KIMAI_API_URL = "https://kimai.autonomic.zone/api" self.auth_headers = {"X-AUTH-USER": username, "X-AUTH-TOKEN": api_key}
# KIMAI_API_URL = "https://kimaitest.autonomic.zone/api" self.kimai_api_url = api_url
# NOTE: Uncomment the following line to enable requests_cache, which can make development a *lot* faster
KIMAI_USERNAME = "3wordchant" # TODO: Add a config option or something for this
KIMAI_API_KEY = os.environ["KIMAI_API_KEY"] # import requests_cache
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) # requests_cache.install_cache("kimai", backend="sqlite", expire_after=1800)
self.customers_json = self.get("customers", {"visible": 3}) self.customers_json = self.get("customers", {"visible": 3})
self.projects_json = self.get("projects", {"visible": 3, "ignoreDates": 1}) self.projects_json = self.get("projects", {"visible": 3, "ignoreDates": 1})
@ -31,7 +25,7 @@ class KimaiAPI(object):
def get(self, endpoint, params=None): def get(self, endpoint, params=None):
result = requests.get( result = requests.get(
f"{self.KIMAI_API_URL}/{endpoint}", params=params, headers=self.auth_headers f"{self.kimai_api_url}/{endpoint}", params=params, headers=self.auth_headers
).json() ).json()
try: try:
if result["code"] != 200: if result["code"] != 200:
@ -42,7 +36,7 @@ class KimaiAPI(object):
def post(self, endpoint, data): def post(self, endpoint, data):
return requests.post( return requests.post(
f"{self.KIMAI_API_URL}/{endpoint}", json=data, headers=self.auth_headers f"{self.kimai_api_url}/{endpoint}", json=data, headers=self.auth_headers
) )

View File

@ -14,9 +14,7 @@ from .db import (
) )
def sync() -> None: def sync(api) -> None:
api = KimaiAPI()
KimaiCustomer.delete().execute() KimaiCustomer.delete().execute()
KimaiProject.delete().execute() KimaiProject.delete().execute()
KimaiActivity.delete().execute() KimaiActivity.delete().execute()

View File

@ -4,3 +4,4 @@ requests-cache==1.1.1
textual==0.44.1 textual==0.44.1
textual-autocomplete==2.1.0b0 textual-autocomplete==2.1.0b0
textual-dev==1.2.1 textual-dev==1.2.1
xdg==6.0.0