diff --git a/.gitignore b/.gitignore index e1a11b3..a3da041 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,5 @@ cython_debug/ # Other files and folders .python-version + +.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 1423fe3..2687598 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ uv add jetbase ```bash jetbase init -cd jetbase ``` This creates a `jetbase/` directory with: diff --git a/docs/commands/current.md b/docs/commands/current.md index 3c98b0c..f2a3ad2 100644 --- a/docs/commands/current.md +++ b/docs/commands/current.md @@ -40,5 +40,4 @@ No migrations have been applied yet. ## Notes -- Must be run from inside the `jetbase/` directory - Shows only the version of the latest **versioned** migration diff --git a/docs/commands/history.md b/docs/commands/history.md index 3d9f257..eae674d 100644 --- a/docs/commands/history.md +++ b/docs/commands/history.md @@ -80,8 +80,3 @@ The exact timestamp when the migration was applied. The format is: ``` YYYY-MM-DD HH:MM:SS.microseconds ``` - -## Notes - -- Must be run from inside the `jetbase/` directory - diff --git a/docs/commands/init.md b/docs/commands/init.md index 01a87d8..a5a1d96 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -36,16 +36,6 @@ sqlalchemy_url = "postgresql://user:password@localhost:5432/mydb" !!! important After running `init`, you need to update the `sqlalchemy_url` in `env.py` with your actual database connection string. -> **Next Step:** -> After running `jetbase init`, move into the `jetbase` directory with: -> -> ```bash -> cd jetbase -> ``` -> -> **Important:** -> All future `jetbase` commands should be run *inside* the `jetbase` directory. - ## Examples ### Basic Usage @@ -59,7 +49,6 @@ Output: ``` Initialized Jetbase project in /path/to/your/project/jetbase -Run 'cd jetbase' to get started! ``` ### Complete Workflow @@ -68,21 +57,16 @@ Run 'cd jetbase' to get started! # 1. Initialize Jetbase jetbase init -# 2. Move into the jetbase directory -cd jetbase - -# 3. Edit env.py with your database URL +# 2. Edit env.py with your database URL # (use your favorite editor) -# 4. Create your first migration +# 3. Create your first migration jetbase new "create users table" -v 1 -# 5. Apply migrations +# 4. Apply migrations jetbase upgrade ``` ## Notes -- All other Jetbase commands must be run from inside the `jetbase/` directory. - The `migrations/` directory will be empty after initialization. Use `jetbase new` to create your first migration, (or create migration files manually. See [Migrations Overview](../migrations/index.md)). - diff --git a/docs/commands/lock-status.md b/docs/commands/lock-status.md index 7da5ead..1e0feca 100644 --- a/docs/commands/lock-status.md +++ b/docs/commands/lock-status.md @@ -54,7 +54,6 @@ If you are absolutely certain that no migration or fix command is active, you ma For a detailed explanation of how migration locking works, see [Migration Locking](../advanced/migration-locking.md). -- Must be run from inside the `jetbase/` directory - The lock is stored in the `jetbase_lock` database table - Locks are automatically released whether the migration was successful or an error occurred. diff --git a/docs/commands/new.md b/docs/commands/new.md index 55badd0..bf583bf 100644 --- a/docs/commands/new.md +++ b/docs/commands/new.md @@ -115,7 +115,6 @@ Include `-- rollback` sections. This allows you to safely undo migrations if nee ## Notes -- Must be run from inside the `jetbase/` directory - Use `-v` to specify a custom version number (e.g., `1`, `1.5`) - If `-v` is not provided, a timestamp-based version is automatically generated - You do not have to use the `jetbase new` CLI command to create a new migration. You can create a new file manually in the `jetbase/migrations` directory and follow the `V__.sql` naming convention. For full details and best practices, see [Migrations Overview](../migrations/index.md). diff --git a/docs/commands/rollback.md b/docs/commands/rollback.md index 82e1229..1ed8e0a 100644 --- a/docs/commands/rollback.md +++ b/docs/commands/rollback.md @@ -159,7 +159,6 @@ jetbase status ## Notes -- Must be run from inside the `jetbase/` directory - Rollbacks are applied in reverse chronological order - Cannot use both `--count` and `--to-version` together diff --git a/docs/commands/status.md b/docs/commands/status.md index fba81d8..563191c 100644 --- a/docs/commands/status.md +++ b/docs/commands/status.md @@ -105,6 +105,5 @@ Example: ## Notes -- Must be run from inside the `jetbase/` directory - Compares files in `migrations/` with database records diff --git a/docs/commands/upgrade.md b/docs/commands/upgrade.md index ae46dfa..ef65e26 100644 --- a/docs/commands/upgrade.md +++ b/docs/commands/upgrade.md @@ -137,7 +137,6 @@ Jetbase stops safely at the first sign of trouble, preventing partial or out-of- ## Notes -- Must be run from inside the `jetbase/` directory - Cannot use both `--count` and `--to-version` together - The automatic lock prevents concurrent migrations from causing conflicts diff --git a/docs/commands/validate-checksums.md b/docs/commands/validate-checksums.md index e55f651..4b38cf9 100644 --- a/docs/commands/validate-checksums.md +++ b/docs/commands/validate-checksums.md @@ -126,7 +126,6 @@ Checksums help catch when: ## Notes -- Must be run from inside the `jetbase/` directory - Only validates migrations that have been applied - Only uses upgrade SQL statements in the migration file to calculate checksums diff --git a/docs/getting-started.md b/docs/getting-started.md index 7b1e8fa..8532971 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -53,16 +53,7 @@ jetbase/ └── env.py # Database configuration ``` -### Step 2: Navigate to the Jetbase Directory - -```bash -cd jetbase -``` - -!!! important -All Jetbase commands must be run from inside the `jetbase/` directory. - -### Step 3: Configure Your Database Connection +### Step 2: Configure Your Database Connection Open `env.py` and update the `sqlalchemy_url` with your database connection string: diff --git a/docs/index.md b/docs/index.md index 53e4b8c..15b033c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,7 +34,6 @@ Jetbase helps you manage database migrations in a simple, version-controlled way ```bash jetbase init -cd jetbase ``` This creates a `jetbase/` directory with: diff --git a/jetbase/commands/fix_checksums.py b/jetbase/commands/fix_checksums.py index 5ab44cf..64b880a 100644 --- a/jetbase/commands/fix_checksums.py +++ b/jetbase/commands/fix_checksums.py @@ -1,6 +1,3 @@ -import os - -from jetbase.constants import MIGRATIONS_DIR from jetbase.engine.checksum import calculate_checksum from jetbase.engine.file_parser import parse_upgrade_statements from jetbase.engine.lock import migration_lock @@ -10,6 +7,7 @@ MigrationVersionMismatchError, ) from jetbase.logging import logger +from jetbase.paths import get_migrations_directory from jetbase.repositories.migrations_repo import ( get_checksums_by_version, update_migration_checksums, @@ -151,7 +149,7 @@ def _find_checksum_mismatches( [("1.0", "def456")] # If file changed """ migration_filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version( - directory=os.path.join(os.getcwd(), MIGRATIONS_DIR), + directory=str(get_migrations_directory()), end_version=latest_migrated_version, ) diff --git a/jetbase/commands/fix_files.py b/jetbase/commands/fix_files.py index c7b3b15..c229544 100644 --- a/jetbase/commands/fix_files.py +++ b/jetbase/commands/fix_files.py @@ -1,5 +1,3 @@ -import os - from jetbase.engine.lock import migration_lock from jetbase.engine.repeatable import get_repeatable_filenames from jetbase.engine.version import ( @@ -7,6 +5,7 @@ ) from jetbase.logging import logger from jetbase.models import MigrationRecord +from jetbase.paths import get_migrations_directory from jetbase.repositories.lock_repo import create_lock_table_if_not_exists from jetbase.repositories.migrations_repo import ( create_migrations_table_if_not_exists, @@ -40,7 +39,7 @@ def fix_files_cmd(audit_only: bool = False) -> None: migrated_versions: list[str] = get_migrated_versions() current_migration_filepaths_by_version: dict[str, str] = ( get_migration_filepaths_by_version( - directory=os.path.join(os.getcwd(), "migrations") + directory=str(get_migrations_directory()), ) ) repeatable_migrations: list[MigrationRecord] = fetch_repeatable_migrations() diff --git a/jetbase/commands/init.py b/jetbase/commands/init.py index 7f66778..14078a9 100644 --- a/jetbase/commands/init.py +++ b/jetbase/commands/init.py @@ -22,6 +22,6 @@ def initialize_cmd() -> None: f.write(ENV_FILE_CONTENT) logger.info( - "Initialized Jetbase project in %s\nRun 'cd jetbase' to get started!", + "Initialized Jetbase project in %s", Path(BASE_DIR).absolute(), ) diff --git a/jetbase/commands/new.py b/jetbase/commands/new.py index 5e4afbb..2dd7c81 100644 --- a/jetbase/commands/new.py +++ b/jetbase/commands/new.py @@ -1,14 +1,14 @@ import datetime as dt import os -from jetbase.constants import MIGRATIONS_DIR, NEW_MIGRATION_FILE_CONTENT +from jetbase.constants import NEW_MIGRATION_FILE_CONTENT from jetbase.engine.file_parser import is_filename_length_valid, is_valid_version from jetbase.exceptions import ( - DirectoryNotFoundError, InvalidVersionError, MigrationFilenameTooLongError, ) from jetbase.logging import logger +from jetbase.paths import get_migrations_directory def generate_new_migration_file_cmd( @@ -40,13 +40,7 @@ def generate_new_migration_file_cmd( Created migration file: V20251201.120000__create_users_table.sql """ - migrations_dir_path: str = os.path.join(os.getcwd(), MIGRATIONS_DIR) - - if not os.path.exists(migrations_dir_path): - raise DirectoryNotFoundError( - "Migrations directory not found. Run 'jetbase initialize' to set up jetbase.\n" - "If you have already done so, run this command from the jetbase directory." - ) + migrations_dir_path = str(get_migrations_directory()) filename: str = _generate_new_filename(description=description, version=version) filepath: str = os.path.join(migrations_dir_path, filename) diff --git a/jetbase/commands/rollback.py b/jetbase/commands/rollback.py index c743373..e9af85a 100644 --- a/jetbase/commands/rollback.py +++ b/jetbase/commands/rollback.py @@ -9,6 +9,7 @@ from jetbase.enums import MigrationDirectionType from jetbase.exceptions import VersionNotFoundError from jetbase.logging import logger +from jetbase.paths import get_migrations_directory from jetbase.repositories.lock_repo import create_lock_table_if_not_exists from jetbase.repositories.migrations_repo import ( create_migrations_table_if_not_exists, @@ -138,7 +139,7 @@ def _get_versions_to_rollback(latest_migration_versions: list[str]) -> dict[str, newest versions are rolled back first. """ versions_to_rollback: dict[str, str] = get_migration_filepaths_by_version( - directory=os.path.join(os.getcwd(), "migrations"), + directory=str(get_migrations_directory()), version_to_start_from=latest_migration_versions[-1], end_version=latest_migration_versions[0], ) diff --git a/jetbase/commands/status.py b/jetbase/commands/status.py index 2850233..a968a20 100644 --- a/jetbase/commands/status.py +++ b/jetbase/commands/status.py @@ -9,6 +9,7 @@ from jetbase.engine.version import get_migration_filepaths_by_version from jetbase.enums import MigrationType from jetbase.models import MigrationRecord +from jetbase.paths import get_migrations_directory from jetbase.repositories.migrations_repo import ( create_migrations_table_if_not_exists, get_existing_on_change_filenames_to_checksums, @@ -47,8 +48,10 @@ def status_cmd() -> None: versioned_migration_records[-1].version if versioned_migration_records else None ) + migrations_directory = str(get_migrations_directory()) + pending_versioned_filepaths: dict[str, str] = get_migration_filepaths_by_version( - directory=os.path.join(os.getcwd(), "migrations"), + directory=migrations_directory, version_to_start_from=latest_migrated_version, ) @@ -62,7 +65,7 @@ def status_cmd() -> None: roc_filenames_changed_only: list[str] = [ os.path.basename(filepath) for filepath in get_runs_on_change_filepaths( - directory=os.path.join(os.getcwd(), "migrations"), changed_only=True + directory=migrations_directory, changed_only=True ) ] @@ -72,9 +75,7 @@ def status_cmd() -> None: all_roc_filenames: list[str] = [ os.path.basename(filepath) - for filepath in get_runs_on_change_filepaths( - directory=os.path.join(os.getcwd(), "migrations") - ) + for filepath in get_runs_on_change_filepaths(directory=migrations_directory) ] console: Console = Console() diff --git a/jetbase/commands/upgrade.py b/jetbase/commands/upgrade.py index 975a351..6a6200e 100644 --- a/jetbase/commands/upgrade.py +++ b/jetbase/commands/upgrade.py @@ -1,6 +1,5 @@ import os -from jetbase.constants import MIGRATIONS_DIR from jetbase.engine.dry_run import process_dry_run from jetbase.engine.file_parser import parse_upgrade_statements from jetbase.engine.lock import migration_lock @@ -15,6 +14,7 @@ from jetbase.enums import MigrationDirectionType, MigrationType from jetbase.logging import logger from jetbase.models import MigrationRecord +from jetbase.paths import get_migrations_directory from jetbase.repositories.lock_repo import create_lock_table_if_not_exists from jetbase.repositories.migrations_repo import ( create_migrations_table_if_not_exists, @@ -78,12 +78,14 @@ def upgrade_cmd( to_version=to_version, ) + migrations_directory = str(get_migrations_directory()) + repeatable_always_filepaths: list[str] = get_repeatable_always_filepaths( - directory=os.path.join(os.getcwd(), MIGRATIONS_DIR) + directory=migrations_directory ) runs_on_change_filepaths: list[str] = get_runs_on_change_filepaths( - directory=os.path.join(os.getcwd(), MIGRATIONS_DIR), + directory=migrations_directory, changed_only=True, ) @@ -141,7 +143,7 @@ def _get_filepaths_by_version( FileNotFoundError: If to_version is not found in pending migrations. """ filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version( - directory=os.path.join(os.getcwd(), MIGRATIONS_DIR), + directory=str(get_migrations_directory()), version_to_start_from=latest_migration.version if latest_migration else None, ) diff --git a/jetbase/commands/validators.py b/jetbase/commands/validators.py index 0cf878b..0256074 100644 --- a/jetbase/commands/validators.py +++ b/jetbase/commands/validators.py @@ -1,37 +1,18 @@ -from pathlib import Path - -from jetbase.exceptions import DirectoryNotFoundError +from jetbase.paths import get_migrations_directory def validate_jetbase_directory() -> None: """ - Ensure command is run from jetbase directory with migrations folder. + Ensure a jetbase directory exists in the current directory or parent directories. - Validates that the current working directory is named 'jetbase' and - contains a 'migrations' subdirectory. This validation is required - before running most Jetbase CLI commands. + Validates that a 'jetbase' directory exists in the current working directory + or any of its parent directories, and that it contains a 'migrations' subdirectory. + This validation is required before running most Jetbase CLI commands. Returns: None: Returns silently if validation passes. Raises: - DirectoryNotFoundError: If the current directory is not named - 'jetbase' or if the 'migrations' subdirectory does not exist. + DirectoryNotFoundError: If the jetbase directory or migrations subdirectory does not exist. """ - current_dir = Path.cwd() - - # Check if current directory is named 'jetbase' - if current_dir.name != "jetbase": - raise DirectoryNotFoundError( - "Command must be run from the 'jetbase' directory.\n" - "You can run 'jetbase init' to create a Jetbase project." - ) - - # Check if migrations directory exists - migrations_dir = current_dir / "migrations" - if not migrations_dir.exists() or not migrations_dir.is_dir(): - raise DirectoryNotFoundError( - f"'migrations' directory not found in {current_dir}.\n" - "Add a migrations directory inside the 'jetbase' directory to proceed.\n" - "You can also run 'jetbase init' to create a Jetbase project." - ) + get_migrations_directory() diff --git a/jetbase/config.py b/jetbase/config.py index 059fccd..739c2b0 100644 --- a/jetbase/config.py +++ b/jetbase/config.py @@ -9,6 +9,7 @@ import tomli from jetbase.constants import ENV_FILE +from jetbase.paths import get_jetbase_directory @dataclass @@ -193,7 +194,7 @@ def _get_config_from_env_py(key: str, filepath: str = ENV_FILE) -> Any | None: Any | None: The configuration value if the attribute exists, otherwise None. """ - config_path: str = os.path.join(os.getcwd(), filepath) + config_path: str = str(get_jetbase_directory() / filepath) if not os.path.exists(config_path): return None diff --git a/jetbase/engine/repeatable.py b/jetbase/engine/repeatable.py index a6bfa4b..4dd5c5c 100644 --- a/jetbase/engine/repeatable.py +++ b/jetbase/engine/repeatable.py @@ -6,6 +6,7 @@ parse_upgrade_statements, validate_filename_format, ) +from jetbase.paths import get_migrations_directory from jetbase.repositories.migrations_repo import ( get_existing_on_change_filenames_to_checksums, ) @@ -87,7 +88,7 @@ def get_runs_on_change_filepaths( return runs_on_change_filepaths -def get_ra_filenames() -> list[str]: +def get_ra_filenames(directory: str | None = None) -> list[str]: """ Get all runs-always (RA__) migration filenames from the migrations directory. @@ -97,15 +98,17 @@ def get_ra_filenames() -> list[str]: Returns: list[str]: List of RA__ migration filenames (not full paths). """ + directory = directory or str(get_migrations_directory()) + ra_filenames: list[str] = [] - for root, _, files in os.walk(os.path.join(os.getcwd(), "migrations")): + for root, _, files in os.walk(directory): for filename in files: if filename.startswith(RUNS_ALWAYS_FILE_PREFIX): ra_filenames.append(filename) return ra_filenames -def get_repeatable_filenames() -> list[str]: +def get_repeatable_filenames(directory: str | None = None) -> list[str]: """ Get all repeatable migration filenames from the migrations directory. @@ -115,8 +118,10 @@ def get_repeatable_filenames() -> list[str]: Returns: list[str]: List of all repeatable migration filenames (not full paths). """ + directory = directory or str(get_migrations_directory()) + repeatable_filenames: list[str] = [] - for root, _, files in os.walk(os.path.join(os.getcwd(), "migrations")): + for root, _, files in os.walk(directory): for filename in files: if filename.startswith(RUNS_ALWAYS_FILE_PREFIX) or filename.startswith( RUNS_ON_CHANGE_FILE_PREFIX diff --git a/jetbase/engine/validation.py b/jetbase/engine/validation.py index 181cc8c..ebd912d 100644 --- a/jetbase/engine/validation.py +++ b/jetbase/engine/validation.py @@ -3,7 +3,6 @@ from packaging.version import parse as parse_version from jetbase.config import get_config -from jetbase.constants import MIGRATIONS_DIR from jetbase.engine.checksum import calculate_checksum from jetbase.engine.file_parser import parse_upgrade_statements from jetbase.engine.repeatable import get_repeatable_filenames @@ -14,6 +13,7 @@ ChecksumMismatchError, OutOfOrderMigrationError, ) +from jetbase.paths import get_migrations_directory from jetbase.repositories.migrations_repo import ( fetch_repeatable_migrations, get_checksums_by_version, @@ -197,7 +197,7 @@ def run_migration_validations( skip_checksum_validation_config: bool = get_config().skip_checksum_validation skip_file_validation_config: bool = get_config().skip_file_validation - migrations_directory_path: str = os.path.join(os.getcwd(), MIGRATIONS_DIR) + migrations_directory_path: str = str(get_migrations_directory()) migration_filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version( directory=migrations_directory_path @@ -222,7 +222,9 @@ def run_migration_validations( migrated_repeatable_filenames=[ r.filename for r in fetch_repeatable_migrations() ], - all_repeatable_filenames=get_repeatable_filenames(), + all_repeatable_filenames=get_repeatable_filenames( + directory=migrations_directory_path + ), ) migrated_filepaths_by_version: dict[str, str] = ( diff --git a/jetbase/paths.py b/jetbase/paths.py new file mode 100644 index 0000000..a6b8e10 --- /dev/null +++ b/jetbase/paths.py @@ -0,0 +1,55 @@ +from pathlib import Path + +from jetbase.constants import BASE_DIR, MIGRATIONS_DIR +from jetbase.exceptions import DirectoryNotFoundError + + +def get_jetbase_directory() -> Path: + """ + Get the path to the jetbase directory. + + Find the root project directory by looking for the 'jetbase' folder. + + Returns: + Path: The path to the jetbase directory. + + Raises: + DirectoryNotFoundError: If the jetbase directory cannot be found in the current path or + any parent directories. + """ + current_dir = Path.cwd() + + if current_dir.name == BASE_DIR: + return current_dir + + if (current_dir / BASE_DIR).is_dir(): + return current_dir / BASE_DIR + + for parent in current_dir.parents: + if (parent / BASE_DIR).is_dir(): + return parent / BASE_DIR + + raise DirectoryNotFoundError( + f"'{BASE_DIR}' directory not found. Run 'jetbase init' to create a Jetbase project." + ) + + +def get_migrations_directory() -> Path: + """ + Get the path to the migrations directory. + + Returns: + Path: The path to the migrations directory. + + Raises: + DirectoryNotFoundError: If the migrations directory does not exist in the jetbase directory. + """ + jetbase_dir = get_jetbase_directory() + migrations_dir = jetbase_dir / MIGRATIONS_DIR + if not migrations_dir.exists() or not migrations_dir.is_dir(): + raise DirectoryNotFoundError( + f"'{MIGRATIONS_DIR}' directory not found in {jetbase_dir}.\n" + "Add a migrations directory inside the 'jetbase' directory to proceed.\n" + "You can also run 'jetbase init' to create a Jetbase project." + ) + return migrations_dir diff --git a/pyproject.toml b/pyproject.toml index ffba3e8..e3ce8ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dev = [ "cryptography>=46.0.3", "pymysql>=1.1.2", "clickhouse-sqlalchemy>=0.3.0", + "pre-commit>=4.6.0", ] [project.optional-dependencies] @@ -58,3 +59,9 @@ markers = [ "dry_run: tests --dry-run param", "snowflake: tests for snowflake database", ] + +[tool.pyrefly] +project-includes = [ + "**/*.py*", + "**/*.ipynb", +] diff --git a/tests/cli/test_current.py b/tests/cli/test_current.py index 9523fd5..42da2c8 100644 --- a/tests/cli/test_current.py +++ b/tests/cli/test_current.py @@ -11,7 +11,6 @@ def test_current_success_versions_only( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -34,7 +33,6 @@ def test_current_success_repeatables( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -59,8 +57,6 @@ def test_current_no_migrations_or_tables( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") - caplog.clear() result = runner.invoke(app, ["current"]) assert result.exit_code == 0 diff --git a/tests/cli/test_fix_checksums.py b/tests/cli/test_fix_checksums.py index fe8a9f5..bf18c9f 100644 --- a/tests/cli/test_fix_checksums.py +++ b/tests/cli/test_fix_checksums.py @@ -12,7 +12,6 @@ def test_fix_checksums_success( runner, test_db_url, clean_db, setup_migrations_versions_only ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -32,7 +31,7 @@ def test_fix_checksums_success( assert initial_checksum is not None # Modify the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_modify = os.path.join(migrations_dir, "V2__m2.sql") with open(file_to_modify, "w") as f: f.write("\n-- Modified content\n") diff --git a/tests/cli/test_fix_files.py b/tests/cli/test_fix_files.py index 71f8168..32e5252 100644 --- a/tests/cli/test_fix_files.py +++ b/tests/cli/test_fix_files.py @@ -11,7 +11,6 @@ def test_fix_files_success( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -23,7 +22,7 @@ def test_fix_files_success( assert count == 3 # Delete the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_delete = os.path.join(migrations_dir, "V2__m2.sql") os.remove(file_to_delete) diff --git a/tests/cli/test_history.py b/tests/cli/test_history.py index 61bd983..55b1c98 100644 --- a/tests/cli/test_history.py +++ b/tests/cli/test_history.py @@ -1,12 +1,9 @@ -import os - from jetbase.cli.main import app def test_history_success_versions( runner, test_db_url, clean_db, setup_migrations_versions_only ): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -21,7 +18,6 @@ def test_history_success_versions( def test_history_success_repeatables(runner, test_db_url, clean_db, setup_migrations): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -40,7 +36,6 @@ def test_history_success_repeatables(runner, test_db_url, clean_db, setup_migrat def test_history_success_migrations_pending_versions( runner, test_db_url, clean_db, setup_migrations_versions_only ): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -57,7 +52,5 @@ def test_history_success_migrations_pending_versions( def test_history_empty_migrations_dir( runner, test_db_url, clean_db, setup_migrations_versions_only ): - os.chdir("jetbase") - result = runner.invoke(app, ["history"]) assert result.exit_code == 0 diff --git a/tests/cli/test_lock_status.py b/tests/cli/test_lock_status.py index 077b064..52dd15a 100644 --- a/tests/cli/test_lock_status.py +++ b/tests/cli/test_lock_status.py @@ -1,4 +1,3 @@ -import datetime as dt import os import uuid @@ -15,7 +14,6 @@ def test_lock_status_unlocked( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -32,7 +30,6 @@ def test_lock_status_locked( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -41,13 +38,12 @@ def test_lock_status_locked( """ UPDATE jetbase_lock SET is_locked = TRUE, - locked_at = :locked_at, + locked_at = CURRENT_TIMESTAMP, process_id = :process_id WHERE id = 1 AND is_locked = FALSE """ ), { - "locked_at": dt.datetime.now(dt.timezone.utc), "process_id": str(uuid.uuid4()), }, ) diff --git a/tests/cli/test_new.py b/tests/cli/test_new.py index c9fe36b..05a784e 100644 --- a/tests/cli/test_new.py +++ b/tests/cli/test_new.py @@ -24,7 +24,6 @@ def test_new_command_success(tmp_path): """Test the 'new' command successfully creates a migration file.""" os.chdir(tmp_path) result = runner.invoke(app, ["init"]) - os.chdir("jetbase") with patch("jetbase.commands.new.dt") as mock_dt: mock_dt.datetime.now.return_value.strftime.return_value = "20251214.160000" @@ -37,14 +36,13 @@ def test_new_command_success(tmp_path): # Check file was created with correct name expected_filename = "V20251214.160000__create_users_table.sql" - expected_filepath = Path("migrations") / expected_filename + expected_filepath = Path("jetbase") / "migrations" / expected_filename assert expected_filepath.exists() def test_new_success_file_content(tmp_path): os.chdir(tmp_path) result = runner.invoke(app, ["init"]) - os.chdir("jetbase") with patch("jetbase.commands.new.dt") as mock_dt: mock_dt.datetime.now.return_value.strftime.return_value = "20251214.160000" @@ -56,7 +54,7 @@ def test_new_success_file_content(tmp_path): assert result.exit_code == 0 expected_filename = "V20251214.160000__create_users_table.sql" - expected_filepath = Path("migrations") / expected_filename + expected_filepath = Path("jetbase") / "migrations" / expected_filename assert expected_filepath.exists() with open(expected_filepath, "r") as f: @@ -68,7 +66,6 @@ def test_new_command_with_custom_version(tmp_path): """Test the 'new' command successfully creates a migration file with a custom version.""" os.chdir(tmp_path) result = runner.invoke(app, ["init"]) - os.chdir("jetbase") with patch("jetbase.commands.new.dt") as mock_dt: mock_dt.datetime.now.return_value.strftime.return_value = "20251214.160000" @@ -81,5 +78,5 @@ def test_new_command_with_custom_version(tmp_path): # Check file was created with correct name expected_filename = "V1.5__create_users_table.sql" - expected_filepath = Path("migrations") / expected_filename + expected_filepath = Path("jetbase") / "migrations" / expected_filename assert expected_filepath.exists() diff --git a/tests/cli/test_rollback.py b/tests/cli/test_rollback.py index 33d4cf5..7c71987 100644 --- a/tests/cli/test_rollback.py +++ b/tests/cli/test_rollback.py @@ -11,7 +11,6 @@ def test_rollback(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -39,7 +38,6 @@ def test_rollback(runner, test_db_url, clean_db, setup_migrations): def test_rollback_with_count(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -69,7 +67,6 @@ def test_rollback_with_count(runner, test_db_url, clean_db, setup_migrations): def test_rollback_to_version(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -103,7 +100,6 @@ def test_rollback_to_version(runner, test_db_url, clean_db, setup_migrations): def test_rollback_with_dry_run(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -131,7 +127,6 @@ def test_rollback_with_deleted_file(runner, test_db_url, clean_db, setup_migrati os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index 0ec1c3a..761e187 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -6,7 +6,6 @@ def test_status_success_all_applied_versions( runner, test_db_url, clean_db, setup_migrations_versions_only ): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -33,7 +32,6 @@ def test_status_success_all_applied_versions( def test_status_success_repeatables(runner, test_db_url, clean_db, setup_migrations): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -66,7 +64,6 @@ def test_status_success_repeatables(runner, test_db_url, clean_db, setup_migrati def test_status_success_partial_applied_versions( runner, test_db_url, clean_db, setup_migrations_versions_only ): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -93,12 +90,11 @@ def test_status_success_partial_applied_versions( def test_status_success_roc_changed(runner, test_db_url, clean_db, setup_migrations): - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 # Modify the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations) file_to_modify = os.path.join(migrations_dir, "ROC__roc.sql") with open(file_to_modify, "w") as f: f.write("\n-- Modified content\n") @@ -130,8 +126,6 @@ def test_status_success_roc_changed(runner, test_db_url, clean_db, setup_migrati def test_status_success_roc_not_migrated( runner, test_db_url, clean_db, setup_migrations ): - os.chdir("jetbase") - result = runner.invoke(app, ["status"]) assert result.exit_code == 0 diff --git a/tests/cli/test_unlock.py b/tests/cli/test_unlock.py index e807c05..efe97b4 100644 --- a/tests/cli/test_unlock.py +++ b/tests/cli/test_unlock.py @@ -1,4 +1,3 @@ -import datetime as dt import os import uuid @@ -15,7 +14,6 @@ def test_unlock_already_unlocked( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -32,7 +30,6 @@ def test_lock_status_locked( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -41,13 +38,12 @@ def test_lock_status_locked( """ UPDATE jetbase_lock SET is_locked = TRUE, - locked_at = :locked_at, + locked_at = CURRENT_TIMESTAMP, process_id = :process_id WHERE id = 1 AND is_locked = FALSE """ ), { - "locked_at": dt.datetime.now(dt.timezone.utc), "process_id": str(uuid.uuid4()), }, ) diff --git a/tests/cli/test_upgrade.py b/tests/cli/test_upgrade.py index 0667804..b5c78a1 100644 --- a/tests/cli/test_upgrade.py +++ b/tests/cli/test_upgrade.py @@ -1,4 +1,5 @@ import os + import pytest from sqlalchemy import text @@ -16,7 +17,6 @@ def test_upgrade_versions( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -31,7 +31,6 @@ def test_upgrade_versions( def test_upgrade_count(runner, test_db_url, clean_db, setup_migrations_versions_only): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -61,7 +60,6 @@ def test_upgrade_count_greater_than_pending( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "20"]) assert result.exit_code == 0 @@ -88,7 +86,6 @@ def test_upgrade_count_negative( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "-1"]) assert result.exit_code != 0 @@ -103,7 +100,6 @@ def test_upgrade_to_version( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--to-version", "2"]) assert result.exit_code == 0 @@ -120,7 +116,6 @@ def test_upgrade_to_version_not_exists( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--to-version", "1.5"]) assert result.exit_code != 0 @@ -135,7 +130,6 @@ def test_upgrade_then_remove_files( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -147,7 +141,7 @@ def test_upgrade_then_remove_files( assert count == 3 # Remove a migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_remove = os.path.join(migrations_dir, "V2__m2.sql") if os.path.exists(file_to_remove): os.remove(file_to_remove) @@ -167,7 +161,6 @@ def test_upgrade_with_dry_run( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--dry-run"]) assert result.exit_code == 0 @@ -185,7 +178,6 @@ def test_upgrade_then_add_lower_version_file( os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -197,7 +189,7 @@ def test_upgrade_then_add_lower_version_file( assert count == 3 # Add a lower version migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) lower_version_file = os.path.join(migrations_dir, "V1.5__m1_5.sql") with open(lower_version_file, "w") as f: f.write("-- SQL statements for migration V1.5\n") @@ -216,9 +208,8 @@ def test_upgrade_with_duplicate_version_files( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") # Add a duplicate version migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) duplicate_version_file = os.path.join(migrations_dir, "V2__duplicate_m2.sql") with open(duplicate_version_file, "w") as f: f.write("-- SQL statements for duplicate migration V2\n") @@ -237,7 +228,6 @@ def test_upgrade_skip_checksum_validation( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -250,7 +240,7 @@ def test_upgrade_skip_checksum_validation( assert count == 3 # Modify the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_modify = os.path.join(migrations_dir, "V2__m2.sql") with open(file_to_modify, "w") as f: f.write("\n-- Modified content\n") @@ -279,7 +269,6 @@ def test_upgrade_repeatables(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -295,7 +284,6 @@ def test_roc_with_no_changes(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url with clean_db.begin() as connection: - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -332,7 +320,6 @@ def test_roc_with_no_changes(runner, test_db_url, clean_db, setup_migrations): def test_roc_with_changes(runner, test_db_url, clean_db, setup_migrations): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -352,7 +339,7 @@ def test_roc_with_changes(runner, test_db_url, clean_db, setup_migrations): applied_at_before = timestamp_result.scalar() # Modify the ROC migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations) roc_file = os.path.join(migrations_dir, "ROC__roc.sql") with open(roc_file, "w") as f: f.write("\n-- Modified content to trigger reapplication\n") @@ -379,7 +366,6 @@ def test_repeatable_always_multiple_upgrades( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade"]) assert result.exit_code == 0 @@ -420,7 +406,6 @@ def test_upgrade_skip_validation( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -433,7 +418,7 @@ def test_upgrade_skip_validation( assert count == 3 # Modify the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_modify = os.path.join(migrations_dir, "V2__m2.sql") with open(file_to_modify, "w") as f: f.write("\n-- Modified content\n") @@ -446,7 +431,7 @@ def test_upgrade_skip_validation( ) # Add a lower version migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) lower_version_file = os.path.join(migrations_dir, "V1.5__m1_5.sql") with open(lower_version_file, "w") as f: f.write("-- SQL statements for migration V1.5\n") @@ -474,7 +459,6 @@ def test_upgrade_skip_file_validation_lower_file( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -487,7 +471,7 @@ def test_upgrade_skip_file_validation_lower_file( assert count == 3 # Add a lower version migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) lower_version_file = os.path.join(migrations_dir, "V1.5__m1_5.sql") with open(lower_version_file, "w") as f: f.write("-- SQL statements for migration V1.5\n") @@ -515,7 +499,6 @@ def test_upgrade_skip_file_validation_deleted_migrated_file( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -528,7 +511,7 @@ def test_upgrade_skip_file_validation_deleted_migrated_file( assert count == 3 # Delete the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_delete = os.path.join(migrations_dir, "V2__m2.sql") os.remove(file_to_delete) diff --git a/tests/cli/test_validate_checksums.py b/tests/cli/test_validate_checksums.py index be65708..6e6f628 100644 --- a/tests/cli/test_validate_checksums.py +++ b/tests/cli/test_validate_checksums.py @@ -1,5 +1,6 @@ -import pytest import os + +import pytest from sqlalchemy import text from jetbase.cli.main import app @@ -20,7 +21,6 @@ def test_validate_checksums_with_fix( ): os.environ["JETBASE_SQLALCHEMY_URL"] = test_db_url - os.chdir("jetbase") result = runner.invoke(app, ["upgrade", "--count", "3"]) assert result.exit_code == 0 @@ -40,7 +40,7 @@ def test_validate_checksums_with_fix( assert initial_checksum is not None # Modify the migration file - migrations_dir = os.path.join(os.getcwd(), "migrations") + migrations_dir = str(setup_migrations_versions_only) file_to_modify = os.path.join(migrations_dir, "V2__m2.sql") with open(file_to_modify, "w") as f: f.write("\n-- Modified content\n") diff --git a/tests/unit/commands/test_new.py b/tests/unit/commands/test_new.py index 67fa6bf..7cf7750 100644 --- a/tests/unit/commands/test_new.py +++ b/tests/unit/commands/test_new.py @@ -1,10 +1,11 @@ import tempfile +from pathlib import Path from unittest.mock import patch import pytest from jetbase.commands.new import _generate_new_filename, generate_new_migration_file_cmd -from jetbase.constants import MIGRATIONS_DIR +from jetbase.constants import BASE_DIR, MIGRATIONS_DIR from jetbase.exceptions import ( DirectoryNotFoundError, InvalidVersionError, @@ -15,11 +16,11 @@ def test_generate_new_migration_file_cmd_success(tmp_path, caplog): """Test successful generation of a new migration file.""" # Create migrations directory - migrations_dir = tmp_path / MIGRATIONS_DIR + migrations_dir = tmp_path / BASE_DIR / MIGRATIONS_DIR migrations_dir.mkdir(parents=True) - # Mock os.getcwd to return tmp_path - with patch("os.getcwd", return_value=str(tmp_path)): + # Mock pathlib.Path.cwd to return tmp_path + with patch("pathlib.Path.cwd", return_value=tmp_path): # Mock datetime to get predictable timestamp with patch("jetbase.commands.new.dt") as mock_dt: mock_dt.datetime.now.return_value.strftime.return_value = "20251214.153000" @@ -40,11 +41,10 @@ def test_generate_new_migration_file_cmd_success(tmp_path, caplog): def test_generate_new_migration_file_cmd_with_custom_version(tmp_path, caplog): """Test successful generation of a migration file with a custom version.""" # Create migrations directory - migrations_dir = tmp_path / MIGRATIONS_DIR + migrations_dir = tmp_path / BASE_DIR / MIGRATIONS_DIR migrations_dir.mkdir(parents=True) - # Mock os.getcwd to return tmp_path - with patch("os.getcwd", return_value=str(tmp_path)): + with patch("pathlib.Path.cwd", return_value=tmp_path): # Generate migration file with custom version generate_new_migration_file_cmd("create users table", version="1.5") @@ -61,14 +61,14 @@ def test_generate_new_migration_file_cmd_with_custom_version(tmp_path, caplog): def test_generate_new_migration_file_cmd_directory_not_found(): """Test that DirectoryNotFoundError is raised when migrations directory doesn't exist.""" with tempfile.TemporaryDirectory() as tmpdir: - # Mock os.getcwd to return directory without migrations folder - with patch("os.getcwd", return_value=tmpdir): + # Mock pathlib.Path.cwd to return directory without migrations folder + with patch("pathlib.Path.cwd", return_value=Path(tmpdir)): with pytest.raises(DirectoryNotFoundError) as exc_info: generate_new_migration_file_cmd("create users table") # Check error message - assert "Migrations directory not found" in str(exc_info.value) - assert "jetbase initialize" in str(exc_info.value) + assert "'jetbase' directory not found" in str(exc_info.value) + assert "jetbase init" in str(exc_info.value) def test_generate_new_filename_with_timestamp(): diff --git a/tests/unit/commands/test_rollback.py b/tests/unit/commands/test_rollback.py index 5920ca5..6a30f84 100644 --- a/tests/unit/commands/test_rollback.py +++ b/tests/unit/commands/test_rollback.py @@ -47,8 +47,11 @@ def test_defaults_to_one(self, mock_get_latest: Mock) -> None: class TestGetVersionsToRollback: """Tests for the _get_versions_to_rollback function.""" + @patch("jetbase.commands.rollback.get_migrations_directory") @patch("jetbase.commands.rollback.get_migration_filepaths_by_version") - def test_returns_reversed_dict(self, mock_get_filepaths: Mock) -> None: + def test_returns_reversed_dict( + self, mock_get_filepaths: Mock, mock_get_migrations_directory: Mock + ) -> None: """Test that versions are returned in reverse order.""" mock_get_filepaths.return_value = { "1": "/path/V1__test.sql", diff --git a/tests/unit/engine/test_file_parser.py b/tests/unit/engine/test_file_parser.py index bb0d4aa..0f31d82 100644 --- a/tests/unit/engine/test_file_parser.py +++ b/tests/unit/engine/test_file_parser.py @@ -5,15 +5,15 @@ import pytest from jetbase.engine.file_parser import ( + _extract_delimiter_from_file, _get_raw_description_from_filename, _get_version_from_filename, - is_valid_version, get_description_from_filename, is_filename_format_valid, is_filename_length_valid, + is_valid_version, parse_rollback_statements, parse_upgrade_statements, - _extract_delimiter_from_file, ) diff --git a/tests/unit/engine/test_repeatable.py b/tests/unit/engine/test_repeatable.py index ed4e830..0af5728 100644 --- a/tests/unit/engine/test_repeatable.py +++ b/tests/unit/engine/test_repeatable.py @@ -68,7 +68,10 @@ def test_returns_ra_filenames(self, tmp_path: Path) -> None: (migrations_dir / "RA__test.sql").touch() (migrations_dir / "V1__other.sql").touch() - with patch("jetbase.engine.repeatable.os.getcwd", return_value=str(tmp_path)): + with patch( + "jetbase.engine.repeatable.get_migrations_directory", + return_value=migrations_dir, + ): result = get_ra_filenames() assert result == ["RA__test.sql"] @@ -85,7 +88,10 @@ def test_returns_all_repeatable_filenames(self, tmp_path: Path) -> None: (migrations_dir / "ROC__test.sql").touch() (migrations_dir / "V1__other.sql").touch() - with patch("jetbase.engine.repeatable.os.getcwd", return_value=str(tmp_path)): + with patch( + "jetbase.engine.repeatable.get_migrations_directory", + return_value=migrations_dir, + ): result = get_repeatable_filenames() assert len(result) == 2 diff --git a/tests/unit/engine/test_validation.py b/tests/unit/engine/test_validation.py index 0db63ce..2026c8f 100644 --- a/tests/unit/engine/test_validation.py +++ b/tests/unit/engine/test_validation.py @@ -15,6 +15,7 @@ DirectoryNotFoundError, OutOfOrderMigrationError, ) +from jetbase.paths import get_migrations_directory class TestValidateJetbaseDirectory: @@ -24,20 +25,60 @@ def test_success(self, tmp_path: Path) -> None: jetbase_dir.mkdir() (jetbase_dir / "migrations").mkdir() - with patch("jetbase.commands.validators.Path.cwd", return_value=jetbase_dir): + with patch("jetbase.paths.Path.cwd", return_value=jetbase_dir): validate_jetbase_directory() def test_wrong_directory_name(self, tmp_path: Path) -> None: - """Test validation fails when not in a directory named 'jetbase'.""" + """Test validation fails when no jetbase directory exists in the directory tree.""" wrong_dir = tmp_path / "wrong_name" wrong_dir.mkdir() (wrong_dir / "migrations").mkdir() - with patch("jetbase.commands.validators.Path.cwd", return_value=wrong_dir): + with patch("jetbase.paths.Path.cwd", return_value=wrong_dir): with pytest.raises(DirectoryNotFoundError): validate_jetbase_directory() +class TestGetMigrationsDirectory: + def test_success(self, tmp_path: Path) -> None: + """Test getting migrations directory succeeds when it exists.""" + jetbase_dir = tmp_path / "jetbase" + jetbase_dir.mkdir() + migrations_dir = jetbase_dir / "migrations" + migrations_dir.mkdir() + another_dir = jetbase_dir / "another_dir" + another_dir.mkdir() + + with patch("jetbase.paths.Path.cwd", return_value=jetbase_dir): + result = get_migrations_directory() + assert result == migrations_dir + + with patch("jetbase.paths.Path.cwd", return_value=another_dir): + result = get_migrations_directory() + assert result == migrations_dir + + with patch("jetbase.paths.Path.cwd", return_value=tmp_path): + result = get_migrations_directory() + assert result == migrations_dir + + def test_missing_migrations_directory(self, tmp_path: Path) -> None: + """Test getting migrations directory fails when it doesn't exist.""" + jetbase_dir = tmp_path / "jetbase" + jetbase_dir.mkdir() + + with patch("jetbase.paths.Path.cwd", return_value=jetbase_dir): + with pytest.raises(DirectoryNotFoundError) as exc_info: + get_migrations_directory() + assert "'migration' directory not found" in str(exc_info.value) + + def test_missing_jetbase_directory(self, tmp_path: Path) -> None: + """Test getting migrations directory fails when jetbase directory doesn't exist.""" + with patch("jetbase.paths.Path.cwd", return_value=tmp_path): + with pytest.raises(DirectoryNotFoundError) as exc_info: + get_migrations_directory() + assert "'jetbase' directory not found" in str(exc_info.value) + + class TestValidateMigratedVersionsInCurrentMigrationFiles: def test_passes_when_all_files_exist(self) -> None: """Test validation passes when all migrated versions have files.""" diff --git a/uv.lock b/uv.lock index 3f77d45..2a2761f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -154,6 +154,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -517,6 +526,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/02/e44836f3181dd879a6af5070f548caab3dd1c203bf07d827e1b351f15e8b/databricks_sqlalchemy-2.0.8-py3-none-any.whl", hash = "sha256:755b6f9dc2aaabfdc1243064f4054b8dc325da342bc9939b84988e00938b3c56", size = 27401, upload-time = "2025-09-08T04:54:16.792Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -540,11 +558,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] @@ -556,7 +574,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, - { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, @@ -567,7 +584,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, @@ -578,7 +594,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, @@ -589,7 +604,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, @@ -600,7 +614,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, @@ -608,6 +621,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -654,6 +676,7 @@ snowflake = [ dev = [ { name = "clickhouse-sqlalchemy" }, { name = "cryptography" }, + { name = "pre-commit" }, { name = "psycopg2-binary" }, { name = "pymysql" }, { name = "pyrefly" }, @@ -680,6 +703,7 @@ provides-extras = ["snowflake", "databricks", "clickhouse"] dev = [ { name = "clickhouse-sqlalchemy", specifier = ">=0.3.0" }, { name = "cryptography", specifier = ">=46.0.3" }, + { name = "pre-commit", specifier = ">=4.6.0" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pymysql", specifier = ">=1.1.2" }, { name = "pyrefly", specifier = ">=0.38.2" }, @@ -762,6 +786,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -1022,6 +1055,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.11" @@ -1246,6 +1295,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1255,6 +1317,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -1574,6 +1700,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "virtualenv" +version = "21.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, +] + [[package]] name = "zstd" version = "1.5.7.3"