jetbase 0.7.0__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.
- jetbase/cli/main.py +61 -0
- jetbase/config.py +60 -0
- jetbase/constants.py +12 -0
- jetbase/core/dry_run.py +38 -0
- jetbase/core/file_parser.py +199 -0
- jetbase/core/initialize.py +33 -0
- jetbase/core/repository.py +169 -0
- jetbase/core/rollback.py +67 -0
- jetbase/core/upgrade.py +75 -0
- jetbase/core/version.py +163 -0
- jetbase/enums.py +6 -0
- jetbase/queries.py +72 -0
- jetbase-0.7.0.dist-info/METADATA +12 -0
- jetbase-0.7.0.dist-info/RECORD +17 -0
- jetbase-0.7.0.dist-info/WHEEL +4 -0
- jetbase-0.7.0.dist-info/entry_points.txt +2 -0
- jetbase-0.7.0.dist-info/licenses/LICENSE +21 -0
jetbase/cli/main.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import typer
|
|
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
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Jetbase CLI")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def init():
|
|
12
|
+
"""Initialize jetbase in current directory"""
|
|
13
|
+
initialize_cmd()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
def upgrade(
|
|
18
|
+
count: int = typer.Option(
|
|
19
|
+
None, "--count", "-c", help="Number of migrations to apply"
|
|
20
|
+
),
|
|
21
|
+
to_version: str | None = typer.Option(
|
|
22
|
+
None, "--to-version", "-t", help="Rollback to a specific version"
|
|
23
|
+
),
|
|
24
|
+
dry_run: bool = typer.Option(
|
|
25
|
+
False, "--dry-run", "-d", help="Simulate the upgrade without making changes"
|
|
26
|
+
),
|
|
27
|
+
):
|
|
28
|
+
"""Execute pending migrations"""
|
|
29
|
+
upgrade_cmd(
|
|
30
|
+
count=count,
|
|
31
|
+
to_version=to_version.replace("_", ".") if to_version else None,
|
|
32
|
+
dry_run=dry_run,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def rollback(
|
|
38
|
+
count: int = typer.Option(
|
|
39
|
+
None, "--count", "-c", help="Number of migrations to rollback"
|
|
40
|
+
),
|
|
41
|
+
to_version: str | None = typer.Option(
|
|
42
|
+
None, "--to-version", "-t", help="Rollback to a specific version"
|
|
43
|
+
),
|
|
44
|
+
dry_run: bool = typer.Option(
|
|
45
|
+
False, "--dry-run", "-d", help="Simulate the upgrade without making changes"
|
|
46
|
+
),
|
|
47
|
+
):
|
|
48
|
+
"""Rollback migration(s)"""
|
|
49
|
+
rollback_cmd(
|
|
50
|
+
count=count,
|
|
51
|
+
to_version=to_version.replace("_", ".") if to_version else None,
|
|
52
|
+
dry_run=dry_run,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
app()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
jetbase/config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import importlib.machinery
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# from importlib import ModuleType
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from jetbase.constants import CONFIG_FILE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_sqlalchemy_url(filename: str = CONFIG_FILE) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Load a config file and extract the sqlalchemy_url.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config_filename: Name of the config file (e.g., "config.py")
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The sqlalchemy_url from the config file, or None if not found
|
|
21
|
+
"""
|
|
22
|
+
config_path: str = os.path.join(os.getcwd(), filename)
|
|
23
|
+
spec: importlib.machinery.ModuleSpec | None = (
|
|
24
|
+
importlib.util.spec_from_file_location("config", config_path)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert spec is not None
|
|
28
|
+
assert spec.loader is not None
|
|
29
|
+
|
|
30
|
+
config: ModuleType = importlib.util.module_from_spec(spec)
|
|
31
|
+
spec.loader.exec_module(module=config)
|
|
32
|
+
|
|
33
|
+
raw_sqlalchemy_url: Any | None = getattr(config, "sqlalchemy_url", None)
|
|
34
|
+
|
|
35
|
+
if raw_sqlalchemy_url is None:
|
|
36
|
+
raise AttributeError(
|
|
37
|
+
f"'sqlalchemy_url' not found or is set to None. Please define it in {config_path}."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
sqlalchemy_url: str = _validate_sqlalchemy_url(url=raw_sqlalchemy_url)
|
|
41
|
+
|
|
42
|
+
return sqlalchemy_url
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_sqlalchemy_url(url: Any) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Validates a SQLAlchemy URL string.
|
|
48
|
+
This function checks if the provided URL is a valid string.
|
|
49
|
+
Args:
|
|
50
|
+
url (Any): The SQLAlchemy URL to validate (could be any type from user config).
|
|
51
|
+
Returns:
|
|
52
|
+
str: The validated SQLAlchemy URL string.
|
|
53
|
+
Raises:
|
|
54
|
+
TypeError: If the provided URL is not a string.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if not isinstance(url, str):
|
|
58
|
+
raise TypeError(f"sqlalchemy_url must be a string, got {type(url).__name__}")
|
|
59
|
+
|
|
60
|
+
return url
|
jetbase/constants.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
BASE_DIR: Final[str] = "jetbase"
|
|
4
|
+
MIGRATIONS_DIR: Final[str] = "migrations"
|
|
5
|
+
CONFIG_FILE: Final[str] = "config.py"
|
|
6
|
+
|
|
7
|
+
CONFIG_FILE_CONTENT: Final[str] = """
|
|
8
|
+
# Jetbase Configuration
|
|
9
|
+
# Update the sqlalchemy_url with your database connection string.
|
|
10
|
+
|
|
11
|
+
sqlalchemy_url = "postgresql://user:password@localhost:5432/mydb"
|
|
12
|
+
"""
|
jetbase/core/dry_run.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.core.file_parser import parse_rollback_statements, parse_upgrade_statements
|
|
4
|
+
from jetbase.enums import MigrationOperationType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def process_dry_run(
|
|
8
|
+
version_to_filepath: dict[str, str], migration_operation: MigrationOperationType
|
|
9
|
+
) -> None:
|
|
10
|
+
print("\nJETBASE - Dry Run Mode")
|
|
11
|
+
print("No SQL will be executed. This is a preview of what would happen.")
|
|
12
|
+
print("----------------------------------------\n\n")
|
|
13
|
+
|
|
14
|
+
for version, file_path in version_to_filepath.items():
|
|
15
|
+
if migration_operation == MigrationOperationType.UPGRADE:
|
|
16
|
+
sql_statements: list[str] = parse_upgrade_statements(
|
|
17
|
+
file_path=file_path, dry_run=True
|
|
18
|
+
)
|
|
19
|
+
elif migration_operation == MigrationOperationType.ROLLBACK:
|
|
20
|
+
sql_statements: list[str] = parse_rollback_statements(
|
|
21
|
+
file_path=file_path, dry_run=True
|
|
22
|
+
)
|
|
23
|
+
else:
|
|
24
|
+
raise NotImplementedError(
|
|
25
|
+
f"Dry run not implemented for migration operation: {migration_operation}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
filename: str = os.path.basename(file_path)
|
|
29
|
+
|
|
30
|
+
num_sql_statements: int = len(sql_statements)
|
|
31
|
+
|
|
32
|
+
print(
|
|
33
|
+
f"\nSQL Preview for {filename} ({num_sql_statements} {'statements' if num_sql_statements != 1 else 'statement'})"
|
|
34
|
+
)
|
|
35
|
+
for statement in sql_statements:
|
|
36
|
+
print("\n")
|
|
37
|
+
print(statement)
|
|
38
|
+
print("----------------------------------------\n")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from jetbase.enums import MigrationOperationType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_upgrade_statements(file_path: str, dry_run: bool = False) -> list[str]:
|
|
7
|
+
statements = []
|
|
8
|
+
current_statement = []
|
|
9
|
+
|
|
10
|
+
with open(file_path, "r") as file:
|
|
11
|
+
for line in file:
|
|
12
|
+
if not dry_run:
|
|
13
|
+
line = line.strip()
|
|
14
|
+
else:
|
|
15
|
+
line = line.rstrip()
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
line.strip().startswith("--")
|
|
19
|
+
and line[2:].strip().lower() == MigrationOperationType.ROLLBACK.value
|
|
20
|
+
):
|
|
21
|
+
break
|
|
22
|
+
|
|
23
|
+
if not line or line.strip().startswith("--"):
|
|
24
|
+
continue
|
|
25
|
+
current_statement.append(line)
|
|
26
|
+
|
|
27
|
+
if line.strip().endswith(";"):
|
|
28
|
+
if not dry_run:
|
|
29
|
+
statement = " ".join(current_statement)
|
|
30
|
+
else:
|
|
31
|
+
statement = "\n".join(current_statement)
|
|
32
|
+
statement = statement.rstrip(";").strip()
|
|
33
|
+
if statement:
|
|
34
|
+
statements.append(statement)
|
|
35
|
+
current_statement = []
|
|
36
|
+
|
|
37
|
+
return statements
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_rollback_statements(file_path: str, dry_run: bool = False) -> list[str]:
|
|
41
|
+
statements = []
|
|
42
|
+
current_statement = []
|
|
43
|
+
in_rollback_section = False
|
|
44
|
+
|
|
45
|
+
with open(file_path, "r") as file:
|
|
46
|
+
for line in file:
|
|
47
|
+
if not dry_run:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
else:
|
|
50
|
+
line = line.rstrip()
|
|
51
|
+
|
|
52
|
+
if not in_rollback_section:
|
|
53
|
+
if (
|
|
54
|
+
line.strip().startswith("--")
|
|
55
|
+
and line[2:].strip().lower()
|
|
56
|
+
== MigrationOperationType.ROLLBACK.value
|
|
57
|
+
):
|
|
58
|
+
in_rollback_section = True
|
|
59
|
+
else:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if in_rollback_section:
|
|
63
|
+
if not line or line.strip().startswith("--"):
|
|
64
|
+
continue
|
|
65
|
+
current_statement.append(line)
|
|
66
|
+
|
|
67
|
+
if line.strip().endswith(";"):
|
|
68
|
+
if not dry_run:
|
|
69
|
+
statement = " ".join(current_statement)
|
|
70
|
+
else:
|
|
71
|
+
statement = "\n".join(current_statement)
|
|
72
|
+
statement = statement.rstrip(";").strip()
|
|
73
|
+
if statement:
|
|
74
|
+
statements.append(statement)
|
|
75
|
+
current_statement = []
|
|
76
|
+
|
|
77
|
+
return statements
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_filename_format_valid(filename: str) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Validates if a filename follows the expected migration file naming convention.
|
|
83
|
+
A valid filename must:
|
|
84
|
+
- Start with "V"
|
|
85
|
+
- Have a valid version number following "V"
|
|
86
|
+
- Contain "__" (double underscore)
|
|
87
|
+
- End with ".sql"
|
|
88
|
+
- Have a valid version number extractable from the filename
|
|
89
|
+
Args:
|
|
90
|
+
filename (str): The filename to validate.
|
|
91
|
+
Returns:
|
|
92
|
+
bool: True if the filename meets all validation criteria, False otherwise.
|
|
93
|
+
Example:
|
|
94
|
+
>>> is_valid_filename_format("V1__initial_migration.sql")
|
|
95
|
+
True
|
|
96
|
+
>>> is_valid_filename_format("migration.sql")
|
|
97
|
+
False
|
|
98
|
+
"""
|
|
99
|
+
if not filename.endswith(".sql"):
|
|
100
|
+
return False
|
|
101
|
+
if not filename.startswith("V"):
|
|
102
|
+
return False
|
|
103
|
+
if "__" not in filename:
|
|
104
|
+
return False
|
|
105
|
+
description: str = _get_raw_description_from_filename(filename=filename)
|
|
106
|
+
if len(description.strip()) == 0:
|
|
107
|
+
return False
|
|
108
|
+
raw_version: str = _get_version_from_filename(filename=filename)
|
|
109
|
+
if not _is_valid_version(version=raw_version):
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_description_from_filename(filename: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Extract and format the description from a migration filename.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: The formatted description string (e.g., "add feature")
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
raw_description: str = _get_raw_description_from_filename(filename=filename)
|
|
126
|
+
formatted_description: str = raw_description.replace("_", " ")
|
|
127
|
+
return formatted_description
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_filename_length_valid(filename: str, max_length: int = 512) -> bool:
|
|
131
|
+
"""
|
|
132
|
+
Check if the filename length is within the specified maximum length.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
filename: The migration filename to check.
|
|
136
|
+
max_length: The maximum allowed length for the filename.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
bool: True if the filename length is within the maximum length, False otherwise.
|
|
140
|
+
"""
|
|
141
|
+
return len(filename) <= max_length
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _get_version_from_filename(filename: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Extract the version string from a migration filename.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The extracted version string (e.g., "1_2_0")
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
version: str = filename[1 : filename.index("__")]
|
|
156
|
+
return version
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _get_raw_description_from_filename(filename: str) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Extract the description string from a migration filename.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
filename: The migration filename (e.g., "V1_2_0__add_feature.sql")
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
str: The extracted description string (e.g., "add_feature")
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
description: str = filename[
|
|
171
|
+
filename.index("__") + 2 : filename.index(".sql")
|
|
172
|
+
].strip()
|
|
173
|
+
return description
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_valid_version(version: str) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Validate that a version string follows the correct format.
|
|
179
|
+
|
|
180
|
+
Rules:
|
|
181
|
+
- Must start and end with a number
|
|
182
|
+
- Can be any length (1 or greater)
|
|
183
|
+
- Can contain periods (.) or underscores (_) between numbers
|
|
184
|
+
|
|
185
|
+
Valid examples: "1", "1.2", "1_2", "1.2.3", "1_2_3", "10.20.30"
|
|
186
|
+
Invalid examples: ".1", "1.", "_1", "1_", "1..2", "1__2", "abc", ""
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
version: The version string to validate
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
bool: True if version is valid, False otherwise
|
|
193
|
+
"""
|
|
194
|
+
if not version:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
# Pattern: starts with digit, ends with digit, can have periods/underscores between digits
|
|
198
|
+
pattern = r"^\d+([._]\d+)*$"
|
|
199
|
+
return bool(re.match(pattern, version))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from jetbase.constants import BASE_DIR, CONFIG_FILE, CONFIG_FILE_CONTENT, MIGRATIONS_DIR
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_directory_structure(base_path: str) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Create the basic directory structure for a new Jetbase project.
|
|
9
|
+
|
|
10
|
+
This function creates:
|
|
11
|
+
- A migrations directory
|
|
12
|
+
- A config file with default content
|
|
13
|
+
|
|
14
|
+
After creating the structure, it prints a confirmation message.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
base_path (str): The base path where the Jetbase project structure will be created
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
None
|
|
21
|
+
"""
|
|
22
|
+
migrations_dir: Path = Path(base_path) / MIGRATIONS_DIR
|
|
23
|
+
migrations_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
config_path: Path = Path(base_path) / CONFIG_FILE
|
|
26
|
+
with open(config_path, "w") as f:
|
|
27
|
+
f.write(CONFIG_FILE_CONTENT)
|
|
28
|
+
|
|
29
|
+
print(f"Initialized Jetbase project in {Path(base_path).absolute()}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def initialize_cmd() -> None:
|
|
33
|
+
create_directory_structure(base_path=BASE_DIR)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from sqlalchemy import Engine, Result, create_engine, text
|
|
2
|
+
|
|
3
|
+
from jetbase.config import get_sqlalchemy_url
|
|
4
|
+
from jetbase.core.file_parser import get_description_from_filename
|
|
5
|
+
from jetbase.enums import MigrationOperationType
|
|
6
|
+
from jetbase.queries import (
|
|
7
|
+
CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY,
|
|
8
|
+
CHECK_IF_VERSION_EXISTS_QUERY,
|
|
9
|
+
CREATE_MIGRATIONS_TABLE_STMT,
|
|
10
|
+
DELETE_VERSION_STMT,
|
|
11
|
+
INSERT_VERSION_STMT,
|
|
12
|
+
LATEST_VERSION_QUERY,
|
|
13
|
+
LATEST_VERSIONS_BY_STARTING_VERSION_QUERY,
|
|
14
|
+
LATEST_VERSIONS_QUERY,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_last_updated_version() -> str | None:
|
|
19
|
+
"""
|
|
20
|
+
Retrieves the latest version from the database.
|
|
21
|
+
This function connects to the database, executes a query to get the most recent version,
|
|
22
|
+
and returns that version as a string.
|
|
23
|
+
Returns:
|
|
24
|
+
str | None: The latest version string if available, None if no version was found.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
28
|
+
|
|
29
|
+
with engine.begin() as connection:
|
|
30
|
+
result: Result[tuple[str]] = connection.execute(LATEST_VERSION_QUERY)
|
|
31
|
+
latest_version: str | None = result.scalar()
|
|
32
|
+
if not latest_version:
|
|
33
|
+
return None
|
|
34
|
+
return latest_version
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_migrations_table_if_not_exists() -> None:
|
|
38
|
+
"""
|
|
39
|
+
Creates the migrations table in the database
|
|
40
|
+
if it does not already exist.
|
|
41
|
+
Returns:
|
|
42
|
+
None
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
46
|
+
with engine.begin() as connection:
|
|
47
|
+
connection.execute(statement=CREATE_MIGRATIONS_TABLE_STMT)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_migration(
|
|
51
|
+
sql_statements: list[str],
|
|
52
|
+
version: str,
|
|
53
|
+
migration_operation: MigrationOperationType,
|
|
54
|
+
filename: str | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Execute a database migration by running SQL statements and recording the migration version.
|
|
58
|
+
Args:
|
|
59
|
+
sql_statements (list[str]): List of SQL statements to execute as part of the migration
|
|
60
|
+
version (str): Version identifier to record after successful migration
|
|
61
|
+
Returns:
|
|
62
|
+
None
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if migration_operation == MigrationOperationType.UPGRADE and filename is None:
|
|
66
|
+
raise ValueError("Filename must be provided for upgrade migrations.")
|
|
67
|
+
|
|
68
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
69
|
+
|
|
70
|
+
with engine.begin() as connection:
|
|
71
|
+
for statement in sql_statements:
|
|
72
|
+
connection.execute(text(statement))
|
|
73
|
+
|
|
74
|
+
if migration_operation == MigrationOperationType.UPGRADE:
|
|
75
|
+
assert filename is not None
|
|
76
|
+
description: str = get_description_from_filename(filename=filename)
|
|
77
|
+
connection.execute(
|
|
78
|
+
statement=INSERT_VERSION_STMT,
|
|
79
|
+
parameters={
|
|
80
|
+
"version": version,
|
|
81
|
+
"description": description,
|
|
82
|
+
"filename": filename,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
elif migration_operation == MigrationOperationType.ROLLBACK:
|
|
87
|
+
connection.execute(
|
|
88
|
+
statement=DELETE_VERSION_STMT, parameters={"version": version}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_latest_versions(limit: int) -> list[str]:
|
|
93
|
+
"""
|
|
94
|
+
Retrieve the latest N migration versions from the database.
|
|
95
|
+
Args:
|
|
96
|
+
limit (int): The number of latest versions to retrieve
|
|
97
|
+
Returns:
|
|
98
|
+
list[str]: A list of the latest migration version strings
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
102
|
+
latest_versions: list[str] = []
|
|
103
|
+
|
|
104
|
+
with engine.begin() as connection:
|
|
105
|
+
result: Result[tuple[str]] = connection.execute(
|
|
106
|
+
statement=LATEST_VERSIONS_QUERY,
|
|
107
|
+
parameters={"limit": limit},
|
|
108
|
+
)
|
|
109
|
+
latest_versions: list[str] = [row[0] for row in result.fetchall()]
|
|
110
|
+
|
|
111
|
+
return latest_versions
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_latest_versions_by_starting_version(
|
|
115
|
+
starting_version: str,
|
|
116
|
+
) -> list[str]:
|
|
117
|
+
"""
|
|
118
|
+
Retrieve the latest N migration versions from the database,
|
|
119
|
+
starting from a specific version.
|
|
120
|
+
Args:
|
|
121
|
+
starting_version (str): The version to start retrieving from
|
|
122
|
+
limit (int): The number of latest versions to retrieve
|
|
123
|
+
Returns:
|
|
124
|
+
list[str]: A list of the latest migration version strings
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
128
|
+
latest_versions: list[str] = []
|
|
129
|
+
starting_version = starting_version
|
|
130
|
+
|
|
131
|
+
with engine.begin() as connection:
|
|
132
|
+
version_exists_result: Result[tuple[int]] = connection.execute(
|
|
133
|
+
statement=CHECK_IF_VERSION_EXISTS_QUERY,
|
|
134
|
+
parameters={"version": starting_version},
|
|
135
|
+
)
|
|
136
|
+
version_exists: int = version_exists_result.scalar_one()
|
|
137
|
+
|
|
138
|
+
if version_exists == 0:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"'{starting_version}' has not been applied yet or does not exist."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
latest_versions_result: Result[tuple[str]] = connection.execute(
|
|
144
|
+
statement=LATEST_VERSIONS_BY_STARTING_VERSION_QUERY,
|
|
145
|
+
parameters={"starting_version": starting_version},
|
|
146
|
+
)
|
|
147
|
+
latest_versions: list[str] = [
|
|
148
|
+
row[0] for row in latest_versions_result.fetchall()
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
return latest_versions
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def migrations_table_exists() -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if the jetbase_migrations table exists in the database.
|
|
157
|
+
Returns:
|
|
158
|
+
bool: True if the jetbase_migrations table exists, False otherwise.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
162
|
+
|
|
163
|
+
with engine.begin() as connection:
|
|
164
|
+
result: Result[tuple[bool]] = connection.execute(
|
|
165
|
+
statement=CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY
|
|
166
|
+
)
|
|
167
|
+
table_exists: bool = result.scalar_one()
|
|
168
|
+
|
|
169
|
+
return table_exists
|
jetbase/core/rollback.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.core.dry_run import process_dry_run
|
|
4
|
+
from jetbase.core.file_parser import parse_rollback_statements
|
|
5
|
+
from jetbase.core.repository import (
|
|
6
|
+
get_latest_versions,
|
|
7
|
+
get_latest_versions_by_starting_version,
|
|
8
|
+
migrations_table_exists,
|
|
9
|
+
run_migration,
|
|
10
|
+
)
|
|
11
|
+
from jetbase.core.version import get_migration_filepaths_by_version
|
|
12
|
+
from jetbase.enums import MigrationOperationType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def rollback_cmd(
|
|
16
|
+
count: int | None = None, to_version: str | None = None, dry_run: bool = False
|
|
17
|
+
) -> None:
|
|
18
|
+
table_exists: bool = migrations_table_exists()
|
|
19
|
+
if not table_exists:
|
|
20
|
+
print("No migrations have been applied; nothing to rollback.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
if count is not None and to_version is not None:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"Cannot specify both 'count' and 'to_version' for rollback. "
|
|
26
|
+
"Select only one, or do not specify either to rollback the last migration."
|
|
27
|
+
)
|
|
28
|
+
if count is None and to_version is None:
|
|
29
|
+
count = 1
|
|
30
|
+
|
|
31
|
+
latest_migration_versions: list[str] = []
|
|
32
|
+
if count:
|
|
33
|
+
latest_migration_versions = get_latest_versions(limit=count)
|
|
34
|
+
elif to_version:
|
|
35
|
+
latest_migration_versions = get_latest_versions_by_starting_version(
|
|
36
|
+
starting_version=to_version
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if not latest_migration_versions:
|
|
40
|
+
print("No migrations have been applied; nothing to rollback.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
versions_to_rollback: dict[str, str] = get_migration_filepaths_by_version(
|
|
44
|
+
directory=os.path.join(os.getcwd(), "migrations"),
|
|
45
|
+
version_to_start_from=latest_migration_versions[-1],
|
|
46
|
+
end_version=latest_migration_versions[0],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
versions_to_rollback: dict[str, str] = dict(reversed(versions_to_rollback.items()))
|
|
50
|
+
|
|
51
|
+
if not dry_run:
|
|
52
|
+
for version, file_path in versions_to_rollback.items():
|
|
53
|
+
sql_statements: list[str] = parse_rollback_statements(file_path=file_path)
|
|
54
|
+
run_migration(
|
|
55
|
+
sql_statements=sql_statements,
|
|
56
|
+
version=version,
|
|
57
|
+
migration_operation=MigrationOperationType.ROLLBACK,
|
|
58
|
+
)
|
|
59
|
+
filename: str = os.path.basename(file_path)
|
|
60
|
+
|
|
61
|
+
print(f"Rollback applied successfully: {filename}")
|
|
62
|
+
|
|
63
|
+
else:
|
|
64
|
+
process_dry_run(
|
|
65
|
+
version_to_filepath=versions_to_rollback,
|
|
66
|
+
migration_operation=MigrationOperationType.ROLLBACK,
|
|
67
|
+
)
|
jetbase/core/upgrade.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.core.dry_run import process_dry_run
|
|
4
|
+
from jetbase.core.file_parser import parse_upgrade_statements
|
|
5
|
+
from jetbase.core.repository import (
|
|
6
|
+
create_migrations_table_if_not_exists,
|
|
7
|
+
get_last_updated_version,
|
|
8
|
+
run_migration,
|
|
9
|
+
)
|
|
10
|
+
from jetbase.core.version import get_migration_filepaths_by_version
|
|
11
|
+
from jetbase.enums import MigrationOperationType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def upgrade_cmd(
|
|
15
|
+
count: int | None = None, to_version: str | None = None, dry_run: bool = False
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Run database migrations by applying all pending SQL migration files.
|
|
19
|
+
Executes migration files in order starting from the last applied version,
|
|
20
|
+
updating the migrations tracking table after each successful migration.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
None
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if count is not None and to_version is not None:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
"Cannot specify both 'count' and 'to_version' for upgrade. "
|
|
29
|
+
"Select only one, or do not specify either to run all pending migrations."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
create_migrations_table_if_not_exists()
|
|
33
|
+
latest_version: str | None = get_last_updated_version()
|
|
34
|
+
|
|
35
|
+
all_versions: dict[str, str] = get_migration_filepaths_by_version(
|
|
36
|
+
directory=os.path.join(os.getcwd(), "migrations"),
|
|
37
|
+
version_to_start_from=latest_version,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if latest_version:
|
|
41
|
+
all_versions = dict(list(all_versions.items())[1:])
|
|
42
|
+
|
|
43
|
+
if count:
|
|
44
|
+
all_versions = dict(list(all_versions.items())[:count])
|
|
45
|
+
elif to_version:
|
|
46
|
+
if all_versions.get(to_version) is None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"The specified to_version '{to_version}' does not exist among pending migrations."
|
|
49
|
+
)
|
|
50
|
+
all_versions_list = []
|
|
51
|
+
for file_version, file_path in all_versions.items():
|
|
52
|
+
all_versions_list.append((file_version, file_path))
|
|
53
|
+
if file_version == to_version:
|
|
54
|
+
break
|
|
55
|
+
all_versions = dict(all_versions_list)
|
|
56
|
+
|
|
57
|
+
if not dry_run:
|
|
58
|
+
for version, file_path in all_versions.items():
|
|
59
|
+
sql_statements: list[str] = parse_upgrade_statements(file_path=file_path)
|
|
60
|
+
filename: str = os.path.basename(file_path)
|
|
61
|
+
|
|
62
|
+
run_migration(
|
|
63
|
+
sql_statements=sql_statements,
|
|
64
|
+
version=version,
|
|
65
|
+
migration_operation=MigrationOperationType.UPGRADE,
|
|
66
|
+
filename=filename,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(f"Migration applied successfully: {filename}")
|
|
70
|
+
|
|
71
|
+
else:
|
|
72
|
+
process_dry_run(
|
|
73
|
+
version_to_filepath=all_versions,
|
|
74
|
+
migration_operation=MigrationOperationType.UPGRADE,
|
|
75
|
+
)
|
jetbase/core/version.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.core.file_parser import is_filename_format_valid, is_filename_length_valid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _get_version_key_from_filename(filename: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Extract and normalize version key from a filename.
|
|
9
|
+
|
|
10
|
+
The function extracts the version part from a filename that follows the format:
|
|
11
|
+
'V{version}__{description}.sql' where version can be like '1', '1_1', or '1.1'.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
filename (str): The filename to extract version from.
|
|
15
|
+
Must follow pattern like 'V1__description.sql' or 'V1_1__description.sql'
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: Normalized version string where underscores are replaced with periods.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If the filename doesn't follow the expected format.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
>>> _get_version_key_from_filename("V1__my_description.sql")
|
|
25
|
+
'1'
|
|
26
|
+
>>> _get_version_key_from_filename("V1_1__my_description.sql")
|
|
27
|
+
'1.1'
|
|
28
|
+
>>> _get_version_key_from_filename("V1.1__my_description.sql")
|
|
29
|
+
'1.1'
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
version = filename.split("__")[0][1:]
|
|
33
|
+
except Exception:
|
|
34
|
+
raise (
|
|
35
|
+
ValueError(
|
|
36
|
+
"Filename must be in the following format: V1__my_description.sql, V1_1__my_description.sql, V1.1__my_description.sql"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
return version.replace("_", ".")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _convert_version_tuple_to_str(version_tuple: tuple[str, ...]) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Convert a version tuple to a string representation.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
version_tuple (tuple[str, ...]): A tuple containing version components as strings.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: A string representation of the version, with components joined by periods.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> _convert_version_tuple_to_str(('1', '2', '3'))
|
|
54
|
+
'1.2.3'
|
|
55
|
+
"""
|
|
56
|
+
return ".".join(version_tuple)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def convert_version_to_tuple(version: str) -> tuple[str, ...]:
|
|
60
|
+
"""
|
|
61
|
+
Convert a version string to a tuple of version components.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
version_str (str): A version string with components separated by periods.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple[str, ...]: A tuple containing the version components as strings.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> convert_version_to_tuple("1.2.3")
|
|
71
|
+
('1', '2', '3')
|
|
72
|
+
"""
|
|
73
|
+
return tuple(version.split("."))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_migration_filepaths_by_version(
|
|
77
|
+
directory: str,
|
|
78
|
+
version_to_start_from: str | None = None,
|
|
79
|
+
end_version: str | None = None,
|
|
80
|
+
) -> dict[str, str]:
|
|
81
|
+
"""
|
|
82
|
+
Retrieve migration file paths organized by version number.
|
|
83
|
+
|
|
84
|
+
Walks through the specified directory to find SQL migration files and creates
|
|
85
|
+
a dictionary mapping version strings to their file paths. Files are validated
|
|
86
|
+
for proper naming format and length. Results can be filtered by version range.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
directory (str): The directory path to search for SQL migration files.
|
|
90
|
+
version_to_start_from (str | None): Optional minimum version (inclusive).
|
|
91
|
+
Only files with versions >= this value are included. Defaults to None.
|
|
92
|
+
end_version (str | None): Optional maximum version (exclusive).
|
|
93
|
+
Only files with versions < this value are included. Defaults to None.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict[str, str]: Dictionary mapping version strings to file paths,
|
|
97
|
+
sorted in ascending order by version number.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If a filename doesn't match the required format or exceeds
|
|
101
|
+
the maximum length of 512 characters.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> get_migration_filepaths_by_version('/path/to/migrations')
|
|
105
|
+
{'1.0.0': '/path/to/migrations/V1_0_0__init.sql',
|
|
106
|
+
'1.2.0': '/path/to/migrations/V1_2_0__add_users.sql'}
|
|
107
|
+
>>> get_migration_filepaths_by_version('/path/to/migrations', version_to_start_from='1.1.0')
|
|
108
|
+
{'1.2.0': '/path/to/migrations/V1_2_0__add_users.sql'}
|
|
109
|
+
"""
|
|
110
|
+
version_to_filepath_dict: dict[str, str] = {}
|
|
111
|
+
|
|
112
|
+
for root, _, files in os.walk(directory):
|
|
113
|
+
for filename in files:
|
|
114
|
+
if filename.endswith(".sql") and not is_filename_format_valid(
|
|
115
|
+
filename=filename
|
|
116
|
+
):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Invalid migration filename format: {filename}. "
|
|
119
|
+
"Filenames must start with 'V', followed by the version number, "
|
|
120
|
+
"two underscores '__', a description, and end with '.sql'. "
|
|
121
|
+
"V<version_number>__<my_description>.sql. "
|
|
122
|
+
"Examples: 'V1_2_0__add_new_table.sql' or 'V1.2.0__add_new_table.sql'"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if filename.endswith(".sql") and not is_filename_length_valid(
|
|
126
|
+
filename=filename
|
|
127
|
+
):
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Migration filename too long: {filename}. "
|
|
130
|
+
f"Filename is currently {len(filename)} characters. "
|
|
131
|
+
"Filenames must not exceed 512 characters."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if is_filename_format_valid(filename=filename):
|
|
135
|
+
file_path: str = os.path.join(root, filename)
|
|
136
|
+
version: str = _get_version_key_from_filename(filename=filename)
|
|
137
|
+
version_tuple: tuple[str, ...] = convert_version_to_tuple(
|
|
138
|
+
version=version
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if end_version:
|
|
142
|
+
if version_tuple > convert_version_to_tuple(version=end_version):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if version_to_start_from:
|
|
146
|
+
if version_tuple >= convert_version_to_tuple(
|
|
147
|
+
version=version_to_start_from
|
|
148
|
+
):
|
|
149
|
+
version_to_filepath_dict[
|
|
150
|
+
_convert_version_tuple_to_str(version_tuple=version_tuple)
|
|
151
|
+
] = file_path
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
version_to_filepath_dict[
|
|
155
|
+
_convert_version_tuple_to_str(version_tuple=version_tuple)
|
|
156
|
+
] = file_path
|
|
157
|
+
|
|
158
|
+
ordered_version_to_filepath_dict: dict[str, str] = {
|
|
159
|
+
version: version_to_filepath_dict[version]
|
|
160
|
+
for version in sorted(version_to_filepath_dict.keys())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ordered_version_to_filepath_dict
|
jetbase/enums.py
ADDED
jetbase/queries.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from sqlalchemy import TextClause, text
|
|
2
|
+
|
|
3
|
+
LATEST_VERSION_QUERY: TextClause = text("""
|
|
4
|
+
SELECT
|
|
5
|
+
version
|
|
6
|
+
FROM
|
|
7
|
+
jetbase_migrations
|
|
8
|
+
ORDER BY
|
|
9
|
+
applied_at DESC
|
|
10
|
+
LIMIT 1
|
|
11
|
+
""")
|
|
12
|
+
|
|
13
|
+
CREATE_MIGRATIONS_TABLE_STMT: TextClause = text("""
|
|
14
|
+
CREATE TABLE IF NOT EXISTS jetbase_migrations (
|
|
15
|
+
version VARCHAR(255) PRIMARY KEY,
|
|
16
|
+
description VARCHAR(500),
|
|
17
|
+
filename VARCHAR(512),
|
|
18
|
+
order_executed INT GENERATED ALWAYS AS IDENTITY,
|
|
19
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
20
|
+
)
|
|
21
|
+
""")
|
|
22
|
+
|
|
23
|
+
INSERT_VERSION_STMT: TextClause = text("""
|
|
24
|
+
INSERT INTO jetbase_migrations (version, description, filename)
|
|
25
|
+
VALUES (:version, :description, :filename)
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
DELETE_VERSION_STMT: TextClause = text("""
|
|
29
|
+
DELETE FROM jetbase_migrations
|
|
30
|
+
WHERE version = :version
|
|
31
|
+
""")
|
|
32
|
+
|
|
33
|
+
LATEST_VERSIONS_QUERY: TextClause = text("""
|
|
34
|
+
SELECT
|
|
35
|
+
version
|
|
36
|
+
FROM
|
|
37
|
+
jetbase_migrations
|
|
38
|
+
ORDER BY
|
|
39
|
+
applied_at DESC
|
|
40
|
+
LIMIT :limit
|
|
41
|
+
""")
|
|
42
|
+
|
|
43
|
+
LATEST_VERSIONS_BY_STARTING_VERSION_QUERY: TextClause = text("""
|
|
44
|
+
SELECT
|
|
45
|
+
version
|
|
46
|
+
FROM
|
|
47
|
+
jetbase_migrations
|
|
48
|
+
WHERE applied_at >
|
|
49
|
+
(select applied_at from jetbase_migrations
|
|
50
|
+
where version = :starting_version)
|
|
51
|
+
ORDER BY
|
|
52
|
+
applied_at DESC
|
|
53
|
+
""")
|
|
54
|
+
|
|
55
|
+
CHECK_IF_VERSION_EXISTS_QUERY: TextClause = text("""
|
|
56
|
+
SELECT
|
|
57
|
+
COUNT(*)
|
|
58
|
+
FROM
|
|
59
|
+
jetbase_migrations
|
|
60
|
+
WHERE
|
|
61
|
+
version = :version
|
|
62
|
+
""")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
CHECK_IF_MIGRATIONS_TABLE_EXISTS_QUERY: TextClause = text("""
|
|
66
|
+
SELECT EXISTS (
|
|
67
|
+
SELECT 1
|
|
68
|
+
FROM information_schema.tables
|
|
69
|
+
WHERE table_schema = 'public'
|
|
70
|
+
AND table_name = 'jetbase_migrations'
|
|
71
|
+
)
|
|
72
|
+
""")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jetbase
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Jetbase is a Python database migration tool
|
|
5
|
+
Author-email: jaz <jaz.allibhai@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: sqlalchemy>=2.0.10
|
|
9
|
+
Requires-Dist: typer>=0.12.3
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# jetbase
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
jetbase/config.py,sha256=-YZgS-fVW8k53azx9Ur0GjJvYCZLwOHkguMr_Ujc-mo,1731
|
|
2
|
+
jetbase/constants.py,sha256=X2SYgXRxj77zWB-6N764RQsJmKKUd5Wa7RaQGN6eTiQ,339
|
|
3
|
+
jetbase/enums.py,sha256=LFNZnWcBxvF4kIdZYdGKn2Z65ADHJs38MnGAqeEbZ_U,110
|
|
4
|
+
jetbase/queries.py,sha256=jZ9ei8V-dEb_8ZAwtSAFqLnIEI5rumm3KvQmw4CiZk4,1619
|
|
5
|
+
jetbase/cli/main.py,sha256=I0HIRGC7dmTvEhgdEBksF-p7QPoY4-CfVcm-niVhzck,1504
|
|
6
|
+
jetbase/core/dry_run.py,sha256=zIV4NM_OL-aR7UJlEXYysiGairMOgytnT5rg8baFSms,1469
|
|
7
|
+
jetbase/core/file_parser.py,sha256=Aau7iFaBoZxOgoxB-4C5XAvE_a9142kv7sJHOAANe2A,6069
|
|
8
|
+
jetbase/core/initialize.py,sha256=lm3FWhe1YNKCTmZix465tBGjJsl_YshhRo2ENm7iPFM,955
|
|
9
|
+
jetbase/core/repository.py,sha256=JrmqFkIIbICPsxnY7O3ei9kLL-0mXWFefU0QPHppcgw,5586
|
|
10
|
+
jetbase/core/rollback.py,sha256=e87xlI3WJaOAQD_4QdmeZIDj9a5CW7wKUxQkgIFXy3c,2385
|
|
11
|
+
jetbase/core/upgrade.py,sha256=GowNGAxXRD9dks8FWqSMoQPCMnMh_yP01HNEYB5Py2Q,2603
|
|
12
|
+
jetbase/core/version.py,sha256=mPeJEgcqA0hZMb6YMZQcfc-MGcNI2Ls_7hiH6vJnC1k,6136
|
|
13
|
+
jetbase-0.7.0.dist-info/METADATA,sha256=6vue-I4pBPIQRmMHY3JrxjU7j81fgL6ITMiWcwnmbOs,306
|
|
14
|
+
jetbase-0.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
jetbase-0.7.0.dist-info/entry_points.txt,sha256=-Pe7A0r8aGF-6C14hB0Ck8aEuszuH5pdadt5OWSiWwA,50
|
|
16
|
+
jetbase-0.7.0.dist-info/licenses/LICENSE,sha256=hyssQNKtnK32aFCk8mVR406ysMtGNP6_dZCig8zBk4Q,1065
|
|
17
|
+
jetbase-0.7.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 jaz-alli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|