v0.1 fastapi: route + model + orm

This commit is contained in:
geeker
2026-09-19 10:41:18 +08:00
commit f4fa2f9714
55 changed files with 3615 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="~/arch/code_repo/infrastructure_system_softwares/language_dev/python/fastapi_app_dev/fastapi_tutor/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings" />
<component name="PyDocumentationSettings" />
<component name="ReSTService" />
<component name="TestRunnerService" />
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="~/arch/code_repo/infrastructure_system_softwares/language_dev/python/fastapi_app_dev/fastapi_tutor/.venv" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fastapi-tutor.iml" filepath="$PROJECT_DIR$/.idea/fastapi-tutor.iml" />
</modules>
</component>
</project>
View File
View File
+7
View File
@@ -0,0 +1,7 @@
from typing import Annotated
def say_hello(name: Annotated[str, "this is just metadata"]) -> str:
return f"Hello {name}"
print(say_hello('Annotated'))
View File
+27
View File
@@ -0,0 +1,27 @@
import pymysql.cursors
# Connect to the database
connection = pymysql.connect(
host="localhost",
user="geeker",
password="geeker",
database="fastapi_tutor",
cursorclass=pymysql.cursors.DictCursor,
)
with connection:
with connection.cursor() as cursor:
# Create a new record
sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
cursor.execute(sql, ("webmaster@python.org", "very-secret"))
# connection is not autocommit by default. So you must commit to save
# your changes.
connection.commit()
with connection.cursor() as cursor:
# Read a single record
sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
cursor.execute(sql, ("webmaster@python.org",))
result = cursor.fetchone()
print(result)
+4
View File
@@ -0,0 +1,4 @@
id title content user_id created_at
-- ---------- ------------- ------- --------------------------
1 Alice 的第一篇 Hello world 1 2026-09-18 22:42:47.786259
2 Alice 的第二篇 SQLAlchemy 真香 1 2026-09-18 22:42:48.010497
+234
View File
@@ -0,0 +1,234 @@
from sqlalchemy import create_engine, Column, Integer,String,DATETIME,DECIMAL, or_, and_,not_,func
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime
db_url = "mysql+pymysql://geeker:geeker@localhost:3306/fastapi_tutor?charset=utf8mb4"
engine = create_engine(db_url, pool_size= 50, echo=True)
Base = declarative_base() # 获取基类!
class Order(Base): # python里的表名字
__tablename__ = 'order_info_detail' # 实际数据库的表名字
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='订单编号,自增主键' # 注释
)
title = Column( String(50) # 对应数据库的varchar类型
,nullable=False # 不允许为null
)
price = Column(DECIMAL)
userid = Column( Integer
# ,ForeignKey('user_info_detail.id')
,nullable=False ) # 外键约束
create_date = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_date = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
class User(Base): # python里的表名字
__tablename__ = 'user_info_detail' # 实际数据库的表名字
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='用户编号,自增主键' # 注释
)
username = Column( String(50) # 对应数据库的varchar类型
,nullable=False # 不允许为null
,comment='用户名'
)
create_date = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_date = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
Base.metadata.create_all(engine) # 所有表创建 <- 只要数据库存在这个表了,就不会触发,也不会更新表结构
# Order.__table__.create(engine,checkfirst = True) # 单张表的创建
# 三、【表数据的新增】
'''1.实例化数据库表模型类,得到具体的数据'''
u1 = User( username='清风飞扬' )
print( 'User1的属性:',u1.id,u1.username,u1.create_date,u1.update_date )
u2 = User( username='北大青鸟' )
print( 'User2的属性:',u2.username)
'''2.将实例化好的数据,添加到数据库里'''
# 拿到数据库会话管理工具
Session = sessionmaker( bind=engine
,autoflush=False # 是否开启自动刷新python新增的数据到实际的数据库内存缓存中
,autocommit = False # 不自动提交数据,这个是真正提交到数据库磁盘永久生效的
)
session = Session() # 实例化一个数据库操作会话
# session.add( u1 ) # 新增数据到数据库里
# session.add( u2 )
session.commit() # 提交,永久生效,注意提交后不能回滚
print(session.query(User).all())
#
# # session.rollback() # 回滚,事务没有提交前,dml操作可以回滚
# session.close() # 关闭数据操作会话
#
#
# '''
# 等价于sql语句:
# insert into user_info_detail(username) values('清风飞扬');
# insert into user_info_detail(username) values('北大青鸟');
# '''
#
# # 四、【表数据的更改】
# session = Session() # 实例化一个数据库操作会话
# print(f'{session.query(User)=}, {session.query(User).all()=}')
# session.query(User)\
# .filter( User.username == '北大青鸟' )\
# .update( {"username":'沃林数智'
# , "create_date":"2000-10-10"
# } # 同时更新多个字段
# )
# session.commit()
# '''
# 多个复合条件:
# filter( 条件1,条件2,条件3.. ) # and的关系
#
# filter( or_( 条件1,条件2), not_(条件2),and_(条件1,条件2) )
#
# session.query(表模型).filter(条件1).filter(条件2).filter(条件3) 条件可以拼接成and的关系
#
# or_: 函数里的条件都是 or 的关系
# and_: 函数里的条件是 and 的关系
# not_ : 将条件的结果进行取反
# '''
#
# # 五、【表数据的删除】
# try:
# session.query( User ).filter( User.username == '清风飞扬' ).delete() # 只要命中就删除
# session.commit()
# except:
# session.rollback() # 如果报错,就回滚
#
# # 清空数据,并且重置自增主键 truncate table 表名; 无法回滚
# # session.execute( text( "truncate table user_info_detail" ) ) # 直接书写原生的sql语句
# # 可以select、update、delete全部支持
'''1.单表基础查询'''
# ① 单条数据查询 first(),返回的是一个查询结果对象
r1 = session.query(User).first()
print('获取单个查询结果对象:',r1)
print(r1.id,r1.usernmae,r1.create_date,r1.update_date)
# ② 全量查询 all() , 返回的是一个列表list
r2 = session.query(User).all()
print('获取多个查询结果对象:',r2)
for i in range( len(r2) ):
print(f'第{i+1}行数据:',r2[i].id,r2[i].usernmae,r2[i].create_date,r2[i].update_date)
q = session.query(User) # 得到一个查询语句对象
r3 = q.all() # 获取查询语句对象的所有内容
print(q)
print(r3)
# ③ where 条件查询
r4 = session.query(User).filter( or_(User.id == 1, User.usernmae == '李四') ).all()
for i in r4:
print(i.id,i.usernmae)
# ④ like模糊匹配查询
r5 = session.query(User).where( User.usernmae.like('%三%') ).all()
for i in r5:
print('like模糊匹配:',i.id, i.usernmae)
# ⑤ order by排序
r5 = session.query(User).order_by(User.id.desc(), User.usernmae).all()
for i in r5:
print('排序:', i.id, i.usernmae)
# ⑥ limit 分页
'''
分页公式:
原生sql:limit( (页数-1)*每页数据量, 每页数据量 )
orm里: offset( (页数-1)*每页数据量 ).limit(每页数据量)
'''
n = 1 # 页数
m = 2 # 每页数据量
r6 = session.query(User).offset((n-1)*m).limit(m).all()
for i in r6:
print('分页:', i.id, i.usernmae)
'''2.分组聚合group by'''
'''
select usernmae,count(1)
from user_info_detail
group by usernmae
having count(1) >= 2
'''
r7 = session.query(User.usernmae,func.count(User.id).label('num')).\
group_by( User.usernmae ).\
having( func.count(User.id) >= 2 ).\
all()
for i in r7:
print('分组聚合:',i.usernmae,i.num)
'''
func函数支持:max、min、sum、count、avg常用的聚合函数
用法: func.count(聚合的字段)
'''
'''3.多表关联查询join内连接/outerjoin左连接'''
r8 = session.query(User.id.label('user_id') , User.usernmae, Order.id.label('order_id'), Order.title ).\
join( Order, User.id == Order.userid ).\
order_by(User.id).\
all()
for i in r8:
print('多表关联内连接:',i.user_id,i.usernmae,i.order_id,i.title)
# outerjoin(谁放在里面,谁就是从表)
r8 = session.query(User.id.label('user_id') , User.usernmae, Order.id.label('order_id'), Order.title ).\
outerjoin( Order, User.id == Order.userid ).\
order_by(User.id).\
all()
for i in r8:
print('多表关联左连接:',i.user_id,i.usernmae,i.order_id,i.title)
'''
select ——> session.query( 表模型名 )
from 表1
join 表2 on 条件 ——> join(连接的表,条件) / outerjoin(连接的表,条件)
where 筛选条件 ——> filter(条件)
group by 分组字段 ——> group_by(分组的字段)
having 分组后过滤 ——> having(过滤的条件)
order by 排序 ——> order_by( 排序的字段.desc() )
limit 分页 ——> offset(偏移量).limit(数据量)
'''
n = 1 # 页数
m = 2 # 每页数据量
r9 = session.query(User.usernmae,Order.title,func.count(1).label('ordernum')).\
join(Order,User.id == Order.userid).\
filter(Order.title == '苹果').\
group_by(User.usernmae,Order.title).\
having(func.count(1) >= 1).\
order_by(func.count(1).desc()).\
offset((n-1)*m).limit(m).\
all()
for i in r9:
print('完整的查询:',i.usernmae,i.title,i.ordernum)
+180
View File
@@ -0,0 +1,180 @@
from datetime import datetime
from typing import List, Optional
from sqlalchemy import (
create_engine, String, Integer, ForeignKey, DateTime, select, func
)
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, relationship,
Session, selectinload
)
# ============================================================
# 1. 定义 Base 和模型
# ============================================================
class Base(DeclarativeBase):
"""所有模型的基类,SQLAlchemy 2.0 推荐写法"""
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
# 一对多:一个用户有多个文章
posts: Mapped[List["Post"]] = relationship(
back_populates="author",
cascade="all, delete-orphan", # 删除用户时级联删除文章
lazy="selectin" # 查询用户时自动预加载文章
)
def __repr__(self):
return f"<User(id={self.id}, name='{self.name}', email='{self.email}')>"
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
content: Mapped[Optional[str]] = mapped_column(String(1000))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
# 多对一:多篇文章属于一个用户
author: Mapped["User"] = relationship(back_populates="posts")
def __repr__(self):
return f"<Post(id={self.id}, title='{self.title}', user_id={self.user_id})>"
# ============================================================
# 2. 创建引擎和表
# ============================================================
engine = create_engine("sqlite:///users_posts.db", echo=False)
Base.metadata.create_all(engine)
# ============================================================
# 3. CRUD 操作
# ============================================================
def create_user(session: Session, name: str, email: str) -> User:
user = User(name=name, email=email)
session.add(user)
session.commit()
session.refresh(user) # 刷新以获取数据库生成的 id
return user
def create_post(session: Session, title: str, content: str, user_id: int) -> Post:
post = Post(title=title, content=content, user_id=user_id)
session.add(post)
session.commit()
session.refresh(post)
return post
def get_user_by_id(session: Session, user_id: int) -> Optional[User]:
return session.get(User, user_id)
def get_user_by_email(session: Session, email: str) -> Optional[User]:
stmt = select(User).where(User.email == email)
return session.execute(stmt).scalar_one_or_none()
def get_all_users(session: Session) -> List[User]:
stmt = select(User).order_by(User.id)
return list(session.execute(stmt).scalars().all())
def update_user_email(session: Session, user_id: int, new_email: str) -> bool:
user = session.get(User, user_id)
if not user:
return False
user.email = new_email
session.commit()
return True
def delete_user(session: Session, user_id: int) -> bool:
user = session.get(User, user_id)
if not user:
return False
session.delete(user) # cascade 会自动删除关联的 posts
session.commit()
return True
# ============================================================
# 4. 复杂查询
# ============================================================
def get_users_with_post_count(session: Session):
"""统计每个用户的文章数"""
stmt = (
select(User.name, func.count(Post.id).label("post_count"))
.outerjoin(Post, User.id == Post.user_id)
.group_by(User.id)
.order_by(func.count(Post.id).desc())
)
return session.execute(stmt).all()
def get_posts_by_user(session: Session, user_id: int) -> List[Post]:
"""查询某个用户的所有文章"""
stmt = select(Post).where(Post.user_id == user_id).order_by(Post.created_at.desc())
return list(session.execute(stmt).scalars().all())
# ============================================================
# 5. 演示
# ============================================================
def main():
with Session(engine) as session:
# 清理旧数据(演示用)
session.query(Post).delete()
session.query(User).delete()
session.commit()
# 创建用户
alice = create_user(session, "Alice", "alice@example.com")
bob = create_user(session, "Bob", "bob@example.com")
print("创建用户:", alice, bob)
# 创建文章
create_post(session, "Alice 的第一篇", "Hello world", alice.id)
create_post(session, "Alice 的第二篇", "SQLAlchemy 真香", alice.id)
create_post(session, "Bob 的独苗", "Python ORM", bob.id)
# 查询
print("\n所有用户:")
for u in get_all_users(session):
print(f" {u} -> 文章: {[p.title for p in u.posts]}")
print("\n用户文章数统计:")
for name, count in get_users_with_post_count(session):
print(f" {name}: {count} 篇")
# 更新
update_user_email(session, alice.id, "alice_new@example.com")
print("\n更新后:", get_user_by_id(session, alice.id))
# 删除(级联删除文章)
delete_user(session, bob.id)
print("\n删除 Bob 后,所有用户:")
for u in get_all_users(session):
print(f" {u}")
if __name__ == "__main__":
main()
Binary file not shown.
View File
+97
View File
@@ -0,0 +1,97 @@
class Field:
"""字段描述符:记录列名和类型"""
def __init__(self, column_type):
self.column_type = column_type
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class IntegerField(Field):
def __init__(self, primary_key=False):
super().__init__("INTEGER")
self.primary_key = primary_key
class StringField(Field):
def __init__(self, max_length=255):
super().__init__(f"VARCHAR({max_length})")
class ModelMeta(type):
"""元类:在类创建时收集字段,生成 SQL"""
def __new__(mcs, name, bases, namespace):
# 跳过基类 Model 本身
if name == "Model":
return super().__new__(mcs, name, bases, namespace)
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
# 把收集到的字段挂到类上
namespace["_fields"] = fields
namespace["_table"] = name.lower() + "s"
cls = super().__new__(mcs, name, bases, namespace)
return cls
class Model(metaclass=ModelMeta):
"""所有模型的基类"""
@classmethod
def create_table_sql(cls):
columns = []
for name, field in cls._fields.items():
col = f"{name} {field.column_type}"
if isinstance(field, IntegerField) and field.primary_key:
col += " PRIMARY KEY AUTOINCREMENT"
columns.append(col)
return f"CREATE TABLE IF NOT EXISTS {cls._table} ({', '.join(columns)});"
@classmethod
def insert_sql(cls, **kwargs):
keys = ", ".join(kwargs.keys())
values = ", ".join(
f"'{v}'" if isinstance(v, str) else str(v)
for v in kwargs.values()
)
return f"INSERT INTO {cls._table} ({keys}) VALUES ({values});"
# ===== 使用 =====
class User(Model):
id = IntegerField(primary_key=True)
name = StringField(50)
email = StringField(100)
class Post(Model):
id = IntegerField(primary_key=True)
title = StringField(200)
user_id = IntegerField()
print(User.create_table_sql())
# CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(50), email VARCHAR(100));
print(User.insert_sql(name="Alice", email="alice@example.com"))
# INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
print(Post.create_table_sql())
# CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(200), user_id INTEGER);
print(Post.insert_sql(title="orm is best", user_id=1))
print(Post.insert_sql(title="orm simple imp", user_id=1))
print(Post.insert_sql(title="mete class is the base", user_id=1))
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
from datetime import datetime
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = "John Doe"
signup_ts: datetime | None = None
friends: list[int] = []
external_data = {
"id": "123",
"signup_ts": "2017-06-01 12:22",
"friends": [1, "2", b"3"],
}
user = User(**external_data)
print(user)
# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]
print(user.id)
# > 123
print(type(user.friends[0]),type(user.friends[1]),type(user.friends[2]))
View File
+55
View File
@@ -0,0 +1,55 @@
class RouterMeta(type):
"""元类:自动收集被 @route 标记的方法"""
def __new__(mcs, name, bases, namespace):
routes = {}
for key, value in namespace.items():
if hasattr(value, "_route_path"):
routes[value._route_path] = (value._http_method, key)
namespace["_routes"] = routes
return super().__new__(mcs, name, bases, namespace)
def route(path, method="GET"):
"""装饰器:给方法打上路由标记"""
def decorator(func):
func._route_path = path
func._http_method = method
return func
return decorator
class Controller(metaclass=RouterMeta):
"""所有控制器的基类"""
@classmethod
def dispatch(cls, path, method):
if path not in cls._routes:
return 404, "Not Found"
http_method, func_name = cls._routes[path]
if http_method != method:
return 405, "Method Not Allowed"
return 200, getattr(cls(), func_name)()
# ===== 使用 =====
class UserController(Controller):
@route("/users", "GET")
def list_users(self):
return [{"id": 1, "name": "Alice"}]
@route("/users/create", "POST")
def create_user(self):
return {"status": "created"}
print(UserController._routes)
# {'/users': ('GET', 'list_users'), '/users/create': ('POST', 'create_user')}
print(UserController.dispatch("/users", "GET"))
# (200, [{'id': 1, 'name': 'Alice'}])
print(UserController.dispatch("/users", "POST"))
# (405, 'Method Not Allowed')
print(UserController.dispatch("/unknown", "GET"))
# (404, 'Not Found')
+24
View File
@@ -0,0 +1,24 @@
class Class:
name = 'Class'
def __init__(self, at):
self.attribute = at
def change(self, ano):
self.attribute = ano
self.name = ano
self.__class__.name = ano
def change_obj(self, ano):
self.attribute = ano
self.name = ano
clazz = Class('abc')
print(clazz.name)
# clazz.change('good')
# print(Class.name, clazz.name, clazz.attribute)
clazz.change_obj('good')
print(Class.name, clazz.name, clazz.attribute)
View File
+46
View File
@@ -0,0 +1,46 @@
# log.py
import logging
class Log:
# name, level no need to record
def __init__(self, name):
self.__log_name = name
self.__log_levlel = None
# deal duplicate handler
self.__handlers = []
# init default logger
self.set_console()
def set_console(self):
self.logger = logging.getLogger(self.__log_name)
# self.logger.setLevel(eval("logging." + level.upper()))
fh = logging.StreamHandler()
log_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'
,datefmt='%Y-%m-%d %H:%M:%S')
fh.setFormatter(log_format)
if fh not in self.__handlers:
self.logger.addHandler(fh)
self.__handlers.append(fh)
def set_file(self, file_name):
self.logger = logging.getLogger(self.__log_name)
# self.logger.setLevel(eval("logging." + level.upper()))
fh = logging.FileHandler(file_name,encoding='utf-8') # FileHandler 的默认模式是 'a',用 'a+' 在某些平台上可能导致文件指针行为异常。
log_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'
,datefmt='%Y-%m-%d %H:%M:%S')
fh.setFormatter(log_format)
if fh not in self.__handlers:
self.logger.addHandler(fh)
self.__handlers.append(fh)
def message(self,level,message):
# self.logger.log(eval("logging." + level.upper()),message) # not safe for eval
self.logger.log(getattr(logging,level.upper()),message)
l = Log('ai-model')
l.message("warning","test Log class to console")
# l.set_file('libplus.log')
# l.message("info","test info Log to file")
+4
View File
@@ -0,0 +1,4 @@
2026-09-17 08:43:50 - ai-model - INFO - ai-model - 0
2026-09-17 08:43:50 - ai-model - INFO - ai-model - 1
2026-09-17 08:51:39 - ai-model - INFO - ai-model - 0
2026-09-17 08:51:39 - ai-model - INFO - ai-model - 1
+44
View File
@@ -0,0 +1,44 @@
import logging,os
class Log:
def __init__(self, name, level="INFO"):
self.logger = logging.getLogger(name)
self.logger.setLevel(getattr(logging, level.upper()))
# fix duplicated name logger
if not self.logger.hasHandlers():
self.set_console()
def set_console(self):
fh = logging.StreamHandler()
fh.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
if not self.logger.hasHandlers():
self.logger.addHandler(fh)
def set_file(self, file_name):
# 检查是否已有 FileHandler 指向同一个文件
for h in self.logger.handlers:
if isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(file_name):
return # 已存在,直接返回,不重复添加
fh = logging.FileHandler(file_name, encoding='utf-8')
fh.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
print(f'{self.logger.hasHandlers()=}')
self.logger.addHandler(fh)
def message(self, level, message):
self.logger.log(getattr(logging, level.upper()), message)
# single instance
l = Log('ai-model')
l1 = Log('ai-model')
l.set_file("logplus.log")
l1.set_file("logplus.log")
l.message("info", "ai-model - 0")
l1.message("info", "ai-model - 1")
+53
View File
@@ -0,0 +1,53 @@
import logging
import os
class Log:
_added = {} # 类级别:记录每个 logger 已添加的 Handler 标识
def __init__(self, name, level="INFO"):
self.logger = logging.getLogger(name)
self.logger.setLevel(getattr(logging, level.upper()))
self.logger.propagate = False # 避免向 root 传播导致重复
if not self.logger.hasHandlers():
self.set_console()
def _handler_exists(self, key):
return key in Log._added.get(self.logger.name, set())
def _mark_handler(self, key):
Log._added.setdefault(self.logger.name, set()).add(key)
def set_console(self):
if self._handler_exists("console"):
return
fh = logging.StreamHandler()
fh.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
self.logger.addHandler(fh)
self._mark_handler("console")
def set_file(self, file_name):
key = f"file:{os.path.abspath(file_name)}"
if self._handler_exists(key):
return
fh = logging.FileHandler(file_name, encoding='utf-8')
fh.setFormatter(logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
self.logger.addHandler(fh)
self._mark_handler(key)
def message(self, level, message):
self.logger.log(getattr(logging, level.upper()), message)
l = Log('ai-model')
l1 = Log('ai-model')
l.set_file("logplus.log")
l1.set_file("logplus.log")
l.message("info", "ai-model - 0")
l1.message("info", "ai-model - 1")
+388
View File
@@ -0,0 +1,388 @@
2026-09-18 16:24:53 - ai-model - INFO - ai-model - 0
2026-09-18 16:24:53 - ai-model - INFO - ai-model - 1
2026-09-18 16:24:53 - ai-model - INFO - ai-model - 0
2026-09-18 16:24:53 - ai-model - INFO - ai-model - 1
2026-09-18 16:47:18 - ai-model - INFO - ai-model - 0
2026-09-18 16:47:18 - ai-model - INFO - ai-model - 1
2026-09-18 16:47:19 - ai-model - INFO - ai-model - 0
2026-09-18 16:47:19 - ai-model - INFO - ai-model - 1
2026-09-18 16:49:01 - ai-model - INFO - ai-model - 0
2026-09-18 16:49:01 - ai-model - INFO - ai-model - 1
2026-09-18 16:49:03 - ai-model - INFO - ai-model - 0
2026-09-18 16:49:03 - ai-model - INFO - ai-model - 1
2026-09-18 16:50:05 - ai-model - INFO - ai-model - 0
2026-09-18 16:50:05 - ai-model - INFO - ai-model - 1
2026-09-18 16:50:06 - ai-model - INFO - ai-model - 0
2026-09-18 16:50:06 - ai-model - INFO - ai-model - 1
2026-09-18 16:50:08 - ai-model - INFO - ai-model - 0
2026-09-18 16:50:08 - ai-model - INFO - ai-model - 1
2026-09-18 16:53:40 - ai-model - INFO - ai-model - 0
2026-09-18 16:53:40 - ai-model - INFO - ai-model - 1
2026-09-18 16:53:41 - ai-model - INFO - ai-model - 0
2026-09-18 16:53:41 - ai-model - INFO - ai-model - 1
2026-09-18 16:53:57 - ai-model - INFO - ai-model - 0
2026-09-18 16:53:57 - ai-model - INFO - ai-model - 1
2026-09-18 16:53:58 - ai-model - INFO - ai-model - 0
2026-09-18 16:53:58 - ai-model - INFO - ai-model - 1
2026-09-18 16:57:05 - ai-model - INFO - ai-model - 0
2026-09-18 16:57:05 - ai-model - INFO - ai-model - 1
2026-09-18 16:57:06 - ai-model - INFO - ai-model - 0
2026-09-18 16:57:06 - ai-model - INFO - ai-model - 1
2026-09-18 16:58:19 - ai-model - INFO - ai-model - 0
2026-09-18 16:58:19 - ai-model - INFO - ai-model - 1
2026-09-18 16:58:20 - ai-model - INFO - ai-model - 0
2026-09-18 16:58:20 - ai-model - INFO - ai-model - 1
2026-09-18 17:00:58 - ai-model - INFO - ai-model - 0
2026-09-18 17:00:58 - ai-model - INFO - ai-model - 1
2026-09-18 17:00:59 - ai-model - INFO - ai-model - 0
2026-09-18 17:00:59 - ai-model - INFO - ai-model - 1
2026-09-18 17:01:36 - ai-model - INFO - ai-model - 0
2026-09-18 17:01:36 - ai-model - INFO - ai-model - 1
2026-09-18 17:01:37 - ai-model - INFO - ai-model - 0
2026-09-18 17:01:37 - ai-model - INFO - ai-model - 1
2026-09-18 17:05:30 - ai-model - INFO - ai-model - 0
2026-09-18 17:05:30 - ai-model - INFO - ai-model - 1
2026-09-18 17:05:31 - ai-model - INFO - ai-model - 0
2026-09-18 17:05:31 - ai-model - INFO - ai-model - 1
2026-09-18 17:08:13 - ai-model - INFO - ai-model - 0
2026-09-18 17:08:13 - ai-model - INFO - ai-model - 1
2026-09-18 17:08:46 - ai-model - INFO - ai-model - 0
2026-09-18 17:08:46 - ai-model - INFO - ai-model - 1
2026-09-18 17:08:47 - ai-model - INFO - ai-model - 0
2026-09-18 17:08:47 - ai-model - INFO - ai-model - 1
2026-09-18 17:11:19 - ai-model - INFO - ai-model - 0
2026-09-18 17:11:19 - ai-model - INFO - ai-model - 1
2026-09-18 17:38:34 - ai-model - INFO - ai-model - 0
2026-09-18 17:38:34 - ai-model - INFO - ai-model - 1
2026-09-18 17:38:46 - ai-model - INFO - ai-model - 0
2026-09-18 17:38:46 - ai-model - INFO - ai-model - 1
2026-09-18 17:39:43 - ai-model - INFO - ai-model - 0
2026-09-18 17:39:43 - ai-model - INFO - ai-model - 1
2026-09-18 17:39:44 - ai-model - INFO - ai-model - 0
2026-09-18 17:39:44 - ai-model - INFO - ai-model - 1
2026-09-18 17:39:50 - ai-model - INFO - ai-model - 0
2026-09-18 17:39:50 - ai-model - INFO - ai-model - 1
2026-09-18 17:39:51 - ai-model - INFO - ai-model - 0
2026-09-18 17:39:51 - ai-model - INFO - ai-model - 1
2026-09-18 17:39:59 - ai-model - INFO - ai-model - 0
2026-09-18 17:39:59 - ai-model - INFO - ai-model - 1
2026-09-18 17:40:00 - ai-model - INFO - ai-model - 0
2026-09-18 17:40:00 - ai-model - INFO - ai-model - 1
2026-09-18 17:52:13 - ai-model - INFO - ai-model - 0
2026-09-18 17:52:13 - ai-model - INFO - ai-model - 1
2026-09-18 18:23:08 - ai-model - INFO - ai-model - 0
2026-09-18 18:23:08 - ai-model - INFO - ai-model - 1
2026-09-18 18:23:19 - ai-model - INFO - ai-model - 0
2026-09-18 18:23:19 - ai-model - INFO - ai-model - 1
2026-09-18 18:23:40 - ai-model - INFO - ai-model - 0
2026-09-18 18:23:40 - ai-model - INFO - ai-model - 1
2026-09-18 18:24:49 - ai-model - INFO - ai-model - 0
2026-09-18 18:24:49 - ai-model - INFO - ai-model - 1
2026-09-18 18:37:13 - ai-model - INFO - ai-model - 0
2026-09-18 18:37:13 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:18 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:18 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:19 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:19 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:37 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:37 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:38 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:38 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:40 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:40 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:41 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:41 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:54 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:54 - ai-model - INFO - ai-model - 1
2026-09-18 18:38:55 - ai-model - INFO - ai-model - 0
2026-09-18 18:38:55 - ai-model - INFO - ai-model - 1
2026-09-18 18:39:06 - ai-model - INFO - ai-model - 0
2026-09-18 18:39:06 - ai-model - INFO - ai-model - 1
2026-09-18 18:39:35 - ai-model - INFO - ai-model - 0
2026-09-18 18:39:35 - ai-model - INFO - ai-model - 1
2026-09-18 18:39:39 - ai-model - INFO - ai-model - 0
2026-09-18 18:39:39 - ai-model - INFO - ai-model - 1
2026-09-18 18:39:40 - ai-model - INFO - ai-model - 0
2026-09-18 18:39:40 - ai-model - INFO - ai-model - 1
2026-09-18 18:40:21 - ai-model - INFO - ai-model - 0
2026-09-18 18:40:21 - ai-model - INFO - ai-model - 1
2026-09-18 18:40:21 - ai-model - INFO - ai-model - 0
2026-09-18 18:40:21 - ai-model - INFO - ai-model - 1
2026-09-18 18:40:28 - ai-model - INFO - ai-model - 0
2026-09-18 18:40:28 - ai-model - INFO - ai-model - 1
2026-09-18 18:40:29 - ai-model - INFO - ai-model - 0
2026-09-18 18:40:29 - ai-model - INFO - ai-model - 1
2026-09-18 18:41:07 - ai-model - INFO - ai-model - 0
2026-09-18 18:41:07 - ai-model - INFO - ai-model - 1
2026-09-18 18:41:07 - ai-model - INFO - ai-model - 0
2026-09-18 18:41:07 - ai-model - INFO - ai-model - 1
2026-09-18 18:41:41 - ai-model - INFO - ai-model - 0
2026-09-18 18:41:41 - ai-model - INFO - ai-model - 1
2026-09-18 18:42:10 - ai-model - INFO - ai-model - 0
2026-09-18 18:42:10 - ai-model - INFO - ai-model - 1
2026-09-18 19:37:06 - ai-model - INFO - ai-model - 0
2026-09-18 19:37:06 - ai-model - INFO - ai-model - 1
2026-09-18 19:37:45 - ai-model - INFO - ai-model - 0
2026-09-18 19:37:45 - ai-model - INFO - ai-model - 1
2026-09-18 19:37:46 - ai-model - INFO - ai-model - 0
2026-09-18 19:37:46 - ai-model - INFO - ai-model - 1
2026-09-18 19:38:18 - ai-model - INFO - ai-model - 0
2026-09-18 19:38:18 - ai-model - INFO - ai-model - 1
2026-09-18 19:38:54 - ai-model - INFO - ai-model - 0
2026-09-18 19:38:54 - ai-model - INFO - ai-model - 1
2026-09-18 19:38:55 - ai-model - INFO - ai-model - 0
2026-09-18 19:38:55 - ai-model - INFO - ai-model - 1
2026-09-18 19:39:12 - ai-model - INFO - ai-model - 0
2026-09-18 19:39:12 - ai-model - INFO - ai-model - 1
2026-09-18 19:39:13 - ai-model - INFO - ai-model - 0
2026-09-18 19:39:13 - ai-model - INFO - ai-model - 1
2026-09-18 19:39:51 - ai-model - INFO - ai-model - 0
2026-09-18 19:39:51 - ai-model - INFO - ai-model - 1
2026-09-18 19:39:52 - ai-model - INFO - ai-model - 0
2026-09-18 19:39:52 - ai-model - INFO - ai-model - 1
2026-09-18 20:06:50 - ai-model - INFO - ai-model - 0
2026-09-18 20:06:50 - ai-model - INFO - ai-model - 1
2026-09-18 20:06:51 - ai-model - INFO - ai-model - 0
2026-09-18 20:06:51 - ai-model - INFO - ai-model - 1
2026-09-18 21:35:37 - ai-model - INFO - ai-model - 0
2026-09-18 21:35:37 - ai-model - INFO - ai-model - 1
2026-09-18 21:35:40 - ai-model - INFO - ai-model - 0
2026-09-18 21:35:40 - ai-model - INFO - ai-model - 1
2026-09-18 22:18:23 - ai-model - INFO - ai-model - 0
2026-09-18 22:18:23 - ai-model - INFO - ai-model - 1
2026-09-18 22:20:42 - ai-model - INFO - ai-model - 0
2026-09-18 22:20:42 - ai-model - INFO - ai-model - 1
2026-09-18 22:20:43 - ai-model - INFO - ai-model - 0
2026-09-18 22:20:43 - ai-model - INFO - ai-model - 1
2026-09-18 22:42:41 - ai-model - INFO - ai-model - 0
2026-09-18 22:42:41 - ai-model - INFO - ai-model - 1
2026-09-18 22:42:58 - ai-model - INFO - ai-model - 0
2026-09-18 22:42:58 - ai-model - INFO - ai-model - 1
2026-09-18 22:43:03 - ai-model - INFO - ai-model - 0
2026-09-18 22:43:03 - ai-model - INFO - ai-model - 1
2026-09-18 23:17:24 - ai-model - INFO - ai-model - 0
2026-09-18 23:17:24 - ai-model - INFO - ai-model - 1
2026-09-18 23:17:41 - ai-model - INFO - ai-model - 0
2026-09-18 23:17:41 - ai-model - INFO - ai-model - 1
2026-09-18 23:17:56 - ai-model - INFO - ai-model - 0
2026-09-18 23:17:56 - ai-model - INFO - ai-model - 1
2026-09-18 23:17:57 - ai-model - INFO - ai-model - 0
2026-09-18 23:17:57 - ai-model - INFO - ai-model - 1
2026-09-18 23:18:43 - ai-model - INFO - ai-model - 0
2026-09-18 23:18:43 - ai-model - INFO - ai-model - 1
2026-09-18 23:19:18 - ai-model - INFO - ai-model - 0
2026-09-18 23:19:18 - ai-model - INFO - ai-model - 1
2026-09-18 23:21:05 - ai-model - INFO - ai-model - 0
2026-09-18 23:21:05 - ai-model - INFO - ai-model - 1
2026-09-18 23:21:25 - ai-model - INFO - ai-model - 0
2026-09-18 23:21:25 - ai-model - INFO - ai-model - 1
2026-09-18 23:24:03 - ai-model - INFO - ai-model - 0
2026-09-18 23:24:03 - ai-model - INFO - ai-model - 1
2026-09-18 23:24:04 - ai-model - INFO - ai-model - 0
2026-09-18 23:24:04 - ai-model - INFO - ai-model - 1
2026-09-18 23:27:30 - ai-model - INFO - ai-model - 0
2026-09-18 23:27:30 - ai-model - INFO - ai-model - 1
2026-09-18 23:27:31 - ai-model - INFO - ai-model - 0
2026-09-18 23:27:31 - ai-model - INFO - ai-model - 1
2026-09-18 23:28:16 - ai-model - INFO - ai-model - 0
2026-09-18 23:28:16 - ai-model - INFO - ai-model - 1
2026-09-18 23:28:17 - ai-model - INFO - ai-model - 0
2026-09-18 23:28:17 - ai-model - INFO - ai-model - 1
2026-09-18 23:30:44 - ai-model - INFO - ai-model - 0
2026-09-18 23:30:44 - ai-model - INFO - ai-model - 1
2026-09-18 23:30:46 - ai-model - INFO - ai-model - 0
2026-09-18 23:30:46 - ai-model - INFO - ai-model - 1
2026-09-18 23:31:47 - ai-model - INFO - ai-model - 0
2026-09-18 23:31:47 - ai-model - INFO - ai-model - 1
2026-09-18 23:32:17 - ai-model - INFO - ai-model - 0
2026-09-18 23:32:17 - ai-model - INFO - ai-model - 1
2026-09-18 23:32:18 - ai-model - INFO - ai-model - 0
2026-09-18 23:32:18 - ai-model - INFO - ai-model - 1
2026-09-18 23:32:52 - ai-model - INFO - ai-model - 0
2026-09-18 23:32:52 - ai-model - INFO - ai-model - 1
2026-09-18 23:32:53 - ai-model - INFO - ai-model - 0
2026-09-18 23:32:53 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:22 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:22 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:23 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:23 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:36 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:36 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:37 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:37 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:42 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:42 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:43 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:43 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:47 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:47 - ai-model - INFO - ai-model - 1
2026-09-18 23:33:48 - ai-model - INFO - ai-model - 0
2026-09-18 23:33:48 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:02 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:02 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:03 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:03 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:14 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:14 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:15 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:15 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:29 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:29 - ai-model - INFO - ai-model - 1
2026-09-18 23:34:30 - ai-model - INFO - ai-model - 0
2026-09-18 23:34:30 - ai-model - INFO - ai-model - 1
2026-09-18 23:35:11 - ai-model - INFO - ai-model - 0
2026-09-18 23:35:11 - ai-model - INFO - ai-model - 1
2026-09-18 23:35:18 - ai-model - INFO - ai-model - 0
2026-09-18 23:35:18 - ai-model - INFO - ai-model - 1
2026-09-18 23:37:55 - ai-model - INFO - ai-model - 0
2026-09-18 23:37:55 - ai-model - INFO - ai-model - 1
2026-09-18 23:37:56 - ai-model - INFO - ai-model - 0
2026-09-18 23:37:56 - ai-model - INFO - ai-model - 1
2026-09-18 23:43:44 - ai-model - INFO - ai-model - 0
2026-09-18 23:43:44 - ai-model - INFO - ai-model - 1
2026-09-18 23:43:45 - ai-model - INFO - ai-model - 0
2026-09-18 23:43:45 - ai-model - INFO - ai-model - 1
2026-09-18 23:43:51 - ai-model - INFO - ai-model - 0
2026-09-18 23:43:51 - ai-model - INFO - ai-model - 1
2026-09-18 23:43:52 - ai-model - INFO - ai-model - 0
2026-09-18 23:43:52 - ai-model - INFO - ai-model - 1
2026-09-18 23:44:26 - ai-model - INFO - ai-model - 0
2026-09-18 23:44:26 - ai-model - INFO - ai-model - 1
2026-09-18 23:44:27 - ai-model - INFO - ai-model - 0
2026-09-18 23:44:27 - ai-model - INFO - ai-model - 1
2026-09-18 23:46:53 - ai-model - INFO - ai-model - 0
2026-09-18 23:46:53 - ai-model - INFO - ai-model - 1
2026-09-18 23:46:54 - ai-model - INFO - ai-model - 0
2026-09-18 23:46:54 - ai-model - INFO - ai-model - 1
2026-09-18 23:51:35 - ai-model - INFO - ai-model - 0
2026-09-18 23:51:35 - ai-model - INFO - ai-model - 1
2026-09-18 23:51:36 - ai-model - INFO - ai-model - 0
2026-09-18 23:51:36 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:31 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:31 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:32 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:32 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:39 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:39 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:41 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:41 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:43 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:43 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:44 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:44 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:47 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:47 - ai-model - INFO - ai-model - 1
2026-09-18 23:54:48 - ai-model - INFO - ai-model - 0
2026-09-18 23:54:48 - ai-model - INFO - ai-model - 1
2026-09-18 23:55:34 - ai-model - INFO - ai-model - 0
2026-09-18 23:55:34 - ai-model - INFO - ai-model - 1
2026-09-18 23:55:35 - ai-model - INFO - ai-model - 0
2026-09-18 23:55:35 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:07 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:07 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:08 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:08 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:27 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:27 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:28 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:28 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:50 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:50 - ai-model - INFO - ai-model - 1
2026-09-18 23:57:51 - ai-model - INFO - ai-model - 0
2026-09-18 23:57:51 - ai-model - INFO - ai-model - 1
2026-09-18 23:58:23 - ai-model - INFO - ai-model - 0
2026-09-18 23:58:23 - ai-model - INFO - ai-model - 1
2026-09-18 23:58:24 - ai-model - INFO - ai-model - 0
2026-09-18 23:58:24 - ai-model - INFO - ai-model - 1
2026-09-18 23:59:39 - ai-model - INFO - ai-model - 0
2026-09-18 23:59:39 - ai-model - INFO - ai-model - 1
2026-09-19 00:03:24 - ai-model - INFO - ai-model - 0
2026-09-19 00:03:24 - ai-model - INFO - ai-model - 1
2026-09-19 00:03:25 - ai-model - INFO - ai-model - 0
2026-09-19 00:03:25 - ai-model - INFO - ai-model - 1
2026-09-19 00:04:13 - ai-model - INFO - ai-model - 0
2026-09-19 00:04:13 - ai-model - INFO - ai-model - 1
2026-09-19 00:04:14 - ai-model - INFO - ai-model - 0
2026-09-19 00:04:14 - ai-model - INFO - ai-model - 1
2026-09-19 09:52:47 - ai-model - INFO - ai-model - 0
2026-09-19 09:52:47 - ai-model - INFO - ai-model - 1
2026-09-19 09:56:37 - ai-model - INFO - ai-model - 0
2026-09-19 09:56:37 - ai-model - INFO - ai-model - 1
2026-09-19 09:56:38 - ai-model - INFO - ai-model - 0
2026-09-19 09:56:38 - ai-model - INFO - ai-model - 1
2026-09-19 09:56:55 - ai-model - INFO - ai-model - 0
2026-09-19 09:56:55 - ai-model - INFO - ai-model - 1
2026-09-19 09:58:10 - ai-model - INFO - ai-model - 0
2026-09-19 09:58:10 - ai-model - INFO - ai-model - 1
2026-09-19 09:58:11 - ai-model - INFO - ai-model - 0
2026-09-19 09:58:11 - ai-model - INFO - ai-model - 1
2026-09-19 09:58:47 - ai-model - INFO - ai-model - 0
2026-09-19 09:58:47 - ai-model - INFO - ai-model - 1
2026-09-19 09:59:15 - ai-model - INFO - ai-model - 0
2026-09-19 09:59:15 - ai-model - INFO - ai-model - 1
2026-09-19 09:59:16 - ai-model - INFO - ai-model - 0
2026-09-19 09:59:16 - ai-model - INFO - ai-model - 1
2026-09-19 10:01:22 - ai-model - INFO - ai-model - 0
2026-09-19 10:01:22 - ai-model - INFO - ai-model - 1
2026-09-19 10:01:34 - ai-model - INFO - ai-model - 0
2026-09-19 10:01:34 - ai-model - INFO - ai-model - 1
2026-09-19 10:01:56 - ai-model - INFO - ai-model - 0
2026-09-19 10:01:56 - ai-model - INFO - ai-model - 1
2026-09-19 10:01:57 - ai-model - INFO - ai-model - 0
2026-09-19 10:01:57 - ai-model - INFO - ai-model - 1
2026-09-19 10:05:20 - ai-model - INFO - ai-model - 0
2026-09-19 10:05:20 - ai-model - INFO - ai-model - 1
2026-09-19 10:05:21 - ai-model - INFO - ai-model - 0
2026-09-19 10:05:21 - ai-model - INFO - ai-model - 1
2026-09-19 10:06:46 - ai-model - INFO - ai-model - 0
2026-09-19 10:06:46 - ai-model - INFO - ai-model - 1
2026-09-19 10:08:09 - ai-model - INFO - ai-model - 0
2026-09-19 10:08:09 - ai-model - INFO - ai-model - 1
2026-09-19 10:08:10 - ai-model - INFO - ai-model - 0
2026-09-19 10:08:10 - ai-model - INFO - ai-model - 1
2026-09-19 10:08:45 - ai-model - INFO - ai-model - 0
2026-09-19 10:08:45 - ai-model - INFO - ai-model - 1
2026-09-19 10:08:46 - ai-model - INFO - ai-model - 0
2026-09-19 10:08:46 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:05 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:05 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:08 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:08 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:19 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:19 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:21 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:21 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:35 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:35 - ai-model - INFO - ai-model - 1
2026-09-19 10:09:36 - ai-model - INFO - ai-model - 0
2026-09-19 10:09:36 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:04 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:04 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:08 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:08 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:09 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:09 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:31 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:31 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:57 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:57 - ai-model - INFO - ai-model - 1
2026-09-19 10:10:58 - ai-model - INFO - ai-model - 0
2026-09-19 10:10:58 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:32 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:32 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:33 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:33 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:35 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:35 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:36 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:36 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:45 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:45 - ai-model - INFO - ai-model - 1
2026-09-19 10:11:46 - ai-model - INFO - ai-model - 0
2026-09-19 10:11:46 - ai-model - INFO - ai-model - 1
2026-09-19 10:12:16 - ai-model - INFO - ai-model - 0
2026-09-19 10:12:16 - ai-model - INFO - ai-model - 1
2026-09-19 10:13:19 - ai-model - INFO - ai-model - 0
2026-09-19 10:13:19 - ai-model - INFO - ai-model - 1
2026-09-19 10:13:20 - ai-model - INFO - ai-model - 0
2026-09-19 10:13:20 - ai-model - INFO - ai-model - 1
+85
View File
@@ -0,0 +1,85 @@
from fastapi import FastAPI,Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
from router.features import form, method, file, validation, mimetype, path_query_variables, json
from router.dbopt import mysql,order
from router.workhome import wk_0915,wk_0916,wk_0917
app = FastAPI(
swagger_ui_parameters={
"docExpansion": "none", # 完全折叠,所有 Tags 默认收起
"defaultModelsExpandDepth": -1 # 顺便把底部的 Schemas 模型也收起来
},
title="fastapi tutor - gk"
)
# request response model
app.include_router(mimetype.route)
app.include_router(method.route)
app.include_router(path_query_variables.route)
app.include_router(validation.route)
app.include_router(form.route)
app.include_router(file.route)
app.include_router(json.route)
app.include_router(mysql.route)
app.include_router(order.route)
# homeworks...
app.include_router(wk_0915.route)
app.include_router(wk_0916.route)
app.include_router(wk_0917.route)
# for r in app.routes:
# print(r.path, r.methods, r.name)
app.mount(path="/static",app=StaticFiles(directory="/home/geeker/Pictures"),name="sn")
@app.get("/proxy")
def get(request: Request):
path = request.url_for('sn',path = 'head.png')
return path
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源,开发阶段用
# allow_origins=["null","http://localhost:8000"], # 允许所有来源,开发阶段用
allow_credentials=True, # 允许携带 Cookie
allow_methods=["*"], # 允许所有 HTTP 方法
allow_headers=["*"], # 允许所有请求头
)
from middle import middles
app.middleware("http")(middles.black)
app.middleware("http")(middles.time_)
# black_list = ['192.168.1.40','192.168.1.94'] # '127.0.0.1','192.168.1.26‘
# @app.middleware("http",)
# async def middleware(req:Request, next):
# if req.client.host in black_list:
# return Response("you are limited to access!")
# res = await next(req)
# # print('exite http',req.client.host)
# return res
#
# @app.middleware("https")
# async def middleware(req:Request, next):
# print('enter https',time.time())
# res = await next(req)
# print('exite https',time.time())
# return res
#
# @app.middleware("websocket")
# async def middleware(req:Request, next):
# print('enter websocket',req.client.host)
# res = await next(req)
# print('exite websocket',req.client.host)
# return res
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app",port=9090)
View File
+19
View File
@@ -0,0 +1,19 @@
from fastapi import Request,Response
import time
black_list = ['192.168.1.40','192.168.1.94'] # '127.0.0.1','192.168.1.26‘
# @app.middleware("http",)
async def black(req:Request, next):
if req.client.host in black_list:
return Response("you are limited to access!")
res = await next(req)
# print('exite http',req.client.host)
return res
# @app.middleware("https")
async def time_(req:Request, next):
print('enter https',time.time())
res = await next(req)
print('exite https',time.time())
return res
View File
+56
View File
@@ -0,0 +1,56 @@
from pydantic import BaseModel
from sqlalchemy.orm import declarative_base,sessionmaker
from sqlalchemy import Column, Integer,String,DATETIME,create_engine
from datetime import datetime
db_url = "mysql+pymysql://geeker:geeker@127.0.0.1:3306/fastapi_tutor?charset=utf8mb4"
engine = create_engine(db_url)
def get_session():
Session = sessionmaker(bind=engine
, autoflush=False
, autocommit=False
)
yield Session()
# 定义创建订单的请求体模型
class OrderRequest(BaseModel):
id:int|None = None
title:str|None = None
userid:int|None =None
class OrderResponse(BaseModel):
id:int|None = None
title:str|None = None
userid:int|None =None
create_date:datetime|None = datetime.now()
update_date:datetime|None = datetime.now()
status_code:str|None = 200
detail:str|None = 'succeed!'
Base = declarative_base() # 执行函数,返回一个基类
class Order(Base): # python里的表名字
__tablename__ = 'order_info_detail' # 实际数据库的表名字
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='订单编号,自增主键' # 注释
)
title = Column( String(50) # 对应数据库的varchar类型
,nullable=False # 不允许为null
)
userid = Column( Integer
# ,ForeignKey('user_info_detail.id')
,nullable=False ) # 外键约束
create_date = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_date = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
Order.__table__.create(engine,checkfirst = True)
+9
View File
@@ -0,0 +1,9 @@
[project]
name = "fastapi-tutor"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]>=0.141.1",
"pymysql>=1.2.3",
"sqlalchemy>=2.0.54",
]
View File
View File
+73
View File
@@ -0,0 +1,73 @@
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer,String,DATETIME,DECIMAL
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime
from typing import Annotated
from fastapi import Depends, FastAPI,APIRouter,Form,File,UploadFile
from fastapi.security import OAuth2PasswordRequestForm
route = APIRouter(prefix="/mysql",tags=["mysql"])
db_url = "mysql+pymysql://geeker:geeker@localhost:3306/fastapi_tutor?charset=utf8mb4"
engine = create_engine(db_url, pool_size= 50, echo=True)
Session = sessionmaker( bind=engine
,autoflush=False # 是否开启自动刷新python新增的数据到实际的数据库内存缓存中
,autocommit = False # 不自动提交数据,这个是真正提交到数据库磁盘永久生效的 -> 2.0 不支持True自动提交
)
session = Session() # 实例化一个数据库操作会话
Base = declarative_base() # 获取基类!
class Order(Base): # python里的表名字
__tablename__ = 'order_info_detail' # 实际数据库的表名字
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='订单编号,自增主键' # 注释
)
title = Column( String(50) # 对应数据库的varchar类型
,nullable=False # 不允许为null
)
price = Column(DECIMAL)
userid = Column( Integer
# ,ForeignKey('user_info_detail.id')
,nullable=False ) # 外键约束
create_date = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_date = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
class User(Base): # python里的表名字
__tablename__ = 'user_info_detail' # 实际数据库的表名字
id = Column( Integer # 声明字段的数据类型
, primary_key=True # 声明是主键
, autoincrement=True # 声明是自增主键
, comment='用户编号,自增主键' # 注释
)
username = Column( String(50) # 对应数据库的varchar类型
,nullable=False # 不允许为null
,comment='用户名'
)
create_date = Column(DATETIME
, default=datetime.now # 默认值是当前时间
)
update_date = Column(DATETIME
, default=datetime.now # 首次创建的时间跟更新时间一致
, onupdate=datetime.now
)
@route.get("/user")
def user_detail_info():
return session.query(User).all()
@route.post("/user")
def user_detail_info(username:str):
session.add(User(username=username))
return session.query(User).all()
+51
View File
@@ -0,0 +1,51 @@
# 编写更订单相关的增删改查接口,结合数据库
from fastapi import FastAPI, APIRouter, Depends
from sqlalchemy import *
from sqlalchemy.orm import declarative_base,sessionmaker
from datetime import datetime
from pydantic import BaseModel
from model.order import OrderRequest,Order,get_session,OrderResponse
route = APIRouter(prefix="/order",tags=["orders"])
# 模糊查询
@route.get('/',response_model=list[OrderResponse])
def orders(id:int=None, user_id:int=None, title:str=None,session=Depends(get_session)):
q = session.query(Order)
if id: q = q.filter( Order.id == id )
if user_id: q = q.filter( Order.userid == userid )
if title: q = q.where(Order.title.like('%'+title+'%'))
r = q.all()
session.close()
return r
@route.get('/{id}',response_model=OrderResponse)
def orders(id:int):
session = Session()
r = session.query(Order).filter( Order.id == id ).all()
session.close()
return [ {"order_id":i.id,'order_title':i.title,"user_id":i.userid,"create_date":i.create_date,"update_date":i.update_date} for i in r]
@route.delete('/',response_model=OrderResponse)
def orders(session=Depends(get_session)):
return '删除订单'
@route.post('/',response_model=OrderResponse)
def orders(order:OrderRequest,session=Depends(get_session)):
d = order.model_dump() # d = {"title":xxx,"userid":xxx}
o1 = Order( **d )
session.add(o1)
session.commit()
session.close()
return {'code':200,'detail':'添加成功!'}
@route.put('/{id}',response_model=OrderResponse)
def orders(order:OrderRequest,id:int,session=Depends(get_session)):
session.query( Order ).filter( Order.id == id ).update( order.model_dump(exclude_unset=True) )
session.commit()
res = session.query(Order).filter(Order.id == id).first()
session.close()
return res
View File
+23
View File
@@ -0,0 +1,23 @@
from typing import Annotated
from fastapi import Depends, FastAPI,APIRouter,Form,File,UploadFile
from fastapi.security import OAuth2PasswordRequestForm
route = APIRouter(prefix="/file",tags=["file"])
@route.post("/bytes")
def bytes(file:bytes = File()):
print("bytes:\t" + str(file))
print("str:\t" + file.decode("utf-8"))
return file.decode("utf-8")
@route.post("/uploadfile")
async def uploadfile(file:UploadFile):
data = await file.read()
print("bytes:\t" + str(data))
print("str:\t" + data.decode("utf-8"))
return data.decode("utf-8")
# text read/write
+46
View File
@@ -0,0 +1,46 @@
from typing import Annotated
from fastapi import Depends, FastAPI,APIRouter,Form,Request,Response
from fastapi.security import OAuth2PasswordRequestForm
route = APIRouter(prefix="/form",tags=["form"])
@route.post("/token")
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
# form_data 是解析后的表单对象
# form_data.username → OAuth2 规范要求字段名必须是 "username"
# form_data.password → OAuth2 规范要求字段名必须是 "password"
# form_data.grant_type → 密码流程中必须是 "password"
# form_data.scopes → 空格分隔的权限列表
# form_data.client_id / client_secret → 如果客户端选择 client_secret_post
return {
"username": form_data.username,
"password":form_data.password,
"client_id": form_data.client_id,
"client_secret":form_data.client_secret,
"grant_type":form_data.grant_type,
"scopes":form_data.scopes,
}
@route.post("/form")
def login(username:str=Form(),password:str=Form()):
# form_data 是解nt_id / client_secret → 如果客户端选择 client_secret_post
return {
"username": username,
"password":password,
}
@route.post("/query")
def login(username:str,password:str):
# form_data 是解nt_id / client_secret → 如果客户端选择 client_secret_post
return {
"username": username,
"password":password,
}
@route.post("/raw")
async def upload(request: Request):
raw = await request.body() # bytes
text = raw.decode("utf-8") # 明确解码
return text
+10
View File
@@ -0,0 +1,10 @@
from typing import Annotated
from fastapi import Depends, FastAPI,APIRouter,Form,File,UploadFile
from fastapi.security import OAuth2PasswordRequestForm
route = APIRouter(prefix="/json",tags=["json"])
@route.get("/")
def json():
return [1,"str",[1,2,3],{1,2,3},{"a":1,"b":2}]
+171
View File
@@ -0,0 +1,171 @@
import fastapi
from fastapi import APIRouter,Query,Path,Header,Cookie,File,UploadFile,Form,Request,Response,Depends
from pydantic import BaseModel,Field
from typing import Union,Optional,Literal,Annotated
route = APIRouter(prefix="/methods",tags=["methods"])
'''
Filed & Query : 做param validate!
'''
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
class StuUpdate(BaseModel):
id : int|None = None
name:str|None = None
age:int = None
sex:Literal['男','女']|None = None
address:str|None = None
@route.put('/model_dump/{sid}')
def model_dump(sid:int,s:StuUpdate):
d = s.model_dump(exclude_unset=True)
return d
@route.post("/model_dump/")
async def create_item(item: Item):
item_dict = item.model_dump()
if item.tax is not None:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
@route.put("/body_path/{item_id}")
async def update_item(item_id: int, item: Item):
return {"item_id": item_id, **item.model_dump()}
@route.put("/body_path_query/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
result = {"item_id": item_id, **item.model_dump()}
if q:
result.update({"q": q})
return result
class Body(BaseModel):
id: int = 1 # :
name: str = Field(default="good", min_length=2, max_length=20,description="test in class defination name")
age: int = Field(default=18,ge=1,le=100)
isNone: bool | None = False
lst: list[int|str|BaseModel]
option: Literal['a','b']
bodies = [{"id":1,"name":"test"},]
@route.post("/body_default")
async def post(body:Body) ->list: # 500
bodies.append(body)
return bodies
@route.post("/body_fastapi.Body")
async def body_fastapi_Body(body: Body = fastapi.Body()) ->list: # 500
return bodies
@route.post("/body_fastapi.query")
async def body_fastapi_query(body: Body = fastapi.Query()) ->list: # 500
return bodies
@route.get("debug/{item_id}")
async def read_item(item_id: str = Path(min_length=2,max_length=100,description="test str in path variables")
, q: str | None = Query(min_length=2)):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
@route.get("debugs/{item_id}")
async def read_item(item_id: Optional[str], q: str | None = None, short: bool = False):
item = {"item_id": item_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
@route.get("debugs1/{item_id}")
async def read_item(item_id: Union[str,int], q: str | None = None, short: bool = False):
item = {"item_id": item_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
@route.get("/users/{user_id}/items/{item_id}",tags=['users'])
async def read_user_item(
user_id: int, item_id: str, q: str | None = None, short: bool = False
):
item = {"item_id": item_id, "owner_id": user_id}
if q:
item.update({"q": q})
if not short:
item.update(
{"description": "This is an amazing item that has a long description"}
)
return item
@route.api_route("/multi", methods=["GET", "POST", "PUT"],summary="summary",description="description",tags=['multi'])
def multi():
return {"message": "支持多种方法"}
@route.api_route("/trace", methods=["TRACE"])
def trace_endpoint():
return {"method": "TRACE"}
@route.api_route("/connect", methods=["CONNECT"])
def connect_endpoint():
return {"method": "CONNECT"}
from fastapi import WebSocket
from starlette.websockets import WebSocketDisconnect
@route.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
# await websocket.accept()
# while True:
# data = await websocket.receive_text()
# await websocket.send_text(f"收到: {data}")
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message: {data}")
except WebSocketDisconnect:
print("客户端断开连接")
finally:
# 清理资源
pass
def ori_func(e):
return e
def ori_func1(a,b,c,d,z=Depends(ori_func)):
return ori_func
@route.get("/depends")
def depends(req: Request, r=Depends(ori_func1)):
# req
return r
@route.get("/depends_nested")
def depends_nested(req: Request, r=Depends(depends)):
# req
return r
+235
View File
@@ -0,0 +1,235 @@
from fastapi import APIRouter, Request,Header,Cookie
from pydantic import constr,Field, field_validator
from typing import Annotated
from fastapi import Path,Query, Form,File,UploadFile
from router.features import method
route = APIRouter(prefix="/mimetypes",tags=["mimetypes"])
from uuid import UUID
from pydantic import BaseModel, EmailStr, HttpUrl, IPvAnyAddress, conint
@route.get("/any_types")
def any_types(email: EmailStr
, url: HttpUrl = Query()
, ipv4: IPvAnyAddress | None=None
, ipv6: IPvAnyAddress | None=None
, uuid: UUID | None=None
# , date: date
# , datetime: datetime
# , time: time
# , timedelta: timedelta
# , decimal: Decimal
# , json: Json
# , secret: SecretStr
# , secret_bytes: SecretBytes
# , conint: conint(gt=0, lt=100)
):
return {
"email": email,
"url": url,
"ipv4": ipv4,
"ipv6": ipv6,
"uuid": uuid
}
pass
'''
form/ file
'''
@route.post('/login')
def login(name:str = Form()):
return name
@route.post('/upload')
def upload(file_name:bytes = File()):
return file_name
@route.post("/upload_raw")
async def upload(request: Request):
return await request.body()
@route.post('/upload_files')
def upload(file_name:list[bytes] = File()):
return file_name
@route.post('/upload_files_annotated')
def upload(file_name:Annotated[list[bytes],File()]):
return file_name
@route.post('/upload_uploadfiles')
def upload(files:list[UploadFile] = File()):
return [f.filename for f in files]
@route.post('/upload_file')
def upload(file:UploadFile):
return file
@route.post('/upload_str')
def upload(file_name:str = File()):
return file_name
'''
validator app
'''
@route.get("/validate_con")
def web(
age: conint(ge=0, lt=150),
name: constr(min_length=1, max_length=50),
):
return {
"age":age,
"name":name
}
@route.get("/validate_field")
def web(
num:int = Query(Field(10,le=100,ge=8))
):
return num
@route.get("/validate_annotated")
def web(
age: Annotated[int, Field(ge=0, lt=150)],
name: Annotated[str, Field(min_length=1, max_length=50)],
):
return {
"age": age,
"name": name
}
@route.get("/validated_annotated_query")
async def get(option: Annotated[str|None, Query(max_length=10)] = None):
return option
@route.get("/validated_annotated_path/{option}")
async def get(option: Annotated[str|None, Path(max_length=10)]):
return option
'''
'''
class ValidatorSelf(BaseModel):
email: EmailStr
age: int
@field_validator("age")
def age_must_be_realistic(cls, v):
if v > 150:
raise ValueError("age too large")
return v
@route.post("/validated_self")
async def post(vs: ValidatorSelf):
return vs
@route.get("/request")
def request(res: Request):
return res.headers.raw
from fastapi import Request
# from method import Item
# @route.api_route("/anything/{item}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def anything(request: Request):
return {
"method": request.method,
"path_params": request.path_params,
"query": dict(request.query_params),
"headers": dict(request.headers),
"cookies": request.cookies,
"body": (await request.body()).decode("utf-8", "ignore"),
}
@route.post("/anything_params/{item_id}")
async def anything_params(
item_id: Annotated[int, Path()],
dry_run: Annotated[bool, Query()] = False,
x_token: Annotated[str, Header()] = None,
session: Annotated[str | None, Cookie()] = None,
# item: Item | None = None, # JSON body & multi-part cannot occured same time!
item: str = Form(),
avatar: UploadFile | None = File(None)):
import json
item_obj = method.Item.model_validate(json.loads(item))
return {
"item_id": item_id,
"dry_run": dry_run,
"x_token": x_token,
"session": session,
"item": json.dumps(item_obj),
"avatar": avatar
}
@route.get("/proxy")
def get(request: Request):
path = request.url_for('sn',path = 'head.png')
return path
# 定义请求体模型
class UserRequest(BaseModel):
username:str
passwd:str
class UserResponse(BaseModel):
code:int = 200
detail:str = 'ok'
totals:int = 0
username:str
@route.post('/user',response_model=UserResponse)
def user(u:UserRequest):
# return UserResponse(username=u.username)
return u # 等价于 return UserResponse(username=u.username)
@route.post('/users')
def users(u:list[UserRequest]):
x = [_.username for _ in u]
return UserResponse(code='200',totals = len(x),username=x[0])
@route.post('/users_list')
def users(u:list[str]):
return UserResponse(code='200',totals = len(u),username=u[0])
@route.get('/url_for/{id}',name='url_for')
def url_for(id:int):
return id
@route.get('/test_url_for')
def url_for(res:Request):
url = res.url_for('url_for',id = 123)
return {"url":str(url)}
from fastapi import status,Response
@route.post("/status_code", status_code=status.HTTP_201_CREATED)
async def status_code(name:str):
return name
# 或者动态设置
@route.post("/response")
async def response(name: str, response: Response):
response.status_code = 201
response.headers["X-Custom"] = "value"
return name
+84
View File
@@ -0,0 +1,84 @@
# from idlelib.query import Query
from typing import Optional
from fastapi import APIRouter,Query
from pydantic import BaseModel
import re
from enum import Enum
route = APIRouter(prefix="/path_query_variables",tags=["path_query_variables"])
stu = [{"sid":'001','name':'gk','age':18,'address':'dd_abc_lg_', 'sex':'male'},
{"sid":'002','name':'gk2','age':19},
{"sid":'003','name':'gk3','age':20}]
class Sex(str,Enum):
MALE = 1
FEMAIL = 2,
TRANS = 3
maile = '男'
femail = '女',
trans = '跨性别'
@route.get("/{sid}")
def get(sid: str):
for s in stu:
if s['sid'] == sid: return s
return None
@route.get("/")
def get(age_min: int | None = None, age_max: int| None=None):
lst = []
for s in stu:
if age_min <= s['age'] <= age_max: lst.append(s)
return lst
# todo: not support int|None? <0.141.1
'''
BP:
- Optional[str,None]
- str | None
- str | None = None
'''
@route.get("/validate")
def get(age_min: int=Query(default=0, ge=0,le=100),
age_max: int=Query(default=0, ge=0,le=100),
sname:str=Query(default='',min_length=2,max_length=10),
address:str=Query(pattern=".*lg.*"),
sex:Sex=Query()
):
return [x for x in stu if age_min <= x['age'] <= age_max and x['name']==sname and re.search(address,x['address'])]
# path
@route.get("/good/{path:path}")
def get_path(path: str):
return path
@route.get("/bad/{path}")
def get_path(path: str):
return path
class Object(BaseModel):
id:int|None = None
name: str|None = None
price: float|None = None
@route.post("/model_dump_extraset")
def model_dump_extraset(obj: Object):
return obj.model_dump(exclude_unset=True)
@route.get("/model_dump_extraset")
def model_dump_extraset(obj: Object):
return obj.model_dump(exclude_unset=True)
+133
View File
@@ -0,0 +1,133 @@
import fastapi
from fastapi import APIRouter,Query,Path,HTTPException
from pydantic import Field,BaseModel,field_validator,model_validator
from typing import Annotated,Union,Optional
route = APIRouter(prefix="/validations",tags=["validations"])
@route.get("/annotated_query")
async def get(option: Annotated[str|None, Query(max_length=10)] = None):
return option
@route.get("/annotated_query_lst")
async def get(option: Annotated[list[str], Query()] = None):
return option
@route.get("/annotated_query_re")
async def get(option: Annotated[str|None, Query(min_length=2,max_length=10,pattern="^g.*k$")] = None):
return option
@route.get("/annotated_path/{option}")
async def get(option: Annotated[str|None, Path(max_length=10)]):
return option
@route.get("/query")
async def get(option: str = Query(min_length=2, max_length=10)):
return option
@route.get("/params_validate")
async def params_validate(name:Union[str,None], # default
id: Optional[int]):
pass
# @route.get("/field")
# async def get(option: str = Field(min_length=2, max_length=10)):
# return option
class Clazz(BaseModel):
name: str = Field(min_length=2,max_length=12)
@route.get("/field")
async def get(clazz: Clazz):
return clazz
@route.get("/field_body")
async def get(clazz: Clazz = fastapi.Body()):
return clazz
@route.get("/path_path/{option}")
async def get(option: str = Path(min_length=2, max_length=10)):
return option
#
# @route.get("/path_query/{option}")
# async def get(option: str = Query(min_length=2, max_length=10)):
# return option
# param not got!
@route.get("/path_bad")
async def get(option: str = Path(min_length=2, max_length=10)):
return option
@route.get("/items/")
async def read_items(q: str|None = Query(default=None, max_length=50)):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
@route.get("/items_default_bad/")
async def read_items(q: str = Query(default=None, max_length=50)):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
# field/model_validator
class User(BaseModel):
name: str
passwd: str
# @field_validator("passwd")
# def check_passwd(cls,passwd,values): #
# # print(f'{values=}') # values=ValidationInfo(config={'title': 'User'}, context=None, data={'name': 'string'}, field_name='passwd')
# # 无法cannot access passwd/ name ?!
# # print(f'{values["passwd"]=}')
# # print(f'{values["name"]=}')
# if passwd == '001':
# raise HTTPException(status_code=408,detail="001 only root can set!")
# return passwd
@model_validator(mode="before")
def check_model_before(cls, data): #
print('model_validator before............')
return data
@field_validator("passwd","name")
def check_field(cls,value,values): #
# print(f'{values=}') # values=ValidationInfo(config={'title': 'User'}, context=None, data={'name': 'string'}, field_name='passwd')
# cannot access passwd/ name ?!
# print(f'{values["passwd"]=}')
# print(f'{values["name"]=}')
print('field_validator............')
if value == '001':
raise HTTPException(status_code=408,detail="001 only root can set!")
if value == 'root':
raise HTTPException(status_code=408,detail="only root can set name!")
return value
@model_validator(mode="after")
def check_model_after(self): #
print('model_validator............')
if self.passwd == '001':
raise HTTPException(status_code=408,detail="model : 001 only root can set!")
if self.name == 'root':
raise HTTPException(status_code=408,detail="model : only root can set name!")
return self
# u = User(age="20", name="geeker")
@route.post("/registe")
def registe(user: User):
return user
+1
View File
@@ -0,0 +1 @@
b
+1
View File
@@ -0,0 +1 @@
a
View File
+1
View File
@@ -0,0 +1 @@
/home/geeker/architecture/home/geeker/code_repo/infrastructure_system_softwares/language_dev/python/fastapi_pycharm_proj/router/workhome/a
+1
View File
@@ -0,0 +1 @@
/home/geeker/architecture/home/geeker/code_repo/infrastructure_system_softwares/language_dev/python/fastapi_pycharm_proj/router/workhome/b
+135
View File
@@ -0,0 +1,135 @@
from fastapi import APIRouter,Query,Request,Path,Query,Body,Form,File,UploadFile
from pydantic import constr,Field,field_validator,BaseModel, EmailStr, HttpUrl, IPvAnyAddress, conint
from typing import Annotated,Literal
from datetime import date, timedelta
import datetime
from decimal import Decimal
from time import time
from uuid import UUID
import json,os,logging
route = APIRouter(prefix="/wk0915",tags=["wk0915"])
class Login(BaseModel):
username: str = Field(min_length=3, max_length=20,pattern="^[a-zA-Z]")
passwd: str = Field(min_length=6, max_length=20)
is_ok: bool
@route.post("/login/{session_id}")
def login(session_id:str = Path(min_length=10, pattern="^[a-zA-Z0-9]+$"),
usertype:int| None = None,
login:Login =Form()):
d = {
"username":login.username,
"usertype":usertype,
"is_ok":login.is_ok
}
# lg = Login.model_validate(json.loads())
# return {
# "login": d
# }
return d
@route.get("/user/{user_id}")
def user(user_id: int = Path(gt=1)):
return user_id
class Product(BaseModel):
pname: str = Field(min_length=2,max_length=50)
price: int = Field(gt=0)
stock: int = Field(gt=0)
type: str = Field(min_length=1) # todo not blank?
@route.post("/product")
def product(product:Product):
return product
stu = teac = []
class Student(BaseModel):
sid: int | None = None
sname: str
age: int | None = Field(default=None, ge=0)
sex: Literal['男','女']
hobby: set[str]
create_date: datetime.datetime = Field(default_factory=datetime.datetime.now)
@route.post("/student")
def student(student: Student):
stu.append(student)
return student
@route.get('/student')
def student(sname:str|None = Query(default=None),
sex:str|None = Query(default=None), # todo Literal validate
age:int|None = Query(default=None, ge=0, le=120)
):
# res = [s for s in stu if s['sname'] == sname]
if sname == None and sex == None and age == None : return stu
if sname != None and sex != None and age != None: return [s for s in stu if (sname != None and s.sname == sname)
and (sex != None and s.sex == sex)
and (age != None and s.age == age)]
if sname != None and sex != None : return [s for s in stu if (sname != None and s.sname == sname)
and (sex != None and s.sex == sex)]
if sname != None and age != None : return [s for s in stu if (sname != None and s.sname == sname)
and (age != None and s.age == age)]
if sex != None and age != None : return [s for s in stu if (sex != None and s.sex == sex)
and (age != None and s.age == age)]
if sex != None : return [s for s in stu if s.sex == sex]
if sname != None : return [s for s in stu if s.sname == sname]
if age != None : return [s for s in stu if s.age == age]
class Teacher(BaseModel):
name: str|None
salary: float
hiredate: date
stu: list[Student]
@route.post("/teacher")
def teacher(teacher:Teacher):
teac.append(teacher)
return teac
import random
@route.post("/upload")
def upload(files:list[bytes]= File()):
# names = random.choices(['001','002','geeker'],len(files)) # todo duplacate names
# names = random.choices(range(len(files)),len(files)) # set fix!
i = 0 # order name
saved = []
for file in files:
try:
# filepath = os.path.join(os.getcwd(), i)
filepath = os.getcwd() + "/router/workhome/" + str(i)
with open(filepath,'wb') as f:
f.write(file)
i += 1
saved.append(filepath)
except Exception: # exception too large!
pass
return saved
@route.post("/upload_file")
async def upload(files:list[UploadFile] = File()):
saved = []
for file in files:
path = os.getcwd() + "/router/workhome/" + file.filename
data = await file.read()
try:
with open(path,'w',encoding='utf-8') as f:
f.write(data)
saved.append(path)
except Exception:
pass
return saved
+86
View File
@@ -0,0 +1,86 @@
from fastapi import APIRouter,Query,Request,Path,Query,Body,Form,File,UploadFile,HTTPException
from pydantic import constr,Field,field_validator,BaseModel, EmailStr, HttpUrl, IPvAnyAddress, conint
from typing import Annotated,Literal
from datetime import date, timedelta
import datetime
from decimal import Decimal
from time import time
from uuid import UUID
import json,os,logging
from hashlib import md5 # ①导入md5加密
from libplus.logpluslplus import Log
route = APIRouter(prefix="/wk0916",tags=["wk0916"])
users = []
@route.post("/registe")
def registe(username:str, passwd:str):
users.append({"username":username,"passwd":passwd})
return users
@route.post("upload")
async def upload(file:UploadFile,res:Request):
data = await file.read()
if "洗脚" in data:
md = md5()
md.update(("sensitive_word" + str(range(10))).encode())
file_name = md.hexdigest()
with open(file_name,'w',encoding='utf-8') as f:
f.write(data)
log = Log('local_log','warning')
log.set_file_logger("logfile.log")
log.warning("洗脚"+res.client.host)
else:
log = Log('local_log', 'info')
log.get_file_logger("logfile.log")
log.warning("no洗脚" + res.client.host)
return "该文件没有敏感词汇"
stu = [{"sname":"gk","classid":"java"},
{"sname":"yb","classid":"python"}]
@route.get("/student")
def student(name:str):
for s in stu:
if s["sname"] == name:
return s
raise HTTPException(status_code=404,detail="学生不存在")
# app.mount(path="/staticfile",app=StaticFiles(directory="d://tmp"),name="sn")
class Response(BaseModel):
code: int = 200
detail: str = "添加成功"
total: int = 1
sname: str
classid: int
class Request(BaseModel):
sname: str
classid: int
@route.get("/student_model",response_model=Response)
def student_model(s:Request):
for s0 in stu:
if s0["sname"] == s.sname:
raise HTTPException(status_code=404,detail="学生不存在")
return s
@route.post("/student_model_post",response_model=Response)
def student_model_post(s:Request):
stu.append({"sname":s.sname,"classid":s.classid})
# suc / fail
flag = True
if flag : return Response(code = '200',sname=s.sname,classid = s.classid)
else :return Response(code = '500',sname=s.sname,classid = s.classid)
+50
View File
@@ -0,0 +1,50 @@
from fastapi import APIRouter,Query,Request,Path,Query,Body,Form,File,UploadFile,HTTPException,Response
from pydantic import constr,Field,field_validator,BaseModel, EmailStr, HttpUrl, IPvAnyAddress, conint,model_validator
from typing import Annotated,Literal
from datetime import date, timedelta
import datetime
from decimal import Decimal
from time import time
from uuid import UUID
import json,os,logging
from hashlib import md5 # ①导入md5加密
from libplus.logpluslplus import Log
route = APIRouter(prefix="/wk0917",tags=["wk0917"])
products = [{"name":"a","price":123.9,"stock":2},{"name":"a","price":123.9,"stock":2},{"name":"a","price":123.9,"stock":2},]
class Product(BaseModel):
name:str
price:float
stock: int
@model_validator(mode='after') #
def check(self):
pnames = [p['name'] for p in products]
if self.name in pnames: raise HTTPException(status_code="409",detail="name duplicate")
return self
@route.get("/product")
def product(name:str):
for p in products:
if name == p["name"]: return p
else: return Response(status_code="409",content="not find!")
@route.post("/product")
def product(p: Product):
products.append(p.model_dump())
return p
orders = []
@route.get("/order")
def order(name:str):
return name
@route.post("/order")
def order(name:str):
orders.append(name)
return name
Generated
+1120
View File
File diff suppressed because it is too large Load Diff