Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
589668b
Fix timestamp handling
abrookins Aug 20, 2025
a728492
Refactor datetime conversion exception handling
abrookins Aug 20, 2025
1afdd24
Fix spellcheck issues in migration documentation
abrookins Aug 20, 2025
801554f
Revert datetime conversion refactoring to fix timezone issues
abrookins Aug 20, 2025
de0a3d3
Fix spellcheck by adding migration-related words to wordlist
abrookins Aug 20, 2025
94b6d4b
Address Copilot code review feedback
abrookins Aug 20, 2025
7372fce
Address Copilot review feedback
abrookins Aug 20, 2025
815567e
Expand migrations CLI to support create/run/rollback/status
abrookins Aug 27, 2025
d8dbc8b
Fix sync CLI commands and test failures
abrookins Aug 28, 2025
ded5c29
Fix Python 3.9 compatibility in CLI type annotations
abrookins Aug 28, 2025
a35c6f0
Fix spellcheck errors in migration documentation
abrookins Aug 28, 2025
ab337df
Fix MyPy errors for _meta attribute access
abrookins Aug 28, 2025
865ef35
Fix CLI async/sync function call issues causing test failures
abrookins Aug 28, 2025
63287ed
Fix CLI async/sync transformation and execution pattern
abrookins Aug 28, 2025
a83b591
Fix missing newline at end of CLI migrate file
abrookins Aug 28, 2025
31cf4b0
Fix CLI sync/async transformation issues
abrookins Aug 28, 2025
3e9fdfb
Trigger CI rebuild after network timeout
abrookins Aug 29, 2025
2929b7c
Fix trailing whitespace in CLI docstring
abrookins Aug 29, 2025
88a5ab7
Fix async/sync CLI transformation issues
abrookins Aug 29, 2025
06ab091
Fix schema migration rollback logic bug
abrookins Aug 29, 2025
929a22c
Fix test isolation for parallel execution
abrookins Aug 29, 2025
5fd01cf
Improve test worker isolation by overriding APPLIED_MIGRATIONS_KEY
abrookins Aug 29, 2025
9662cda
Separate legacy and new migration CLIs
abrookins Aug 29, 2025
9e667a7
Remove test migration file and update docs
abrookins Aug 29, 2025
a3720fc
Fix linting issues in legacy migrate CLI
abrookins Aug 29, 2025
d752422
Apply final code formatting fixes
abrookins Aug 29, 2025
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: 5 additions & 1 deletion .github/wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ unix
utf
validator
validators
virtualenv
virtualenv
datetime
Datetime
reindex
schemas
1 change: 1 addition & 0 deletions aredis_om/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# CLI package
24 changes: 24 additions & 0 deletions aredis_om/cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Redis-OM CLI - Main entry point for the async 'om' command.
"""

import click

from ..model.cli.migrate import migrate
from ..model.cli.migrate_data import migrate_data


@click.group()
@click.version_option()
def om():
"""Redis-OM Python CLI - Object mapping and migrations for Redis."""
pass


# Add subcommands
om.add_command(migrate)
om.add_command(migrate_data, name="migrate-data")


if __name__ == "__main__":
om()
260 changes: 260 additions & 0 deletions aredis_om/model/cli/migrate_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
"""
Async CLI for Redis-OM data migrations.

This module provides command-line interface for managing data migrations
in Redis-OM Python applications.
"""

import asyncio
import os
from pathlib import Path

import click

from ..migrations.data_migrator import DataMigrationError, DataMigrator


def run_async(coro):
"""Helper to run async functions in Click commands."""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# We're in an async context, create a new loop
import concurrent.futures

with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(asyncio.run, coro)
return future.result()
else:
return loop.run_until_complete(coro)
except RuntimeError:
# No event loop exists, create one
return asyncio.run(coro)


@click.group()
def migrate_data():
"""Manage data migrations for Redis-OM models."""
pass


@migrate_data.command()
@click.option(
"--migrations-dir",
default="migrations",
help="Directory containing migration files (default: migrations)",
)
@click.option("--module", help="Python module containing migrations")
def status(migrations_dir: str, module: str):
"""Show current migration status."""

async def _status():
try:
migrator = DataMigrator(
migrations_dir=migrations_dir if not module else None,
migration_module=module,
)

status_info = await migrator.status()

click.echo("Migration Status:")
click.echo(f" Total migrations: {status_info['total_migrations']}")
click.echo(f" Applied: {status_info['applied_count']}")
click.echo(f" Pending: {status_info['pending_count']}")

if status_info["pending_migrations"]:
click.echo("\nPending migrations:")
for migration_id in status_info["pending_migrations"]:
click.echo(f"- {migration_id}")

if status_info["applied_migrations"]:
click.echo("\nApplied migrations:")
for migration_id in status_info["applied_migrations"]:
click.echo(f"- {migration_id}")

except Exception as e:
click.echo(f"Error: {e}", err=True)
raise click.Abort()

run_async(_status())


@migrate_data.command()
@click.option(
"--migrations-dir",
default="migrations",
help="Directory containing migration files (default: migrations)",
)
@click.option("--module", help="Python module containing migrations")
@click.option(
"--dry-run", is_flag=True, help="Show what would be done without applying changes"
)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
@click.option("--limit", type=int, help="Limit number of migrations to run")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def run(
migrations_dir: str,
module: str,
dry_run: bool,
verbose: bool,
limit: int,
yes: bool,
):
"""Run pending migrations."""

async def _run():
try:
migrator = DataMigrator(
migrations_dir=migrations_dir if not module else None,
migration_module=module,
)

# Get pending migrations for confirmation
pending = await migrator.get_pending_migrations()

if not pending:
if verbose:
click.echo("No pending migrations found.")
return

count_to_run = len(pending)
if limit:
count_to_run = min(count_to_run, limit)
pending = pending[:limit]

if dry_run:
click.echo(f"Would run {count_to_run} migration(s):")
for migration in pending:
click.echo(f"- {migration.migration_id}: {migration.description}")
return

# Confirm unless --yes is specified
if not yes:
migration_list = "\n".join(f"- {m.migration_id}" for m in pending)
if not click.confirm(
f"Run {count_to_run} migration(s)?\n{migration_list}"
):
click.echo("Aborted.")
return

# Run migrations
count = await migrator.run_migrations(
dry_run=False, limit=limit, verbose=verbose
)

if verbose:
click.echo(f"Successfully applied {count} migration(s).")

except DataMigrationError as e:
click.echo(f"Migration error: {e}", err=True)
raise click.Abort()
except Exception as e:
click.echo(f"Error: {e}", err=True)
raise click.Abort()

run_async(_run())


@migrate_data.command()
@click.argument("name")
@click.option(
"--migrations-dir",
default="migrations",
help="Directory to create migration in (default: migrations)",
)
def create(name: str, migrations_dir: str):
"""Create a new migration file."""

async def _create():
try:
migrator = DataMigrator(migrations_dir=migrations_dir)
filepath = await migrator.create_migration_file(name, migrations_dir)
click.echo(f"Created migration: {filepath}")

except Exception as e:
click.echo(f"Error creating migration: {e}", err=True)
raise click.Abort()

run_async(_create())


@migrate_data.command()
@click.argument("migration_id")
@click.option(
"--migrations-dir",
default="migrations",
help="Directory containing migration files (default: migrations)",
)
@click.option("--module", help="Python module containing migrations")
@click.option(
"--dry-run", is_flag=True, help="Show what would be done without applying changes"
)
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def rollback(
migration_id: str,
migrations_dir: str,
module: str,
dry_run: bool,
verbose: bool,
yes: bool,
):
"""Rollback a specific migration."""

async def _rollback():
try:
migrator = DataMigrator(
migrations_dir=migrations_dir if not module else None,
migration_module=module,
)

# Check if migration exists and is applied
all_migrations = await migrator.discover_migrations()
applied_migrations = await migrator.get_applied_migrations()

if migration_id not in all_migrations:
click.echo(f"Migration '{migration_id}' not found.", err=True)
raise click.Abort()

if migration_id not in applied_migrations:
click.echo(f"Migration '{migration_id}' is not applied.", err=True)
return

migration = all_migrations[migration_id]

if dry_run:
click.echo(f"Would rollback migration: {migration_id}")
click.echo(f"Description: {migration.description}")
return

# Confirm unless --yes is specified
if not yes:
if not click.confirm(f"Rollback migration '{migration_id}'?"):
click.echo("Aborted.")
return

# Attempt rollback
success = await migrator.rollback_migration(
migration_id, dry_run=False, verbose=verbose
)

if success:
if verbose:
click.echo(f"Successfully rolled back migration: {migration_id}")
else:
click.echo(
f"Migration '{migration_id}' does not support rollback.", err=True
)

except DataMigrationError as e:
click.echo(f"Migration error: {e}", err=True)
raise click.Abort()
except Exception as e:
click.echo(f"Error: {e}", err=True)
raise click.Abort()

run_async(_rollback())


if __name__ == "__main__":
migrate_data()
Loading
Loading