Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"data": "guard",
"unit": "test",
"migrate": "Husky",
"business": "components"
"business": "components",
"dog": "Husky"
},
"material-icon-theme.files.associations": {
".env.mock": "Tune"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pattern = ".*\"version\":\\s*\"(?P<version>[^\"]+)\""
filename = "requirements.txt"

[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] # 动态设置可选依赖
oauth = ["requirements-swan.txt"] # 连接swanlab云端的可选依赖
oauth = ["requirements-cloud.txt"] # 连接swanlab云端的可选依赖

[tool.hatch.metadata.hooks.fancy-pypi-readme] # 动态设置readme
content-type = "text/markdown"
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ numpy
fastapi
uvicorn>=0.14.0
click
requests


# database
Expand Down
12 changes: 12 additions & 0 deletions swanlab/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024-03-18 21:28:47
@File: swanlab/data/auth/__init__.py
@IDE: vscode
@Description:
用户认证模块,当用户登录时,需要验证用户的身份
这一块因为可能swanlog模块没有初始化,所以需要自己单独打印一下
"""
from .login import terminal_login, code_login
from .experiment import get_exp_token
59 changes: 59 additions & 0 deletions swanlab/auth/experiment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024-03-19 19:22:12
@File: swanlab/auth/experiment.py
@IDE: vscode
@Description:
实验认证接口,此时用户已登录,但是可能认证失败,需要重新认证
"""
import asyncio
import requests
from ..utils import FONT
from ..env import is_login, get_user_api_key
from ..error import NotLoginError, TokenFileError
from .info import ExpInfo


async def _get_exp_token(user_token: str):
"""用户登录,异步调用

Parameters
----------
user_token : str
用户api_key经过后端验证后的token
"""
await asyncio.sleep(5)
# TODO 如果token是12345,模拟错误
if user_token == "12345":
return None
return "token"


async def get_exp_token():
"""通过apikey获取实验令牌
接下来通过此令牌上传实验日志
获取令牌的途中显示转圈圈,表示正在获取
"""
if not is_login():
raise NotLoginError("Please login first")
# 此时get_user_api_key必然成功
api_key = get_user_api_key()
command = FONT.bold("swanlab login --relogin")
print(FONT.swanlab("API key is configured. Use` " + command + "` to force relogin"))
login_task = asyncio.create_task(_get_exp_token(api_key))
# 显示加载动画
prefix = FONT.bold(FONT.blue("swanlab: "))
tip = "Creating experiment in swanlab cloud..."
loading_task = asyncio.create_task(FONT.loading(tip, interval=0.5, prefix=prefix))
data = await login_task
# 取消加载动画任务
loading_task.cancel()
# 最后需要刷去当前行, 不再显示加载动画
FONT.brush("")
# 在此完成错误处理,比如后端请求失败之类的,直接抛出错误
if data is None:
raise TokenFileError("Failed to get experiment token: 500")
# TODO 其他错误就直接返回状态码

return "token"
50 changes: 50 additions & 0 deletions swanlab/auth/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024-03-20 14:41:25
@File: swanlab/auth/info.py
@IDE: vscode
@Description:
定义认证数据格式
"""
from ..utils.token import save_token
from ..env import get_api_key_file_path
from ..utils.package import get_host_api


class LoginInfo:
"""
登录信息类,负责解析登录接口返回的信息,并且进行保存
无论接口请求成功还是失败,都会初始化一个LoginInfo对象
"""

def __init__(self, api_key: str, **kwargs):
self.api_key = api_key

@property
def is_fail(self):
"""
判断登录是否失败
"""
# TODO 作为测试,api_key如果为123456时返回None
return self.api_key == "123456"

def __str__(self) -> str:
return f"LoginInfo"

def save(self):
"""
保存登录信息
"""
return save_token(get_api_key_file_path(), get_host_api(), "user", self.api_key)


class ExpInfo:
"""
实验信息类,负责解析实验注册接口返回的信息,并且进行保存
包含实验token和实验信息,也会包含其他的信息
"""

def __init__(self, token: str, **kwargs):
self.token = token
self.kwargs = kwargs
114 changes: 114 additions & 0 deletions swanlab/auth/login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024-03-19 15:30:24
@File: swanlab/auth/login.py
@IDE: vscode
@Description:
用户登录接口,输入用户的apikey,保存用户token到本地
"""
import asyncio
from ..error import ValidationError
from ..utils import FONT
from ..utils.package import USER_SETTING_PATH
import sys
from .info import LoginInfo
import getpass


async def _login(api_key: str, timeout: int = 20) -> LoginInfo:
"""用户登录,异步调用接口完成验证
返回后端内容(dict),如果后端请求失败,返回None

Parameters
----------
api_key : str
用户api_key
timeout : int, optional
请求认证的超时时间,单位秒
"""
await asyncio.sleep(5)
# api key写入token文件
login_info = LoginInfo(api_key)
not login_info.is_fail and login_info.save()
return login_info


def input_api_key(
tip: str = "Paste an API key from your profile and hit enter, or press 'CTRL-C' to quit: ",
again: bool = False,
) -> str:
"""让用户输入apikey
此时有两条消息,第一条消息为固定格式,第二条消息

Parameters
----------
str : str
用户api_key
again : bool, optional
是否是重新输入api_key,如果是,不显示额外的提示信息

"""
_t = sys.excepthook
sys.excepthook = _abort_tip
if not again:
print(FONT.swanlab("Logging into swanlab cloud."))
print(FONT.swanlab("You can find your API key at: " + USER_SETTING_PATH))
key = getpass.getpass(FONT.swanlab(tip))
sys.excepthook = _t
return key


async def code_login(api_key: str):
"""
代码内登录,此时会覆盖本地token文件

Parameters
----------
api_key : str
用户api_key
save : bool, optional
是否保存api_key到本地token文件
"""
login_task = asyncio.create_task(_login(api_key))
prefix = FONT.bold(FONT.blue("swanlab: "))
tip = "Waiting for the swanlab cloud response."
loading_task = asyncio.create_task(FONT.loading(tip, interval=0.5, prefix=prefix))
login_info: LoginInfo = await login_task
# 取消加载动画任务
loading_task.cancel()
# 最后需要刷去当前行, 不再显示加载动画
FONT.brush("", length=100, flush=False)
if login_info.is_fail:
print(FONT.swanlab("Login failed! Please try again.", color="red"))
raise ValidationError("Login failed: " + str(login_info))


def terminal_login(api_key: str = None):
"""
终端登录,此时直接覆盖本地token文件,但是新增交互,让用户输入api_key
运行此函数,如果是认证失败的错误,重新要求用户输入api_key
"""
# 1. api_key存在,跳过输入环节,直接请求登录接口,这与代码内swanlab.login方法一致
# 2. api_key为None,提示用户输入
input_key = api_key is None
if api_key is None:
api_key = input_api_key()
while True:
try:
asyncio.run(code_login(api_key))
break
# 如果是登录失败且是输入的api_key,提示重新输入api_key
except ValidationError as e:
if input_key:
api_key = input_api_key("Please try again, or press 'CTRL-C' to quit: ", True)
else:
raise e


def _abort_tip(tp, val, tb):
"""处理用户在input_api_key输入时按下CTRL+C的情况"""
if tp == KeyboardInterrupt:
print("\n" + FONT.swanlab("Aborted!", color="red"))
sys.exit(0)
# 如果不是CTRL+C,交给默认的异常处理
40 changes: 37 additions & 3 deletions swanlab/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
import click
from .utils import is_valid_ip, is_valid_port, is_valid_root_dir, URL
from ..utils import FONT, version_limit
from ..env import get_server_host, get_server_port, get_swanlog_dir
from ..env import get_server_host, get_server_port, get_swanlog_dir, is_login
import time
from ..db import connect
from ..utils import get_package_version
from ..error import TokenFileError
from ..auth import terminal_login


@click.group(invoke_without_command=True)
Expand All @@ -23,6 +25,9 @@ def cli():
pass


# ---------------------------------- watch命令,开启本地后端服务 ----------------------------------


@cli.command()
# 控制服务发布的ip地址
@click.option(
Expand Down Expand Up @@ -95,8 +100,9 @@ def watch(log_level: str, **kwargs):
tip = "\n".join([URL(i, port).__str__() for i in ipv4])
else:
tip = URL(host, port).__str__()
tip = tip + "\n"
swl.info(f"SwanLab Experiment Dashboard ready in " + FONT.bold(take_time) + tip)
tip = tip + "\n" + URL.last_tip() + "\n"
v = FONT.bold("v" + get_package_version())
swl.info(f"SwanLab Experiment Dashboard " + v + " ready in " + FONT.bold(take_time) + tip)

# ---------------------------------- 启动服务 ----------------------------------
# 使用 uvicorn 启动 FastAPI 应用,关闭原生日志
Expand All @@ -112,5 +118,33 @@ def watch(log_level: str, **kwargs):
swl.critical("Unhandled Exit Code: {}".format(code))


# ---------------------------------- 登录命令,进行登录 ----------------------------------
# @cli.command()
# @click.option(
# "--relogin",
# "-r",
# is_flag=True,
# default=False,
# help="Relogin to the swanlab cloud, it will recover the token file.",
# )
# @click.option(
# "--api-key",
# "-k",
# default=None,
# type=str,
# help="If you prefer not to engage in command-line interaction to input the api key, this will allow automatic login.",
# )
# def login(api_key: str, relogin: bool, **kwargs):
# """Login to the swanlab cloud."""
# # 其实还可以有别的方式,但是现阶段只有输入api key的方式,直接运行login函数即可
# if not relogin and is_login():
# # 此时代表token已经获取,需要打印一条信息:已经登录
# command = FONT.bold("swanlab login --relogin")
# tip = FONT.swanlab("You are already logged in. Use `" + command + "` to force relogin.")
# return print(tip)
# # 进行登录,此时将直接覆盖本地token文件
# terminal_login(api_key)


if __name__ == "__main__":
cli()
17 changes: 13 additions & 4 deletions swanlab/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,30 @@ def is_valid_root_dir(ctx, param, log_dir: str) -> str:

class URL(object):
# 生成链接提示,先生成各个组件
arrow = FONT.green("\t\t\t➜")
local = arrow + " Local: "
netwo = arrow + " Network: "
_arrow = "\t\t\t➜"
arrow = FONT.bold(FONT.green(_arrow))
local = arrow + FONT.bold(" Local: ")
netwo = arrow + FONT.bold(" Network: ")

def __init__(self, ip, port) -> None:
self.ip = ip
self.port = port

def __str__(self) -> str:
url = FONT.bold(f"http://{self.ip}:{self.port}")
url = FONT.blue(f"http://{self.ip}:{self.port}")
if self.is_localhost(self.ip):
return self.local + url
else:
return self.netwo + url

@classmethod
def last_tip(cls) -> str:
"""
打印最后一条提示信息
"""
t = FONT.dark_gray(" press ") + FONT.bold(FONT.default("ctrl + c")) + FONT.dark_gray(" to quit")
return FONT.dark_green(cls._arrow) + t

@staticmethod
def is_localhost(ip):
return ip == "127.0.0.1" or ip == "localhost"
Expand Down
4 changes: 4 additions & 0 deletions swanlab/data/dog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 看门小狗🐶

看门狗用于嗅探并监视文件资源、系统信息,并且完成向服务端推送的工作。
本部分不涉及认证工作,单纯进行嗅探。认证的token应该在在其他模块启动小狗时传入。
9 changes: 9 additions & 0 deletions swanlab/data/dog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024-03-17 16:57:32
@File: swanlab/data/dog/__init__.py
@IDE: vscode
@Description:
看门狗模块,导出run函数,开启看门狗,新建进程
"""
Loading