mirror of https://github.com/jumpserver/jumpserver
perf: 优化登录
parent
379c7198da
commit
8b819f3779
|
@ -52,6 +52,8 @@ class JMSBaseAuthBackend:
|
||||||
logger.debug(info)
|
logger.debug(info)
|
||||||
return allow
|
return allow
|
||||||
|
|
||||||
|
from redis_lock.django_cache import RedisCache
|
||||||
|
from redis import StrictRedis
|
||||||
|
|
||||||
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
|
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -87,11 +87,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.jms-title {
|
.jms-title {
|
||||||
padding: 40px 10px 10px;
|
padding: 60px 10px 10px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-captcha-challenge .jms-title {
|
.no-captcha-challenge .jms-title {
|
||||||
padding: 60px 10px 10px;
|
padding: 60px 10px 10px 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-captcha-challenge .welcome-message {
|
.no-captcha-challenge .welcome-message {
|
||||||
|
@ -125,15 +125,27 @@
|
||||||
font-weight: 350 !important;
|
font-weight: 350 !important;
|
||||||
min-height: auto !important;
|
min-height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-image {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.jms-title {
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight:400;
|
||||||
|
color: #151515;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="login-content">
|
<div class="login-content">
|
||||||
<div class="right-image-box">
|
<div class="right-image-box">
|
||||||
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
|
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver.git{% endif %}">
|
||||||
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
|
<img src="{{ LOGIN_IMAGE_URL }}" class="right-image" alt="screen-image"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
|
||||||
|
@ -142,26 +154,23 @@
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
|
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
|
||||||
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
|
||||||
{% if request.COOKIES.django_language == 'en' %}
|
<span>{{ current_lang.title }}<b class="caret"></b></span>
|
||||||
<span>English<b class="caret"></b></span>
|
|
||||||
{% elif request.COOKIES.django_language == 'ja' %}
|
|
||||||
<span>日本語<b class="caret"></b></span>
|
|
||||||
{% else %}
|
|
||||||
<span>中文(简体)<b class="caret"></b></span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
|
||||||
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
|
{% for lang in langs %}
|
||||||
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
|
<li>
|
||||||
<li> <a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}"> <span>日本語</span> </a> </li>
|
<a href="{% url 'i18n-switch' lang=lang.code %}">
|
||||||
|
<span>{{ lang.title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="jms-title">
|
<div class="jms-title">
|
||||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
<span style="">{% trans 'Login' %}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-form col-md-10 col-md-offset-1">
|
<div class="contact-form col-md-10 col-md-offset-1">
|
||||||
|
|
||||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||||
|
@ -177,7 +186,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% bootstrap_field form.username show_label=False %}
|
{% bootstrap_field form.username show_label=False %}
|
||||||
|
|
||||||
<div class="form-group {% if form.password.errors %} has-error {% endif %}">
|
<div class="form-group {% if form.password.errors %} has-error {% endif %}">
|
||||||
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required>
|
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required>
|
||||||
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
|
||||||
|
|
|
@ -10,8 +10,7 @@ from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import reverse, redirect
|
from django.shortcuts import reverse, redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.db import transaction
|
from django.utils.translation import ugettext as _, get_language
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
|
@ -181,6 +180,29 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
]
|
]
|
||||||
return [method for method in auth_methods if method['enabled']]
|
return [method for method in auth_methods if method['enabled']]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_support_langs():
|
||||||
|
langs = [
|
||||||
|
{
|
||||||
|
'title': '中文(简体)',
|
||||||
|
'code': 'zh-hans'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'English',
|
||||||
|
'code': 'en'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': '日本語',
|
||||||
|
'code': 'ja'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
return langs
|
||||||
|
|
||||||
|
def get_current_lang(self):
|
||||||
|
langs = self.get_support_langs()
|
||||||
|
matched_lang = filter(lambda x: x['code'] == get_language(), langs)
|
||||||
|
return next(matched_lang, langs[0])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_forgot_password_url():
|
def get_forgot_password_url():
|
||||||
forgot_password_url = reverse('authentication:forgot-password')
|
forgot_password_url = reverse('authentication:forgot-password')
|
||||||
|
@ -191,6 +213,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||||
context = {
|
context = {
|
||||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||||
'auth_methods': self.get_support_auth_methods(),
|
'auth_methods': self.get_support_auth_methods(),
|
||||||
|
'langs': self.get_support_langs(),
|
||||||
|
'current_lang': self.get_current_lang(),
|
||||||
'forgot_password_url': self.get_forgot_password_url(),
|
'forgot_password_url': self.get_forgot_password_url(),
|
||||||
**self.get_user_mfa_context(self.request.user)
|
**self.get_user_mfa_context(self.request.user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ from itertools import chain
|
||||||
import paramiko
|
import paramiko
|
||||||
import sshpubkeys
|
import sshpubkeys
|
||||||
from itsdangerous import (
|
from itsdangerous import (
|
||||||
|
TimedSerializer,
|
||||||
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer,
|
||||||
BadSignature, SignatureExpired
|
BadSignature, SignatureExpired
|
||||||
)
|
)
|
||||||
|
|
|
@ -148,19 +148,8 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
||||||
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
||||||
SESSION_ENGINE = 'jumpserver.rewriting.session'
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||||
SESSION_REDIS = {
|
SESSION_CACHE_ALIAS = "default"
|
||||||
'url': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
|
||||||
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
|
||||||
'password': CONFIG.REDIS_PASSWORD,
|
|
||||||
'host': CONFIG.REDIS_HOST,
|
|
||||||
'port': CONFIG.REDIS_PORT,
|
|
||||||
'db': CONFIG.REDIS_DB_CACHE,
|
|
||||||
},
|
|
||||||
'prefix': 'auth_session',
|
|
||||||
'socket_timeout': 1,
|
|
||||||
'retry_on_timeout': False
|
|
||||||
}
|
|
||||||
|
|
||||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
||||||
# Database
|
# Database
|
||||||
|
@ -283,7 +272,6 @@ REDIS_SSL_REQUIRED = CONFIG.REDIS_SSL_REQUIRED or 'none'
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
# 'BACKEND': 'redis_cache.RedisCache',
|
|
||||||
'BACKEND': 'redis_lock.django_cache.RedisCache',
|
'BACKEND': 'redis_lock.django_cache.RedisCache',
|
||||||
'LOCATION': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
'LOCATION': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
|
||||||
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
|
||||||
|
|
|
@ -153,3 +153,5 @@ ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')
|
||||||
REDIS_HOST = CONFIG.REDIS_HOST
|
REDIS_HOST = CONFIG.REDIS_HOST
|
||||||
REDIS_PORT = CONFIG.REDIS_PORT
|
REDIS_PORT = CONFIG.REDIS_PORT
|
||||||
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
|
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
|
||||||
|
|
||||||
|
DJANGO_REDIS_SCAN_ITERSIZE = 1000
|
||||||
|
|
|
@ -3,9 +3,9 @@ ansible==2.10.7
|
||||||
asn1crypto==0.24.0
|
asn1crypto==0.24.0
|
||||||
bcrypt==3.1.4
|
bcrypt==3.1.4
|
||||||
billiard==3.6.4.0
|
billiard==3.6.4.0
|
||||||
boto3==1.18.11
|
boto3==1.24.12
|
||||||
botocore==1.21.11
|
botocore==1.27.12
|
||||||
celery==5.2.2
|
celery==5.2.7
|
||||||
certifi==2018.1.18
|
certifi==2018.1.18
|
||||||
cffi==1.13.2
|
cffi==1.13.2
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
|
@ -17,117 +17,118 @@ decorator==4.1.2
|
||||||
Django==3.1.14
|
Django==3.1.14
|
||||||
django-auth-ldap==2.2.0
|
django-auth-ldap==2.2.0
|
||||||
django-bootstrap3==14.2.0
|
django-bootstrap3==14.2.0
|
||||||
django-celery-beat==2.2.1
|
django-celery-beat==2.3.0
|
||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
django-formtools==2.2
|
django-formtools==2.2
|
||||||
django-ranged-response==0.2.0
|
django-ranged-response==0.2.0
|
||||||
django-rest-swagger==2.2.0
|
django-rest-swagger==2.2.0
|
||||||
django-simple-captcha==0.5.13
|
django-simple-captcha==0.5.17
|
||||||
django-timezone-field==4.1.0
|
django-timezone-field==5.0
|
||||||
djangorestframework==3.12.2
|
djangorestframework==3.13.1
|
||||||
djangorestframework-bulk==0.2.1
|
djangorestframework-bulk==0.2.1
|
||||||
docutils==0.14
|
docutils==0.14
|
||||||
ecdsa==0.13.3
|
ecdsa==0.13.3
|
||||||
enum-compat==0.0.2
|
enum-compat==0.0.2
|
||||||
ephem==3.7.6.0
|
ephem==3.7.6.0
|
||||||
eventlet==0.31.1
|
eventlet==0.33.1
|
||||||
future==0.16.0
|
future==0.16.0
|
||||||
ForgeryPy3==0.3.1
|
ForgeryPy3==0.3.1
|
||||||
greenlet==0.4.14
|
greenlet==1.1.2
|
||||||
gunicorn==19.9.0
|
gunicorn==20.1.0
|
||||||
idna==2.6
|
idna==2.6
|
||||||
itsdangerous==0.24
|
itsdangerous==1.1.0
|
||||||
itypes==1.1.0
|
itypes==1.2.0
|
||||||
Jinja2==2.11.3
|
Jinja2==3.1.2
|
||||||
jmespath==0.9.3
|
jmespath==1.0.1
|
||||||
kombu==5.2.2
|
kombu==5.2.4
|
||||||
ldap3==2.4
|
ldap3==2.9.1
|
||||||
MarkupSafe==1.1.1
|
MarkupSafe==2.1.1
|
||||||
mysqlclient==2.0.1
|
mysqlclient==2.1.0
|
||||||
olefile==0.44
|
olefile==0.46
|
||||||
openapi-codec==1.3.2
|
openapi-codec==1.3.2
|
||||||
paramiko==2.10.1
|
paramiko==2.11.0
|
||||||
passlib==1.7.1
|
passlib==1.7.4
|
||||||
Pillow==9.0.1
|
Pillow==9.1.1
|
||||||
pyasn1==0.4.8
|
pyasn1==0.4.8
|
||||||
pycparser==2.19
|
pycparser==2.21
|
||||||
pycryptodome==3.12.0
|
pycryptodome==3.14.1
|
||||||
pycryptodomex==3.12.0
|
pycryptodomex==3.14.1
|
||||||
pyotp==2.2.6
|
pyotp==2.6.0
|
||||||
PyNaCl==1.5.0
|
PyNaCl==1.5.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
pytz==2018.3
|
pytz==2022.1
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
redis==4.3.1
|
redis==4.3.3
|
||||||
requests==2.25.1
|
requests==2.28.0
|
||||||
jms-storage==0.0.42
|
# jms-storage==0.0.42
|
||||||
s3transfer==0.5.0
|
s3transfer==0.6.0
|
||||||
simplejson==3.13.2
|
simplejson==3.17.6
|
||||||
six==1.11.0
|
six==1.16.0
|
||||||
sshpubkeys==3.1.0
|
sshpubkeys==3.3.1
|
||||||
uritemplate==3.0.0
|
uritemplate==4.1.1
|
||||||
urllib3==1.26.5
|
urllib3==1.26.9
|
||||||
vine==5.0.0
|
vine==5.0.0
|
||||||
drf-yasg==1.20.0
|
drf-yasg==1.20.0
|
||||||
Werkzeug==0.15.3
|
Werkzeug==2.1.2
|
||||||
drf-nested-routers==0.91
|
drf-nested-routers==0.93.4
|
||||||
aliyun-python-sdk-core-v3==2.9.1
|
|
||||||
aliyun-python-sdk-ecs==4.10.1
|
|
||||||
rest_condition==1.0.3
|
rest_condition==1.0.3
|
||||||
python-ldap==3.4.0
|
python-ldap==3.4.0
|
||||||
tencentcloud-sdk-python==3.0.477
|
django-radius==1.5.0
|
||||||
django-radius==1.4.0
|
|
||||||
django-redis-sessions==0.6.1
|
|
||||||
unicodecsv==0.14.1
|
unicodecsv==0.14.1
|
||||||
python-daemon==2.2.3
|
python-daemon==2.3.0
|
||||||
httpsig==1.3.0
|
httpsig==1.3.0
|
||||||
treelib==1.5.3
|
treelib==1.6.1
|
||||||
django-proxy==1.2.1
|
django-proxy==1.2.1
|
||||||
flower==1.0.0
|
flower==1.0.0
|
||||||
channels-redis==3.2.0
|
channels-redis==3.4.0
|
||||||
channels==2.4.0
|
channels==3.0.4
|
||||||
daphne==2.4.1
|
daphne==3.0.2
|
||||||
psutil==5.6.6
|
psutil==5.9.1
|
||||||
django-cas-ng==4.0.1
|
django-cas-ng==4.0.1
|
||||||
python-cas==1.5.0
|
python-cas==1.5.0
|
||||||
ipython
|
ipython
|
||||||
huaweicloud-sdk-python==1.0.21
|
django-redis==5.2.0
|
||||||
django-redis==4.11.0
|
|
||||||
python-redis-lock==3.7.0
|
python-redis-lock==3.7.0
|
||||||
jumpserver-django-oidc-rp==0.3.7.8
|
jumpserver-django-oidc-rp==0.3.7.8
|
||||||
django-mysql==3.9.0
|
django-mysql==3.9.0
|
||||||
gmssl==3.2.1
|
gmssl==3.2.1
|
||||||
azure-mgmt-compute==4.6.2
|
|
||||||
azure-mgmt-network==2.7.0
|
|
||||||
msrestazure==0.6.4
|
msrestazure==0.6.4
|
||||||
adal==1.2.5
|
adal==1.2.5
|
||||||
openpyxl==3.0.5
|
openpyxl==3.0.10
|
||||||
pyexcel==0.6.6
|
pyexcel==0.7.0
|
||||||
pyexcel-xlsx==0.6.0
|
pyexcel-xlsx==0.6.0
|
||||||
data-tree==0.0.1
|
data-tree==0.0.1
|
||||||
pyvmomi==7.0.1
|
pyvmomi==7.0.1
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
azure-identity==1.5.0
|
django-simple-history==3.1.1
|
||||||
azure-mgmt-subscription==1.0.0
|
geoip2==4.5.0
|
||||||
qingcloud-sdk==1.2.12
|
|
||||||
django-simple-history==3.0.0
|
|
||||||
google-cloud-compute==0.5.0
|
|
||||||
PyMySQL==1.0.2
|
|
||||||
cx-Oracle==8.2.1
|
|
||||||
psycopg2-binary==2.9.1
|
|
||||||
alibabacloud_dysmsapi20170525==2.0.2
|
|
||||||
geoip2==4.4.0
|
|
||||||
html2text==2020.1.16
|
html2text==2020.1.16
|
||||||
python-novaclient==11.0.1
|
|
||||||
pyzipper==0.3.5
|
pyzipper==0.3.5
|
||||||
python3-saml==1.12.0
|
python3-saml==1.12.0
|
||||||
python-keystoneclient==4.3.0
|
|
||||||
pymssql==2.1.5
|
|
||||||
kubernetes==21.7.0
|
kubernetes==21.7.0
|
||||||
websocket-client==1.2.3
|
websocket-client==1.2.3
|
||||||
numpy==1.22.0
|
numpy==1.22.0
|
||||||
pandas==1.3.5
|
pandas==1.3.5
|
||||||
pyjwkest==1.4.2
|
pyjwkest==1.4.2
|
||||||
jsonfield2==4.0.0.post0
|
jsonfield2==4.0.0.post0
|
||||||
bce-python-sdk==0.8.64
|
|
||||||
ipip-ipdb==1.6.1
|
ipip-ipdb==1.6.1
|
||||||
|
# Cloud req
|
||||||
|
qingcloud-sdk==1.2.12
|
||||||
|
azure-mgmt-subscription==1.0.0
|
||||||
|
azure-identity==1.5.0
|
||||||
|
azure-mgmt-compute==4.6.2
|
||||||
|
azure-mgmt-network==2.7.0
|
||||||
|
google-cloud-compute==0.5.0
|
||||||
|
alibabacloud_dysmsapi20170525==2.0.2
|
||||||
|
python-novaclient==11.0.1
|
||||||
|
python-keystoneclient==4.3.0
|
||||||
|
bce-python-sdk==0.8.64
|
||||||
|
tencentcloud-sdk-python==3.0.477
|
||||||
|
aliyun-python-sdk-core-v3==2.9.1
|
||||||
|
aliyun-python-sdk-ecs==4.10.1
|
||||||
|
huaweicloud-sdk-python==1.0.21
|
||||||
|
# DB requirements
|
||||||
|
PyMySQL==1.0.2
|
||||||
|
cx-Oracle==8.2.1
|
||||||
|
psycopg2-binary==2.9.1
|
||||||
|
pymssql==2.1.5
|
||||||
|
|
Loading…
Reference in New Issue