Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ The application follows a repository pattern with clear separation of concerns:
- Uses SQLite with two separate databases:
- `transactions.db` - stores all expense/income transactions
- `merchant_categories.db` - stores merchant-to-category mappings
- **Database Location**: Databases are stored in platform-specific user data directories:
- macOS: `~/Library/Application Support/spendwise-tracker/`
- Linux/Unix: `~/.local/share/spendwise-tracker/`
- Windows: `%LOCALAPPDATA%\spendwise-tracker\`
- Database paths are resolved via `get_database_path()` in [expense_tracker/utils/path.py](expense_tracker/utils/path.py)
- Legacy databases from `expense_tracker/data/` are automatically migrated on first launch

**Merchant Categorization System:**
- `normalize_merchant()` ([expense_tracker/utils/merchant_normalizer.py](expense_tracker/utils/merchant_normalizer.py)) standardizes merchant names by:
Expand Down
13 changes: 11 additions & 2 deletions expense_tracker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
from tkinter import Tk, ttk
from expense_tracker.gui.main_window import MainWindow
from expense_tracker.version import versions
from expense_tracker.utils.path import get_database_path
from expense_tracker.utils.migration import migrate_legacy_databases


def main():
"""Start the Expense Tracker application."""

versions()

transaction_repo = TransactionRepository("expense_tracker/data/transactions.db")
# Migrate legacy databases if they exist
migrate_legacy_databases()

# Use platform-specific data directory for databases
print("Using data directory for databases.")
print(f" - Transactions DB: {get_database_path('transactions.db')}")
print(f" - Merchant Categories DB: {get_database_path('merchant_categories.db')}")
transaction_repo = TransactionRepository(str(get_database_path("transactions.db")))
merchant_repo = MerchantCategoryRepository(
"expense_tracker/data/merchant_categories.db"
str(get_database_path("merchant_categories.db"))
)
root = Tk()
root.title("Expense Tracker")
Expand Down
54 changes: 54 additions & 0 deletions expense_tracker/utils/migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from pathlib import Path
import shutil
import logging

from expense_tracker.utils.path import get_data_directory

logger = logging.getLogger(__name__)


def migrate_legacy_databases() -> None:
"""
Migrate databases from the legacy source code directory to the new
platform-specific data directory.

Legacy location: expense_tracker/data/
New location: Platform-specific (e.g., ~/Library/Application Support/spendwise-tracker/)

If legacy databases exist and new location doesn't have them, copy them over.
If both exist, prefer the new location (no migration).
"""
legacy_dir = Path("expense_tracker/data")

# If legacy directory doesn't exist, no migration needed
if not legacy_dir.exists():
logger.debug("No legacy database directory found, skipping migration")
return

new_dir = get_data_directory()
db_files = ["transactions.db", "merchant_categories.db"]
migrated_files = []

for db_file in db_files:
legacy_path = legacy_dir / db_file
new_path = new_dir / db_file

# Only migrate if legacy file exists and new file doesn't
if legacy_path.exists() and not new_path.exists():
try:
shutil.copy2(legacy_path, new_path)
migrated_files.append(db_file)
logger.info(f"Migrated {db_file} from {legacy_path} to {new_path}")
except Exception as e:
logger.error(f"Failed to migrate {db_file}: {e}")
# Continue with other files even if one fails

if migrated_files:
logger.info(
f"Database migration complete. Migrated: {', '.join(migrated_files)}"
)
logger.info(
f"Legacy files remain at {legacy_dir} and can be manually deleted if desired"
)
else:
logger.debug("No database files needed migration")
43 changes: 43 additions & 0 deletions expense_tracker/utils/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pathlib import Path
import platform
import os


def get_data_directory() -> Path:
"""
Returns the path to the data directory for the expense tracker application.
Creates the directory if it does not exist.

Platform-specific locations:
- macOS: ~/Library/Application Support/spendwise-tracker/
- Linux/Unix: ~/.local/share/spendwise-tracker/
- Windows: %LOCALAPPDATA%/spendwise-tracker/
"""
system = platform.system()

if system == "Darwin": # macOS
base_dir = Path.home() / "Library" / "Application Support"
elif system == "Windows":
# Use LOCALAPPDATA environment variable, fallback to home/AppData/Local
base_dir = Path(
os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
)
else: # Linux and other Unix-like systems
base_dir = Path.home() / ".local" / "share"

data_dir = base_dir / "spendwise-tracker"
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir


def get_database_path(filename: str) -> Path:
"""
Returns the full path to a database file in the data directory.

Args:
filename: Name of the database file (e.g., "transactions.db")

Returns:
Path object pointing to the database file
"""
return get_data_directory() / filename
173 changes: 173 additions & 0 deletions openspec/changes/fix-database-location-for-package-install/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Design Document: Database Location for Package Installation

## Context
The current implementation uses hardcoded relative paths (`expense_tracker/data/transactions.db`) that only work when the application is run from the source directory. When users install the package via `uv tool install spendwise-tracker`, the working directory is different from the package location, causing the application to fail when trying to create or access database files.

This is a common issue with Python desktop applications that need persistent data storage. The solution must:
- Follow platform conventions for user data storage
- Work with multiple installation methods (source, pip, uv tool install)
- Provide backward compatibility for existing users
- Remain simple and maintainable

## Goals / Non-Goals

### Goals
- Store databases in platform-appropriate user data directories
- Automatically create data directories on first launch
- Migrate existing databases from legacy location seamlessly
- Work correctly with `uv tool install`, `pip install`, and source execution
- Maintain zero-configuration setup for users

### Non-Goals
- Supporting custom database locations via configuration files
- Environment variable overrides for database paths
- Multi-user database sharing (remains single-user desktop app)
- Backing up or removing legacy database files automatically
- Supporting portable/USB installation modes

## Decisions

### Decision 1: Use Standard Platform Directories
**Choice**: Use platform-specific standard user data directories via Python's `pathlib` and platform detection.

**Alternatives considered**:
1. **Current working directory**: Doesn't work with tool installations
2. **Package installation directory**: Not writable in many installation scenarios
3. **Environment variable**: Adds configuration complexity, not user-friendly
4. **XDG/platformdirs library**: Adds external dependency for simple functionality

**Rationale**: Standard directories are expected by users on each platform, don't require additional dependencies, and work with all installation methods. We can implement this with stdlib `pathlib` and `sys.platform`.

**Implementation**:
```python
# expense_tracker/utils/paths.py
from pathlib import Path
import sys

def get_data_directory() -> Path:
if sys.platform == "darwin": # macOS
base = Path.home() / "Library" / "Application Support"
elif sys.platform == "win32": # Windows
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
else: # Linux and other Unix
base = Path.home() / ".local" / "share"

data_dir = base / "spendwise-tracker"
data_dir.mkdir(parents=True, exist_ok=True)
return data_dir

def get_database_path(filename: str) -> Path:
return get_data_directory() / filename
```

### Decision 2: Automatic Migration on First Launch
**Choice**: Detect legacy databases and copy them to new location on first launch after upgrade.

**Alternatives considered**:
1. **No migration**: Existing users lose data
2. **Manual migration instructions**: Poor user experience
3. **In-place database location**: Doesn't solve the tool installation problem
4. **Move instead of copy**: Risk of data loss if migration fails

**Rationale**: Seamless migration provides the best user experience. Copying (instead of moving) legacy files is safer and allows users to roll back if needed. Migration logic runs once on first launch and has minimal performance impact.

**Implementation**:
```python
# expense_tracker/utils/migration.py
from pathlib import Path
import shutil

def migrate_legacy_databases() -> None:
legacy_dir = Path("expense_tracker/data")
if not legacy_dir.exists():
return

new_dir = get_data_directory()
for db_file in ["transactions.db", "merchant_categories.db"]:
legacy_path = legacy_dir / db_file
new_path = new_dir / db_file

if legacy_path.exists() and not new_path.exists():
shutil.copy2(legacy_path, new_path)
```

### Decision 3: Directory Creation on Initialization
**Choice**: Create data directory with `mkdir(parents=True, exist_ok=True)` when getting the data directory path.

**Alternatives considered**:
1. **Manual directory creation**: Adds user friction
2. **Check and fail if missing**: Poor user experience
3. **Lazy creation on first write**: More complex, fails on read operations

**Rationale**: Automatic creation is the expected behavior for desktop applications. The `exist_ok=True` flag makes it safe to call repeatedly, and `parents=True` ensures parent directories are created if needed.

### Decision 4: No External Dependencies for Path Resolution
**Choice**: Use stdlib `pathlib`, `sys.platform`, and `os.environ` for path resolution.

**Alternatives considered**:
1. **platformdirs library**: Popular but adds external dependency
2. **appdirs library**: Older, less maintained
3. **XDG library**: Linux-only

**Rationale**: The path resolution logic is simple enough to implement with stdlib. Avoiding an external dependency reduces package size, installation complexity, and potential version conflicts. The implementation is ~20 lines of code.

## Risks / Trade-offs

### Risk 1: Platform Detection Edge Cases
**Risk**: Uncommon platforms (FreeBSD, exotic Unix variants) might not be handled correctly.

**Mitigation**: Default to Unix-style `~/.local/share` for unknown platforms. This is a sensible fallback that works on most Unix-like systems.

### Risk 2: Migration Failure
**Risk**: Copying legacy databases might fail due to permissions or disk space.

**Mitigation**:
- Wrap migration in try-except and log errors
- Allow application to continue with fresh databases if migration fails
- Don't delete legacy files so users can manually recover

### Risk 3: Hardcoded Paths in Tests
**Risk**: Existing tests might break due to changed database paths.

**Mitigation**: Update tests to use temporary directories or mock the path resolution functions. Use pytest fixtures for database setup.

### Trade-off: No Custom Database Location
**Trade-off**: Users cannot configure a custom database location via config file or environment variable.

**Rationale**: Adds complexity for minimal benefit in a single-user desktop application. Users who need this can modify the source code. Can be added later if there's demand.

## Migration Plan

### Phase 1: Implementation
1. Create path utilities module (`expense_tracker/utils/paths.py`)
2. Create migration module (`expense_tracker/utils/migration.py`)
3. Update `expense_tracker/app.py` to use new path resolution
4. Update tests to use new path resolution

### Phase 2: Testing
1. Unit tests for path resolution on all platforms
2. Integration tests for migration scenarios
3. Manual testing with `uv tool install`
4. Test on macOS, Linux, and Windows if possible

### Phase 3: Deployment
1. Update documentation (CLAUDE.md, README, project.md)
2. Create release notes mentioning database location change
3. Deploy via PyPI/GitHub release

### Rollback Plan
If critical issues are discovered:
1. Users can downgrade to previous version
2. Legacy databases remain in `expense_tracker/data/`
3. No data loss occurs because migration copies (doesn't move) files

## Open Questions

### Q1: Should we add logging for migration events?
**Status**: Yes - add INFO level logging when migration occurs, showing source and destination paths. Helps with debugging user issues.

### Q2: Should we provide a CLI command to show database location?
**Status**: Not in this change - can be added later as a separate feature if needed. Users can find databases in platform-standard locations.

### Q3: Should we handle the case where both legacy and new databases exist with different data?
**Status**: No - the spec says to prefer new location and ignore legacy. This scenario should be rare and users can manually merge if needed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Change: Fix Database Location for Package Installation

## Why
When users install spendwise-tracker via `uv tool install spendwise-tracker`, the application fails to create or find database files because it uses hardcoded relative paths (`expense_tracker/data/transactions.db`) that assume execution from the source code directory. This breaks the application for users installing it as a tool package, as the working directory differs from the package installation location.

## What Changes
- Replace hardcoded relative database paths with platform-appropriate user data directory paths
- Use standard user data directory conventions:
- **Linux/Unix**: `~/.local/share/spendwise-tracker/`
- **macOS**: `~/Library/Application Support/spendwise-tracker/`
- **Windows**: `%LOCALAPPDATA%\spendwise-tracker\`
- Automatically create the data directory if it doesn't exist
- Update database initialization to use the new path resolution logic
- Maintain backward compatibility by migrating existing databases from old location if found

## Impact
- **Affected specs**: database-location (new capability)
- **Affected code**:
- [expense_tracker/app.py:15-17](expense_tracker/app.py#L15-L17) - Database path initialization
- [expense_tracker/core/repositories.py:16](expense_tracker/core/repositories.py#L16) - Repository initialization
- **Breaking**: None - backward compatible migration for existing users
- **Benefits**: Application works correctly when installed via `uv tool install`
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Database Location Specification

## ADDED Requirements

### Requirement: Platform-Specific Data Directory Resolution
The application SHALL resolve database storage location using platform-appropriate user data directories, ensuring the application works correctly regardless of installation method (source code, pip install, uv tool install).

#### Scenario: macOS user data directory
- **WHEN** the application runs on macOS
- **THEN** database files SHALL be stored in `~/Library/Application Support/spendwise-tracker/`

#### Scenario: Linux/Unix user data directory
- **WHEN** the application runs on Linux or Unix systems
- **THEN** database files SHALL be stored in `~/.local/share/spendwise-tracker/`

#### Scenario: Windows user data directory
- **WHEN** the application runs on Windows
- **THEN** database files SHALL be stored in `%LOCALAPPDATA%\spendwise-tracker\`

### Requirement: Automatic Data Directory Creation
The application SHALL automatically create the data directory and any parent directories if they do not exist when initializing database connections.

#### Scenario: First-time application launch
- **WHEN** the application launches for the first time
- **THEN** the data directory SHALL be created automatically
- **AND** both database files (transactions.db and merchant_categories.db) SHALL be initialized in the data directory

#### Scenario: Missing data directory
- **WHEN** the data directory is deleted after initial setup
- **THEN** the application SHALL recreate the directory on next launch
- **AND** database files SHALL be re-initialized with empty schemas

### Requirement: Database File Path Resolution
The application SHALL construct absolute paths to database files by combining the platform-specific data directory with the database filename.

#### Scenario: Transaction database path resolution
- **WHEN** the TransactionRepository is initialized
- **THEN** the database path SHALL be `<platform_data_dir>/transactions.db`

#### Scenario: Merchant categories database path resolution
- **WHEN** the MerchantCategoryRepository is initialized
- **THEN** the database path SHALL be `<platform_data_dir>/merchant_categories.db`

### Requirement: Backward Compatibility Migration
The application SHALL detect and migrate existing databases from the legacy source code directory (`expense_tracker/data/`) to the new platform-specific location on first launch after upgrade.

#### Scenario: Existing user with legacy database location
- **WHEN** the application launches with existing databases in `expense_tracker/data/`
- **THEN** the application SHALL copy both database files to the new platform-specific location
- **AND** the application SHALL continue using the new location for all subsequent operations
- **AND** the legacy files SHALL remain untouched (manual cleanup by user)

#### Scenario: Fresh installation with no legacy databases
- **WHEN** the application launches with no existing databases
- **THEN** the application SHALL create new empty databases in the platform-specific location
- **AND** no migration SHALL occur

#### Scenario: Multiple application instances
- **WHEN** databases exist in both legacy and new locations
- **THEN** the application SHALL prefer the new platform-specific location
- **AND** ignore the legacy location databases
Loading