I am currently trying to build a mobile app using Python, Kivy and SQLAlchemy. However, I am having difficulty implementing database migrations using Alembic.
Basically, the migrations work just fine on my computer but Alembic is not finding any migrations when called from my phone. I am using buildozer for the APK building.
My guess is that Alembic is not finding the migrations files from the versions
folder when run on android, probably because of a path issue.
Additional notes on the app functioning on Android :
The database is created correctly when the app the launched for the first time
When the app is launched, Alembic starts to run but doesn't find any migrations to apply. Then the app exits without any error code.
If I replace manually the database with one already up-to-date, the app runs smootly.
If I replace the database with one initialized but not up-to-date (containing the initial migration but not the updates). I get this error using
adb logcat
:-
02-17 21:27:48.451 17547 21720 I python : Migration failed: Can't locate revision identified by '2c282135e834' 02-17 21:27:48.933 17547 21720 I python : Python for android ended.
-
For reference here is my current code. I left all the debugging prints I added, hopefully it can help...
The Kivy App definition :
class AStatApp(MDApp):
def build(self):
self.theme_cls.theme_style = "Light"
self.theme_cls.primary_palette = "Darkred"
db_path = get_db_path()
# This is the function currently not working
try:
run_migrations()
except Exception as e:
print(f"Migration failed: {e}")
self.Session = self.init_db(db_path)
Builder.load_file("kv/selector.kv")
Builder.load_file("kv/ascent-list-screen.kv")
Builder.load_file("kv/ascent-screen.kv")
Builder.load_file("kv/settings-screen.kv")
Builder.load_file("kv/area-screen.kv")
Builder.load_file("kv/statistic-screen.kv")
Builder.load_file("kv/statistic-filter-screen.kv")
Builder.load_file("kv/to-do-list-screen.kv")
Builder.load_file("kv/screenmanager.kv")
return MainScreenManager()
The get_db_path()
:
def get_db_path():
db_filename = "astat.db"
if platform == "win":
data_dir = os.getcwd()
elif platform == "android":
# Get Android context
from jnius import autoclass, cast
PythonActivity = autoclass(".kivy.android.PythonActivity")
context = cast("android.content.Context", PythonActivity.mActivity)
# Get external storage path for the app
file_p = cast("java.io.File", context.getExternalFilesDir(None))
data_dir = file_p.getAbsolutePath()
writable_db_path = os.path.join(data_dir, db_filename)
print(f"Final DB path: {writable_db_path}")
print(f"Path exists: {os.path.exists(writable_db_path)}")
return writable_db_path
The run_migrations()
def run_migrations():
"""Run migrations using Alembic's command API with proper configuration."""
db_path = get_db_path()
# Creates database file if it doesnt exist
if not os.path.exists(db_path):
open(db_path, "a").close()
base_dir = os.path.abspath(os.path.dirname(__file__))
script_location = os.path.join(base_dir, "migrations")
# Configure Alembic programmatically
alembic_cfg = Config("alembic.ini")
alembic_cfg.set_main_option("script_location", script_location)
alembic_cfg.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}")
# Debug prints
db_url = alembic_cfg.get_main_option("sqlalchemy.url")
migrations = os.listdir(os.path.join(script_location, "versions"))
print(f"database url : {db_url}")
print(f"migration path: {script_location}")
print(f"Migration versions: {migrations}")
try:
command.upgrade(alembic_cfg, "head")
print("Database migrations applied successfully.")
except SystemExit as e:
if e.code != 0:
print(f"Migration failed with code {e.code}")
raise
the env.py
:
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
from alembic.script import ScriptDirectory
script = ScriptDirectory.from_config(config)
# Debug prints
print(ScriptDirectory.versions)
print("Discovered migrations:")
for rev in script.walk_revisions():
print(f"- {rev.revision} ({rev.doc})")
db_path = get_db_path()
print(f"Android DB Path: {db_path}")
print(f"File exists: {os.path.exists(db_path)}")
print(f"Migration files: {os.listdir(os.path.dirname(__file__))}")
# Actual migration run
connectable = create_engine(f"sqlite:///{db_path}")
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
So far, I have tried many things to reset the way Alembic is looking for migrations but nothing works (and I have a hard time remembering everything I tried...)
Also, with the current code, this is the adb logcat
i get when I launch the app for the first time (I removed everything before that is just about the normal kivy app setup) :
02-17 21:19:43.422 12846 17556 I python : [INFO ] [Window ] auto add sdl2 input provider
02-17 21:19:43.423 12846 17556 I python : [INFO ] [Window ] virtual keyboard not allowed, single mode, not docked
02-17 21:19:43.672 12846 17556 I python : [INFO ] [Clipboard ] Provider: android
02-17 21:19:43.912 12846 17556 I python : Final DB path: /data/user/0/com.cmareau.astat/files/astat.db
02-17 21:19:43.912 12846 17556 I python : Path exists: False
02-17 21:19:43.913 12846 17556 I python : Final DB path: /data/user/0/com.cmareau.astat/files/astat.db
02-17 21:19:43.913 12846 17556 I python : Path exists: False
02-17 21:19:43.914 12846 17556 I python : database url : sqlite:////data/user/0/com.cmareau.astat/files/astat.db
02-17 21:19:43.914 12846 17556 I python : migration path: /data/data/com.cmareau.astat/files/app/migrations
02-17 21:19:43.914 12846 17556 I python : Migration path content: ['2c282135e834_initial_migration.pyc', '3b4994652668_populate_grade_table.pyc', '__pycache__', 'db33e18bf97f_adding_of_todolist_sector_and_climbtodo_.pyc']
02-17 21:19:43.920 12846 17556 I python : Final DB path: /data/user/0/com.cmareau.astat/files/astat.db
02-17 21:19:43.920 12846 17556 I python : Path exists: True
02-17 21:19:43.920 12846 17556 I python : Android DB Path: /data/user/0/com.cmareau.astat/files/astat.db
02-17 21:19:43.920 12846 17556 I python : File exists: True
02-17 21:19:43.920 12846 17556 I python : Migration files: ['README', '__pycache__', 'env.pyc', 'script.py.mako', 'versions']
02-17 21:19:43.936 12846 17556 I python : Database migrations applied successfully.
02-17 21:19:44.232 12846 17556 I python : Python for android ended.