refs #0 The root commit.

This commit is contained in:
sbps-test user
2023-09-13 20:37:14 +03:00
commit 10eb607154
66 changed files with 1643 additions and 0 deletions

0
back/core/__init__.py Normal file
View File

35
back/core/command.py Normal file
View File

@@ -0,0 +1,35 @@
from os import mkdir
def create_project(name: str) -> None:
"""
Creates project folder and all required files.
from core import command; command.create_project("project")
Args:
name (str): The desired project name.
"""
def _create_file(
file_name: str, # Literal["__init__", "config", "create_schema", "model", "schema"],
content: str = None, # Optional[str] = None,
) -> None:
file = open("config/module/" + name + "/" + file_name, "w")
if content:
file.write(content)
file.close()
mkdir("config/module/" + name)
files_without_content = ("__init__.py", "config.yaml")
files_with_content = ("create_schema.py", "model.py", "schema.py")
content = "from core.util import update_module_locals\n\nupdate_module_locals(__name__, locals())\n"
for file_name in files_without_content:
_create_file(file_name)
for file_name in files_with_content:
_create_file(file_name, content)

View File

@@ -0,0 +1,99 @@
from config.module import Module
from core.field import get_field
from core.pydantic_field import get_pydantic_field
from sqlalchemy import Column, Integer
from yaml import safe_load
class Config:
"""
Base class for ModelConfig, SchemaConfig and CreateSchemaConfig.
"""
def __init__(self, module_name: str) -> None:
"""
Initialize configuration.
:param module_name: The name of module.
"""
self.module = Module(module_name)
self.project = self.get_project()
def get_project(self) -> dict:
"""
Construct and return dictionary with project attributes from yaml file.
"""
with open(self.module.config_path, "r") as stream:
project = safe_load(stream)
return project
class Config:
orm_mode = True
class ModelConfig(Config):
"""
Configurates sqlalchemy model of module.
"""
def get_model_attrs(self) -> dict:
"""
Returns model configuration for the module.
"""
file_fields = set()
model_attrs = {
"__tablename__": self.module.title,
"id": Column(Integer, primary_key=True, index=True),
}
for field_name, field_attrs in self.project.get("fields").items():
model_attrs[field_name] = get_field(field_attrs)
if field_attrs["type"] == "file":
file_fields.add(field_name)
model_attrs["_file_fields"] = file_fields
return model_attrs
class CreateSchemaConfig(Config):
"""
Configurates pydantic model for creating items of module.
"""
def get_create_field_defs(self):
"""
Returns all pydantic model fields.
"""
create_field_definitions = {
field_name: get_pydantic_field(field_attrs)
for field_name, field_attrs in self.project.get("fields").items()
if field_attrs["type"] != "file" and get_pydantic_field(field_attrs)
}
return create_field_definitions
class SchemaConfig(Config):
"""
Configurates pydantic model for updating and returning items of module.
"""
def get_field_defs(self):
"""
Return all pydantic model fields and ID field.
"""
field_definitions = {}
field_definitions["id"] = (int, ...)
for field_name, field_attrs in self.project.get("fields").items():
if get_pydantic_field(field_attrs):
field_definitions[field_name] = get_pydantic_field(field_attrs)
return field_definitions

68
back/core/crud.py Normal file
View File

@@ -0,0 +1,68 @@
import os
from sqlalchemy.orm import Session
def add_item(db: Session, cls, item):
db_item = cls(**item.dict())
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
def view_item(db: Session, cls, item_id: int):
return db.query(cls).filter(cls.id == item_id).first()
def edit_item(db: Session, cls, item_id: int, item):
db_item = db.query(cls).filter(cls.id == item_id).first()
for key, value in item.dict().items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return db_item
def remove_item(db: Session, cls, item_id: int):
db_item = db.query(cls).filter(cls.id == item_id).first()
db.delete(db_item)
db.commit()
return db_item
def view_items(db: Session, cls, skip: int = 0, limit: int = 100):
return db.query(cls).offset(skip).limit(limit).all()
def add_file(db: Session, cls, item_id: int, field: str, file):
content = file.file.read()
if content:
file_path = os.path.join("media", file.filename)
else:
file_path = None
if file_path:
with open(file_path, "wb") as f:
f.write(content)
db_item = db.query(cls).filter(cls.id == item_id).first()
setattr(db_item, field, file_path)
db.commit()
db.refresh(db_item)
return db_item
def remove_file(db: Session, cls, item_id: int, field: str):
db_item = db.query(cls).filter(cls.id == item_id).first()
file_path = getattr(db_item, field)
if file_path:
os.remove(file_path)
setattr(db_item, field, None)
db.commit()
db.refresh(db_item)
return db_item

13
back/core/database.py Normal file
View File

@@ -0,0 +1,13 @@
import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = os.getenv("POSTGRES_DATABASE_URL")
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

0
back/core/exception.py Normal file
View File

27
back/core/field.py Normal file
View File

@@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, String
def get_field(field_attrs: dict) -> Column:
"""
Construct sqlalchemy field using dictionary with field attributes.
Returns the sqlalchemy column.
:param field_attrs: The dictionary with field attributes.
"""
column_attrs = {}
for key, value in field_attrs.items():
if key == "type":
match value:
case "str":
column_attrs["type_"] = String
case "int":
column_attrs["type_"] = Integer
case "file":
column_attrs["type_"] = String
elif key == "nullable":
column_attrs["nullable"] = value
return Column(**column_attrs)

18
back/core/helper.py Normal file
View File

@@ -0,0 +1,18 @@
def snake_to_camel(snake_string: str) -> str:
"""
Transform string from snake case to string in camel case.
Returns the string in tme camel case.
:param snake_string: The string in the snake case.
"""
return "".join(part.capitalize() for part in snake_string.split("_"))
def get_file_name(name: str) -> str:
return name.split(".")[-1]
def get_parent_directory_name(name: str) -> str:
return name.split(".")[-2]

108
back/core/main.py Normal file
View File

@@ -0,0 +1,108 @@
from typing import Optional
from config.module import MODULES, Module
from core import crud
from core.database import Base, engine
from core.util import get_session
from fastapi import APIRouter, Depends, FastAPI, HTTPException, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
app = FastAPI()
def get_router(module: Module) -> APIRouter:
cls = module.get_item("model")
Item = module.get_item("schema")
ItemCreate = module.get_item("create_schema")
router = APIRouter(prefix="/" + module.title, tags=[module.title])
@router.post("/", response_model=Item)
async def create_item(item: ItemCreate, db: Session = Depends(get_session)):
return crud.add_item(db, cls, item)
@router.get("/", response_model=list[Item])
async def list_item(
skip: int = 0, limit: int = 100, db: Session = Depends(get_session)
):
items = crud.view_items(db, cls, skip, limit)
return items
@router.get("/{item_id}", response_model=Item)
async def read_item(item_id: int, db: Session = Depends(get_session)):
db_item = crud.view_item(db, cls, item_id)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
return db_item
@router.put("/{item_id}", response_model=Item)
async def update_item(
item_id: int,
item: ItemCreate,
db: Session = Depends(get_session),
):
return crud.edit_item(db, cls, item_id, item)
@router.delete("/{item_id}", response_model=Item)
async def delete_item(item_id: int, db: Session = Depends(get_session)):
db_item = crud.view_item(db, cls, item_id)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
for field in db_item._file_fields:
crud.remove_file(db, cls, item_id, field)
crud.remove_item(db, cls, item_id)
return db_item
@router.get("/{item_id}/{field}", response_class=FileResponse)
async def read_file(item_id: int, field: str, db: Session = Depends(get_session)):
db_item = crud.view_item(db, cls, item_id)
return getattr(db_item, field)
@router.post("/{item_id}/{field}", response_model=Optional[str])
async def create_file(
item_id: int, field: str, file: UploadFile, db: Session = Depends(get_session)
):
try:
db_item = crud.add_file(db, cls, item_id, field, file)
except Exception:
raise HTTPException(status_code=500, detail="Can not upload file")
finally:
file.file.close()
return getattr(db_item, field)
@router.delete("/{item_id}/{field}", response_model=Item)
async def delete_file(item_id: int, field: str, db: Session = Depends(get_session)):
db_item = crud.remove_file(db, cls, item_id, field)
return db_item
return router
for module in tuple(Module(module_name) for module_name in MODULES):
app.include_router(get_router(module))
module_router = APIRouter(prefix="/_module", tags=["_module"])
@module_router.get("/", response_model=list[str])
def list_module(skip: int = 0, limit: int = 100):
return MODULES
@module_router.get("/{module}", response_model=dict)
def read_module(module: str):
if module not in MODULES:
raise HTTPException(status_code=404, detail="Module not found")
return Module(module).get_body()
app.include_router(module_router)
Base.metadata.create_all(bind=engine)

View File

@@ -0,0 +1,10 @@
def get_pydantic_field(field_attrs: dict) -> tuple:
match field_attrs["type"]:
case "int" | "str":
field = (field_attrs["type"], ...)
case "file":
field = (str, None)
case _:
return ()
return field

75
back/core/test.py Normal file
View File

@@ -0,0 +1,75 @@
from re import match
from os.path import exists
from unittest import TestCase, main
from shutil import rmtree
from command import create_project
def increment_name(full_name: str) -> str:
name, index = match(
pattern="^([a-z\d]+)(?:_(\d+))?", string=full_name.lower()
).groups()
if not index:
postfix = "_1"
else:
postfix = f"_{index+1}"
return name + postfix
def get_temporary_directory_name() -> str:
dir_name = "test"
while exists("config/module/" + dir_name + "/"):
dir_name = increment_name(dir_name)
return dir_name
class TestCommandMethods(TestCase):
def test_create_project_creates_project_directory(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
directory_exists = exists(f"config/module/{directory}/")
rmtree(f"config/module/{directory}/")
self.assertTrue(directory_exists)
def test_create_project_creates_init(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
file_exists = exists(f"config/module/{directory}/__init__.py")
rmtree(f"config/module/{directory}/")
self.assertTrue(file_exists)
def test_create_project_creates_config(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
file_exists = exists(f"config/module/{directory}/config.yaml")
rmtree(f"config/module/{directory}/")
self.assertTrue(file_exists)
def test_create_project_creates_create_schema(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
file_exists = exists(f"config/module/{directory}/create_schema.py")
rmtree(f"config/module/{directory}/")
self.assertTrue(file_exists)
def test_create_project_creates_model(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
file_exists = exists(f"config/module/{directory}/model.py")
rmtree(f"config/module/{directory}/")
self.assertTrue(file_exists)
def test_create_project_creates_schema(self: TestCase):
directory = get_temporary_directory_name()
create_project(directory)
file_exists = exists(f"config/module/{directory}/schema.py")
rmtree(f"config/module/{directory}/")
self.assertTrue(file_exists)
if __name__ == "__main__":
main()

9
back/core/typing.py Normal file
View File

@@ -0,0 +1,9 @@
from typing import TypeVar
from pydantic import BaseModel
from core.database import Base
Model = TypeVar("Model", bound=Base)
Schema = TypeVar("Schema", bound=BaseModel)
CreateSchema = TypeVar("CreateSchema", bound=BaseModel)

24
back/core/util.py Normal file
View File

@@ -0,0 +1,24 @@
from config.module import Module
from core.database import SessionLocal
from core.helper import get_file_name, get_parent_directory_name
def update_module_locals(path, _locals):
item = get_file_name(path)
module_name = get_parent_directory_name(path)
module = Module(module_name)
_locals[module.get_name(item)] = module.get_item(item)
def get_session():
session = SessionLocal()
try:
yield session
finally:
session.close()
def get_module(module: str) -> dict:
return Module(module).get_item("create_schema").construct().schema()