Compare commits

..
Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda b32a068fb9 Remove the unused mock services 2025-07-24 21:08:28 -03:00
Gustavo Henrique Miranda 447dffd7df Merge pull request #81 from gmbrax/refactor/clean-the-ruff-errors
Fix all the Ruff errors
2025-07-24 20:26:17 -03:00
Gustavo Henrique Santos Souza de Miranda 2b79b08df0 Fix all the Ruff errors 2025-07-24 20:19:09 -03:00
Gustavo Henrique Miranda 48b3ec5fd1 Merge pull request #80 from gmbrax/feat/backup-database
add the backup feature
2025-07-24 19:59:15 -03:00
Gustavo Henrique Santos Souza de Miranda c3397ab982 Remove the wrong error message and made the program exit if no session is found 2025-07-24 19:36:12 -03:00
Gustavo Henrique Mirandaandcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> bb9457978c Update src/pilgrim/service/backup_service.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-24 19:31:13 -03:00
Gustavo Henrique Santos Souza de Miranda c09425cd48 Merge remote-tracking branch 'origin/feat/backup-database' into feat/backup-database 2025-07-24 19:23:59 -03:00
Gustavo Henrique Mirandaandcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> 7c950b93ab Update src/pilgrim/ui/screens/diary_list_screen.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-24 19:23:40 -03:00
Gustavo Henrique Santos Souza de Miranda 17371088da Changed the way it uses the database connection to be more robust 2025-07-24 19:21:47 -03:00
Gustavo Henrique Santos Souza de Miranda 209099cc5e Add a error handling on backup operation 2025-07-24 19:05:55 -03:00
Gustavo Henrique Santos Souza de Miranda 890fc470bb Change the filename variable to conform to the proper pathlike use 2025-07-24 19:00:04 -03:00
16 changed files with 24 additions and 298 deletions
-2
View File
@@ -1,9 +1,7 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
from pathlib import Path
import os
import shutil
from pilgrim.utils import ConfigManager
+7 -6
View File
@@ -1,6 +1,4 @@
import sqlite3
import zipfile
from pathlib import Path
from pilgrim.utils.directory_manager import DirectoryManager
@@ -14,9 +12,12 @@ class BackupService:
db_path = DirectoryManager.get_database_path()
if not db_path.exists():
raise FileNotFoundError("No Database Found")
conn = self.session.connection().connection
dump = "\n".join(line for line in conn.iterdump())
filename = str(DirectoryManager.get_config_directory()) + "/backup.zip"
with self.session.connection() as conn:
raw_conn = conn.connection
dump = "\n".join(line for line in raw_conn.iterdump())
filename = DirectoryManager.get_config_directory() / "backup.zip"
diaries_root_path = DirectoryManager.get_diaries_root()
try:
@@ -27,7 +28,7 @@ class BackupService:
if file_path.is_file():
arcname = file_path.relative_to(diaries_root_path.parent)
zipf.write(file_path, arcname=arcname)
return True, None
return True, filename
except Exception as e:
return False, str(e)
@@ -1,123 +0,0 @@
from typing import List, Tuple
import asyncio
from pilgrim.service.entry_service import EntryService
from pilgrim.models.entry import Entry
class EntryServiceMock(EntryService):
def __init__(self):
super().__init__(None)
self.mock_data = {
1: Entry(title="The Adventure Begins", text="I'm hopping in the Plane to finally visit canadian lands",
date="26/07/2025", travel_diary_id=1, id=1,
photos=[]),
2: Entry(title="The Landing", text="Finally on Canadian Soil", date="27/07/2025",
travel_diary_id=1, id=2,photos=[]),
3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025",
travel_diary_id=1, id=3, photos=[]),
4: Entry(title="Old Montreal", text="Exploring the historic district", date="29/07/2025",
travel_diary_id=1, id=4, photos=[]),
5: Entry(title="Notre-Dame Basilica", text="Beautiful architecture", date="30/07/2025",
travel_diary_id=1, id=5, photos=[]),
6: Entry(title="Parc Jean-Drapeau", text="Great views of the city", date="31/07/2025",
travel_diary_id=1, id=6, photos=[]),
7: Entry(title="La Ronde", text="Amusement park fun", date="01/08/2025",
travel_diary_id=1, id=7, photos=[]),
8: Entry(title="Biodome", text="Nature and science", date="02/08/2025",
travel_diary_id=1, id=8, photos=[]),
9: Entry(title="Botanical Gardens", text="Peaceful walk", date="03/08/2025",
travel_diary_id=1, id=9, photos=[]),
10: Entry(title="Olympic Stadium", text="Historic venue", date="04/08/2025",
travel_diary_id=1, id=10, photos=[]),
}
self._next_id = 11
# Synchronous methods (kept for compatibility)
def create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry:
"""Synchronous version"""
new_entry = Entry(title, text, date, travel_diary_id, id=self._next_id)
self.mock_data[self._next_id] = new_entry
self._next_id += 1
return new_entry
def read_by_id(self, entry_id: int) -> Entry | None:
"""Synchronous version"""
return self.mock_data.get(entry_id)
def read_all(self) -> List[Entry]:
"""Synchronous version"""
return list(self.mock_data.values())
def read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]:
"""Synchronous version - reads entries by diary"""
return [entry for entry in self.mock_data.values() if entry.fk_travel_diary_id == travel_diary_id]
def read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]:
"""Synchronous version - reads paginated entries by diary"""
entries = self.read_by_travel_diary_id(travel_diary_id)
entries.sort(key=lambda x: x.id, reverse=True) # Most recent first
total_entries = len(entries)
total_pages = (total_entries + page_size - 1) // page_size
start_index = (page - 1) * page_size
end_index = start_index + page_size
page_entries = entries[start_index:end_index]
return page_entries, total_pages, total_entries
def update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None:
"""Synchronous version"""
item_to_update = self.mock_data.get(entry_src.id)
if item_to_update:
item_to_update.title = entry_dst.title if entry_dst.title is not None else item_to_update.title
item_to_update.text = entry_dst.text if entry_dst.text is not None else item_to_update.text
item_to_update.date = entry_dst.date if entry_dst.date is not None else item_to_update.date
item_to_update.fk_travel_diary_id = entry_dst.fk_travel_diary_id if (entry_dst.fk_travel_diary_id
is not None) else entry_dst.id
item_to_update.photos.extend(entry_dst.photos)
return item_to_update
return None
def delete(self, entry_src: Entry) -> Entry | None:
"""Synchronous version"""
return self.mock_data.pop(entry_src.id, None)
# Async methods (main)
async def async_create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry:
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.create(travel_diary_id, title, text, date)
async def async_read_by_id(self, entry_id: int) -> Entry | None:
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_by_id(entry_id)
async def async_read_all(self) -> List[Entry]:
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_all()
async def async_read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]:
"""Async version - reads entries by diary"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_by_travel_diary_id(travel_diary_id)
async def async_read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]:
"""Async version - reads paginated entries by diary"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_paginated(travel_diary_id, page, page_size)
async def async_update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None:
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.update(entry_src, entry_dst)
async def async_delete(self, entry_src: Entry) -> Entry | None:
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.delete(entry_src)
@@ -1,48 +0,0 @@
from pathlib import Path
from typing import List
from pilgrim.models.photo import Photo
from pilgrim.service.photo_service import PhotoService
class PhotoServiceMock(PhotoService):
def __init__(self):
super().__init__(None)
self.mock_data = {}
self._next_id = 1
def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None:
new_photo = Photo(
filepath=filepath,
name=name,
addition_date=addition_date,
caption=caption,
fk_travel_diary_id=travel_diary_id
)
new_photo.id = self._next_id
self.mock_data[self._next_id] = new_photo
self._next_id += 1
return new_photo
def read_by_id(self, photo_id: int) -> Photo:
return self.mock_data.get(photo_id)
def read_all(self) -> List[Photo]:
return list(self.mock_data.values())
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
item_to_update: Photo = self.mock_data.get(photo_src.id)
if item_to_update:
item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath
item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name
item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption
item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date else item_to_update.addition_date
item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id else item_to_update.fk_travel_diary_id
if photo_dst.entries:
item_to_update.entries = photo_dst.entries
return item_to_update
return None
def delete(self, photo_src: Photo) -> Photo | None:
return self.mock_data.pop(photo_src.id, None)
@@ -1,23 +0,0 @@
from pilgrim.service.mocks.entry_service_mock import EntryServiceMock
from pilgrim.service.mocks.photo_service_mock import PhotoServiceMock
from pilgrim.service.mocks.travel_diary_service_mock import TravelDiaryServiceMock
from pilgrim.service.photo_service import PhotoService
from pilgrim.service.servicemanager import ServiceManager
class ServiceManagerMock(ServiceManager):
def __init__(self):
super().__init__()
# Cria instâncias únicas para manter estado consistente
self._travel_diary_service = TravelDiaryServiceMock()
self._entry_service = EntryServiceMock()
self._photo_service = PhotoServiceMock()
def get_entry_service(self):
return self._entry_service
def get_travel_diary_service(self):
return self._travel_diary_service
def get_photo_service(self):
return self._photo_service
@@ -1,67 +0,0 @@
from pilgrim.service.travel_diary_service import TravelDiaryService
from pilgrim.models.travel_diary import TravelDiary
import asyncio
class TravelDiaryServiceMock(TravelDiaryService):
def __init__(self):
super().__init__(None)
self.mock_data = {
1: TravelDiary(id=1, name="Montreal"),
2: TravelDiary(id=2, name="Rio de Janeiro"),
}
self._next_id = 3
# Synchronous methods (original)
def create(self, name: str):
"""Synchronous version"""
new_travel_diary = TravelDiary(id=self._next_id, name=name)
self.mock_data[self._next_id] = new_travel_diary
self._next_id += 1
return new_travel_diary
def read_by_id(self, travel_id: int):
"""Synchronous version"""
return self.mock_data.get(travel_id)
def read_all(self):
"""Synchronous version"""
return list(self.mock_data.values())
def update(self, travel_diary_id: int, name: str):
"""Synchronous version"""
item_to_update = self.mock_data.get(travel_diary_id)
if item_to_update:
item_to_update.name = name
return item_to_update
return None
def delete(self, travel_diary_id: int):
"""Synchronous version"""
return self.mock_data.pop(travel_diary_id, None)
# Async methods (new)
async def async_create(self, name: str):
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.create(name)
async def async_read_by_id(self, travel_id: int):
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_by_id(travel_id)
async def async_read_all(self):
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.read_all()
async def async_update(self, travel_diary_id: int, name: str):
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.update(travel_diary_id, name)
async def async_delete(self, travel_diary_id: int):
"""Async version"""
await asyncio.sleep(0.01) # Simulates I/O
return self.delete(travel_diary_id)
@@ -3,7 +3,6 @@ import re
import shutil
from pathlib import Path
from pilgrim.models.entry import Entry
from pilgrim.utils import DirectoryManager
from sqlalchemy.exc import IntegrityError
+12 -9
View File
@@ -1,13 +1,11 @@
from typing import Optional, Tuple
import asyncio
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Label, Static, OptionList, Button
from textual.widgets import Header, Footer, Static, OptionList, Button
from textual.binding import Binding
from textual.containers import Vertical, Container, Horizontal
from textual.containers import Container, Horizontal
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.about_screen import AboutScreen
from pilgrim.ui.screens.diary_settings_screen import SettingsScreen
from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal
@@ -213,11 +211,11 @@ class DiaryListScreen(Screen):
def _on_new_diary_submitted(self, result):
"""Callback after diary creation"""
if result: # Se result não é string vazia, o diário foi criado
self.notify(f"Returning to diary list...")
self.notify("Returning to diary list...")
# Atualiza a lista de diários
self.refresh_diaries()
else:
self.notify(f"Creation canceled...")
self.notify("Creation canceled...")
def _on_screen_resume(self) -> None:
super()._on_screen_resume()
@@ -306,7 +304,12 @@ class DiaryListScreen(Screen):
session = self.app.service_manager.get_session()
if session:
backup_service = BackupService(session)
result = backup_service.create_backup()
result_operation, result_data = backup_service.create_backup()
if result_operation:
self.notify(f"Backup result: {result_data}")
else:
self.notify(f"Error performing backup: {result_data}")
else:
self.notify("You must be logged in to perform this action")
self.notify(f"Backup result: {result}")
self.notify("Error: Session not found",severity="error")
self.app.exit()
@@ -1,13 +1,12 @@
from textual.widgets import Static
from textual.containers import Container
from textual.widgets import Header, Footer, Label, Button,Checkbox,Input
from textual.widgets import Header, Footer, Label, Button,Checkbox
from textual.screen import Screen
from textual.reactive import reactive
from textual.binding import Binding
from textual import on
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.modals.delete_all_entries_from_diary_modal import DeleteAllEntriesModal
from pilgrim.ui.screens.modals.delete_all_photos_from_diary_modal import DeleteAllPhotosModal
from pilgrim.ui.screens.modals.delete_diary_modal import DeleteDiaryModal
+4 -6
View File
@@ -5,11 +5,9 @@ from typing import Optional, List
from pilgrim.models.entry import Entry
from pilgrim.models.photo import Photo
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.modals.add_photo_modal import AddPhotoModal
from pilgrim.ui.screens.modals.confirm_delete_modal import ConfirmDeleteModal
from pilgrim.ui.screens.modals.edit_photo_modal import EditPhotoModal
from pilgrim.ui.screens.modals.file_picker_modal import FilePickerModal
from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal
from textual.app import ComposeResult
from textual.binding import Binding
@@ -189,7 +187,7 @@ class EditEntryScreen(Screen):
"""Ensures the diary info widget is always updated with current diary name"""
try:
self.diary_info.update(f"Diary: {self.diary_name}")
except Exception as e:
except Exception:
self.diary_info.update(f"Diary: {self.diary_id}")
def refresh_entries(self):
@@ -427,7 +425,7 @@ class EditEntryScreen(Screen):
photo_details += f"🔗 {photo_hash}\n"
photo_details += f"📅 {selected_photo.addition_date}\n"
photo_details += f"💬 {selected_photo.caption or 'No caption'}\n"
photo_details += f"[b]Reference formats:[/b]\n"
photo_details += "[b]Reference formats:[/b]\n"
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
self.photo_info.update(photo_details)
@@ -745,8 +743,8 @@ class EditEntryScreen(Screen):
if selected_photo.caption:
photo_details += f"Caption: {selected_photo.caption}\n"
else:
photo_details += f"Caption: No Caption\n"
photo_details += f"[b]Reference formats:[/b]\n"
photo_details += "Caption: No Caption\n"
photo_details += "[b]Reference formats:[/b]\n"
photo_details += f"\\[\\[photo::{photo_hash}]]"
self.photo_info.update(photo_details)
@@ -1,11 +1,9 @@
import os
from pathlib import Path
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Input, Button
from textual.containers import Horizontal, Container
from .file_picker_modal import FilePickerModal
import hashlib
class AddPhotoModal(Screen):
"""Modal for adding a new photo"""
-1
View File
@@ -1,5 +1,4 @@
import os.path
from os import PathLike
from threading import Lock
import tomli
-6
View File
@@ -1,14 +1,8 @@
from re import search
import pytest
from datetime import datetime
from unittest.mock import Mock
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import sessionmaker
from pilgrim.database import Base
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.models.entry import Entry
from pilgrim.models.photo import Photo
-1
View File
@@ -1,4 +1,3 @@
import pytest
from pathlib import Path
from pilgrim import TravelDiary
-1
View File
@@ -4,7 +4,6 @@ from pathlib import Path
from unittest.mock import patch
from pilgrim.utils.config_manager import ConfigManager, SingletonMeta
from pilgrim.utils.directory_manager import DirectoryManager
@pytest.fixture
def clean_singleton():