feat: more functionality and suchh

This commit is contained in:
Yandrik 2025-03-26 18:00:07 +01:00
parent 5d9139bafb
commit a6f4cef792
5 changed files with 482 additions and 76 deletions

View File

@ -23,3 +23,20 @@ I always use uv to install stuff, not pip
use `uv add <package>` to install instead of `pip install <package>`. use `uv add <package>` to install instead of `pip install <package>`.
use `uv run <command>` to run commands, e.g. `uv run python hello.py`. use `uv run <command>` to run commands, e.g. `uv run python hello.py`.
## Coding
For dialogs and snackbars, do the following:
```python
dialog = < create dialog >
# to show
page.open(dialog)
# to close
page.close(dialog)
```
Do NOT use `page.show_snack_bar or page.dialog = ...` as those don't exist in recent Flet versions anymore.

108
anki.py
View File

@ -1,32 +1,44 @@
from result import Result, Ok, Err from result import Result, Ok, Err
import requests import requests
import logging import logging
from typing import List, Dict from typing import List, Dict, Tuple, Any
logger = logging.getLogger("flashcard-creator") logger = logging.getLogger("flashcard-creator")
class AnkiError(Exception): class AnkiError(Exception):
"""Base class for Anki-related errors""" """Base class for Anki-related errors"""
pass pass
class ConnectionError(AnkiError): class ConnectionError(AnkiError):
"""Error connecting to AnkiConnect""" """Error connecting to AnkiConnect"""
pass pass
class DeckError(AnkiError): class DeckError(AnkiError):
"""Error related to deck operations""" """Error related to deck operations"""
pass pass
class CardError(AnkiError): class CardError(AnkiError):
"""Error related to card operations""" """Error related to card operations"""
pass pass
class AnkiConnect: class AnkiConnect:
def __init__(self, url="http://127.0.0.1:8765"): def __init__(self, url="http://127.0.0.1:8765"):
self.url = url self.url = url
def get_decks(self) -> Result[List[str], AnkiError]: def get_decks(self) -> Result[List[str], AnkiError]:
"""Get the list of decks from Anki using AnkiConnect.""" """
Get the list of decks from Anki using AnkiConnect.
Returns Ok(list_of_deck_names) on success, Err(AnkiError) on failure.
"""
try: try:
response = requests.post( response = requests.post(
self.url, json={"action": "deckNames", "version": 6} self.url, json={"action": "deckNames", "version": 6}
@ -40,37 +52,71 @@ class AnkiConnect:
logger.error(f"Error getting Anki decks: {e}") logger.error(f"Error getting Anki decks: {e}")
return Err(DeckError(f"Failed to get decks: {e}")) return Err(DeckError(f"Failed to get decks: {e}"))
def create_cards(self, deck: str, cards: List[Dict[str, str]]) -> Result[bool, AnkiError]: def create_cards(
"""Create flashcards in Anki using AnkiConnect.""" self,
try: deck: str,
# Prepare notes for addition cards: List[Dict[str, str]],
notes = [] source_language: str,
logger.info(f"Preparing to create {len(cards)} Anki cards in deck: {deck}") target_language: str,
for card in cards: ) -> Result[Tuple[int, List[Dict[str, Any]]], AnkiError]:
notes.append( """Create flashcards in Anki using AnkiConnect, adding each note one by one.
{
"deckName": deck,
"modelName": "Basic", # Using the basic model
"fields": {"Front": card["front"], "Back": card["back"]},
"options": {"allowDuplicate": False},
"tags": [f"{self.source_language}-{self.target_language}"],
}
)
# Add notes to Anki Returns:
logger.info("Sending request to AnkiConnect to add notes") Ok((successful_count, failed_cards_list)): Where failed_cards_list contains
response = requests.post( dictionaries like {'card': original_card_dict, 'error': error_message}.
self.url, Err(ConnectionError): If connection to AnkiConnect fails.
json={"action": "addNotes", "version": 6, "params": {"notes": notes}}, Err(CardError): If a non-connection error occurs during the process.
) """
data = response.json() try:
success = all(id is not None for id in data.get("result", [])) logger.info(f"Preparing to create {len(cards)} Anki cards in deck: {deck}")
if success:
logger.info("Successfully added all cards to Anki") failed_cards = []
return Ok(True) successful_count = 0
# Add each note individually
for card in cards:
note = {
"deckName": deck,
"modelName": "Basic", # Using the basic model
"fields": {"Front": card["front"], "Back": card["back"]},
"options": {"allowDuplicate": False},
"tags": [f"{source_language}-{target_language}"],
}
# Send request for a single note
response = requests.post(
self.url,
json={"action": "addNote", "version": 6, "params": {"note": note}},
)
data = response.json()
# Check if the note was added successfully
if data.get("error") is not None or data.get("result") is None:
error_msg = data.get("error", "Unknown error")
failed_cards.append({"card": card, "error": error_msg})
logger.warning(
f"Failed to add card: {card['front']} - Error: {error_msg}"
)
else:
successful_count += 1
logger.debug(f"Successfully added card: {card['front']}")
# Report results
if failed_cards:
logger.warning(
f"Some cards failed to be added to Anki. Added {successful_count}/{len(cards)}"
)
elif successful_count > 0:
logger.info(f"Successfully added all {successful_count} cards to Anki")
else: else:
logger.warning("Some cards failed to be added to Anki") logger.info(
return Err(CardError("Some cards failed to be added")) "No cards were added."
) # Should ideally not happen if input list wasn't empty
# Always return Ok with success count and failed list (which might be empty)
# The caller can decide if the presence of failed_cards constitutes an overall "error" state for the UI
return Ok((successful_count, failed_cards))
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error(f"Connection error creating Anki cards: {e}") logger.error(f"Connection error creating Anki cards: {e}")
return Err(ConnectionError(f"Failed to connect to Anki: {e}")) return Err(ConnectionError(f"Failed to connect to Anki: {e}"))

412
main.py
View File

@ -6,7 +6,9 @@ import os
import csv import csv
import logging import logging
from typing import Dict, List, Any from typing import Dict, List, Any
from result import Ok from result import Ok, Result
from anki import AnkiError
# Set up logging # Set up logging
logging.basicConfig( logging.basicConfig(
@ -19,11 +21,14 @@ class FlashcardCreator:
def __init__(self): def __init__(self):
# Anki configuration # Anki configuration
from anki import AnkiConnect from anki import AnkiConnect
self.anki = AnkiConnect() self.anki = AnkiConnect()
# File picker # File picker
self.file_picker = ft.FilePicker() self.file_picker = ft.FilePicker()
self.is_processing = False
# LiteLLM configuration # LiteLLM configuration
self.llm_provider = "openai" self.llm_provider = "openai"
self.llm_model = "gpt-3.5-turbo" self.llm_model = "gpt-3.5-turbo"
@ -144,19 +149,25 @@ class FlashcardCreator:
width=page.width, # Make table as wide as the window width=page.width, # Make table as wide as the window
) )
# Initialize the flashcard table with the "add new card" row
self.add_new_card_row()
# Wrap the table in a Container with specific height to ensure visibility # Wrap the table in a Container with specific height to ensure visibility
flashcard_table_container = ft.Container( flashcard_table_container = ft.Container(
content=ft.Column( content=ft.Row(
controls=[
ft.Column(
expand=True,
controls=[self.flashcard_table],
scroll=ft.ScrollMode.ALWAYS,
)
],
expand=True, expand=True,
controls=[self.flashcard_table],
scroll=ft.ScrollMode.ALWAYS,
), ),
padding=10, padding=10,
expand=True, expand=True,
# height=400,
border=ft.border.all(1, ft.colors.OUTLINE), border=ft.border.all(1, ft.colors.OUTLINE),
border_radius=5, border_radius=5,
width=page.width,
) )
# Add file picker to page overlay # Add file picker to page overlay
@ -230,6 +241,8 @@ class FlashcardCreator:
"base_url": self.base_url, "base_url": self.base_url,
"api_key": self.api_key, "api_key": self.api_key,
} }
# Save the currently selected provider
settings["provider"] = self.llm_provider
with open(settings_path, "w") as f: with open(settings_path, "w") as f:
json.dump(settings, f, indent=4) json.dump(settings, f, indent=4)
@ -237,6 +250,33 @@ class FlashcardCreator:
e.page.close(dialog) e.page.close(dialog)
e.page.update() e.page.update()
# Function to update displayed settings when provider changes
def on_provider_change(e):
selected_provider = provider_dropdown.value
# Load settings from file
settings_path = os.path.join(
os.path.expanduser("~"), ".flashcard_creator", "settings.json"
)
settings = {}
if os.path.exists(settings_path):
try:
with open(settings_path, "r") as f:
settings = json.load(f)
except:
pass
# Get selected provider's settings or empty dict if not found
provider_settings = settings.get(selected_provider, {})
# Update the input fields with the provider's settings
model_input.value = provider_settings.get("model", "")
base_url_input.value = provider_settings.get("base_url", "")
api_key_input.value = provider_settings.get("api_key", "")
# Update the dialog
e.page.update()
# Load settings if they exist # Load settings if they exist
settings_path = os.path.join( settings_path = os.path.join(
os.path.expanduser("~"), ".flashcard_creator", "settings.json" os.path.expanduser("~"), ".flashcard_creator", "settings.json"
@ -253,7 +293,8 @@ class FlashcardCreator:
self.llm_model = provider_settings.get("model", self.llm_model) self.llm_model = provider_settings.get("model", self.llm_model)
self.base_url = provider_settings.get("base_url", self.base_url) self.base_url = provider_settings.get("base_url", self.base_url)
self.api_key = provider_settings.get("api_key", self.api_key) self.api_key = provider_settings.get("api_key", self.api_key)
except: except e:
logger.info(f"Settings couldn't be loaded: {e}")
pass pass
provider_dropdown = ft.Dropdown( provider_dropdown = ft.Dropdown(
@ -266,6 +307,7 @@ class FlashcardCreator:
ft.dropdown.Option("deepseek"), ft.dropdown.Option("deepseek"),
], ],
value=self.llm_provider, value=self.llm_provider,
on_change=on_provider_change, # Add the on_change handler
) )
model_input = ft.TextField(label="Model Name", value=self.llm_model) model_input = ft.TextField(label="Model Name", value=self.llm_model)
@ -287,11 +329,16 @@ class FlashcardCreator:
) )
e.page.open(dialog) e.page.open(dialog)
# e.page.dialog = dialog
# dialog.open = True
e.page.update() e.page.update()
def on_process_text(self, e): def on_process_text(self, e):
# Prevent multiple simultaneous processing
if self.is_processing:
e.page.open(
ft.SnackBar(content=ft.Text("Processing in progress, please wait..."))
)
return
input_text = self.input_text.value input_text = self.input_text.value
if not input_text: if not input_text:
e.page.open( e.page.open(
@ -301,11 +348,21 @@ class FlashcardCreator:
logger.info("Processing input text") logger.info("Processing input text")
# Use LiteLLM to process the text and generate flashcards self.is_processing = True
flashcards = self.generate_flashcards(input_text, e.page) self.process_button.disabled = True
e.page.update()
# Update the data table with the generated flashcards try:
self.update_flashcard_table(flashcards, e.page) # Use LiteLLM to process the text and generate flashcards
flashcards = self.generate_flashcards(input_text, e.page)
# Update the data table with the generated flashcards
self.update_flashcard_table(flashcards, e.page)
finally:
# Reset processing state and re-enable button
self.is_processing = False
self.process_button.disabled = False
e.page.update()
def generate_flashcards(self, text: str, page: ft.Page = None) -> Dict: def generate_flashcards(self, text: str, page: ft.Page = None) -> Dict:
"""Use LiteLLM to generate flashcards from the input text.""" """Use LiteLLM to generate flashcards from the input text."""
@ -314,9 +371,17 @@ class FlashcardCreator:
# Show progress bar # Show progress bar
progress = None progress = None
if page: if page:
progress = ft.ProgressBar(width=600) progress = ft.ProgressBar(width=max(page.width - 40, 200))
page.add( page.add(
ft.Column([ft.Text("Generating flashcards...", size=16), progress]) ft.Column(
[
ft.Text(
f"Generating flashcards using {self.llm_provider}/{self.llm_model}",
size=16,
),
progress,
]
)
) )
page.update() page.update()
@ -432,6 +497,12 @@ class FlashcardCreator:
return flashcards return flashcards
except Exception as e: except Exception as e:
logger.error(f"Error generating flashcards: {e}", exc_info=True) logger.error(f"Error generating flashcards: {e}", exc_info=True)
if page:
page.open(
ft.SnackBar(
content=ft.Text(f"Error generating flashcards: {str(e)}")
)
)
return {"name": "", "description": "", "cards": []} return {"name": "", "description": "", "cards": []}
finally: finally:
# Remove progress bar # Remove progress bar
@ -561,14 +632,19 @@ class FlashcardCreator:
e.page.update() e.page.update()
return return
# Add a new row with the entered values # remove add_new_card row
if self.flashcard_table.rows:
self.flashcard_table.rows.pop()
# add card to table
self.add_row_to_table(front_field.value, back_field.value) self.add_row_to_table(front_field.value, back_field.value)
# Clear the input fields # re-add add_new_card_row to the end
self.add_new_card_row()
front_field.value = "" front_field.value = ""
back_field.value = "" back_field.value = ""
# Update the page
e.page.update() e.page.update()
def add_to_anki(self, e): def add_to_anki(self, e):
@ -609,21 +685,80 @@ class FlashcardCreator:
e.page.open(ft.SnackBar(content=ft.Text("Please select a deck"))) e.page.open(ft.SnackBar(content=ft.Text("Please select a deck")))
return return
success = self.create_anki_cards(selected_deck, cards) # Close the deck selection dialog first
e.page.close(dialog) e.page.close(dialog)
e.page.update() # Update to ensure dialog is closed visually
# Show a progress indicator while adding cards
progress_ring = ft.ProgressRing(width=16, height=16, stroke_width=2)
progress_snackbar = ft.SnackBar(
content=ft.Row(
[
progress_ring,
ft.Text(f"Adding cards to {selected_deck}... Please wait."),
]
),
open=True,
duration=None, # Keep open until manually closed or replaced
)
e.page.open(progress_snackbar)
e.page.update() e.page.update()
if success: try:
e.page.open( result = self.create_anki_cards(selected_deck, cards)
ft.SnackBar(
content=ft.Text( # Close the progress snackbar
f"Added {len(cards)} flashcards to {selected_deck}" e.page.close(progress_snackbar)
e.page.update()
if result.is_ok():
successful_count, failed_cards_list = result.ok_value
total_attempted = len(cards)
if not failed_cards_list:
# All cards added successfully
e.page.open(
ft.SnackBar(
content=ft.Text(
f"Successfully added {successful_count}/{total_attempted} flashcards to {selected_deck}"
),
open=True,
)
)
else:
# Some cards failed, show the failure dialog
logger.warning(
f"Failed to add {len(failed_cards_list)} cards. Opening retry dialog."
)
self.show_failed_cards_dialog(
e.page,
selected_deck,
failed_cards_list,
successful_count,
total_attempted,
)
else: # Handle Err case from create_anki_cards
e.page.open(
ft.SnackBar(
content=ft.Text(
f"An error occurred while adding cards to Anki: {result.err_value}"
),
open=True,
) )
) )
except Exception as ex: # Catch any unexpected errors during the process
# Close the progress snackbar in case of an error
e.page.close(progress_snackbar)
e.page.update()
logger.error(
f"Unexpected error during Anki card addition: {ex}", exc_info=True
) )
else:
e.page.open( e.page.open(
ft.SnackBar(content=ft.Text("Failed to add flashcards to Anki")) ft.SnackBar(
content=ft.Text(f"An unexpected error occurred: {ex}"),
open=True,
)
) )
dialog = ft.AlertDialog( dialog = ft.AlertDialog(
@ -643,20 +778,22 @@ class FlashcardCreator:
def get_anki_decks(self) -> List[str]: def get_anki_decks(self) -> List[str]:
"""Get the list of decks from Anki using AnkiConnect.""" """Get the list of decks from Anki using AnkiConnect."""
result = self.anki.get_decks() result = self.anki.get_decks()
if isinstance(result, Ok): if result.is_ok():
return result.value return result.ok_value
else: else:
logger.error(f"Error getting Anki decks: {result.value}") logger.error(f"Error getting Anki decks: {result.err_value}")
return [] return []
def create_anki_cards(self, deck: str, cards: List[Dict[str, str]]) -> bool: def create_anki_cards(
self, deck: str, cards: List[Dict[str, str]]
) -> Result[None, AnkiError]:
"""Create flashcards in Anki using Anki-connect.""" """Create flashcards in Anki using Anki-connect."""
result = self.anki.create_cards(deck, cards) result = self.anki.create_cards(
if isinstance(result, Ok): deck, cards, self.source_language, self.target_language
return result.value )
else: if result.is_err():
logger.error(f"Error creating Anki cards: {result.value}") logger.error(f"Error creating Anki cards: {result.err_value}")
return False return result
def export_as_yaml(self, e): def export_as_yaml(self, e):
"""Export the flashcards as a YAML file.""" """Export the flashcards as a YAML file."""
@ -735,6 +872,209 @@ class FlashcardCreator:
allowed_extensions=["csv"], allowed_extensions=["csv"],
) )
# --- Methods for Handling Failed Anki Card Additions ---
def _build_failed_card_row(
self, page: ft.Page, card_data: Dict[str, Any]
) -> ft.Row:
"""Builds a Flet Row for displaying a failed card with editable fields."""
front_field = ft.TextField(
value=card_data["card"]["front"],
width=300,
multiline=True,
min_lines=1,
max_lines=3,
)
back_field = ft.TextField(
value=card_data["card"]["back"],
width=300,
multiline=True,
min_lines=1,
max_lines=3,
)
error_text = ft.Text(
f"Error: {card_data['error']}", color=ft.colors.ERROR, size=12
)
# Store references to the fields within the row for later access
row = ft.Row(
controls=[
ft.Column([front_field], width=310), # Column to constrain width
ft.Column([back_field], width=310), # Column to constrain width
ft.Column(
[error_text], expand=True, alignment=ft.MainAxisAlignment.CENTER
),
],
alignment=ft.MainAxisAlignment.START,
)
# Attach fields and original data to the row object itself for easy retrieval
row.front_field = front_field
row.back_field = back_field
row.card_data = card_data
return row
def show_failed_cards_dialog(
self,
page: ft.Page,
deck: str,
failed_cards: List[Dict[str, Any]],
initial_success_count: int,
total_attempted: int,
):
"""Displays a dialog with failed Anki cards, allowing editing and retrying."""
failed_card_rows = [
self._build_failed_card_row(page, card_data) for card_data in failed_cards
]
# Use ListView for scrollability if many cards fail
content_list = ft.ListView(controls=failed_card_rows, expand=True, spacing=10)
dialog_title = ft.Text(f"Failed Cards for Deck: {deck}")
status_text = ft.Text(
f"{initial_success_count}/{total_attempted} added initially. {len(failed_cards)} failed."
)
# --- Retry Logic ---
def _retry_failed_cards(e):
cards_to_retry = []
for (
row
) in content_list.controls: # Access controls directly from the ListView
if hasattr(row, "front_field") and hasattr(
row, "back_field"
): # Ensure it's a card row
cards_to_retry.append(
{"front": row.front_field.value, "back": row.back_field.value}
)
if not cards_to_retry:
page.open(
ft.SnackBar(
content=ft.Text("No cards remaining to retry."), open=True
)
)
return
# Disable button during retry
retry_button.disabled = True
page.update()
# Show progress in dialog (optional, could use snackbar again)
# status_text.value = "Retrying..."
# page.update()
try:
retry_result = self.create_anki_cards(deck, cards_to_retry)
if retry_result.is_ok():
retry_successful_count, still_failed_list = retry_result.ok_value
new_total_successful = (
initial_success_count + retry_successful_count
)
# Update the dialog content: keep only the ones that *still* failed
new_failed_rows = []
still_failed_cards_data = {
tuple(fc["card"].items()): fc for fc in still_failed_list
} # For quick lookup
updated_failed_cards_list = [] # Keep track of data for next retry
for row in content_list.controls:
if hasattr(row, "card_data"):
original_card_tuple = tuple(row.card_data["card"].items())
if original_card_tuple in still_failed_cards_data:
# Update error message if it changed (though unlikely for duplicates)
row.card_data["error"] = still_failed_cards_data[
original_card_tuple
]["error"]
# Rebuild or update the error text in the row if needed
# For simplicity, we'll just keep the row
new_failed_rows.append(row)
updated_failed_cards_list.append(row.card_data)
content_list.controls = (
new_failed_rows # Replace controls in ListView
)
status_text.value = f"{new_total_successful}/{total_attempted} added. {len(new_failed_rows)} still failing."
if not new_failed_rows:
# All retried cards were successful! Close dialog.
page.close(dialog)
page.open(
ft.SnackBar(
content=ft.Text(
f"All cards successfully added! ({new_total_successful}/{total_attempted})"
),
open=True,
)
)
else:
# Some still failed, update dialog view
page.open(
ft.SnackBar(
content=ft.Text(
f"Successfully retried {retry_successful_count} cards. {len(new_failed_rows)} still failed."
),
open=True,
)
)
else: # Handle Err from retry attempt
status_text.value = f"Retry Error: {retry_result.err_value}"
page.open(
ft.SnackBar(
content=ft.Text(
f"Error during retry: {retry_result.err_value}"
),
open=True,
)
)
except Exception as ex:
logger.error(
f"Unexpected error during Anki card retry: {ex}", exc_info=True
)
status_text.value = f"Unexpected Retry Error: {ex}"
page.open(
ft.SnackBar(
content=ft.Text(
f"An unexpected error occurred during retry: {ex}"
),
open=True,
)
)
finally:
# Re-enable button
retry_button.disabled = (
False if content_list.controls else True
) # Disable if list is empty
page.update() # Update UI (dialog content, status text, button state)
# --- Dialog Buttons ---
retry_button = ft.TextButton(
"Retry Failed Cards",
on_click=_retry_failed_cards,
disabled=not failed_card_rows,
)
close_button = ft.TextButton("Close", on_click=lambda e: page.close(dialog))
dialog = ft.AlertDialog(
modal=True,
title=dialog_title,
content=ft.Container( # Use Container for better sizing control
content=ft.Column([status_text, content_list], tight=True),
width=max(page.width * 0.8, 700), # Make dialog wider
height=max(page.height * 0.7, 400), # Make dialog taller
padding=10,
),
actions=[retry_button, close_button],
actions_alignment=ft.MainAxisAlignment.END,
)
page.open(dialog)
page.update()
def main(page: ft.Page): def main(page: ft.Page):
app = FlashcardCreator() app = FlashcardCreator()

View File

@ -7,5 +7,5 @@ requires-python = ">=3.12"
dependencies = [ dependencies = [
"flet[all]>=0.27.6", "flet[all]>=0.27.6",
"litellm>=1.63.12", "litellm>=1.63.12",
"logging>=0.4.9.6", "result>=0.17.0",
] ]

19
uv.lock
View File

@ -574,12 +574,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/01/6f2fb287b93144851bfd442b265a67933bbd677e6dcbfa9951063199cf94/litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e", size = 6954945 }, { url = "https://files.pythonhosted.org/packages/e4/01/6f2fb287b93144851bfd442b265a67933bbd677e6dcbfa9951063199cf94/litellm-1.63.12-py3-none-any.whl", hash = "sha256:ae72a9d7099100b4b1172aaa2954bf6d7b205d47ba76beec5cd53f62dd57913e", size = 6954945 },
] ]
[[package]]
name = "logging"
version = "0.4.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/93/4b/979db9e44be09f71e85c9c8cfc42f258adfb7d93ce01deed2788b2948919/logging-0.4.9.6.tar.gz", hash = "sha256:26f6b50773f085042d301085bd1bf5d9f3735704db9f37c1ce6d8b85c38f2417", size = 96029 }
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" version = "3.0.0"
@ -646,14 +640,14 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "flet", extra = ["all"] }, { name = "flet", extra = ["all"] },
{ name = "litellm" }, { name = "litellm" },
{ name = "logging" }, { name = "result" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "flet", extras = ["all"], specifier = ">=0.27.6" }, { name = "flet", extras = ["all"], specifier = ">=0.27.6" },
{ name = "litellm", specifier = ">=1.63.12" }, { name = "litellm", specifier = ">=1.63.12" },
{ name = "logging", specifier = ">=0.4.9.6" }, { name = "result", specifier = ">=0.17.0" },
] ]
[[package]] [[package]]
@ -1027,6 +1021,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
] ]
[[package]]
name = "result"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/47/2175be65744aa4d8419c27bd3a7a7d65af5bcad7a4dc6a812c00778754f0/result-0.17.0.tar.gz", hash = "sha256:b73da420c0cb1a3bf741dbd41ff96dedafaad6a1b3ef437a9e33e380bb0d91cf", size = 20180 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/90/19110ce9374c3db619e2df0816f2c58e4ddc5cdad5f7284cd81d8b30b7cb/result-0.17.0-py3-none-any.whl", hash = "sha256:49fd668b4951ad15800b8ccefd98b6b94effc789607e19c65064b775570933e8", size = 11689 },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "13.9.4" version = "13.9.4"