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.

@@ -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
@@ -172,3 +172,6 @@ cython_debug/
172
172
 
173
173
  # PyPI configuration file
174
174
  .pypirc
175
+
176
+ # Other files and folders
177
+ .python-version
@@ -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.1
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.9
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
+ ]