Compare commits

..

21 Commits
prod ... dev

Author SHA1 Message Date
s.mostryukov e8e9e52021 loc 2024-08-06 10:05:03 +03:00
sergey f866f66a77 update gitignore 2024-08-03 20:14:31 +03:00
sergey 57407ab87b дальше колупаю htmx 2024-07-14 21:37:57 +03:00
sergey a4268180cd дальше колупаю htmx 2024-07-14 21:37:00 +03:00
sergey a4c7c00254 пробую переползти на fastui 2024-07-13 20:56:34 +03:00
s.mostryukov 8cf56bdc89 add .keep to alembic 2024-07-12 14:55:48 +03:00
s.mostryukov 3e099c9263 final ORJSONResponse 2024-07-12 14:49:52 +03:00
s.mostryukov 3b175a1f83 final 2024-07-12 14:44:02 +03:00
sergey fab67f6ea9 https://www.youtube.com/watch?v=XWJWJfTWjSs&list=PLYnH8mpFQ4akzzS1D9IHkMuXacb-bD4Cl&index=9 2024-07-11 22:25:42 +03:00
sergey a32abcf472 use alembic 2024-07-11 22:13:33 +03:00
sergey afdc8e30ca use alembic 2024-07-11 21:56:42 +03:00
sergey 8a567298bc set alembic 2024-07-11 21:52:45 +03:00
sergey dc0b008e14 set alembic 2024-07-11 21:49:14 +03:00
sergey 62541a0795 set alembic 2024-07-11 21:48:42 +03:00
sergey 249ce34717 alembic 2024-07-11 21:44:03 +03:00
sergey 6cf72d26b5 добавлен конвертер + базовый класс 2024-07-11 21:26:30 +03:00
sergey 88f6524ef6 работает 2024-07-11 21:01:46 +03:00
s.mostryukov ca88bec514 set sqlal 2024-07-11 19:01:26 +03:00
s.mostryukov f0f7e369fb git ignore change 2024-07-11 17:24:30 +03:00
s.mostryukov 22c45c01e6 start project 2024-07-11 17:23:10 +03:00
s.mostryukov 1a0311a827 add poetry 2024-07-11 17:16:52 +03:00
58 changed files with 7938 additions and 4 deletions

9
.gitignore vendored
View File

@ -1,4 +1,9 @@
.vscode
!alembic
__pycache__
*.pyc
.env
.venv
.vscode/
.venv/
.log
.idea/
*.idea

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
services:
pg:
image: postgres
environment:
POSTGRES_DB: sipi
POSTGRES_USER: sys.sipidb
POSTGRES_PASSWORD: password
ports:
- "5432:5432"

View File

@ -1 +0,0 @@
print('asdf')

1524
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
pyproject.toml Normal file
View File

@ -0,0 +1,26 @@
[tool.poetry]
name = "sipi-web"
version = "0.1.0"
description = ""
authors = ["sergey <sergey@sm8255082.ru>"]
license = "GPL3"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.111.0"
uvicorn = {extras = ["stsndart"], version = "^0.30.1"}
pydantic = {extras = ["email"], version = "^2.8.2"}
pydantic-settings = "^2.3.4"
sqlalchemy = {extras = ["asyncio"], version = "^2.0.31"}
aiomysql = "^0.2.0"
asyncpg = "^0.29.0"
alembic = "^1.13.2"
[tool.poetry.group.dev.dependencies]
pytest = "^8.2.2"
black = "^24.4.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

2
sipi-app/.env-template Normal file
View File

@ -0,0 +1,2 @@
SIPI_CONFIG__DB__URL=postgresql+asyncpg://username:password@localhost:5432/dbname
SIPI_CONFIG__DB__ECHO=0

114
sipi-app/alembic.ini Normal file
View File

@ -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

1
sipi-app/alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.

93
sipi-app/alembic/env.py Normal file
View File

@ -0,0 +1,93 @@
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 alembic import context
from settings import settings
from models import Base
# 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
# 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.
config.set_main_option("sqlalchemy.url", str(settings.db.url))
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()

View File

@ -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"}

View File

@ -0,0 +1,34 @@
"""create users table
Revision ID: 799cc8915cb1
Revises:
Create Date: 2024-07-11 22:08:25.362239
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "799cc8915cb1"
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:
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_users")),
sa.UniqueConstraint("username", name=op.f("uq_users_username")),
)
def downgrade() -> None:
op.drop_table("users")

View File

@ -0,0 +1,37 @@
"""update users table
Revision ID: 443e39c236a6
Revises: 799cc8915cb1
Create Date: 2024-07-11 22:11:47.011912
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "443e39c236a6"
down_revision: Union[str, None] = "799cc8915cb1"
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.add_column("users", sa.Column("foo", sa.Integer(), nullable=False))
op.add_column("users", sa.Column("bar", sa.Integer(), nullable=False))
op.create_unique_constraint(
op.f("uq_users_foo_bar"), "users", ["foo", "bar"]
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f("uq_users_foo_bar"), "users", type_="unique")
op.drop_column("users", "bar")
op.drop_column("users", "foo")
# ### end Alembic commands ###

View File

@ -0,0 +1,41 @@
"""create user table
Revision ID: b068e8be0d4b
Revises: 443e39c236a6
Create Date: 2024-07-13 13:27:25.512678
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "b068e8be0d4b"
down_revision: Union[str, None] = "443e39c236a6"
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.drop_constraint("uq_users_foo_bar", "users", type_="unique")
op.drop_column("users", "foo")
op.drop_column("users", "bar")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"users",
sa.Column("bar", sa.INTEGER(), autoincrement=False, nullable=False),
)
op.add_column(
"users",
sa.Column("foo", sa.INTEGER(), autoincrement=False, nullable=False),
)
op.create_unique_constraint("uq_users_foo_bar", "users", ["foo", "bar"])
# ### end Alembic commands ###

View File

@ -0,0 +1,42 @@
"""create isp_connection table
Revision ID: 1855b4dcf566
Revises: b068e8be0d4b
Create Date: 2024-07-13 15:27:44.137123
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "1855b4dcf566"
down_revision: Union[str, None] = "b068e8be0d4b"
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(
"isps",
sa.Column("name", sa.String(), nullable=False),
sa.Column("manager_name", sa.String(), nullable=True),
sa.Column("manager_phone", sa.String(), nullable=True),
sa.Column("manager_email", sa.String(), nullable=True),
sa.Column("tech_support_phone", sa.String(), nullable=True),
sa.Column("tesh_support_email", sa.String(), nullable=True),
sa.Column("comment", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_isps")),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("isps")
# ### end Alembic commands ###

View File

@ -0,0 +1,51 @@
"""create isp_connection table
Revision ID: 85fef76b3dcd
Revises: 1855b4dcf566
Create Date: 2024-07-13 15:30:30.938434
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "85fef76b3dcd"
down_revision: Union[str, None] = "1855b4dcf566"
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(
"isp_connections",
sa.Column("isp_name", sa.String(), nullable=False),
sa.Column("location_code", sa.String(), nullable=False),
sa.Column("contract_num", sa.String(), nullable=True),
sa.Column("contract_date", sa.String(), nullable=True),
sa.Column("contract_company", sa.String(), nullable=True),
sa.Column("cost", sa.Integer(), nullable=True),
sa.Column("speed", sa.Integer(), nullable=True),
sa.Column("connection_type", sa.String(), nullable=True),
sa.Column("network", sa.String(), nullable=True),
sa.Column("address_type", sa.String(), nullable=True),
sa.Column("isp_id", sa.Integer(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["isp_id"],
["isps.id"],
name=op.f("fk_isp_connections_isp_id_isps"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_isp_connections")),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("isp_connections")
# ### end Alembic commands ###

View File

@ -0,0 +1,36 @@
"""create isp_connection table
Revision ID: f073180e963c
Revises: 85fef76b3dcd
Create Date: 2024-07-13 16:15:44.004704
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "f073180e963c"
down_revision: Union[str, None] = "85fef76b3dcd"
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.drop_column("isp_connections", "isp_name")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"isp_connections",
sa.Column(
"isp_name", sa.VARCHAR(), autoincrement=False, nullable=False
),
)
# ### end Alembic commands ###

23
sipi-app/api/__init__.py Normal file
View File

@ -0,0 +1,23 @@
from fastapi import APIRouter
from settings import settings
from .users import router as user_router
from .isp import router as isp_router
from .isp_connection import router as isp_connection_router
router = APIRouter()
router.include_router(
user_router,
prefix=settings.api.users,
)
router.include_router(
isp_router,
prefix=settings.api.isp,
)
router.include_router(
isp_connection_router,
prefix=settings.api.isp_connections,
)

28
sipi-app/api/isp.py Normal file
View File

@ -0,0 +1,28 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from models import db_helper
from schemas.isp import IspRead, IspCreate
from crud import isp as isp_crud
router = APIRouter(
tags=["Isp"],
)
@router.get("", response_model=list[IspRead])
async def get_isp(session: Annotated[AsyncSession, Depends(db_helper.session_getter)]):
users = await isp_crud.get_all_isp(session=session)
return users
@router.post("", response_model=IspRead)
async def create_isp(
session: Annotated[AsyncSession, Depends(db_helper.session_getter)],
isp_scheme: IspCreate,
):
isp = await isp_crud.create_isp(session=session, isp_scheme=isp_scheme)
return isp

View File

@ -0,0 +1,32 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from models import db_helper
from schemas.isp_connections import IspConnectionRead, IspConnectionCreate
from crud import isp_connection as isp_connections_crud
router = APIRouter(
tags=["IspConnections"],
)
@router.get("", response_model=list[IspConnectionRead])
async def get_isp_connection(
session: Annotated[AsyncSession, Depends(db_helper.session_getter)]
):
users = await isp_connections_crud.get_all_isp_connection(session=session)
return users
@router.post("", response_model=IspConnectionRead)
async def create_isp_connection(
session: Annotated[AsyncSession, Depends(db_helper.session_getter)],
isp_connection_create: IspConnectionCreate,
):
isp_connection = await isp_connections_crud.create_isp_connection(
session=session, isp_connection_scheme=isp_connection_create
)
return isp_connection

31
sipi-app/api/users.py Normal file
View File

@ -0,0 +1,31 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from models import db_helper
from schemas.user import UserRead, UserCreate
from crud import user as user_crud
router = APIRouter(
tags=["User"],
)
@router.get("", response_model=list[UserRead])
async def get_users(
session: Annotated[AsyncSession, Depends(db_helper.session_getter)]
):
users = await user_crud.get_all_users(session=session)
return users
@router.post("", response_model=UserRead)
async def create_user(
session: Annotated[AsyncSession, Depends(db_helper.session_getter)],
user_create: UserCreate,
):
user = await user_crud.create_user(session=session, user_create=user_create)
return user

View File

25
sipi-app/crud/isp.py Normal file
View File

@ -0,0 +1,25 @@
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload
from models import Isp
from schemas.isp import IspCreate, IspRead
async def get_all_isp(
session: AsyncSession,
) -> Sequence[Isp]:
stmt = select(Isp).options(selectinload(Isp.isp_connections)).order_by(Isp.id)
result = await session.scalars(stmt)
return result.all()
async def create_isp(session: AsyncSession, isp_scheme: IspCreate) -> Isp:
isp = Isp(**isp_scheme.model_dump())
session.add(isp)
await session.commit()
return isp

View File

@ -0,0 +1,23 @@
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models import IspConnection
from schemas.isp_connections import IspConnectionCreate
async def get_all_isp_connection(session: AsyncSession) -> Sequence[IspConnection]:
stmt = select(IspConnection).order_by(IspConnection.id)
result = await session.scalars(stmt)
return result.all()
async def create_isp_connection(
session: AsyncSession, isp_connection_scheme: IspConnectionCreate
) -> IspConnection:
isp_connection = IspConnection(**isp_connection_scheme.model_dump())
session.add(isp_connection)
await session.commit()
return isp_connection

20
sipi-app/crud/user.py Normal file
View File

@ -0,0 +1,20 @@
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from models import User
from schemas.user import UserCreate
async def get_all_users(session: AsyncSession) -> Sequence[User]:
stmt = select(User).order_by(User.id)
result = await session.scalars(stmt)
return result.all()
async def create_user(session: AsyncSession, user_create: UserCreate) -> User:
user = User(**user_create.model_dump())
session.add(user)
await session.commit()
return user

51
sipi-app/main.py Normal file
View File

@ -0,0 +1,51 @@
from contextlib import asynccontextmanager
from fastapi.openapi.docs import get_swagger_ui_html
from starlette.staticfiles import StaticFiles
from settings import settings, BASE_DIR
from models import db_helper
from api import router as api_router
from views import router as web_router
import uvicorn
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
await db_helper.dispose()
main_app = FastAPI(
default_response_class=ORJSONResponse,
lifespan=lifespan,
docs_url=None,
)
main_app.include_router(api_router)
main_app.include_router(web_router)
main_app.mount("/static", StaticFiles(directory=BASE_DIR / "views" / "static"), name="static")
@main_app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=main_app.openapi_url,
title="My API",
oauth2_redirect_url=main_app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger/swagger-ui-bundle.js",
swagger_css_url="/static/swagger/swagger-ui.css",
swagger_favicon_url="/static/swagger/favicon.png",
)
if __name__ == "__main__":
uvicorn.run(
"main:main_app",
host=settings.run.host,
port=settings.run.port,
reload=settings.run.reload,
)

View File

@ -0,0 +1,13 @@
__all__ = (
"db_helper",
"Base",
"User",
"Isp",
"IspConnection",
)
from .db_helper import db_helper
from .base import Base
from .user import User
from .isp import Isp
from .isp_connection import IspConnection

19
sipi-app/models/base.py Normal file
View File

@ -0,0 +1,19 @@
from sqlalchemy import MetaData
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
from settings import settings
from utils import camel_case_to_snake_case
class Base(DeclarativeBase):
__abstract__ = True
metadata = MetaData(
naming_convention=settings.db.naming_convention,
)
@declared_attr.directive
def __tablename__(cls) -> str:
return f"{camel_case_to_snake_case(cls.__name__)}s"
id: Mapped[int] = mapped_column(primary_key=True)

View File

@ -0,0 +1,48 @@
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession
from typing import AsyncGenerator
from settings import settings
class DatabaseHelper:
def __init__(
self,
url: str,
echo: bool = False,
echo_pool: bool = False,
max_overflow: int = 10,
pool_size: int = 5,
):
self.engine: AsyncEngine = create_async_engine(
url=url,
echo=echo,
echo_pool=echo_pool,
max_overflow=max_overflow,
pool_size=pool_size,
)
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=str(settings.db.url),
echo=settings.db.echo,
echo_pool=settings.db.echo_pool,
max_overflow=settings.db.max_overflow,
pool_size=settings.db.pool_size,
)

21
sipi-app/models/isp.py Normal file
View File

@ -0,0 +1,21 @@
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from .base import Base
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from isp_connection import IspConnection
class Isp(Base):
name: Mapped[str]
manager_name: Mapped[str] = mapped_column(nullable=True)
manager_phone: Mapped[str] = mapped_column(nullable=True)
manager_email: Mapped[str] = mapped_column(nullable=True)
tech_support_phone: Mapped[str] = mapped_column(nullable=True)
tesh_support_email: Mapped[str] = mapped_column(nullable=True)
comment: Mapped[str] = mapped_column(nullable=True)
isp_connections: Mapped[list["IspConnection"]] = relationship(back_populates="isp")

View File

@ -0,0 +1,23 @@
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy import ForeignKey
from .base import Base
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .isp import Isp
class IspConnection(Base):
location_code: Mapped[str]
contract_num: Mapped[str] = mapped_column(nullable=True)
contract_date: Mapped[str] = mapped_column(nullable=True)
contract_company: Mapped[str] = mapped_column(nullable=True)
cost: Mapped[int] = mapped_column(nullable=True)
speed: Mapped[int] = mapped_column(nullable=True)
connection_type: Mapped[str] = mapped_column(nullable=True)
network: Mapped[str] = mapped_column(nullable=True)
address_type: Mapped[str] = mapped_column(nullable=True)
isp_id: Mapped[int] = mapped_column(ForeignKey("isps.id"))
isp: Mapped["Isp"] = relationship(back_populates="isp_connections")

7
sipi-app/models/user.py Normal file
View File

@ -0,0 +1,7 @@
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from .base import Base
class User(Base):
username: Mapped[str] = mapped_column(unique=True)

View File

33
sipi-app/schemas/isp.py Normal file
View File

@ -0,0 +1,33 @@
from typing import TYPE_CHECKING
from pydantic import BaseModel
from .isp_connections import IspConnectionBase
class IspBase(BaseModel):
name: str
manager_name: str
manager_phone: str
manager_email: str
tech_support_phone: str
tesh_support_email: str
comment: str
isp_connections: list["IspConnectionBase"] | None
class IspCreate(IspBase):
pass
class IspRead(IspBase):
id: int
class IspRemove(IspBase):
pass
class IspChange(IspBase):
pass

View File

@ -0,0 +1,29 @@
from pydantic import BaseModel
class IspConnectionBase(BaseModel):
location_code: str
contract_num: str
contract_date: str
contract_company: str
cost: int
speed: int
connection_type: str
network: str
address_type: str
class IspConnectionCreate(IspConnectionBase):
isp_id: int
class IspConnectionRead(IspConnectionBase):
id: int
class IspConnectionRemove(IspConnectionBase):
pass
class IspConnectionChange(IspConnectionBase):
pass

21
sipi-app/schemas/user.py Normal file
View File

@ -0,0 +1,21 @@
from pydantic import BaseModel
class UserBase(BaseModel):
username: str
class UserCreate(UserBase):
pass
class UserRead(UserBase):
id: int
class UserRemove(UserBase):
pass
class UserChange(UserBase):
pass

63
sipi-app/settings.py Normal file
View File

@ -0,0 +1,63 @@
from pydantic import BaseModel
from pydantic import PostgresDsn
# from pydantic import MariaDBDsn
# from pydantic import MySQLDsn
from pydantic_settings import (
BaseSettings,
SettingsConfigDict,
)
from pathlib import Path
BASE_DIR = Path(__file__).parent
class RunConfig(BaseModel):
host: str = "0.0.0.0"
port: int = 8000
reload: bool = True
class ApiPrefix(BaseModel):
users: str = "/api/users"
isp: str = "/api/isp"
isp_connections: str = "/api/isp_connections"
class WebPrefix(BaseModel):
isp: str = "/web/isp"
settings: str = "/web/settings"
isp_connections: str = "/web/isp_connections"
start: str = "/web"
class DatabaseConfig(BaseModel):
url: PostgresDsn
echo: bool = False
echo_pool: bool = False
pool_size: int = 50
max_overflow: int = 10
naming_convention: dict[str, str] = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_N_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=(BASE_DIR / ".env-template", BASE_DIR / ".env"),
case_sensitive=False,
env_nested_delimiter="__",
env_prefix="SIPI_CONFIG__",
)
run: RunConfig = RunConfig()
api: ApiPrefix = ApiPrefix()
web: WebPrefix = WebPrefix()
db: DatabaseConfig
settings = Settings()

View File

@ -0,0 +1,5 @@
__all__ = (
'camel_case_to_snake_case',
)
from .case_converter import camel_case_to_snake_case

View File

@ -0,0 +1,29 @@
"""
Taken from
https://github.com/mahenzon/ri-sdk-python-wrapper/blob/master/ri_sdk_codegen/utils/case_converter.py
"""
def camel_case_to_snake_case(input_str: str) -> str:
"""
>>> camel_case_to_snake_case("SomeSDK")
'some_sdk'
>>> camel_case_to_snake_case("RServoDrive")
'r_servo_drive'
>>> camel_case_to_snake_case("SDKDemo")
'sdk_demo'
"""
chars = []
for c_idx, char in enumerate(input_str):
if c_idx and char.isupper():
nxt_idx = c_idx + 1
# idea of the flag is to separate abbreviations
# as new words, show them in lower case
flag = nxt_idx >= len(input_str) or input_str[nxt_idx].isupper()
prev_char = input_str[c_idx - 1]
if prev_char.isupper() and flag:
pass
else:
chars.append("_")
chars.append(char.lower())
return "".join(chars)

View File

@ -0,0 +1,22 @@
from fastapi import APIRouter
from settings import settings
from .web import router as web_router
from .isp import router as isp_router
from .setting import router as settings_router
router = APIRouter()
router.include_router(
web_router,
prefix=settings.web.start,
)
router.include_router(
isp_router,
prefix=settings.web.isp,
)
router.include_router(
settings_router,
prefix=settings.web.settings,
)

39
sipi-app/views/isp.py Normal file
View File

@ -0,0 +1,39 @@
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.templating import Jinja2Templates
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from models import db_helper
from crud import isp as isp_crud
from fastapi.encoders import jsonable_encoder
router = APIRouter(
tags=["Web"],
)
templates = Jinja2Templates(directory="views/templates")
@router.get("/get_all", response_class=HTMLResponse)
async def get_all_isp(
request: Request,
session: Annotated[AsyncSession, Depends(db_helper.session_getter)],
):
isps = jsonable_encoder(
await isp_crud.get_all_isp(
session=session,
)
)
print(isps)
return templates.TemplateResponse(
"body-isp.html",
{
"request": request,
"isps": isps,
},
)

25
sipi-app/views/setting.py Normal file
View File

@ -0,0 +1,25 @@
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.templating import Jinja2Templates
from fastapi import APIRouter, Depends
router = APIRouter(
tags=["Web"],
)
templates = Jinja2Templates(directory="views/templates")
@router.get("/", response_class=HTMLResponse)
async def get_all_settings(
request: Request,
):
return templates.TemplateResponse(
"body-settings.html",
{
"request": request,
},
)

View File

@ -0,0 +1,32 @@
header {
background-color: green;
position: relative;
top: 0;
left: 0;
width: 100%;
margin-bottom: 10px;
}
div.header-button{
#background-color: red;
display: inline-block;
float: right;
}
div.body{
position: relative;
}
footer {
background-color: red;
position:fixed;
left: 0;
bottom:0;
width:100%;
text-align: right;
}

View File

View File

@ -0,0 +1,25 @@
<html>
<head>
<title>Hello...</title>
</head>
<body>
<div id="parent-div">
<h1>Hello...</h1>
</div>
<div id="tabs" hx-target="#tab-contents" role="tablist" _="on htmx:afterOnLoad set @aria-selected of <[aria-selected=true]/> to false tell the target take .selected set @aria-selected to true">
<button role="tab" aria-controls="tab-content" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button>
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab2">Tab 2</button>
<button role="tab" aria-controls="tab-content" aria-selected="false" hx-get="/tab3">Tab 3</button>
</div>
<div id="tab-contents" role="tabpanel" hx-get="/tab1" hx-trigger="load"></div>
<button
hx-get="/world"
hx-trigger="click"
hx-target="#parent-div"
hx-swap="innerHTML"
>Click Me!
</button>
<script src="/static/js/htmx.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

1
sipi-app/views/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<title>SiPi-web</title>
<link href="/static/css/base.css" rel="stylesheet" type="text/css" />
<link rel="icon" href="data:;base64,=">
<script src="/static/js/htmx.js"></script>
</head>
{% include 'header.html' %}
{% include 'body.html' %}
{% include 'footer.html' %}
</html>

View File

@ -0,0 +1,13 @@
<div id="body">
{% for isp in isps %}
{{isp.name}}
{% for connection in isp.isp_connections %}
{{connection.contract_num}}
{% endfor %}
<br>
{% endfor %}
</div>

View File

@ -0,0 +1,3 @@
<div id="body">
Настройки
</div>

View File

@ -0,0 +1,3 @@
<div id="body">
</div>

View File

@ -0,0 +1,3 @@
<footer class="footer">
Сирожа корпорейтед &#169;
</footer>

View File

@ -0,0 +1,13 @@
<header>
<label>SiPi-web</label>
<div class="header-button">
<button hx-get="/web/isp/get_all" hx-target='#body'>Подключения провайдеров</button>
<button hx-get="/web/settings" hx-target='#body'>Настройки</button>
<!--
<button hx-get="/web/isp/get_all" hx-target='#body' hx-swap="outerHTML" hx-trigger="click">Площадки</button>
<button hx-get="/web/isp/get_all" hx-target='#body'>Внешние адреса</button>
<button hx-get="/web/isp/get_all" hx-target='#body'>Внутренние подсети</button>
<button hx-get="/web/isp/get_all" hx-target='#body'>Сетевое оборудование</button>
-->
</div>
</header>

21
sipi-app/views/web.py Normal file
View File

@ -0,0 +1,21 @@
from fastapi import APIRouter
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.templating import Jinja2Templates
router = APIRouter(
tags=["Web"],
)
templates = Jinja2Templates(directory="views/templates")
@router.get("/", response_class=HTMLResponse)
async def start_page(request: Request):
return templates.TemplateResponse(
"base.html",
{
"request": request,
},
)

View File

@ -1 +0,0 @@
print(123123)