parent
2cb0f1a8f5
commit
179f46b2b3
@ -1,90 +0,0 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 4月 |
||||
# author: liuyu |
||||
# date: 2020/4/7 |
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler |
||||
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job |
||||
from fir_ser.settings import SYNC_CACHE_TO_DATABASE, GEETEST_CYCLE_TIME |
||||
from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_job_log, \ |
||||
auto_delete_tmp_file, auto_check_ios_developer_active |
||||
import logging |
||||
from api.utils.storage.storage import get_local_storage |
||||
from api.utils.geetest.geetest_utils import check_bypass_status |
||||
|
||||
logger = logging.getLogger(__file__) |
||||
|
||||
import atexit |
||||
import fcntl |
||||
|
||||
logging.basicConfig() |
||||
logging.getLogger('apscheduler').setLevel(logging.ERROR) |
||||
|
||||
# 主要是为了防止多进程出现的多个定时任务同时执行 |
||||
f = open("scheduler.lock", "wb") |
||||
try: |
||||
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) |
||||
|
||||
# 开启定时工作 |
||||
try: |
||||
# 实例化调度器 |
||||
scheduler = BackgroundScheduler() |
||||
# 调度器使用DjangoJobStore() |
||||
scheduler.add_jobstore(DjangoJobStore(), "default") |
||||
|
||||
|
||||
# 另一种方式为每天固定时间执行任务,对应代码为: |
||||
# @register_job(scheduler, 'cron', day_of_week='mon-fri', hour='9', minute='30', second='10',id='task_time') |
||||
|
||||
@register_job(scheduler, "interval", seconds=SYNC_CACHE_TO_DATABASE.get("download_times")) |
||||
def sync_download_times_job(): |
||||
# 这里写你要执行的任务 |
||||
sync_download_times() |
||||
|
||||
|
||||
@register_job(scheduler, "interval", seconds=GEETEST_CYCLE_TIME) |
||||
def check_bypass_status_job(): |
||||
check_bypass_status() |
||||
|
||||
|
||||
@register_job(scheduler, "interval", seconds=SYNC_CACHE_TO_DATABASE.get("auto_clean_tmp_file_times")) |
||||
def auto_clean_upload_tmp_file_job(): |
||||
auto_clean_upload_tmp_file() |
||||
auto_delete_job_log() |
||||
|
||||
|
||||
@register_job(scheduler, "interval", seconds=SYNC_CACHE_TO_DATABASE.get("auto_clean_local_tmp_file_times")) |
||||
def auto_delete_tmp_file_job(): |
||||
auto_delete_tmp_file() |
||||
|
||||
|
||||
@register_job(scheduler, "interval", |
||||
seconds=SYNC_CACHE_TO_DATABASE.get("auto_check_ios_developer_active_times")) |
||||
def auto_check_ios_developer_active_job(): |
||||
auto_check_ios_developer_active() |
||||
|
||||
|
||||
register_events(scheduler) |
||||
scheduler.start() |
||||
|
||||
# 启动服务的时候,同时执行下面操作 |
||||
get_local_storage(clean_cache=True) |
||||
check_bypass_status() |
||||
|
||||
except Exception as e: |
||||
logger.error("scheduler failed,so shutdown it Exception:%s" % (e)) |
||||
# 有错误就停止定时器 |
||||
scheduler.shutdown() |
||||
|
||||
|
||||
except: |
||||
pass |
||||
|
||||
|
||||
def unlock(): |
||||
fcntl.flock(f, fcntl.LOCK_UN) |
||||
f.close() |
||||
|
||||
|
||||
atexit.register(unlock) |
@ -1,53 +0,0 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 4月 |
||||
# author: liuyu |
||||
# date: 2020/4/7 |
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler |
||||
from django_apscheduler.jobstores import DjangoJobStore, register_events, register_job |
||||
from fir_ser.settings import SYNC_CACHE_TO_DATABASE |
||||
from api.utils.crontab.ctasks import sync_download_times |
||||
|
||||
# import atexit |
||||
# import fcntl |
||||
# |
||||
# # 主要是为了防止多进程出现的多个定时任务同时执行 |
||||
# f = open("scheduler.lock", "wb") |
||||
# try: |
||||
# fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) |
||||
|
||||
# 开启定时工作 |
||||
try: |
||||
# 实例化调度器 |
||||
scheduler = BackgroundScheduler() |
||||
# 调度器使用DjangoJobStore() |
||||
scheduler.add_jobstore(DjangoJobStore(), "default") |
||||
|
||||
|
||||
# 设置定时任务,选择方式为interval,时间间隔为10s |
||||
# 另一种方式为每天固定时间执行任务,对应代码为: |
||||
# @register_job(scheduler, 'cron', day_of_week='mon-fri', hour='9', minute='30', second='10',id='task_time') |
||||
@register_job(scheduler, "interval", seconds=SYNC_CACHE_TO_DATABASE.get("download_times")) |
||||
def sync_download_times_job(): |
||||
# 这里写你要执行的任务 |
||||
sync_download_times() |
||||
|
||||
|
||||
register_events(scheduler) |
||||
scheduler.start() |
||||
except Exception as e: |
||||
print(e) |
||||
# 有错误就停止定时器 |
||||
scheduler.shutdown() |
||||
# |
||||
# except: |
||||
# pass |
||||
# |
||||
# |
||||
# def unlock(): |
||||
# fcntl.flock(f, fcntl.LOCK_UN) |
||||
# f.close() |
||||
# |
||||
# |
||||
# atexit.register(unlock) |
@ -0,0 +1,130 @@ |
||||
from functools import wraps, WRAPPER_ASSIGNMENTS |
||||
|
||||
from django.http.response import HttpResponse |
||||
|
||||
|
||||
def get_cache(alias): |
||||
from django.core.cache import caches |
||||
return caches[alias] |
||||
|
||||
|
||||
class CacheResponse: |
||||
""" |
||||
Store/Receive and return cached `HttpResponse` based on DRF response. |
||||
.. note:: |
||||
This decorator will render and discard the original DRF response in |
||||
favor of Django's `HttpResponse`. The allows the cache to retain a |
||||
smaller memory footprint and eliminates the need to re-render |
||||
responses on each request. Furthermore it eliminates the risk for users |
||||
to unknowingly cache whole Serializers and QuerySets. |
||||
""" |
||||
|
||||
def __init__(self, |
||||
timeout=None, |
||||
key_func=None, |
||||
cache=None, |
||||
cache_errors=None): |
||||
if timeout is None: |
||||
self.timeout = None |
||||
else: |
||||
self.timeout = timeout |
||||
|
||||
if key_func is None: |
||||
self.key_func = '' |
||||
else: |
||||
self.key_func = key_func |
||||
|
||||
if cache_errors is None: |
||||
self.cache_errors = True |
||||
else: |
||||
self.cache_errors = cache_errors |
||||
|
||||
self.cache = get_cache(cache or 'default') |
||||
|
||||
def __call__(self, func): |
||||
this = self |
||||
|
||||
@wraps(func, assigned=WRAPPER_ASSIGNMENTS) |
||||
def inner(self, request, *args, **kwargs): |
||||
return this.process_cache_response( |
||||
view_instance=self, |
||||
view_method=func, |
||||
request=request, |
||||
args=args, |
||||
kwargs=kwargs, |
||||
) |
||||
|
||||
return inner |
||||
|
||||
def process_cache_response(self, |
||||
view_instance, |
||||
view_method, |
||||
request, |
||||
args, |
||||
kwargs): |
||||
|
||||
key = self.calculate_key( |
||||
view_instance=view_instance, |
||||
view_method=view_method, |
||||
request=request, |
||||
args=args, |
||||
kwargs=kwargs |
||||
) |
||||
|
||||
timeout = self.calculate_timeout(view_instance=view_instance) |
||||
|
||||
response_triple = self.cache.get(key) |
||||
if not response_triple: |
||||
# render response to create and cache the content byte string |
||||
response = view_method(view_instance, request, *args, **kwargs) |
||||
response = view_instance.finalize_response(request, response, *args, **kwargs) |
||||
response.render() |
||||
|
||||
if not response.status_code >= 400 or self.cache_errors: |
||||
# django 3.0 has not .items() method, django 3.2 has not ._headers |
||||
if hasattr(response, '_headers'): |
||||
headers = response._headers.copy() |
||||
else: |
||||
headers = {k: (k, v) for k, v in response.items()} |
||||
response_triple = ( |
||||
response.rendered_content, |
||||
response.status_code, |
||||
headers |
||||
) |
||||
self.cache.set(key, response_triple, timeout) |
||||
else: |
||||
# build smaller Django HttpResponse |
||||
content, status, headers = response_triple |
||||
response = HttpResponse(content=content, status=status) |
||||
for k, v in headers.values(): |
||||
response[k] = v |
||||
if not hasattr(response, '_closable_objects'): |
||||
response._closable_objects = [] |
||||
|
||||
return response |
||||
|
||||
def calculate_key(self, |
||||
view_instance, |
||||
view_method, |
||||
request, |
||||
args, |
||||
kwargs): |
||||
if isinstance(self.key_func, str): |
||||
key_func = getattr(view_instance, self.key_func) |
||||
else: |
||||
key_func = self.key_func |
||||
return key_func( |
||||
view_instance=view_instance, |
||||
view_method=view_method, |
||||
request=request, |
||||
args=args, |
||||
kwargs=kwargs, |
||||
) |
||||
|
||||
def calculate_timeout(self, view_instance, **_): |
||||
if isinstance(self.timeout, str): |
||||
self.timeout = getattr(view_instance, self.timeout) |
||||
return self.timeout |
||||
|
||||
|
||||
cache_response = CacheResponse |
@ -0,0 +1,276 @@ |
||||
|
||||
导入默认数据 |
||||
python manage.py dumpdata api.Price api.DomainCnameInfo > dumpdata.json |
||||
python manage.py loaddata dumpdata.json |
||||
|
||||
#启动超级签工作者 |
||||
#celery -A fir_ser worker --scheduler django --loglevel=debug |
||||
|
||||
启动多节点 |
||||
celery multi start 4 -A fir_ser -l INFO -c4 --pidfile=/var/run/celery/%n.pid --logfile=logs/%p.log |
||||
|
||||
启动beat |
||||
celery -A fir_ser beat --scheduler django -l INFO |
||||
|
||||
|
||||
#开发测试 |
||||
celery -A fir_ser worker --beat --scheduler django --loglevel=debug |
||||
|
||||
var app_id = 21254; |
||||
var app_name = ''; |
||||
var btnstr = '免费安装'; |
||||
var udid= ''; |
||||
var tipstr= ''; |
||||
var btnstatus = '1'; |
||||
var urlscheme = 'wxec1a63d7795bef19'; |
||||
var android_url = ''; |
||||
var geetest = JSON.parse('{}'); |
||||
|
||||
window.onload = function () { |
||||
Vue.use(VueAwesomeSwiper) |
||||
Vue.component(VueQrcode.name, VueQrcode) |
||||
const router = new VueRouter({ |
||||
mode: 'history' |
||||
}) |
||||
new Vue({ |
||||
el: '#app', |
||||
router, |
||||
data: { |
||||
init: false, |
||||
message: 'Hello Vue.js!', |
||||
app_id: app_id, |
||||
udid: udid, |
||||
btnstr: btnstr, |
||||
tipstr: tipstr, |
||||
btnstatus: btnstatus, |
||||
app_name : app_name, |
||||
permissioncodedialog: false, |
||||
btndownloading: false, |
||||
showgosafari: false, |
||||
showgobrowser: false, |
||||
showsteptips: false, |
||||
showmchelp: false, |
||||
showios12help: false, |
||||
ismobile: false, |
||||
isandroid: false, |
||||
copyurl: 'text', |
||||
permissioncode: '', |
||||
urlscheme: urlscheme, |
||||
swiperOption: { |
||||
pagination: { |
||||
el: '.swiper-pagination' |
||||
} |
||||
}, |
||||
|
||||
swiper2option: { |
||||
scrollbar: '.imgs-box .swiper-scrollbar', |
||||
scrollbarHide: true, |
||||
slidesPerView: 'auto', |
||||
grabCursor: true |
||||
}, |
||||
|
||||
captchaObj: {} |
||||
}, |
||||
mounted() { |
||||
var thats = this |
||||
if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mac|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) { |
||||
this.ismobile = true |
||||
this.isandroid = navigator.userAgent.indexOf('Android') > -1 || navigator.userAgent.indexOf('Adr') > -1; |
||||
} |
||||
this.copyurl = location.href |
||||
var copyBtn = new ClipboardJS('.gosafari .copy-url button'); |
||||
copyBtn.on('success', function (e) { |
||||
thats.gosafari() |
||||
alert('链接复制成功,快去分享吧~'); |
||||
}); |
||||
|
||||
var copyBtn = new ClipboardJS('.gobrowser .copy-url button'); |
||||
copyBtn.on('success', function (e) { |
||||
thats.gobrowser() |
||||
alert('链接复制成功,快去分享吧~'); |
||||
}); |
||||
|
||||
copyBtn.on('error', function (e) { |
||||
console.log(e); |
||||
}); |
||||
document.getElementById('app').style.display = 'block'; |
||||
if (this.$route.query.autorequest == 1) { |
||||
this.install() |
||||
} |
||||
if (this.$route.query.code) { |
||||
this.permissioncode = this.$route.query.code |
||||
} |
||||
|
||||
thats.init = true |
||||
|
||||
}, |
||||
methods: { |
||||
getudid() { |
||||
let button = document.createElement('button'); |
||||
let clipboard = new ClipboardJS(button, { |
||||
text: () => this.udid |
||||
}) |
||||
clipboard.on('success', event => { |
||||
event.clearSelection(); |
||||
weui.toast('复制成功'); |
||||
}); |
||||
|
||||
clipboard.on('error', event => { |
||||
event.clearSelection(); |
||||
weui.toast('复制失败'); |
||||
}); |
||||
weui.alert('如果出现无法安装等情况,请将下面的设备号复制一份发给我们!<br/><br/>当前设备:' + this.udid, { |
||||
className: "text-align-left", |
||||
buttons: [{ |
||||
label: '复制设备号', |
||||
type: 'primary', |
||||
onClick: () => { |
||||
button.click() |
||||
} |
||||
}] |
||||
}); |
||||
}, |
||||
gosafari() { |
||||
this.showgosafari = !this.showgosafari |
||||
}, |
||||
gobrowser() { |
||||
this.showgobrowser = !this.showgobrowser |
||||
}, |
||||
install() { |
||||
if (this.isandroid) { |
||||
if(this.isWeChat()) { |
||||
this.gobrowser() |
||||
} else { |
||||
this.jsgotourl(android_url) |
||||
} |
||||
} else if (this.isiOSSafari()) { |
||||
if (this.btnstatus == 1) { |
||||
this.goinstall() |
||||
} else if (this.btnstatus == 2) { |
||||
this.doinstall() |
||||
} else if (this.btnstatus == 4) { |
||||
this.jsgotourl(this.urlscheme + "://") |
||||
} |
||||
} else { |
||||
this.gosafari() |
||||
} |
||||
}, |
||||
|
||||
goinstall() { |
||||
this.jsgotourl('/Kirin/MobileConfig?app_id=' + this.app_id + '&code=' + this.permissioncode) |
||||
|
||||
var str = navigator.userAgent.toLowerCase(); |
||||
var ver = str.match(/os (.*?) like mac os/); |
||||
if (ver) { |
||||
ver = ver[1].replace(/_/g, ".") |
||||
} |
||||
|
||||
if (ver && this.cpr_version(ver, "12.2") >= 0) { |
||||
var thats = this |
||||
setTimeout(function () { |
||||
thats.jsgotourl('/Kirin/MobileConfig/JumpToSetting') |
||||
}, 5000) |
||||
} |
||||
}, |
||||
doinstall() { |
||||
var thats = this |
||||
axios.get('/Kirin/Download/Request?udid=' + this.udid + '&app_id=' + this.app_id + '&permissioncode=' + this.permissioncode).then(function(response){ |
||||
if (response.data.errno == 2005) { |
||||
thats.permissioncodedialog = true |
||||
} else if (response.data.errno !== 0) { |
||||
thats.$message({ |
||||
message: response.data.errmsg, |
||||
type: 'error' |
||||
}); |
||||
} else { |
||||
thats.permissioncodedialog = false |
||||
location.href = response.data.data.plist_url |
||||
thats.getprocess(response.data.data.task_id) |
||||
} |
||||
}) |
||||
}, |
||||
getprocess(task_id) { |
||||
var thats = this |
||||
axios.get('/Kirin/Download/Process?task_id=' + task_id).then(function(response){ |
||||
if(response.data.errno !== 0) { |
||||
thats.$message({ |
||||
message: response.data.errmsg, |
||||
type: 'error' |
||||
}); |
||||
} else { |
||||
if (response.data.data.status == 1) { |
||||
thats.getinstallprocess(response.data.data.installtime) |
||||
} else { |
||||
thats.btndownloading = true |
||||
thats.btnstr = "<span>下载中 " + response.data.data.process + "%</span><em style='width: " + response.data.data.process + "%'></em>" |
||||
setTimeout(function () { |
||||
thats.getprocess(task_id); |
||||
}, 1000) |
||||
} |
||||
} |
||||
}) |
||||
}, |
||||
getinstallprocess(installtime) { |
||||
var thats = this |
||||
thats.btnstr = "安装中" |
||||
thats.btndownloading = false |
||||
thats.btnstatus = 3 |
||||
setTimeout(function () { |
||||
thats.btnstr = "请回到桌面查看" |
||||
}, 1000 * installtime) |
||||
}, |
||||
gosteptips() { |
||||
this.showsteptips = !this.showsteptips |
||||
}, |
||||
gomchelp() { |
||||
this.showmchelp = !this.showmchelp |
||||
}, |
||||
jsgotourl(url) { |
||||
// 创建隐藏的可下载链接 |
||||
var eleLink = document.createElement('a'); |
||||
eleLink.style.display = 'none'; |
||||
eleLink.href = url; |
||||
// 触发点击 |
||||
document.body.appendChild(eleLink); |
||||
eleLink.click(); |
||||
// 然后移除 |
||||
document.body.removeChild(eleLink); |
||||
}, |
||||
toNum(a) { |
||||
var a = a.toString(); |
||||
//也可以这样写 var c=a.split(/\./); |
||||
var c = a.split('.'); |
||||
var num_place = ["", "0", "00", "000", "0000"], r = num_place.reverse(); |
||||
for (var i = 0; i < c.length; i++) { |
||||
var len = c[i].length; |
||||
c[i] = r[len] + c[i]; |
||||
} |
||||
var res = c.join(''); |
||||
return res; |
||||
}, |
||||
cpr_version(a, b) { |
||||
var _a = this.toNum(a), _b = this.toNum(b); |
||||
if (_a == _b) return 0; |
||||
if (_a > _b) return 1; |
||||
if (_a < _b) return -1; |
||||
}, |
||||
isiOSSafari() { |
||||
var userAgent = navigator.userAgent |
||||
return (/(iPad|iPhone|iPod|Mac)/gi).test(userAgent) && |
||||
(/Safari/).test(userAgent) && |
||||
!(/CriOS/).test(userAgent) && |
||||
!(/FxiOS/).test(userAgent) && |
||||
!(/OPiOS/).test(userAgent) && |
||||
!(/mercury/).test(userAgent) && |
||||
!(/UCBrowser/).test(userAgent) && |
||||
!(/Baidu/).test(userAgent) && |
||||
!(/MicroMessenger/).test(userAgent) && |
||||
!(/QQ/i).test(userAgent) |
||||
}, |
||||
isWeChat() { |
||||
var userAgent = navigator.userAgent |
||||
return (/MicroMessenger/).test(userAgent) |
||||
} |
||||
} |
||||
}) |
||||
} |
@ -1,96 +1,17 @@ |
||||
aliyun-python-sdk-core==2.13.15 |
||||
aliyun-python-sdk-core-v3==2.13.11 |
||||
aliyun-python-sdk-kms==2.10.1 |
||||
aliyun-python-sdk-sts==3.0.1 |
||||
androguard==3.3.5 |
||||
appdirs==1.4.4 |
||||
APScheduler==3.6.3 |
||||
asgiref==3.2.3 |
||||
asn1crypto==1.3.0 |
||||
backcall==0.1.0 |
||||
bcrypt==3.1.7 |
||||
biplist==1.0.3 |
||||
certifi==2019.11.28 |
||||
cffi==1.14.0 |
||||
chardet==3.0.4 |
||||
Click==7.0 |
||||
codecov==2.1.11 |
||||
colorama==0.4.3 |
||||
construct==2.10.61 |
||||
coverage==5.5 |
||||
crcmod==1.7 |
||||
cryptodemo==0.0.1 |
||||
Cryptodome==1.0 |
||||
cryptography==3.4.6 |
||||
cycler==0.10.0 |
||||
decorator==4.4.2 |
||||
distlib==0.3.1 |
||||
Django==3.0.3 |
||||
django-apscheduler==0.3.0 |
||||
django-qrcode==0.3 |
||||
django-ranged-response==0.2.0 |
||||
django-redis==4.11.0 |
||||
django-rest-framework==0.1.0 |
||||
django-simple-captcha==0.5.12 |
||||
djangorestframework==3.11.0 |
||||
dnspython==1.16.0 |
||||
drf-extensions==0.6.0 |
||||
filelock==3.0.12 |
||||
future==0.18.2 |
||||
idna==2.9 |
||||
importlib-metadata==3.10.0 |
||||
importlib-resources==5.1.2 |
||||
ipython==7.13.0 |
||||
ipython-genutils==0.2.0 |
||||
jedi==0.16.0 |
||||
jmespath==0.9.5 |
||||
kiwisolver==1.1.0 |
||||
lxml==4.5.0 |
||||
matplotlib==3.2.0 |
||||
memoizer==0.0.1 |
||||
mock==2.0.0 |
||||
mysqlclient==1.4.6 |
||||
networkx==2.4 |
||||
numpy==1.18.1 |
||||
oss2==2.9.1 |
||||
packaging==20.9 |
||||
paramiko==2.7.1 |
||||
parso==0.6.2 |
||||
pbr==5.5.1 |
||||
pexpect==4.8.0 |
||||
pickleshare==0.7.5 |
||||
Pillow==7.0.0 |
||||
pluggy==0.13.1 |
||||
prompt-toolkit==3.0.3 |
||||
ptyprocess==0.6.0 |
||||
py==1.10.0 |
||||
pyasn1==0.4.8 |
||||
pycparser==2.20 |
||||
pycryptodome==3.10.1 |
||||
pycryptodomex==3.9.4 |
||||
pydot==1.4.1 |
||||
Pygments==2.5.2 |
||||
PyJWT==1.7.1 |
||||
PyMySQL==0.9.3 |
||||
PyNaCl==1.3.0 |
||||
pyOpenSSL==20.0.1 |
||||
pyparsing==2.4.6 |
||||
python-dateutil==2.8.1 |
||||
pytz==2019.3 |
||||
qiniu==7.2.8 |
||||
redis==3.4.1 |
||||
requests==2.23.0 |
||||
rsa==4.7.2 |
||||
six==1.14.0 |
||||
sqlparse==0.3.1 |
||||
toml==0.10.2 |
||||
tox==3.23.0 |
||||
traitlets==4.3.3 |
||||
typing-extensions==3.7.4.3 |
||||
tzlocal==2.0.0 |
||||
urllib3==1.25.8 |
||||
uWSGI==2.0.18 |
||||
virtualenv==20.4.3 |
||||
wcwidth==0.1.8 |
||||
celery==5.1.0 |
||||
django-celery-results==2.0.1 |
||||
django-celery-beat==2.2.0 |
||||
Django==3.2.3 |
||||
djangorestframework==3.12.4 |
||||
django-simple-captcha==0.5.14 |
||||
mysqlclient==2.0.3 |
||||
django-redis==4.12.1 |
||||
dnspython==2.1.0 |
||||
oss2==2.14.0 |
||||
aliyun-python-sdk-sts==3.0.2 |
||||
qiniu==7.4.1 |
||||
xmltodict==0.12.0 |
||||
zipp==3.4.1 |
||||
pyOpenSSL==20.0.1 |
||||
paramiko==2.7.2 |
||||
PyJWT==2.1.0 |
||||
drf-extensions==0.7.0 |
Loading…
Reference in new issue