abhijan 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.
abhijan-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: abhijan
3
+ Version: 0.1.0
4
+ Summary: Project bootstrapper for SQLAlchemy + Alembic
5
+ Author: abhijan
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # abhijan
10
+
11
+ Bootstrap a SQLAlchemy + Alembic project scaffold.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ pip install abhijan
17
+ abhijan init --async
18
+ # or
19
+ abhijan init --sync
20
+ ```
@@ -0,0 +1,12 @@
1
+ # abhijan
2
+
3
+ Bootstrap a SQLAlchemy + Alembic project scaffold.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ pip install abhijan
9
+ abhijan init --async
10
+ # or
11
+ abhijan init --sync
12
+ ```
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from .commands.init import init_project
7
+
8
+
9
+ def build_parser() -> argparse.ArgumentParser:
10
+ parser = argparse.ArgumentParser(prog="abhijan")
11
+ subparsers = parser.add_subparsers(dest="command", required=True)
12
+
13
+ init_parser = subparsers.add_parser("init", help="Initialize project scaffold")
14
+ mode_group = init_parser.add_mutually_exclusive_group(required=True)
15
+ mode_group.add_argument("--async", dest="is_async", action="store_true", help="Generate async SQLAlchemy setup")
16
+ mode_group.add_argument("--sync", dest="is_sync", action="store_true", help="Generate sync SQLAlchemy setup")
17
+ init_parser.add_argument("--path", type=Path, default=Path.cwd(), help="Target project directory")
18
+
19
+ return parser
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = build_parser()
24
+ args = parser.parse_args(argv)
25
+
26
+ if args.command == "init":
27
+ mode = "async" if args.is_async else "sync"
28
+ init_project(args.path, mode)
29
+ print(f"Initialized project at {args.path} ({mode})")
30
+
31
+ return 0
32
+
33
+
34
+ if __name__ == "__main__":
35
+ raise SystemExit(main())
File without changes
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import resources
4
+ from pathlib import Path
5
+
6
+
7
+ def _render_template(name: str) -> str:
8
+ return resources.files("abhijan.templates").joinpath(name).read_text(encoding="utf-8")
9
+
10
+
11
+ def _write_file(path: Path, content: str) -> None:
12
+ path.parent.mkdir(parents=True, exist_ok=True)
13
+ path.write_text(content, encoding="utf-8")
14
+
15
+
16
+ def init_project(project_root: Path, mode: str) -> None:
17
+ if mode not in {"sync", "async"}:
18
+ raise ValueError("mode must be 'sync' or 'async'")
19
+
20
+ app_dir = project_root / "app"
21
+ alembic_dir = project_root / "alembic"
22
+ versions_dir = alembic_dir / "versions"
23
+
24
+ app_dir.mkdir(parents=True, exist_ok=True)
25
+ versions_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ _write_file(app_dir / "__init__.py", "")
28
+ _write_file(app_dir / "settings.py", _render_template("settings.py"))
29
+ _write_file(app_dir / "database.py", _render_template(f"database_{mode}.py"))
30
+ _write_file(project_root / ".env", _render_template(f"default_{mode}.env"))
31
+
32
+ _write_file(project_root / "alembic.ini", _render_template("alembic.ini"))
33
+ _write_file(alembic_dir / "env.py", _render_template(f"alembic_{mode}_env.py"))
34
+
35
+ pyproject = project_root / "pyproject.toml"
36
+ if not pyproject.exists():
37
+ _write_file(pyproject, _render_template("project_pyproject.toml"))
@@ -0,0 +1,38 @@
1
+ [alembic]
2
+ script_location = alembic
3
+ prepend_sys_path = .
4
+
5
+ [loggers]
6
+ keys = root,sqlalchemy,alembic
7
+
8
+ [handlers]
9
+ keys = console
10
+
11
+ [formatters]
12
+ keys = generic
13
+
14
+ [logger_root]
15
+ level = WARN
16
+ handlers = console
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+
37
+ auto_generate = true
38
+ sqlalchemy.url = sqlite:///./app.db
@@ -0,0 +1,52 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import pool
5
+ from sqlalchemy.engine import Connection
6
+ from sqlalchemy.ext.asyncio import async_engine_from_config
7
+
8
+ config = context.config
9
+
10
+ if config.config_file_name is not None:
11
+ fileConfig(config.config_file_name)
12
+
13
+
14
+ def run_migrations_offline() -> None:
15
+ url = config.get_main_option("sqlalchemy.url")
16
+ context.configure(
17
+ url=url,
18
+ target_metadata=None,
19
+ literal_binds=True,
20
+ dialect_opts={"paramstyle": "named"},
21
+ )
22
+
23
+ with context.begin_transaction():
24
+ context.run_migrations()
25
+
26
+
27
+ def do_run_migrations(connection: Connection) -> None:
28
+ context.configure(connection=connection, target_metadata=None)
29
+
30
+ with context.begin_transaction():
31
+ context.run_migrations()
32
+
33
+
34
+ async def run_migrations_online() -> None:
35
+ connectable = async_engine_from_config(
36
+ config.get_section(config.config_ini_section, {}),
37
+ prefix="sqlalchemy.",
38
+ poolclass=pool.NullPool,
39
+ )
40
+
41
+ async with connectable.connect() as connection:
42
+ await connection.run_sync(do_run_migrations)
43
+
44
+ await connectable.dispose()
45
+
46
+
47
+ if context.is_offline_mode():
48
+ run_migrations_offline()
49
+ else:
50
+ import asyncio
51
+
52
+ asyncio.run(run_migrations_online())
@@ -0,0 +1,42 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+
6
+ config = context.config
7
+
8
+ if config.config_file_name is not None:
9
+ fileConfig(config.config_file_name)
10
+
11
+
12
+ def run_migrations_offline() -> None:
13
+ url = config.get_main_option("sqlalchemy.url")
14
+ context.configure(
15
+ url=url,
16
+ target_metadata=None,
17
+ literal_binds=True,
18
+ dialect_opts={"paramstyle": "named"},
19
+ )
20
+
21
+ with context.begin_transaction():
22
+ context.run_migrations()
23
+
24
+
25
+ def run_migrations_online() -> None:
26
+ connectable = engine_from_config(
27
+ config.get_section(config.config_ini_section, {}),
28
+ prefix="sqlalchemy.",
29
+ poolclass=pool.NullPool,
30
+ )
31
+
32
+ with connectable.connect() as connection:
33
+ context.configure(connection=connection, target_metadata=None)
34
+
35
+ with context.begin_transaction():
36
+ context.run_migrations()
37
+
38
+
39
+ if context.is_offline_mode():
40
+ run_migrations_offline()
41
+ else:
42
+ run_migrations_online()
@@ -0,0 +1,22 @@
1
+ from sqlalchemy.ext.asyncio import (
2
+ AsyncSession,
3
+ async_sessionmaker,
4
+ create_async_engine,
5
+ )
6
+
7
+ from .settings import settings
8
+
9
+
10
+ engine = create_async_engine(
11
+ settings.DATABASE_URL,
12
+ )
13
+
14
+ async_session_maker = async_sessionmaker(
15
+ engine,
16
+ expire_on_commit=False,
17
+ )
18
+
19
+
20
+ async def get_db():
21
+ async with async_session_maker() as session:
22
+ yield session
@@ -0,0 +1,13 @@
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker
3
+
4
+ from .settings import settings
5
+
6
+
7
+ engine = create_engine(settings.DATABASE_URL)
8
+
9
+ SessionLocal = sessionmaker(
10
+ bind=engine,
11
+ autoflush=False,
12
+ autocommit=False,
13
+ )
@@ -0,0 +1 @@
1
+ DATABASE_URL=sqlite+aiosqlite:///./app.db
@@ -0,0 +1 @@
1
+ DATABASE_URL=sqlite:///./app.db
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "my-project"
3
+ version = "0.1.0"
4
+ dependencies = [
5
+ "sqlalchemy>=2.0",
6
+ "alembic",
7
+ "pydantic-settings",
8
+ ]
@@ -0,0 +1,10 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ DATABASE_URL: str
6
+
7
+ model_config = SettingsConfigDict(env_file=".env")
8
+
9
+
10
+ settings = Settings()
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: abhijan
3
+ Version: 0.1.0
4
+ Summary: Project bootstrapper for SQLAlchemy + Alembic
5
+ Author: abhijan
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # abhijan
10
+
11
+ Bootstrap a SQLAlchemy + Alembic project scaffold.
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ pip install abhijan
17
+ abhijan init --async
18
+ # or
19
+ abhijan init --sync
20
+ ```
@@ -0,0 +1,21 @@
1
+ README.md
2
+ pyproject.toml
3
+ abhijan/__init__.py
4
+ abhijan/cli.py
5
+ abhijan.egg-info/PKG-INFO
6
+ abhijan.egg-info/SOURCES.txt
7
+ abhijan.egg-info/dependency_links.txt
8
+ abhijan.egg-info/entry_points.txt
9
+ abhijan.egg-info/top_level.txt
10
+ abhijan/commands/__init__.py
11
+ abhijan/commands/init.py
12
+ abhijan/templates/alembic.ini
13
+ abhijan/templates/alembic_async_env.py
14
+ abhijan/templates/alembic_sync_env.py
15
+ abhijan/templates/database_async.py
16
+ abhijan/templates/database_sync.py
17
+ abhijan/templates/default_async.env
18
+ abhijan/templates/default_sync.env
19
+ abhijan/templates/project_pyproject.toml
20
+ abhijan/templates/settings.py
21
+ tests/test_init.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ abhijan = abhijan.cli:main
@@ -0,0 +1 @@
1
+ abhijan
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "abhijan"
7
+ version = "0.1.0"
8
+ description = "Project bootstrapper for SQLAlchemy + Alembic"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "abhijan" }]
12
+ dependencies = []
13
+
14
+ [project.scripts]
15
+ abhijan = "abhijan.cli:main"
16
+
17
+ [tool.setuptools]
18
+ include-package-data = true
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["abhijan*"]
22
+
23
+ [tool.setuptools.package-data]
24
+ abhijan = ["templates/*.py", "templates/*.ini", "templates/*.toml", "templates/*.env"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from abhijan.cli import main
6
+ from abhijan.commands.init import init_project
7
+
8
+
9
+ def test_init_async_generates_expected_files(tmp_path: Path) -> None:
10
+ root = tmp_path / "project"
11
+ main(["init", "--async", "--path", str(root)])
12
+
13
+ assert (root / "app" / "settings.py").exists()
14
+ assert (root / "app" / "database.py").read_text(encoding="utf-8").find("create_async_engine") != -1
15
+ assert (root / "alembic" / "env.py").read_text(encoding="utf-8").find("async_engine_from_config") != -1
16
+ assert (root / ".env").read_text(encoding="utf-8").strip() == "DATABASE_URL=sqlite+aiosqlite:///./app.db"
17
+
18
+
19
+ def test_init_sync_generates_expected_files(tmp_path: Path) -> None:
20
+ root = tmp_path / "project"
21
+ main(["init", "--sync", "--path", str(root)])
22
+
23
+ assert (root / "app" / "database.py").read_text(encoding="utf-8").find("create_engine") != -1
24
+ assert (root / "alembic" / "env.py").read_text(encoding="utf-8").find("engine_from_config") != -1
25
+ assert (root / ".env").read_text(encoding="utf-8").strip() == "DATABASE_URL=sqlite:///./app.db"
26
+
27
+
28
+ def test_init_rejects_invalid_mode(tmp_path: Path) -> None:
29
+ with pytest.raises(ValueError):
30
+ init_project(tmp_path, "invalid")