Compare commits
3 Commits
a01652f301
...
26b8b5f334
Author | SHA1 | Date |
---|---|---|
3wc | 26b8b5f334 | |
3wc | b62eb5cb22 | |
3wc | 1145f5e806 |
|
@ -22,6 +22,7 @@ from .db import (
|
||||||
HamsterActivityKimaiMapping,
|
HamsterActivityKimaiMapping,
|
||||||
HamsterFactKimaiImport,
|
HamsterFactKimaiImport,
|
||||||
)
|
)
|
||||||
|
from .kimaiapi import KimaiAPI, Timesheet
|
||||||
from .sync import sync
|
from .sync import sync
|
||||||
|
|
||||||
HAMSTER_DIR = Path.home() / ".local/share/hamster"
|
HAMSTER_DIR = Path.home() / ".local/share/hamster"
|
||||||
|
@ -481,7 +482,7 @@ def check(username, api_key, just_errors, ignore_activities, mapping_path=None):
|
||||||
found_activities.append(activity_str)
|
found_activities.append(activity_str)
|
||||||
|
|
||||||
|
|
||||||
@kimai.command("import")
|
@kimai.command("csv")
|
||||||
@click.option(
|
@click.option(
|
||||||
"--mapping-path",
|
"--mapping-path",
|
||||||
help="Mapping file (default ~/.local/share/hamster/mapping.kimai.csv)",
|
help="Mapping file (default ~/.local/share/hamster/mapping.kimai.csv)",
|
||||||
|
@ -492,7 +493,7 @@ def check(username, api_key, just_errors, ignore_activities, mapping_path=None):
|
||||||
@click.option("--after", help="Only show time entries after this date")
|
@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.option("--show-missing", help="Just report on the missing entries", is_flag=True)
|
||||||
@click.argument("username")
|
@click.argument("username")
|
||||||
def _import(
|
def _csv(
|
||||||
username,
|
username,
|
||||||
mapping_path=None,
|
mapping_path=None,
|
||||||
output=None,
|
output=None,
|
||||||
|
@ -637,6 +638,103 @@ def _import(
|
||||||
output_file.close()
|
output_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
@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(
|
||||||
|
f"fact {f.id}: project @{mappings[0].kimai_project.id} {mappings[0].kimai_project.name} does not allow global activity {mappings[0].kimai_activity}",
|
||||||
|
fg="red",
|
||||||
|
)
|
||||||
|
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)",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
description=f.description
|
||||||
|
if f.description != ""
|
||||||
|
else mapping.kimai_description,
|
||||||
|
# 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
|
||||||
|
|
||||||
|
set_trace()
|
||||||
|
|
||||||
|
else:
|
||||||
|
HamsterFactKimaiImport.create(hamster_fact=f, kimai_id=r["id"]).save()
|
||||||
|
print(f'Created Kimai timesheet {r["id"]}')
|
||||||
|
|
||||||
|
|
||||||
@kimai.group("db")
|
@kimai.group("db")
|
||||||
def db_():
|
def db_():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
from .db import db
|
from .db import db
|
||||||
|
from .kimaiapi import KimaiAPI
|
||||||
|
|
||||||
from .screens.hamster import HamsterScreen
|
from .screens.hamster import HamsterScreen
|
||||||
from .screens.kimai import KimaiScreen
|
from .screens.kimai import KimaiScreen
|
||||||
|
@ -8,12 +9,21 @@ from .screens.kimai import KimaiScreen
|
||||||
|
|
||||||
class HamsterToolsApp(App):
|
class HamsterToolsApp(App):
|
||||||
CSS_PATH = "app.tcss"
|
CSS_PATH = "app.tcss"
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("h", "switch_mode('hamster')", "Hamster"),
|
("h", "switch_mode('hamster')", "Hamster"),
|
||||||
("k", "switch_mode('kimai')", "Kimai"),
|
("k", "switch_mode('kimai')", "Kimai"),
|
||||||
("q", "quit", "Quit"),
|
("q", "quit", "Quit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
api_ = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api(self) -> KimaiAPI:
|
||||||
|
if self.api_ is None:
|
||||||
|
self.api_ = KimaiAPI()
|
||||||
|
return self.api_
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.MODES = {
|
self.MODES = {
|
||||||
"hamster": HamsterScreen(),
|
"hamster": HamsterScreen(),
|
||||||
|
|
|
@ -21,16 +21,22 @@ class KimaiAPI(object):
|
||||||
auth_headers = {"X-AUTH-USER": KIMAI_USERNAME, "X-AUTH-TOKEN": KIMAI_API_KEY}
|
auth_headers = {"X-AUTH-USER": KIMAI_USERNAME, "X-AUTH-TOKEN": KIMAI_API_KEY}
|
||||||
|
|
||||||
def __init__(self):
|
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})
|
||||||
self.activities_json = self.get("activities", {"visible": 3})
|
self.activities_json = self.get("activities", {"visible": 3})
|
||||||
self.user_json = self.get("users/me")
|
self.user_json = self.get("users/me")
|
||||||
|
|
||||||
def get(self, endpoint, params=None):
|
def get(self, endpoint, params=None):
|
||||||
return 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:
|
||||||
|
if result["code"] != 200:
|
||||||
|
raise NotFound()
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
def post(self, endpoint, data):
|
def post(self, endpoint, data):
|
||||||
return requests.post(
|
return requests.post(
|
||||||
|
@ -133,7 +139,7 @@ class Activity(BaseAPI):
|
||||||
api,
|
api,
|
||||||
a["id"],
|
a["id"],
|
||||||
a["name"],
|
a["name"],
|
||||||
Project.get_by_id(api, a["project"]),
|
Project.get_by_id(api, a["project"], none),
|
||||||
a["visible"],
|
a["visible"],
|
||||||
)
|
)
|
||||||
if not none:
|
if not none:
|
||||||
|
@ -169,6 +175,23 @@ class Timesheet(BaseAPI):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_by(api, **kwargs):
|
||||||
|
kwargs["size"] = 10000
|
||||||
|
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", params=kwargs)
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_by_id(api, id, none=False):
|
def get_by_id(api, id, none=False):
|
||||||
t = api.get(
|
t = api.get(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
|
from textual.coordinate import Coordinate
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import DataTable, TabbedContent, TabPane, Header, Footer
|
from textual.widgets import DataTable, TabbedContent, TabPane, Header, Footer
|
||||||
|
@ -12,6 +13,7 @@ from ..db import (
|
||||||
KimaiCustomer,
|
KimaiCustomer,
|
||||||
KimaiActivity,
|
KimaiActivity,
|
||||||
)
|
)
|
||||||
|
from ..kimaiapi import Timesheet as KimaiAPITimesheet
|
||||||
|
|
||||||
from .list import ListPane
|
from .list import ListPane
|
||||||
|
|
||||||
|
@ -89,6 +91,7 @@ class KimaiActivityList(ListPane):
|
||||||
("s", "sort", "Sort"),
|
("s", "sort", "Sort"),
|
||||||
("r", "refresh", "Refresh"),
|
("r", "refresh", "Refresh"),
|
||||||
("g", "get", "Get data"),
|
("g", "get", "Get data"),
|
||||||
|
("#", "count", "Count"),
|
||||||
("/", "filter", "Search"),
|
("/", "filter", "Search"),
|
||||||
Binding(key="escape", action="cancelfilter", show=False),
|
Binding(key="escape", action="cancelfilter", show=False),
|
||||||
]
|
]
|
||||||
|
@ -122,6 +125,7 @@ class KimaiActivityList(ListPane):
|
||||||
activity.id,
|
activity.id,
|
||||||
truncate(activity.name, 40),
|
truncate(activity.name, 40),
|
||||||
activity.visible,
|
activity.visible,
|
||||||
|
"?",
|
||||||
]
|
]
|
||||||
for activity in activities
|
for activity in activities
|
||||||
]
|
]
|
||||||
|
@ -144,10 +148,20 @@ class KimaiActivityList(ListPane):
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"visible",
|
"visible",
|
||||||
|
"times",
|
||||||
)
|
)
|
||||||
self.sort = (self.columns[1], self.columns[3])
|
self.sort = (self.columns[1], self.columns[3])
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
||||||
|
def action_count(self) -> None:
|
||||||
|
row_idx: int = self.table.cursor_row
|
||||||
|
row_cells = self.table.get_row_at(row_idx)
|
||||||
|
|
||||||
|
activity_id = row_cells[2]
|
||||||
|
count = len(KimaiAPITimesheet.list_by(self.app.api, activity=activity_id))
|
||||||
|
|
||||||
|
self.table.update_cell_at(Coordinate(row_idx, 5), count)
|
||||||
|
|
||||||
|
|
||||||
class KimaiScreen(Screen):
|
class KimaiScreen(Screen):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from peewee import JOIN
|
|
||||||
|
|
||||||
from hamstertools.db import (
|
|
||||||
HamsterCategory,
|
|
||||||
HamsterActivity,
|
|
||||||
HamsterFact,
|
|
||||||
HamsterFactKimaiImport,
|
|
||||||
)
|
|
||||||
from hamstertools.kimaiapi import KimaiAPI, Timesheet, Project, Activity
|
|
||||||
|
|
||||||
api = KimaiAPI()
|
|
||||||
|
|
||||||
DATE_FROM = "2023-10-01"
|
|
||||||
DATE_TO = "2023-11-01"
|
|
||||||
SEARCH = "auto"
|
|
||||||
|
|
||||||
facts = (
|
|
||||||
HamsterFact.select(HamsterFact, HamsterActivity, HamsterCategory)
|
|
||||||
.join(HamsterActivity, JOIN.LEFT_OUTER)
|
|
||||||
.join(HamsterCategory, JOIN.LEFT_OUTER)
|
|
||||||
.where(
|
|
||||||
(HamsterFact.start_time > datetime.strptime(DATE_FROM, "%Y-%m-%d"))
|
|
||||||
& (HamsterFact.start_time < datetime.strptime(DATE_TO, "%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
|
|
||||||
):
|
|
||||||
print(
|
|
||||||
f"fact {f.id}: project @{mappings[0].kimai_project.id} {mappings[0].kimai_project.name} does not allow global activity {mappings[0].kimai_activity}"
|
|
||||||
)
|
|
||||||
has_errors = True
|
|
||||||
continue
|
|
||||||
if f.imports.count() > 0:
|
|
||||||
print(
|
|
||||||
f"fact {f.id}: activity @{f.activity.id} {f.activity.name} was already imported {f.imports.count()} time(s)"
|
|
||||||
)
|
|
||||||
has_errors = True
|
|
||||||
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,
|
|
||||||
description=f.description if f.description != "" else mapping.kimai_description,
|
|
||||||
# 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
|
|
||||||
|
|
||||||
set_trace()
|
|
||||||
|
|
||||||
else:
|
|
||||||
HamsterFactKimaiImport.create(hamster_fact=f, kimai_id=r["id"]).save()
|
|
||||||
print(f'Created Kimai timesheet {r["id"]}')
|
|
Loading…
Reference in New Issue