19 changed files with 526 additions and 37 deletions
-
114backend-app/alembic.ini
-
1backend-app/alembic/README
-
90backend-app/alembic/env.py
-
26backend-app/alembic/script.py.mako
-
54backend-app/alembic/versions/2024_07_31_2249-12fb0121c6a3_create_tables_user_login.py
-
4backend-app/core/__init__.py
-
7backend-app/core/models/__init__.py
-
5backend-app/core/models/base.py
-
41backend-app/core/models/db_helper.py
-
18backend-app/core/settings.py
-
23backend-app/database.py
-
19backend-app/main.py
-
5backend-app/repo/__init__.py
-
12backend-app/repo/models.py
-
0backend-app/repo/schemas.py
-
0backend-app/utils/__init__.py
-
133poetry.lock
-
2pyproject.toml
-
9readme.md
@ -0,0 +1,114 @@ |
|||
# A generic, single database configuration. |
|||
|
|||
[alembic] |
|||
# path to migration scripts. |
|||
# Use forward slashes (/) also on windows to provide an os agnostic path |
|||
script_location = alembic |
|||
|
|||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s |
|||
# Uncomment the line below if you want the files to be prepended with date and time |
|||
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s |
|||
|
|||
# sys.path path, will be prepended to sys.path if present. |
|||
# defaults to the current working directory. |
|||
prepend_sys_path = . |
|||
|
|||
# timezone to use when rendering the date within the migration file |
|||
# as well as the filename. |
|||
# If specified, requires the python>=3.9 or backports.zoneinfo library. |
|||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements |
|||
# string value is passed to ZoneInfo() |
|||
# leave blank for localtime |
|||
# timezone = |
|||
|
|||
# max length of characters to apply to the "slug" field |
|||
# truncate_slug_length = 40 |
|||
|
|||
# set to 'true' to run the environment during |
|||
# the 'revision' command, regardless of autogenerate |
|||
# revision_environment = false |
|||
|
|||
# set to 'true' to allow .pyc and .pyo files without |
|||
# a source .py file to be detected as revisions in the |
|||
# versions/ directory |
|||
# sourceless = false |
|||
|
|||
# version location specification; This defaults |
|||
# to alembic/versions. When using multiple version |
|||
# directories, initial revisions must be specified with --version-path. |
|||
# The path separator used here should be the separator specified by "version_path_separator" below. |
|||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions |
|||
|
|||
# version path separator; As mentioned above, this is the character used to split |
|||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. |
|||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. |
|||
# Valid values for version_path_separator are: |
|||
# |
|||
# version_path_separator = : |
|||
# version_path_separator = ; |
|||
# version_path_separator = space |
|||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects. |
|||
|
|||
# set to 'true' to search source files recursively |
|||
# in each "version_locations" directory |
|||
# new in Alembic version 1.10 |
|||
# recursive_version_locations = false |
|||
|
|||
# the output encoding used when revision files |
|||
# are written from script.py.mako |
|||
# output_encoding = utf-8 |
|||
|
|||
sqlalchemy.url = driver://user:pass@localhost/dbname |
|||
|
|||
|
|||
[post_write_hooks] |
|||
# post_write_hooks defines scripts or Python functions that are run |
|||
# on newly generated revision scripts. See the documentation for further |
|||
# detail and examples |
|||
|
|||
# format using "black" - use the console_scripts runner, against the "black" entrypoint |
|||
hooks = black |
|||
black.type = console_scripts |
|||
black.entrypoint = black |
|||
black.options = -l 79 REVISION_SCRIPT_FILENAME |
|||
|
|||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary |
|||
# hooks = ruff |
|||
# ruff.type = exec |
|||
# ruff.executable = %(here)s/.venv/bin/ruff |
|||
# ruff.options = --fix REVISION_SCRIPT_FILENAME |
|||
|
|||
# Logging configuration |
|||
[loggers] |
|||
keys = root,sqlalchemy,alembic |
|||
|
|||
[handlers] |
|||
keys = console |
|||
|
|||
[formatters] |
|||
keys = generic |
|||
|
|||
[logger_root] |
|||
level = WARN |
|||
handlers = console |
|||
qualname = |
|||
|
|||
[logger_sqlalchemy] |
|||
level = WARN |
|||
handlers = |
|||
qualname = sqlalchemy.engine |
|||
|
|||
[logger_alembic] |
|||
level = INFO |
|||
handlers = |
|||
qualname = alembic |
|||
|
|||
[handler_console] |
|||
class = StreamHandler |
|||
args = (sys.stderr,) |
|||
level = NOTSET |
|||
formatter = generic |
|||
|
|||
[formatter_generic] |
|||
format = %(levelname)-5.5s [%(name)s] %(message)s |
|||
datefmt = %H:%M:%S |
@ -0,0 +1 @@ |
|||
Generic single-database configuration with an async dbapi. |
@ -0,0 +1,90 @@ |
|||
import asyncio |
|||
from logging.config import fileConfig |
|||
|
|||
from sqlalchemy import pool |
|||
from sqlalchemy.engine import Connection |
|||
from sqlalchemy.ext.asyncio import async_engine_from_config |
|||
from core.settings import settings |
|||
from core.models.base import Base |
|||
from alembic import context |
|||
|
|||
# this is the Alembic Config object, which provides |
|||
# access to the values within the .ini file in use. |
|||
config = context.config |
|||
|
|||
# Interpret the config file for Python logging. |
|||
# This line sets up loggers basically. |
|||
if config.config_file_name is not None: |
|||
fileConfig(config.config_file_name) |
|||
|
|||
# add your model's MetaData object here |
|||
# for 'autogenerate' support |
|||
# from myapp import mymodel |
|||
# target_metadata = mymodel.Base.metadata |
|||
target_metadata = Base.metadata |
|||
config.set_main_option("sqlalchemy.url", settings.db.url) |
|||
# other values from the config, defined by the needs of env.py, |
|||
# can be acquired: |
|||
# my_important_option = config.get_main_option("my_important_option") |
|||
# ... etc. |
|||
|
|||
|
|||
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 do_run_migrations(connection: Connection) -> None: |
|||
context.configure(connection=connection, target_metadata=target_metadata) |
|||
|
|||
with context.begin_transaction(): |
|||
context.run_migrations() |
|||
|
|||
|
|||
async def run_async_migrations() -> None: |
|||
"""In this scenario we need to create an Engine |
|||
and associate a connection with the context. |
|||
|
|||
""" |
|||
|
|||
connectable = async_engine_from_config( |
|||
config.get_section(config.config_ini_section, {}), |
|||
prefix="sqlalchemy.", |
|||
poolclass=pool.NullPool, |
|||
) |
|||
|
|||
async with connectable.connect() as connection: |
|||
await connection.run_sync(do_run_migrations) |
|||
|
|||
await connectable.dispose() |
|||
|
|||
|
|||
def run_migrations_online() -> None: |
|||
"""Run migrations in 'online' mode.""" |
|||
|
|||
asyncio.run(run_async_migrations()) |
|||
|
|||
|
|||
if context.is_offline_mode(): |
|||
run_migrations_offline() |
|||
else: |
|||
run_migrations_online() |
@ -0,0 +1,26 @@ |
|||
"""${message} |
|||
|
|||
Revision ID: ${up_revision} |
|||
Revises: ${down_revision | comma,n} |
|||
Create Date: ${create_date} |
|||
|
|||
""" |
|||
from typing import Sequence, Union |
|||
|
|||
from alembic import op |
|||
import sqlalchemy as sa |
|||
${imports if imports else ""} |
|||
|
|||
# revision identifiers, used by Alembic. |
|||
revision: str = ${repr(up_revision)} |
|||
down_revision: Union[str, None] = ${repr(down_revision)} |
|||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} |
|||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} |
|||
|
|||
|
|||
def upgrade() -> None: |
|||
${upgrades if upgrades else "pass"} |
|||
|
|||
|
|||
def downgrade() -> None: |
|||
${downgrades if downgrades else "pass"} |
@ -0,0 +1,54 @@ |
|||
"""create tables user, login |
|||
|
|||
Revision ID: 12fb0121c6a3 |
|||
Revises: |
|||
Create Date: 2024-07-31 22:49:23.147480 |
|||
|
|||
""" |
|||
|
|||
from typing import Sequence, Union |
|||
|
|||
from alembic import op |
|||
import sqlalchemy as sa |
|||
|
|||
|
|||
# revision identifiers, used by Alembic. |
|||
revision: str = "12fb0121c6a3" |
|||
down_revision: Union[str, None] = None |
|||
branch_labels: Union[str, Sequence[str], None] = None |
|||
depends_on: Union[str, Sequence[str], None] = None |
|||
|
|||
|
|||
def upgrade() -> None: |
|||
# ### commands auto generated by Alembic - please adjust! ### |
|||
op.create_table( |
|||
"logins", |
|||
sa.Column("id", sa.Integer(), nullable=False), |
|||
sa.Column("username", sa.String(length=255), nullable=False), |
|||
sa.Column("password", sa.String(length=255), nullable=False), |
|||
sa.PrimaryKeyConstraint("id"), |
|||
sa.UniqueConstraint("username"), |
|||
) |
|||
op.create_table( |
|||
"users", |
|||
sa.Column("id", sa.Integer(), nullable=False), |
|||
sa.Column("firstname", sa.String(length=255), nullable=True), |
|||
sa.Column("lastname", sa.String(length=255), nullable=True), |
|||
sa.Column("age", sa.Integer(), nullable=True), |
|||
sa.Column("email", sa.String(length=255), nullable=True), |
|||
sa.Column("login_id", sa.Integer(), nullable=False), |
|||
sa.ForeignKeyConstraint( |
|||
["login_id"], |
|||
["logins.id"], |
|||
), |
|||
sa.PrimaryKeyConstraint("id"), |
|||
sa.UniqueConstraint("email"), |
|||
) |
|||
# ### end Alembic commands ### |
|||
|
|||
|
|||
def downgrade() -> None: |
|||
# ### commands auto generated by Alembic - please adjust! ### |
|||
op.drop_table("users") |
|||
op.drop_table("logins") |
|||
# ### end Alembic commands ### |
@ -0,0 +1,4 @@ |
|||
__all__ = ( |
|||
"settings" |
|||
) |
|||
from .settings import settings |
@ -0,0 +1,7 @@ |
|||
__all__ = ( |
|||
"db_helper", |
|||
"Base" |
|||
) |
|||
from .db_helper import db_helper |
|||
from .base import Base |
|||
from repo.models import LoginModel, UserModel |
@ -0,0 +1,5 @@ |
|||
from sqlalchemy.orm import DeclarativeBase |
|||
|
|||
class Base(DeclarativeBase): |
|||
pass |
|||
|
@ -0,0 +1,41 @@ |
|||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, async_sessionmaker, AsyncSession |
|||
from typing import AsyncGenerator |
|||
from core.settings import settings |
|||
class DatabaseHelper: |
|||
def __init__( |
|||
self, |
|||
url: str, |
|||
echo:bool=False, |
|||
echo_pool:bool=False, |
|||
pool_size: int = 5, |
|||
max_overflow: int =10, |
|||
) -> None: |
|||
self.engine: AsyncEngine = create_async_engine( |
|||
url=url, |
|||
echo=echo, |
|||
echo_pool=echo_pool, |
|||
pool_size=pool_size, |
|||
max_overflow = max_overflow |
|||
) |
|||
self.session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker( |
|||
bind= self.engine, |
|||
autoflush=False, |
|||
autocommit=False, |
|||
expire_on_commit=False |
|||
) |
|||
|
|||
async def dispose(self) -> None: |
|||
await self.engine.dispose() |
|||
|
|||
async def session_getter(self) -> AsyncGenerator[AsyncSession,None]: |
|||
async with self.session_factory() as session: |
|||
yield session |
|||
|
|||
db_helper = DatabaseHelper( |
|||
url = settings.db.url, |
|||
echo = settings.db.echo, |
|||
echo_pool = settings.db.echo_pool, |
|||
pool_size = settings.db.pool_size, |
|||
max_overflow = settings.db.max_overflow, |
|||
|
|||
) |
@ -1,11 +1,25 @@ |
|||
from pydantic_settings import BaseSettings |
|||
from pydantic_settings import BaseSettings, SettingsConfigDict |
|||
from pydantic import BaseModel |
|||
|
|||
class RunSettings(BaseModel): |
|||
host: str = "0.0.0.0" |
|||
port: int = 8000 |
|||
|
|||
class DatabaseConfig(BaseModel): |
|||
url: str |
|||
echo:bool=False |
|||
echo_pool:bool=False |
|||
pool_size: int = 50 |
|||
max_overflow: int =10 |
|||
|
|||
class Settings(BaseSettings): |
|||
model_config = SettingsConfigDict( |
|||
env_file=".env", |
|||
case_sensitive=False, |
|||
env_nested_delimiter="__", |
|||
env_prefix="APP_CONFIG__" |
|||
) |
|||
run: RunSettings = RunSettings() |
|||
db: DatabaseConfig |
|||
|
|||
settings = Settings() |
|||
settings = Settings() |
@ -1,23 +0,0 @@ |
|||
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine |
|||
from sqlalchemy.orm import DeclarativeBase |
|||
|
|||
class Model(DeclarativeBase): |
|||
pass |
|||
|
|||
|
|||
async_engine=create_async_engine( |
|||
url = "mssql+aioodbc://sa:159357"\ |
|||
"@10.124.30.208:1433/test_db"\ |
|||
"?driver=ODBC+Driver+17+for+SQL+Server", |
|||
connect_args={"check_same_thread": False} |
|||
) |
|||
|
|||
async_session = async_sessionmaker( |
|||
async_engine, |
|||
autoflush=True, |
|||
autocommit=False, |
|||
expire_on_commit =False |
|||
) |
|||
# async def get_async_session() -> AsyncSession: |
|||
# async with async_session() as session: |
|||
# session. |
@ -0,0 +1,5 @@ |
|||
__all__ = ( |
|||
"UserModel", |
|||
"LoginModel" |
|||
) |
|||
from .models import UserModel, LoginModel |
@ -1,21 +1,21 @@ |
|||
from database import Model |
|||
from core.models import Base |
|||
from sqlalchemy.orm import Mapped, mapped_column |
|||
from sqlalchemy import String, Integer, Boolean, ForeignKey |
|||
from typing import Optional, Sequence |
|||
|
|||
class UserModel(Model): |
|||
class UserModel(Base): |
|||
__tablename__ ="users" |
|||
|
|||
id: Mapped[int] = mapped_column(Integer, primary_key=True) |
|||
firstname: Mapped[str | None]= mapped_column(String(255), nullable=True) |
|||
lastname: Mapped[str | None]= mapped_column(String(255), nullable=True) |
|||
age: Mapped[int | None] = mapped_column(Integer, nullable=True) |
|||
email: Mapped[str | None] = mapped_column(String(255), nullable=True) |
|||
email: Mapped[str | None] = mapped_column(String(255), nullable=True, unique=True) |
|||
login_id: Mapped[int] = mapped_column(Integer, ForeignKey("logins.id")) |
|||
|
|||
class LoginModel(Model): |
|||
class LoginModel(Base): |
|||
__tablename__ ="logins" |
|||
|
|||
id: Mapped[int] = mapped_column(Integer, primary_key=True) |
|||
login: Mapped[str]= mapped_column(String(255), nullable=False) |
|||
password: Mapped[str]= mapped_column(String(255), nullable=False) |
|||
username: Mapped[str]= mapped_column(String(255), nullable=False, unique=True) |
|||
password: Mapped[str]= mapped_column(String(255), nullable=False) |
Reference in new issue
xxxxxxxxxx