Preliminary clockify support
This commit is contained in:
		@ -9,6 +9,7 @@ import sys
 | 
			
		||||
import tomllib
 | 
			
		||||
 | 
			
		||||
import click
 | 
			
		||||
from clockify_sdk import Clockify
 | 
			
		||||
import requests
 | 
			
		||||
from peewee import fn, JOIN
 | 
			
		||||
from textual.logging import TextualHandler
 | 
			
		||||
@ -28,6 +29,9 @@ from .db import (
 | 
			
		||||
    HamsterActivityKimaiMapping,
 | 
			
		||||
    HamsterFactKimaiImport,
 | 
			
		||||
    HamsterFactTag,
 | 
			
		||||
    ClockifyProject,
 | 
			
		||||
    HamsterClockifyMapping,
 | 
			
		||||
    HamsterFactClockifyImport
 | 
			
		||||
)
 | 
			
		||||
from .kimaiapi import KimaiAPI, Timesheet
 | 
			
		||||
from .sync import sync
 | 
			
		||||
@ -43,11 +47,8 @@ db.init(HAMSTER_FILE)
 | 
			
		||||
@click.group(context_settings={"auto_envvar_prefix": "HAMSTERTOOL"})
 | 
			
		||||
@click.option("-d", "--debug", is_flag=True)
 | 
			
		||||
@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):
 | 
			
		||||
def cli(ctx, config, debug):
 | 
			
		||||
    file_config = {}
 | 
			
		||||
    if os.path.exists(config):
 | 
			
		||||
        with open(config, "rb") as f:
 | 
			
		||||
@ -64,11 +65,8 @@ def cli(ctx, config, debug, kimai_api_url=None, kimai_username=None, kimai_api_k
 | 
			
		||||
        requests_log.setLevel(logging.DEBUG)
 | 
			
		||||
        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"), 
 | 
			
		||||
    )
 | 
			
		||||
    ctx.ensure_object(dict)
 | 
			
		||||
    ctx.obj['config'] = file_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.group()
 | 
			
		||||
@ -334,7 +332,17 @@ def find_duplicates():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.group()
 | 
			
		||||
def kimai():
 | 
			
		||||
@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 kimai(ctx, kimai_api_url, kimai_username, kimai_api_key):
 | 
			
		||||
    file_config = ctx.obj['config']
 | 
			
		||||
    ctx.obj['kimai'] = 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"), 
 | 
			
		||||
    )
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -535,7 +543,7 @@ def _csv(
 | 
			
		||||
        timestamp = datetime.now().strftime("%F")
 | 
			
		||||
        output = f"kimai_{timestamp}.csv"
 | 
			
		||||
 | 
			
		||||
    if type(mapping_path) == tuple:
 | 
			
		||||
    if isinstance(mapping_path, tuple):
 | 
			
		||||
        mapping_files = []
 | 
			
		||||
        for mapping_path_item in mapping_path:
 | 
			
		||||
            mapping_file = _get_kimai_mapping_file(mapping_path_item, category_search)
 | 
			
		||||
@ -552,7 +560,7 @@ def _csv(
 | 
			
		||||
        for row in mapping_reader
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if type(mapping_path) == tuple:
 | 
			
		||||
    if isinstance(mapping_path, tuple):
 | 
			
		||||
        for mapping_file in mapping_files:
 | 
			
		||||
            mapping_file.close()
 | 
			
		||||
    else:
 | 
			
		||||
@ -561,25 +569,13 @@ def _csv(
 | 
			
		||||
    output_file = open(output, "w")
 | 
			
		||||
    output_writer = csv.writer(output_file)
 | 
			
		||||
 | 
			
		||||
    args = []
 | 
			
		||||
 | 
			
		||||
    facts = (
 | 
			
		||||
        HamsterFact.select(HamsterFact, HamsterActivity, HamsterCategory)
 | 
			
		||||
        .join(HamsterActivity)
 | 
			
		||||
        .join(HamsterCategory, JOIN.LEFT_OUTER)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    sql = """
 | 
			
		||||
    SELECT
 | 
			
		||||
        facts.id, facts.start_time, facts.end_time, facts.description,
 | 
			
		||||
        activities.id, activities.name,
 | 
			
		||||
        categories.name, categories.id
 | 
			
		||||
    FROM
 | 
			
		||||
        facts
 | 
			
		||||
    LEFT JOIN
 | 
			
		||||
        activities ON facts.activity_id = activities.id
 | 
			
		||||
    LEFT JOIN
 | 
			
		||||
        categories ON activities.category_id = categories.id """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if category_search is not None:
 | 
			
		||||
        facts = facts.where(HamsterCategory.name.contains(category_search))
 | 
			
		||||
@ -665,8 +661,9 @@ def _csv(
 | 
			
		||||
@click.argument("search")
 | 
			
		||||
@click.argument("after")
 | 
			
		||||
@click.argument("before")
 | 
			
		||||
def _import(search, after, before):
 | 
			
		||||
    api = KimaiAPI(username=KIMAI_USERNAME, api_key=KIMAI_API_KEY)
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def _import(ctx, search, after, before):
 | 
			
		||||
    api = ctx.obj
 | 
			
		||||
 | 
			
		||||
    SEARCH = "auto"
 | 
			
		||||
 | 
			
		||||
@ -760,11 +757,11 @@ def _import(search, after, before):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@kimai.group("db")
 | 
			
		||||
def db_():
 | 
			
		||||
def kimai_db():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@db_.command()
 | 
			
		||||
@kimai_db.command()
 | 
			
		||||
def init():
 | 
			
		||||
    db.create_tables(
 | 
			
		||||
        [
 | 
			
		||||
@ -778,20 +775,20 @@ def init():
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@db_.command()
 | 
			
		||||
@kimai_db.command()
 | 
			
		||||
def reset():
 | 
			
		||||
    HamsterActivityKimaiMapping.delete().execute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@db_.command("sync")
 | 
			
		||||
@click.pass_obj
 | 
			
		||||
def kimai_db_sync(api):
 | 
			
		||||
@kimai_db.command("sync")
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def kimai_db_sync(ctx):
 | 
			
		||||
    sync(
 | 
			
		||||
        api
 | 
			
		||||
        ctx.obj['kimai']
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@db_.command()
 | 
			
		||||
@kimai_db.command()
 | 
			
		||||
@click.option(
 | 
			
		||||
    "-g",
 | 
			
		||||
    "--global",
 | 
			
		||||
@ -828,14 +825,63 @@ def mapping2db(mapping_path=None, global_=False):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.group()
 | 
			
		||||
@click.option("--api-key", envvar="CLOCKIFY_API_KEY", help="Clockify API key")
 | 
			
		||||
@click.option("--workspace-id", envvar="CLOCKIFY_WORKSPACE_ID", help="Clockify workspace ID")
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def clockify(ctx, api_key, workspace_id):
 | 
			
		||||
    file_config = ctx.obj['config']
 | 
			
		||||
    ctx.obj['clockify'] = Clockify(
 | 
			
		||||
        api_key=api_key if api_key is not None else file_config.get("clockify_api_key"), 
 | 
			
		||||
        workspace_id=workspace_id if workspace_id is not None else file_config.get("clockify_workspace_id")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@clockify.group("db")
 | 
			
		||||
def clockify_db():
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@clockify_db.command("sync")
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def clockify_db_sync(ctx):
 | 
			
		||||
    from .clockify import sync_projects
 | 
			
		||||
    count = sync_projects(ctx.obj['clockify'], db)
 | 
			
		||||
    click.echo(f"Synced {count} Clockify projects")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@clockify_db.command("init")
 | 
			
		||||
def clockify_db_init():
 | 
			
		||||
    db.create_tables(
 | 
			
		||||
        [
 | 
			
		||||
            ClockifyProject,
 | 
			
		||||
            HamsterClockifyMapping,
 | 
			
		||||
            HamsterFactClockifyImport,
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@clockify.command("app")
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def clockify_app(ctx):
 | 
			
		||||
    from .app import HamsterToolsAppClockify
 | 
			
		||||
 | 
			
		||||
    app = HamsterToolsAppClockify(ctx.obj['clockify'])
 | 
			
		||||
    app.run()
 | 
			
		||||
 | 
			
		||||
@kimai.command("app")
 | 
			
		||||
@click.pass_context
 | 
			
		||||
def kimai_app(ctx):
 | 
			
		||||
    from .app import HamsterToolsAppKimai
 | 
			
		||||
 | 
			
		||||
    app = HamsterToolsAppKimai(ctx.obj['kimai'])
 | 
			
		||||
    app.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@click.pass_obj
 | 
			
		||||
def app(kimai_api):
 | 
			
		||||
def app():
 | 
			
		||||
    from .app import HamsterToolsApp
 | 
			
		||||
 | 
			
		||||
    app = HamsterToolsApp(
 | 
			
		||||
        kimai_api
 | 
			
		||||
    )
 | 
			
		||||
    app = HamsterToolsApp()
 | 
			
		||||
    app.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
from textual.app import App
 | 
			
		||||
 | 
			
		||||
from hamstertools import clockify
 | 
			
		||||
 | 
			
		||||
from .db import db
 | 
			
		||||
from .kimaiapi import KimaiAPI
 | 
			
		||||
 | 
			
		||||
from .screens.hamster import HamsterScreen
 | 
			
		||||
from .screens.kimai import KimaiScreen
 | 
			
		||||
from .screens.clockify.projects import ClockifyProjectScreen
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HamsterToolsApp(App):
 | 
			
		||||
@ -12,18 +14,19 @@ class HamsterToolsApp(App):
 | 
			
		||||
 | 
			
		||||
    BINDINGS = [
 | 
			
		||||
        ("h", "switch_mode('hamster')", "Hamster"),
 | 
			
		||||
        ("k", "switch_mode('kimai')", "Kimai"),
 | 
			
		||||
        ("q", "quit", "Quit"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, kimai_api=None):
 | 
			
		||||
    def __init__(self, kimai_api=None, clockify_api=None):
 | 
			
		||||
        self.add_mode("hamster", HamsterScreen())
 | 
			
		||||
        self.add_mode("kimai", KimaiScreen())
 | 
			
		||||
        # self.mode MODES = {
 | 
			
		||||
        #     "hamster": HamsterScreen(),
 | 
			
		||||
        #     "kimai": KimaiScreen(),
 | 
			
		||||
        # }
 | 
			
		||||
        self.api = kimai_api
 | 
			
		||||
 | 
			
		||||
        if kimai_api is not None:
 | 
			
		||||
            self.kimai_api = kimai_api
 | 
			
		||||
            self.add_mode("kimai", KimaiScreen())
 | 
			
		||||
 | 
			
		||||
        if clockify_api is not None:
 | 
			
		||||
            self.clockify_api = clockify_api
 | 
			
		||||
            self.add_mode("clockify", ClockifyProjectScreen())
 | 
			
		||||
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
@ -33,3 +36,26 @@ class HamsterToolsApp(App):
 | 
			
		||||
    async def action_quit(self) -> None:
 | 
			
		||||
        db.close()
 | 
			
		||||
        self.exit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HamsterToolsAppKimai(HamsterToolsApp):
 | 
			
		||||
    BINDINGS = HamsterToolsApp.BINDINGS + [
 | 
			
		||||
        ("k", "switch_mode('kimai')", "Kimai"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, kimai_api):
 | 
			
		||||
        self.kimai_api = kimai_api
 | 
			
		||||
        self.add_mode("kimai", KimaiScreen())
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HamsterToolsAppClockify(HamsterToolsApp):
 | 
			
		||||
    BINDINGS = HamsterToolsApp.BINDINGS + [
 | 
			
		||||
        ("c", "switch_mode('clockify_projects')", "Clockify"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __init__(self, clockify_api):
 | 
			
		||||
        self.clockify_api = clockify_api
 | 
			
		||||
        self.add_mode("clockify_projects", ClockifyProjectScreen())
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										65
									
								
								hamstertools/clockify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								hamstertools/clockify.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from clockify_sdk import Clockify
 | 
			
		||||
 | 
			
		||||
class NotFound(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass()
 | 
			
		||||
class Project:
 | 
			
		||||
    api: Clockify = field(repr=False)
 | 
			
		||||
    id: str
 | 
			
		||||
    name: str
 | 
			
		||||
    workspace_id: str
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def list(api):
 | 
			
		||||
        projects = api.projects.get_all()
 | 
			
		||||
        return [
 | 
			
		||||
            Project(api, p['id'], p['name'], p['workspaceId']) 
 | 
			
		||||
            for p in projects
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_by_id(api, project_id):
 | 
			
		||||
        return api.projects.get_by_id(project_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sync_projects(api, db):
 | 
			
		||||
    """Sync Clockify projects to local database"""
 | 
			
		||||
    from .db import ClockifyProject
 | 
			
		||||
    
 | 
			
		||||
    # Fetch and store all projects
 | 
			
		||||
    projects = Project.list(api)
 | 
			
		||||
    with db.atomic():
 | 
			
		||||
        # Delete all existing projects
 | 
			
		||||
        ClockifyProject.delete().execute()
 | 
			
		||||
 | 
			
		||||
        query = ClockifyProject.insert_many(
 | 
			
		||||
            [
 | 
			
		||||
                {
 | 
			
		||||
                    "clockify_id": project.id,
 | 
			
		||||
                    "name": project.name,
 | 
			
		||||
                    "workspace_id": project.workspace_id
 | 
			
		||||
                }
 | 
			
		||||
                for project in projects
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
        breakpoint()
 | 
			
		||||
        query.execute()
 | 
			
		||||
    return len(projects)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def export_fact(api, fact, project_id):
 | 
			
		||||
    """Export a Hamster fact to Clockify as a time entry"""
 | 
			
		||||
    start = fact.start_time.isoformat()
 | 
			
		||||
    end = fact.end_time.isoformat() if fact.end_time else datetime.now().isoformat()
 | 
			
		||||
    
 | 
			
		||||
    time_entry = api.create_time_entry(
 | 
			
		||||
        project_id=project_id,
 | 
			
		||||
        start=start,
 | 
			
		||||
        end=end,
 | 
			
		||||
        description=fact.description
 | 
			
		||||
    )
 | 
			
		||||
    return time_entry.id
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from peewee import (
 | 
			
		||||
    CompositeKey,
 | 
			
		||||
    SqliteDatabase,
 | 
			
		||||
    Model,
 | 
			
		||||
    CharField,
 | 
			
		||||
@ -122,3 +121,33 @@ class HamsterFactKimaiImport(Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        database = db
 | 
			
		||||
        table_name = "hamster_fact_kimai_imports"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClockifyProject(Model):
 | 
			
		||||
    clockify_id = CharField()
 | 
			
		||||
    name = CharField()
 | 
			
		||||
    workspace_id = CharField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        database = db
 | 
			
		||||
        table_name = "clockify_projects"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HamsterClockifyMapping(Model):
 | 
			
		||||
    hamster_activity = ForeignKeyField(HamsterActivity, backref="clockify_mappings")
 | 
			
		||||
    clockify_project = ForeignKeyField(ClockifyProject, backref="mappings")
 | 
			
		||||
    created_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        database = db
 | 
			
		||||
        table_name = "hamster_clockify_mappings"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HamsterFactClockifyImport(Model):
 | 
			
		||||
    hamster_fact = ForeignKeyField(HamsterFact, backref="clockify_imports")
 | 
			
		||||
    clockify_time_entry_id = CharField()
 | 
			
		||||
    exported_at = DateTimeField(default=datetime.now)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        database = db
 | 
			
		||||
        table_name = "hamster_fact_clockify_imports"
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import pdb
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								hamstertools/screens/clockify/mappings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								hamstertools/screens/clockify/mappings.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
from textual.app import ComposeResult
 | 
			
		||||
from textual.containers import VerticalScroll
 | 
			
		||||
from textual.screen import Screen
 | 
			
		||||
from textual.widgets import DataTable, Header, Footer
 | 
			
		||||
 | 
			
		||||
from hamstertools.db import HamsterActivity, ClockifyProject, HamsterClockifyMapping
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClockifyMappingScreen(Screen):
 | 
			
		||||
    """Screen for managing Clockify activity-project mappings"""
 | 
			
		||||
    
 | 
			
		||||
    BINDINGS = [("q", "quit", "Quit")]
 | 
			
		||||
    
 | 
			
		||||
    def compose(self) -> ComposeResult:
 | 
			
		||||
        yield Header()
 | 
			
		||||
        yield VerticalScroll(DataTable())
 | 
			
		||||
        yield Footer()
 | 
			
		||||
        
 | 
			
		||||
    def on_mount(self) -> None:
 | 
			
		||||
        table = self.query_one(DataTable)
 | 
			
		||||
        table.add_columns("Hamster Activity", "Clockify Project")
 | 
			
		||||
        
 | 
			
		||||
        # Query all mappings
 | 
			
		||||
        mappings = (HamsterClockifyMapping
 | 
			
		||||
                   .select()
 | 
			
		||||
                   .join(HamsterActivity)
 | 
			
		||||
                   .join(ClockifyProject))
 | 
			
		||||
        
 | 
			
		||||
        for mapping in mappings:
 | 
			
		||||
            activity = f"{mapping.hamster_activity.category.name} » {mapping.hamster_activity.name}"
 | 
			
		||||
            project = mapping.clockify_project.name
 | 
			
		||||
            table.add_row(activity, project)
 | 
			
		||||
							
								
								
									
										24
									
								
								hamstertools/screens/clockify/projects.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								hamstertools/screens/clockify/projects.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
from textual.app import ComposeResult
 | 
			
		||||
from textual.containers import VerticalScroll
 | 
			
		||||
from textual.screen import Screen
 | 
			
		||||
from textual.widgets import DataTable, Header, Footer
 | 
			
		||||
 | 
			
		||||
from hamstertools.db import ClockifyProject
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ClockifyProjectScreen(Screen):
 | 
			
		||||
    """Screen for listing Clockify projects"""
 | 
			
		||||
    
 | 
			
		||||
    BINDINGS = [("q", "quit", "Quit")]
 | 
			
		||||
    
 | 
			
		||||
    def compose(self) -> ComposeResult:
 | 
			
		||||
        yield Header()
 | 
			
		||||
        yield VerticalScroll(DataTable())
 | 
			
		||||
        yield Footer()
 | 
			
		||||
        
 | 
			
		||||
    def on_mount(self) -> None:
 | 
			
		||||
        table = self.query_one(DataTable)
 | 
			
		||||
        table.add_columns("ID", "Name", "Workspace ID")
 | 
			
		||||
        
 | 
			
		||||
        for project in ClockifyProject.select():
 | 
			
		||||
            table.add_row(project.clockify_id, project.name, project.workspace_id)
 | 
			
		||||
@ -14,7 +14,7 @@ from .tags import TagList
 | 
			
		||||
 | 
			
		||||
class HamsterScreen(Screen):
 | 
			
		||||
    BINDINGS = [
 | 
			
		||||
        ("c", "show_tab('categories')", "Categories"),
 | 
			
		||||
        ("g", "show_tab('categories')", "Categories"),
 | 
			
		||||
        ("a", "show_tab('activities')", "Activities"),
 | 
			
		||||
        ("t", "show_tab('tags')", "Tags"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,11 @@
 | 
			
		||||
 | 
			
		||||
from peewee import JOIN, fn
 | 
			
		||||
from textual import on
 | 
			
		||||
from textual.app import ComposeResult
 | 
			
		||||
from textual.binding import Binding
 | 
			
		||||
from textual.containers import Horizontal, Vertical
 | 
			
		||||
from textual.coordinate import Coordinate
 | 
			
		||||
from textual.events import DescendantBlur
 | 
			
		||||
from textual.screen import ModalScreen
 | 
			
		||||
from textual.widgets import DataTable, Input, Label
 | 
			
		||||
from textual_autocomplete import AutoComplete, Dropdown
 | 
			
		||||
from hamstertools.db import HamsterFactTag, HamsterTag
 | 
			
		||||
from hamstertools.screens.list import ListPane
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
from .kimaiapi import (
 | 
			
		||||
    KimaiAPI,
 | 
			
		||||
    Customer as KimaiAPICustomer,
 | 
			
		||||
    Project as KimaiAPIProject,
 | 
			
		||||
    Activity as KimaiAPIActivity,
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user