jetbase 0.0.1__tar.gz → 0.1.0__tar.gz
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.
Potentially problematic release.
This version of jetbase might be problematic. Click here for more details.
- jetbase-0.1.0/.github/workflows/ci.yml +99 -0
- jetbase-0.1.0/.github/workflows/publish.yml +35 -0
- {jetbase-0.0.1 → jetbase-0.1.0}/.gitignore +3 -0
- jetbase-0.1.0/.pre-commit-config.yaml +14 -0
- {jetbase-0.0.1 → jetbase-0.1.0}/PKG-INFO +3 -2
- jetbase-0.1.0/jetbase/cli/main.py +19 -0
- jetbase-0.1.0/jetbase/config.py +60 -0
- jetbase-0.1.0/jetbase/constants.py +12 -0
- jetbase-0.1.0/jetbase/core/file_parser.py +29 -0
- jetbase-0.1.0/jetbase/core/initialize.py +33 -0
- jetbase-0.1.0/jetbase/core/repository.py +59 -0
- jetbase-0.1.0/jetbase/core/upgrade.py +34 -0
- jetbase-0.1.0/jetbase/core/version.py +117 -0
- jetbase-0.1.0/jetbase/queries.py +23 -0
- jetbase-0.1.0/pyproject.toml +30 -0
- jetbase-0.1.0/tests/core/test_file_parser.py +131 -0
- jetbase-0.1.0/tests/core/test_initialize.py +25 -0
- jetbase-0.1.0/tests/core/test_version.py +46 -0
- jetbase-0.1.0/tests/test_integration/jetbase/config.py +5 -0
- jetbase-0.1.0/tests/test_integration/jetbase/migrations/V1__create_users_table.sql +4 -0
- jetbase-0.1.0/tests/test_integration/jetbase/migrations/V2__add_initial_users.sql +5 -0
- jetbase-0.1.0/tests/test_integration/test_cli.py +29 -0
- jetbase-0.1.0/uv.lock +354 -0
- jetbase-0.0.1/.python-version +0 -1
- jetbase-0.0.1/pyproject.toml +0 -14
- jetbase-0.0.1/src/jetbase/__init__.py +0 -2
- {jetbase-0.0.1 → jetbase-0.1.0}/LICENSE +0 -0
- {jetbase-0.0.1 → jetbase-0.1.0}/README.md +0 -0
- /jetbase-0.0.1/src/jetbase/py.typed → /jetbase-0.1.0/tests/core/test_upgrade.py +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- "my_package/**" # only run when code inside my_package changes
|
|
7
|
+
- ".github/workflows/ci-pr.yml" # rerun if this workflow changes
|
|
8
|
+
- "pyproject.toml" # rerun if deps/config change
|
|
9
|
+
- "uv.lock" # rerun if lockfile changes
|
|
10
|
+
workflow_dispatch: {}
|
|
11
|
+
jobs:
|
|
12
|
+
lint-type-format-checks:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.10"
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v6
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --dev
|
|
28
|
+
|
|
29
|
+
- name: Pyrefly type checker
|
|
30
|
+
run: uv run pyrefly check
|
|
31
|
+
|
|
32
|
+
- name: Ruff linter
|
|
33
|
+
run: uv run ruff check jetbase tests
|
|
34
|
+
|
|
35
|
+
- name: Ruff formatter
|
|
36
|
+
run: uv run ruff format jetbase tests --check
|
|
37
|
+
|
|
38
|
+
unit-tests:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v5
|
|
43
|
+
|
|
44
|
+
- name: Set up Python
|
|
45
|
+
uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: "3.10"
|
|
48
|
+
|
|
49
|
+
- name: Install uv
|
|
50
|
+
uses: astral-sh/setup-uv@v6
|
|
51
|
+
|
|
52
|
+
- name: Install dependencies
|
|
53
|
+
run: uv sync --dev
|
|
54
|
+
|
|
55
|
+
- name: Run unit tests
|
|
56
|
+
run: uv run pytest tests/core/ -v
|
|
57
|
+
|
|
58
|
+
integration-tests:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
|
|
61
|
+
env:
|
|
62
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
|
|
63
|
+
|
|
64
|
+
services:
|
|
65
|
+
postgres:
|
|
66
|
+
image: postgres:15
|
|
67
|
+
env:
|
|
68
|
+
POSTGRES_USER: postgres
|
|
69
|
+
POSTGRES_PASSWORD: postgres
|
|
70
|
+
POSTGRES_DB: test_db
|
|
71
|
+
options: >-
|
|
72
|
+
--health-cmd pg_isready
|
|
73
|
+
--health-interval 10s
|
|
74
|
+
--health-timeout 5s
|
|
75
|
+
--health-retries 5
|
|
76
|
+
ports:
|
|
77
|
+
- 5432:5432
|
|
78
|
+
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v5
|
|
81
|
+
|
|
82
|
+
- name: Set up Python
|
|
83
|
+
uses: actions/setup-python@v5
|
|
84
|
+
with:
|
|
85
|
+
python-version: "3.10"
|
|
86
|
+
|
|
87
|
+
- name: Install uv
|
|
88
|
+
uses: astral-sh/setup-uv@v6
|
|
89
|
+
|
|
90
|
+
- name: Install dependencies
|
|
91
|
+
run: uv sync --dev
|
|
92
|
+
|
|
93
|
+
- name: Run jetbase
|
|
94
|
+
working-directory: tests/test_integration/jetbase
|
|
95
|
+
run: uv run jetbase upgrade
|
|
96
|
+
|
|
97
|
+
- name: Run integration tests
|
|
98
|
+
working-directory: tests/test_integration
|
|
99
|
+
run: uv run pytest
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
if: github.actor == 'jaz-alli'
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout code
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.10"
|
|
26
|
+
|
|
27
|
+
- name: Build package
|
|
28
|
+
run: |
|
|
29
|
+
pip install build
|
|
30
|
+
python -m build
|
|
31
|
+
|
|
32
|
+
- name: Publish to PyPI
|
|
33
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
34
|
+
with:
|
|
35
|
+
attestations: true
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.14.1
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
args: ["--select", "I", "--fix"]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
|
|
9
|
+
- repo: https://github.com/facebook/pyrefly-pre-commit
|
|
10
|
+
rev: 0.0.1
|
|
11
|
+
hooks:
|
|
12
|
+
- id: pyrefly-typecheck-system
|
|
13
|
+
name: Pyrefly
|
|
14
|
+
pass_filenames: false
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jetbase
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Jetbase is a Python database migration tool
|
|
5
5
|
Author-email: jaz <jaz.allibhai@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: sqlalchemy>=2.0.44
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
|
|
10
11
|
# jetbase
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from jetbase.core.initialize import initilize_jetbase
|
|
4
|
+
from jetbase.core.upgrade import upgrade
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(description="Jetbase CLI")
|
|
9
|
+
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
11
|
+
subparsers.add_parser("init", help="Initialize jetbase in current directory")
|
|
12
|
+
subparsers.add_parser("upgrade", help="Execute pending migrations")
|
|
13
|
+
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
if args.command == "init":
|
|
17
|
+
initilize_jetbase()
|
|
18
|
+
elif args.command == "upgrade":
|
|
19
|
+
upgrade()
|
|
@@ -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
|
|
@@ -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
|
+
"""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
def parse_sql_file(file_path: str) -> list[str]:
|
|
2
|
+
"""
|
|
3
|
+
Parse a SQL file and return a list of SQL statements.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
file_path (str): Path to the SQL file
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
list[str]: List of SQL statements
|
|
10
|
+
"""
|
|
11
|
+
statements = []
|
|
12
|
+
current_statement = []
|
|
13
|
+
|
|
14
|
+
with open(file_path, "r") as file:
|
|
15
|
+
for line in file:
|
|
16
|
+
line = line.strip()
|
|
17
|
+
|
|
18
|
+
if not line or line.startswith("--"):
|
|
19
|
+
continue
|
|
20
|
+
current_statement.append(line)
|
|
21
|
+
|
|
22
|
+
if line.endswith(";"):
|
|
23
|
+
statement = " ".join(current_statement)
|
|
24
|
+
statement = statement.rstrip(";").strip()
|
|
25
|
+
if statement:
|
|
26
|
+
statements.append(statement)
|
|
27
|
+
current_statement = []
|
|
28
|
+
|
|
29
|
+
return statements
|
|
@@ -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 initilize_jetbase() -> None:
|
|
33
|
+
create_directory_structure(base_path=BASE_DIR)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from sqlalchemy import Engine, Result, create_engine, text
|
|
2
|
+
|
|
3
|
+
from jetbase.config import get_sqlalchemy_url
|
|
4
|
+
from jetbase.queries import (
|
|
5
|
+
CREATE_MIGRATIONS_TABLE_STMT,
|
|
6
|
+
INSERT_VERSION_STMT,
|
|
7
|
+
LATEST_VERSION_QUERY,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_last_updated_version() -> str | None:
|
|
12
|
+
"""
|
|
13
|
+
Retrieves the latest version from the database.
|
|
14
|
+
This function connects to the database, executes a query to get the most recent version,
|
|
15
|
+
and returns that version as a string.
|
|
16
|
+
Returns:
|
|
17
|
+
str | None: The latest version string if available, None if no version was found.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
21
|
+
|
|
22
|
+
with engine.begin() as connection:
|
|
23
|
+
result: Result[tuple[str]] = connection.execute(LATEST_VERSION_QUERY)
|
|
24
|
+
latest_version: str | None = result.scalar()
|
|
25
|
+
if not latest_version:
|
|
26
|
+
return None
|
|
27
|
+
return latest_version
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_migrations_table() -> None:
|
|
31
|
+
"""
|
|
32
|
+
Creates the migrations table in the database
|
|
33
|
+
if it does not already exist.
|
|
34
|
+
Returns:
|
|
35
|
+
None
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
39
|
+
with engine.begin() as connection:
|
|
40
|
+
connection.execute(statement=CREATE_MIGRATIONS_TABLE_STMT)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_migration(sql_statements: list[str], version: str) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Execute a database migration by running SQL statements and recording the migration version.
|
|
46
|
+
Args:
|
|
47
|
+
sql_statements (list[str]): List of SQL statements to execute as part of the migration
|
|
48
|
+
version (str): Version identifier to record after successful migration
|
|
49
|
+
Returns:
|
|
50
|
+
None
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
engine: Engine = create_engine(url=get_sqlalchemy_url())
|
|
54
|
+
with engine.begin() as connection:
|
|
55
|
+
for statement in sql_statements:
|
|
56
|
+
connection.execute(text(statement))
|
|
57
|
+
connection.execute(
|
|
58
|
+
statement=INSERT_VERSION_STMT, parameters={"version": version}
|
|
59
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from jetbase.core.file_parser import parse_sql_file
|
|
4
|
+
from jetbase.core.repository import (
|
|
5
|
+
create_migrations_table,
|
|
6
|
+
get_last_updated_version,
|
|
7
|
+
run_migration,
|
|
8
|
+
)
|
|
9
|
+
from jetbase.core.version import get_versions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def upgrade() -> None:
|
|
13
|
+
"""
|
|
14
|
+
Run database migrations by applying all pending SQL migration files.
|
|
15
|
+
Executes migration files in order starting from the last applied version,
|
|
16
|
+
updating the migrations tracking table after each successful migration.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
None
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
create_migrations_table()
|
|
23
|
+
latest_version: str | None = get_last_updated_version()
|
|
24
|
+
all_versions = get_versions(
|
|
25
|
+
directory=os.path.join(os.getcwd(), "migrations"),
|
|
26
|
+
version_to_start_from=latest_version,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for version, file_path in all_versions.items():
|
|
30
|
+
sql_statements: list[str] = parse_sql_file(file_path)
|
|
31
|
+
run_migration(sql_statements=sql_statements, version=version)
|
|
32
|
+
filename: str = os.path.basename(file_path)
|
|
33
|
+
|
|
34
|
+
print(f"Migration applied successfully: {filename}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _get_version_key_from_filename(filename: str) -> str:
|
|
5
|
+
"""
|
|
6
|
+
Extract and normalize version key from a filename.
|
|
7
|
+
|
|
8
|
+
The function extracts the version part from a filename that follows the format:
|
|
9
|
+
'V{version}__{description}.sql' where version can be like '1', '1_1', or '1.1'.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
filename (str): The filename to extract version from.
|
|
13
|
+
Must follow pattern like 'V1__description.sql' or 'V1_1__description.sql'
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
str: Normalized version string where underscores are replaced with periods.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If the filename doesn't follow the expected format.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
>>> _get_version_key_from_filename("V1__my_description.sql")
|
|
23
|
+
'1'
|
|
24
|
+
>>> _get_version_key_from_filename("V1_1__my_description.sql")
|
|
25
|
+
'1.1'
|
|
26
|
+
>>> _get_version_key_from_filename("V1.1__my_description.sql")
|
|
27
|
+
'1.1'
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
version = filename.split("__")[0][1:]
|
|
31
|
+
except Exception:
|
|
32
|
+
raise (
|
|
33
|
+
ValueError(
|
|
34
|
+
"Filename must be in the following format: V1__my_description.sql, V1_1__my_description.sql, V1.1__my_description.sql"
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
return version.replace("_", ".")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _convert_version_tuple_to_str(version_tuple: tuple[str, ...]) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Convert a version tuple to a string representation.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
version_tuple (tuple[str, ...]): A tuple containing version components as strings.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: A string representation of the version, with components joined by periods.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> _convert_version_tuple_to_str(('1', '2', '3'))
|
|
52
|
+
'1.2.3'
|
|
53
|
+
"""
|
|
54
|
+
return ".".join(version_tuple)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def convert_version_to_tuple(version: str) -> tuple[str, ...]:
|
|
58
|
+
"""
|
|
59
|
+
Convert a version string to a tuple of version components.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
version_str (str): A version string with components separated by periods.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
tuple[str, ...]: A tuple containing the version components as strings.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> convert_version_to_tuple("1.2.3")
|
|
69
|
+
('1', '2', '3')
|
|
70
|
+
"""
|
|
71
|
+
return tuple(version.split("."))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_versions(
|
|
75
|
+
directory: str, version_to_start_from: str | None = None
|
|
76
|
+
) -> dict[str, str]:
|
|
77
|
+
"""
|
|
78
|
+
Retrieves SQL files from the specified directory and organizes them by version.
|
|
79
|
+
This function walks through the directory tree, collects all SQL files, and creates a dictionary
|
|
80
|
+
mapping version strings to file paths. Files can be filtered to only include versions greater
|
|
81
|
+
than a specified starting version.
|
|
82
|
+
Args:
|
|
83
|
+
directory: The directory path to search for SQL files
|
|
84
|
+
version_to_start_from: Optional version string to filter results, only returning
|
|
85
|
+
versions greater than this value
|
|
86
|
+
Returns:
|
|
87
|
+
A dictionary mapping version strings to file paths, sorted by version number
|
|
88
|
+
Example:
|
|
89
|
+
>>> get_versions('/path/to/sql/files')
|
|
90
|
+
{'1.0.0': '/path/to/sql/files/v1_0_0__description.sql', '1.2.0': '/path/to/sql/files/v1_2_0__description.sql'}
|
|
91
|
+
"""
|
|
92
|
+
version_to_filepath_dict: dict[str, str] = {}
|
|
93
|
+
for root, _, files in os.walk(directory):
|
|
94
|
+
for file in files:
|
|
95
|
+
if file.endswith(".sql"):
|
|
96
|
+
file_path: str = os.path.join(root, file)
|
|
97
|
+
version: str = _get_version_key_from_filename(filename=file)
|
|
98
|
+
version_tuple: tuple[str, ...] = convert_version_to_tuple(
|
|
99
|
+
version=version
|
|
100
|
+
)
|
|
101
|
+
if version_to_start_from:
|
|
102
|
+
if version_tuple > convert_version_to_tuple(
|
|
103
|
+
version=version_to_start_from
|
|
104
|
+
):
|
|
105
|
+
version_to_filepath_dict[
|
|
106
|
+
_convert_version_tuple_to_str(version_tuple=version_tuple)
|
|
107
|
+
] = file_path
|
|
108
|
+
else:
|
|
109
|
+
version_to_filepath_dict[
|
|
110
|
+
_convert_version_tuple_to_str(version_tuple=version_tuple)
|
|
111
|
+
] = file_path
|
|
112
|
+
ordered_version_to_filepath_dict: dict[str, str] = {
|
|
113
|
+
version: version_to_filepath_dict[version]
|
|
114
|
+
for version in sorted(version_to_filepath_dict.keys())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ordered_version_to_filepath_dict
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
created_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
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
17
|
+
)
|
|
18
|
+
""")
|
|
19
|
+
|
|
20
|
+
INSERT_VERSION_STMT: TextClause = text("""
|
|
21
|
+
INSERT INTO jetbase_migrations (version)
|
|
22
|
+
VALUES (:version)
|
|
23
|
+
""")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jetbase"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Jetbase is a Python database migration tool"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "jaz", email = "jaz.allibhai@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"sqlalchemy>=2.0.44",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
jetbase = "jetbase.cli.main:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["jetbase"]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"psycopg2-binary>=2.9.11",
|
|
27
|
+
"pyrefly>=0.38.2",
|
|
28
|
+
"pytest>=8.4.2",
|
|
29
|
+
"ruff>=0.14.2",
|
|
30
|
+
]
|