feat(projects): 新增FastAPI项目相关源码

This commit is contained in:
100gle
2023-01-05 09:29:22 +08:00
parent fdd3e5ce6d
commit bb1e8b5101
29 changed files with 1217 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,41 @@
from sqlalchemy import Column, Integer, String, create_engine, insert, select
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
engine = create_engine("sqlite://", echo=True, future=True)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(10), nullable=False)
email = Column(String(20))
telephone = Column(String(11))
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}', telephone='{self.telephone}')>"
def main():
Base.metadata.create_all(engine)
db = engine.connect()
stmt = (
insert(User)
.values(name="100gle", email="100gle@example.com", telephone="001-1234-5678")
.compile()
)
db.execute(stmt)
db.commit()
query = select(User)
data = db.execute(query).all()
for row in data:
print(row)
db.close()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,76 @@
import pathlib
from sqlalchemy import Column, Integer, String, create_engine, insert
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
ROOT = pathlib.Path(__file__).parent
Base = declarative_base()
engine = create_engine(f"sqlite:///{ROOT / 'alchemy.db'}", future=True)
Session = sessionmaker(bind=engine)
fake_data = [
dict(name="100gle", email="100gle@example.com", telephone="001-1234-5678"),
dict(name="Steve", email="steve@example.com", telephone="011-1234-4321"),
dict(name="John", email="john@example.com", telephone="000-8764-4321"),
]
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(10), nullable=False)
email = Column(String(20))
telephone = Column(String(11))
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}', telephone='{self.telephone}')>"
def main():
Base.metadata.create_all(engine)
db = Session()
# create data
users = [User(**data) for data in fake_data]
# same as: db.bulk_save_objects(users)
db.add_all(users)
# insert expression
stmt = insert(User).values(
name="Tom", email="tom@example.com", telephone="000-1234-5678"
)
db.execute(stmt)
db.commit()
# select data
print("query all data: \n", db.query(User).all())
print(
"query the data where name is '100gle': \n",
db.query(User).filter(User.name == "100gle").first(),
)
print(
"query the data where telephone number ends with '4321': \n",
db.query(User).filter(User.telephone.endswith("4321")).all(),
)
# update data
u = db.query(User).filter(User.name == "100gle")
print("before updating: \n", u.first())
u.update({"email": ""})
db.commit()
print(f'after updating: \n', db.query(User).filter(User.name == "100gle").all())
# delete data
u = db.query(User).filter(User.name == "John")
u.delete()
db.commit()
print("after deleting:\n", db.query(User).all())
db.close()
Base.metadata.drop_all(engine)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,38 @@
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
Base = declarative_base()
engine = create_engine("sqlite://", echo=True, future=True)
Session = sessionmaker(bind=engine)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(10), nullable=False)
email = Column(String(20))
telephone = Column(String(11))
def __repr__(self):
return f"<User(name='{self.name}', email='{self.email}', telephone='{self.telephone}')>"
def main():
Base.metadata.create_all(engine)
with Session() as db:
user = User(
name="100gle", email="100gle@example.com", telephone="001-1234-5678"
)
db.add(user)
db.commit()
data = db.query(User).all()
for row in data:
print(row)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,45 @@
import uvicorn
from fastapi import APIRouter, FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/")
def index():
response = """
<div style="text-align: center;">
<h1>Hello, World</h1>
<p>you got it!</p>
</div>
"""
return HTMLResponse(content=response)
router = APIRouter(prefix="/api")
@router.get("/users")
def get_users():
...
@router.post("/users/create")
def create_user():
...
@router.put("/users/update/{user_id}")
def update_user(user_id):
...
@router.delete("/users/delete/{user_id}")
def delete_user(user_id):
...
app.include_router(router)
if __name__ == "__main__":
uvicorn.run("basic:app")

View File

@@ -0,0 +1,29 @@
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class LoginData(BaseModel):
username: str
password: str
@app.post("/login")
def login(data: LoginData):
response = dict(message="Succeed", token="user-token")
if data.username == "admin":
response["token"] = "superuser-token"
elif data.username != "user":
invalid = dict(
message="Login failed. Invalid credentials",
token=None,
)
response.update(invalid)
return response
if __name__ == '__main__':
uvicorn.run("body:app")

View File

@@ -0,0 +1,22 @@
import uvicorn
from fastapi import FastAPI
app = FastAPI()
fake_data = {
1: {"name": "100gle", "age": 18},
2: {"name": "Jenny", "age": 28},
3: {"name": "Peter", "age": 25},
}
@app.get("/user/{user_id}")
def get_user_by_id(user_id):
id = int(user_id)
data = fake_data.get(id, None)
if not data:
return {"message": "User not found"}
return data
if __name__ == '__main__':
uvicorn.run("path:app")

View File

@@ -0,0 +1,24 @@
import uvicorn
from fastapi import FastAPI
app = FastAPI()
fake_data = {
1: {"name": "100gle", "age": 18},
2: {"name": "Jenny", "age": 28},
3: {"name": "Peter", "age": 25},
}
# just change this part:
# ===============
@app.get("/user")
def get_user_by_id(id: int):
data = fake_data.get(id, None)
if not data:
return {"message": "User not found"}
return data
# ==============
if __name__ == '__main__':
uvicorn.run("query:app")

View File

@@ -0,0 +1,95 @@
from fastapi import APIRouter, Depends, Form
from popup_api.models import Group, Task, get_db
from popup_api.schema import Response
from sqlalchemy import delete
from starlette.responses import RedirectResponse
router = APIRouter()
@router.get("/tasks")
def query_all_tasks(db=Depends(get_db)):
data = (
db.query(Task)
.with_entities(
Task.id,
Task.name,
Task.priority,
Task.description,
Task.is_done,
Task.group_id,
)
.all()
)
response = Response(data=data, message="query all tasks successfully.")
return response
@router.get("/groups")
def query_all_groups(db=Depends(get_db)):
data = db.query(Group).all()
response = Response(data=data, message="query all groups successfully.")
return response
@router.post("/tasks/create")
def create_task(
name: str = Form(..., alias="taskName", example="测试"),
priority: int = Form(1, alias="taskPriority", example=1),
description: str = Form("", alias="taskDescription", example="任务描述测试"),
group_id: int = Form(1, alias="taskGroup", example=1),
db=Depends(get_db),
):
task = Task(
name=name, priority=priority, description=description, group_id=group_id
)
db.add(task)
db.commit()
response = RedirectResponse("/")
return response
@router.put("/tasks/update/{task_id}")
def update_task(task_id: int, db=Depends(get_db)):
task = db.get(Task, task_id)
if not task:
response = Response(
code=10001, message=f"can't found the task which id is: {task_id}"
)
else:
status = 0 if task.is_done == 1 else 1
task.is_done = status
db.commit()
response = Response(
data=dict(id=task_id, name=task.name),
message=f"update #{task_id} task successfully.",
)
return response
@router.delete("/tasks/delete/{task_id}")
def delete_task(task_id: int, db=Depends(get_db)):
task = db.get(Task, task_id)
if not task:
response = Response(
code=10001, message=f"can't found the task which id is: {task_id}"
)
else:
stmt = delete(Task).where(Task.id == task_id)
db.execute(stmt)
db.commit()
response = Response(
data=dict(id=task_id, name=task.name),
message=f"delete #{task_id} task successfully.",
)
return response

Binary file not shown.

View File

@@ -0,0 +1,79 @@
import pathlib
import uvicorn
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from popup_api.api import router
from popup_api.models import init_db
from popup_api.settings import settings
app = FastAPI(
title=settings.SITE_NAME,
version=settings.VERSION,
description=settings.DESCRIPTION,
docs_url=None,
redoc_url=None,
)
ROOT = pathlib.Path(__file__).parent
templates = Jinja2Templates(directory=ROOT.joinpath("templates"))
@app.get("/")
def index(request: Request):
return templates.TemplateResponse("index.html", context={"request": request})
@app.post("/")
def index(request: Request):
return templates.TemplateResponse("index.html", context={"request": request})
@app.get("/docs", include_in_schema=False)
def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/js/swagger-ui-bundle.js",
swagger_css_url="/static/css/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="/static/js/redoc.standalone.js",
)
@app.on_event("startup")
def startup():
init_db()
print("initialization for app")
app.include_router(router, prefix="/api")
app.mount("/static", StaticFiles(directory=ROOT.joinpath("static")), name="static")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)

View File

@@ -0,0 +1,108 @@
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
String,
create_engine,
func,
insert,
select,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from popup_api.settings import settings # isort:skip
engine = create_engine(
url=settings.DATABASE_URL,
echo=settings.DATABASE_ECHO,
connect_args=settings.DATABASE_CONNECT_ARGS,
)
Base = declarative_base()
Session = sessionmaker(bind=engine)
# ==========
# Data Model
# ==========
class Task(Base):
__tablename__ = "popup_task"
id = Column(Integer, primary_key=True, autoincrement=True, comment="序号")
name = Column(String(50), nullable=False, comment="任务名称")
priority = Column(Integer, nullable=False, default=0, comment="任务优先级")
description = Column(String(200), nullable=True, comment="任务描述", default="")
create_at = Column(
DateTime(timezone=True),
server_default=func.now(),
comment="创建时间",
)
update_at = Column(
DateTime(timezone=True),
onupdate=func.now(),
comment="更新时间",
)
is_done = Column(Integer, nullable=False, default=0, comment="是否完成")
group_id = Column(Integer, ForeignKey("popup_group.id"), default=1, comment="分类序号")
def __repr__(self):
return f"<Task(id={self.id}, name={self.name}, is_done={self.is_done}, group_id={self.group_id})>"
class Group(Base):
__tablename__ = "popup_group"
id = Column(Integer, primary_key=True, autoincrement=True, comment="分类序号")
name = Column(
String(50),
nullable=False,
default="收集箱",
comment="分类名称",
unique=True,
)
def __repr__(self):
return f"<Group(id={self.id}, name={self.name})"
# ============
# Data handler
# ============
def init_db():
Base.metadata.create_all(engine)
group_check = engine.execute(select(Group).limit(5)).all()
if not group_check:
groups = [
dict(name="收集箱"),
dict(name="生活"),
dict(name="工作"),
]
stmt = insert(Group).values(groups)
engine.execute(stmt)
task_check = engine.execute(select(Task).limit(5)).all()
if not task_check:
tasks = [
dict(name="吃饭", priority=3, group_id=2),
dict(name="睡觉", priority=3, group_id=2),
dict(name="打豆豆", priority=0, description="打豆豆是一件很有意思的事情"),
dict(name="逛逛少数派", priority=1, description="https://sspai.com"),
dict(name="记得下班打卡", priority=3),
]
stmt = insert(Task).values(tasks)
engine.execute(stmt)
def get_db():
session = Session()
try:
yield session
finally:
session.close()

View File

@@ -0,0 +1,11 @@
from typing import Optional, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class Response(BaseModel):
code: int = 0
message: str
data: Optional[T]

View File

@@ -0,0 +1,24 @@
import pathlib
from pydantic import BaseSettings
APP_ROOT = pathlib.Path(__file__).parent
class Settings(BaseSettings):
# fastapi settings
VERSION = 0.1
SITE_NAME = "Todolist - Popup"
DESCRIPTION = """
This is the brand new Todolist application,
which built in FastAPI with RESTful APIs.
""".strip()
# database settings
DATABASE_URL = f"sqlite:///{APP_ROOT / 'db.sqlite3'}"
DATABASE_ECHO = True
DATABASE_CONNECT_ARGS = {"check_same_thread": False}
settings = Settings()

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,123 @@
.container {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* navbar */
.navbar ul {
display: flex;
align-items: center;
justify-content: space-between;
text-align: center;
list-style: none;
padding-left: 0;
}
.navbar a {
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
border: none;
font-size: 20px;
font-weight: bold;
}
.navbar a:hover {
color: goldenrod !important;
}
/* data table */
.data-table {
display: table;
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
}
.data-table th:first-child {
width: 5%;
}
.data-table th:nth-child(2) {
width: 15%;
}
.data-table th:nth-child(4) {
width: 30%;
}
.data-table td[class="ops"] {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.data-table .ops span {
display: block;
color: var(--accent);
cursor: pointer;
}
.btn-add-task {
display: flex;
flex-direction: row-reverse;
}
.btn-add-task > button {
display: block;
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 20px;
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
}
.btn-add-task > button:hover {
background-color: rgb(204, 77, 77);
}
.task-form {
display: none;
flex-direction: column;
align-items: center;
}
.form {
display: block;
width: 80%;
}
.form > div {
display: flex;
flex-direction: row;
justify-content: space-between;
vertical-align: middle;
}
.form .btn-submit {
text-align: center;
transition: 0.1ms;
}
.form .btn-submit > input {
display: block;
width: 100%;
font-size: 20px;
transition: 0.1ms;
}
#taskDescription {
width: 60% !important;
resize: none !important;
}
/* footer */
#footer {
text-align: center;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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,84 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="123" height="44" viewBox="0 0 123 44" style="enable-background:new 0 0 123 44;" xml:space="preserve">
<style type="text/css">
.st0{fill:#292525;}
.st1{fill:#DA282A;}
.st2{fill:#FFFFFF;}
</style>
<path class="st0" d="M59.8,31.7C59.8,31.7,59.7,31.7,59.8,31.7c-0.1,0-0.2-0.1-0.2-0.1l-0.9-1.3c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1
c0,0,0-0.1,0.1-0.1c0,0,0.1,0,0.1,0c6.6-0.6,11.8-2.9,17-7.5c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0,0,0.1,0.1
l0.9,1.1c0,0,0.1,0.1,0,0.2c0,0.1,0,0.1-0.1,0.1C71.6,28.6,66.6,30.8,59.8,31.7L59.8,31.7z" />
<path class="st0" d="M69.2,12.2h-1.5c-0.1,0-0.2,0.1-0.2,0.2v12.4c0,0.1,0.1,0.2,0.2,0.2h1.5c0.1,0,0.2-0.1,0.2-0.2V12.5
C69.4,12.3,69.3,12.2,69.2,12.2z" />
<path class="st0" d="M60.7,23.5H59c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1l3.6-7.9c0,0,0-0.1,0.1-0.1
c0,0,0.1,0,0.1,0h1.7c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1l-3.6,7.9c0,0,0,0.1-0.1,0.1
C60.8,23.5,60.7,23.5,60.7,23.5z" />
<path class="st0" d="M75.6,21c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1-0.1l-3.1-5.4c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0.1-0.1
c0,0,0.1,0,0.1,0h1.8c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0.1l3.1,5.4c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1-0.1,0.1
c0,0-0.1,0-0.1,0H75.6L75.6,21z" />
<path class="st0" d="M108.5,31.6C108.5,31.6,108.4,31.6,108.5,31.6l-1.4-0.5c-0.1,0-0.1-0.1-0.1-0.1c0,0,0-0.1,0-0.1
c0,0,0-0.1,0-0.1l0.8-2c0.3-0.8,0.6-1.6,0.8-2.5c0.3-1.3,0.4-2.5,0.4-3.8V14c0-0.1,0-0.1,0.1-0.2c0,0,0.1-0.1,0.2-0.1
c5.3,0.1,7.5-0.5,10.3-1.3l0.7-0.2c0,0,0.1,0,0.1,0s0.1,0,0.1,0.1l0.8,1.2c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1-0.1,0.1
c0,0-0.1,0-0.1,0l-0.3,0.1c-2.9,0.9-5,1.5-9.8,1.6c-0.1,0-0.1,0-0.1,0.1c0,0-0.1,0.1-0.1,0.1v6.9c0,1.4-0.2,2.7-0.5,4.1l-0.1,0.3
c-0.2,0.8-0.4,1.7-0.8,2.5l-0.8,2C108.7,31.5,108.6,31.6,108.5,31.6C108.6,31.6,108.5,31.6,108.5,31.6z" />
<path class="st0" d="M105.6,22c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1-0.1L103,18c0,0,0,0,0-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1
c0,0,0,0,0.1-0.1l1.8-1.1c0,0,0.1,0,0.1-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1l-1.9-2.9c0,0,0-0.1,0-0.1
c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0,0,0.1-0.1l1.3-0.8c0,0,0.1,0,0.2,0c0.1,0,0.1,0,0.1,0.1l2.3,3.6c0,0,0,0,0,0.1l0.6,0.9
c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0-0.1,0.1l-1.8,1.1c0,0-0.1,0-0.1,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1
c0,0,0,0.1,0,0.1l1.4,2.2c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0.1,0,0.1-0.1,0.1l-1.3,0.8C105.7,22,105.6,22,105.6,22z" />
<path class="st0" d="M104.4,31.8C104.4,31.8,104.4,31.8,104.4,31.8l-1.5-0.6c0,0-0.1,0-0.1,0c0,0,0,0,0-0.1c0,0,0-0.1,0-0.1
c0,0,0-0.1,0-0.1l3.3-8.3c0,0,0-0.1,0-0.1c0,0,0,0,0.1,0s0.1,0,0.1,0c0,0,0.1,0,0.1,0l1.4,0.6c0,0,0.1,0,0.1,0c0,0,0,0,0,0.1
c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1l-3.3,8.3c0,0,0,0,0,0.1C104.6,31.7,104.6,31.7,104.4,31.8C104.5,31.8,104.5,31.8,104.4,31.8z" />
<path class="st0" d="M120.8,31.7C120.8,31.7,120.8,31.7,120.8,31.7c-0.1,0-0.2-0.1-0.2-0.1l-1.6-2.3c-0.3-0.4-0.6-0.8-0.8-1.3
l-0.8-1.6c-0.3-0.6-0.6-1.3-0.8-2l-1.5-5.7c0,0,0-0.1-0.1-0.1c0,0-0.1,0-0.1,0c-0.4,0-0.7,0-1.1,0.1c0,0-0.1,0-0.1,0
c0,0,0,0.1,0,0.1v10.9c0,0.1,0,0.1,0.1,0.1c0,0,0.1,0.1,0.1,0.1h2.6c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2v1.4
c0,0.1,0,0.1-0.1,0.2c0,0-0.1,0.1-0.2,0.1h-4.3c-0.1,0-0.1,0-0.2-0.1c0,0-0.1-0.1-0.1-0.2V17.6c0,0,0,0,0-0.1V17
c0-0.1,0-0.1,0.1-0.2c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0c2.4,0,4.7-0.3,7-0.8l0.9-0.2c0,0,0.1,0,0.1,0c0,0,0.1,0.1,0.1,0.1l0.7,1.3
c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1-0.1,0.1c0,0-0.1,0-0.1,0l-0.9,0.2c-1,0.2-1.9,0.4-2.9,0.6c0,0,0,0-0.1,0c0,0,0,0,0,0
c0,0,0,0,0,0.1c0,0,0,0,0,0.1l0.9,3.5c0,0,0,0.1,0,0.1c0,0,0,0,0.1,0.1c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0l2.9-1.2c0,0,0.1,0,0.1,0
c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0,0,0,0.1l0.5,1.3c0,0.1,0,0.1,0,0.2c0,0.1-0.1,0.1-0.1,0.1l-3.2,1.3c0,0-0.1,0-0.1,0.1
c0,0,0,0.1,0,0.1c0.2,0.6,0.4,1.1,0.6,1.6l0.7,1.3c0.2,0.4,0.4,0.8,0.7,1.2l1.9,2.7c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1
c0,0,0,0.1,0,0.1c0,0-0.1,0-0.1,0.1L120.8,31.7C120.9,31.7,120.9,31.7,120.8,31.7z" />
<path class="st0" d="M86.8,22.3h-1.5c-0.1,0-0.1,0-0.2-0.1c0,0-0.1-0.1-0.1-0.2v-4c0,0,0-0.1-0.1-0.1c0,0-0.1-0.1-0.1-0.1h-4
c-0.1,0-0.1,0-0.2-0.1c0,0-0.1-0.1-0.1-0.2v-1.4c0-0.1,0-0.1,0.1-0.2c0,0,0.1-0.1,0.2-0.1h4c0,0,0.1,0,0.1-0.1c0,0,0.1-0.1,0.1-0.1
v-3.5c0-0.1,0-0.1,0.1-0.2c0,0,0.1-0.1,0.2-0.1h1.5c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2V16c0,0,0,0.1,0.1,0.1
c0,0,0.1,0.1,0.1,0.1h4c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2v1.4c0,0.1,0,0.1-0.1,0.2c0,0-0.1,0.1-0.2,0.1h-4c0,0,0,0-0.1,0
c0,0,0,0-0.1,0c0,0,0,0,0,0.1c0,0,0,0,0,0.1v4c0,0.1,0,0.1-0.1,0.2C86.9,22.3,86.9,22.3,86.8,22.3z" />
<path class="st0" d="M83,16.2c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1-0.1l-1.5-2.8c0-0.1,0-0.1,0-0.2c0-0.1,0.1-0.1,0.1-0.1l1.4-0.7
c0.1,0,0.1,0,0.2,0c0.1,0,0.1,0.1,0.1,0.1l1.5,2.8c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1c0,0.1-0.1,0.1-0.1,0.1L83,16.2
C83,16.2,83,16.2,83,16.2z" />
<path class="st0" d="M89,16.2c0,0-0.1,0-0.1,0l-1.2-1c0,0-0.1-0.1-0.1-0.1c0-0.1,0-0.1,0-0.2l2.1-2.5c0,0,0.1-0.1,0.1-0.1
c0.1,0,0.1,0,0.2,0l1.2,1c0,0,0.1,0.1,0.1,0.1c0,0.1,0,0.1,0,0.2l-2.1,2.5C89.1,16.1,89.1,16.2,89,16.2C89.1,16.2,89,16.2,89,16.2z" />
<path class="st0" d="M82.2,22c-0.1,0-0.1,0-0.1-0.1l-1.2-1c0,0,0,0-0.1-0.1c0,0,0-0.1,0-0.1c0-0.1,0-0.1,0.1-0.2l2.2-2.6
c0,0,0,0,0.1-0.1c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0l1.2,1c0,0,0,0,0.1,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1
c0,0,0,0.1,0,0.1l-2.2,2.6C82.4,21.9,82.3,21.9,82.2,22C82.3,22,82.3,22,82.2,22z" />
<path class="st0" d="M89.8,21.9C89.7,21.9,89.7,21.8,89.8,21.9c-0.1,0-0.2-0.1-0.2-0.1l-1.9-2.6c0,0,0,0,0-0.1c0,0,0-0.1,0-0.1
c0,0,0-0.1,0-0.1c0,0,0,0,0.1-0.1L89,18c0,0,0,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0,0,0.1,0.1l1.9,2.6c0,0,0,0.1,0,0.2
c0,0.1,0,0.1-0.1,0.1l-1.2,0.9C89.8,21.8,89.8,21.9,89.8,21.9z" />
<path class="st0" d="M91.6,30.2C91.6,30.2,91.7,30.2,91.6,30.2c0.1,0.1,0.1,0.1,0.1,0.2c0,0,0,0.1,0,0.1c0,0,0,0.1-0.1,0.1l-1.2,1
c-0.1,0.1-0.1,0.1-0.2,0.1c-0.1,0-0.2,0-0.3,0l-3.6-1.7c0,0-0.1,0-0.1,0s-0.1,0-0.1,0l-1.4,0.6c-0.6,0.3-1.3,0.5-1.9,0.7l-1.7,0.5
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1-0.1-0.1-0.1l-0.3-1.1c0-0.1,0-0.1,0-0.2c0-0.1,0-0.1,0-0.2c0-0.1,0.1-0.1,0.1-0.1
c0,0,0.1-0.1,0.2-0.1l1.8-0.6c0.3-0.1,0.7-0.2,1-0.4l0.4-0.2l-0.8-0.4l-1.8-0.8c0,0-0.1,0-0.1-0.1c0,0,0,0,0-0.1c0,0,0-0.1,0-0.1
c0,0,0-0.1,0-0.1l1-1.8c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0-0.1-0.1-0.1-0.1c0,0-0.1,0-0.1,0h-1.1c-0.1,0-0.2,0-0.3-0.1
c-0.1-0.1-0.1-0.2-0.1-0.3v-0.9c0-0.1,0-0.2,0.1-0.3c0.1-0.1,0.2-0.1,0.3-0.1h2.5l0.3-0.5c0,0,0-0.1,0.1-0.1c0,0,0.1,0,0.1,0H86
c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0.1c0,0,0,0.1,0,0.1c0,0,0,0.1,0,0.1L86,23.3h5.2c0.1,0,0.2,0,0.3,0.1c0.1,0.1,0.1,0.2,0.1,0.3v1.6
c0,0.1,0,0.2,0,0.2c0,0.1-0.1,0.1-0.1,0.2l-1,1c-0.7,0.7-1.4,1.3-2.2,1.8L91.6,30.2z M84.4,26.9l1.8,0.8c0,0,0.1,0,0.1,0
c0,0,0.1,0,0.1,0l0.9-0.6c0.6-0.4,1.1-0.9,1.7-1.4l0.6-0.6h-4.4c-0.1,0-0.2,0-0.2,0.1c-0.1,0-0.1,0.1-0.2,0.2l-0.6,1
c0,0,0,0.1-0.1,0.2c0,0.1,0,0.1,0,0.2c0,0.1,0,0.1,0.1,0.1C84.3,26.9,84.3,26.9,84.4,26.9z" />
<path class="st0" d="M98,28l2.1,2.3c0,0,0.1,0.1,0.1,0.2c0,0.1,0,0.1-0.1,0.2l-1.1,1c0,0,0,0-0.1,0c0,0-0.1,0-0.1,0
c-0.1,0-0.1,0-0.2-0.1L96.2,29c0,0-0.1,0-0.1-0.1c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0c0,0-0.1,0-0.1,0.1l-2.5,2.7c0,0-0.1,0.1-0.2,0.1
c-0.1,0-0.1,0-0.2-0.1l-1.1-1c0,0,0,0,0-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1c0,0,0-0.1,0-0.1l2-2.1l0.8-0.9c0,0,0.1-0.1,0.1-0.1
c0-0.1,0-0.1-0.1-0.1l-0.2-0.3c-0.8-1-1.3-2.2-1.6-3.5l-0.2-0.7c-0.1-0.7-0.2-1.3-0.2-2v-4c0-0.1,0-0.1,0.1-0.2c0,0,0,0,0.1,0
c0,0,0.1,0,0.1,0h1.5c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2v3.8c0,0.6,0.1,1.3,0.2,1.9l0.1,0.3c0.2,1,0.6,2,1.2,2.9
c0,0,0,0,0.1,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0c0,0,0,0,0.1,0c0.6-0.9,1-1.9,1.3-3l0-0.1c0.2-0.7,0.2-1.3,0.2-2v-4.4c0,0,0-0.1,0-0.1
c0,0-0.1,0-0.1,0h-4.9c-0.1,0-0.1,0-0.2-0.1c0,0-0.1-0.1-0.1-0.2v-2.9c0-0.1,0-0.1,0.1-0.2c0,0,0.1-0.1,0.2-0.1h1.5
c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2v1.3h5c0.1,0,0.1,0,0.2,0.1c0,0,0.1,0.1,0.1,0.2v1.4v5c0,0.7-0.1,1.4-0.2,2.2l-0.2,0.7
c-0.3,1.2-0.8,2.4-1.6,3.4L97.4,27c0,0-0.1,0.1-0.1,0.2c0,0.1,0,0.1,0.1,0.2L98,28z" />
<path class="st1" d="M22,44c12.2,0,22-9.8,22-22C44,9.8,34.2,0,22,0C9.8,0,0,9.8,0,22C0,34.2,9.8,44,22,44z" />
<path class="st2" d="M32,11.3l-21.2,3.6c-0.2,0-0.4,0.1-0.6,0.3c-0.1,0.2-0.2,0.4-0.3,0.6l-0.2,1.8c0,0.2,0,0.3,0,0.4
c0.1,0.1,0.1,0.3,0.2,0.4c0.1,0.1,0.2,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0l4.1-0.7v14.7c0,0.2,0,0.4,0.1,0.5c0.1,0.2,0.2,0.3,0.4,0.4
c0.2,0.1,0.3,0.1,0.5,0.1c0.2,0,0.4-0.1,0.5-0.2l1.6-1c0.2-0.1,0.3-0.3,0.4-0.5c0.1-0.2,0.2-0.4,0.2-0.6v-14l5.7-0.9v10.3
c0,0.6,0.2,1.3,0.5,1.8l2.5,4.7c0.1,0.1,0.2,0.3,0.3,0.3c0.1,0.1,0.3,0.1,0.4,0.1h2.2c0.1,0,0.3,0,0.4-0.1c0.1-0.1,0.2-0.2,0.3-0.3
c0.1-0.1,0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4l-2.5-4.9C28.2,27,28,26.4,28,25.7V15.7l4-0.7c0.2,0,0.4-0.1,0.5-0.3
c0.1-0.2,0.2-0.4,0.3-0.6l0.2-1.8c0-0.2,0-0.3,0-0.5s-0.1-0.3-0.2-0.4s-0.2-0.2-0.4-0.2C32.3,11.3,32.1,11.2,32,11.3z" />
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todolist</title>
<link rel="stylesheet" href="../static/css/simple-v1.min.css" />
<link rel="stylesheet" href="../static/css/style.css" />
</head>
<body>
<div class="container">
<nav class="navbar">
<ul>
<li><a href="https://sspai.com" class="logo"><img src="../static/sspai-logo-light.svg" alt="logo"></a>
</li>
<li><a href="#">Home</a></li>
<li><a href="#">Task</a></li>
<li><a href="#">About</a></li>
</ul>
</nav>
<hr />
<div class="data-table">
<p v-if="tasks.length === 0">
🤔 Whoops……似乎任务都做完了那么可以点击下方的按钮添加任务
</p>
<table v-else>
<!-- Fields -->
<thead>
<tr>
<th v-for="field, index in fields" :key="index" scope="col">
[[ mappingFields(field) ]]
</th>
</tr>
</thead>
<!-- Data -->
<tbody>
<tr v-for="task in tasks">
<td>[[ task.id ]]</td>
<td>[[ task.name ]]</td>
<td>[[ mappingPriority(task.priority) ]]</td>
<td>[[ task.description ]]</td>
<td>[[ isDone(task.is_done) ]]</td>
<td>[[ mappingGroups(task.group_id) ]]</td>
<td>
<div class="ops">
<span @click="updateTask(task.id)">更新</span>
<span @click="deleteTask(task.id)">删除</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn-add-task" @click="showForm">
<button>+</button>
</div>
<div class="task-form">
<form v-bind:action="apis.createTask" method="post" class="form">
<div>
<label for="taskName">任务名称:</label>
<input type="text" name="taskName" id="taskName" placeholder="请输入待办事项名称" />
</div>
<div>
<label for="taskPriority">优先级:</label>
<select name="taskPriority" id="taskPriority">
<option value="1" selected>一般</option>
<option value="2">优先</option>
<option value="3">紧急</option>
</select>
</div>
<div>
<label for="taskGroup"> 分组: </label>
<select name="taskGroup" id="taskGroup">
<option :value="index" v-for="([index, group]) in Object.entries(groups)">
[[ group ]]
</option>
</select>
</div>
<div>
<label for="taskDescription">任务备注:</label>
<textarea name="taskDescription" id="taskDescription" rows="5" placeholder="备注内容(可选)"></textarea>
</div>
<div class="btn-submit">
<input ref="submit" type="submit" value="添加" />
</div>
</form>
</div>
<footer id="footer">
<p class="copyright">&copy; 100gle & sspai</p>
</footer>
</div>
</body>
<script src="../static/js/vue.min.js"></script>
<script src="../static/js/axios.min.js"></script>
<script>
const address = "http://localhost:8000";
const app = new Vue({
el: ".container",
delimiters: ["[[", "]]"],
data() {
return {
tasks: [],
fields: [],
apis: {
getAllTasks: `${address}/api/tasks`,
createTask: `${address}/api/tasks/create`,
updateTask: `${address}/api/tasks/update`,
deleteTask: `${address}/api/tasks/delete`,
getGroups: `${address}/api/groups`
},
groups: [],
}
},
methods: {
showForm(e) {
const formDiv = document.querySelector(".task-form");
if (formDiv.style.display === "") {
formDiv.style.display = "none";
}
formDiv.style.display = formDiv.style.display === "none" ? "flex" : "none";
},
getAllTasks() {
axios.get(this.apis.getAllTasks)
.then(res => {
let json = res.data.data
this.tasks = json
let fields = Object.keys(json[0])
fields.push("operators")
this.fields = fields
})
.catch(err => console.error)
},
updateTask(task_id) {
axios.put(`${this.apis.updateTask}/${task_id}`).then(res => {
this.getAllTasks();
})
},
deleteTask(task_id) {
axios.delete(`${this.apis.deleteTask}/${task_id}`).then(res => {
this.getAllTasks();
})
},
mappingFields(field) {
let mapping = {
id: "序号",
name: "任务名称",
priority: "优先级",
description: "任务描述",
is_done: "是否完成",
group_id: "分组",
operators: "操作",
}
return mapping[field];
},
isDone(done) {
return done === 1 ? "完成" : "未完成"
},
mappingPriority(value) {
let mapping = {
0: "一般",
1: "优先",
3: "紧急"
}
return mapping[value]
},
mappingGroups(value) {
return this.groups[value]
},
getGroups() {
axios.get(this.apis.getGroups)
.then(res => {
let data = res.data.data
let mappings = {}
data.reduce((id, group) => {
Object.assign(mappings, {
[group.id]: group.name
})
}, {})
this.groups = mappings
})
.catch(err => console.error)
},
},
mounted: function (e) {
this.getAllTasks();
this.getGroups();
}
})
</script>
</html>

View File

@@ -0,0 +1,30 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker
# isort: off
from popup_api.main import app
from popup_api.settings import settings
engine = create_engine(
url=settings.DATABASE_URL,
echo=settings.DATABASE_ECHO,
connect_args=settings.DATABASE_CONNECT_ARGS,
)
TestSession = sessionmaker(bind=engine)
@pytest.fixture(scope="package")
def client():
yield TestClient(app)
@pytest.fixture()
def db():
sess = TestSession()
try:
yield sess
finally:
sess.close()

View File

@@ -0,0 +1,103 @@
import pytest
from fastapi.testclient import TestClient
from popup_api.models import Task
def test_query_all_tasks(client: TestClient):
resp = client.get("/api/tasks")
assert resp.status_code == 200
data = resp.json()
assert data["code"] == 0
assert data["message"] == "query all tasks successfully."
assert len(data["data"]) > 0
def test_query_all_groups(client: TestClient):
resp = client.get("/api/groups")
assert resp.status_code == 200
data = resp.json()
assert data["code"] == 0
assert data["message"] == "query all groups successfully."
assert len(data["data"]) > 0
@pytest.mark.parametrize(
"data",
[
dict(
taskName="测试 1",
taskPriority=1,
taskDescription="测试描述",
taskGroup=0,
),
dict(
taskName="测试 2",
taskPriority=2,
taskDescription="测试一下最长的情况如何" * 100,
taskGroup=1,
),
dict(
taskName="测试 3",
taskPriority=1,
taskDescription="测试描述",
taskGroup=-1,
),
],
)
def test_create_task(client: TestClient, db, data):
resp = client.post("/api/tasks/create", data=data)
assert resp.status_code == 307
tasks = db.query(Task).filter(Task.name.like("%测试%")).all()
assert len(tasks) > 0
def test_update_task(client: TestClient, db):
pre_resp = client.get("/api/tasks")
pre_json = pre_resp.json()["data"]
task_id = pre_json[0]["id"]
is_done = pre_json[0]["is_done"]
resp = client.put(f"/api/tasks/update/{task_id}")
json = resp.json()
assert json["data"]["id"] == task_id
assert json["message"] == f"update #{task_id} task successfully."
task = db.get(Task, task_id)
assert task.is_done == (not is_done)
@pytest.mark.parametrize("invalid_id", [-1, -10])
def test_update_task_with_invalid_task_id(client: TestClient, invalid_id: int):
resp = client.put(f"/api/tasks/update/{invalid_id}")
assert resp.status_code == 200
json = resp.json()
assert json["code"] == 10001
assert json["message"] == f"can't found the task which id is: {invalid_id}"
def test_delete_task(client: TestClient, db):
pre_resp = client.get("/api/tasks")
pre_json = pre_resp.json()["data"]
task_id = pre_json[0]["id"]
resp = client.delete(f"/api/tasks/delete/{task_id}")
json = resp.json()
assert json["data"]["id"] == task_id
assert json["message"] == f"delete #{task_id} task successfully."
task = db.get(Task, task_id)
assert not task
@pytest.mark.parametrize("invalid_id", [-1, -10])
def test_delete_task_With_invalid_task_id(client: TestClient, invalid_id):
resp = client.delete(f"/api/tasks/delete/{invalid_id}")
assert resp.status_code == 200
json = resp.json()
assert json["code"] == 10001
assert json["message"] == f"can't found the task which id is: {invalid_id}"

View File

@@ -0,0 +1,20 @@
from popup_api.schema import Response
def test_Response():
resp1 = Response(code=200, message="success", data=[{"foo": 1}])
assert resp1.dict() == dict(code=200, message="success", data=[{"foo": 1}])
assert resp1.json() == '{"code": 200, "message": "success", "data": [{"foo": 1}]}'
resp2 = Response(code=200, message="success", data=[])
assert resp2.dict() == dict(code=200, message="success", data=[])
assert resp2.json() == '{"code": 200, "message": "success", "data": []}'
resp3 = Response(code=200, message="success", data="foo")
assert resp3.dict() == dict(code=200, message="success", data="foo")
assert resp3.json() == '{"code": 200, "message": "success", "data": "foo"}'
resp4 = Response(code=400, message="failed")
assert resp4.dict() == dict(code=400, message="failed", data=None)
assert resp4.json() == '{"code": 400, "message": "failed", "data": null}'

View File

@@ -0,0 +1,10 @@
name: str = "100gle"
number: int = 1
yes_or_no: bool = True
def greet(name: str = '') -> str:
if not name:
name = "world"
return f"Hello, {name}"

View File

@@ -0,0 +1,39 @@
import random
from typing import List, Optional
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
random.seed(233)
class User(BaseModel):
name: str
email: Optional[str] = None
age: int
telephone: Optional[str] = None
app = FastAPI()
users = {
1: User(name="John", age=42),
2: User(name="Jane", age=36, email="jane@example.com"),
3: User(name="Jack", age=40, telephone="555-555-5555"),
}
@app.post("/users/create/", response_model=User)
def create_user(user: User):
user_id = random.randint(4, 100)
users[user_id] = user
return user
@app.get("/users", response_model=List[User])
def get_users():
return [user for user in users.values()]
if __name__ == "__main__":
uvicorn.run("pydantic_usage:app")