Added Application HolidayViewer
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
from core.utils import get_user_id
|
||||
|
||||
|
||||
class BaseComponent:
|
||||
"""
|
||||
Base class for all components that need to have a session and an id
|
||||
@@ -13,6 +16,9 @@ class BaseComponent:
|
||||
def get_session(self):
|
||||
return self._session
|
||||
|
||||
def get_user_id(self):
|
||||
return get_user_id(self._session)
|
||||
|
||||
def __repr__(self):
|
||||
return self._id
|
||||
|
||||
|
||||
51
src/components/admin/AdminApp.py
Normal file
51
src/components/admin/AdminApp.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from components.admin.constants import Routes
|
||||
from core.instance_manager import debug_session, InstanceManager
|
||||
|
||||
admin_app, rt = fast_app()
|
||||
|
||||
logger = logging.getLogger("admin")
|
||||
|
||||
|
||||
@rt(Routes.AiBuddy)
|
||||
def get(session, _id: str, boundaries: str):
|
||||
logger.debug(f"Entering {Routes.AiBuddy} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_ai_buddy_form(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.AiBuddy)
|
||||
def post(session, _id: str, args: dict):
|
||||
logger.debug(f"Entering {Routes.AiBuddy} - POST with args {debug_session(session)}, {_id=}, {args=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.update_ai_buddy_settings(args)
|
||||
|
||||
@rt(Routes.AiBuddyCancel)
|
||||
def post(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.AiBuddyCancel} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.cancel_ai_buddy_settings()
|
||||
|
||||
|
||||
@rt(Routes.ImportHolidays)
|
||||
def get(session, _id: str, boundaries: str):
|
||||
logger.debug(f"Entering {Routes.ImportHolidays} - GET with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_import_holidays_form(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.PasteHolidays)
|
||||
def post(session, _id: str, content: str):
|
||||
logger.debug(f"Entering {Routes.PasteHolidays} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.on_paste(content)
|
||||
|
||||
@rt(Routes.ImportHolidays)
|
||||
def post(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.ImportHolidays} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.import_holidays()
|
||||
0
src/components/admin/__init__.py
Normal file
0
src/components/admin/__init__.py
Normal file
39
src/components/admin/admin_db_manager.py
Normal file
39
src/components/admin/admin_db_manager.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ai.mcp_client import InProcessMCPClientCustomTools
|
||||
from ai.mcp_tools import MCPServerTools
|
||||
from auth.auth_manager import AuthManager
|
||||
from components.admin.constants import ADMIN_SETTINGS_ENTRY
|
||||
from config import OLLAMA_HOST
|
||||
from core.settings_management import SettingsManager, NestedSettingsManager
|
||||
from core.utils import split_host_port
|
||||
|
||||
|
||||
@dataclass
|
||||
class AiBuddySettingsEntry:
|
||||
ollama_ip: str = ""
|
||||
ollama_port: int = 11434
|
||||
ollama_model: str = "mistral"
|
||||
llm_mode: str = InProcessMCPClientCustomTools.ID
|
||||
available_tools: list = field(default_factory=MCPServerTools.list_tools)
|
||||
|
||||
def __post_init__(self):
|
||||
host, port = split_host_port(OLLAMA_HOST)
|
||||
self.ollama_ip = host
|
||||
self.ollama_port = port
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminSettings:
|
||||
ai_buddy: AiBuddySettingsEntry = field(default_factory=AiBuddySettingsEntry)
|
||||
|
||||
|
||||
class AdminDbManager:
|
||||
def __init__(self, session: dict, settings_manager: SettingsManager):
|
||||
self._session = session
|
||||
self._settings_manager = settings_manager
|
||||
self.ai_buddy = NestedSettingsManager(AuthManager.admin_session(),
|
||||
settings_manager,
|
||||
ADMIN_SETTINGS_ENTRY,
|
||||
AdminSettings,
|
||||
"ai_buddy")
|
||||
8
src/components/admin/assets/Admin.css
Normal file
8
src/components/admin/assets/Admin.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.adm-items-group {
|
||||
border: 1px solid color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||
border-radius: .25rem;
|
||||
padding: 4px;
|
||||
background-color: var(--color-base-100);
|
||||
align-items: center;
|
||||
padding-inline: .75rem;
|
||||
}
|
||||
0
src/components/admin/assets/__init__.py
Normal file
0
src/components/admin/assets/__init__.py
Normal file
62
src/components/admin/commands.py
Normal file
62
src/components/admin/commands.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from components.BaseCommandManager import BaseCommandManager
|
||||
from components.admin.constants import ROUTE_ROOT, Routes
|
||||
|
||||
|
||||
class AdminCommandManager(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def show_ai_buddy(self):
|
||||
return {
|
||||
"hx-get": f"{ROUTE_ROOT}{Routes.AiBuddy}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def save_ai_buddy(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddy}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
# The form adds the rest
|
||||
}
|
||||
|
||||
def cancel_ai_buddy(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.AiBuddyCancel}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def show_import_holidays(self):
|
||||
return {
|
||||
"hx-get": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
|
||||
class ImportHolidaysCommandManager(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def on_paste(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.PasteHolidays}",
|
||||
"hx-target": f"#{self._owner.datagrid.get_id()}",
|
||||
"hx_trigger": "keydown[ctrlKey && key=='Enter']",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def import_holidays(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.ImportHolidays}",
|
||||
"hx-target": f"#{self._owner.datagrid.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||
}
|
||||
136
src/components/admin/components/Admin.py
Normal file
136
src/components/admin/components/Admin.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from ai.mcp_client import MPC_CLIENTS_IDS
|
||||
from ai.mcp_tools import MCPServerTools
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.admin.admin_db_manager import AdminDbManager
|
||||
from components.admin.commands import AdminCommandManager
|
||||
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm
|
||||
from components.admin.components.ImportHolidays import ImportHolidays
|
||||
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
|
||||
from components.aibuddy.assets.icons import icon_brain_ok
|
||||
from components.hoildays.assets.icons import icon_holidays
|
||||
from components.tabs.components.MyTabs import MyTabs
|
||||
from components_helpers import mk_ellipsis, mk_icon
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
class Admin(BaseComponent):
|
||||
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager: MyTabs = None):
|
||||
super().__init__(session, _id)
|
||||
self.settings_manager = settings_manager
|
||||
self.tabs_manager = tabs_manager
|
||||
self.commands = AdminCommandManager(self)
|
||||
self.db = AdminDbManager(session, settings_manager)
|
||||
|
||||
def show_ai_buddy_form(self, boundaries):
|
||||
fields = [
|
||||
AdminFormItem('ollama_ip', "Ollama IP", "IP address of the Ollama server.", AdminFormType.TEXT),
|
||||
AdminFormItem("ollama_port", "Ollama Port", "Port of the Ollama server.", AdminFormType.NUMBER),
|
||||
AdminFormItem("ollama_model", "LLM Model", "Model to use for Ollama.", AdminFormType.SELECT, ["mistral"]),
|
||||
AdminFormItem("llm_mode", "LLM Mode", "Type of LLM to use (Dev)", AdminFormType.SELECT, MPC_CLIENTS_IDS),
|
||||
AdminFormItem("available_tools", "Available Tools", "Possible tools", AdminFormType.CHECKBOX,
|
||||
MCPServerTools.list_tools()),
|
||||
]
|
||||
hooks = {
|
||||
"on_ok": self.commands.save_ai_buddy(),
|
||||
"on_cancel": self.commands.cancel_ai_buddy(),
|
||||
"ok_title": "Apply"
|
||||
}
|
||||
form = InstanceManager.get(self._session,
|
||||
AdminForm.create_component_id(self._session, prefix=self._id),
|
||||
AdminForm,
|
||||
owner=self,
|
||||
title="AI Buddy Configuration Page",
|
||||
obj=self.db.ai_buddy,
|
||||
form_fields=fields,
|
||||
hooks=hooks,
|
||||
key=ADMIN_AI_BUDDY_INSTANCE_ID,
|
||||
boundaries=boundaries)
|
||||
|
||||
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - AI Buddy", form)
|
||||
|
||||
def show_import_holidays_form(self, boundaries):
|
||||
form = InstanceManager.get(self._session,
|
||||
ImportHolidays.create_component_id(self._session, prefix=self._id),
|
||||
ImportHolidays,
|
||||
settings_manager=self.settings_manager,
|
||||
boundaries=boundaries)
|
||||
|
||||
return self._add_tab(ADMIN_AI_BUDDY_INSTANCE_ID, "Admin - Import Holidays", form)
|
||||
|
||||
def update_ai_buddy_settings(self, values: dict):
|
||||
values = self.manage_lists(values)
|
||||
self.db.ai_buddy.update(values, ignore_missing=True)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def cancel_ai_buddy_settings(self):
|
||||
tab_id = self.tabs_manager.get_tab_id(ADMIN_AI_BUDDY_INSTANCE_ID)
|
||||
self.tabs_manager.remove_tab(tab_id)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(cls="divider"),
|
||||
Div(mk_ellipsis("Admin", cls="text-sm font-medium mb-1 mr-3")),
|
||||
#
|
||||
Div(
|
||||
mk_icon(icon_brain_ok, can_select=False),
|
||||
mk_ellipsis("AI Buddy", cls="text-sm", **self.commands.show_ai_buddy()),
|
||||
cls="flex p-0 min-h-0 truncate",
|
||||
),
|
||||
Div(
|
||||
mk_icon(icon_holidays, can_select=False),
|
||||
mk_ellipsis("holidays", cls="text-sm", **self.commands.show_import_holidays()),
|
||||
cls="flex p-0 min-h-0 truncate",
|
||||
),
|
||||
#
|
||||
# cls=""),
|
||||
# Script(f"bindAdmin('{self._id}')"),
|
||||
id=f"{self._id}"
|
||||
)
|
||||
|
||||
def _add_tab(self, tab_key, title, content):
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{ADMIN_INSTANCE_ID}{session['user_id']}"
|
||||
|
||||
@staticmethod
|
||||
def manage_lists(data_dict):
|
||||
"""
|
||||
Processes a dictionary of key-value pairs to reorganize keys based on specific
|
||||
criteria. If a key ends with its corresponding string value, the method extracts
|
||||
the prefix of the key (the portion of the key before the value) and groups the
|
||||
value under this prefix in a list. Otherwise, the original key-value pair is
|
||||
preserved in the resulting dictionary.
|
||||
|
||||
:param data_dict: Dictionary where each key is a string and its corresponding
|
||||
value can be of any type.
|
||||
:type data_dict: dict
|
||||
:return: A dictionary where the keys have been categorized into groups
|
||||
based on whether they end with the same string value, reorganized into
|
||||
lists, while preserving other key-value pairs as they are.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
result_dict = {}
|
||||
|
||||
for key, value in data_dict.items():
|
||||
# Check if the value is a string and the key ends with the value
|
||||
if isinstance(value, str) and key.endswith(value):
|
||||
# Find the beginning part of the key (before the value)
|
||||
prefix = key.replace(value, '').rstrip('_')
|
||||
|
||||
# Add the value to the list under the prefix key
|
||||
if prefix not in result_dict:
|
||||
result_dict[prefix] = []
|
||||
|
||||
result_dict[prefix].append(value)
|
||||
|
||||
else:
|
||||
result_dict[key] = value
|
||||
|
||||
return result_dict
|
||||
121
src/components/admin/components/AdminForm.py
Normal file
121
src/components/admin/components/AdminForm.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components_helpers import set_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
|
||||
from core.utils import get_unique_id
|
||||
|
||||
|
||||
class AdminFormType:
|
||||
TEXT = "text"
|
||||
NUMBER = "number"
|
||||
DATE = "date"
|
||||
SELECT = "select"
|
||||
CHECKBOX = "checkbox"
|
||||
RADIO = "radio"
|
||||
TEXTAREA = "textarea"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminFormItem:
|
||||
name: str
|
||||
title: str
|
||||
description: str
|
||||
type: str
|
||||
possible_values: list[str] = None
|
||||
|
||||
|
||||
class AdminForm(BaseComponent):
|
||||
def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None,
|
||||
boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._owner = owner
|
||||
self._hooks = hooks
|
||||
self._key = key
|
||||
self._boundaries = boundaries
|
||||
self.title = title
|
||||
self.obj = obj
|
||||
self.form_fields = form_fields
|
||||
|
||||
def mk_input(self, item: AdminFormItem):
|
||||
return Input(
|
||||
type=item.type,
|
||||
value=getattr(self.obj, item.name),
|
||||
id=item.name,
|
||||
cls="input"
|
||||
)
|
||||
|
||||
def mk_checkbox(self, item: AdminFormItem):
|
||||
if not item.possible_values:
|
||||
return Div("No options available")
|
||||
|
||||
current_values = getattr(self.obj, item.name, [])
|
||||
|
||||
checkbox_items = [Div(
|
||||
Input(
|
||||
Label(value),
|
||||
type="checkbox",
|
||||
value=value,
|
||||
id=f"{item.name}_{value}",
|
||||
cls="checkbox checkbox-xs",
|
||||
checked=value in current_values
|
||||
),
|
||||
|
||||
cls="checkbox-item") for value in item.possible_values]
|
||||
|
||||
return Div(*checkbox_items, cls="adm-items-group")
|
||||
|
||||
def mk_select(self, item: AdminFormItem):
|
||||
if not item.possible_values:
|
||||
return Div("No options available")
|
||||
|
||||
current_value = getattr(self.obj, item.name, "")
|
||||
|
||||
options = [
|
||||
Option(
|
||||
value,
|
||||
value=value,
|
||||
selected=value == current_value
|
||||
) for value in item.possible_values
|
||||
]
|
||||
|
||||
return Select(
|
||||
*options,
|
||||
id=item.name,
|
||||
cls="select select-bordered w-full"
|
||||
)
|
||||
|
||||
def mk_item(self, item: AdminFormItem):
|
||||
if item.type == AdminFormType.CHECKBOX:
|
||||
return self.mk_checkbox(item)
|
||||
elif item.type == AdminFormType.SELECT:
|
||||
return self.mk_select(item)
|
||||
else:
|
||||
return self.mk_input(item)
|
||||
|
||||
def __ft__(self):
|
||||
return Form(
|
||||
Fieldset(Legend(self.title, cls="fieldset-legend"),
|
||||
*[
|
||||
Div(
|
||||
Label(item.title, cls="label"),
|
||||
self.mk_item(item),
|
||||
P(item.description, cls="label"),
|
||||
)
|
||||
|
||||
for item in self.form_fields
|
||||
],
|
||||
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
||||
**set_boundaries(self._boundaries),
|
||||
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session, prefix=None, suffix=None):
|
||||
if suffix is None:
|
||||
suffix = get_unique_id()
|
||||
|
||||
return f"{prefix}{suffix}"
|
||||
59
src/components/admin/components/ImportHolidays.py
Normal file
59
src/components/admin/components/ImportHolidays.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from fasthtml.components import *
|
||||
from pandas import DataFrame
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from helpers.ComponentsInstancesHelper import ComponentsInstancesHelper
|
||||
from components.admin.commands import ImportHolidaysCommandManager
|
||||
from components.admin.constants import ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID
|
||||
from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.datagrid_new.settings import DataGridSettings
|
||||
from components.hoildays.helpers.nibelisparser import NibelisParser
|
||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
||||
from components_helpers import mk_dialog_buttons, set_boundaries
|
||||
from conftest import session
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
class ImportHolidays(BaseComponent):
|
||||
def __init__(self, session, _id, settings_manager, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._settings_manager = settings_manager
|
||||
self._boundaries = boundaries
|
||||
self.commands = ImportHolidaysCommandManager(self)
|
||||
self.datagrid = InstanceManager.get(session,
|
||||
DataGrid.create_component_id(session),
|
||||
DataGrid,
|
||||
grid_settings=DataGridSettings(views_visible=False,
|
||||
open_file_visible=False,
|
||||
open_settings_visible=False))
|
||||
|
||||
def on_paste(self, content):
|
||||
parser = NibelisParser(content)
|
||||
holidays = parser.parse()
|
||||
self.datagrid.init_from_dataframe(DataFrame(holidays))
|
||||
return self.datagrid
|
||||
|
||||
def import_holidays(self):
|
||||
repositories = ComponentsInstancesHelper.get_repositories(self._session)
|
||||
key = repositories.db.ensure_exists(USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME)
|
||||
datagrid = DataGrid(self._session, DataGrid.create_component_id(self._session), key, self._settings_manager)
|
||||
datagrid.init_from_dataframe(self.datagrid.get_dataframe(), save_state=True)
|
||||
return repositories.refresh()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div("Import holidays...", cls="mb-2"),
|
||||
Textarea(name="content",
|
||||
cls="textarea",
|
||||
placeholder="Paste the content.\nCtl+Enter when done.",
|
||||
**self.commands.on_paste()),
|
||||
Div(self.datagrid, cls="mt-2"),
|
||||
mk_dialog_buttons(ok_title="Import", cls="mt-2", on_ok=self.commands.import_holidays()),
|
||||
id=self._id,
|
||||
cls="m-2",
|
||||
**set_boundaries(self._boundaries, other=26),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session, prefix=None, suffix=None):
|
||||
return f"{ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID}{session['user_id']}"
|
||||
0
src/components/admin/components/__init__.py
Normal file
0
src/components/admin/components/__init__.py
Normal file
11
src/components/admin/constants.py
Normal file
11
src/components/admin/constants.py
Normal file
@@ -0,0 +1,11 @@
|
||||
ADMIN_INSTANCE_ID = "__Admin__"
|
||||
ADMIN_AI_BUDDY_INSTANCE_ID = "__AdminAIBuddy__"
|
||||
ADMIN_IMPORT_HOLIDAYS_INSTANCE_ID = "__AdminImportHolidays__"
|
||||
ROUTE_ROOT = "/admin"
|
||||
ADMIN_SETTINGS_ENTRY = "Admin"
|
||||
|
||||
class Routes:
|
||||
AiBuddy = "/ai-buddy"
|
||||
AiBuddyCancel = "/ai-buddy-cancel"
|
||||
ImportHolidays = "/import-holidays"
|
||||
PasteHolidays = "/paste-holidays"
|
||||
0
src/components/admin/settings.py
Normal file
0
src/components/admin/settings.py
Normal file
39
src/components/aibuddy/AIBuddyApp.py
Normal file
39
src/components/aibuddy/AIBuddyApp.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from components.aibuddy.constants import Routes
|
||||
from core.instance_manager import debug_session, InstanceManager
|
||||
|
||||
ai_buddy_app, rt = fast_app()
|
||||
|
||||
logger = logging.getLogger("AIBuddy")
|
||||
|
||||
|
||||
@rt(Routes.Request)
|
||||
def get(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.Request} - GET with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_request_form()
|
||||
|
||||
|
||||
@rt(Routes.Request)
|
||||
async def post(session, _id: str, q: str):
|
||||
logger.debug(f"Entering {Routes.Request} - POST with args {debug_session(session)}, {_id=}, {q=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
res = await instance.make_async_ai_request(q)
|
||||
return res
|
||||
|
||||
|
||||
@rt(Routes.ResetRequest)
|
||||
def post(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.ResetRequest} with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.reset_ai_request()
|
||||
|
||||
|
||||
@rt(Routes.LlmStatus)
|
||||
def get(session, _id: str):
|
||||
logger.debug(f"Entering {Routes.LlmStatus} - GET with args {debug_session(session)}, {_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.get_llm_status()
|
||||
11
src/components/aibuddy/Readme.md
Normal file
11
src/components/aibuddy/Readme.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# id
|
||||
|
||||
**Datagrid ids**:
|
||||
|
||||
using `AIBuddy(id=my_id)`
|
||||
|
||||
| Name | value |
|
||||
|----------------------------|----------------|
|
||||
| Status icon | `s_{self._id}` |
|
||||
| Question (input) component | `q_{self._id}` |
|
||||
| Answer component | `a_{self._id}` |
|
||||
0
src/components/aibuddy/__init__.py
Normal file
0
src/components/aibuddy/__init__.py
Normal file
52
src/components/aibuddy/assets/AIBuddy.js
Normal file
52
src/components/aibuddy/assets/AIBuddy.js
Normal file
@@ -0,0 +1,52 @@
|
||||
function bindAIBuddy(elementId) {
|
||||
console.debug("bindAIBuddy on element " + elementId);
|
||||
|
||||
const aiBuddy = document.getElementById(elementId);
|
||||
|
||||
if (!aiBuddy) {
|
||||
console.error(`AIBuddy with id "${elementId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let lastShiftPress = 0;
|
||||
const doublePressDelay = 300;
|
||||
|
||||
const makeAIRequest = () => {
|
||||
const targetID = "q_" + elementId;
|
||||
|
||||
htmx.ajax('GET', '/ai/request', {
|
||||
target: `#${targetID}`,
|
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
swap: "outerHTML",
|
||||
values: {
|
||||
_id: elementId,
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the htmx:afterSwap event to focus on the input
|
||||
document.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.target.id === targetID) {
|
||||
event.target.focus();
|
||||
}
|
||||
|
||||
// Remove this event listener after it's been triggered
|
||||
document.removeEventListener('htmx:afterSwap', arguments.callee);
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.ctrlKey && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
makeAIRequest();
|
||||
}
|
||||
|
||||
if (event.key === 'Shift') {
|
||||
const currentTime = new Date().getTime();
|
||||
if (currentTime - lastShiftPress <= doublePressDelay) {
|
||||
event.preventDefault();
|
||||
makeAIRequest();
|
||||
}
|
||||
lastShiftPress = currentTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
0
src/components/aibuddy/assets/__init__.py
Normal file
0
src/components/aibuddy/assets/__init__.py
Normal file
17
src/components/aibuddy/assets/icons.py
Normal file
17
src/components/aibuddy/assets/icons.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
# Fluent BrainCircuit20Regular
|
||||
icon_brain_ok = NotStr("""<svg name="ai_ok" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20">
|
||||
<g fill="none">
|
||||
<path d="M6.13 2.793A3.91 3.91 0 0 1 8.5 2a1.757 1.757 0 0 1 1.5.78A1.757 1.757 0 0 1 11.5 2a3.91 3.91 0 0 1 2.37.793c.525.408.93.973 1.073 1.656c.328.025.628.161.88.366c.382.31.66.775.835 1.267c.274.765.348 1.74.064 2.57c.072.034.143.074.212.12c.275.183.484.445.638.754c.303.605.428 1.449.428 2.474c0 1.141-.435 1.907-.987 2.38a2.68 2.68 0 0 1-1.054.555c-.1.558-.38 1.204-.819 1.752C14.57 17.402 13.686 18 12.5 18c-.94 0-1.688-.52-2.174-1.03a4.252 4.252 0 0 1-.326-.385a4.245 4.245 0 0 1-.326.385C9.188 17.48 8.441 18 7.5 18c-1.186 0-2.069-.598-2.64-1.313a4.057 4.057 0 0 1-.819-1.752a2.68 2.68 0 0 1-1.054-.555C2.435 13.907 2 13.14 2 12c0-1.025.126-1.87.428-2.474c.154-.309.363-.57.638-.755a1.58 1.58 0 0 1 .212-.118c-.284-.832-.21-1.806.064-2.571c.175-.492.453-.957.835-1.267c.252-.205.552-.34.88-.366c.144-.683.549-1.248 1.074-1.656zM9.5 4.5V4.49l-.002-.05a2.744 2.744 0 0 0-.154-.764a1.222 1.222 0 0 0-.309-.49A.76.76 0 0 0 8.5 3a2.91 2.91 0 0 0-1.756.582C6.28 3.943 6 4.432 6 5a.5.5 0 0 1-.658.474c-.188-.062-.356-.027-.535.117c-.196.16-.387.444-.524.827c-.279.782-.25 1.729.133 2.305A.5.5 0 0 1 4.5 9h.75a2.25 2.25 0 0 1 2.25 2.25v.335a1.5 1.5 0 1 1-1 0v-.335c0-.69-.56-1.25-1.25-1.25H3.5a.499.499 0 0 1-.175-.032l-.003.006C3.124 10.369 3 11.025 3 12c0 .859.315 1.343.638 1.62c.347.298.732.38.862.38a.5.5 0 0 1 .5.5c0 .368.2 1.011.64 1.563c.429.535 1.046.937 1.86.937c.56 0 1.062-.313 1.45-.72c.191-.2.34-.407.437-.577a1.573 1.573 0 0 0 .113-.236V7.5H8.415a1.5 1.5 0 1 1 0-1H9.5v-2zm1 9.999v.967a1.575 1.575 0 0 0 .113.236c.098.17.246.377.436.577c.389.407.892.72 1.451.72c.814 0 1.431-.402 1.86-.937c.44-.552.64-1.195.64-1.563a.5.5 0 0 1 .5-.5c.13 0 .515-.082.862-.38c.323-.277.638-.761.638-1.62c0-.975-.125-1.63-.322-2.026a.923.923 0 0 0-.3-.37A.657.657 0 0 0 16 9.5a.5.5 0 0 1-.416-.777c.384-.576.412-1.523.133-2.305c-.137-.383-.328-.668-.524-.827c-.179-.144-.347-.18-.535-.117A.5.5 0 0 1 14 5c0-.568-.28-1.057-.745-1.418A2.91 2.91 0 0 0 11.5 3a.76.76 0 0 0-.535.186a1.22 1.22 0 0 0-.31.49a2.579 2.579 0 0 0-.155.814v9.01h.75c.69 0 1.25-.56 1.25-1.25v-1.835a1.5 1.5 0 1 1 1 0v1.835a2.25 2.25 0 0 1-2.25 2.25h-.75zM6.5 7a.5.5 0 1 0 1 0a.5.5 0 0 0-1 0zM13 9.5a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1zm-6 3a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1z" fill="currentColor">
|
||||
</path>
|
||||
</g>
|
||||
</svg>""")
|
||||
|
||||
# Fluent Warning20Regular
|
||||
icon_brain_warning = NotStr("""<svg name="ai_nok" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20">
|
||||
<g fill="none">
|
||||
<path d="M10 7a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0v-4A.5.5 0 0 1 10 7zm0 7.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5zM8.686 2.852a1.5 1.5 0 0 1 2.628 0l6.56 11.925A1.5 1.5 0 0 1 16.558 17H3.44a1.5 1.5 0 0 1-1.314-2.223L8.686 2.852zm1.752.482a.5.5 0 0 0-.876 0L3.003 15.26a.5.5 0 0 0 .438.741H16.56a.5.5 0 0 0 .438-.74L10.438 3.333z" fill="currentColor">
|
||||
</path>
|
||||
</g>
|
||||
</svg>""")
|
||||
32
src/components/aibuddy/commands.py
Normal file
32
src/components/aibuddy/commands.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from components.BaseCommandManager import BaseCommandManager
|
||||
from components.aibuddy.constants import ROUTE_ROOT, Routes
|
||||
|
||||
|
||||
class AIBuddyCommandManager(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def make_request(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.Request}",
|
||||
"hx-target": f"#a_{self._id}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def reset_request(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.ResetRequest}",
|
||||
"hx-target": f"#q_{self._id}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}"}}',
|
||||
}
|
||||
|
||||
def get_llm_status(self):
|
||||
return {
|
||||
"hx-get": f"{ROUTE_ROOT}{Routes.LlmStatus}",
|
||||
"hx-target": f"#s_{self._id}",
|
||||
"hx-trigger": "every 2s",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}"}}',
|
||||
}
|
||||
217
src/components/aibuddy/components/AIBuddy.py
Normal file
217
src/components/aibuddy/components/AIBuddy.py
Normal file
@@ -0,0 +1,217 @@
|
||||
import asyncio
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
from fasthtml.components import *
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from ai.debug_lmm import DebugRequest, DebugRequestMetadata, DebugConversation
|
||||
from ai.mcp_client import InProcessMCPClientCustomTools, InProcessMCPClientNativeTools
|
||||
from assets.icons import icon_dismiss_regular
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.aibuddy.assets.icons import icon_brain_ok, icon_brain_warning
|
||||
from components.aibuddy.commands import AIBuddyCommandManager
|
||||
from components.aibuddy.constants import *
|
||||
from components.aibuddy.settings import AI_BUDDY_SETTINGS_ENTRY, AIBuddySettings
|
||||
from components_helpers import mk_ellipsis, mk_icon, mk_tooltip
|
||||
from config import OLLAMA_HOST
|
||||
from core.settings_management import GenericDbManager
|
||||
|
||||
|
||||
class AIBuddy(BaseComponent):
|
||||
def __init__(self, session, _id: str = None, settings_manager=None, tabs_manager=None):
|
||||
super().__init__(session, _id)
|
||||
self.settings_manager = settings_manager
|
||||
self.db = GenericDbManager(session, settings_manager, AI_BUDDY_SETTINGS_ENTRY, AIBuddySettings)
|
||||
self.tabs_manager = tabs_manager
|
||||
self.commands = AIBuddyCommandManager(self)
|
||||
self.llm_status = None
|
||||
self.mcp_clients = {
|
||||
InProcessMCPClientNativeTools.ID: InProcessMCPClientNativeTools(session, settings_manager, OLLAMA_HOST),
|
||||
InProcessMCPClientCustomTools.ID: InProcessMCPClientCustomTools(session, settings_manager, OLLAMA_HOST)
|
||||
}
|
||||
# self.db.mcp_client_mod
|
||||
# self.db.llm_use_tools
|
||||
self.conversations: list[DebugConversation] = self.db.conversations
|
||||
|
||||
# Check LLM status once at initialization in a background thread
|
||||
threading.Thread(target=self._initial_status_check, daemon=True).start()
|
||||
|
||||
def _initial_status_check(self):
|
||||
"""Check LLM status once in a background thread"""
|
||||
try:
|
||||
# Run the async function in a new event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
self.llm_status = loop.run_until_complete(self.async_check_llm_active())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
self.llm_status = f"Error checking LLM status: {e}"
|
||||
|
||||
def get_conversations(self):
|
||||
return self.conversations
|
||||
|
||||
def show_request_form(self):
|
||||
return self.mk_input()
|
||||
|
||||
def register_components(self):
|
||||
return [
|
||||
(self.mk_input(), "TOP"),
|
||||
(self.mk_response(), "BOTTOM"),
|
||||
]
|
||||
|
||||
async def make_async_ai_request(self, request):
|
||||
if len(self.conversations) == 0:
|
||||
start = datetime.now()
|
||||
conversation = DebugConversation(f"{self.get_user_id()}{start.timestamp()}",
|
||||
int(start.timestamp()),
|
||||
request)
|
||||
self.conversations.append(conversation)
|
||||
else:
|
||||
conversation = self.conversations[-1]
|
||||
|
||||
debug = DebugRequest(request)
|
||||
conversation.requests.append(debug)
|
||||
|
||||
mcp_client = self.mcp_clients[self.db.mcp_client_mod]
|
||||
start = datetime.now() # datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
response = await mcp_client.generate_with_mcp_context(debug, request, self.db.llm_use_tools)
|
||||
|
||||
request_metadata = DebugRequestMetadata(f"{self.get_user_id()}{start.timestamp()}",
|
||||
self.get_user_id(),
|
||||
int(start.timestamp()),
|
||||
int(datetime.now().timestamp()),
|
||||
mcp_client.model,
|
||||
self.db.mcp_client_mod,
|
||||
self.db.llm_use_tools,
|
||||
self._debug_available_tools(mcp_client.available_tools))
|
||||
debug.metadata = request_metadata
|
||||
|
||||
self.db.conversations = self.conversations
|
||||
|
||||
return self.mk_response(response)
|
||||
|
||||
def reset_ai_request(self):
|
||||
return self.mk_input(True), self.mk_response(None, True)
|
||||
|
||||
def get_llm_status(self):
|
||||
return self.mk_status(), self.mk_input(True)
|
||||
|
||||
def mk_status(self):
|
||||
# Non-blocking, simply returns UI based on the current status
|
||||
if self.llm_status is None:
|
||||
return Span(cls="loading loading-infinity loading-sm", id=f"s_{self._id}", **self.commands.get_llm_status())
|
||||
elif self.llm_status is not True:
|
||||
return mk_tooltip(mk_icon(icon_brain_warning), self.llm_status, id=f"s_{self._id}")
|
||||
else:
|
||||
return mk_icon(icon_brain_ok, id=f"s_{self._id}")
|
||||
|
||||
def mk_input(self, oob=False):
|
||||
if self.llm_status is True:
|
||||
return self._mk_input_ok(oob)
|
||||
else:
|
||||
return Div(id=f"q_{self._id}", hx_swap_oob="true" if oob else None)
|
||||
|
||||
def mk_response(self, response=None, oob=False):
|
||||
if self.llm_status is True:
|
||||
return self._mk_response_ok(response, oob=oob)
|
||||
else:
|
||||
return Div(id=f"a_{self._id}",
|
||||
hx_swap_oob="true")
|
||||
|
||||
def _mk_input_ok(self, oob=False):
|
||||
return Div(
|
||||
Label(
|
||||
mk_icon(icon_brain_ok),
|
||||
Input(
|
||||
name="q",
|
||||
placeholder="Ask me anything...",
|
||||
tabindex="1",
|
||||
**self.commands.make_request()
|
||||
),
|
||||
cls="input",
|
||||
),
|
||||
mk_icon(icon_dismiss_regular, cls="ml-2", tooltip="Reset question", **self.commands.reset_request()),
|
||||
cls="flex",
|
||||
id=f"q_{self._id}",
|
||||
hx_swap_oob="true" if oob else None,
|
||||
)
|
||||
|
||||
def _mk_response_ok(self, response=None, oob=False):
|
||||
return Div(response or "Your response will appear here.",
|
||||
cls="w-full px-4 py-2 border border-gray-300 rounded-md",
|
||||
id=f"a_{self._id}",
|
||||
hx_swap_oob="true" if oob else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _debug_available_tools(available_tools):
|
||||
return [
|
||||
{"name": tool["name"],
|
||||
"parameters": tool["parameters"],
|
||||
"description": tool["description"]}
|
||||
for name, tool in available_tools.items()
|
||||
]
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(cls="divider"),
|
||||
Div(
|
||||
mk_ellipsis("AI Buddy", cls="text-sm font-medium mb-1 mr-3"),
|
||||
self.mk_status(),
|
||||
cls="flex items-center"),
|
||||
Script(f"bindAIBuddy('{self._id}')"),
|
||||
id=f"{self._id}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{AI_BUDDY_INSTANCE_ID}{session['user_id']}"
|
||||
|
||||
@staticmethod
|
||||
async def async_check_llm_active():
|
||||
"""Asynchronous LLM status check"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.post(
|
||||
f"{OLLAMA_HOST}/api/pull",
|
||||
json={"name": "mistral"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
return f"Error pulling Mistral model: {e}"
|
||||
|
||||
def query_mistral(self, prompt):
|
||||
"""Send a query to the Mistral model via Ollama API"""
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.post(
|
||||
f"{OLLAMA_HOST}/api/generate",
|
||||
json={
|
||||
"model": "mistral",
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e), "response": f"Error querying model: {e}"}
|
||||
|
||||
async def async_query_mistral(self, prompt):
|
||||
"""Asynchronous version of query_mistral"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.post(
|
||||
f"{OLLAMA_HOST}/api/generate",
|
||||
json={
|
||||
"model": "mistral",
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e), "response": f"Error querying model: {e}"}
|
||||
0
src/components/aibuddy/components/__init__.py
Normal file
0
src/components/aibuddy/components/__init__.py
Normal file
8
src/components/aibuddy/constants.py
Normal file
8
src/components/aibuddy/constants.py
Normal file
@@ -0,0 +1,8 @@
|
||||
AI_BUDDY_INSTANCE_ID = "__AIBuddy__"
|
||||
ROUTE_ROOT = "/ai"
|
||||
|
||||
|
||||
class Routes:
|
||||
Request = "/request"
|
||||
ResetRequest = "/reset-request"
|
||||
LlmStatus = "/llm-status"
|
||||
16
src/components/aibuddy/settings.py
Normal file
16
src/components/aibuddy/settings.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import dataclasses
|
||||
|
||||
from ai.debug_lmm import DebugConversation
|
||||
from ai.mcp_client import InProcessMCPClientCustomTools
|
||||
from core.settings_objects import BaseSettingObj
|
||||
|
||||
AI_BUDDY_SETTINGS_ENTRY = "AIBuddy"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AIBuddySettings(BaseSettingObj):
|
||||
__ENTRY_NAME__ = AI_BUDDY_SETTINGS_ENTRY
|
||||
|
||||
mcp_client_mod: str = InProcessMCPClientCustomTools.ID
|
||||
llm_use_tools: bool = True
|
||||
conversations: list[DebugConversation] = dataclasses.field(default_factory=list)
|
||||
16
src/components/applications/ApplicationsApp.py
Normal file
16
src/components/applications/ApplicationsApp.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
|
||||
from components.applications.constants import Routes
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
logger = logging.getLogger("Applications")
|
||||
|
||||
applications_app, rt = fast_app()
|
||||
|
||||
@rt(Routes.Open)
|
||||
def post(session, _id: str, app: str, boundaries: str):
|
||||
logger.debug(f"Entering post with args {_id=}, {app=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.open_application(app, boundaries)
|
||||
0
src/components/applications/__init__.py
Normal file
0
src/components/applications/__init__.py
Normal file
0
src/components/applications/assets/__init__.py
Normal file
0
src/components/applications/assets/__init__.py
Normal file
16
src/components/applications/commands.py
Normal file
16
src/components/applications/commands.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from components.BaseCommandManager import BaseCommandManager
|
||||
from components.applications.constants import ROUTE_ROOT, Routes
|
||||
|
||||
|
||||
class Commands(BaseCommandManager):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner)
|
||||
|
||||
def open_application(self, app_name: str):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.Open}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "app": "{app_name}", boundaries: getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
47
src/components/applications/components/Applications.py
Normal file
47
src/components/applications/components/Applications.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.applications.commands import Commands
|
||||
from components.applications.constants import APPLICATION_INSTANCE_ID
|
||||
from components.hoildays.assets.icons import icon_holidays
|
||||
from components.hoildays.components.HolidaysViewer import HolidaysViewer
|
||||
from components.hoildays.constants import HOLIDAYS_VIEWER_INSTANCE_ID
|
||||
from components_helpers import mk_ellipsis, mk_icon
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
class Applications(BaseComponent):
|
||||
def __init__(self, session: dict, _id: str, settings_manager=None, tabs_manager=None):
|
||||
super().__init__(session, _id)
|
||||
self.tabs_manager = tabs_manager
|
||||
self.settings_manager = settings_manager
|
||||
self.commands = Commands(self)
|
||||
|
||||
def open_application(self, app_name, boundaries):
|
||||
if app_name == "holidays":
|
||||
holiday = InstanceManager.get(self._session,
|
||||
HolidaysViewer.create_component_id(self._session),
|
||||
HolidaysViewer,
|
||||
settings_manager=self.settings_manager,
|
||||
boundaries=boundaries,
|
||||
)
|
||||
self.tabs_manager.add_tab("holidays", holiday, key=HOLIDAYS_VIEWER_INSTANCE_ID)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
raise NotImplementedError(app_name)
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(cls="divider"),
|
||||
|
||||
mk_ellipsis("Applications", cls="text-sm font-medium mb-1"),
|
||||
Div(
|
||||
mk_icon(icon_holidays, can_select=False), mk_ellipsis("Holidays"),
|
||||
cls="p-0 min-h-0 flex truncate",
|
||||
**self.commands.open_application("holidays")
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{APPLICATION_INSTANCE_ID}{session['user_id']}"
|
||||
0
src/components/applications/components/__init__.py
Normal file
0
src/components/applications/components/__init__.py
Normal file
6
src/components/applications/constants.py
Normal file
6
src/components/applications/constants.py
Normal file
@@ -0,0 +1,6 @@
|
||||
APPLICATION_INSTANCE_ID = "__Applications__"
|
||||
ROUTE_ROOT = "/apps"
|
||||
|
||||
|
||||
class Routes:
|
||||
Open = "/open"
|
||||
@@ -125,13 +125,14 @@ def post(session, _id: str, key: str, arg: str = None):
|
||||
|
||||
|
||||
@rt(Routes.OnClick)
|
||||
def post(session, _id: str, cell_id: str = None, modifier: str = None, boundaries: str=None):
|
||||
def post(session, _id: str, cell_id: str = None, modifier: str = None, boundaries: str = None):
|
||||
logger.debug(f"Entering on_click with args {_id=}, {cell_id=}, {modifier=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.manage_click(cell_id, modifier, json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.UpdateState)
|
||||
def post(session, _id: str, state: str, args: str = None):
|
||||
logger.debug(f"Entering on_state_changed with args {_id=}, {state=}, {args=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.manage_state_changed(state, args)
|
||||
return instance.manage_state_changed(state, args)
|
||||
|
||||
@@ -10,6 +10,7 @@ using `Datagrid(id=my_id)`
|
||||
| filter all | `fa_{datagrid_id}` |
|
||||
| file upload | `fu_{datagrid_id}` |
|
||||
| footer menu | `fm_{datagrid_id}` |
|
||||
| main | `m_{datagrid_id}` |
|
||||
| sidebar | `sb_{datagrid_id}` |
|
||||
| scroll bars | `scb_{datagrid_id}` |
|
||||
| Settings columns | `scol_{datagrid_id}` |
|
||||
|
||||
@@ -469,12 +469,14 @@ function getClickModifier(event) {
|
||||
}
|
||||
|
||||
function validateOnClickRequest(datagridId, event) {
|
||||
if (event.target.id !== datagridId) { // only try to cancel event sent by the datagrid
|
||||
console.debug("validateOnClickRequest : event", event)
|
||||
if (event.target.id !== "m_" + datagridId) { // only try to cancel event sent by the datagrid
|
||||
return;
|
||||
}
|
||||
|
||||
const triggeringElt = event.detail.requestConfig.triggeringEvent.target
|
||||
const sidebar = triggeringElt.closest('.dt2-sidebar');
|
||||
console.debug("sidebar", sidebar)
|
||||
if (sidebar) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ from components.datagrid_new.settings import DataGridRowState, DataGridColumnSta
|
||||
DataGridFooterConf, DataGridState, DataGridSettings, DatagridView
|
||||
from components_helpers import mk_icon, mk_ellipsis
|
||||
from core.instance_manager import InstanceManager
|
||||
from core.settings_management import SettingsManager
|
||||
from core.utils import get_unique_id, make_safe_id
|
||||
|
||||
logger = logging.getLogger("DataGrid")
|
||||
@@ -32,7 +33,13 @@ class DataGrid(BaseComponent):
|
||||
like filtering, column management, and view management.
|
||||
"""
|
||||
|
||||
def __init__(self, session, _id: str = None, key: Any = None, settings_manager=None, boundaries=None):
|
||||
def __init__(self,
|
||||
session,
|
||||
_id: str = None,
|
||||
key: Any = None,
|
||||
settings_manager: SettingsManager = None,
|
||||
grid_settings: DataGridSettings = None,
|
||||
boundaries: dict = None):
|
||||
"""
|
||||
Initialize the DataGrid component.
|
||||
|
||||
@@ -50,17 +57,17 @@ class DataGrid(BaseComponent):
|
||||
|
||||
# Load state and data
|
||||
self._state: DataGridState = self._db.load_state()
|
||||
self._settings: DataGridSettings = self._db.load_settings()
|
||||
self._settings: DataGridSettings = grid_settings or self._db.load_settings()
|
||||
self._df: DataFrame | None = self._db.load_dataframe()
|
||||
|
||||
# update boundaries if possible
|
||||
self.set_boundaries(boundaries)
|
||||
|
||||
# Create child components
|
||||
self._file_upload = self._create_component(FileUpload, f"fu_{self._id}")
|
||||
self._filter_all = self._create_component(FilterAll, f"fa_{self._id}")
|
||||
self._file_upload = self._create_component(FileUpload, f"fu_{self._id}", permission="open_file")
|
||||
self._filter_all = self._create_component(FilterAll, f"fa_{self._id}", permission="filter_all")
|
||||
self._columns_settings = self._create_component(ColumnsSettings, f"scol_{self._id}")
|
||||
self._views = self._create_component(Views, f"v_{self._id}")
|
||||
self._views = self._create_component(Views, f"v_{self._id}", permission="views")
|
||||
|
||||
# Initial setup
|
||||
self.close_sidebar()
|
||||
@@ -69,10 +76,6 @@ class DataGrid(BaseComponent):
|
||||
if len(self._state.footers) == 0:
|
||||
self._state.footers.append(DataGridFooterConf())
|
||||
|
||||
# ----------------------
|
||||
# Initialization Methods
|
||||
# ----------------------
|
||||
|
||||
def init_from_excel(self):
|
||||
"""
|
||||
Initialize the DataGrid from an Excel file uploaded by the user.
|
||||
@@ -344,6 +347,9 @@ class DataGrid(BaseComponent):
|
||||
def get_settings(self) -> DataGridSettings:
|
||||
return self._settings
|
||||
|
||||
def get_dataframe(self) -> DataFrame:
|
||||
return self._df
|
||||
|
||||
def get_table_id(self):
|
||||
return f"t_{self._id}"
|
||||
|
||||
@@ -625,14 +631,20 @@ class DataGrid(BaseComponent):
|
||||
# self.mk_selection_mode_button(),
|
||||
# self.mk_resize_table_button(),
|
||||
# self.mk_reset_filter_button(),
|
||||
self.mk_download_button(),
|
||||
self.mk_settings_button(),
|
||||
self.mk_button(self.mk_download_button(), permission="open_file"),
|
||||
self.mk_button(self.mk_settings_button(), permission="open_settings"),
|
||||
cls="flex mr-2",
|
||||
name="dt-menu",
|
||||
id=f"cm_{self._id}",
|
||||
hx_swap_oob='true' if oob else None,
|
||||
)
|
||||
|
||||
def mk_button(self, button, permission=None):
|
||||
if not self._validate_permission(permission):
|
||||
return None
|
||||
|
||||
return button
|
||||
|
||||
def mk_download_button(self):
|
||||
return mk_icon(icon_open, **self.commands.download())
|
||||
|
||||
@@ -775,7 +787,10 @@ class DataGrid(BaseComponent):
|
||||
df = df[df[col_id].astype(str).isin(values)]
|
||||
return df
|
||||
|
||||
def _create_component(self, component_type: type, component_id: str):
|
||||
def _create_component(self, component_type: type, component_id: str, permission=None):
|
||||
if not self._validate_permission(permission):
|
||||
return None
|
||||
|
||||
safe_create_component_id = getattr(component_type, "create_component_id")
|
||||
return InstanceManager.get(self._session,
|
||||
safe_create_component_id(self._session, component_id, ""),
|
||||
@@ -797,6 +812,16 @@ class DataGrid(BaseComponent):
|
||||
|
||||
return max_height
|
||||
|
||||
def _validate_permission(self, permission):
|
||||
if permission is None:
|
||||
return True
|
||||
|
||||
settings_attr = permission + "_visible"
|
||||
if hasattr(self._settings, settings_attr) and not getattr(self._settings, settings_attr):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(
|
||||
@@ -810,11 +835,20 @@ class DataGrid(BaseComponent):
|
||||
),
|
||||
|
||||
Script(f"bindDatagrid('{self._id}', false);"),
|
||||
**self.commands.on_click()
|
||||
**self.commands.on_click(),
|
||||
id=f"m_{self._id}",
|
||||
),
|
||||
id=f"{self._id}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def new(session, data, index=None):
|
||||
datagrid = DataGrid(session, DataGrid.create_component_id(session))
|
||||
#dataframe = DataFrame(data, index=index)
|
||||
dataframe = DataFrame(data)
|
||||
datagrid.init_from_dataframe(dataframe)
|
||||
return datagrid
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
prefix = f"{DATAGRID_INSTANCE_ID}{session['user_id']}"
|
||||
|
||||
@@ -26,7 +26,7 @@ class DataframeWrapper:
|
||||
|
||||
|
||||
class DataGridDbManager:
|
||||
def __init__(self, session: dict, settings_manager: SettingsManager, key: str):
|
||||
def __init__(self, session: dict, settings_manager: SettingsManager, key: tuple):
|
||||
self._session = session
|
||||
self._settings_manager = settings_manager
|
||||
self._key = "#".join(make_safe_id(item) for item in key) if key else ""
|
||||
|
||||
@@ -54,6 +54,10 @@ class DataGridSettings:
|
||||
file_name: str = None
|
||||
selected_sheet_name: str = None
|
||||
header_visible: bool = True
|
||||
filter_all_visible: bool = True
|
||||
views_visible: bool = True
|
||||
open_file_visible: bool = True
|
||||
open_settings_visible: bool = True
|
||||
views: list[DatagridView] = dataclasses.field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fasthtml.fastapp import fast_app
|
||||
@@ -14,7 +15,28 @@ logger = logging.getLogger("Debugger")
|
||||
def post(session, _id: str, user_id: str, digest: str = None):
|
||||
logger.debug(f"Entering {Routes.DbEngineData} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.add_tab(user_id, digest)
|
||||
return instance.db_engine_headers(user_id, digest)
|
||||
|
||||
|
||||
@rt(Routes.DbEngineDigest)
|
||||
def post(session, _id: str, user_id: str, digest: str):
|
||||
logger.debug(f"Entering {Routes.DbEngineDigest} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.open_digest(user_id, digest)
|
||||
|
||||
|
||||
@rt(Routes.IaBuddyRequests)
|
||||
def post(session, _id: str, boundaries: str = None):
|
||||
logger.debug(f"Entering {Routes.IaBuddyRequests} with args {debug_session(session)}, {_id=}, {boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.ia_requests(json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.IaBuddyRequest)
|
||||
def post(session, _id: str, request: str, boundaries: str = None):
|
||||
logger.debug(f"Entering {Routes.IaBuddyRequest} with args {debug_session(session)}, {_id=}, {request=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.ia_request(request, json.loads(boundaries) if boundaries else None)
|
||||
|
||||
|
||||
@rt(Routes.JsonViewerFold)
|
||||
@@ -23,10 +45,3 @@ def post(session, _id: str, node_id: str, folding: str):
|
||||
instance = InstanceManager.get(session, _id)
|
||||
instance.set_node_folding(node_id, folding)
|
||||
return instance.render_node(node_id)
|
||||
|
||||
@rt(Routes.JsonOpenDigest)
|
||||
def post(session, _id: str, user_id: str, digest: str):
|
||||
logger.debug(f"Entering {Routes.JsonOpenDigest} with args {debug_session(session)}, {_id=}, {user_id=}, {digest=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.open_digest(user_id, digest)
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
.mmt-jsonviewer {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
max-height: 100%
|
||||
}
|
||||
|
||||
/* Use inherited CSS variables for your custom theme */
|
||||
|
||||
@@ -11,7 +11,7 @@ class Commands:
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineData}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "user_id": "{user_id}"}}',
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "user_id": "{user_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def db_engine_refs(self, ref_id: str | None):
|
||||
@@ -19,7 +19,23 @@ class Commands:
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineRefs}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "ref_id": "{ref_id}"}}',
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "ref_id": "{ref_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def ia_buddy_requests(self):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.IaBuddyRequests}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
def ia_buddy_request(self, request_id: str | None):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.IaBuddyRequest}",
|
||||
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'js:{{"_id": "{self._id}", "request": "{request_id}", "boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}',
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +54,7 @@ class JsonViewerCommands:
|
||||
|
||||
def open_digest(self, user_id, digest):
|
||||
return {
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.JsonOpenDigest}",
|
||||
"hx-post": f"{ROUTE_ROOT}{Routes.DbEngineDigest}",
|
||||
"hx-target": f"#{self._owner.get_owner().tabs_manager.get_id()}",
|
||||
"hx-swap": "outerHTML",
|
||||
"hx-vals": f'{{"_id": "{self._id}", "user_id": "{user_id}", "digest": "{digest}"}}',
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fasthtml.components import *
|
||||
|
||||
from ai.debug_lmm import DebugConversation
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.aibuddy.assets.icons import icon_brain_ok
|
||||
from components.aibuddy.components.AIBuddy import AIBuddy
|
||||
from components.debugger.assets.icons import icon_dbengine
|
||||
from components.debugger.commands import Commands
|
||||
from components.debugger.components.JsonViewer import JsonViewer
|
||||
from components.debugger.constants import DBENGINE_DEBUGGER_INSTANCE_ID
|
||||
from components_helpers import mk_ellipsis, mk_icon
|
||||
from components_helpers import mk_ellipsis, mk_accordion_section
|
||||
from core.instance_manager import InstanceManager
|
||||
from core.utils import get_unique_id
|
||||
|
||||
@@ -22,43 +26,83 @@ class Debugger(BaseComponent):
|
||||
self.tabs_manager = tabs_manager
|
||||
self.commands = Commands(self)
|
||||
|
||||
def add_tab(self, user_id, digest):
|
||||
content = self.mk_db_engine_object(user_id, digest)
|
||||
def ia_requests(self, boundaries: dict):
|
||||
def _mk_span(conversation, helper):
|
||||
return Span(
|
||||
f"{datetime.fromtimestamp(conversation.start_time).strftime('%Y-%m-%d %H:%M:%S')} - {conversation.initial_prompt}",
|
||||
cls=helper.class_string,
|
||||
**self.commands.ia_buddy_request(conversation.id))
|
||||
|
||||
ia_buddy = InstanceManager.get(self._session, AIBuddy.create_component_id(self._session))
|
||||
conversations = ia_buddy.get_conversations()
|
||||
|
||||
logger.debug(f"mk_ia_requests_object: {conversations}")
|
||||
|
||||
hook = (lambda key, node, helper: isinstance(node.value, DebugConversation),
|
||||
lambda key, node, helper: _mk_span(node.value, helper))
|
||||
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=None,
|
||||
data=ia_buddy.get_conversations(),
|
||||
hooks=[hook],
|
||||
boundaries=boundaries)
|
||||
return self._add_tab(f"debugger-iabuddy-requests", "AI requests", jsonviewer)
|
||||
|
||||
def ia_request(self, request_id, boundaries: dict):
|
||||
def _mk_text_area(value, helper):
|
||||
return Textarea(value, cls="textarea textarea-sm w-full p-2", disabled=True)
|
||||
|
||||
ia_buddy = InstanceManager.get(self._session, AIBuddy.create_component_id(self._session))
|
||||
requests = ia_buddy.get_conversations()
|
||||
request = next((req for req in requests if req.id == request_id), None)
|
||||
|
||||
logger.debug(f"request: {request}")
|
||||
if request is None:
|
||||
return None
|
||||
|
||||
hook = (lambda key, node, helper: key in ("extended_prompt", "response"),
|
||||
lambda key, node, helper: _mk_text_area(node.value, helper))
|
||||
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=None,
|
||||
data=request.to_dict(),
|
||||
hooks=[hook],
|
||||
boundaries=boundaries)
|
||||
return self._add_tab(f"debugger-iabuddy-{request_id}", f"Request {request.initial_prompt}", jsonviewer)
|
||||
|
||||
def db_engine_headers(self, user_id, digest):
|
||||
data = self.db_engine.debug_load(user_id, digest) if digest else self.db_engine.debug_head(user_id)
|
||||
logger.debug(f"mk_db_engine: {data}")
|
||||
|
||||
tab_key = f"debugger-dbengine-{digest}"
|
||||
title = f"DBEngine-{digest if digest else 'head'}"
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def mk_db_engine_object(self, user_id, digest):
|
||||
data = self.db_engine.debug_load(user_id, digest) if digest else self.db_engine.debug_head(user_id)
|
||||
|
||||
logger.debug(f"mk_db_engine: {data}")
|
||||
return InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=user_id,
|
||||
data=data)
|
||||
jsonviewer = InstanceManager.get(self._session,
|
||||
JsonViewer.create_component_id(self._session, prefix=self._id),
|
||||
JsonViewer,
|
||||
owner=self,
|
||||
user_id=user_id,
|
||||
data=data,
|
||||
key=tab_key)
|
||||
return self._add_tab(tab_key, title, jsonviewer)
|
||||
|
||||
def mk_db_engine(self, selected):
|
||||
return Div(
|
||||
Input(type="radio",
|
||||
name=f"dbengine-accordion-{self._id}",
|
||||
checked="checked" if selected else None,
|
||||
cls="p-0! min-h-0!",
|
||||
),
|
||||
Div(
|
||||
mk_icon(icon_dbengine, can_select=False), mk_ellipsis("DbEngine", cls="text-sm"),
|
||||
cls="collapse-title p-0 min-h-0 flex truncate",
|
||||
),
|
||||
Div(
|
||||
*[Div(user_id, **self.commands.db_engine_data(user_id)) for user_id in self.db_engine.debug_users()],
|
||||
Div("refs", **self.commands.db_engine_refs(None)),
|
||||
cls="collapse-content pr-0! truncate",
|
||||
),
|
||||
cls="collapse mb-2",
|
||||
id=f"db_engine_{self._id}",
|
||||
)
|
||||
content = [Div(user_id, **self.commands.db_engine_data(user_id)) for user_id in self.db_engine.debug_users()]
|
||||
content.append(Div("refs", **self.commands.db_engine_refs(None)))
|
||||
return mk_accordion_section(self._id, "DBEngine", icon_dbengine, content, selected)
|
||||
|
||||
def mk_ia(self, selected):
|
||||
ia_request = Div("requests", **self.commands.ia_buddy_requests())
|
||||
return mk_accordion_section(self._id, "IABuddy", icon_brain_ok, [ia_request], selected)
|
||||
|
||||
def _add_tab(self, tab_key, title, content):
|
||||
self.tabs_manager.add_tab(title, content, key=tab_key)
|
||||
return self.tabs_manager.render()
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
@@ -66,7 +110,7 @@ class Debugger(BaseComponent):
|
||||
mk_ellipsis("Debugger", cls="text-sm font-medium mb-1"),
|
||||
Div(
|
||||
self.mk_db_engine(True),
|
||||
cls="flex truncate",
|
||||
self.mk_ia(False),
|
||||
),
|
||||
|
||||
id=self._id,
|
||||
|
||||
@@ -9,6 +9,7 @@ from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.debugger.assets.icons import icon_expanded, icon_collapsed, icon_class
|
||||
from components.debugger.commands import JsonViewerCommands
|
||||
from components.debugger.constants import INDENT_SIZE, MAX_TEXT_LENGTH, NODE_OBJECT, NODES_KEYS_TO_NOT_EXPAND
|
||||
from components_helpers import set_boundaries
|
||||
from core.serializer import TAG_OBJECT
|
||||
from core.utils import get_unique_id
|
||||
|
||||
@@ -42,13 +43,31 @@ class DictNode(Node):
|
||||
children: dict[str, Node] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
class JsonViewerHelper:
|
||||
class_string = f"mmt-jsonviewer-string"
|
||||
class_bool = f"mmt-jsonviewer-bool"
|
||||
class_number = f"mmt-jsonviewer-number"
|
||||
class_null = f"mmt-jsonviewer-null"
|
||||
class_digest = f"mmt-jsonviewer-digest"
|
||||
class_object = f"mmt-jsonviewer-object"
|
||||
class_dataframe = f"mmt-jsonviewer-dataframe"
|
||||
|
||||
@staticmethod
|
||||
def is_sha256(_value):
|
||||
return (isinstance(_value, str) and
|
||||
len(_value) == 64 and
|
||||
all(c in '0123456789abcdefABCDEF' for c in _value))
|
||||
|
||||
|
||||
class JsonViewer(BaseComponent):
|
||||
def __init__(self, session, _id, owner, user_id, data):
|
||||
def __init__(self, session, _id, owner, user_id, data, hooks=None, key=None, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._key = key
|
||||
self._owner = owner # debugger component
|
||||
self.user_id = user_id
|
||||
self.data = data
|
||||
self._node_id = -1
|
||||
self._boundaries = boundaries if boundaries else {"height": "600"}
|
||||
self._commands = JsonViewerCommands(self)
|
||||
|
||||
# A little explanation on how the folding / unfolding work
|
||||
@@ -62,6 +81,12 @@ class JsonViewer(BaseComponent):
|
||||
self._nodes_by_id = {}
|
||||
|
||||
self.node = self._create_node(None, data)
|
||||
|
||||
# hooks are used to define specific rendering
|
||||
# They are tuple (Predicate, Element to render (eg Div))
|
||||
self.hooks = hooks or []
|
||||
|
||||
self._helper = JsonViewerHelper()
|
||||
|
||||
def set_node_folding(self, node_id, folding):
|
||||
if folding == self._folding_mode:
|
||||
@@ -84,7 +109,7 @@ class JsonViewer(BaseComponent):
|
||||
return self._owner
|
||||
|
||||
def open_digest(self, user_id: str, digest: str):
|
||||
return self._owner.add_tab(user_id, digest)
|
||||
return self._owner.db_engine_headers(user_id, digest)
|
||||
|
||||
def _create_node(self, key, data, level=0):
|
||||
if isinstance(data, list):
|
||||
@@ -115,17 +140,18 @@ class JsonViewer(BaseComponent):
|
||||
return node
|
||||
|
||||
def _must_expand(self, node):
|
||||
if not isinstance(node, (ListNode, DictNode)):
|
||||
return None
|
||||
|
||||
if self._folding_mode == FoldingMode.COLLAPSE:
|
||||
return node.node_id in self._nodes_to_track
|
||||
else:
|
||||
return node.node_id not in self._nodes_to_track
|
||||
|
||||
def _mk_folding(self, node: Node):
|
||||
if not isinstance(node, (ListNode, DictNode)):
|
||||
def _mk_folding(self, node: Node, must_expand: bool | None):
|
||||
if must_expand is None:
|
||||
return None
|
||||
|
||||
must_expand = self._must_expand(node)
|
||||
|
||||
return Span(icon_expanded if must_expand else icon_collapsed,
|
||||
cls="icon-16-inline mmt-jsonviewer-folding",
|
||||
style=f"margin-left: -{INDENT_SIZE}px;",
|
||||
@@ -136,15 +162,20 @@ class JsonViewer(BaseComponent):
|
||||
self._node_id += 1
|
||||
return f"{self._id}-{self._node_id}"
|
||||
|
||||
def _render_value(self, node):
|
||||
def _is_sha256(_value):
|
||||
return isinstance(_value, str) and len(_value) == 64 and all(
|
||||
c in '0123456789abcdefABCDEF' for c in _value)
|
||||
def _render_value(self, key, node, must_expand):
|
||||
if must_expand is False:
|
||||
return Span("[...]" if isinstance(node, ListNode) else "{...}",
|
||||
id=node.node_id,
|
||||
**self._commands.fold(node.node_id, FoldingMode.EXPAND))
|
||||
|
||||
for predicate, renderer in self.hooks:
|
||||
if predicate(key, node, self._helper):
|
||||
return renderer(key, node, self._helper)
|
||||
|
||||
if isinstance(node, DictNode):
|
||||
return self._render_dict(node)
|
||||
return self._render_dict(key, node)
|
||||
elif isinstance(node, ListNode):
|
||||
return self._render_list(node)
|
||||
return self._render_list(key, node)
|
||||
else:
|
||||
data_tooltip = None
|
||||
htmx_params = {}
|
||||
@@ -159,7 +190,7 @@ class JsonViewer(BaseComponent):
|
||||
elif node.value is None:
|
||||
str_value = "null"
|
||||
data_class = "null"
|
||||
elif _is_sha256(node.value):
|
||||
elif self._helper.is_sha256(node.value):
|
||||
str_value = str(node.value)
|
||||
data_class = "digest"
|
||||
htmx_params = self._commands.open_digest(self.user_id, node.value)
|
||||
@@ -194,24 +225,22 @@ class JsonViewer(BaseComponent):
|
||||
|
||||
return Span(str_value, cls=cls, data_tooltip=data_tooltip, **htmx_params)
|
||||
|
||||
def _render_dict(self, node: DictNode):
|
||||
if self._must_expand(node):
|
||||
return Span("{",
|
||||
*[
|
||||
self._render_node(key, value)
|
||||
for key, value in node.children.items()
|
||||
],
|
||||
Div("}"),
|
||||
id=node.node_id)
|
||||
else:
|
||||
return Span("{...}", id=node.node_id)
|
||||
def _render_dict(self, key, node: DictNode):
|
||||
return Span("{",
|
||||
*[
|
||||
self._render_node(child_key, value)
|
||||
for child_key, value in node.children.items()
|
||||
],
|
||||
Div("}"),
|
||||
id=node.node_id)
|
||||
|
||||
def _render_list(self, node: ListNode):
|
||||
def _all_the_same(_node):
|
||||
def _render_list(self, key, node: ListNode):
|
||||
def _all_the_same(_key, _node):
|
||||
if len(_node.children) == 0:
|
||||
return False
|
||||
|
||||
sample_value = _node.children[0].value
|
||||
sample_node = _node.children[0]
|
||||
sample_value = sample_node.value
|
||||
|
||||
if sample_value is None:
|
||||
return False
|
||||
@@ -220,6 +249,11 @@ class JsonViewer(BaseComponent):
|
||||
if type_ in (int, float, str, bool, list, dict, ValueNode):
|
||||
return False
|
||||
|
||||
# a specific rendering is specified
|
||||
for predicate, renderer in self.hooks:
|
||||
if predicate(_key, sample_node, self._helper):
|
||||
return False
|
||||
|
||||
return all(type(item.value) == type_ for item in _node.children)
|
||||
|
||||
def _render_as_grid(_node):
|
||||
@@ -244,27 +278,40 @@ class JsonViewer(BaseComponent):
|
||||
Div("]"),
|
||||
)
|
||||
|
||||
if self._must_expand(node):
|
||||
if _all_the_same(node):
|
||||
return _render_as_grid(node)
|
||||
return _render_as_list(node)
|
||||
else:
|
||||
return Span("[...]", id=node.node_id)
|
||||
return _render_as_grid(node) if _all_the_same(key, node) else _render_as_list(node)
|
||||
|
||||
def _render_node(self, key, node):
|
||||
must_expand = self._must_expand(node) # to be able to update the folding when the node is updated
|
||||
return Div(
|
||||
self._mk_folding(node),
|
||||
|
||||
self._mk_folding(node, must_expand),
|
||||
Span(f'{key} : ') if key is not None else None,
|
||||
self._render_value(node),
|
||||
self._render_value(key, node, must_expand),
|
||||
|
||||
style=f"margin-left: {INDENT_SIZE}px;",
|
||||
id=node.node_id if hasattr(node, "node_id") else None,
|
||||
)
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(self._render_node(None, self.node), id=f"{self._id}-root"),
|
||||
Div(self._render_node(None, self.node),
|
||||
id=f"{self._id}-root",
|
||||
style="margin-left: 0px;"),
|
||||
cls="mmt-jsonviewer",
|
||||
id=f"{self._id}")
|
||||
id=f"{self._id}",
|
||||
**set_boundaries(self._boundaries),
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is type(self):
|
||||
return self._key is not None and self._key == other._key
|
||||
else:
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._key) if self._key is not None else super().__hash__()
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def add_quotes(value: str):
|
||||
|
||||
@@ -10,5 +10,7 @@ NODES_KEYS_TO_NOT_EXPAND = ["Dataframe", "__parent__"]
|
||||
class Routes:
|
||||
DbEngineData = "/dbengine-data"
|
||||
DbEngineRefs = "/dbengine-refs"
|
||||
DbEngineDigest = "/dbengine-digest"
|
||||
IaBuddyRequests = "/iabuddy-requests"
|
||||
IaBuddyRequest = "/iabuddy-request"
|
||||
JsonViewerFold = "/jsonviewer-fold"
|
||||
JsonOpenDigest = "/jsonviewer-open-digest"
|
||||
|
||||
@@ -3,6 +3,9 @@ from fasthtml.xtend import Script
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.addstuff.components.AddStuffMenu import AddStuffMenu
|
||||
from components.admin.components.Admin import Admin
|
||||
from components.aibuddy.components.AIBuddy import AIBuddy
|
||||
from components.applications.components.Applications import Applications
|
||||
from components.debugger.components.Debugger import Debugger
|
||||
from components.drawerlayout.assets.icons import icon_panel_contract_regular, icon_panel_expand_regular
|
||||
from components.drawerlayout.constants import DRAWER_LAYOUT_INSTANCE_ID
|
||||
@@ -23,13 +26,22 @@ class DrawerLayout(BaseComponent):
|
||||
self._repositories = self._create_component(Repositories)
|
||||
self._debugger = self._create_component(Debugger)
|
||||
self._add_stuff = self._create_component(AddStuffMenu)
|
||||
self._ai_buddy = self._create_component(AIBuddy)
|
||||
self._admin = self._create_component(Admin)
|
||||
self._applications = self._create_component(Applications)
|
||||
|
||||
self.top_components = self._get_sub_components("TOP", [self._ai_buddy])
|
||||
self.bottom_components = self._get_sub_components("BOTTOM", [self._ai_buddy])
|
||||
|
||||
def __ft__(self):
|
||||
return Div(
|
||||
Div(
|
||||
Div(
|
||||
self._add_stuff,
|
||||
self._ai_buddy,
|
||||
self._applications,
|
||||
self._repositories,
|
||||
self._admin,
|
||||
self._debugger,
|
||||
),
|
||||
Div(cls="dl-splitter", id=f"splitter_{self._id}"),
|
||||
@@ -45,8 +57,10 @@ class DrawerLayout(BaseComponent):
|
||||
icon_panel_expand_regular,
|
||||
cls="swap",
|
||||
),
|
||||
|
||||
Div(*[component for component in self.top_components], name="top", cls='dl-top'),
|
||||
Div(self._tabs, id=f"page_{self._id}", name="page", cls='dl-page'),
|
||||
Div(*[component for component in self.bottom_components], name="bottom", cls='dl-bottom'),
|
||||
|
||||
cls='dl-main',
|
||||
tabindex="0",
|
||||
),
|
||||
@@ -64,3 +78,15 @@ class DrawerLayout(BaseComponent):
|
||||
@staticmethod
|
||||
def create_component_id(session, suffix: str = ""):
|
||||
return f"{DRAWER_LAYOUT_INSTANCE_ID}{session['user_id']}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def _get_sub_components(location, components):
|
||||
sub_components = []
|
||||
for component in components:
|
||||
if hasattr(component, "register_components"):
|
||||
sub_components.extend([
|
||||
sub_component for sub_component, loc in component.register_components()
|
||||
if loc == location
|
||||
])
|
||||
|
||||
return sub_components
|
||||
|
||||
0
src/components/hoildays/__init__.py
Normal file
0
src/components/hoildays/__init__.py
Normal file
0
src/components/hoildays/assets/Holidays.css
Normal file
0
src/components/hoildays/assets/Holidays.css
Normal file
0
src/components/hoildays/assets/__init__.py
Normal file
0
src/components/hoildays/assets/__init__.py
Normal file
8
src/components/hoildays/assets/icons.py
Normal file
8
src/components/hoildays/assets/icons.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
# Material - HolidayVillageTwotone
|
||||
icon_holidays = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
|
||||
<path opacity=".3" d="M8 6.83l-4 4V18h3v-3h2v3h3v-7.17l-4-4zM9 13H7v-2h2v2z" fill="currentColor"></path>
|
||||
<path d="M8 4l-6 6v10h12V10L8 4zm4 14H9v-3H7v3H4v-7.17l4-4l4 4V18zm-3-5H7v-2h2v2zm9 7V8.35L13.65 4h-2.83L16 9.18V20h2zm4 0V6.69L19.31 4h-2.83L20 7.52V20h2z" fill="currentColor"></path>
|
||||
</svg>
|
||||
""")
|
||||
0
src/components/hoildays/commands.py
Normal file
0
src/components/hoildays/commands.py
Normal file
33
src/components/hoildays/components/HolidaysViewer.py
Normal file
33
src/components/hoildays/components/HolidaysViewer.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.hoildays.constants import HOLIDAYS_VIEWER_INSTANCE_ID
|
||||
from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.hoildays.helpers.calendar_helper import CalendarHelper
|
||||
from components.hoildays.helpers.nibelisparser import OffPeriodDetails
|
||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
||||
from helpers.Datahelper import DataHelper
|
||||
|
||||
|
||||
class HolidaysViewer(BaseComponent):
|
||||
def __init__(self, session, _id, settings_manager, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._settings_manager = settings_manager
|
||||
self._boundaries = boundaries
|
||||
|
||||
def __ft__(self):
|
||||
records = DataHelper.get(self._session,
|
||||
self._settings_manager,
|
||||
USERS_REPOSITORY_NAME,
|
||||
HOLIDAYS_TABLE_NAME,
|
||||
OffPeriodDetails)
|
||||
names, holidays = CalendarHelper.create_calendar(records)
|
||||
calendar = DataGrid.new(self._session, holidays, index=names)
|
||||
return Div(
|
||||
calendar,
|
||||
cls="mt-2",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{HOLIDAYS_VIEWER_INSTANCE_ID}{session['user_id']}"
|
||||
0
src/components/hoildays/components/__init__.py
Normal file
0
src/components/hoildays/components/__init__.py
Normal file
1
src/components/hoildays/constants.py
Normal file
1
src/components/hoildays/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
HOLIDAYS_VIEWER_INSTANCE_ID = "__HolidaysViewer__"
|
||||
0
src/components/hoildays/helpers/__init__.py
Normal file
0
src/components/hoildays/helpers/__init__.py
Normal file
81
src/components/hoildays/helpers/calendar_helper.py
Normal file
81
src/components/hoildays/helpers/calendar_helper.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
from components.hoildays.helpers.nibelisparser import OffPeriodDetails
|
||||
|
||||
|
||||
class CalendarHelper:
|
||||
@staticmethod
|
||||
def create_calendar(records: list[OffPeriodDetails], start_date: date | None = None, end_date: date | None = None):
|
||||
"""
|
||||
|
||||
:param records:
|
||||
:param start_date:
|
||||
:param end_date:
|
||||
:return:
|
||||
"""
|
||||
# multiple algorithms possible. The chosen one
|
||||
# two steps
|
||||
# step one
|
||||
# create a dict [date, [name, holiday]] for all record in records
|
||||
# in the loop, records all the names
|
||||
#
|
||||
# step two
|
||||
# create a dict [date, list[holiday]] according to the sorted list names
|
||||
|
||||
# step 1
|
||||
temp = {}
|
||||
names = set()
|
||||
for record in records:
|
||||
full_name = record.first_name + " " + record.last_name
|
||||
names.add(full_name)
|
||||
duration = CalendarHelper.get_period(record.start_date, record.end_date)
|
||||
last_index = len(duration) - 1
|
||||
for index, current_date in enumerate(duration):
|
||||
calendar = temp.setdefault(current_date, {})
|
||||
if full_name in calendar:
|
||||
calendar[full_name].append(CalendarHelper.get_reason(record, index == 0, index == last_index))
|
||||
else:
|
||||
calendar[full_name] = [CalendarHelper.get_reason(record, index == 0, index == last_index)]
|
||||
|
||||
res = {}
|
||||
names = list(sorted(names))
|
||||
start_date_to_use = start_date or min(temp.keys())
|
||||
end_date_to_use = end_date or max(temp.keys())
|
||||
|
||||
for current_date in CalendarHelper.get_period(start_date_to_use, end_date_to_use):
|
||||
if current_date in temp:
|
||||
values = []
|
||||
res[current_date] = values
|
||||
for name in names:
|
||||
if name in temp[current_date]:
|
||||
values.append(temp[current_date][name])
|
||||
else:
|
||||
values.append(None)
|
||||
|
||||
else:
|
||||
res[current_date] = [None] * len(names)
|
||||
|
||||
return names, res
|
||||
|
||||
@staticmethod
|
||||
def get_reason(record: OffPeriodDetails, is_start, is_end):
|
||||
suffix = ""
|
||||
if is_start and record.start_am_pm:
|
||||
suffix += "_" + record.start_am_pm
|
||||
if is_end and record.end_am_pm:
|
||||
suffix += "_" + record.end_am_pm
|
||||
|
||||
return record.reason + suffix
|
||||
|
||||
@staticmethod
|
||||
def get_period(start_date: date, end_date: date):
|
||||
if end_date < start_date:
|
||||
raise ValueError("end date is before start date.")
|
||||
|
||||
current_date = start_date
|
||||
res = [current_date]
|
||||
while current_date < end_date:
|
||||
current_date = current_date + timedelta(days=1)
|
||||
res.append(current_date)
|
||||
|
||||
return res
|
||||
240
src/components/hoildays/helpers/nibelisparser.py
Normal file
240
src/components/hoildays/helpers/nibelisparser.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
from typing import Literal
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ParsingError:
|
||||
def __init__(self, reason: str, parser):
|
||||
self.reason = reason
|
||||
self.pos = parser.pos
|
||||
self.line = parser.line
|
||||
self.column = parser.column
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class OffPeriodDetails:
|
||||
first_name: str
|
||||
last_name: str
|
||||
start_date: datetime.date
|
||||
start_am_pm: Literal["am", "pm"] | None
|
||||
end_date: datetime.date
|
||||
end_am_pm: Literal["am", "pm"] | None
|
||||
total: float
|
||||
reason: str
|
||||
date_import: datetime.date
|
||||
|
||||
def get_key(self):
|
||||
return (f"{self.first_name}|{self.last_name}|"
|
||||
f"{self.start_date}|{self.start_am_pm}|"
|
||||
f"{self.end_date}|{self.end_am_pm}")
|
||||
|
||||
|
||||
class NibelisParser:
|
||||
def __init__(self, data: str):
|
||||
self.data = data
|
||||
self.pos = 0
|
||||
self.line = 1
|
||||
self.column = 1
|
||||
self.error_sink: list[ParsingError] = []
|
||||
|
||||
def parse(self):
|
||||
return self.read_input()
|
||||
|
||||
def read_input(self):
|
||||
# self.data = self.data.replace("\n\t\n", "\t\n")
|
||||
|
||||
while (line := self.read_line().rstrip("\t")) != "Demandes qualifiées" and line is not None:
|
||||
pass
|
||||
|
||||
res = []
|
||||
|
||||
try:
|
||||
while True:
|
||||
if (detail := self.read_detail()) is not None:
|
||||
res.append(detail)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
return res
|
||||
|
||||
def read_detail(self):
|
||||
start = self.read_line() # Détail de la demande
|
||||
if start is None:
|
||||
raise StopIteration()
|
||||
|
||||
if start.strip() == "" or start == "Demandes qualifiées":
|
||||
return None
|
||||
|
||||
if start != "Détail de la demande":
|
||||
self.error_sink.append(ParsingError("'Détail de la demande' not found.", self))
|
||||
return None
|
||||
|
||||
try:
|
||||
self.read_word() # ENGI402
|
||||
names = []
|
||||
while (token := self.read_word()) not in ["Le", "Du"]:
|
||||
names.append(token)
|
||||
first_name, last_name = self.get_first_name_last_name(names)
|
||||
|
||||
if token == "Le":
|
||||
start_date, start_am_pm, end_date, end_am_pm = self.read_one_date()
|
||||
else:
|
||||
start_date, start_am_pm, end_date, end_am_pm = self.read_period()
|
||||
total = self.read_total()
|
||||
reason = " ".join([self.read_word(), self.read_word()])
|
||||
self.read_line() # finish the line
|
||||
|
||||
return OffPeriodDetails(first_name,
|
||||
last_name,
|
||||
start_date,
|
||||
start_am_pm,
|
||||
end_date,
|
||||
end_am_pm,
|
||||
total,
|
||||
reason,
|
||||
datetime.date.today())
|
||||
|
||||
except Exception as ex:
|
||||
self.error_sink.append(ParsingError(str(ex), self))
|
||||
self.read_line() # finish the line
|
||||
return None
|
||||
|
||||
def read_one_date(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
# ... Le] lundi 20/05/2024 [0.50 J. ...
|
||||
# ... Le] jeudi 04/01/2024 après-midi exclu [0.50 J. ...
|
||||
# ... Le] lundi 29/01/2024 matin exclu [0.50 J. ...
|
||||
|
||||
self.read_word() # read day
|
||||
date_as_str = self.read_word()
|
||||
start_date = datetime.datetime.strptime(date_as_str, "%d/%m/%Y").date()
|
||||
end_date = start_date + datetime.timedelta(days=1)
|
||||
am_pm_as_str = self.read_until(lambda c: c.isdigit(), "").strip()
|
||||
am_pm = self.decode_am_pm(am_pm_as_str)
|
||||
return start_date, am_pm, end_date, am_pm
|
||||
|
||||
def read_period(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
# ... Du] <= lundi 27/05/2024 au mardi 28/05/2024 => [3.50 J. ..
|
||||
# ... Du] <= lundi 19/02/2024 matin inclus au mardi 20/02/2024 après-midi exclu => [3.50 J. ..
|
||||
# ... Du] <= mardi 20/02/2024 matin exclu au vendredi 23/02/2024 après-midi inclus => [3.50 J. ..
|
||||
self.read_word() # read day
|
||||
date_as_str = self.read_word()
|
||||
start_am_pm_content = self.read_words_until(["au"])
|
||||
self.read_word() # read day
|
||||
end_as_str = self.read_word()
|
||||
end_am_pm_content = self.read_until(lambda c: c.isdigit())
|
||||
|
||||
start_date = datetime.datetime.strptime(date_as_str, "%d/%m/%Y").date()
|
||||
end_date = datetime.datetime.strptime(end_as_str, "%d/%m/%Y").date()
|
||||
end_date = end_date + datetime.timedelta(days=1)
|
||||
start_am_pm = self.decode_am_pm(start_am_pm_content)
|
||||
end_am_pm = self.decode_am_pm(end_am_pm_content)
|
||||
|
||||
return start_date, start_am_pm, end_date, end_am_pm
|
||||
|
||||
def read_total(self):
|
||||
total_as_str = self.read_word()
|
||||
self.read_word() # parse J.
|
||||
return float(total_as_str)
|
||||
|
||||
def read_line(self, strip=False):
|
||||
return self._read_content(["\n"], strip=strip)
|
||||
|
||||
def read_word(self, strip=True):
|
||||
return self._read_content(["\n", "\t", " "], strip=strip)
|
||||
|
||||
def read_words_until(self, words: list):
|
||||
names = []
|
||||
while True:
|
||||
token = self.read_word()
|
||||
if token is None or token in words:
|
||||
break
|
||||
names.append(token)
|
||||
|
||||
return names
|
||||
|
||||
def read_until(self, predicate, default=None):
|
||||
if self.pos == len(self.data):
|
||||
return default
|
||||
|
||||
buffer = ""
|
||||
while self.pos < len(self.data) and not predicate(self.data[self.pos]):
|
||||
buffer += self.data[self.pos]
|
||||
self._move_forward()
|
||||
|
||||
return buffer
|
||||
|
||||
def _read_content(self, tokens: list, strip):
|
||||
if self.pos >= len(self.data):
|
||||
return None
|
||||
|
||||
buffer = ""
|
||||
while self.pos < len(self.data) and self.data[self.pos] not in tokens:
|
||||
buffer += self.data[self.pos]
|
||||
self._move_forward()
|
||||
|
||||
# eat the token
|
||||
self._move_forward()
|
||||
|
||||
if strip:
|
||||
while self.pos < len(self.data) and self.data[self.pos] in tokens:
|
||||
self._move_forward()
|
||||
|
||||
return buffer
|
||||
|
||||
def _move_forward(self):
|
||||
self.pos += 1
|
||||
try:
|
||||
if self.data[self.pos - 1] == "\n": # \n is a 'new line', so it's counted in the new line
|
||||
self.line += 1
|
||||
self.column = 1
|
||||
|
||||
else:
|
||||
self.column += 1
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_first_name_last_name(names: list[str]):
|
||||
if names[0][0].isdigit():
|
||||
names.pop(0) # sometimes, the code (ex PROD134) is in two parts
|
||||
|
||||
for i, name in enumerate(names):
|
||||
if name.isupper():
|
||||
break
|
||||
|
||||
return " ".join(names[:i]), " ".join(names[i:])
|
||||
|
||||
@staticmethod
|
||||
def decode_am_pm(input_):
|
||||
if not input_:
|
||||
return None
|
||||
|
||||
if isinstance(input_, list):
|
||||
input_ = " ".join(input_)
|
||||
else:
|
||||
input_ = input_.replace("\t", " ")
|
||||
input_ = input_.strip()
|
||||
input_ = " ".join(input_.split())
|
||||
|
||||
match input_:
|
||||
case "matin inclus":
|
||||
return "am"
|
||||
case "matin exclu":
|
||||
return "pm"
|
||||
case "après-midi inclus":
|
||||
return "pm"
|
||||
case "après-midi exclu":
|
||||
return "am"
|
||||
case _:
|
||||
return None
|
||||
@@ -1,5 +1,7 @@
|
||||
REPOSITORIES_INSTANCE_ID = "__Repositories__"
|
||||
ROUTE_ROOT = "/repositories"
|
||||
USERS_REPOSITORY_NAME = "__USERS___"
|
||||
HOLIDAYS_TABLE_NAME = "__HOLIDAYS__"
|
||||
|
||||
class Routes:
|
||||
AddRepository = "/add-repo"
|
||||
|
||||
@@ -97,6 +97,13 @@ class RepositoriesDbManager:
|
||||
self.settings_manager.save(self.session, REPOSITORIES_SETTINGS_ENTRY, settings)
|
||||
return repository
|
||||
|
||||
def exists_repository(self, repository_name):
|
||||
if repository_name is None or repository_name == "":
|
||||
raise ValueError("Repository name cannot be empty.")
|
||||
|
||||
settings = self._get_settings()
|
||||
return repository_name in [repo.name for repo in settings.repositories]
|
||||
|
||||
def get_repositories(self):
|
||||
return self._get_settings().repositories
|
||||
|
||||
@@ -141,6 +148,36 @@ class RepositoriesDbManager:
|
||||
repository.tables.remove(table_name)
|
||||
self.settings_manager.save(self.session, REPOSITORIES_SETTINGS_ENTRY, settings)
|
||||
|
||||
def exists_table(self, repository_name: str, table_name: str):
|
||||
if repository_name is None or repository_name == "":
|
||||
raise ValueError("Repository name cannot be empty.")
|
||||
|
||||
if table_name is None or table_name == "":
|
||||
raise ValueError("Table name cannot be empty.")
|
||||
|
||||
settings = self._get_settings()
|
||||
repository = next(filter(lambda r: r.name == repository_name, settings.repositories), None)
|
||||
if repository is None:
|
||||
return False
|
||||
|
||||
return table_name in repository.tables
|
||||
|
||||
def ensure_exists(self, repository_name: str, table_name: str):
|
||||
"""
|
||||
|
||||
:param repository_name:
|
||||
:param table_name:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if not self.exists_table(repository_name, table_name):
|
||||
self.add_table(repository_name, table_name)
|
||||
|
||||
except NameError:
|
||||
self.add_repository(repository_name, [table_name])
|
||||
|
||||
return repository_name, table_name
|
||||
|
||||
def select_repository(self, repository_name: str):
|
||||
"""
|
||||
Select and save the specified repository name in the current session's settings.
|
||||
@@ -172,12 +209,12 @@ class RepositoriesDbManager:
|
||||
settings = self._get_settings()
|
||||
repository = next(filter(lambda r: r.name == repository_name, settings.repositories), None)
|
||||
if repository is None:
|
||||
raise ValueError(f"Repository '{repository_name}' does not exist.")
|
||||
raise NameError(f"Repository '{repository_name}' does not exist.")
|
||||
|
||||
for table_name, must_exist in zip(tables_names, [t1_must_exists, t2_must_exists]):
|
||||
if must_exist:
|
||||
if table_name not in repository.tables:
|
||||
raise ValueError(f"Table '{table_name}' does not exist in repository '{repository_name}'.")
|
||||
raise NameError(f"Table '{table_name}' does not exist in repository '{repository_name}'.")
|
||||
else:
|
||||
if table_name in repository.tables:
|
||||
raise ValueError(f"Table '{table_name}' already exists in repository '{repository_name}'.")
|
||||
|
||||
@@ -59,6 +59,9 @@ class MyTabs(BaseComponent):
|
||||
if key in self.tabs_by_key:
|
||||
self.select_tab_by_id(self.tabs_by_key[key].id)
|
||||
|
||||
def get_tab_id(self, tab_key):
|
||||
return self.tabs_by_key[tab_key].id
|
||||
|
||||
def remove_tab(self, tab_id):
|
||||
"""
|
||||
Removes a tab with the specified ID from the current list of tabs.
|
||||
|
||||
Reference in New Issue
Block a user