Compare commits

...

2 Commits

Author SHA1 Message Date
3wc cb909540fa Reformatting 🧹 2023-11-01 19:33:30 +00:00
3wc 05928b1244 Add (temporary?) scripts 2023-11-01 19:32:00 +00:00
6 changed files with 215 additions and 66 deletions

View File

@ -18,7 +18,7 @@ from .db import (
KimaiProject, KimaiProject,
KimaiActivity, KimaiActivity,
HamsterActivityKimaiMapping, HamsterActivityKimaiMapping,
HamsterFactKimaiImport HamsterFactKimaiImport,
) )
HAMSTER_DIR = Path.home() / ".local/share/hamster" HAMSTER_DIR = Path.home() / ".local/share/hamster"
@ -631,8 +631,15 @@ def db_():
@db_.command() @db_.command()
def init(): def init():
db.create_tables([KimaiCustomer, KimaiProject, KimaiActivity, db.create_tables(
HamsterActivityKimaiMapping, HamsterFactKimaiImport]) [
KimaiCustomer,
KimaiProject,
KimaiActivity,
HamsterActivityKimaiMapping,
HamsterFactKimaiImport,
]
)
@db_.command() @db_.command()
@ -665,10 +672,7 @@ def mapping2db(mapping_path=None, global_=False):
try: try:
kimai_activity = KimaiActivity.get(name=row[4], project_id=kimai_project.id) kimai_activity = KimaiActivity.get(name=row[4], project_id=kimai_project.id)
except KimaiActivity.DoesNotExist: except KimaiActivity.DoesNotExist:
kimai_activity = KimaiActivity.get( kimai_activity = KimaiActivity.get(name=row[4], project_id=None)
name=row[4],
project_id=None
)
HamsterActivityKimaiMapping.create( HamsterActivityKimaiMapping.create(
hamster_activity=hamster_activity, hamster_activity=hamster_activity,

View File

@ -38,9 +38,15 @@ class ListScreen(Screen):
with Vertical(): with Vertical():
yield DataTable() yield DataTable()
with Horizontal(id="filter"): with Horizontal(id="filter"):
yield Input(id="search", placeholder="Category/activity name contains text") yield Input(
yield Input(id="date", id="search", placeholder="Category/activity name contains text"
placeholder='After date, in {0} format'.format(datetime.now().strftime('%Y-%m-%d'))) )
yield Input(
id="date",
placeholder="After date, in {0} format".format(
datetime.now().strftime("%Y-%m-%d")
),
)
yield Footer() yield Footer()
def action_refresh(self) -> None: def action_refresh(self) -> None:
@ -69,7 +75,7 @@ class ListScreen(Screen):
self.table.focus() self.table.focus()
self._refresh() self._refresh()
@on(Input.Changed, '#filter Input') @on(Input.Changed, "#filter Input")
def filter(self, event): def filter(self, event):
self._refresh() self._refresh()
@ -81,7 +87,7 @@ class ActivityEditScreen(ModalScreen):
] ]
category_id = None category_id = None
category_name = '' category_name = ""
def _get_categories(self, input_state): def _get_categories(self, input_state):
categories = [DropdownItem(c.name, str(c.id)) for c in HamsterCategory.select()] categories = [DropdownItem(c.name, str(c.id)) for c in HamsterCategory.select()]
@ -96,15 +102,20 @@ class ActivityEditScreen(ModalScreen):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Vertical( yield Vertical(
Horizontal(Label("Category:"), Horizontal(
Label("Category:"),
AutoComplete( AutoComplete(
Input(placeholder="Type to search...", id="category", Input(
value=self.category_name), placeholder="Type to search...",
id="category",
value=self.category_name,
),
Dropdown(items=self._get_categories), Dropdown(items=self._get_categories),
), ),
), ),
Horizontal(Label("Activity:"), Input(value=self.activity_name, Horizontal(
id='activity')), Label("Activity:"), Input(value=self.activity_name, id="activity")
),
) )
@on(Input.Submitted, "#category") @on(Input.Submitted, "#category")
@ -129,7 +140,7 @@ class ActivityEditScreen(ModalScreen):
self.dismiss( self.dismiss(
{ {
"category": self.category_id, "category": self.category_id,
"activity": self.query_one('#activity').value, "activity": self.query_one("#activity").value,
} }
) )
@ -340,18 +351,18 @@ class ActivityListScreen(ListScreen):
.group_by(HamsterActivity) .group_by(HamsterActivity)
) )
filter_search = self.query_one('#filter #search').value filter_search = self.query_one("#filter #search").value
if filter_search is not None: if filter_search is not None:
activities = activities.where( activities = activities.where(
HamsterActivity.name.contains(filter_search) HamsterActivity.name.contains(filter_search)
| HamsterCategory.name.contains(filter_search) | HamsterCategory.name.contains(filter_search)
) )
filter_date = self.query_one('#filter #date').value filter_date = self.query_one("#filter #date").value
if filter_date is not None: if filter_date is not None:
try: try:
activities = activities.where( activities = activities.where(
HamsterFact.start_time > datetime.strptime(filter_date, '%Y-%m-%d') HamsterFact.start_time > datetime.strptime(filter_date, "%Y-%m-%d")
) )
except ValueError: except ValueError:
pass pass
@ -431,15 +442,14 @@ class ActivityListScreen(ListScreen):
def handle_edit(properties): def handle_edit(properties):
if properties is None: if properties is None:
return return
activity.name = properties['activity'] activity.name = properties["activity"]
activity.category_id = properties['category'] activity.category_id = properties["category"]
activity.save() activity.save()
self._refresh() self._refresh()
self.app.push_screen(ActivityEditScreen( self.app.push_screen(
category=category, ActivityEditScreen(category=category, activity=activity), handle_edit
activity=activity )
), handle_edit)
def action_mapping(self): def action_mapping(self):
selected_activity = ( selected_activity = (
@ -490,26 +500,24 @@ class CategoryListScreen(ListScreen):
categories = ( categories = (
HamsterCategory.select( HamsterCategory.select(
HamsterCategory, HamsterCategory,
fn.Count(HamsterActivity.id).alias("activities_count"), fn.Count(HamsterActivity.id).alias("activities_count"),
HamsterFact.start_time HamsterFact.start_time,
) )
.join(HamsterActivity, JOIN.LEFT_OUTER) .join(HamsterActivity, JOIN.LEFT_OUTER)
.join(HamsterFact, JOIN.LEFT_OUTER) .join(HamsterFact, JOIN.LEFT_OUTER)
.group_by(HamsterCategory) .group_by(HamsterCategory)
) )
filter_search = self.query_one('#filter #search').value filter_search = self.query_one("#filter #search").value
if filter_search is not None: if filter_search is not None:
categories = categories.where( categories = categories.where(HamsterCategory.name.contains(filter_search))
HamsterCategory.name.contains(filter_search)
)
filter_date = self.query_one('#filter #date').value filter_date = self.query_one("#filter #date").value
if filter_date is not None: if filter_date is not None:
try: try:
categories = categories.where( categories = categories.where(
HamsterFact.start_time > datetime.strptime(filter_date, '%Y-%m-%d') HamsterFact.start_time > datetime.strptime(filter_date, "%Y-%m-%d")
) )
except ValueError: except ValueError:
pass pass
@ -613,7 +621,7 @@ class KimaiProjectListScreen(ListScreen):
"id": project.id, "id": project.id,
"name": project.name, "name": project.name,
"customer_id": project.customer.id, "customer_id": project.customer.id,
"allow_global_activities": project.allow_global_activities "allow_global_activities": project.allow_global_activities,
} }
for project in projects for project in projects
] ]

View File

@ -1,12 +1,20 @@
from datetime import datetime from datetime import datetime
import logging import logging
from peewee import SqliteDatabase, Model, CharField, ForeignKeyField, DateTimeField, SmallIntegerField, BooleanField from peewee import (
SqliteDatabase,
Model,
CharField,
ForeignKeyField,
DateTimeField,
SmallIntegerField,
BooleanField,
)
from textual.logging import TextualHandler from textual.logging import TextualHandler
logger = logging.getLogger("peewee") # logger = logging.getLogger("peewee")
logger.addHandler(TextualHandler()) # logger.addHandler(TextualHandler())
logger.setLevel(logging.DEBUG) # logger.setLevel(logging.DEBUG)
db = SqliteDatabase(None) db = SqliteDatabase(None)

View File

@ -28,12 +28,14 @@ class KimaiAPI(object):
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(f"{self.KIMAI_API_URL}/{endpoint}", return requests.get(
params=params, headers=self.auth_headers).json() f"{self.KIMAI_API_URL}/{endpoint}", params=params, headers=self.auth_headers
).json()
def post(self, endpoint, data): def post(self, endpoint, data):
return requests.post(f"{self.KIMAI_API_URL}/{endpoint}", return requests.post(
json=data, headers=self.auth_headers) f"{self.KIMAI_API_URL}/{endpoint}", json=data, headers=self.auth_headers
)
class BaseAPI(object): class BaseAPI(object):
@ -72,9 +74,13 @@ class Project(BaseAPI):
@staticmethod @staticmethod
def list(api): def list(api):
return [ return [
Project(api, p["id"], p["name"], Customer.get_by_id(api, Project(
p["customer"]), api,
p["globalActivities"]) p["id"],
p["name"],
Customer.get_by_id(api, p["customer"]),
p["globalActivities"],
)
for p in api.projects_json for p in api.projects_json
] ]
@ -87,7 +93,7 @@ class Project(BaseAPI):
value["id"], value["id"],
value["name"], value["name"],
Customer.get_by_id(api, value["customer"]), Customer.get_by_id(api, value["customer"]),
value["globalActivities"] value["globalActivities"],
) )
if not none: if not none:
raise NotFound() raise NotFound()
@ -143,19 +149,19 @@ class Timesheet(BaseAPI):
Project.get_by_id(api, t["project"], none=True), Project.get_by_id(api, t["project"], none=True),
t["begin"], t["begin"],
t["end"], t["end"],
t["id"], t["id"],
t["description"], t["description"],
t["tags"], t["tags"],
) )
for t in api.get( for t in api.get(
'timesheets', "timesheets",
) )
] ]
@staticmethod @staticmethod
def get_by_id(api, id, none=False): def get_by_id(api, id, none=False):
t = api.get( t = api.get(
f'timesheets/{id}', f"timesheets/{id}",
) )
return Timesheet( return Timesheet(
api, api,
@ -163,19 +169,22 @@ class Timesheet(BaseAPI):
Project.get_by_id(api, t["project"], none=True), Project.get_by_id(api, t["project"], none=True),
t["begin"], t["begin"],
t["end"], t["end"],
t["id"], t["id"],
t["description"], t["description"],
t["tags"], t["tags"],
) )
def upload(self): def upload(self):
return self.api.post('timesheets', { return self.api.post(
"begin": self.begin.isoformat(), "timesheets",
"end": self.end.isoformat(), {
"project": self.project.id, "begin": self.begin.isoformat(),
"activity": self.activity.id, "end": self.end.isoformat(),
"description": self.description, "project": self.project.id,
# FIXME: support multiple users "activity": self.activity.id,
# "user": self., "description": self.description,
"tags": self.tags, # FIXME: support multiple users
}) # "user": self.,
"tags": self.tags,
},
)

32
scripts/apitest.py Normal file
View File

@ -0,0 +1,32 @@
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
from datetime import datetime, timedelta
import logging
from hamstertools.kimai import *
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
api = KimaiAPI()
# print(Timesheet.list(api))
t = Timesheet(
api,
activity=Activity.get_by_id(api, 613),
project=Project.get_by_id(api, 233),
begin=datetime.now() - timedelta(minutes=10),
end=datetime.now(),
)
# r = t.upload()
# from pdb import set_trace; set_trace()
print(Timesheet.get_by_id(api, 30683))

88
scripts/upload.py Normal file
View File

@ -0,0 +1,88 @@
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 *
from hamstertools.kimai import *
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 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
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"]}')