"""Generate SQLAlchemy models from MySQL database using sqlacodegen.""" import re from collections.abc import Generator from pathlib import Path from typing import NamedTuple from sqlalchemy import Engine, MetaData from . import AbacusGenerator from .utils.print import info, warn class ModelSection(NamedTuple): """ModelSection class.""" id: int max_index: int min_index: int name: str def generate_models(engine: Engine, output_dir: Path) -> Path: """Generate SQLAlchemy models.""" info('Connecting to the database...') try: with engine.connect() as conn: conn.exec_driver_sql('SELECT 1') info('\tConnection established') info('Reflecting the database schema...') metadata = MetaData() metadata.reflect(bind=engine) all_tables = list(metadata.tables.values()) info(f'\tFound {len(all_tables)} tables') info('Generating models...') # List of string options. Things like --nojoined, --nobidi, etc. options: list[str] = [] generator = AbacusGenerator(metadata, engine, options) generated = generator.generate() finally: engine.dispose() models = list(_get_model_sections(generated.split('\n'))) info(f'\tGenerated {len(models)} models') if len(models) < len(all_tables): diff = len(all_tables) - len(models) warn(f'\tWarning: {diff} models were not generated. Possible reasons:') warn('\tviews, tables without PKs, association tables,') warn('\trollbacks, changelogs, ticketed tables, dated tables') output_file = output_dir / 'generated.py' info(f'Writing {len(models)} models to {output_dir}...') output_file.write_text(generated) info('Generating __init__.py...') models = sorted(models, key=lambda m: m.name) _create_init_file(output_dir, models) return output_file def _create_init_file(output_dir: Path, models: list[ModelSection]) -> Path: """Create __init__.py file that exports all generated models.""" exports: list[str] = [] init_file = output_dir / '__init__.py' lines: list[str] = ['"""Generated SQLAlchemy models."""\n'] # imports for model in models: exports.append(model.name) lines.append(f'from .generated import {model.name}') # __all__ lines.append('\n__all__ = [') for export in exports: lines.append(f'\t"{export}",') lines.append(']') # output init_file.write_text('\n'.join(lines)) return init_file def _get_model_sections(lines: list[str]) -> Generator[ModelSection]: class_name: str = '' model_id = 0 is_in_class = False min_index = 0 for index, line in enumerate(lines): match = re.search(r'^class\s+(\w+)\s*\(', line) if match is None: match = re.search(r'^(\w+)\s*=\s*Table\(', line) if match: if is_in_class: yield ModelSection( max_index=index, id=model_id, min_index=min_index, name=class_name, ) model_id += 1 class_name = match.group(1) is_in_class = True min_index = index if is_in_class: yield ModelSection( max_index=len(lines), id=model_id, min_index=min_index, name=class_name, )