refs #0 The root commit.
This commit is contained in:
0
back/core/__init__.py
Normal file
0
back/core/__init__.py
Normal file
35
back/core/command.py
Normal file
35
back/core/command.py
Normal 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)
|
||||
99
back/core/config_factory.py
Normal file
99
back/core/config_factory.py
Normal 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
68
back/core/crud.py
Normal 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
13
back/core/database.py
Normal 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
0
back/core/exception.py
Normal file
27
back/core/field.py
Normal file
27
back/core/field.py
Normal 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
18
back/core/helper.py
Normal 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
108
back/core/main.py
Normal 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)
|
||||
10
back/core/pydantic_field.py
Normal file
10
back/core/pydantic_field.py
Normal 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
75
back/core/test.py
Normal 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
9
back/core/typing.py
Normal 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
24
back/core/util.py
Normal 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()
|
||||
Reference in New Issue
Block a user