jetbase 0.7.0__py3-none-any.whl → 0.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. jetbase/cli/main.py +146 -4
  2. jetbase/commands/current.py +20 -0
  3. jetbase/commands/fix_checksums.py +172 -0
  4. jetbase/commands/fix_files.py +133 -0
  5. jetbase/commands/history.py +53 -0
  6. jetbase/commands/init.py +29 -0
  7. jetbase/commands/lock_status.py +25 -0
  8. jetbase/commands/new.py +65 -0
  9. jetbase/commands/rollback.py +172 -0
  10. jetbase/commands/status.py +212 -0
  11. jetbase/commands/unlock.py +29 -0
  12. jetbase/commands/upgrade.py +248 -0
  13. jetbase/commands/validators.py +37 -0
  14. jetbase/config.py +304 -25
  15. jetbase/constants.py +10 -2
  16. jetbase/database/connection.py +40 -0
  17. jetbase/database/queries/base.py +353 -0
  18. jetbase/database/queries/default_queries.py +215 -0
  19. jetbase/database/queries/postgres.py +14 -0
  20. jetbase/database/queries/query_loader.py +87 -0
  21. jetbase/database/queries/sqlite.py +197 -0
  22. jetbase/engine/checksum.py +25 -0
  23. jetbase/engine/dry_run.py +105 -0
  24. jetbase/engine/file_parser.py +324 -0
  25. jetbase/engine/formatters.py +61 -0
  26. jetbase/engine/lock.py +65 -0
  27. jetbase/engine/repeatable.py +125 -0
  28. jetbase/engine/validation.py +238 -0
  29. jetbase/engine/version.py +144 -0
  30. jetbase/enums.py +37 -1
  31. jetbase/exceptions.py +87 -0
  32. jetbase/models.py +45 -0
  33. jetbase/repositories/lock_repo.py +129 -0
  34. jetbase/repositories/migrations_repo.py +451 -0
  35. jetbase-0.12.1.dist-info/METADATA +135 -0
  36. jetbase-0.12.1.dist-info/RECORD +39 -0
  37. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/WHEEL +1 -1
  38. jetbase/core/dry_run.py +0 -38
  39. jetbase/core/file_parser.py +0 -199
  40. jetbase/core/initialize.py +0 -33
  41. jetbase/core/repository.py +0 -169
  42. jetbase/core/rollback.py +0 -67
  43. jetbase/core/upgrade.py +0 -75
  44. jetbase/core/version.py +0 -163
  45. jetbase/queries.py +0 -72
  46. jetbase-0.7.0.dist-info/METADATA +0 -12
  47. jetbase-0.7.0.dist-info/RECORD +0 -17
  48. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/entry_points.txt +0 -0
  49. {jetbase-0.7.0.dist-info → jetbase-0.12.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,172 @@
1
+ import os
2
+
3
+ from jetbase.engine.dry_run import process_dry_run
4
+ from jetbase.engine.file_parser import parse_rollback_statements
5
+ from jetbase.engine.lock import (
6
+ migration_lock,
7
+ )
8
+ from jetbase.engine.version import get_migration_filepaths_by_version
9
+ from jetbase.enums import MigrationDirectionType
10
+ from jetbase.exceptions import VersionNotFoundError
11
+ from jetbase.repositories.lock_repo import create_lock_table_if_not_exists
12
+ from jetbase.repositories.migrations_repo import (
13
+ create_migrations_table_if_not_exists,
14
+ get_latest_versions,
15
+ run_migration,
16
+ )
17
+
18
+
19
+ def rollback_cmd(
20
+ count: int | None = None, to_version: str | None = None, dry_run: bool = False
21
+ ) -> None:
22
+ """
23
+ Rollback applied migrations.
24
+
25
+ Reverts previously applied migrations by executing their rollback SQL
26
+ statements in reverse order. Can rollback a specific number of migrations
27
+ or all migrations after a specified version.
28
+
29
+ Args:
30
+ count (int | None): Number of migrations to rollback. If None and
31
+ to_version is also None, defaults to 1. Defaults to None.
32
+ to_version (str | None): Rollback all migrations applied after this
33
+ version. Cannot be used with count. Defaults to None.
34
+ dry_run (bool): If True, shows a preview of the rollback SQL without
35
+ executing it. Defaults to False.
36
+
37
+ Returns:
38
+ None: Prints rollback status for each migration to stdout.
39
+
40
+ Raises:
41
+ ValueError: If both count and to_version are specified.
42
+ VersionNotFoundError: If a required migration file is missing.
43
+ """
44
+ create_migrations_table_if_not_exists()
45
+ create_lock_table_if_not_exists()
46
+
47
+ if count is not None and to_version is not None:
48
+ raise ValueError(
49
+ "Cannot specify both 'count' and 'to_version' for rollback. "
50
+ "Select only one, or do not specify either to rollback the last migration."
51
+ )
52
+ if count is None and to_version is None:
53
+ count = 1
54
+
55
+ latest_migration_versions: list[str] = _get_latest_migration_versions(
56
+ count=count, to_version=to_version
57
+ )
58
+
59
+ if not latest_migration_versions:
60
+ print("Nothing to rollback.")
61
+ return
62
+
63
+ versions_to_rollback: dict[str, str] = _get_versions_to_rollback(
64
+ latest_migration_versions=latest_migration_versions
65
+ )
66
+
67
+ _validate_rollback_files_exist(
68
+ latest_migration_versions=latest_migration_versions,
69
+ versions_to_rollback=versions_to_rollback,
70
+ )
71
+
72
+ if not dry_run:
73
+ with migration_lock():
74
+ print("Starting rollback...")
75
+ for version, file_path in versions_to_rollback.items():
76
+ sql_statements: list[str] = parse_rollback_statements(
77
+ file_path=file_path
78
+ )
79
+ filename: str = os.path.basename(file_path)
80
+
81
+ run_migration(
82
+ sql_statements=sql_statements,
83
+ version=version,
84
+ migration_operation=MigrationDirectionType.ROLLBACK,
85
+ filename=filename,
86
+ )
87
+
88
+ print(f"Rollback applied successfully: {filename}")
89
+ print("Rollbacks completed successfully.")
90
+
91
+ else:
92
+ process_dry_run(
93
+ version_to_filepath=versions_to_rollback,
94
+ migration_operation=MigrationDirectionType.ROLLBACK,
95
+ )
96
+
97
+
98
+ def _get_latest_migration_versions(
99
+ count: int | None = None, to_version: str | None = None
100
+ ) -> list[str]:
101
+ """
102
+ Get the latest migration versions from the database.
103
+
104
+ Retrieves migration versions either by count or by starting version.
105
+ If neither is specified, returns the single most recent migration.
106
+
107
+ Args:
108
+ count (int | None): Number of migrations to retrieve.
109
+ Defaults to None.
110
+ to_version (str | None): Retrieve all migrations applied after
111
+ this version. Defaults to None.
112
+
113
+ Returns:
114
+ list[str]: List of version strings in order of application.
115
+ """
116
+ if count:
117
+ return get_latest_versions(limit=count)
118
+ elif to_version:
119
+ return get_latest_versions(starting_version=to_version)
120
+ else:
121
+ return get_latest_versions(limit=1)
122
+
123
+
124
+ def _get_versions_to_rollback(latest_migration_versions: list[str]) -> dict[str, str]:
125
+ """
126
+ Get migration file paths for versions to rollback.
127
+
128
+ Maps version strings to their corresponding file paths, ordered
129
+ in reverse (newest first) for rollback execution.
130
+
131
+ Args:
132
+ latest_migration_versions (list[str]): List of version strings
133
+ to rollback, ordered oldest to newest.
134
+
135
+ Returns:
136
+ dict[str, str]: Mapping of version to file path, reversed so
137
+ newest versions are rolled back first.
138
+ """
139
+ versions_to_rollback: dict[str, str] = get_migration_filepaths_by_version(
140
+ directory=os.path.join(os.getcwd(), "migrations"),
141
+ version_to_start_from=latest_migration_versions[-1],
142
+ end_version=latest_migration_versions[0],
143
+ )
144
+
145
+ return dict(reversed(versions_to_rollback.items()))
146
+
147
+
148
+ def _validate_rollback_files_exist(
149
+ latest_migration_versions: list[str], versions_to_rollback: dict[str, str]
150
+ ) -> None:
151
+ """
152
+ Validate that all migration files exist for rollback.
153
+
154
+ Ensures every version that needs to be rolled back has a corresponding
155
+ migration file in the migrations directory.
156
+
157
+ Args:
158
+ latest_migration_versions (list[str]): Version strings that need
159
+ to be rolled back.
160
+ versions_to_rollback (dict[str, str]): Mapping of version to
161
+ file path for available migration files.
162
+
163
+ Raises:
164
+ VersionNotFoundError: If any migration file is missing.
165
+ """
166
+ for version in latest_migration_versions:
167
+ if version not in list(versions_to_rollback.keys()):
168
+ raise VersionNotFoundError(
169
+ f"Migration file for version '{version}' not found. Cannot proceed with rollback.\n"
170
+ "Please restore the missing migration file and try again, or run 'jetbase fix' "
171
+ "to synchronize the migrations table with existing files before retrying the rollback."
172
+ )
@@ -0,0 +1,212 @@
1
+ import os
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from jetbase.engine.file_parser import get_description_from_filename
7
+ from jetbase.engine.formatters import get_display_version
8
+ from jetbase.engine.repeatable import get_ra_filenames, get_runs_on_change_filepaths
9
+ from jetbase.engine.version import get_migration_filepaths_by_version
10
+ from jetbase.enums import MigrationType
11
+ from jetbase.models import MigrationRecord
12
+ from jetbase.repositories.migrations_repo import (
13
+ create_migrations_table_if_not_exists,
14
+ get_existing_on_change_filenames_to_checksums,
15
+ get_migration_records,
16
+ migrations_table_exists,
17
+ )
18
+
19
+
20
+ def status_cmd() -> None:
21
+ """
22
+ Display applied and pending migrations.
23
+
24
+ Shows two tables: one listing all migrations that have been applied to
25
+ the database, and another listing pending migrations that are available
26
+ in the migrations directory but have not yet been applied.
27
+
28
+ Returns:
29
+ None: Prints formatted tables to stdout showing migration status.
30
+ """
31
+ is_migrations_table: bool = migrations_table_exists()
32
+ if not is_migrations_table:
33
+ create_migrations_table_if_not_exists()
34
+ is_migrations_table = True
35
+
36
+ migration_records: list[MigrationRecord] = (
37
+ get_migration_records() if is_migrations_table else []
38
+ )
39
+
40
+ versioned_migration_records: list[MigrationRecord] = [
41
+ record
42
+ for record in migration_records
43
+ if record.migration_type == MigrationType.VERSIONED.value
44
+ ]
45
+
46
+ latest_migrated_version: str | None = (
47
+ versioned_migration_records[-1].version if versioned_migration_records else None
48
+ )
49
+
50
+ pending_versioned_filepaths: dict[str, str] = get_migration_filepaths_by_version(
51
+ directory=os.path.join(os.getcwd(), "migrations"),
52
+ version_to_start_from=latest_migrated_version,
53
+ )
54
+
55
+ if latest_migrated_version:
56
+ pending_versioned_filepaths = dict(
57
+ list(pending_versioned_filepaths.items())[1:]
58
+ )
59
+
60
+ all_roc_filenames: list[str] = get_ra_filenames()
61
+
62
+ roc_filenames_changed_only: list[str] = [
63
+ os.path.basename(filepath)
64
+ for filepath in get_runs_on_change_filepaths(
65
+ directory=os.path.join(os.getcwd(), "migrations"), changed_only=True
66
+ )
67
+ ]
68
+
69
+ roc_filenames_migrated: list[str] = list(
70
+ get_existing_on_change_filenames_to_checksums().keys()
71
+ )
72
+
73
+ all_roc_filenames: list[str] = [
74
+ os.path.basename(filepath)
75
+ for filepath in get_runs_on_change_filepaths(
76
+ directory=os.path.join(os.getcwd(), "migrations")
77
+ )
78
+ ]
79
+
80
+ console = Console()
81
+
82
+ applied_table: Table = _create_migrations_display_table(title="Migrations Applied")
83
+
84
+ _add_applied_rows(table=applied_table, migration_records=migration_records)
85
+
86
+ console.print(applied_table)
87
+ console.print()
88
+
89
+ pending_table: Table = _create_migrations_display_table(title="Migrations Pending")
90
+
91
+ _add_pending_rows(
92
+ table=pending_table,
93
+ pending_versioned_filepaths=pending_versioned_filepaths,
94
+ migration_records=migration_records,
95
+ roc_filenames_changed_only=roc_filenames_changed_only,
96
+ all_roc_filenames=all_roc_filenames,
97
+ roc_filenames_migrated=roc_filenames_migrated,
98
+ )
99
+
100
+ console.print(pending_table)
101
+
102
+
103
+ def _create_migrations_display_table(title: str) -> Table:
104
+ """
105
+ Create a rich Table for displaying migrations.
106
+
107
+ Configures a table with styled columns for displaying migration
108
+ version numbers and descriptions.
109
+
110
+ Args:
111
+ title (str): The title to display above the table.
112
+
113
+ Returns:
114
+ Table: A configured rich Table with Version and Description columns.
115
+ """
116
+ display_table: Table = Table(
117
+ title=title, show_header=True, header_style="bold magenta"
118
+ )
119
+ display_table.add_column("Version", style="cyan")
120
+ display_table.add_column("Description", style="green")
121
+
122
+ return display_table
123
+
124
+
125
+ def _add_applied_rows(table: Table, migration_records: list[MigrationRecord]) -> None:
126
+ """
127
+ Add applied migration rows to the table.
128
+
129
+ Populates the table with rows for each applied migration, displaying
130
+ the version number and description for both versioned and repeatable
131
+ migrations.
132
+
133
+ Args:
134
+ table (Table): The rich Table to add rows to.
135
+ migration_records (list[MigrationRecord]): List of migration records
136
+ that have been applied to the database.
137
+
138
+ Returns:
139
+ None: Modifies the table in place.
140
+ """
141
+ for record in migration_records:
142
+ if record.migration_type == MigrationType.VERSIONED.value:
143
+ table.add_row(record.version, record.description)
144
+ else:
145
+ table.add_row(
146
+ get_display_version(migration_type=record.migration_type),
147
+ record.description,
148
+ )
149
+
150
+
151
+ def _add_pending_rows(
152
+ table: Table,
153
+ pending_versioned_filepaths: dict[str, str],
154
+ migration_records: list[MigrationRecord],
155
+ roc_filenames_changed_only: list[str],
156
+ all_roc_filenames: list[str],
157
+ roc_filenames_migrated: list[str],
158
+ ) -> None:
159
+ """
160
+ Add pending migration rows to the table.
161
+
162
+ Populates the table with rows for each pending migration including
163
+ versioned migrations, runs-always migrations, and runs-on-change
164
+ migrations that have been modified or not yet applied.
165
+
166
+ Args:
167
+ table (Table): The rich Table to add rows to.
168
+ pending_versioned_filepaths (dict[str, str]): Mapping of version
169
+ strings to file paths for pending versioned migrations.
170
+ migration_records (list[MigrationRecord]): All migration records
171
+ from the database.
172
+ roc_filenames_changed_only (list[str]): Filenames of runs-on-change
173
+ migrations that have been modified since last applied.
174
+ all_roc_filenames (list[str]): All runs-on-change migration filenames.
175
+ roc_filenames_migrated (list[str]): Runs-on-change migrations that
176
+ have been previously applied.
177
+
178
+ Returns:
179
+ None: Modifies the table in place.
180
+ """
181
+ for version, filepath in pending_versioned_filepaths.items():
182
+ table.add_row(
183
+ version, get_description_from_filename(filename=os.path.basename(filepath))
184
+ )
185
+
186
+ # Runs always
187
+ for ra_filename in get_ra_filenames():
188
+ description: str = get_description_from_filename(filename=ra_filename)
189
+ table.add_row(
190
+ get_display_version(migration_type=MigrationType.RUNS_ALWAYS.value),
191
+ description,
192
+ )
193
+
194
+ # Runs on change - changed only
195
+ for record in migration_records:
196
+ if (
197
+ record.migration_type == MigrationType.RUNS_ON_CHANGE.value
198
+ and record.filename in roc_filenames_changed_only
199
+ ):
200
+ table.add_row(
201
+ get_display_version(migration_type=MigrationType.RUNS_ON_CHANGE.value),
202
+ record.description,
203
+ )
204
+
205
+ # Runs on change - new
206
+ for filename in all_roc_filenames:
207
+ if filename not in roc_filenames_migrated:
208
+ description: str = get_description_from_filename(filename=filename)
209
+ table.add_row(
210
+ get_display_version(migration_type=MigrationType.RUNS_ON_CHANGE.value),
211
+ description,
212
+ )
@@ -0,0 +1,29 @@
1
+ from jetbase.config import get_config
2
+ from jetbase.repositories.lock_repo import (
3
+ lock_table_exists,
4
+ unlock_database,
5
+ )
6
+ from jetbase.repositories.migrations_repo import migrations_table_exists
7
+
8
+ sqlalchemy_url: str = get_config(required={"sqlalchemy_url"}).sqlalchemy_url
9
+
10
+
11
+ def unlock_cmd() -> None:
12
+ """
13
+ Force release the migration lock.
14
+
15
+ Unconditionally releases the migration lock in the jetbase_lock table.
16
+ Use this only if you are certain that no migration is currently running,
17
+ as unlocking during an active migration can cause database corruption.
18
+
19
+ Returns:
20
+ None: Prints "Unlock successful." to stdout.
21
+ """
22
+
23
+ if not lock_table_exists() or not migrations_table_exists():
24
+ print("Unlock successful.")
25
+ return
26
+ #
27
+ unlock_database()
28
+
29
+ print("Unlock successful.")
@@ -0,0 +1,248 @@
1
+ import os
2
+
3
+ from jetbase.constants import MIGRATIONS_DIR
4
+ from jetbase.engine.dry_run import process_dry_run
5
+ from jetbase.engine.file_parser import parse_upgrade_statements
6
+ from jetbase.engine.lock import migration_lock
7
+ from jetbase.engine.repeatable import (
8
+ get_repeatable_always_filepaths,
9
+ get_runs_on_change_filepaths,
10
+ )
11
+ from jetbase.engine.validation import run_migration_validations
12
+ from jetbase.engine.version import (
13
+ get_migration_filepaths_by_version,
14
+ )
15
+ from jetbase.enums import MigrationDirectionType, MigrationType
16
+ from jetbase.models import MigrationRecord
17
+ from jetbase.repositories.lock_repo import create_lock_table_if_not_exists
18
+ from jetbase.repositories.migrations_repo import (
19
+ create_migrations_table_if_not_exists,
20
+ fetch_latest_versioned_migration,
21
+ get_existing_on_change_filenames_to_checksums,
22
+ get_existing_repeatable_always_migration_filenames,
23
+ run_migration,
24
+ run_update_repeatable_migration,
25
+ )
26
+
27
+
28
+ def upgrade_cmd(
29
+ count: int | None = None,
30
+ to_version: str | None = None,
31
+ dry_run: bool = False,
32
+ skip_validation: bool = False,
33
+ skip_checksum_validation: bool = False,
34
+ skip_file_validation: bool = False,
35
+ ) -> None:
36
+ """
37
+ Apply pending migrations to the database in order.
38
+
39
+ Args:
40
+ count (int | None): Maximum number of migrations to apply.
41
+ to_version (str | None): Apply migrations up to this version.
42
+ dry_run (bool): Preview SQL without executing.
43
+ skip_validation (bool): Skip all validations.
44
+ skip_checksum_validation (bool): Skip checksum validation only.
45
+ skip_file_validation (bool): Skip file validation only.
46
+
47
+ Raises:
48
+ ValueError: If both count and to_version are specified.
49
+ """
50
+
51
+ if count is not None and to_version is not None:
52
+ raise ValueError(
53
+ "Cannot specify both 'count' and 'to_version' for upgrade. "
54
+ "Select only one, or do not specify either to run all pending migrations."
55
+ )
56
+
57
+ if count:
58
+ if count < 1 or not isinstance(count, int):
59
+ raise ValueError("'count' must be a positive integer.")
60
+
61
+ create_migrations_table_if_not_exists()
62
+ create_lock_table_if_not_exists()
63
+
64
+ latest_migration: MigrationRecord | None = fetch_latest_versioned_migration()
65
+
66
+ if latest_migration:
67
+ run_migration_validations(
68
+ latest_migrated_version=latest_migration.version,
69
+ skip_validation=skip_validation,
70
+ skip_checksum_validation=skip_checksum_validation,
71
+ skip_file_validation=skip_file_validation,
72
+ )
73
+
74
+ filepaths_by_version: dict[str, str] = _get_filepaths_by_version(
75
+ latest_migration=latest_migration,
76
+ count=count,
77
+ to_version=to_version,
78
+ )
79
+
80
+ repeatable_always_filepaths: list[str] = get_repeatable_always_filepaths(
81
+ directory=os.path.join(os.getcwd(), MIGRATIONS_DIR)
82
+ )
83
+
84
+ runs_on_change_filepaths: list[str] = get_runs_on_change_filepaths(
85
+ directory=os.path.join(os.getcwd(), MIGRATIONS_DIR),
86
+ changed_only=True,
87
+ )
88
+
89
+ if not dry_run:
90
+ if (
91
+ not filepaths_by_version
92
+ and not repeatable_always_filepaths
93
+ and not runs_on_change_filepaths
94
+ ):
95
+ print("Migrations are up to date.")
96
+ return
97
+
98
+ with migration_lock():
99
+ print("Starting migrations...")
100
+
101
+ _run_versioned_migrations(filepaths_by_version=filepaths_by_version)
102
+
103
+ _run_repeatable_always_migrations(
104
+ repeatable_always_filepaths=repeatable_always_filepaths
105
+ )
106
+
107
+ _run_repeatable_on_change_migrations(
108
+ runs_on_change_filepaths=runs_on_change_filepaths
109
+ )
110
+
111
+ print("Migrations completed successfully.")
112
+ else:
113
+ process_dry_run(
114
+ version_to_filepath=filepaths_by_version,
115
+ migration_operation=MigrationDirectionType.UPGRADE,
116
+ repeatable_always_filepaths=repeatable_always_filepaths,
117
+ runs_on_change_filepaths=runs_on_change_filepaths,
118
+ )
119
+
120
+
121
+ def _get_filepaths_by_version(
122
+ latest_migration: MigrationRecord | None,
123
+ count: int | None = None,
124
+ to_version: str | None = None,
125
+ ) -> dict[str, str]:
126
+ """
127
+ Get pending migration file paths filtered by count or target version.
128
+
129
+ Args:
130
+ latest_migration (MigrationRecord | None): The most recently
131
+ applied migration, or None if no migrations applied.
132
+ count (int | None): Limit to this many migrations. Defaults to None.
133
+ to_version (str | None): Include migrations up to this version.
134
+ Defaults to None.
135
+
136
+ Returns:
137
+ dict[str, str]: Mapping of version to file path for pending migrations.
138
+
139
+ Raises:
140
+ FileNotFoundError: If to_version is not found in pending migrations.
141
+ """
142
+ filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version(
143
+ directory=os.path.join(os.getcwd(), MIGRATIONS_DIR),
144
+ version_to_start_from=latest_migration.version if latest_migration else None,
145
+ )
146
+
147
+ if latest_migration:
148
+ filepaths_by_version = dict(list(filepaths_by_version.items())[1:])
149
+
150
+ if count:
151
+ filepaths_by_version = dict(list(filepaths_by_version.items())[:count])
152
+ elif to_version:
153
+ if filepaths_by_version.get(to_version) is None:
154
+ raise FileNotFoundError(
155
+ f"The specified to_version '{to_version}' does not exist among pending migrations."
156
+ )
157
+ seen_versions: dict[str, str] = {}
158
+ for file_version, file_path in filepaths_by_version.items():
159
+ seen_versions[file_version] = file_path
160
+ if file_version == to_version:
161
+ break
162
+ filepaths_by_version = seen_versions
163
+
164
+ return filepaths_by_version
165
+
166
+
167
+ def _run_versioned_migrations(filepaths_by_version: dict[str, str]) -> None:
168
+ """
169
+ Execute versioned (V__) migrations.
170
+
171
+ Args:
172
+ filepaths_by_version (dict[str, str]): Mapping of version to file path.
173
+ """
174
+ for version, file_path in filepaths_by_version.items():
175
+ sql_statements: list[str] = parse_upgrade_statements(file_path=file_path)
176
+ filename: str = os.path.basename(file_path)
177
+
178
+ run_migration(
179
+ sql_statements=sql_statements,
180
+ version=version,
181
+ migration_operation=MigrationDirectionType.UPGRADE,
182
+ filename=filename,
183
+ )
184
+
185
+ print(f"Migration applied successfully: {filename}")
186
+
187
+
188
+ def _run_repeatable_always_migrations(
189
+ repeatable_always_filepaths: list[str],
190
+ ) -> None:
191
+ """
192
+ Execute runs-always (RA__) migrations.
193
+
194
+ Args:
195
+ repeatable_always_filepaths (list[str]): List of RA__ file paths.
196
+ """
197
+ if repeatable_always_filepaths:
198
+ for filepath in repeatable_always_filepaths:
199
+ sql_statements: list[str] = parse_upgrade_statements(file_path=filepath)
200
+ filename: str = os.path.basename(filepath)
201
+
202
+ if filename in get_existing_repeatable_always_migration_filenames():
203
+ run_update_repeatable_migration(
204
+ sql_statements=sql_statements,
205
+ filename=filename,
206
+ migration_type=MigrationType.RUNS_ALWAYS,
207
+ )
208
+ print(f"Migration applied successfully: {filename}")
209
+ else:
210
+ run_migration(
211
+ sql_statements=sql_statements,
212
+ version=None,
213
+ migration_operation=MigrationDirectionType.UPGRADE,
214
+ filename=filename,
215
+ migration_type=MigrationType.RUNS_ALWAYS,
216
+ )
217
+ print(f"Migration applied successfully: {filename}")
218
+
219
+
220
+ def _run_repeatable_on_change_migrations(runs_on_change_filepaths: list[str]) -> None:
221
+ """
222
+ Execute runs-on-change (ROC__) migrations.
223
+
224
+ Args:
225
+ runs_on_change_filepaths (list[str]): List of ROC__ file paths.
226
+ """
227
+ if runs_on_change_filepaths:
228
+ for filepath in runs_on_change_filepaths:
229
+ sql_statements: list[str] = parse_upgrade_statements(file_path=filepath)
230
+ filename: str = os.path.basename(filepath)
231
+
232
+ if filename in list(get_existing_on_change_filenames_to_checksums().keys()):
233
+ # update migration
234
+ run_update_repeatable_migration(
235
+ sql_statements=sql_statements,
236
+ filename=filename,
237
+ migration_type=MigrationType.RUNS_ON_CHANGE,
238
+ )
239
+ print(f"Migration applied successfully: {filename}")
240
+ else:
241
+ run_migration(
242
+ sql_statements=sql_statements,
243
+ version=None,
244
+ migration_operation=MigrationDirectionType.UPGRADE,
245
+ filename=filename,
246
+ migration_type=MigrationType.RUNS_ON_CHANGE,
247
+ )
248
+ print(f"Migration applied successfully: {filename}")
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+
3
+ from jetbase.exceptions import DirectoryNotFoundError
4
+
5
+
6
+ def validate_jetbase_directory() -> None:
7
+ """
8
+ Ensure command is run from jetbase directory with migrations folder.
9
+
10
+ Validates that the current working directory is named 'jetbase' and
11
+ contains a 'migrations' subdirectory. This validation is required
12
+ before running most Jetbase CLI commands.
13
+
14
+ Returns:
15
+ None: Returns silently if validation passes.
16
+
17
+ Raises:
18
+ DirectoryNotFoundError: If the current directory is not named
19
+ 'jetbase' or if the 'migrations' subdirectory does not exist.
20
+ """
21
+ current_dir = Path.cwd()
22
+
23
+ # Check if current directory is named 'jetbase'
24
+ if current_dir.name != "jetbase":
25
+ raise DirectoryNotFoundError(
26
+ "Command must be run from the 'jetbase' directory.\n"
27
+ "You can run 'jetbase init' to create a Jetbase project."
28
+ )
29
+
30
+ # Check if migrations directory exists
31
+ migrations_dir = current_dir / "migrations"
32
+ if not migrations_dir.exists() or not migrations_dir.is_dir():
33
+ raise DirectoryNotFoundError(
34
+ f"'migrations' directory not found in {current_dir}.\n"
35
+ "Add a migrations directory inside the 'jetbase' directory to proceed.\n"
36
+ "You can also run 'jetbase init' to create a Jetbase project."
37
+ )