Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
539011b
opus wrote main
nucccc Mar 3, 2026
ed5964e
refactored main to use functions from cli folder, mysql now has parsi…
nucccc Mar 5, 2026
e010225
added test for sql from file and error in case no source is specified
nucccc Mar 6, 2026
c850771
testing for mysql cli param without dbname
nucccc Mar 6, 2026
07edf4c
added better description to extras required for cli commands
nucccc Mar 6, 2026
4ca71d6
began working on a parametric form of testing with multiple codegen s…
nucccc Mar 13, 2026
3b1504a
parametric test for postgres
nucccc Mar 13, 2026
f01b576
improved postgres strategy to verify also cli generation
nucccc Mar 13, 2026
bc0db93
verify has cli steps also for sqlite
nucccc Mar 13, 2026
094cff8
written code to parse the sql out of a file in parametric test
nucccc Mar 14, 2026
b00bcd1
test parametric for a nullable element, prepared support code for mysql
nucccc Mar 14, 2026
7d63b8d
opus suggested me a very smart way to parametrize the test on the spe…
nucccc Mar 14, 2026
b721a3d
ai helped me refactor tests to reuse verifiers (which are supposed to…
nucccc Mar 21, 2026
e867bde
completed the code mysql uri parse
nucccc Mar 25, 2026
7f5160c
test setup for mysql now return info to build connection string
nucccc Mar 25, 2026
8fe8157
written code for mysql to launch cli inside verifier
nucccc Mar 25, 2026
b110b22
test_parametric now has docstring with '
nucccc Mar 27, 2026
3700291
actually testing for mysql cli usage
nucccc Mar 27, 2026
e872877
readme has description of cli usage
nucccc Mar 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,37 @@ class Athletes(SQLModel, table = True):
nation: Nations | None = Relationship(back_populates='athletess')
```

### CLI usage

CLI usage is supported, for example one can invokem with:

```bash
python3 -m sqlmodelgen -f /my/path/to/file.sql -o /my/path/to/output.py
```

help description for input arguments:

```
usage: sqlmodelgen [-h] (-f FILE | -s SQLITE | -p POSTGRES | -m MYSQL) [-o OUTPUT] [-r] [--schema SCHEMA]
[--dbname DBNAME]

sqlmodel classes code generation

options:
-h, --help show this help message and exit
-f, --file FILE SQL file path
-s, --sqlite SQLITE SQLite database path
-p, --postgres POSTGRES
PostgreSQL connection URL, requires postgres extension to be installed with "pip install
sqlmodelgen[postgres]"
-m, --mysql MYSQL MySQL connection URL, requires mysql extension to be installed with "pip install
sqlmodelgen[mysql]"
-o, --output OUTPUT Output file (default: stdout)
-r, --relationships Generate relationships
--schema SCHEMA PostgreSQL schema (default: public)
--dbname DBNAME MySQL database name (required with --mysql)
```

## Internal functioning

The library relies on [sqloxide](https://github.com/wseaton/sqloxide) to parse SQL code, then generates sqlmodel classes accordingly
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dev-dependencies = [
"coverage>=7.6.9",
"docker>=7.1.0",
"sqlmodel>=0.0.27",
"uuid>=1.30",
]

[build-system]
Expand Down
5 changes: 5 additions & 0 deletions src/sqlmodelgen/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlmodelgen.cli import main_cli


if __name__ == '__main__':
main_cli()
99 changes: 99 additions & 0 deletions src/sqlmodelgen/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'''
here there shall be code to actually have the cli
'''
import argparse
import sys
from pathlib import Path


from sqlmodelgen import gen_code_from_sql, gen_code_from_sqlite
from sqlmodelgen.utils.dependency_checker import check_postgres_deps, check_mysql_deps
from sqlmodelgen.utils.mysql_parse import parse_mysql


def main_cli(args=None):
parser = _build_parser()
args = parser.parse_args(args)

_act_on_args(args, parser)


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog='sqlmodelgen',
description='sqlmodel classes code generation',
)

# Mutually exclusive input sources
input_group = parser.add_mutually_exclusive_group(required=True)
input_group.add_argument('-f', '--file', type=Path, help='SQL file path')
input_group.add_argument('-s', '--sqlite', type=Path, help='SQLite database path')
input_group.add_argument('-p', '--postgres', type=str, help='PostgreSQL connection URL, requires postgres extension to be installed with "pip install sqlmodelgen[postgres]"')
input_group.add_argument('-m', '--mysql', type=str, help='MySQL connection URL, requires mysql extension to be installed with "pip install sqlmodelgen[mysql]"')

# Additional options
parser.add_argument('-o', '--output', type=Path, help='Output file (default: stdout)')
parser.add_argument('-r', '--relationships', action='store_true', help='Generate relationships')
parser.add_argument('--schema', type=str, default='public', help='PostgreSQL schema (default: public)')
parser.add_argument('--dbname', type=str, help='MySQL database name (required with --mysql)')

return parser


def _act_on_args(args: argparse.Namespace, usage: str):
# Validation: --mysql requires --dbname
if args.mysql and not args.dbname:
_exit('--dbname is required when using --mysql', usage)

# Generate code based on input source
if args.file:
sql_code = args.file.read_text()
output = gen_code_from_sql(sql_code, generate_relationships=args.relationships)

elif args.sqlite:
output = gen_code_from_sqlite(str(args.sqlite), generate_relationships=args.relationships)

elif args.postgres:
if not check_postgres_deps():
sys.exit('PostgreSQL support requires psycopg. Please install with: pip install sqlmodelgen[postgres]')
from sqlmodelgen import gen_code_from_postgres
output = gen_code_from_postgres(args.postgres, schema_name=args.schema, generate_relationships=args.relationships)

elif args.mysql:
if not check_mysql_deps():
sys.exit('MySQL support requires mysql-connector-python. Please install with: pip install sqlmodelgen[mysql]')
from mysql import connector
from sqlmodelgen import gen_code_from_mysql

try:
mysql_info = parse_mysql(args.mysql)
except Exception as e:
# TODO: possibly characterize the error a little better
_exit(str(e))

with connector.connect(
host=mysql_info.host,
port=mysql_info.port,
user=mysql_info.user,
password=mysql_info.psw
) as conn:
output = gen_code_from_mysql(
conn=conn,
dbname=args.dbname,
generate_relationships=args.relationships,
)
else:
# This should never happen due to required=True on the mutually exclusive group
_exit('No input source specified', usage)

# Output result
if args.output:
args.output.write_text(output)
else:
print(output)


def _exit(error: str, usage: str | None = None):
if usage:
print(usage)
sys.exit(error)
38 changes: 38 additions & 0 deletions src/sqlmodelgen/utils/mysql_parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'''
to parse mysql uris from cli
'''

from dataclasses import dataclass
from urllib3.util import parse_url

@dataclass
class MySQLURInfo:
user: str
psw: str | None
host: str
port: int

def parse_mysql(uri: str):
parsed = parse_url(uri)

if parsed.scheme != 'mysql':
raise ValueError('uri provided has not mysql scheme')

auth = parsed.auth
host = parsed.host
port = parsed.port

splauth = auth.split(':')
# TODO: evaluate a case of no auth schema? At lease check
# if that's conceived
if len(splauth) > 2:
raise ValueError('invalid auth scheme')
user = splauth[0]
psw = splauth[1] if len(splauth) == 2 else None

return MySQLURInfo(
user=user,
psw=psw,
host=host,
port=port,
)
23 changes: 23 additions & 0 deletions tests/helpers/cli_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path
from tempfile import tempdir

from sqlmodelgen.cli import main_cli

def launch_cli_in_tmpfile(args: list[str]) -> str:
'''
this helper ensures that the cli will be launched
having as target output -o a temporary file whose content
will be returned by this function

it is thus unnecessary to insert the -o args in input
to this function
'''
if '-o' in args:
raise RuntimeError('-o already present in args')

# TODO: use a random filename generator
tpath = Path(tempdir) / "f.py"

main_cli(args + ['-o', str(tpath)])

return tpath.read_text()
37 changes: 32 additions & 5 deletions tests/helpers/mysql_container.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import time
from contextlib import contextmanager
from dataclasses import dataclass

import docker
import mysql.connector


TEST_USER = 'root'
TEST_PSW = 'my-secret-pw'
TEST_HOST = 'localhost'
# TODO: maybe at a point this port exposed could randomically
# chosen to be a random one like in postgres
TEST_PORT = 3306


@dataclass
class MySqlConnAttrs:
user: str
psw: str
host: str
port: int

@property
def conn_str(self) -> str:
return f'mysql://{self.user}:{self.psw}@{self.host}:{self.port}'


def wait_until_conn(n_attempts: int = 100, delay: int = 1) -> mysql.connector.CMySQLConnection:
for _ in range(n_attempts):
try:
conn = mysql.connector.connect(host='localhost', port=3306, user='root', password='my-secret-pw')
conn = mysql.connector.connect(host=TEST_HOST, port=TEST_PORT, user=TEST_USER, password=TEST_PSW)
except mysql.connector.errors.OperationalError:
time.sleep(delay)
else:
Expand All @@ -22,16 +43,22 @@ def mysql_docker():
'mysql:9.5.0',
detach=True,
environment={
"MYSQL_ROOT_PASSWORD":"my-secret-pw"
'MYSQL_ROOT_PASSWORD':TEST_PSW
},
ports={f'3306/tcp': 3306},
hostname='127.0.0.1',
ports={'3306/tcp': TEST_PORT},
hostname=TEST_HOST,
remove=True,
)
try:
container.start()
conn = wait_until_conn()
yield (container, conn)
conn_data = MySqlConnAttrs(
user=TEST_USER,
psw=TEST_PSW,
host=TEST_HOST,
port=TEST_PORT,
)
yield (container, conn, conn_data)
finally:
container.stop()
conn.close()
Expand Down
Loading
Loading