diff --git a/auth/__init__.py b/auth/__init__.py new file mode 100644 index 0000000..115a1ef --- /dev/null +++ b/auth/__init__.py @@ -0,0 +1,52 @@ +from pathlib import Path +from datetime import datetime + +from chongming_worker import auto_bind_models +from chongming_worker.runtime import create_workers as _create_workers +from chongming_password import hash_password + +from .router import * + + +auto_bind_models("auth") + + +async def create_workers(session, config, count=1, worker_manager=None): + from chongming_database import Database + from .global_var import set_global_var + from .models.auth import Users + from sqlalchemy import select + + db_path = Path(config.get("data_dir", "./data")) / "auth.db" + db = Database(db_path) + await db.initialize() + + # 初始化管理员 + async with db.get_session() as sess: + select_stmt = select(Users) \ + .where(Users.username == "admin") # type: ignore + result = await sess.execute(select_stmt) + user: Users | None = result.scalar_one_or_none() + + if user is None: + user = Users( + username="admin", + hashed_password=hash_password("admin"), + email="admin@example.com", + created_at=datetime.now(), + updated_at=datetime.now(), + is_superuser=True + ) + sess.add(user) + await sess.commit() + + set_global_var("db", db) + + return _create_workers(session, config, count, worker_manager) + +async def cleanup(worker_item: str): + from .global_var import db + + if db is not None: + await db.dispose() + print(f"{worker_item} 清理完毕") \ No newline at end of file diff --git a/auth/global_var.py b/auth/global_var.py new file mode 100644 index 0000000..bbdef28 --- /dev/null +++ b/auth/global_var.py @@ -0,0 +1,29 @@ +from typing import Any + +from chongming_config import get_config +from chongming_jwt import JWT +from chongming_database import Database +from .models import * + +def set_global_var(name: str, value: Any) -> None: + """ + 设置当前模块的全局变量。 + + Args: + name: 变量名(字符串) + value: 任意值 + """ + globals()[name] = value + +config = get_config() + +db: Database | None = None + +secret = config["security"]["secret_key"] +algorithm = config["security"]["algorithm"] +jwt = JWT(secret, algorithm) + +__all__ = [ + "db", + "jwt" +] \ No newline at end of file diff --git a/auth/models/__init__.py b/auth/models/__init__.py new file mode 100644 index 0000000..887b9a9 --- /dev/null +++ b/auth/models/__init__.py @@ -0,0 +1,33 @@ +""" +模型导出 — auth +自动生成,请勿手动修改 +""" + +from .pydantic import AuthResponse +from .pydantic import ChangePasswordPostRequest +from .pydantic import ChangePasswordPostResponse +from .pydantic import LoginPostRequest +from .pydantic import LoginPostResponse +from .pydantic import MeGetRequest +from .pydantic import MeGetResponse +from .pydantic import RegisterPostRequest +from .pydantic import RegisterPostResponse +from .pydantic import UpdateProfilePostRequest +from .pydantic import UpdateProfilePostResponse + +from .auth import Users + +__all__ = [ + "AuthResponse", + "ChangePasswordPostRequest", + "ChangePasswordPostResponse", + "LoginPostRequest", + "LoginPostResponse", + "MeGetRequest", + "MeGetResponse", + "RegisterPostRequest", + "RegisterPostResponse", + "UpdateProfilePostRequest", + "UpdateProfilePostResponse", + "Users", +] diff --git a/auth/models/auth.py b/auth/models/auth.py new file mode 100644 index 0000000..d92b96a --- /dev/null +++ b/auth/models/auth.py @@ -0,0 +1,102 @@ +import os +import time +from typing import Optional +from datetime import datetime +from uuid import uuid4 + +from sqlmodel import Field, SQLModel, Column +from sqlalchemy import func, DateTime + + +def primary_key_create(): + return f"{uuid4()}_{os.getpid()}_{int(time.time())}" + + +class Users(SQLModel, table=True): + __tablename__ = "users" # type: ignore + + # ---------- 主键 ---------- + id: str = Field( + default_factory=primary_key_create, + primary_key=True, + index=True, + description="用户唯一ID(UUID格式)" + ) + + # ---------- 登录凭证(唯一索引) ---------- + username: str = Field( + max_length=50, + unique=True, + index=True, + nullable=False, + description="登录用户名" + ) + email: str = Field( + max_length=255, + unique=True, + index=True, + nullable=False, + description="电子邮箱" + ) + hashed_password: str = Field( + max_length=255, + nullable=False, + description="加密后的密码(不存明文)" + ) + + # ---------- 个人信息(可选) ---------- + full_name: Optional[str] = Field( + default=None, + max_length=100, + description="用户全名" + ) + avatar_url: Optional[str] = Field( + default=None, + max_length=500, + description="头像图片链接" + ) + phone: Optional[str] = Field( + default=None, + max_length=20, + description="手机号码" + ) + + # ---------- 安全与状态标识 ---------- + is_active: bool = Field( + default=True, + description="账户是否启用(用于软禁用)" + ) + is_verified: bool = Field( + default=False, + description="邮箱/手机是否已验证" + ) + is_superuser: bool = Field( + default=False, + description="是否为超级管理员" + ) + + # ---------- 审计时间戳(数据库自动维护) ---------- + created_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + comment="创建时间" + ) + ) + updated_at: datetime = Field( + sa_column=Column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + comment="最后更新时间" + ) + ) + + # ---------- 最后登录记录(可选) ---------- + last_login_at: Optional[datetime] = Field( + default=None, + sa_column=Column(DateTime(timezone=True), nullable=True), + description="最后一次登录时间" + ) diff --git a/auth/models/pydantic.py b/auth/models/pydantic.py new file mode 100644 index 0000000..c8c220b --- /dev/null +++ b/auth/models/pydantic.py @@ -0,0 +1,86 @@ +""" +自动生成的 Pydantic 模型 — auth +来源: config/workers/auth/ +""" + +from __future__ import annotations + +from typing import Any, Optional +from pydantic import BaseModel, Field + +class ChangePasswordPostRequest(BaseModel): + """修改密码 — 请求体""" + user_id: str = Field(...) + old_password: str = Field(...) + new_password: str = Field(...) + +class ChangePasswordPostResponse(BaseModel): + """修改密码 — 响应体""" + result: str = Field(...) + message: str = Field(...) + timestamp: Optional[float] = Field(default=None) + +class LoginPostRequest(BaseModel): + """登录 — 请求体""" + username: str = Field(...) + password: str = Field(...) + +class LoginPostResponse(BaseModel): + """登录 — 响应体""" + result: str = Field(...) + token: str = Field(...) + message: str = Field(...) + timestamp: Optional[float] = Field(default=None) + +class MeGetRequest(BaseModel): + """获取当前用户信息 — 请求体""" + user_id: str = Field(...) + +class MeGetResponse(BaseModel): + """获取当前用户信息 — 响应体""" + result: str = Field(...) + id: str = Field(...) + username: str = Field(...) + email: str = Field(...) + full_name: Optional[str] = Field(default=None) + avatar_url: Optional[str] = Field(default=None) + phone: Optional[str] = Field(default=None) + is_active: bool = Field(...) + is_verified: bool = Field(...) + is_superuser: bool = Field(...) + created_at: Optional[str] = Field(default=None) + updated_at: Optional[str] = Field(default=None) + last_login_at: Optional[str] = Field(default=None) + +class RegisterPostRequest(BaseModel): + """用户注册 — 请求体""" + username: str = Field(...) + password: str = Field(...) + email: str = Field(...) + full_name: str = Field(...) + phone: str = Field(...) + +class RegisterPostResponse(BaseModel): + """用户注册 — 响应体""" + result: str = Field(...) + message: str = Field(...) + user_id: Optional[str] = Field(default=None) + timestamp: Optional[float] = Field(default=None) + +class UpdateProfilePostRequest(BaseModel): + """更新用户资料 — 请求体""" + user_id: str = Field(...) + full_name: str = Field(...) + email: str = Field(...) + avatar_url: str = Field(...) + phone: str = Field(...) + +class UpdateProfilePostResponse(BaseModel): + """更新用户资料 — 响应体""" + result: str = Field(...) + message: str = Field(...) + timestamp: Optional[float] = Field(default=None) + + +# 所有响应模型的联合类型 +AuthResponse = LoginPostResponse | ChangePasswordPostResponse | MeGetResponse | UpdateProfilePostResponse | RegisterPostResponse diff --git a/auth/router/__init__.py b/auth/router/__init__.py new file mode 100644 index 0000000..0eae3fb --- /dev/null +++ b/auth/router/__init__.py @@ -0,0 +1,8 @@ +from .login import login +from .register import register +from .me import me +from .update_profile import update_profile +from .change_password import change_password + + +__all__ = ["login", "register", "me", "update_profile", "change_password"] \ No newline at end of file diff --git a/auth/router/change_password.py b/auth/router/change_password.py new file mode 100644 index 0000000..98a6216 --- /dev/null +++ b/auth/router/change_password.py @@ -0,0 +1,81 @@ +import time +from datetime import datetime + +from sqlalchemy import select + +from chongming_worker import rpc_method +from chongming_password import hash_password, verify_password + +from ..models.pydantic import ( + ChangePasswordPostRequest, + ChangePasswordPostResponse +) +from ..models.auth import Users + + +@rpc_method( + subject="auth.change_password", + method="POST", + summary="修改密码", + response_model=ChangePasswordPostResponse, +) +async def change_password(change_password_post_request: ChangePasswordPostRequest) -> ChangePasswordPostResponse: + from ..global_var import db + + if db is None: + raise RuntimeError("Database not initialized") + + user_id = change_password_post_request.user_id + old_password = change_password_post_request.old_password + new_password = change_password_post_request.new_password + + if not old_password: + return ChangePasswordPostResponse( + result="fail", + message="旧密码不能为空", + timestamp=time.time() + ) + if not new_password: + return ChangePasswordPostResponse( + result="fail", + message="新密码不能为空", + timestamp=time.time() + ) + if old_password == new_password: + return ChangePasswordPostResponse( + result="fail", + message="新密码不能与旧密码相同", + timestamp=time.time() + ) + + async with db.get_session() as session: + stmt = select(Users).filter_by(id=user_id) + result = await session.execute(stmt) + user: Users | None = result.scalar_one_or_none() + + if user is None: + return ChangePasswordPostResponse( + result="fail", + message="用户不存在", + timestamp=time.time() + ) + + # 验证旧密码 + if not verify_password(old_password, user.hashed_password): + return ChangePasswordPostResponse( + result="fail", + message="旧密码错误", + timestamp=time.time() + ) + + # 更新密码 + user.hashed_password = hash_password(new_password) + user.updated_at = datetime.now() + session.add(user) + await session.commit() + + return ChangePasswordPostResponse( + result="success", + message="密码修改成功", + timestamp=time.time() + ) diff --git a/auth/router/login.py b/auth/router/login.py new file mode 100644 index 0000000..844363f --- /dev/null +++ b/auth/router/login.py @@ -0,0 +1,85 @@ +import time +from datetime import datetime +import json + +from sqlalchemy import select + +from chongming_worker import rpc_method +from chongming_password import verify_password + +from ..models.pydantic import ( + LoginPostRequest, + LoginPostResponse +) +from ..models.auth import Users + + +@rpc_method( + subject="auth.login", + method="POST", + summary="登录", + response_model=LoginPostResponse, +) +async def login(login_post_request: LoginPostRequest) -> LoginPostResponse: + from ..global_var import jwt, db + + if db is None: + raise RuntimeError("Database not initialized") + async with db.get_session() as session: + statement = select(Users).filter_by( + username=login_post_request.username, + ) + result = await session.execute(statement) + user: Users | None = result.scalar_one_or_none() + + if user is None: + return LoginPostResponse( + result="fail", + message="用户名错误", + token="", + timestamp=time.time() + ) + + if not verify_password(login_post_request.password, user.hashed_password): + return LoginPostResponse( + result="fail", + message="密码错误", + token="", + timestamp=time.time() + ) + + if not user.is_active: + return LoginPostResponse( + result="fail", + message="用户已经注销或被禁用", + token="", + timestamp=time.time() + ) + + user.last_login_at = datetime.now() + + payload = { + "sub": user.id, + "username": user.username, + "email": user.email, + "full_name": user.full_name, + "avatar_url": user.avatar_url, + "phone": user.phone, + "is_verified": user.is_verified, + "is_superuser": user.is_superuser, + "created_at": user.created_at.isoformat(), + "updated_at": user.updated_at.isoformat(), + "last_login_at": user.last_login_at.isoformat() + } + + token = jwt.encode( + json.dumps(payload), + expires_in_seconds=10800 # 3小时有效期 + ) + + return LoginPostResponse( + result="success", + message="登录成功", + token=token, + timestamp=time.time() + ) diff --git a/auth/router/me.py b/auth/router/me.py new file mode 100644 index 0000000..7251a71 --- /dev/null +++ b/auth/router/me.py @@ -0,0 +1,56 @@ +import time + +from sqlalchemy import select + +from chongming_worker import rpc_method + +from ..models.pydantic import ( + MeGetRequest, + MeGetResponse +) +from ..models.auth import Users + + +@rpc_method( + subject="auth.me", + method="GET", + summary="获取当前用户信息", + response_model=MeGetResponse, +) +async def me(me_get_request: MeGetRequest) -> MeGetResponse: + from ..global_var import db + + if db is None: + raise RuntimeError("Database not initialized") + + async with db.get_session() as session: + stmt = select(Users).filter_by(id=me_get_request.user_id) + result = await session.execute(stmt) + user: Users | None = result.scalar_one_or_none() + + if user is None: + return MeGetResponse( + result="fail", + id="", + username="", + email="", + is_active=False, + is_verified=False, + is_superuser=False, + ) + + return MeGetResponse( + result="success", + id=user.id, + username=user.username, + email=user.email, + full_name=user.full_name, + avatar_url=user.avatar_url, + phone=user.phone, + is_active=user.is_active, + is_verified=user.is_verified, + is_superuser=user.is_superuser, + created_at=user.created_at.isoformat() if user.created_at else None, + updated_at=user.updated_at.isoformat() if user.updated_at else None, + last_login_at=user.last_login_at.isoformat() if user.last_login_at else None, + ) diff --git a/auth/router/register.py b/auth/router/register.py new file mode 100644 index 0000000..8d2fa20 --- /dev/null +++ b/auth/router/register.py @@ -0,0 +1,93 @@ +import time +from datetime import datetime + +from sqlalchemy import select + +from chongming_worker import rpc_method +from chongming_password import hash_password + +from ..models.pydantic import ( + RegisterPostRequest, + RegisterPostResponse +) +from ..models.auth import Users + + +@rpc_method( + subject="auth.register", + method="POST", + summary="用户注册", + response_model=RegisterPostResponse, +) +async def register(register_post_request: RegisterPostRequest) -> RegisterPostResponse: + from ..global_var import db + + if db is None: + raise RuntimeError("Database not initialized") + + username = register_post_request.username.strip() + email = register_post_request.email.strip() + full_name = register_post_request.full_name.strip() or None + phone = register_post_request.phone.strip() or None + + # 校验必填字段 + if not username: + return RegisterPostResponse( + result="fail", + message="用户名不能为空", + timestamp=time.time() + ) + if not email: + return RegisterPostResponse( + result="fail", + message="邮箱不能为空", + timestamp=time.time() + ) + if not register_post_request.password: + return RegisterPostResponse( + result="fail", + message="密码不能为空", + timestamp=time.time() + ) + + async with db.get_session() as session: + # 检查用户名是否已存在 + stmt = select(Users).filter_by(username=username) + result = await session.execute(stmt) + if result.scalar_one_or_none() is not None: + return RegisterPostResponse( + result="fail", + message="用户名已存在", + timestamp=time.time() + ) + + # 检查邮箱是否已存在 + stmt = select(Users).filter_by(email=email) + result = await session.execute(stmt) + if result.scalar_one_or_none() is not None: + return RegisterPostResponse( + result="fail", + message="邮箱已被注册", + timestamp=time.time() + ) + + # 创建新用户 + user = Users( + username=username, + hashed_password=hash_password(register_post_request.password), + email=email, + full_name=full_name, + phone=phone, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + session.add(user) + await session.commit() + await session.refresh(user) + + return RegisterPostResponse( + result="success", + message="注册成功", + user_id=user.id, + timestamp=time.time() + ) diff --git a/auth/router/update_profile.py b/auth/router/update_profile.py new file mode 100644 index 0000000..fa4b99a --- /dev/null +++ b/auth/router/update_profile.py @@ -0,0 +1,74 @@ +import time +from datetime import datetime + +from sqlalchemy import select + +from chongming_worker import rpc_method + +from ..models.pydantic import ( + UpdateProfilePostRequest, + UpdateProfilePostResponse +) +from ..models.auth import Users + + +@rpc_method( + subject="auth.update_profile", + method="POST", + summary="更新用户资料", + response_model=UpdateProfilePostResponse, +) +async def update_profile(update_profile_post_request: UpdateProfilePostRequest) -> UpdateProfilePostResponse: + from ..global_var import db + + if db is None: + raise RuntimeError("Database not initialized") + + user_id = update_profile_post_request.user_id + + async with db.get_session() as session: + stmt = select(Users).filter_by(id=user_id) + result = await session.execute(stmt) + user: Users | None = result.scalar_one_or_none() + + if user is None: + return UpdateProfilePostResponse( + result="fail", + message="用户不存在", + timestamp=time.time() + ) + + # 更新字段(只更新非空值) + full_name = update_profile_post_request.full_name.strip() + email = update_profile_post_request.email.strip() + avatar_url = update_profile_post_request.avatar_url.strip() + phone = update_profile_post_request.phone.strip() + + if full_name: + user.full_name = full_name + if email: + # 检查邮箱是否被其他用户占用 + if email != user.email: + check_stmt = select(Users).filter_by(email=email) + check_result = await session.execute(check_stmt) + if check_result.scalar_one_or_none() is not None: + return UpdateProfilePostResponse( + result="fail", + message="邮箱已被其他用户使用", + timestamp=time.time() + ) + user.email = email + if avatar_url: + user.avatar_url = avatar_url + if phone: + user.phone = phone + + user.updated_at = datetime.now() + session.add(user) + await session.commit() + + return UpdateProfilePostResponse( + result="success", + message="资料更新成功", + timestamp=time.time() + ) diff --git a/example/__init__.py b/example/__init__.py index 154690d..6771bde 100644 --- a/example/__init__.py +++ b/example/__init__.py @@ -15,7 +15,6 @@ Example Worker 插件 — 演示 @rpc_method + 显式模型绑定用法 - 装饰器 subject 中的操作名 → 模型前缀(驼峰) example.add → Add, example.subtract → Subtract """ -import modelvault from pathlib import Path from chongming_worker import auto_bind_models @@ -37,14 +36,19 @@ auto_bind_models("example") async def create_workers(session, config, count=1, worker_manager=None): """插件入口 — 委托给 chongming_worker 运行时创建 Worker 协程""" from .global_var import set_global_var + from chongming_database import Database - db_path = Path(config.get("data_dir", "./data")) / "example.modelvault" - db_path.parent.mkdir(parents=True, exist_ok=True) - db = await modelvault.AsyncDatabase.open(str(db_path)) # type: ignore + db_path = Path(config.get("data_dir", "./data")) / "example.db" + + db = Database(db_path) + await db.initialize() set_global_var("db", db) return _create_workers(session, config, count, worker_manager) async def cleanup(worker_item: str): - print(f"{worker_item}关闭") - pass + from .global_var import db + + if db is not None: + await db.dispose() + print(f"{worker_item} 清理完毕") diff --git a/example/global_var.py b/example/global_var.py index 9b45137..212d3bc 100644 --- a/example/global_var.py +++ b/example/global_var.py @@ -1,9 +1,10 @@ -import sys -from typing import Any, Callable, Awaitable, List -import modelvault +from typing import Any +from .models import * +from chongming_database import Database + + +db: Database | None = None -# 初始定义(可以保留 db 作为示例) -db: modelvault.AsyncDatabase | None = None # type: ignore def set_global_var(name: str, value: Any) -> None: """ @@ -15,10 +16,7 @@ def set_global_var(name: str, value: Any) -> None: """ globals()[name] = value - - -# 但这样会污染导出列表,不推荐自动添加。 + __all__ = [ - "db", "set_global_var", -] \ No newline at end of file +] diff --git a/example/models/__init__.py b/example/models/__init__.py index 6860567..6dad973 100644 --- a/example/models/__init__.py +++ b/example/models/__init__.py @@ -16,6 +16,8 @@ from .pydantic import MultiplyGetResponse from .pydantic import SubtractGetRequest from .pydantic import SubtractGetResponse +from .counter import Counter + __all__ = [ "AddGetRequest", "AddGetResponse", @@ -29,4 +31,5 @@ __all__ = [ "MultiplyGetResponse", "SubtractGetRequest", "SubtractGetResponse", + "Counter" ] diff --git a/example/models/counter.py b/example/models/counter.py index 9e6b2e4..34fc786 100644 --- a/example/models/counter.py +++ b/example/models/counter.py @@ -1,14 +1,8 @@ -# src/plugins/example/models/counter.py -from pydantic import BaseModel -import modelvault +from sqlmodel import Field, SQLModel -class Counter(BaseModel): - # 主键字段 - __modelvault_primary_key__ = "name" - # 唯一索引 - __modelvault_indexes__ = [ - modelvault.models.unique("name"), - ] - name: str # 计数器名称(例如 "visits") - value: int = 0 \ No newline at end of file +class Counter(SQLModel, table=True): + __tablename__ = "counter" # type: ignore + name: str = Field(primary_key=True, index=True) + value: int = Field(default=0) + \ No newline at end of file diff --git a/example/router/__init__.py b/example/router/__init__.py index 087cce9..e6d582e 100644 --- a/example/router/__init__.py +++ b/example/router/__init__.py @@ -4,6 +4,7 @@ from .multiply import multiply from .divide import divide from .count import count_get + __all__ = [ "add", "add_post", diff --git a/example/router/add.py b/example/router/add.py index 364c1f9..c93d14c 100644 --- a/example/router/add.py +++ b/example/router/add.py @@ -32,8 +32,10 @@ async def add(add_get_request:AddGetRequest) -> AddGetResponse: summary="Add two numbers via POST", response_model=AddPostResponse, ) -async def add_post(add_post_request: AddPostRequest, session: zenoh.Session) -> AddPostResponse: +async def add_post(add_post_request: AddPostRequest, session: zenoh.Session, user_info: dict | None = None) -> AddPostResponse: """加法运算(POST)— 通过 JSON Body 传入 a、b,返回它们的和""" + print("----------user_info-----------") + print(user_info) async with Client(session=session) as client: add_result = await client.call( worker_type="example", diff --git a/example/router/count.py b/example/router/count.py index 92c237c..11b0fdd 100644 --- a/example/router/count.py +++ b/example/router/count.py @@ -1,4 +1,4 @@ -import modelvault +from sqlalchemy import select, update from chongming_worker import rpc_method @@ -15,19 +15,28 @@ from ..models.counter import Counter async def count_get() -> CountGetResponse: from ..global_var import db - # 确保数据库已初始化(如果未初始化,则同步初始化,但这里假设已就绪) if db is None: raise RuntimeError("Database not initialized") - - counters = modelvault.models.async_collection(db, Counter) # type: ignore - async with db.transaction(): - counter = await counters.get("visits") + async with db.get_session() as session: + statement = select(Counter).where(Counter.name == "visits") # type: ignore + result = await session.execute(statement) + counter = result.scalar_one_or_none() + if counter is None: - counter = Counter(name="visits", value=0) - await counters.insert(counter) + counter = Counter(name="visits", value=1) + session.add(counter) + await session.commit() + return CountGetResponse(name="visits", value=1) + else: new_value = counter.value + 1 - await counters.update("visits", {"value": new_value}) - counter.value = new_value # 更新本地对象以便返回 - return CountGetResponse(name=counter.name, value=counter.value) # type: ignore + update_stmt = ( + update(Counter) + .where(Counter.name == "visits") # type: ignore + .values(value=new_value) + ) + await session.execute(update_stmt) + await session.commit() + + return CountGetResponse(name="visits", value=new_value) diff --git a/example/router/divide.py b/example/router/divide.py index 44d136f..b5bb7b0 100644 --- a/example/router/divide.py +++ b/example/router/divide.py @@ -26,4 +26,4 @@ async def divide(divide_get_request: DivideGetRequest) -> DivideGetResponse: result=divide_get_request.a / divide_get_request.b, operation="divide", timestamp=time.time(), - ) \ No newline at end of file + ) diff --git a/example/router/multiply.py b/example/router/multiply.py index ff082f9..eef9494 100644 --- a/example/router/multiply.py +++ b/example/router/multiply.py @@ -19,4 +19,4 @@ async def multiply(multiply_get_request=MultiplyGetRequest) -> MultiplyGetRespon result=multiply_get_request.a * multiply_get_request.b, operation="multiply", timestamp=time.time(), - ) \ No newline at end of file + ) diff --git a/example/router/subtract.py b/example/router/subtract.py index 1c8b7fb..b08fe85 100644 --- a/example/router/subtract.py +++ b/example/router/subtract.py @@ -19,4 +19,4 @@ async def subtract(subtract_get_request: SubtractGetRequest) -> SubtractGetRespo result=subtract_get_request.a - subtract_get_request.b, operation="subtract", timestamp=time.time(), - ) \ No newline at end of file + )