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
jetbase/cli/main.py CHANGED
@@ -1,8 +1,17 @@
1
1
  import typer
2
2
 
3
- from jetbase.core.initialize import initialize_cmd
4
- from jetbase.core.rollback import rollback_cmd
5
- from jetbase.core.upgrade import upgrade_cmd
3
+ from jetbase.commands.current import current_cmd
4
+ from jetbase.commands.fix_checksums import fix_checksums_cmd
5
+ from jetbase.commands.fix_files import fix_files_cmd
6
+ from jetbase.commands.history import history_cmd
7
+ from jetbase.commands.init import initialize_cmd
8
+ from jetbase.commands.lock_status import lock_status_cmd
9
+ from jetbase.commands.new import generate_new_migration_file_cmd
10
+ from jetbase.commands.rollback import rollback_cmd
11
+ from jetbase.commands.status import status_cmd
12
+ from jetbase.commands.unlock import unlock_cmd
13
+ from jetbase.commands.upgrade import upgrade_cmd
14
+ from jetbase.commands.validators import validate_jetbase_directory
6
15
 
7
16
  app = typer.Typer(help="Jetbase CLI")
8
17
 
@@ -24,12 +33,31 @@ def upgrade(
24
33
  dry_run: bool = typer.Option(
25
34
  False, "--dry-run", "-d", help="Simulate the upgrade without making changes"
26
35
  ),
36
+ skip_validation: bool = typer.Option(
37
+ False,
38
+ "--skip-validation",
39
+ help="Skip both checksum and file version validation when running migrations",
40
+ ),
41
+ skip_checksum_validation: bool = typer.Option(
42
+ False,
43
+ "--skip-checksum-validation",
44
+ help="Skip checksum validation when running migrations",
45
+ ),
46
+ skip_file_validation: bool = typer.Option(
47
+ False,
48
+ "--skip-file-validation",
49
+ help="Skip file version validation when running migrations",
50
+ ),
27
51
  ):
28
52
  """Execute pending migrations"""
53
+ validate_jetbase_directory()
29
54
  upgrade_cmd(
30
55
  count=count,
31
56
  to_version=to_version.replace("_", ".") if to_version else None,
32
57
  dry_run=dry_run,
58
+ skip_validation=skip_validation,
59
+ skip_checksum_validation=skip_checksum_validation,
60
+ skip_file_validation=skip_file_validation,
33
61
  )
34
62
 
35
63
 
@@ -42,10 +70,11 @@ def rollback(
42
70
  None, "--to-version", "-t", help="Rollback to a specific version"
43
71
  ),
44
72
  dry_run: bool = typer.Option(
45
- False, "--dry-run", "-d", help="Simulate the upgrade without making changes"
73
+ False, "--dry-run", "-d", help="Simulate the rollback without making changes"
46
74
  ),
47
75
  ):
48
76
  """Rollback migration(s)"""
77
+ validate_jetbase_directory()
49
78
  rollback_cmd(
50
79
  count=count,
51
80
  to_version=to_version.replace("_", ".") if to_version else None,
@@ -53,7 +82,120 @@ def rollback(
53
82
  )
54
83
 
55
84
 
85
+ @app.command()
86
+ def history():
87
+ """Show migration history"""
88
+ validate_jetbase_directory()
89
+ history_cmd()
90
+
91
+
92
+ @app.command()
93
+ def current():
94
+ """Show the latest version that has been migrated"""
95
+ validate_jetbase_directory()
96
+ current_cmd()
97
+
98
+
99
+ @app.command()
100
+ def unlock():
101
+ """
102
+ Unlock the migration lock to allow migrations to run again.
103
+
104
+ WARNING: Only use this if you're certain no migration is currently running.
105
+ Unlocking then running a migration during an active migration can cause database corruption.
106
+ """
107
+ validate_jetbase_directory()
108
+ unlock_cmd()
109
+
110
+
111
+ @app.command()
112
+ def lock_status() -> None:
113
+ """Checks if the database is currently locked for migrations or not."""
114
+ validate_jetbase_directory()
115
+ lock_status_cmd()
116
+
117
+
118
+ @app.command()
119
+ def fix_checksums() -> None:
120
+ """Updates all stored checksums to their current values."""
121
+ validate_jetbase_directory()
122
+ fix_checksums_cmd()
123
+
124
+
125
+ @app.command()
126
+ def fix() -> None:
127
+ """Repair migration files and versions."""
128
+ validate_jetbase_directory()
129
+ fix_files_cmd(audit_only=False)
130
+ fix_checksums_cmd(audit_only=False)
131
+ print("Fix completed successfully.")
132
+
133
+
134
+ @app.command()
135
+ def validate_checksums(
136
+ fix: bool = typer.Option(
137
+ False,
138
+ "--fix",
139
+ "-f",
140
+ help="Fix any detected checksum mismatches by updating the stored checksum to match any changes in its corresponding migration file",
141
+ ),
142
+ ) -> None:
143
+ """Audit migration checksums without making changes. Use --fix to update stored checksums to match current migration files."""
144
+ validate_jetbase_directory()
145
+ if fix:
146
+ fix_checksums_cmd(audit_only=False)
147
+ else:
148
+ fix_checksums_cmd(audit_only=True)
149
+
150
+
151
+ @app.command()
152
+ def validate_files(
153
+ fix: bool = typer.Option(
154
+ False,
155
+ "--fix",
156
+ "-f",
157
+ help="Fix any detected migration file issues",
158
+ ),
159
+ ) -> None:
160
+ """Check if any migration files are missing. Use --fix to clean up records of migrations whose files no longer exist."""
161
+ validate_jetbase_directory()
162
+ if fix:
163
+ fix_files_cmd(audit_only=False)
164
+ else:
165
+ fix_files_cmd(audit_only=True)
166
+
167
+
168
+ @app.command()
169
+ def fix_files() -> None:
170
+ """Stops jetbase from tracking migrations whose files no longer exist."""
171
+ validate_jetbase_directory()
172
+ fix_files_cmd(audit_only=False)
173
+
174
+
175
+ @app.command()
176
+ def status() -> None:
177
+ """Display migration status: applied migrations and pending migrations."""
178
+ validate_jetbase_directory()
179
+ status_cmd()
180
+
181
+
182
+ # check if typer enforces enum types - if yes then create enum for migration type
183
+ @app.command()
184
+ def new(
185
+ description: str = typer.Argument(..., help="Description of the migration"),
186
+ ) -> None:
187
+ """Create a new migration file with a timestamp-based version and the provided description."""
188
+ validate_jetbase_directory()
189
+ generate_new_migration_file_cmd(description=description)
190
+
191
+
56
192
  def main() -> None:
193
+ """
194
+ Entry point for the Jetbase CLI application.
195
+
196
+ Initializes and runs the Typer application that handles all CLI commands
197
+ including migrations, rollbacks, and database management operations.
198
+ """
57
199
  app()
58
200
 
59
201
 
@@ -0,0 +1,20 @@
1
+ from jetbase.models import MigrationRecord
2
+ from jetbase.repositories.migrations_repo import fetch_latest_versioned_migration
3
+
4
+
5
+ def current_cmd() -> None:
6
+ """
7
+ Display the current (latest applied) migration version.
8
+
9
+ Queries the database for the most recently applied versioned migration
10
+ and prints its version number. If no migrations have been applied,
11
+ displays a message indicating that.
12
+
13
+ Returns:
14
+ None: Prints the current migration version to stdout.
15
+ """
16
+ latest_migration: MigrationRecord | None = fetch_latest_versioned_migration()
17
+ if latest_migration:
18
+ print(f"Latest migration version: {latest_migration.version}")
19
+ else:
20
+ print("No migrations have been applied yet.")
@@ -0,0 +1,172 @@
1
+ import os
2
+
3
+ from jetbase.constants import MIGRATIONS_DIR
4
+ from jetbase.engine.checksum import calculate_checksum
5
+ from jetbase.engine.file_parser import parse_upgrade_statements
6
+ from jetbase.engine.lock import migration_lock
7
+ from jetbase.engine.validation import run_migration_validations
8
+ from jetbase.engine.version import get_migration_filepaths_by_version
9
+ from jetbase.exceptions import (
10
+ MigrationVersionMismatchError,
11
+ )
12
+ from jetbase.repositories.migrations_repo import (
13
+ get_checksums_by_version,
14
+ update_migration_checksums,
15
+ )
16
+
17
+
18
+ def fix_checksums_cmd(audit_only: bool = False) -> None:
19
+ """
20
+ Fix or audit checksums for applied migrations.
21
+
22
+ Compares the checksums stored in the database against the current checksums
23
+ of migration files on disk. Detects drift where files have been modified
24
+ after being applied to the database.
25
+
26
+ Args:
27
+ audit_only (bool): If True, only reports checksum mismatches without
28
+ making any changes to the database. If False, updates the stored
29
+ checksums to match the current file contents. Defaults to False.
30
+
31
+ Returns:
32
+ None: Prints audit report or repair status to stdout.
33
+
34
+ Raises:
35
+ MigrationVersionMismatchError: If there is a mismatch between expected
36
+ and actual migration versions during processing.
37
+ """
38
+
39
+ migrated_versions_and_checksums: list[tuple[str, str]] = get_checksums_by_version()
40
+ if not migrated_versions_and_checksums:
41
+ print("No migrations have been applied; nothing to repair.")
42
+ return
43
+
44
+ latest_migrated_version: str = migrated_versions_and_checksums[-1][0]
45
+
46
+ run_migration_validations(
47
+ latest_migrated_version=latest_migrated_version,
48
+ skip_checksum_validation=True,
49
+ )
50
+
51
+ versions_and_checksums_to_repair: list[tuple[str, str]] = _find_checksum_mismatches(
52
+ migrated_versions_and_checksums=migrated_versions_and_checksums,
53
+ latest_migrated_version=latest_migrated_version,
54
+ )
55
+
56
+ if not versions_and_checksums_to_repair:
57
+ print(
58
+ "All migration checksums are valid - no altered upgrade statments detected."
59
+ )
60
+ return
61
+
62
+ if audit_only:
63
+ _print_audit_report(
64
+ versions_and_checksums_to_repair=versions_and_checksums_to_repair
65
+ )
66
+ return
67
+
68
+ _repair_checksums(versions_and_checksums_to_repair=versions_and_checksums_to_repair)
69
+
70
+
71
+ def _print_audit_report(
72
+ versions_and_checksums_to_repair: list[tuple[str, str]],
73
+ ) -> None:
74
+ """
75
+ Print a formatted report of migrations with checksum drift.
76
+
77
+ Outputs a list of migration versions whose file contents have changed
78
+ since they were originally applied to the database.
79
+
80
+ Args:
81
+ versions_and_checksums_to_repair (list[tuple[str, str]]): List of tuples
82
+ containing (version, new_checksum) for migrations with detected drift.
83
+
84
+ Returns:
85
+ None: Prints the audit report to stdout.
86
+ """
87
+ print("\nJETBASE - Checksum Audit Report")
88
+ print("----------------------------------------")
89
+ print("Changes detected in the following files:")
90
+ for file_version, _ in versions_and_checksums_to_repair:
91
+ print(f" → {file_version}")
92
+
93
+
94
+ def _repair_checksums(versions_and_checksums_to_repair: list[tuple[str, str]]) -> None:
95
+ """
96
+ Update checksums in the database for migrations with detected drift.
97
+
98
+ Acquires a migration lock and updates the stored checksums in the
99
+ jetbase_migrations table to match the current file contents.
100
+
101
+ Args:
102
+ versions_and_checksums_to_repair (list[tuple[str, str]]): List of tuples
103
+ containing (version, new_checksum) for migrations to update.
104
+
105
+ Returns:
106
+ None: Prints repair status for each version to stdout.
107
+ """
108
+ with migration_lock():
109
+ update_migration_checksums(
110
+ versions_and_checksums=versions_and_checksums_to_repair
111
+ )
112
+ for version, _ in versions_and_checksums_to_repair:
113
+ print(f"Repaired checksum for version: {version}")
114
+
115
+ print("Successfully repaired checksums")
116
+
117
+
118
+ def _find_checksum_mismatches(
119
+ migrated_versions_and_checksums: list[tuple[str, str]], latest_migrated_version: str
120
+ ) -> list[tuple[str, str]]:
121
+ """
122
+ Find migrations where the file checksum differs from the stored checksum.
123
+
124
+ Compares the current checksum of each migration file against the checksum
125
+ stored in the database when the migration was originally applied.
126
+
127
+ Args:
128
+ migrated_versions_and_checksums (list[tuple[str, str]]): List of tuples
129
+ containing (version, stored_checksum) from the database.
130
+ latest_migrated_version (str): The most recent version that has been
131
+ migrated, used to limit the scope of files checked.
132
+
133
+ Returns:
134
+ list[tuple[str, str]]: List of (version, new_checksum) tuples for
135
+ migrations where the file has changed since being applied.
136
+
137
+ Raises:
138
+ MigrationVersionMismatchError: If the file versions do not match the
139
+ expected sequence of migrated versions.
140
+
141
+ Example:
142
+ >>> _find_checksum_mismatches([("1.0", "abc123")], "1.0")
143
+ [("1.0", "def456")] # If file changed
144
+ """
145
+ migration_filepaths_by_version: dict[str, str] = get_migration_filepaths_by_version(
146
+ directory=os.path.join(os.getcwd(), MIGRATIONS_DIR),
147
+ end_version=latest_migrated_version,
148
+ )
149
+
150
+ versions_and_checksums_to_repair: list[tuple[str, str]] = []
151
+
152
+ for index, (file_version, filepath) in enumerate(
153
+ migration_filepaths_by_version.items()
154
+ ):
155
+ sql_statements: list[str] = parse_upgrade_statements(file_path=filepath)
156
+ checksum: str = calculate_checksum(sql_statements=sql_statements)
157
+
158
+ # this should never be hit because of the validation check above
159
+ if file_version != migrated_versions_and_checksums[index][0]:
160
+ raise MigrationVersionMismatchError(
161
+ f"Version mismatch123: expected {migrated_versions_and_checksums[index][0]}, found {file_version}."
162
+ )
163
+
164
+ if checksum != migrated_versions_and_checksums[index][1]:
165
+ versions_and_checksums_to_repair.append(
166
+ (
167
+ file_version,
168
+ checksum,
169
+ )
170
+ )
171
+
172
+ return versions_and_checksums_to_repair
@@ -0,0 +1,133 @@
1
+ import os
2
+
3
+ from jetbase.engine.lock import migration_lock
4
+ from jetbase.engine.repeatable import get_repeatable_filenames
5
+ from jetbase.engine.version import (
6
+ get_migration_filepaths_by_version,
7
+ )
8
+ from jetbase.models import MigrationRecord
9
+ from jetbase.repositories.lock_repo import create_lock_table_if_not_exists
10
+ from jetbase.repositories.migrations_repo import (
11
+ create_migrations_table_if_not_exists,
12
+ delete_missing_repeatables,
13
+ delete_missing_versions,
14
+ fetch_repeatable_migrations,
15
+ get_migrated_versions,
16
+ )
17
+
18
+
19
+ def fix_files_cmd(audit_only: bool = False) -> None:
20
+ """
21
+ Remove database records for missing migration files.
22
+
23
+ Reconciles the migration state between the database and the filesystem
24
+ by identifying migrations whose corresponding files no longer exist.
25
+ Handles both versioned and repeatable migrations.
26
+
27
+ Args:
28
+ audit_only (bool): If True, only reports missing migration files
29
+ without making any changes to the database. If False, removes
30
+ database records for missing migrations. Defaults to False.
31
+
32
+ Returns:
33
+ None: Prints audit report or removal status to stdout.
34
+ """
35
+
36
+ create_lock_table_if_not_exists()
37
+ create_migrations_table_if_not_exists()
38
+
39
+ migrated_versions: list[str] = get_migrated_versions()
40
+ current_migration_filepaths_by_version: dict[str, str] = (
41
+ get_migration_filepaths_by_version(
42
+ directory=os.path.join(os.getcwd(), "migrations")
43
+ )
44
+ )
45
+ repeatable_migrations: list[MigrationRecord] = fetch_repeatable_migrations()
46
+ all_repeatable_filenames: list[str] = get_repeatable_filenames()
47
+
48
+ missing_versions: list[str] = []
49
+ missing_repeatables: list[str] = []
50
+
51
+ for migrated_version in migrated_versions:
52
+ if migrated_version not in current_migration_filepaths_by_version:
53
+ missing_versions.append(migrated_version)
54
+
55
+ for r_migration in repeatable_migrations:
56
+ if r_migration.filename not in all_repeatable_filenames:
57
+ missing_repeatables.append(r_migration.filename)
58
+
59
+ if audit_only:
60
+ _print_audit_report(
61
+ missing_versions=missing_versions, missing_repeatables=missing_repeatables
62
+ )
63
+ return
64
+
65
+ _remove_missing_migrations(
66
+ missing_versions=missing_versions, missing_repeatables=missing_repeatables
67
+ )
68
+
69
+
70
+ def _print_audit_report(
71
+ missing_versions: list[str], missing_repeatables: list[str]
72
+ ) -> None:
73
+ """
74
+ Print a formatted report of migrations with missing files.
75
+
76
+ Lists all versioned and repeatable migrations that are tracked in the
77
+ database but no longer have corresponding files on disk.
78
+
79
+ Args:
80
+ missing_versions (list[str]): List of version strings for versioned
81
+ migrations with missing files.
82
+ missing_repeatables (list[str]): List of filenames for repeatable
83
+ migrations with missing files.
84
+
85
+ Returns:
86
+ None: Prints the audit report to stdout.
87
+ """
88
+ if missing_versions or missing_repeatables:
89
+ print("The following migrations are missing their corresponding files:")
90
+ for version in missing_versions:
91
+ print(f"→ {version}")
92
+ for r_file in missing_repeatables:
93
+ print(f"→ {r_file}")
94
+
95
+ else:
96
+ print("All migrations have corresponding files.")
97
+
98
+
99
+ def _remove_missing_migrations(
100
+ missing_versions: list[str], missing_repeatables: list[str]
101
+ ) -> None:
102
+ """
103
+ Delete database records for migrations whose files no longer exist.
104
+
105
+ Acquires a migration lock and removes records from the jetbase_migrations
106
+ table for both versioned and repeatable migrations that are missing files.
107
+
108
+ Args:
109
+ missing_versions (list[str]): List of version strings for versioned
110
+ migrations to remove from tracking.
111
+ missing_repeatables (list[str]): List of filenames for repeatable
112
+ migrations to remove from tracking.
113
+
114
+ Returns:
115
+ None: Prints removal status for each migration to stdout.
116
+ """
117
+ if missing_versions or missing_repeatables:
118
+ with migration_lock():
119
+ if missing_versions:
120
+ delete_missing_versions(versions=missing_versions)
121
+ print("Stopped tracking the following missing versions:")
122
+ for version in missing_versions:
123
+ print(f"→ {version}")
124
+
125
+ if missing_repeatables:
126
+ delete_missing_repeatables(repeatable_filenames=missing_repeatables)
127
+ print(
128
+ "Removed the following missing repeatable migrations from the database:"
129
+ )
130
+ for r_file in missing_repeatables:
131
+ print(f"→ {r_file}")
132
+ else:
133
+ print("No missing migration files.")
@@ -0,0 +1,53 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+
4
+ from jetbase.engine.formatters import format_applied_at, get_display_version
5
+ from jetbase.models import MigrationRecord
6
+ from jetbase.repositories.migrations_repo import (
7
+ get_migration_records,
8
+ migrations_table_exists,
9
+ )
10
+
11
+
12
+ def history_cmd() -> None:
13
+ """
14
+ Display the migration history in a formatted table.
15
+
16
+ Retrieves all applied migrations from the database and displays them
17
+ in a rich-formatted table showing version numbers, execution order,
18
+ descriptions, and timestamps.
19
+
20
+ Returns:
21
+ None: Prints a formatted table to stdout, or a message if no
22
+ migrations have been applied.
23
+ """
24
+ console: Console = Console()
25
+ table_exists: bool = migrations_table_exists()
26
+ if not table_exists:
27
+ console.print("[yellow]No migrations have been applied.[/yellow]")
28
+ return None
29
+
30
+ migration_records: list[MigrationRecord] = get_migration_records()
31
+ if not migration_records:
32
+ console.print("[yellow]No migrations have been applied yet.[/yellow]")
33
+ return
34
+
35
+ migration_history_table: Table = Table(
36
+ title="Migration History", show_header=True, header_style="bold magenta"
37
+ )
38
+ migration_history_table.add_column("Version", style="cyan", no_wrap=True)
39
+ migration_history_table.add_column("Order Executed", style="green")
40
+ migration_history_table.add_column("Description", style="white")
41
+ migration_history_table.add_column("Applied At", style="green", no_wrap=True)
42
+
43
+ for record in migration_records:
44
+ migration_history_table.add_row(
45
+ get_display_version(
46
+ version=record.version, migration_type=record.migration_type
47
+ ),
48
+ str(record.order_executed),
49
+ record.description,
50
+ format_applied_at(applied_at=record.applied_at),
51
+ )
52
+
53
+ console.print(migration_history_table)
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+
3
+ from jetbase.constants import BASE_DIR, ENV_FILE, ENV_FILE_CONTENT, MIGRATIONS_DIR
4
+
5
+ # def initialize_cmd() -> None:
6
+ # create_directory_structure(base_path=BASE_DIR)
7
+
8
+
9
+ def initialize_cmd() -> None:
10
+ """
11
+ Create the directory structure for a new Jetbase project.
12
+
13
+ Creates a 'jetbase' directory containing a 'migrations' subdirectory
14
+ and an 'env.py' configuration file with a template database URL.
15
+
16
+ Returns:
17
+ None: Prints a confirmation message with the project location.
18
+ """
19
+ migrations_dir: Path = Path(BASE_DIR) / MIGRATIONS_DIR
20
+ migrations_dir.mkdir(parents=True, exist_ok=True)
21
+
22
+ config_path: Path = Path(BASE_DIR) / ENV_FILE
23
+ with open(config_path, "w") as f:
24
+ f.write(ENV_FILE_CONTENT)
25
+
26
+ print(
27
+ f"Initialized Jetbase project in {Path(BASE_DIR).absolute()}\n"
28
+ "Run 'cd jetbase' to get started!"
29
+ )
@@ -0,0 +1,25 @@
1
+ from jetbase.models import LockStatus
2
+ from jetbase.repositories.lock_repo import fetch_lock_status, lock_table_exists
3
+ from jetbase.repositories.migrations_repo import migrations_table_exists
4
+
5
+
6
+ def lock_status_cmd() -> None:
7
+ """
8
+ Display whether the migration lock is currently held.
9
+
10
+ Queries the jetbase_lock table to check if migrations are currently
11
+ locked. If locked, displays the timestamp when the lock was acquired.
12
+
13
+ Returns:
14
+ None: Prints "LOCKED" with timestamp or "UNLOCKED" to stdout.
15
+ """
16
+
17
+ if not lock_table_exists() or not migrations_table_exists():
18
+ print("Status: UNLOCKED")
19
+ return
20
+
21
+ lock_status: LockStatus = fetch_lock_status()
22
+ if lock_status.is_locked:
23
+ print(f"Status: LOCKED\nLocked At: {lock_status.locked_at}")
24
+ else:
25
+ print("Status: UNLOCKED")
@@ -0,0 +1,65 @@
1
+ import datetime as dt
2
+ import os
3
+
4
+ from jetbase.constants import MIGRATIONS_DIR, NEW_MIGRATION_FILE_CONTENT
5
+ from jetbase.exceptions import DirectoryNotFoundError
6
+
7
+
8
+ def generate_new_migration_file_cmd(description: str) -> None:
9
+ """
10
+ Generate a new migration file with a timestamped filename.
11
+
12
+ Creates a new SQL migration file in the migrations directory with a
13
+ filename format of V{timestamp}__{description}.sql. The file contains
14
+ template sections for upgrade and rollback SQL statements.
15
+
16
+ Args:
17
+ description (str): A human-readable description for the migration.
18
+ Spaces will be replaced with underscores in the filename.
19
+
20
+ Returns:
21
+ None: Prints the created filename to stdout.
22
+
23
+ Raises:
24
+ DirectoryNotFoundError: If the migrations directory does not exist.
25
+
26
+ Example:
27
+ >>> generate_new_migration_file_cmd("create users table")
28
+ Created migration file: V20251201.120000__create_users_table.sql
29
+ """
30
+
31
+ migrations_dir_path: str = os.path.join(os.getcwd(), MIGRATIONS_DIR)
32
+
33
+ if not os.path.exists(migrations_dir_path):
34
+ raise DirectoryNotFoundError(
35
+ "Migrations directory not found. Run 'jetbase initialize' to set up jetbase.\n"
36
+ "If you have already done so, run this command from the jetbase directory."
37
+ )
38
+
39
+ filename: str = _generate_new_filename(description=description)
40
+ filepath: str = os.path.join(migrations_dir_path, filename)
41
+
42
+ with open(filepath, "w") as f: # noqa: F841
43
+ f.write(NEW_MIGRATION_FILE_CONTENT)
44
+ print(f"Created migration file: {filename}")
45
+
46
+
47
+ def _generate_new_filename(description: str) -> str:
48
+ """
49
+ Generate a timestamped filename for a migration.
50
+
51
+ Creates a filename using the current timestamp in YYYYMMDD.HHMMSS format
52
+ followed by the description with spaces converted to underscores.
53
+
54
+ Args:
55
+ description (str): A human-readable description for the migration.
56
+
57
+ Returns:
58
+ str: Formatted filename like "V20251201.120000__description.sql".
59
+
60
+ Example:
61
+ >>> _generate_new_filename("add users")
62
+ 'V20251201.120000__add_users.sql'
63
+ """
64
+ timestamp = dt.datetime.now().strftime("%Y%m%d.%H%M%S")
65
+ return f"V{timestamp}__{description.replace(' ', '_')}.sql"