mirror of https://github.com/jumpserver/jumpserver
merge: with dev
commit
b284bb60f5
|
@ -1,11 +1,12 @@
|
|||
from django.db.models import Q
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.models import Account
|
||||
from accounts.permissions import AccountTaskActionPermission
|
||||
from accounts.tasks import (
|
||||
remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
)
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
|
||||
__all__ = [
|
||||
|
@ -26,25 +27,35 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
|||
]
|
||||
return super().get_permissions()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
accounts = data.get('accounts', [])
|
||||
params = data.get('params')
|
||||
@staticmethod
|
||||
def get_account_ids(data, action):
|
||||
account_type = 'gather_accounts' if action == 'remove' else 'accounts'
|
||||
accounts = data.get(account_type, [])
|
||||
account_ids = [str(a.id) for a in accounts]
|
||||
|
||||
if data['action'] == 'push':
|
||||
task = push_accounts_to_assets_task.delay(account_ids, params)
|
||||
elif data['action'] == 'remove':
|
||||
gather_accounts = data.get('gather_accounts', [])
|
||||
gather_account_ids = [str(a.id) for a in gather_accounts]
|
||||
task = remove_accounts_task.delay(gather_account_ids)
|
||||
if action == 'remove':
|
||||
return account_ids
|
||||
|
||||
assets = data.get('assets', [])
|
||||
asset_ids = [str(a.id) for a in assets]
|
||||
ids = Account.objects.filter(
|
||||
Q(id__in=account_ids) | Q(asset_id__in=asset_ids)
|
||||
).distinct().values_list('id', flat=True)
|
||||
return [str(_id) for _id in ids]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
ids = self.get_account_ids(data, action)
|
||||
|
||||
if action == 'push':
|
||||
task = push_accounts_to_assets_task.delay(ids, data.get('params'))
|
||||
elif action == 'remove':
|
||||
task = remove_accounts_task.delay(ids)
|
||||
elif action == 'verify':
|
||||
task = verify_accounts_connectivity_task.delay(ids)
|
||||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
raise ValueError(f"Invalid action: {action}")
|
||||
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
|
|
|
@ -145,9 +145,9 @@ class AccountBackupHandler:
|
|||
wb = Workbook(filename)
|
||||
for sheet, data in data_map.items():
|
||||
ws = wb.add_worksheet(str(sheet))
|
||||
for row in data:
|
||||
for col, _data in enumerate(row):
|
||||
ws.write_string(0, col, _data)
|
||||
for row_index, row_data in enumerate(data):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
|
|
|
@ -39,3 +39,4 @@
|
|||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
|
|
|
@ -4,6 +4,7 @@ from copy import deepcopy
|
|||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
|
||||
|
@ -161,7 +162,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.secret = recorder.new_secret
|
||||
account.save(update_fields=['secret'])
|
||||
account.date_updated = timezone.now()
|
||||
account.save(update_fields=['secret', 'date_updated'])
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
|
@ -182,17 +184,33 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_summary(recorders):
|
||||
total, succeed, failed = 0, 0, 0
|
||||
for recorder in recorders:
|
||||
if recorder.status == 'success':
|
||||
succeed += 1
|
||||
else:
|
||||
failed += 1
|
||||
total += 1
|
||||
|
||||
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
||||
return summary
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
super().run(*args, **kwargs)
|
||||
recorders = list(self.name_recorder_mapper.values())
|
||||
summary = self.get_summary(recorders)
|
||||
print(summary, end='')
|
||||
|
||||
if self.record_id:
|
||||
return
|
||||
recorders = self.name_recorder_mapper.values()
|
||||
recorders = list(recorders)
|
||||
self.send_recorder_mail(recorders)
|
||||
|
||||
def send_recorder_mail(self, recorders):
|
||||
self.send_recorder_mail(recorders, summary)
|
||||
|
||||
def send_recorder_mail(self, recorders, summary):
|
||||
recipients = self.execution.recipients
|
||||
if not recorders or not recipients:
|
||||
return
|
||||
|
@ -212,7 +230,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, password, [filename])
|
||||
attachments = [attachment]
|
||||
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
||||
ChangeSecretExecutionTaskMsg(name, user, summary).publish(attachments)
|
||||
os.remove(filename)
|
||||
|
||||
@staticmethod
|
||||
|
@ -228,8 +246,8 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
rows.insert(0, header)
|
||||
wb = Workbook(filename)
|
||||
ws = wb.add_worksheet('Sheet1')
|
||||
for row in rows:
|
||||
for col, data in enumerate(row):
|
||||
ws.write_string(0, col, data)
|
||||
for row_index, row_data in enumerate(rows):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
return True
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Gather posix account
|
||||
- name: Gather windows account
|
||||
ansible.builtin.win_shell: net user
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- name: Define info by set_fact
|
||||
ansible.builtin.set_fact:
|
||||
|
|
|
@ -39,3 +39,4 @@
|
|||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
|
|
|
@ -54,20 +54,23 @@ class AccountBackupByObjStorageExecutionTaskMsg(object):
|
|||
class ChangeSecretExecutionTaskMsg(object):
|
||||
subject = _('Notification of implementation result of encryption change plan')
|
||||
|
||||
def __init__(self, name: str, user: User):
|
||||
def __init__(self, name: str, user: User, summary):
|
||||
self.name = name
|
||||
self.user = user
|
||||
self.summary = summary
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
name = self.name
|
||||
if self.user.secret_key:
|
||||
return _('{} - The encryption change task has been completed. '
|
||||
default_message = _('{} - The encryption change task has been completed. '
|
||||
'See the attachment for details').format(name)
|
||||
|
||||
else:
|
||||
return _("{} - The encryption change task has been completed: the encryption "
|
||||
default_message = _("{} - The encryption change task has been completed: the encryption "
|
||||
"password has not been set - please go to personal information -> "
|
||||
"file encryption password to set the encryption password").format(name)
|
||||
"set encryption password in preferences").format(name)
|
||||
return self.summary + '\n' + default_message
|
||||
|
||||
def publish(self, attachments=None):
|
||||
send_mail_attachment_async(
|
||||
|
|
|
@ -60,7 +60,7 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
|||
for data in initial_data:
|
||||
if not data.get('asset') and not self.instance:
|
||||
raise serializers.ValidationError({'asset': UniqueTogetherValidator.missing_message})
|
||||
asset = data.get('asset') or self.instance.asset
|
||||
asset = data.get('asset') or getattr(self.instance, 'asset', None)
|
||||
self.from_template_if_need(data)
|
||||
self.set_uniq_name_if_need(data, asset)
|
||||
|
||||
|
@ -457,12 +457,14 @@ class AccountHistorySerializer(serializers.ModelSerializer):
|
|||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
('verify', 'verify'),
|
||||
('push', 'push'),
|
||||
('remove', 'remove'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
assets = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
accounts = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Account.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
|
|
|
@ -21,7 +21,8 @@ def on_account_pre_save(sender, instance, **kwargs):
|
|||
if instance.version == 0:
|
||||
instance.version = 1
|
||||
else:
|
||||
instance.version = instance.history.count()
|
||||
history_account = instance.history.first()
|
||||
instance.version = history_account.version + 1 if history_account else 0
|
||||
|
||||
|
||||
@merge_delay_run(ttl=5)
|
||||
|
@ -62,7 +63,7 @@ def create_accounts_activities(account, action='create'):
|
|||
def on_account_create_by_template(sender, instance, created=False, **kwargs):
|
||||
if not created or instance.source != 'template':
|
||||
return
|
||||
push_accounts_if_need(accounts=(instance,))
|
||||
push_accounts_if_need.delay(accounts=(instance,))
|
||||
create_accounts_activities(instance, action='create')
|
||||
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ def clean_historical_accounts():
|
|||
history_model = Account.history.model
|
||||
history_id_mapper = defaultdict(list)
|
||||
|
||||
ids = history_model.objects.values('id').annotate(count=Count('id')) \
|
||||
ids = history_model.objects.values('id').annotate(count=Count('id', distinct=True)) \
|
||||
.filter(count__gte=limit).values_list('id', flat=True)
|
||||
|
||||
if not ids:
|
||||
|
|
|
@ -92,6 +92,7 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|||
model = Asset
|
||||
filterset_class = AssetFilterSet
|
||||
search_fields = ("name", "address", "comment")
|
||||
ordering = ('name',)
|
||||
ordering_fields = ('name', 'address', 'connectivity', 'platform', 'date_updated', 'date_created')
|
||||
serializer_classes = (
|
||||
("default", serializers.AssetSerializer),
|
||||
|
|
|
@ -12,6 +12,6 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'ordering': ['name'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('match_asset', 'Can match asset'), ('change_assetnodes', 'Can change asset nodes')], 'verbose_name': 'Asset'},
|
||||
options={'ordering': [], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('match_asset', 'Can match asset'), ('change_assetnodes', 'Can change asset nodes')], 'verbose_name': 'Asset'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -348,7 +348,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
|
|||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
verbose_name = _("Asset")
|
||||
ordering = ["name", ]
|
||||
ordering = []
|
||||
permissions = [
|
||||
('refresh_assethardwareinfo', _('Can refresh asset hardware info')),
|
||||
('test_assetconnectivity', _('Can test asset connectivity')),
|
||||
|
|
|
@ -429,7 +429,7 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
|||
|
||||
@classmethod
|
||||
@timeit
|
||||
def get_nodes_all_assets(cls, *nodes):
|
||||
def get_nodes_all_assets(cls, *nodes, distinct=True):
|
||||
from .asset import Asset
|
||||
node_ids = set()
|
||||
descendant_node_query = Q()
|
||||
|
@ -439,7 +439,10 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin):
|
|||
if descendant_node_query:
|
||||
_ids = Node.objects.order_by().filter(descendant_node_query).values_list('id', flat=True)
|
||||
node_ids.update(_ids)
|
||||
return Asset.objects.order_by().filter(nodes__id__in=node_ids).distinct()
|
||||
assets = Asset.objects.order_by().filter(nodes__id__in=node_ids)
|
||||
if distinct:
|
||||
assets = assets.distinct()
|
||||
return assets
|
||||
|
||||
def get_all_asset_ids(self):
|
||||
asset_ids = self.get_all_asset_ids_by_node_key(org_id=self.org_id, node_key=self.key)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils import get_logger
|
||||
from assets.models import Node
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -28,6 +28,7 @@ class AssetPaginationBase(LimitOffsetPagination):
|
|||
'key', 'all', 'show_current_asset',
|
||||
'cache_policy', 'display', 'draw',
|
||||
'order', 'node', 'node_id', 'fields_size',
|
||||
'asset'
|
||||
}
|
||||
for k, v in self._request.query_params.items():
|
||||
if k not in exclude_query_params and v is not None:
|
||||
|
|
|
@ -206,9 +206,12 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
|
|||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('domain', 'nodes', 'protocols', ) \
|
||||
.prefetch_related('platform', 'platform__automation') \
|
||||
.prefetch_related('labels', 'labels__label') \
|
||||
.annotate(category=F("platform__category")) \
|
||||
.annotate(type=F("platform__type"))
|
||||
if queryset.model is Asset:
|
||||
queryset = queryset.prefetch_related('labels__label', 'labels')
|
||||
else:
|
||||
queryset = queryset.prefetch_related('asset_ptr__labels__label', 'asset_ptr__labels')
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -56,7 +56,14 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
|||
|
||||
class DomainListSerializer(DomainSerializer):
|
||||
class Meta(DomainSerializer.Meta):
|
||||
fields = list(set(DomainSerializer.Meta.fields) - {'assets'})
|
||||
fields = list(set(DomainSerializer.Meta.fields + ['assets_amount']) - {'assets'})
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.annotate(
|
||||
assets_amount=Count('assets', distinct=True),
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class DomainWithGatewaySerializer(serializers.ModelSerializer):
|
||||
|
|
|
@ -191,7 +191,6 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
|||
def add_type_choices(self, name, label):
|
||||
tp = self.fields['type']
|
||||
tp.choices[name] = label
|
||||
tp.choice_mapper[name] = label
|
||||
tp.choice_strings_to_values[name] = label
|
||||
|
||||
@lazyproperty
|
||||
|
|
|
@ -63,13 +63,13 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
|
|||
return
|
||||
logger.info("Asset create signal recv: {}".format(instance))
|
||||
|
||||
ensure_asset_has_node(assets=(instance,))
|
||||
ensure_asset_has_node.delay(assets=(instance,))
|
||||
|
||||
# 获取资产硬件信息
|
||||
auto_config = instance.auto_config
|
||||
if auto_config.get('ping_enabled'):
|
||||
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
|
||||
test_assets_connectivity_handler(assets=(instance,))
|
||||
test_assets_connectivity_handler.delay(assets=(instance,))
|
||||
if auto_config.get('gather_facts_enabled'):
|
||||
logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
|
||||
gather_assets_facts_handler(assets=(instance,))
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
#
|
||||
from operator import add, sub
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.dispatch import receiver
|
||||
|
||||
from assets.models import Asset, Node
|
||||
from common.const.signals import PRE_CLEAR, POST_ADD, PRE_REMOVE
|
||||
from common.decorators import on_transaction_commit, merge_delay_run
|
||||
from common.signals import django_ready
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from ..tasks import check_node_assets_amount_task
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -34,7 +36,7 @@ def on_node_asset_change(sender, action, instance, reverse, pk_set, **kwargs):
|
|||
node_ids = [instance.id]
|
||||
else:
|
||||
node_ids = list(pk_set)
|
||||
update_nodes_assets_amount(node_ids=node_ids)
|
||||
update_nodes_assets_amount.delay(node_ids=node_ids)
|
||||
|
||||
|
||||
@merge_delay_run(ttl=30)
|
||||
|
@ -52,3 +54,18 @@ def update_nodes_assets_amount(node_ids=()):
|
|||
node.assets_amount = node.get_assets_amount()
|
||||
|
||||
Node.objects.bulk_update(nodes, ['assets_amount'])
|
||||
|
||||
|
||||
@receiver(django_ready)
|
||||
def set_assets_size_to_setting(sender, **kwargs):
|
||||
from assets.models import Asset
|
||||
try:
|
||||
with tmp_to_root_org():
|
||||
amount = Asset.objects.order_by().count()
|
||||
except:
|
||||
amount = 0
|
||||
|
||||
if amount > 20000:
|
||||
settings.ASSET_SIZE = 'large'
|
||||
elif amount > 2000:
|
||||
settings.ASSET_SIZE = 'medium'
|
||||
|
|
|
@ -44,18 +44,18 @@ def on_node_post_create(sender, instance, created, update_fields, **kwargs):
|
|||
need_expire = False
|
||||
|
||||
if need_expire:
|
||||
expire_node_assets_mapping(org_ids=(instance.org_id,))
|
||||
expire_node_assets_mapping.delay(org_ids=(instance.org_id,))
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Node)
|
||||
def on_node_post_delete(sender, instance, **kwargs):
|
||||
expire_node_assets_mapping(org_ids=(instance.org_id,))
|
||||
expire_node_assets_mapping.delay(org_ids=(instance.org_id,))
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_node_asset_change(sender, instance, action='pre_remove', **kwargs):
|
||||
if action.startswith('post'):
|
||||
expire_node_assets_mapping(org_ids=(instance.org_id,))
|
||||
expire_node_assets_mapping.delay(org_ids=(instance.org_id,))
|
||||
|
||||
|
||||
@receiver(django_ready)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from django.urls import path
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from labels.api import LabelViewSet
|
||||
from .. import api
|
||||
|
||||
app_name = 'assets'
|
||||
|
@ -22,6 +23,7 @@ router.register(r'domains', api.DomainViewSet, 'domain')
|
|||
router.register(r'gateways', api.GatewayViewSet, 'gateway')
|
||||
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
|
||||
router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting')
|
||||
router.register(r'labels', LabelViewSet, 'label')
|
||||
|
||||
urlpatterns = [
|
||||
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
|
||||
|
|
|
@ -4,7 +4,6 @@ from urllib.parse import urlencode, urlparse
|
|||
from kubernetes import client
|
||||
from kubernetes.client import api_client
|
||||
from kubernetes.client.api import core_v1_api
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||
|
||||
from common.utils import get_logger
|
||||
|
@ -88,8 +87,9 @@ class KubernetesClient:
|
|||
if hasattr(self, func_name):
|
||||
try:
|
||||
data = getattr(self, func_name)(*args)
|
||||
except ApiException as e:
|
||||
logger.error(e.reason)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise e
|
||||
|
||||
if self.server:
|
||||
self.server.stop()
|
||||
|
|
|
@ -20,6 +20,7 @@ from common.const.http import GET, POST
|
|||
from common.drf.filters import DatetimeRangeFilterBackend
|
||||
from common.permissions import IsServiceAccount
|
||||
from common.plugins.es import QuerySet as ESQuerySet
|
||||
from common.sessions.cache import user_session_manager
|
||||
from common.storage.ftp_file import FTPFileStorageHandler
|
||||
from common.utils import is_uuid, get_logger, lazyproperty
|
||||
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
|
||||
|
@ -289,8 +290,7 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
keys = queryset.values_list('key', flat=True)
|
||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
for key in keys:
|
||||
session_store_cls(key).delete()
|
||||
user_session_manager.decrement_or_remove(key)
|
||||
queryset.delete()
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from django.core.cache import cache
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from notifications.ws import WS_SESSION_KEY
|
||||
from common.sessions.cache import user_session_manager
|
||||
from orgs.utils import current_org
|
||||
from .models import UserSession
|
||||
|
||||
|
@ -41,13 +40,11 @@ class UserSessionFilterSet(BaseFilterSet):
|
|||
|
||||
@staticmethod
|
||||
def filter_is_active(queryset, name, is_active):
|
||||
redis_client = cache.client.get_client()
|
||||
members = redis_client.smembers(WS_SESSION_KEY)
|
||||
members = [member.decode('utf-8') for member in members]
|
||||
keys = user_session_manager.get_active_keys()
|
||||
if is_active:
|
||||
queryset = queryset.filter(key__in=members)
|
||||
queryset = queryset.filter(key__in=keys)
|
||||
else:
|
||||
queryset = queryset.exclude(key__in=members)
|
||||
queryset = queryset.exclude(key__in=keys)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -4,15 +4,15 @@ from datetime import timedelta
|
|||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches, cache
|
||||
from django.core.cache import caches
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from common.sessions.cache import user_session_manager
|
||||
from common.utils import lazyproperty, i18n_trans
|
||||
from notifications.ws import WS_SESSION_KEY
|
||||
from ops.models import JobExecution
|
||||
from orgs.mixins.models import OrgModelMixin, Organization
|
||||
from orgs.utils import current_org
|
||||
|
@ -278,8 +278,7 @@ class UserSession(models.Model):
|
|||
|
||||
@property
|
||||
def is_active(self):
|
||||
redis_client = cache.client.get_client()
|
||||
return redis_client.sismember(WS_SESSION_KEY, self.key)
|
||||
return user_session_manager.check_active(self.key)
|
||||
|
||||
@property
|
||||
def date_expired(self):
|
||||
|
|
|
@ -205,7 +205,7 @@ class RDPFileClientProtocolURLMixin:
|
|||
return data
|
||||
|
||||
def get_smart_endpoint(self, protocol, asset=None):
|
||||
endpoint = Endpoint.match_by_instance_label(asset, protocol)
|
||||
endpoint = Endpoint.match_by_instance_label(asset, protocol, self.request)
|
||||
if not endpoint:
|
||||
target_ip = asset.get_target_ip() if asset else ''
|
||||
endpoint = EndpointRule.match_endpoint(
|
||||
|
|
|
@ -90,6 +90,6 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
|
|||
return Response({'msg': 'ok'})
|
||||
except errors.AuthFailedError as e:
|
||||
data = {"error": e.error, "msg": e.msg}
|
||||
raise ValidationError(data)
|
||||
return Response(data, status=401)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
|
|
@ -10,6 +10,7 @@ from rest_framework import authentication, exceptions
|
|||
from common.auth import signature
|
||||
from common.decorators import merge_delay_run
|
||||
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
|
||||
from users.models import User
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
||||
|
||||
|
@ -19,22 +20,23 @@ def date_more_than(d, seconds):
|
|||
|
||||
@merge_delay_run(ttl=60)
|
||||
def update_token_last_used(tokens=()):
|
||||
for token in tokens:
|
||||
token.date_last_used = timezone.now()
|
||||
token.save(update_fields=['date_last_used'])
|
||||
access_keys_ids = [token.id for token in tokens if isinstance(token, AccessKey)]
|
||||
private_token_keys = [token.key for token in tokens if isinstance(token, PrivateToken)]
|
||||
if len(access_keys_ids) > 0:
|
||||
AccessKey.objects.filter(id__in=access_keys_ids).update(date_last_used=timezone.now())
|
||||
if len(private_token_keys) > 0:
|
||||
PrivateToken.objects.filter(key__in=private_token_keys).update(date_last_used=timezone.now())
|
||||
|
||||
|
||||
@merge_delay_run(ttl=60)
|
||||
def update_user_last_used(users=()):
|
||||
for user in users:
|
||||
user.date_api_key_last_used = timezone.now()
|
||||
user.save(update_fields=['date_api_key_last_used'])
|
||||
User.objects.filter(id__in=users).update(date_api_key_last_used=timezone.now())
|
||||
|
||||
|
||||
def after_authenticate_update_date(user, token=None):
|
||||
update_user_last_used(users=(user,))
|
||||
update_user_last_used.delay(users=(user.id,))
|
||||
if token:
|
||||
update_token_last_used(tokens=(token,))
|
||||
update_token_last_used.delay(tokens=(token,))
|
||||
|
||||
|
||||
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
|
|
|
@ -98,16 +98,19 @@ class OAuth2Backend(JMSModelBackend):
|
|||
access_token_url = '{url}{separator}{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, query=urlencode(query_dict)
|
||||
)
|
||||
# token_method -> get, post(post_data), post_json
|
||||
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
|
||||
requests_func = getattr(requests, token_method, requests.get)
|
||||
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
if token_method == 'post':
|
||||
access_token_response = requests_func(access_token_url, headers=headers, data=query_dict)
|
||||
if token_method.startswith('post'):
|
||||
body_key = 'json' if token_method.endswith('json') else 'data'
|
||||
access_token_response = requests.post(
|
||||
access_token_url, headers=headers, **{body_key: query_dict}
|
||||
)
|
||||
else:
|
||||
access_token_response = requests_func(access_token_url, headers=headers)
|
||||
access_token_response = requests.get(access_token_url, headers=headers)
|
||||
try:
|
||||
access_token_response.raise_for_status()
|
||||
access_token_response_data = access_token_response.json()
|
||||
|
|
|
@ -18,7 +18,7 @@ class EncryptedField(forms.CharField):
|
|||
|
||||
class UserLoginForm(forms.Form):
|
||||
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE \
|
||||
or days_auto_login < 1
|
||||
|
||||
username = forms.CharField(
|
||||
|
|
|
@ -142,23 +142,7 @@ class SessionCookieMiddleware(MiddlewareMixin):
|
|||
return response
|
||||
response.set_cookie(key, value)
|
||||
|
||||
@staticmethod
|
||||
def set_cookie_session_expire(request, response):
|
||||
if not request.session.get('auth_session_expiration_required'):
|
||||
return
|
||||
value = 'age'
|
||||
if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or \
|
||||
not request.session.get('auto_login', False):
|
||||
value = 'close'
|
||||
|
||||
age = request.session.get_expiry_age()
|
||||
expire_timestamp = request.session.get_expiry_date().timestamp()
|
||||
response.set_cookie('jms_session_expire_timestamp', expire_timestamp)
|
||||
response.set_cookie('jms_session_expire', value, max_age=age)
|
||||
request.session.pop('auth_session_expiration_required', None)
|
||||
|
||||
def process_response(self, request, response: HttpResponse):
|
||||
self.set_cookie_session_prefix(request, response)
|
||||
self.set_cookie_public_key(request, response)
|
||||
self.set_cookie_session_expire(request, response)
|
||||
return response
|
||||
|
|
|
@ -37,9 +37,6 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
|||
UserSession.objects.filter(key=session_key).delete()
|
||||
cache.set(lock_key, request.session.session_key, None)
|
||||
|
||||
# 标记登录,设置 cookie,前端可以控制刷新, Middleware 会拦截这个生成 cookie
|
||||
request.session['auth_session_expiration_required'] = 1
|
||||
|
||||
|
||||
@receiver(cas_user_authenticated)
|
||||
def on_cas_user_login_success(sender, request, user, **kwargs):
|
||||
|
|
|
@ -70,11 +70,12 @@ class DingTalkQRMixin(DingTalkBaseMixin, View):
|
|||
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.DINGTALK_APPKEY,
|
||||
'client_id': settings.DINGTALK_APPKEY,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_login',
|
||||
'scope': 'openid',
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
'prompt': 'consent'
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
|
|
@ -104,9 +104,11 @@ class QuerySetMixin:
|
|||
page = super().paginate_queryset(queryset)
|
||||
serializer_class = self.get_serializer_class()
|
||||
if page and serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
|
||||
ids = [i.id for i in page]
|
||||
ids = [str(obj.id) for obj in page]
|
||||
page = self.get_queryset().filter(id__in=ids)
|
||||
page = serializer_class.setup_eager_loading(page)
|
||||
page_mapper = {str(obj.id): obj for obj in page}
|
||||
page = [page_mapper.get(_id) for _id in ids if _id in page_mapper]
|
||||
return page
|
||||
|
||||
|
||||
|
|
|
@ -19,3 +19,17 @@ class Status(models.TextChoices):
|
|||
failed = 'failed', _("Failed")
|
||||
error = 'error', _("Error")
|
||||
canceled = 'canceled', _("Canceled")
|
||||
|
||||
|
||||
COUNTRY_CALLING_CODES = [
|
||||
{'name': 'China(中国)', 'value': '+86'},
|
||||
{'name': 'HongKong(中国香港)', 'value': '+852'},
|
||||
{'name': 'Macao(中国澳门)', 'value': '+853'},
|
||||
{'name': 'Taiwan(中国台湾)', 'value': '+886'},
|
||||
{'name': 'America(America)', 'value': '+1'}, {'name': 'Russia(Россия)', 'value': '+7'},
|
||||
{'name': 'France(français)', 'value': '+33'},
|
||||
{'name': 'Britain(Britain)', 'value': '+44'},
|
||||
{'name': 'Germany(Deutschland)', 'value': '+49'},
|
||||
{'name': 'Japan(日本)', 'value': '+81'}, {'name': 'Korea(한국)', 'value': '+82'},
|
||||
{'name': 'India(भारत)', 'value': '+91'}
|
||||
]
|
||||
|
|
|
@ -362,7 +362,11 @@ class RelatedManager:
|
|||
if name is None or val is None:
|
||||
continue
|
||||
|
||||
if custom_attr_filter:
|
||||
custom_filter_q = None
|
||||
spec_attr_filter = getattr(to_model, "get_{}_filter_attr_q".format(name), None)
|
||||
if spec_attr_filter:
|
||||
custom_filter_q = spec_attr_filter(val, match)
|
||||
elif custom_attr_filter:
|
||||
custom_filter_q = custom_attr_filter(name, val, match)
|
||||
if custom_filter_q:
|
||||
filters.append(custom_filter_q)
|
||||
|
@ -464,10 +468,14 @@ class JSONManyToManyDescriptor:
|
|||
rule_value = rule.get('value', '')
|
||||
rule_match = rule.get('match', 'exact')
|
||||
|
||||
if custom_attr_filter:
|
||||
q = custom_attr_filter(rule['name'], rule_value, rule_match)
|
||||
if q:
|
||||
custom_q &= q
|
||||
custom_filter_q = None
|
||||
spec_attr_filter = getattr(to_model, "get_filter_{}_attr_q".format(rule['name']), None)
|
||||
if spec_attr_filter:
|
||||
custom_filter_q = spec_attr_filter(rule_value, rule_match)
|
||||
elif custom_attr_filter:
|
||||
custom_filter_q = custom_attr_filter(rule['name'], rule_value, rule_match)
|
||||
if custom_filter_q:
|
||||
custom_q &= custom_filter_q
|
||||
continue
|
||||
|
||||
if rule_match == 'in':
|
||||
|
@ -517,7 +525,6 @@ class JSONManyToManyDescriptor:
|
|||
res &= rule_value.issubset(value)
|
||||
else:
|
||||
res &= bool(value & rule_value)
|
||||
|
||||
else:
|
||||
logging.error("unknown match: {}".format(rule['match']))
|
||||
res &= False
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
@ -101,7 +102,11 @@ def run_debouncer_func(cache_key, org, ttl, func, *args, **kwargs):
|
|||
first_run_time = current
|
||||
|
||||
if current - first_run_time > ttl:
|
||||
_loop_debouncer_func_args_cache.pop(cache_key, None)
|
||||
_loop_debouncer_func_task_time_cache.pop(cache_key, None)
|
||||
executor.submit(run_func_partial, *args, **kwargs)
|
||||
logger.debug('pid {} executor submit run {}'.format(
|
||||
os.getpid(), func.__name__, ))
|
||||
return
|
||||
|
||||
loop = _loop_thread.get_loop()
|
||||
|
@ -133,13 +138,26 @@ class Debouncer(object):
|
|||
return await self.loop.run_in_executor(self.executor, func)
|
||||
|
||||
|
||||
ignore_err_exceptions = (
|
||||
"(3101, 'Plugin instructed the server to rollback the current transaction.')",
|
||||
)
|
||||
|
||||
|
||||
def _run_func_with_org(key, org, func, *args, **kwargs):
|
||||
from orgs.utils import set_current_org
|
||||
try:
|
||||
with transaction.atomic():
|
||||
set_current_org(org)
|
||||
func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error('delay run error: %s' % e)
|
||||
msg = str(e)
|
||||
log_func = logger.error
|
||||
if msg in ignore_err_exceptions:
|
||||
log_func = logger.info
|
||||
pid = os.getpid()
|
||||
thread_name = threading.current_thread()
|
||||
log_func('pid {} thread {} delay run {} error: {}'.format(
|
||||
pid, thread_name, func.__name__, msg))
|
||||
_loop_debouncer_func_task_cache.pop(key, None)
|
||||
_loop_debouncer_func_args_cache.pop(key, None)
|
||||
_loop_debouncer_func_task_time_cache.pop(key, None)
|
||||
|
@ -181,18 +199,9 @@ def merge_delay_run(ttl=5, key=None):
|
|||
:return:
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
sigs = inspect.signature(func)
|
||||
if len(sigs.parameters) != 1:
|
||||
raise ValueError('func must have one arguments: %s' % func.__name__)
|
||||
param = list(sigs.parameters.values())[0]
|
||||
if not isinstance(param.default, tuple):
|
||||
raise ValueError('func default must be tuple: %s' % param.default)
|
||||
suffix_key_func = key if key else default_suffix_key
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
def delay(func, *args, **kwargs):
|
||||
from orgs.utils import get_current_org
|
||||
suffix_key_func = key if key else default_suffix_key
|
||||
org = get_current_org()
|
||||
func_name = f'{func.__module__}_{func.__name__}'
|
||||
key_suffix = suffix_key_func(*args, **kwargs)
|
||||
|
@ -210,6 +219,26 @@ def merge_delay_run(ttl=5, key=None):
|
|||
_loop_debouncer_func_args_cache[cache_key] = cache_kwargs
|
||||
run_debouncer_func(cache_key, org, ttl, func, *args, **cache_kwargs)
|
||||
|
||||
def apply(func, sync=False, *args, **kwargs):
|
||||
if sync:
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
return delay(func, *args, **kwargs)
|
||||
|
||||
def inner(func):
|
||||
sigs = inspect.signature(func)
|
||||
if len(sigs.parameters) != 1:
|
||||
raise ValueError('func must have one arguments: %s' % func.__name__)
|
||||
param = list(sigs.parameters.values())[0]
|
||||
if not isinstance(param.default, tuple):
|
||||
raise ValueError('func default must be tuple: %s' % param.default)
|
||||
func.delay = functools.partial(delay, func)
|
||||
func.apply = functools.partial(apply, func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import Q, Count
|
||||
from django.db.models import Q
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
@ -180,36 +180,30 @@ class LabelFilterBackend(filters.BaseFilterBackend):
|
|||
]
|
||||
|
||||
@staticmethod
|
||||
def filter_resources(resources, labels_id):
|
||||
def parse_label_ids(labels_id):
|
||||
from labels.models import Label
|
||||
label_ids = [i.strip() for i in labels_id.split(',')]
|
||||
cleaned = []
|
||||
|
||||
args = []
|
||||
for label_id in label_ids:
|
||||
kwargs = {}
|
||||
if ':' in label_id:
|
||||
k, v = label_id.split(':', 1)
|
||||
kwargs['label__name'] = k.strip()
|
||||
kwargs['name'] = k.strip()
|
||||
if v != '*':
|
||||
kwargs['label__value'] = v.strip()
|
||||
else:
|
||||
kwargs['label_id'] = label_id
|
||||
kwargs['value'] = v.strip()
|
||||
args.append(kwargs)
|
||||
else:
|
||||
cleaned.append(label_id)
|
||||
|
||||
if len(args) == 1:
|
||||
resources = resources.filter(**args[0])
|
||||
return resources
|
||||
|
||||
if len(args) != 0:
|
||||
q = Q()
|
||||
for kwarg in args:
|
||||
q |= Q(**kwarg)
|
||||
|
||||
resources = resources.filter(q) \
|
||||
.values('res_id') \
|
||||
.order_by('res_id') \
|
||||
.annotate(count=Count('res_id')) \
|
||||
.values('res_id', 'count') \
|
||||
.filter(count=len(args))
|
||||
return resources
|
||||
ids = Label.objects.filter(q).values_list('id', flat=True)
|
||||
cleaned.extend(list(ids))
|
||||
return cleaned
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
labels_id = request.query_params.get('labels')
|
||||
|
@ -223,14 +217,15 @@ class LabelFilterBackend(filters.BaseFilterBackend):
|
|||
return queryset
|
||||
|
||||
model = queryset.model.label_model()
|
||||
labeled_resource_cls = model._labels.field.related_model
|
||||
labeled_resource_cls = model.labels.field.related_model
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
resources = labeled_resource_cls.objects.filter(
|
||||
res_type__app_label=app_label, res_type__model=model_name,
|
||||
)
|
||||
resources = self.filter_resources(resources, labels_id)
|
||||
label_ids = self.parse_label_ids(labels_id)
|
||||
resources = model.filter_resources_by_labels(resources, label_ids)
|
||||
res_ids = resources.values_list('res_id', flat=True)
|
||||
queryset = queryset.filter(id__in=set(res_ids))
|
||||
return queryset
|
||||
|
|
|
@ -14,6 +14,7 @@ class CeleryBaseService(BaseService):
|
|||
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
|
||||
ansible_config_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'ansible.cfg')
|
||||
ansible_modules_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'modules')
|
||||
os.environ.setdefault('LC_ALL', 'C.UTF-8')
|
||||
os.environ.setdefault('PYTHONOPTIMIZE', '1')
|
||||
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
|
||||
os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path)
|
||||
|
|
|
@ -28,9 +28,10 @@ class ErrorCode:
|
|||
|
||||
|
||||
class URL:
|
||||
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect'
|
||||
QR_CONNECT = 'https://login.dingtalk.com/oauth2/auth'
|
||||
OAUTH_CONNECT = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize'
|
||||
GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
|
||||
GET_USER_ACCESSTOKEN = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'
|
||||
GET_USER_INFO = 'https://api.dingtalk.com/v1.0/contact/users/me'
|
||||
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
|
||||
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'
|
||||
SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2'
|
||||
|
@ -72,8 +73,9 @@ class DingTalkRequests(BaseRequest):
|
|||
def get(self, url, params=None,
|
||||
with_token=False, with_sign=False,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
**kwargs) -> dict:
|
||||
pass
|
||||
|
||||
get = as_request(get)
|
||||
|
||||
def post(self, url, json=None, params=None,
|
||||
|
@ -81,6 +83,7 @@ class DingTalkRequests(BaseRequest):
|
|||
check_errcode_is_0=True,
|
||||
**kwargs) -> dict:
|
||||
pass
|
||||
|
||||
post = as_request(post)
|
||||
|
||||
def _add_sign(self, kwargs: dict):
|
||||
|
@ -123,17 +126,22 @@ class DingTalk:
|
|||
)
|
||||
|
||||
def get_userinfo_bycode(self, code):
|
||||
# https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619
|
||||
body = {
|
||||
"tmp_auth_code": code
|
||||
'clientId': self._appid,
|
||||
'clientSecret': self._appsecret,
|
||||
'code': code,
|
||||
'grantType': 'authorization_code'
|
||||
}
|
||||
data = self._request.post(URL.GET_USER_ACCESSTOKEN, json=body, check_errcode_is_0=False)
|
||||
token = data['accessToken']
|
||||
|
||||
data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True)
|
||||
return data['user_info']
|
||||
user = self._request.get(URL.GET_USER_INFO,
|
||||
headers={'x-acs-dingtalk-access-token': token}, check_errcode_is_0=False)
|
||||
return user
|
||||
|
||||
def get_user_id_by_code(self, code):
|
||||
user_info = self.get_userinfo_bycode(code)
|
||||
unionid = user_info['unionid']
|
||||
unionid = user_info['unionId']
|
||||
userid = self.get_userid_by_unionid(unionid)
|
||||
return userid, None
|
||||
|
||||
|
|
|
@ -394,20 +394,20 @@ class CommonBulkModelSerializer(CommonBulkSerializerMixin, serializers.ModelSeri
|
|||
|
||||
|
||||
class ResourceLabelsMixin(serializers.Serializer):
|
||||
labels = LabelRelatedField(many=True, label=_('Labels'), required=False, allow_null=True)
|
||||
labels = LabelRelatedField(many=True, label=_('Labels'), required=False, allow_null=True, source='res_labels')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
labels = validated_data.pop('labels', None)
|
||||
labels = validated_data.pop('res_labels', None)
|
||||
res = super().update(instance, validated_data)
|
||||
if labels is not None:
|
||||
instance.labels.set(labels, bulk=False)
|
||||
instance.res_labels.set(labels, bulk=False)
|
||||
return res
|
||||
|
||||
def create(self, validated_data):
|
||||
labels = validated_data.pop('labels', None)
|
||||
labels = validated_data.pop('res_labels', None)
|
||||
instance = super().create(validated_data)
|
||||
if labels is not None:
|
||||
instance.labels.set(labels, bulk=False)
|
||||
instance.res_labels.set(labels, bulk=False)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import re
|
||||
|
||||
from django.contrib.sessions.backends.cache import (
|
||||
SessionStore as DjangoSessionStore
|
||||
)
|
||||
from django.core.cache import cache
|
||||
|
||||
from jumpserver.utils import get_current_request
|
||||
|
||||
|
||||
class SessionStore(DjangoSessionStore):
|
||||
ignore_urls = [
|
||||
r'^/api/v1/users/profile/'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ignore_pattern = re.compile('|'.join(self.ignore_urls))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
request = get_current_request()
|
||||
if request is None or not self.ignore_pattern.match(request.path):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class RedisUserSessionManager:
|
||||
JMS_SESSION_KEY = 'jms_session_key'
|
||||
|
||||
def __init__(self):
|
||||
self.client = cache.client.get_client()
|
||||
|
||||
def add_or_increment(self, session_key):
|
||||
self.client.hincrby(self.JMS_SESSION_KEY, session_key, 1)
|
||||
|
||||
def decrement_or_remove(self, session_key):
|
||||
new_count = self.client.hincrby(self.JMS_SESSION_KEY, session_key, -1)
|
||||
if new_count <= 0:
|
||||
self.client.hdel(self.JMS_SESSION_KEY, session_key)
|
||||
|
||||
def check_active(self, session_key):
|
||||
count = self.client.hget(self.JMS_SESSION_KEY, session_key)
|
||||
count = 0 if count is None else int(count.decode('utf-8'))
|
||||
return count > 0
|
||||
|
||||
def get_active_keys(self):
|
||||
session_keys = []
|
||||
for k, v in self.client.hgetall(self.JMS_SESSION_KEY).items():
|
||||
count = int(v.decode('utf-8'))
|
||||
if count <= 0:
|
||||
continue
|
||||
key = k.decode('utf-8')
|
||||
session_keys.append(key)
|
||||
return session_keys
|
||||
|
||||
|
||||
user_session_manager = RedisUserSessionManager()
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -282,6 +282,7 @@ class Config(dict):
|
|||
'AUTH_LDAP_SYNC_INTERVAL': None,
|
||||
'AUTH_LDAP_SYNC_CRONTAB': None,
|
||||
'AUTH_LDAP_SYNC_ORG_IDS': ['00000000-0000-0000-0000-000000000002'],
|
||||
'AUTH_LDAP_SYNC_RECEIVERS': [],
|
||||
'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False,
|
||||
'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1,
|
||||
|
||||
|
@ -546,7 +547,6 @@ class Config(dict):
|
|||
'REFERER_CHECK_ENABLED': False,
|
||||
'SESSION_ENGINE': 'cache',
|
||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||
'SERVER_REPLAY_STORAGE': {},
|
||||
'SECURITY_DATA_CRYPTO_ALGO': None,
|
||||
'GMSSL_ENABLED': False,
|
||||
|
@ -605,7 +605,9 @@ class Config(dict):
|
|||
'GPT_MODEL': 'gpt-3.5-turbo',
|
||||
'VIRTUAL_APP_ENABLED': False,
|
||||
|
||||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200
|
||||
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
|
||||
|
||||
'TICKET_APPLY_ASSET_SCOPE': 'all'
|
||||
}
|
||||
|
||||
old_config_map = {
|
||||
|
|
|
@ -66,11 +66,6 @@ class RequestMiddleware:
|
|||
def __call__(self, request):
|
||||
set_current_request(request)
|
||||
response = self.get_response(request)
|
||||
is_request_api = request.path.startswith('/api')
|
||||
if not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE and \
|
||||
not is_request_api:
|
||||
age = request.session.get_expiry_age()
|
||||
request.session.set_expiry(age)
|
||||
return response
|
||||
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
path_perms_map = {
|
||||
'xpack': '*',
|
||||
'settings': '*',
|
||||
'img': '*',
|
||||
'replay': 'default',
|
||||
'applets': 'terminal.view_applet',
|
||||
'virtual_apps': 'terminal.view_virtualapp',
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from private_storage.servers import NginxXAccelRedirectServer, DjangoServer
|
||||
|
||||
|
||||
class StaticFileServer(object):
|
||||
|
||||
@staticmethod
|
||||
def serve(private_file):
|
||||
full_path = private_file.full_path
|
||||
# todo: gzip 文件录像 nginx 处理后,浏览器无法正常解析内容
|
||||
# 造成在线播放失败,暂时仅使用 nginx 处理 mp4 录像文件
|
||||
if full_path.endswith('.mp4'):
|
||||
return NginxXAccelRedirectServer.serve(private_file)
|
||||
else:
|
||||
return DjangoServer.serve(private_file)
|
|
@ -50,6 +50,7 @@ AUTH_LDAP_SYNC_IS_PERIODIC = CONFIG.AUTH_LDAP_SYNC_IS_PERIODIC
|
|||
AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL
|
||||
AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB
|
||||
AUTH_LDAP_SYNC_ORG_IDS = CONFIG.AUTH_LDAP_SYNC_ORG_IDS
|
||||
AUTH_LDAP_SYNC_RECEIVERS = CONFIG.AUTH_LDAP_SYNC_RECEIVERS
|
||||
AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS
|
||||
|
||||
# ==============================================================================
|
||||
|
|
|
@ -234,11 +234,9 @@ CSRF_COOKIE_NAME = '{}csrftoken'.format(SESSION_COOKIE_NAME_PREFIX)
|
|||
SESSION_COOKIE_NAME = '{}sessionid'.format(SESSION_COOKIE_NAME_PREFIX)
|
||||
|
||||
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
||||
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.{}".format(CONFIG.SESSION_ENGINE)
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE
|
||||
SESSION_ENGINE = "common.sessions.{}".format(CONFIG.SESSION_ENGINE)
|
||||
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
|
||||
# Database
|
||||
|
@ -319,9 +317,7 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/'
|
|||
PRIVATE_STORAGE_ROOT = MEDIA_ROOT
|
||||
PRIVATE_STORAGE_AUTH_FUNCTION = 'jumpserver.rewriting.storage.permissions.allow_access'
|
||||
PRIVATE_STORAGE_INTERNAL_URL = '/private-media/'
|
||||
PRIVATE_STORAGE_SERVER = 'nginx'
|
||||
if DEBUG_DEV:
|
||||
PRIVATE_STORAGE_SERVER = 'django'
|
||||
PRIVATE_STORAGE_SERVER = 'jumpserver.rewriting.storage.servers.StaticFileServer'
|
||||
|
||||
# Use django-bootstrap-form to format template, input max width arg
|
||||
# BOOTSTRAP_COLUMN_COUNT = 11
|
||||
|
|
|
@ -214,6 +214,9 @@ PERM_TREE_REGEN_INTERVAL = CONFIG.PERM_TREE_REGEN_INTERVAL
|
|||
MAGNUS_ORACLE_PORTS = CONFIG.MAGNUS_ORACLE_PORTS
|
||||
LIMIT_SUPER_PRIV = CONFIG.LIMIT_SUPER_PRIV
|
||||
|
||||
# Asset account may be too many
|
||||
ASSET_SIZE = 'small'
|
||||
|
||||
# Chat AI
|
||||
CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED
|
||||
GPT_API_KEY = CONFIG.GPT_API_KEY
|
||||
|
@ -224,3 +227,5 @@ GPT_MODEL = CONFIG.GPT_MODEL
|
|||
VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED
|
||||
|
||||
FILE_UPLOAD_SIZE_LIMIT_MB = CONFIG.FILE_UPLOAD_SIZE_LIMIT_MB
|
||||
|
||||
TICKET_APPLY_ASSET_SCOPE = CONFIG.TICKET_APPLY_ASSET_SCOPE
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.db.models import OneToOneField
|
||||
from django.db.models import OneToOneField, Count
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from .models import LabeledResource
|
||||
|
||||
__all__ = ['LabeledMixin']
|
||||
|
||||
|
||||
class LabeledMixin(models.Model):
|
||||
_labels = GenericRelation(LabeledResource, object_id_field='res_id', content_type_field='res_type')
|
||||
labels = GenericRelation(LabeledResource, object_id_field='res_id', content_type_field='res_type')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
@ -21,7 +22,7 @@ class LabeledMixin(models.Model):
|
|||
model = pk_field.related_model
|
||||
return model
|
||||
|
||||
@property
|
||||
@lazyproperty
|
||||
def real(self):
|
||||
pk_field = self._meta.pk
|
||||
if isinstance(pk_field, OneToOneField):
|
||||
|
@ -29,9 +30,43 @@ class LabeledMixin(models.Model):
|
|||
return self
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self.real._labels
|
||||
def res_labels(self):
|
||||
return self.real.labels
|
||||
|
||||
@labels.setter
|
||||
def labels(self, value):
|
||||
self.real._labels.set(value, bulk=False)
|
||||
@res_labels.setter
|
||||
def res_labels(self, value):
|
||||
self.real.labels.set(value, bulk=False)
|
||||
|
||||
@classmethod
|
||||
def filter_resources_by_labels(cls, resources, label_ids):
|
||||
return cls._get_filter_res_by_labels_m2m_all(resources, label_ids)
|
||||
|
||||
@classmethod
|
||||
def _get_filter_res_by_labels_m2m_in(cls, resources, label_ids):
|
||||
return resources.filter(label_id__in=label_ids)
|
||||
|
||||
@classmethod
|
||||
def _get_filter_res_by_labels_m2m_all(cls, resources, label_ids):
|
||||
if len(label_ids) == 1:
|
||||
return cls._get_filter_res_by_labels_m2m_in(resources, label_ids)
|
||||
|
||||
resources = resources.filter(label_id__in=label_ids) \
|
||||
.values('res_id') \
|
||||
.order_by('res_id') \
|
||||
.annotate(count=Count('res_id', distinct=True)) \
|
||||
.values('res_id', 'count') \
|
||||
.filter(count=len(label_ids))
|
||||
return resources
|
||||
|
||||
@classmethod
|
||||
def get_labels_filter_attr_q(cls, value, match):
|
||||
resources = LabeledResource.objects.all()
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if match != 'm2m_all':
|
||||
resources = cls._get_filter_res_by_labels_m2m_in(resources, value)
|
||||
else:
|
||||
resources = cls._get_filter_res_by_labels_m2m_all(resources, value)
|
||||
res_ids = set(resources.values_list('res_id', flat=True))
|
||||
return models.Q(id__in=res_ids)
|
||||
|
|
|
@ -34,7 +34,7 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
|||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.annotate(res_count=Count('labeled_resources'))
|
||||
queryset = queryset.annotate(res_count=Count('labeled_resources', distinct=True))
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
import json
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
|
||||
from common.db.utils import safe_db_connection
|
||||
from common.sessions.cache import user_session_manager
|
||||
from common.utils import get_logger
|
||||
from .signal_handlers import new_site_msg_chan
|
||||
from .site_msg import SiteMessageUtil
|
||||
|
||||
logger = get_logger(__name__)
|
||||
WS_SESSION_KEY = 'ws_session_key'
|
||||
|
||||
|
||||
class SiteMsgWebsocket(JsonWebsocketConsumer):
|
||||
sub = None
|
||||
refresh_every_seconds = 10
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self.scope['session']
|
||||
|
||||
def connect(self):
|
||||
user = self.scope["user"]
|
||||
if user.is_authenticated:
|
||||
self.accept()
|
||||
session = self.scope['session']
|
||||
redis_client = cache.client.get_client()
|
||||
redis_client.sadd(WS_SESSION_KEY, session.session_key)
|
||||
user_session_manager.add_or_increment(self.session.session_key)
|
||||
self.sub = self.watch_recv_new_site_msg()
|
||||
else:
|
||||
self.close()
|
||||
|
@ -66,6 +70,32 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
|
|||
if not self.sub:
|
||||
return
|
||||
self.sub.unsubscribe()
|
||||
session = self.scope['session']
|
||||
redis_client = cache.client.get_client()
|
||||
redis_client.srem(WS_SESSION_KEY, session.session_key)
|
||||
|
||||
user_session_manager.decrement_or_remove(self.session.session_key)
|
||||
if self.should_delete_session():
|
||||
thread = Thread(target=self.delay_delete_session)
|
||||
thread.start()
|
||||
|
||||
def should_delete_session(self):
|
||||
return (self.session.modified or settings.SESSION_SAVE_EVERY_REQUEST) and \
|
||||
not self.session.is_empty() and \
|
||||
self.session.get_expire_at_browser_close() and \
|
||||
not user_session_manager.check_active(self.session.session_key)
|
||||
|
||||
def delay_delete_session(self):
|
||||
timeout = 3
|
||||
check_interval = 0.5
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
time.sleep(check_interval)
|
||||
if user_session_manager.check_active(self.session.session_key):
|
||||
return
|
||||
|
||||
self.delete_session()
|
||||
|
||||
def delete_session(self):
|
||||
try:
|
||||
self.session.delete()
|
||||
except Exception as e:
|
||||
logger.info(f'delete session error: {e}')
|
||||
|
|
|
@ -4,6 +4,21 @@ import time
|
|||
import paramiko
|
||||
from sshtunnel import SSHTunnelForwarder
|
||||
|
||||
from packaging import version
|
||||
|
||||
if version.parse(paramiko.__version__) > version.parse("2.8.1"):
|
||||
_preferred_pubkeys = (
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ssh-rsa",
|
||||
"rsa-sha2-256",
|
||||
"rsa-sha2-512",
|
||||
"ssh-dss",
|
||||
)
|
||||
paramiko.transport.Transport._preferred_pubkeys = _preferred_pubkeys
|
||||
|
||||
|
||||
def common_argument_spec():
|
||||
options = dict(
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
@ -166,16 +167,58 @@ class CeleryTaskViewSet(
|
|||
i.next_exec_time = now + next_run_at
|
||||
return queryset
|
||||
|
||||
def generate_summary_state(self, execution_qs):
|
||||
model = self.get_queryset().model
|
||||
executions = execution_qs.order_by('-date_published').values('name', 'state')
|
||||
summary_state_dict = defaultdict(
|
||||
lambda: {
|
||||
'states': [], 'state': 'green',
|
||||
'summary': {'total': 0, 'success': 0}
|
||||
}
|
||||
)
|
||||
for execution in executions:
|
||||
name = execution['name']
|
||||
state = execution['state']
|
||||
|
||||
summary = summary_state_dict[name]['summary']
|
||||
|
||||
summary['total'] += 1
|
||||
summary['success'] += 1 if state == 'SUCCESS' else 0
|
||||
|
||||
states = summary_state_dict[name].get('states')
|
||||
if states is not None and len(states) >= 5:
|
||||
color = model.compute_state_color(states)
|
||||
summary_state_dict[name]['state'] = color
|
||||
summary_state_dict[name].pop('states', None)
|
||||
elif isinstance(states, list):
|
||||
states.append(state)
|
||||
|
||||
return summary_state_dict
|
||||
|
||||
def loading_summary_state(self, queryset):
|
||||
if isinstance(queryset, list):
|
||||
names = [i.name for i in queryset]
|
||||
execution_qs = CeleryTaskExecution.objects.filter(name__in=names)
|
||||
else:
|
||||
execution_qs = CeleryTaskExecution.objects.all()
|
||||
summary_state_dict = self.generate_summary_state(execution_qs)
|
||||
for i in queryset:
|
||||
i.summary = summary_state_dict.get(i.name, {}).get('summary', {})
|
||||
i.state = summary_state_dict.get(i.name, {}).get('state', 'green')
|
||||
return queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
page = self.generate_execute_time(page)
|
||||
page = self.loading_summary_state(page)
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
queryset = self.generate_execute_time(queryset)
|
||||
queryset = self.loading_summary_state(queryset)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
|
|
@ -246,6 +246,6 @@ class UsernameHintsAPI(APIView):
|
|||
.filter(username__icontains=query) \
|
||||
.filter(asset__in=assets) \
|
||||
.values('username') \
|
||||
.annotate(total=Count('username')) \
|
||||
.annotate(total=Count('username', distinct=True)) \
|
||||
.order_by('total', '-username')[:10]
|
||||
return Response(data=top_accounts)
|
||||
|
|
|
@ -15,6 +15,9 @@ class CeleryTask(models.Model):
|
|||
name = models.CharField(max_length=1024, verbose_name=_('Name'))
|
||||
date_last_publish = models.DateTimeField(null=True, verbose_name=_("Date last publish"))
|
||||
|
||||
__summary = None
|
||||
__state = None
|
||||
|
||||
@property
|
||||
def meta(self):
|
||||
task = app.tasks.get(self.name, None)
|
||||
|
@ -25,25 +28,43 @@ class CeleryTask(models.Model):
|
|||
|
||||
@property
|
||||
def summary(self):
|
||||
if self.__summary is not None:
|
||||
return self.__summary
|
||||
executions = CeleryTaskExecution.objects.filter(name=self.name)
|
||||
total = executions.count()
|
||||
success = executions.filter(state='SUCCESS').count()
|
||||
return {'total': total, 'success': success}
|
||||
|
||||
@summary.setter
|
||||
def summary(self, value):
|
||||
self.__summary = value
|
||||
|
||||
@staticmethod
|
||||
def compute_state_color(states: list, default_count=5):
|
||||
color = 'green'
|
||||
states = states[:default_count]
|
||||
if not states:
|
||||
return color
|
||||
if states[0] == 'FAILURE':
|
||||
color = 'red'
|
||||
elif 'FAILURE' in states:
|
||||
color = 'yellow'
|
||||
return color
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
last_five_executions = CeleryTaskExecution.objects \
|
||||
.filter(name=self.name) \
|
||||
.order_by('-date_published')[:5]
|
||||
if self.__state is not None:
|
||||
return self.__state
|
||||
last_five_executions = CeleryTaskExecution.objects.filter(
|
||||
name=self.name
|
||||
).order_by('-date_published').values('state')[:5]
|
||||
states = [i['state'] for i in last_five_executions]
|
||||
color = self.compute_state_color(states)
|
||||
return color
|
||||
|
||||
if len(last_five_executions) > 0:
|
||||
if last_five_executions[0].state == 'FAILURE':
|
||||
return "red"
|
||||
|
||||
for execution in last_five_executions:
|
||||
if execution.state == 'FAILURE':
|
||||
return "yellow"
|
||||
return "green"
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
self.__state = value
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Celery Task")
|
||||
|
|
|
@ -67,6 +67,7 @@ class JMSPermedInventory(JMSInventory):
|
|||
'postgresql': ['postgresql'],
|
||||
'sqlserver': ['sqlserver'],
|
||||
'ssh': ['shell', 'python', 'win_shell', 'raw'],
|
||||
'winrm': ['win_shell', 'shell'],
|
||||
}
|
||||
|
||||
if self.module not in protocol_supported_modules_mapping.get(protocol.name, []):
|
||||
|
|
|
@ -87,7 +87,8 @@ class OrgResourceStatisticsRefreshUtil:
|
|||
if not cache_field_name:
|
||||
return
|
||||
org = getattr(instance, 'org', None)
|
||||
cls.refresh_org_fields(((org, cache_field_name),))
|
||||
cache_field_name = tuple(cache_field_name)
|
||||
cls.refresh_org_fields.delay(org_fields=((org, cache_field_name),))
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import abc
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
|
||||
from assets.api.asset.asset import AssetFilterSet
|
||||
|
@ -7,8 +8,7 @@ from assets.models import Asset, Node
|
|||
from common.utils import get_logger, lazyproperty, is_uuid
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from perms import serializers
|
||||
from perms.pagination import AllPermedAssetPagination
|
||||
from perms.pagination import NodePermedAssetPagination
|
||||
from perms.pagination import NodePermedAssetPagination, AllPermedAssetPagination
|
||||
from perms.utils import UserPermAssetUtil, PermAssetDetailUtil
|
||||
from .mixin import (
|
||||
SelfOrPKUserMixin
|
||||
|
@ -39,7 +39,7 @@ class UserPermedAssetRetrieveApi(SelfOrPKUserMixin, RetrieveAPIView):
|
|||
|
||||
|
||||
class BaseUserPermedAssetsApi(SelfOrPKUserMixin, ListAPIView):
|
||||
ordering = ('name',)
|
||||
ordering = []
|
||||
search_fields = ('name', 'address', 'comment')
|
||||
ordering_fields = ("name", "address")
|
||||
filterset_class = AssetFilterSet
|
||||
|
@ -48,6 +48,8 @@ class BaseUserPermedAssetsApi(SelfOrPKUserMixin, ListAPIView):
|
|||
def get_queryset(self):
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return Asset.objects.none()
|
||||
if settings.ASSET_SIZE == 'small':
|
||||
self.ordering = ['name']
|
||||
assets = self.get_assets()
|
||||
assets = self.serializer_class.setup_eager_loading(assets)
|
||||
return assets
|
||||
|
|
|
@ -14,6 +14,7 @@ from assets.api import SerializeToTreeNodeMixin
|
|||
from assets.models import Asset
|
||||
from assets.utils import KubernetesTree
|
||||
from authentication.models import ConnectionToken
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import get_object_or_none, lazyproperty
|
||||
from common.utils.common import timeit
|
||||
from perms.hands import Node
|
||||
|
@ -181,6 +182,8 @@ class UserPermedNodeChildrenWithAssetsAsCategoryTreeApi(BaseUserNodeWithAssetAsT
|
|||
return self.query_asset_util.get_all_assets()
|
||||
|
||||
def _get_tree_nodes_async(self):
|
||||
if self.request.query_params.get('lv') == '0':
|
||||
return [], []
|
||||
if not self.tp or not all(self.tp):
|
||||
nodes = UserPermAssetUtil.get_type_nodes_tree_or_cached(self.user)
|
||||
return nodes, []
|
||||
|
@ -262,5 +265,8 @@ class UserGrantedK8sAsTreeApi(SelfOrPKUserMixin, ListAPIView):
|
|||
if not any([namespace, pod]) and not key:
|
||||
asset_node = k8s_tree_instance.as_asset_tree_node()
|
||||
tree.append(asset_node)
|
||||
try:
|
||||
tree.extend(k8s_tree_instance.async_tree_node(namespace, pod))
|
||||
return Response(data=tree)
|
||||
except Exception as e:
|
||||
raise JMSException(e)
|
||||
|
|
|
@ -130,7 +130,7 @@ class AssetPermission(LabeledMixin, JMSOrgBaseModel):
|
|||
qs1_ids = User.objects.filter(id__in=user_ids).distinct().values_list('id', flat=True)
|
||||
qs2_ids = User.objects.filter(groups__id__in=group_ids).distinct().values_list('id', flat=True)
|
||||
qs_ids = list(qs1_ids) + list(qs2_ids)
|
||||
qs = User.objects.filter(id__in=qs_ids)
|
||||
qs = User.objects.filter(id__in=qs_ids, is_service_account=False)
|
||||
return qs
|
||||
|
||||
def get_all_assets(self, flat=False):
|
||||
|
|
|
@ -9,7 +9,7 @@ class PermedAssetsWillExpireUserMsg(UserMessage):
|
|||
def __init__(self, user, assets, day_count=0):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
self.day_count = _('today') if day_count == 0 else day_count + _('day')
|
||||
self.day_count = _('today') if day_count == 0 else str(day_count) + _('day')
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("You permed assets is about to expire")
|
||||
|
@ -41,7 +41,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
|||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = _('today') if day_count == 0 else day_count + _('day')
|
||||
self.day_count = _('today') if day_count == 0 else str(day_count) + _('day')
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
|
|
|
@ -197,9 +197,9 @@ class AssetPermissionListSerializer(AssetPermissionSerializer):
|
|||
"""Perform necessary eager loading of data."""
|
||||
queryset = queryset \
|
||||
.prefetch_related('labels', 'labels__label') \
|
||||
.annotate(users_amount=Count("users"),
|
||||
user_groups_amount=Count("user_groups"),
|
||||
assets_amount=Count("assets"),
|
||||
nodes_amount=Count("nodes"),
|
||||
.annotate(users_amount=Count("users", distinct=True),
|
||||
user_groups_amount=Count("user_groups", distinct=True),
|
||||
assets_amount=Count("assets", distinct=True),
|
||||
nodes_amount=Count("nodes", distinct=True),
|
||||
)
|
||||
return queryset
|
||||
|
|
|
@ -8,9 +8,9 @@ from rest_framework import serializers
|
|||
from accounts.models import Account
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Node, Asset, Platform
|
||||
from assets.serializers.asset.common import AssetLabelSerializer, AssetProtocolsPermsSerializer
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from assets.serializers.asset.common import AssetProtocolsPermsSerializer
|
||||
from common.serializers import ResourceLabelsMixin
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from perms.serializers.permission import ActionChoicesField
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class AssetPermissionUtil(object):
|
|||
""" 资产授权相关的方法工具 """
|
||||
|
||||
@timeit
|
||||
def get_permissions_for_user(self, user, with_group=True, flat=False):
|
||||
def get_permissions_for_user(self, user, with_group=True, flat=False, with_expired=False):
|
||||
""" 获取用户的授权规则 """
|
||||
perm_ids = set()
|
||||
# user
|
||||
|
@ -25,7 +25,7 @@ class AssetPermissionUtil(object):
|
|||
groups = user.groups.all()
|
||||
group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True)
|
||||
perm_ids.update(group_perm_ids)
|
||||
perms = self.get_permissions(ids=perm_ids)
|
||||
perms = self.get_permissions(ids=perm_ids, with_expired=with_expired)
|
||||
if flat:
|
||||
return perms.values_list('id', flat=True)
|
||||
return perms
|
||||
|
@ -102,6 +102,8 @@ class AssetPermissionUtil(object):
|
|||
return model.objects.filter(id__in=ids)
|
||||
|
||||
@staticmethod
|
||||
def get_permissions(ids):
|
||||
perms = AssetPermission.objects.filter(id__in=ids).valid().order_by('-date_expired')
|
||||
return perms
|
||||
def get_permissions(ids, with_expired=False):
|
||||
perms = AssetPermission.objects.filter(id__in=ids)
|
||||
if not with_expired:
|
||||
perms = perms.valid()
|
||||
return perms.order_by('-date_expired')
|
||||
|
|
|
@ -7,10 +7,10 @@ from django.db.models import Q
|
|||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from assets.const import AllTypes
|
||||
from assets.models import FavoriteAsset, Asset
|
||||
from assets.models import FavoriteAsset, Asset, Node
|
||||
from common.utils.common import timeit, get_logger
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from perms.models import PermNode, UserAssetGrantedTreeNodeRelation
|
||||
from perms.models import PermNode, UserAssetGrantedTreeNodeRelation, AssetPermission
|
||||
from .permission import AssetPermissionUtil
|
||||
|
||||
__all__ = ['AssetPermissionPermAssetUtil', 'UserPermAssetUtil', 'UserPermNodeUtil']
|
||||
|
@ -21,36 +21,37 @@ logger = get_logger(__name__)
|
|||
class AssetPermissionPermAssetUtil:
|
||||
|
||||
def __init__(self, perm_ids):
|
||||
self.perm_ids = perm_ids
|
||||
self.perm_ids = set(perm_ids)
|
||||
|
||||
def get_all_assets(self):
|
||||
""" 获取所有授权的资产 """
|
||||
node_assets = self.get_perm_nodes_assets()
|
||||
direct_assets = self.get_direct_assets()
|
||||
# 比原来的查到所有 asset id 再搜索块很多,因为当资产量大的时候,搜索会很慢
|
||||
return (node_assets | direct_assets).distinct()
|
||||
return (node_assets | direct_assets).order_by().distinct()
|
||||
|
||||
def get_perm_nodes(self):
|
||||
""" 获取所有授权节点 """
|
||||
nodes_ids = AssetPermission.objects \
|
||||
.filter(id__in=self.perm_ids) \
|
||||
.values_list('nodes', flat=True)
|
||||
nodes_ids = set(nodes_ids)
|
||||
nodes = Node.objects.filter(id__in=nodes_ids).only('id', 'key')
|
||||
return nodes
|
||||
|
||||
@timeit
|
||||
def get_perm_nodes_assets(self, flat=False):
|
||||
def get_perm_nodes_assets(self):
|
||||
""" 获取所有授权节点下的资产 """
|
||||
from assets.models import Node
|
||||
nodes = Node.objects \
|
||||
.prefetch_related('granted_by_permissions') \
|
||||
.filter(granted_by_permissions__in=self.perm_ids) \
|
||||
.only('id', 'key')
|
||||
assets = PermNode.get_nodes_all_assets(*nodes)
|
||||
if flat:
|
||||
return set(assets.values_list('id', flat=True))
|
||||
nodes = self.get_perm_nodes()
|
||||
assets = PermNode.get_nodes_all_assets(*nodes, distinct=False)
|
||||
return assets
|
||||
|
||||
@timeit
|
||||
def get_direct_assets(self, flat=False):
|
||||
def get_direct_assets(self):
|
||||
""" 获取直接授权的资产 """
|
||||
assets = Asset.objects.order_by() \
|
||||
.filter(granted_by_permissions__id__in=self.perm_ids) \
|
||||
.distinct()
|
||||
if flat:
|
||||
return set(assets.values_list('id', flat=True))
|
||||
asset_ids = AssetPermission.assets.through.objects \
|
||||
.filter(assetpermission_id__in=self.perm_ids) \
|
||||
.values_list('asset_id', flat=True)
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
return assets
|
||||
|
||||
|
||||
|
@ -152,6 +153,7 @@ class UserPermAssetUtil(AssetPermissionPermAssetUtil):
|
|||
assets = assets.filter(nodes__id=node.id).order_by().distinct()
|
||||
return assets
|
||||
|
||||
@timeit
|
||||
def _get_indirect_perm_node_all_assets(self, node):
|
||||
""" 获取间接授权节点下的所有资产
|
||||
此算法依据 `UserAssetGrantedTreeNodeRelation` 的数据查询
|
||||
|
|
|
@ -72,7 +72,7 @@ class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin):
|
|||
|
||||
@timeit
|
||||
def refresh_if_need(self, force=False):
|
||||
built_just_now = cache.get(self.cache_key_time)
|
||||
built_just_now = False if settings.ASSET_SIZE == 'small' else cache.get(self.cache_key_time)
|
||||
if built_just_now:
|
||||
logger.info('Refresh user perm tree just now, pass: {}'.format(built_just_now))
|
||||
return
|
||||
|
@ -80,12 +80,18 @@ class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin):
|
|||
if not to_refresh_orgs:
|
||||
logger.info('Not have to refresh orgs')
|
||||
return
|
||||
|
||||
logger.info("Delay refresh user orgs: {} {}".format(self.user, [o.name for o in to_refresh_orgs]))
|
||||
refresh_user_orgs_perm_tree(user_orgs=((self.user, tuple(to_refresh_orgs)),))
|
||||
refresh_user_favorite_assets(users=(self.user,))
|
||||
sync = True if settings.ASSET_SIZE == 'small' else False
|
||||
refresh_user_orgs_perm_tree.apply(sync=sync, user_orgs=((self.user, tuple(to_refresh_orgs)),))
|
||||
refresh_user_favorite_assets.apply(sync=sync, users=(self.user,))
|
||||
|
||||
@timeit
|
||||
def refresh_tree_manual(self):
|
||||
"""
|
||||
用来手动 debug
|
||||
:return:
|
||||
"""
|
||||
built_just_now = cache.get(self.cache_key_time)
|
||||
if built_just_now:
|
||||
logger.info('Refresh just now, pass: {}'.format(built_just_now))
|
||||
|
@ -105,6 +111,7 @@ class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin):
|
|||
return
|
||||
|
||||
self._clean_user_perm_tree_for_legacy_org()
|
||||
if settings.ASSET_SIZE != 'small':
|
||||
ttl = settings.PERM_TREE_REGEN_INTERVAL
|
||||
cache.set(self.cache_key_time, int(time.time()), ttl)
|
||||
|
||||
|
@ -193,7 +200,13 @@ class UserPermTreeExpireUtil(_UserPermTreeCacheMixin):
|
|||
cache_key = self.get_cache_key(uid)
|
||||
p.srem(cache_key, *org_ids)
|
||||
p.execute()
|
||||
logger.info('Expire perm tree for users: [{}], orgs: [{}]'.format(user_ids, org_ids))
|
||||
users_display = ','.join([str(i) for i in user_ids[:3]])
|
||||
if len(user_ids) > 3:
|
||||
users_display += '...'
|
||||
orgs_display = ','.join([str(i) for i in org_ids[:3]])
|
||||
if len(org_ids) > 3:
|
||||
orgs_display += '...'
|
||||
logger.info('Expire perm tree for users: [{}], orgs: [{}]'.format(users_display, orgs_display))
|
||||
|
||||
def expire_perm_tree_for_all_user(self):
|
||||
keys = self.client.keys(self.cache_key_all_user)
|
||||
|
|
|
@ -80,9 +80,11 @@ class RoleViewSet(JMSModelViewSet):
|
|||
queryset = Role.objects.filter(id__in=ids).order_by(*self.ordering)
|
||||
org_id = current_org.id
|
||||
q = Q(role__scope=Role.Scope.system) | Q(role__scope=Role.Scope.org, org_id=org_id)
|
||||
role_bindings = RoleBinding.objects.filter(q).values_list('role_id').annotate(user_count=Count('user_id'))
|
||||
role_bindings = RoleBinding.objects.filter(q).values_list('role_id').annotate(
|
||||
user_count=Count('user_id', distinct=True)
|
||||
)
|
||||
role_user_amount_mapper = {role_id: user_count for role_id, user_count in role_bindings}
|
||||
queryset = queryset.annotate(permissions_amount=Count('permissions'))
|
||||
queryset = queryset.annotate(permissions_amount=Count('permissions', distinct=True))
|
||||
queryset = list(queryset)
|
||||
for role in queryset:
|
||||
role.users_amount = role_user_amount_mapper.get(role.id, 0)
|
||||
|
|
|
@ -137,7 +137,7 @@ class LDAPUserImportAPI(APIView):
|
|||
return Response({'msg': _('Get ldap users is None')}, status=400)
|
||||
|
||||
orgs = self.get_orgs()
|
||||
errors = LDAPImportUtil().perform_import(users, orgs)
|
||||
new_users, errors = LDAPImportUtil().perform_import(users, orgs)
|
||||
if errors:
|
||||
return Response({'errors': errors}, status=400)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from rest_framework import generics
|
|||
from rest_framework.permissions import AllowAny
|
||||
|
||||
from authentication.permissions import IsValidUserOrConnectionToken
|
||||
from common.const.choices import COUNTRY_CALLING_CODES
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.timezone import local_now
|
||||
from .. import serializers
|
||||
|
@ -24,7 +25,8 @@ class OpenPublicSettingApi(generics.RetrieveAPIView):
|
|||
def get_object(self):
|
||||
return {
|
||||
"XPACK_ENABLED": settings.XPACK_ENABLED,
|
||||
"INTERFACE": self.interface_setting
|
||||
"INTERFACE": self.interface_setting,
|
||||
"COUNTRY_CALLING_CODES": COUNTRY_CALLING_CODES
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
from notifications.notifications import UserMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class LDAPImportMessage(UserMessage):
|
||||
def __init__(self, user, extra_kwargs):
|
||||
super().__init__(user)
|
||||
self.orgs = extra_kwargs.pop('orgs', [])
|
||||
self.end_time = extra_kwargs.pop('end_time', '')
|
||||
self.start_time = extra_kwargs.pop('start_time', '')
|
||||
self.time_start_display = extra_kwargs.pop('time_start_display', '')
|
||||
self.new_users = extra_kwargs.pop('new_users', [])
|
||||
self.errors = extra_kwargs.pop('errors', [])
|
||||
self.cost_time = extra_kwargs.pop('cost_time', '')
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _('Notification of Synchronized LDAP User Task Results')
|
||||
context = {
|
||||
'orgs': self.orgs,
|
||||
'start_time': self.time_start_display,
|
||||
'end_time': local_now_display(),
|
||||
'cost_time': self.cost_time,
|
||||
'users': self.new_users,
|
||||
'errors': self.errors
|
||||
}
|
||||
message = render_to_string('ldap/_msg_import_ldap_user.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
|
@ -77,6 +77,9 @@ class LDAPSettingSerializer(serializers.Serializer):
|
|||
required=False, label=_('Connect timeout (s)'),
|
||||
)
|
||||
AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size (piece)'))
|
||||
AUTH_LDAP_SYNC_RECEIVERS = serializers.ListField(
|
||||
required=False, label=_('Recipient'), max_length=36
|
||||
)
|
||||
|
||||
AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth'))
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class OAuth2SettingSerializer(serializers.Serializer):
|
|||
)
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_METHOD = serializers.ChoiceField(
|
||||
default='GET', label=_('Client authentication method'),
|
||||
choices=(('GET', 'GET'), ('POST', 'POST'))
|
||||
choices=(('GET', 'GET'), ('POST', 'POST-DATA'), ('POST_JSON', 'POST-JSON'))
|
||||
)
|
||||
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Provider userinfo endpoint')
|
||||
|
|
|
@ -11,6 +11,7 @@ __all__ = [
|
|||
class PublicSettingSerializer(serializers.Serializer):
|
||||
XPACK_ENABLED = serializers.BooleanField()
|
||||
INTERFACE = serializers.DictField()
|
||||
COUNTRY_CALLING_CODES = serializers.ListField()
|
||||
|
||||
|
||||
class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
import time
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
from ops.celery.decorator import after_app_ready_start
|
||||
from ops.celery.utils import (
|
||||
create_or_update_celery_periodic_tasks, disable_celery_periodic_task
|
||||
)
|
||||
from orgs.models import Organization
|
||||
from settings.notifications import LDAPImportMessage
|
||||
from users.models import User
|
||||
from ..utils import LDAPSyncUtil, LDAPServerUtil, LDAPImportUtil
|
||||
|
||||
__all__ = ['sync_ldap_user', 'import_ldap_user_periodic', 'import_ldap_user']
|
||||
|
@ -23,6 +27,8 @@ def sync_ldap_user():
|
|||
|
||||
@shared_task(verbose_name=_('Periodic import ldap user'))
|
||||
def import_ldap_user():
|
||||
start_time = time.time()
|
||||
time_start_display = local_now_display()
|
||||
logger.info("Start import ldap user task")
|
||||
util_server = LDAPServerUtil()
|
||||
util_import = LDAPImportUtil()
|
||||
|
@ -35,11 +41,26 @@ def import_ldap_user():
|
|||
org_ids = [Organization.DEFAULT_ID]
|
||||
default_org = Organization.default()
|
||||
orgs = list(set([Organization.get_instance(org_id, default=default_org) for org_id in org_ids]))
|
||||
errors = util_import.perform_import(users, orgs)
|
||||
new_users, errors = util_import.perform_import(users, orgs)
|
||||
if errors:
|
||||
logger.error("Imported LDAP users errors: {}".format(errors))
|
||||
else:
|
||||
logger.info('Imported {} users successfully'.format(len(users)))
|
||||
if settings.AUTH_LDAP_SYNC_RECEIVERS:
|
||||
user_ids = settings.AUTH_LDAP_SYNC_RECEIVERS
|
||||
recipient_list = User.objects.filter(id__in=list(user_ids))
|
||||
end_time = time.time()
|
||||
extra_kwargs = {
|
||||
'orgs': orgs,
|
||||
'end_time': end_time,
|
||||
'start_time': start_time,
|
||||
'time_start_display': time_start_display,
|
||||
'new_users': new_users,
|
||||
'errors': errors,
|
||||
'cost_time': end_time - start_time,
|
||||
}
|
||||
for user in recipient_list:
|
||||
LDAPImportMessage(user, extra_kwargs).publish()
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Registration periodic import ldap user task'))
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{% load i18n %}
|
||||
<p>{% trans "Sync task Finish" %}</p>
|
||||
<b>{% trans 'Time' %}:</b>
|
||||
<ul>
|
||||
<li>{% trans 'Date start' %}: {{ start_time }}</li>
|
||||
<li>{% trans 'Date end' %}: {{ end_time }}</li>
|
||||
<li>{% trans 'Time cost' %}: {{ cost_time| floatformat:0 }}s</li>
|
||||
</ul>
|
||||
<b>{% trans "Synced Organization" %}:</b>
|
||||
<ul>
|
||||
{% for org in orgs %}
|
||||
<li>{{ org }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<b>{% trans "Synced User" %}:</b>
|
||||
<ul>
|
||||
{% if users %}
|
||||
{% for user in users %}
|
||||
<li>{{ user }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li>{% trans 'No user synchronization required' %}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if errors %}
|
||||
<b>{% trans 'Error' %}:</b>
|
||||
<ul>
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -400,11 +400,14 @@ class LDAPImportUtil(object):
|
|||
logger.info('Start perform import ldap users, count: {}'.format(len(users)))
|
||||
errors = []
|
||||
objs = []
|
||||
new_users = []
|
||||
group_users_mapper = defaultdict(set)
|
||||
for user in users:
|
||||
groups = user.pop('groups', [])
|
||||
try:
|
||||
obj, created = self.update_or_create(user)
|
||||
if created:
|
||||
new_users.append(obj)
|
||||
objs.append(obj)
|
||||
except Exception as e:
|
||||
errors.append({user['username']: str(e)})
|
||||
|
@ -421,7 +424,7 @@ class LDAPImportUtil(object):
|
|||
for org in orgs:
|
||||
self.bind_org(org, objs, group_users_mapper)
|
||||
logger.info('End perform import ldap users')
|
||||
return errors
|
||||
return new_users, errors
|
||||
|
||||
def exit_user_group(self, user_groups_mapper):
|
||||
# 通过对比查询本次导入用户需要移除的用户组
|
||||
|
|
|
@ -42,7 +42,7 @@ class SmartEndpointViewMixin:
|
|||
return endpoint
|
||||
|
||||
def match_endpoint_by_label(self):
|
||||
return Endpoint.match_by_instance_label(self.target_instance, self.target_protocol)
|
||||
return Endpoint.match_by_instance_label(self.target_instance, self.target_protocol, self.request)
|
||||
|
||||
def match_endpoint_by_target_ip(self):
|
||||
target_ip = self.request.GET.get('target_ip', '') # 支持target_ip参数,用来方便测试
|
||||
|
|
|
@ -75,7 +75,20 @@ class Endpoint(JMSBaseModel):
|
|||
return endpoint
|
||||
|
||||
@classmethod
|
||||
def match_by_instance_label(cls, instance, protocol):
|
||||
def handle_endpoint_host(cls, endpoint, request=None):
|
||||
if not endpoint.host and request:
|
||||
# 动态添加 current request host
|
||||
host_port = request.get_host()
|
||||
# IPv6
|
||||
if host_port.startswith('['):
|
||||
host = host_port.split(']:')[0].rstrip(']') + ']'
|
||||
else:
|
||||
host = host_port.split(':')[0]
|
||||
endpoint.host = host
|
||||
return endpoint
|
||||
|
||||
@classmethod
|
||||
def match_by_instance_label(cls, instance, protocol, request=None):
|
||||
from assets.models import Asset
|
||||
from terminal.models import Session
|
||||
if isinstance(instance, Session):
|
||||
|
@ -88,6 +101,7 @@ class Endpoint(JMSBaseModel):
|
|||
endpoints = cls.objects.filter(name__in=list(values)).order_by('-date_updated')
|
||||
for endpoint in endpoints:
|
||||
if endpoint.is_valid_for(instance, protocol):
|
||||
endpoint = cls.handle_endpoint_host(endpoint, request)
|
||||
return endpoint
|
||||
|
||||
|
||||
|
@ -130,13 +144,5 @@ class EndpointRule(JMSBaseModel):
|
|||
endpoint = endpoint_rule.endpoint
|
||||
else:
|
||||
endpoint = Endpoint.get_or_create_default(request)
|
||||
if not endpoint.host and request:
|
||||
# 动态添加 current request host
|
||||
host_port = request.get_host()
|
||||
# IPv6
|
||||
if host_port.startswith('['):
|
||||
host = host_port.split(']:')[0].rstrip(']') + ']'
|
||||
else:
|
||||
host = host_port.split(':')[0]
|
||||
endpoint.host = host
|
||||
endpoint = Endpoint.handle_endpoint_host(endpoint, request)
|
||||
return endpoint
|
||||
|
|
|
@ -5,3 +5,4 @@ from .ticket import *
|
|||
from .comment import *
|
||||
from .relation import *
|
||||
from .super_ticket import *
|
||||
from .perms import *
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
from django.conf import settings
|
||||
|
||||
from assets.models import Asset, Node
|
||||
from assets.serializers.asset.common import MiniAssetSerializer
|
||||
from assets.serializers.node import NodeSerializer
|
||||
from common.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgReadonlyModelViewSet
|
||||
from perms.utils import AssetPermissionPermAssetUtil
|
||||
from perms.utils.permission import AssetPermissionUtil
|
||||
from tickets.const import TicketApplyAssetScope
|
||||
|
||||
__all__ = ['ApplyAssetsViewSet', 'ApplyNodesViewSet']
|
||||
|
||||
|
||||
class ApplyAssetsViewSet(OrgReadonlyModelViewSet, SuggestionMixin):
|
||||
model = Asset
|
||||
serializer_class = MiniAssetSerializer
|
||||
rbac_perms = (
|
||||
("match", "assets.match_asset"),
|
||||
)
|
||||
|
||||
search_fields = ("name", "address", "comment")
|
||||
|
||||
def get_queryset(self):
|
||||
if TicketApplyAssetScope.is_permed():
|
||||
queryset = self.get_assets(with_expired=True)
|
||||
elif TicketApplyAssetScope.is_permed_valid():
|
||||
queryset = self.get_assets()
|
||||
else:
|
||||
queryset = super().get_queryset()
|
||||
return queryset
|
||||
|
||||
def get_assets(self, with_expired=False):
|
||||
perms = AssetPermissionUtil().get_permissions_for_user(
|
||||
self.request.user, flat=True, with_expired=with_expired
|
||||
)
|
||||
util = AssetPermissionPermAssetUtil(perms)
|
||||
assets = util.get_all_assets()
|
||||
return assets
|
||||
|
||||
|
||||
class ApplyNodesViewSet(OrgReadonlyModelViewSet, SuggestionMixin):
|
||||
model = Node
|
||||
serializer_class = NodeSerializer
|
||||
rbac_perms = (
|
||||
("match", "assets.match_node"),
|
||||
)
|
||||
|
||||
search_fields = ('full_value',)
|
||||
|
||||
def get_queryset(self):
|
||||
if TicketApplyAssetScope.is_permed():
|
||||
queryset = self.get_nodes(with_expired=True)
|
||||
elif TicketApplyAssetScope.is_permed_valid():
|
||||
queryset = self.get_nodes()
|
||||
else:
|
||||
queryset = super().get_queryset()
|
||||
return queryset
|
||||
|
||||
def get_nodes(self, with_expired=False):
|
||||
perms = AssetPermissionUtil().get_permissions_for_user(
|
||||
self.request.user, flat=True, with_expired=with_expired
|
||||
)
|
||||
util = AssetPermissionPermAssetUtil(perms)
|
||||
nodes = util.get_perm_nodes()
|
||||
return nodes
|
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from audits.handler import create_or_update_operate_log
|
||||
|
@ -41,7 +42,6 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
ordering = ('-date_created',)
|
||||
rbac_perms = {
|
||||
'open': 'tickets.view_ticket',
|
||||
'bulk': 'tickets.change_ticket',
|
||||
}
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
@ -122,7 +122,7 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
self._record_operate_log(instance, TicketAction.close)
|
||||
return Response('ok')
|
||||
|
||||
@action(detail=False, methods=[PUT], permission_classes=[RBACPermission, ])
|
||||
@action(detail=False, methods=[PUT], permission_classes=[IsAuthenticated, ])
|
||||
def bulk(self, request, *args, **kwargs):
|
||||
self.ticket_not_allowed()
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from django.conf import settings
|
||||
from django.db.models import TextChoices, IntegerChoices
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -56,3 +57,21 @@ class TicketApprovalStrategy(TextChoices):
|
|||
custom_user = 'custom_user', _("Custom user")
|
||||
super_admin = 'super_admin', _("Super admin")
|
||||
super_org_admin = 'super_org_admin', _("Super admin and org admin")
|
||||
|
||||
|
||||
class TicketApplyAssetScope(TextChoices):
|
||||
all = 'all', _("All assets")
|
||||
permed = 'permed', _("Permed assets")
|
||||
permed_valid = 'permed_valid', _('Permed valid assets')
|
||||
|
||||
@classmethod
|
||||
def get_scope(cls):
|
||||
return settings.TICKET_APPLY_ASSET_SCOPE.lower()
|
||||
|
||||
@classmethod
|
||||
def is_permed(cls):
|
||||
return cls.get_scope() == cls.permed
|
||||
|
||||
@classmethod
|
||||
def is_permed_valid(cls):
|
||||
return cls.get_scope() == cls.permed_valid
|
||||
|
|
|
@ -57,7 +57,7 @@ class TicketStep(JMSBaseModel):
|
|||
assignees.update(state=state)
|
||||
self.status = StepStatus.closed
|
||||
self.state = state
|
||||
self.save(update_fields=['state', 'status'])
|
||||
self.save(update_fields=['state', 'status', 'date_updated'])
|
||||
|
||||
def set_active(self):
|
||||
self.status = StepStatus.active
|
||||
|
|
|
@ -16,6 +16,8 @@ router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login
|
|||
router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket')
|
||||
router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket')
|
||||
router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation')
|
||||
router.register('apply-assets', api.ApplyAssetsViewSet, 'ticket-session-relation')
|
||||
router.register('apply-nodes', api.ApplyNodesViewSet, 'ticket-session-relation')
|
||||
|
||||
urlpatterns = [
|
||||
path('tickets/<uuid:ticket_id>/session/', api.TicketSessionApi.as_view(), name='ticket-session'),
|
||||
|
|
|
@ -729,7 +729,7 @@ class JSONFilterMixin:
|
|||
|
||||
bindings = RoleBinding.objects.filter(**kwargs, role__in=value)
|
||||
if match == 'm2m_all':
|
||||
user_id = bindings.values('user_id').annotate(count=Count('user_id')) \
|
||||
user_id = bindings.values('user_id').annotate(count=Count('user_id', distinct=True)) \
|
||||
.filter(count=len(value)).values_list('user_id', flat=True)
|
||||
else:
|
||||
user_id = bindings.values_list('user_id', flat=True)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
@ -46,7 +46,7 @@ class UserGroupSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
|||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('labels', 'labels__label') \
|
||||
.annotate(users_amount=Count('users'))
|
||||
.annotate(users_amount=Count('users', distinct=True, filter=Q(users__is_service_account=False)))
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -163,9 +163,9 @@ def on_openid_create_or_update_user(sender, request, user, created, name, userna
|
|||
user.save()
|
||||
|
||||
|
||||
@shared_task(verbose_name=_('Clean audits session task log'))
|
||||
@shared_task(verbose_name=_('Clean up expired user sessions'))
|
||||
@register_as_period_task(crontab=CRONTAB_AT_PM_TWO)
|
||||
def clean_audits_log_period():
|
||||
def clean_expired_user_session_period():
|
||||
UserSession.clear_expired_sessions()
|
||||
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ def check_user_expired_periodic():
|
|||
@tmp_to_root_org()
|
||||
def check_unused_users():
|
||||
uncommon_users_ttl = settings.SECURITY_UNCOMMON_USERS_TTL
|
||||
if not uncommon_users_ttl or not uncommon_users_ttl.isdigit():
|
||||
if not uncommon_users_ttl:
|
||||
return
|
||||
|
||||
uncommon_users_ttl = int(uncommon_users_ttl)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
.margin-bottom {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-style {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
@ -22,6 +23,19 @@
|
|||
height: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: 18rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
.input-group-btn .btn-secondary {
|
||||
color: #464a4c;
|
||||
background-color: #eceeef;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block html_title %}{% trans 'Forgot password' %}{% endblock %}
|
||||
|
@ -57,9 +71,26 @@
|
|||
placeholder="{% trans 'Email account' %}" value="{{ email }}">
|
||||
</div>
|
||||
<div id="validate-sms" class="validate-field margin-bottom">
|
||||
<div class="input-group">
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="country-code-value">+86</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu scrollable-menu">
|
||||
{% for country in countries %}
|
||||
<li>
|
||||
<a href="#" class="dropdown-item d-flex justify-content-between">
|
||||
<span class="country-name text-left">{{ country.name }}</span>
|
||||
<span class="country-code">{{ country.value }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<input type="tel" id="sms" name="sms" class="form-control input-style"
|
||||
placeholder="{% trans 'Mobile number' %}" value="{{ sms }}">
|
||||
<small style="color: #999; margin-left: 5px">{{ form.sms.help_text }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="margin-bottom challenge-required">
|
||||
<input type="text" id="code" name="code" class="form-control input-style"
|
||||
|
@ -76,7 +107,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function (){
|
||||
$(function () {
|
||||
const validateSelectRef = $('#validate-backend-select')
|
||||
const formType = $('input[name="form_type"]').val()
|
||||
validateSelectRef.val(formType)
|
||||
|
@ -84,19 +115,31 @@
|
|||
selectChange(formType);
|
||||
}
|
||||
})
|
||||
|
||||
$(".dropdown-menu li a").click(function (evt) {
|
||||
const inputGroup = $('.input-group');
|
||||
const inputGroupAddon = inputGroup.find('.country-code-value');
|
||||
const selectedCountry = $(evt.target).closest('li');
|
||||
const selectedCountryCode = selectedCountry.find('.country-code').html();
|
||||
inputGroupAddon.html(selectedCountryCode)
|
||||
});
|
||||
|
||||
|
||||
function getQueryString(name) {
|
||||
const reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
|
||||
const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
|
||||
const r = window.location.search.substr(1).match(reg);
|
||||
if(r !== null)
|
||||
if (r !== null)
|
||||
return unescape(r[2])
|
||||
return null
|
||||
}
|
||||
|
||||
function selectChange(name) {
|
||||
$('.validate-field').hide()
|
||||
$('#validate-' + name).show()
|
||||
$('#validate-' + name + '-tip').show()
|
||||
$('input[name="form_type"]').attr('value', name)
|
||||
}
|
||||
|
||||
function sendChallengeCode(currentBtn) {
|
||||
let time = 60;
|
||||
const token = getQueryString('token')
|
||||
|
@ -104,7 +147,7 @@
|
|||
|
||||
const formType = $('input[name="form_type"]').val()
|
||||
const email = $('#email').val()
|
||||
const sms = $('#sms').val()
|
||||
let sms = $('#sms').val();
|
||||
const errMsg = "{% trans 'The {} cannot be empty' %}"
|
||||
|
||||
if (formType === 'sms') {
|
||||
|
@ -118,10 +161,11 @@
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
sms = $(".input-group .country-code-value").html() + sms
|
||||
const data = {
|
||||
form_type: formType, email: email, sms: sms,
|
||||
}
|
||||
|
||||
function onSuccess() {
|
||||
const originBtnText = currentBtn.innerHTML;
|
||||
currentBtn.disabled = true
|
||||
|
|
|
@ -14,22 +14,24 @@
|
|||
</strong>
|
||||
</p>
|
||||
<div>
|
||||
<img src="{% static 'img/authenticator_android.png' %}" width="128" height="128" alt="">
|
||||
<img src="{{ authenticator_android_url }}" width="128" height="128" alt="">
|
||||
<p>{% trans 'Android downloads' %}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img src="{% static 'img/authenticator_iphone.png' %}" width="128" height="128" alt="">
|
||||
<img src="{{ authenticator_iphone_url }}" width="128" height="128" alt="">
|
||||
<p>{% trans 'iPhone downloads' %}</p>
|
||||
</div>
|
||||
|
||||
<p style="margin: 20px auto;"><strong style="color: #000000">{% trans 'After installation, click the next step to enter the binding page (if installed, go to the next step directly).' %}</strong></p>
|
||||
<p style="margin: 20px auto;"><strong
|
||||
style="color: #000000">{% trans 'After installation, click the next step to enter the binding page (if installed, go to the next step directly).' %}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'authentication:user-otp-enable-bind' %}" class="next">{% trans 'Next' %}</a>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$(function () {
|
||||
$('.change-color li:eq(1) i').css('color', '{{ INTERFACE.primary_color }}')
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils._os import safe_join
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
|
@ -45,9 +49,26 @@ class UserOtpEnableStartView(AuthMixin, TemplateView):
|
|||
class UserOtpEnableInstallAppView(TemplateView):
|
||||
template_name = 'users/user_otp_enable_install_app.html'
|
||||
|
||||
@staticmethod
|
||||
def replace_authenticator_png(platform):
|
||||
media_url = settings.MEDIA_URL
|
||||
base_path = f'img/authenticator_{platform}.png'
|
||||
authenticator_media_path = safe_join(settings.MEDIA_ROOT, base_path)
|
||||
if os.path.exists(authenticator_media_path):
|
||||
authenticator_url = f'{media_url}{base_path}'
|
||||
else:
|
||||
authenticator_url = static(base_path)
|
||||
return authenticator_url
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = get_user_or_pre_auth_user(self.request)
|
||||
context = {'user': user}
|
||||
authenticator_android_url = self.replace_authenticator_png('android')
|
||||
authenticator_iphone_url = self.replace_authenticator_png('iphone')
|
||||
context = {
|
||||
'user': user,
|
||||
'authenticator_android_url': authenticator_android_url,
|
||||
'authenticator_iphone_url': authenticator_iphone_url
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ from django.views.generic import FormView, RedirectView
|
|||
|
||||
from authentication.errors import IntervalTooShort
|
||||
from authentication.utils import check_user_property_is_correct
|
||||
from common.const.choices import COUNTRY_CALLING_CODES
|
||||
from common.utils import FlashMessageUtil, get_object_or_none, random_string
|
||||
from common.utils.verify_code import SendAndVerifyCodeUtil
|
||||
from users.notifications import ResetPasswordSuccessMsg
|
||||
|
@ -108,7 +109,7 @@ class UserForgotPasswordView(FormView):
|
|||
for k, v in cleaned_data.items():
|
||||
if v:
|
||||
context[k] = v
|
||||
|
||||
context['countries'] = COUNTRY_CALLING_CODES
|
||||
context['form_type'] = 'email'
|
||||
context['XPACK_ENABLED'] = settings.XPACK_ENABLED
|
||||
validate_backends = self.get_validate_backends_context(has_phone)
|
||||
|
|
|
@ -85,7 +85,7 @@ REDIS_PORT: 6379
|
|||
# SECURITY_WATERMARK_ENABLED: False
|
||||
|
||||
# 浏览器关闭页面后,会话过期
|
||||
# SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE: False
|
||||
# SESSION_EXPIRE_AT_BROWSER_CLOSE: False
|
||||
|
||||
# 每次api请求,session续期
|
||||
# SESSION_SAVE_EVERY_REQUEST: True
|
||||
|
|
|
@ -730,17 +730,16 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "azure-core"
|
||||
version = "1.29.6"
|
||||
version = "1.29.7"
|
||||
description = "Microsoft Azure Core Library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "azure-core-1.29.6.tar.gz", hash = "sha256:13b485252ecd9384ae624894fe51cfa6220966207264c360beada239f88b738a"},
|
||||
{file = "azure_core-1.29.6-py3-none-any.whl", hash = "sha256:604a005bce6a49ba661bb7b2be84a9b169047e52fcfcd0a4e4770affab4178f7"},
|
||||
{file = "azure-core-1.29.7.tar.gz", hash = "sha256:2944faf1a7ff1558b1f457cabf60f279869cabaeef86b353bed8eb032c7d8c5e"},
|
||||
{file = "azure_core-1.29.7-py3-none-any.whl", hash = "sha256:95a7b41b4af102e5fcdfac9500fcc82ff86e936c7145a099b7848b9ac0501250"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0,<5.0"
|
||||
requests = ">=2.21.0"
|
||||
six = ">=1.11.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
@ -961,19 +960,22 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.12.2"
|
||||
version = "4.12.3"
|
||||
description = "Screen-scraping library"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
|
||||
{file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
|
||||
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
||||
[package.extras]
|
||||
cchardet = ["cchardet"]
|
||||
chardet = ["chardet"]
|
||||
charset-normalizer = ["charset-normalizer"]
|
||||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
|
@ -1062,6 +1064,22 @@ type = "legacy"
|
|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "cached-property"
|
||||
version = "1.5.2"
|
||||
description = "A decorator for caching properties in classes."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
|
||||
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.3.2"
|
||||
|
@ -1811,6 +1829,22 @@ type = "legacy"
|
|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
description = "XML bomb protection for Python stdlib modules"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
|
||||
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
|
@ -1895,7 +1929,7 @@ reference = "tsinghua"
|
|||
[[package]]
|
||||
name = "django-cas-ng"
|
||||
version = "4.3.0"
|
||||
description = "Django CAS 1.0/2.0/3.0 client authentication library, support Django 2.2, 3.0, 3.1, 3.2, 4.0 and Python 3.7+"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
|
@ -2228,22 +2262,23 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.4.2"
|
||||
version = "2.5.0"
|
||||
description = "DNS toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.8,<4.0"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"},
|
||||
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"},
|
||||
{file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"},
|
||||
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dnssec = ["cryptography (>=2.6,<42.0)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"]
|
||||
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=5.0.3)", "mypy (>=1.0.1)", "pylint (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "sphinx (>=7.0.0)", "twine (>=4.0.0)", "wheel (>=0.41.0)"]
|
||||
dnssec = ["cryptography (>=41)"]
|
||||
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.25.1)"]
|
||||
doq = ["aioquic (>=0.9.20)"]
|
||||
idna = ["idna (>=2.1,<4.0)"]
|
||||
trio = ["trio (>=0.14,<0.23)"]
|
||||
wmi = ["wmi (>=1.5.1,<2.0.0)"]
|
||||
idna = ["idna (>=2.1)"]
|
||||
trio = ["trio (>=0.14)"]
|
||||
wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
|
@ -2528,6 +2563,41 @@ type = "legacy"
|
|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "exchangelib"
|
||||
version = "5.1.0"
|
||||
description = "Client for Microsoft Exchange Web Services (EWS)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "exchangelib-5.1.0-py2.py3-none-any.whl", hash = "sha256:28f492df84c3d74b1183d535d34daa8c7d7a80fe69cf6b35041770824781f4b0"},
|
||||
{file = "exchangelib-5.1.0.tar.gz", hash = "sha256:f835c14d380839dc74f53664f091cafc978a158855877a510c0c5554b85cdbac"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cached-property = "*"
|
||||
defusedxml = ">=0.6.0"
|
||||
dnspython = ">=2.2.0"
|
||||
isodate = "*"
|
||||
lxml = ">3.0"
|
||||
oauthlib = "*"
|
||||
pygments = "*"
|
||||
requests = ">=2.7"
|
||||
requests-ntlm = ">=0.2.0"
|
||||
requests-oauthlib = "*"
|
||||
tzdata = "*"
|
||||
tzlocal = "*"
|
||||
|
||||
[package.extras]
|
||||
complete = ["requests-gssapi", "requests-negotiate-sspi"]
|
||||
kerberos = ["requests-gssapi"]
|
||||
sspi = ["requests-negotiate-sspi"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.0.1"
|
||||
|
@ -2789,13 +2859,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "google-auth"
|
||||
version = "2.25.2"
|
||||
version = "2.27.0"
|
||||
description = "Google Authentication Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "google-auth-2.25.2.tar.gz", hash = "sha256:42f707937feb4f5e5a39e6c4f343a17300a459aaf03141457ba505812841cc40"},
|
||||
{file = "google_auth-2.25.2-py2.py3-none-any.whl", hash = "sha256:473a8dfd0135f75bb79d878436e568f2695dce456764bf3a02b6f8c540b1d256"},
|
||||
{file = "google-auth-2.27.0.tar.gz", hash = "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821"},
|
||||
{file = "google_auth-2.27.0-py2.py3-none-any.whl", hash = "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -3515,12 +3585,12 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "jms-storage"
|
||||
version = "0.0.53"
|
||||
version = "0.0.55"
|
||||
description = "Jumpserver storage python sdk tools"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "jms-storage-0.0.53.tar.gz", hash = "sha256:d197816b3edc6a370892d5a246265ea7ae7267159b1a4ad88a14ae56e92013be"},
|
||||
{file = "jms-storage-0.0.55.tar.gz", hash = "sha256:cab3ac01b9733dd30101b59dd21de104543355c010ca0024cfaed3069e392cfa"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -3718,106 +3788,96 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.0.0"
|
||||
version = "5.1.0"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "lxml-5.0.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73bfab795d354aaf2f4eb7a5b0db513031734fd371047342d5803834ce19ec18"},
|
||||
{file = "lxml-5.0.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cb564bbe55ff0897d9cf1225041a44576d7ae87f06fd60163544c91de2623d3f"},
|
||||
{file = "lxml-5.0.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a5501438dd521bb7e0dde5008c40c7bfcfaafaf86eccb3f9bd27509abb793da"},
|
||||
{file = "lxml-5.0.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ba26a7dc929a1b3487d51bbcb0099afed2fc06e891b82845c8f37a2d7d7fbbd"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:9b59c429e1a2246da86ae237ffc3565efcdc71c281cd38ca8b44d5fb6a3b993a"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:3ffa066db40b0347e48334bd4465de768e295a3525b9a59831228b5f4f93162d"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8ce8b468ab50f9e944719d1134709ec11fe0d2840891a6cae369e22141b1094c"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:583c0e15ae06adc81035346ae2abb2e748f0b5197e7740d8af31222db41bbf7b"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:904d36165848b59c4e04ae5b969072e602bd987485076fca8ec42c6cd7a7aedc"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac21aace6712472e77ea9dfc38329f53830c4259ece54c786107105ebb069053"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f92d73faa0b1a76d1932429d684b7ce95829e93c3eef3715ec9b98ab192c9d31"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-win32.whl", hash = "sha256:03290e2f714f2e7431c8430c08b48167f657da7bc689c6248e828ff3c66d5b1b"},
|
||||
{file = "lxml-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e6cbb68bf70081f036bfc018649cf4b46c4e7eaf7860a277cae92dee2a57f69"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5382612ba2424cea5d2c89e2c29077023d8de88f8d60d5ceff5f76334516df9e"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:07a900735bad9af7be3085480bf384f68ed5580ba465b39a098e6a882c060d6b"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:980ba47c8db4b9d870014c7040edb230825b79017a6a27aa54cdb6fcc02d8cc0"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6507c58431dbd95b50654b3313c5ad54f90e54e5f2cdacf733de61eae478eec5"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4a45a278518e4308865c1e9dbb2c42ce84fb154efb03adeb16fdae3c1687c7c9"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:59cea9ba1c675fbd6867ca1078fc717a113e7f5b7644943b74137b7cc55abebf"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd39ef87fd1f7bb5c4aa53454936e6135cbfe03fe3744e8218be193f9e4fef16"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e6bb39d91bf932e7520cb5718ae3c2f498052aca53294d5d59fdd9068fe1a7f2"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-win32.whl", hash = "sha256:21af2c3862db6f4f486cddf73ec1157b40d5828876c47cd880edcbad8240ea1b"},
|
||||
{file = "lxml-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:c1249aa4eaced30b59ecf8b8cae0b1ccede04583c74ca7d10b6f8bbead908b2c"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f30e697b6215e759d0824768b2c5b0618d2dc19abe6c67eeed2b0460f52470d1"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d1bb64646480c36a4aa1b6a44a5b6e33d0fcbeab9f53f1b39072cd3bb2c6243a"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e69c36c8618707a90ed3fb6f48a6cc9254ffcdbf7b259e439a5ae5fbf9c5206"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca498f8554a09fbc3a2f8fc4b23261e07bc27bef99b3df98e2570688033f6fc"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0326e9b8176ea77269fb39e7af4010906e73e9496a9f8eaf06d253b1b1231ceb"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-win32.whl", hash = "sha256:5fb988e15378d6e905ca8f60813950a0c56da9469d0e8e5d8fe785b282684ec5"},
|
||||
{file = "lxml-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:bb58e8f4b2cfe012cd312239b8d5139995fe8f5945c7c26d5fbbbb1ddb9acd47"},
|
||||
{file = "lxml-5.0.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81509dffd8aba3bdb43e90cbd218c9c068a1f4047d97bc9546b3ac9e3a4ae81d"},
|
||||
{file = "lxml-5.0.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e675a4b95208e74c34ac0751cc4bab9170e7728b61601fb0f4746892c2bb7e0b"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:405e3760f83a8ba3bdb6e622ec79595cdc20db916ce37377bbcb95b5711fa4ca"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f15844a1b93dcaa09c2b22e22a73384f3ae4502347c3881cfdd674e14ac04e21"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88f559f8beb6b90e41a7faae4aca4c8173a4819874a9bf8e74c8d7c1d51f3162"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e8c63f5c7d87e7044880b01851ac4e863c3349e6f6b6ab456fe218d9346e816d"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:0d277d4717756fe8816f0beeff229cb72f9dd02a43b70e1d3f07c8efadfb9fe1"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8954da15403db1acfc0544b3c3f963a6ef4e428283ab6555e3e298bbbff1cf6"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:aebd8fd378e074b22e79cad329dcccd243c40ff1cafaa512d19276c5bb9554e1"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b6d4e148edee59c2ad38af15810dbcb8b5d7b13e5de3509d8cf3edfe74c0adca"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:70ab4e02f7aa5fb4131c8b222a111ce7676f3767e36084fba3a4e7338dc82dcd"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-win32.whl", hash = "sha256:de1a8b54170024cf1c0c2718c82412bca42cd82e390556e3d8031af9541b416f"},
|
||||
{file = "lxml-5.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5b39f63edbe7e018c2ac1cf0259ee0dd2355274e8a3003d404699b040782e55e"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:77b73952534967a4497d9e4f26fbeebfba19950cbc66b7cc3a706214429d8106"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8cc0a951e5616ac626f7036309c41fb9774adcd4aa7db0886463da1ce5b65edb"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:4b9d5b01900a760eb3acf6cef50aead4ef2fa79e7ddb927084244e41dfe37b65"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:173bcead3af5d87c7bca9a030675073ddaad8e0a9f0b04be07cd9390453e7226"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:44fa9afd632210f1eeda51cf284ed8dbab0c7ec8b008dd39ba02818e0e114e69"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fef10f27d6318d2d7c88680e113511ddecf09ee4f9559b3623b73ee89fa8f6cc"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3663542aee845129a981889c19b366beab0b1dadcf5ca164696aabfe1aa51667"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7188495c1bf71bfda87d78ed50601e72d252119ce11710d6e71ff36e35fea5a0"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-win32.whl", hash = "sha256:6a2de85deabf939b0af89e2e1ea46bfb1239545e2da6f8ac96522755a388025f"},
|
||||
{file = "lxml-5.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ea56825c1e23c9c8ea385a191dac75f9160477057285b88c88736d9305e6118f"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:3f908afd0477cace17f941d1b9cfa10b769fe1464770abe4cfb3d9f35378d0f8"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52a9ab31853d3808e7cf0183b3a5f7e8ffd622ea4aee1deb5252dbeaefd5b40d"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c7fe19abb3d3c55a9e65d289b12ad73b3a31a3f0bda3c539a890329ae9973bd6"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1ef0793e1e2dd221fce7c142177008725680f7b9e4a184ab108d90d5d3ab69b7"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:581a78f299a9f5448b2c3aea904bfcd17c59bf83016d221d7f93f83633bb2ab2"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:affdd833f82334fdb10fc9a1c7b35cdb5a86d0b672b4e14dd542e1fe7bcea894"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6bba06d8982be0f0f6432d289a8d104417a0ab9ed04114446c4ceb6d4a40c65d"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:80209b31dd3908bc5b014f540fd192c97ea52ab179713a730456c5baf7ce80c1"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-win32.whl", hash = "sha256:dac2733fe4e159b0aae0439db6813b7b1d23ff96d0b34c0107b87faf79208c4e"},
|
||||
{file = "lxml-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:ee60f33456ff34b2dd1d048a740a2572798356208e4c494301c931de3a0ab3a2"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:5eff173f0ff408bfa578cbdafd35a7e0ca94d1a9ffe09a8a48e0572d0904d486"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:78d6d8e5b54ed89dc0f0901eaaa579c384ad8d59fa43cc7fb06e9bb89115f8f4"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:71a7cee869578bc17b18050532bb2f0bc682a7b97dda77041741a1bd2febe6c7"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7df433d08d4587dc3932f7fcfc3194519a6824824104854e76441fd3bc000d29"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:793be9b4945c2dfd69828fb5948d7d9569b78e0599e4a2e88d92affeb0ff3aa3"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7cfb6af73602c8d288581df8a225989d7e9d5aab0a174be0e19fcfa800b6797"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bfdc4668ac56687a89ca3eca44231144a2e9d02ba3b877558db74ba20e2bd9fa"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2992591e2294bb07faf7f5f6d5cb60710c046404f4bfce09fb488b85d2a8f58f"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4786b0af7511ea614fd86407a52a7bc161aa5772d311d97df2591ed2351de768"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-win32.whl", hash = "sha256:016de3b29a262655fc3d2075dc1b2611f84f4c3d97a71d579c883d45e201eee4"},
|
||||
{file = "lxml-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:52c0acc2f29b0a204efc11a5ed911a74f50a25eb7d7d5069c2b1fd3b3346ce11"},
|
||||
{file = "lxml-5.0.0-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:96095bfc0c02072fc89afa67626013a253596ea5118b8a7f4daaae049dafa096"},
|
||||
{file = "lxml-5.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:992029258ed719f130d5a9c443d142c32843046f1263f2c492862b2a853be570"},
|
||||
{file = "lxml-5.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:db40e85cffd22f7d65dcce30e85af565a66401a6ed22fc0c56ed342cfa4ffc43"},
|
||||
{file = "lxml-5.0.0-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:cfa8a4cdc3765574b7fd0c7cfa5fbd1e2108014c9dfd299c679e5152bea9a55e"},
|
||||
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:049fef98d02513c34f5babd07569fc1cf1ed14c0f2fbff18fe72597f977ef3c2"},
|
||||
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a85136d0ee18a41c91cc3e2844c683be0e72e6dda4cb58da9e15fcaef3726af7"},
|
||||
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:766868f729f3ab84125350f1a0ea2594d8b1628a608a574542a5aff7355b9941"},
|
||||
{file = "lxml-5.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99cad5c912f359e59e921689c04e54662cdd80835d80eeaa931e22612f515df7"},
|
||||
{file = "lxml-5.0.0-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:c90c593aa8dd57d5dab0ef6d7d64af894008971d98e6a41b320fdd75258fbc6e"},
|
||||
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8134d5441d1ed6a682e3de3d7a98717a328dce619ee9c4c8b3b91f0cb0eb3e28"},
|
||||
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f298ac9149037d6a3d5c74991bded39ac46292520b9c7c182cb102486cc87677"},
|
||||
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:894c5f71186b410679aaab5774543fcb9cbabe8893f0b31d11cf28a0740e80be"},
|
||||
{file = "lxml-5.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9cd3d6c2c67d4fdcd795e4945e2ba5434909c96640b4cc09453bd0dc7e8e1bac"},
|
||||
{file = "lxml-5.0.0.zip", hash = "sha256:2219cbf790e701acf9a21a31ead75f983e73daf0eceb9da6990212e4d20ebefe"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"},
|
||||
{file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"},
|
||||
{file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"},
|
||||
{file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"},
|
||||
{file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"},
|
||||
{file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"},
|
||||
{file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"},
|
||||
{file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"},
|
||||
{file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"},
|
||||
{file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"},
|
||||
{file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"},
|
||||
{file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"},
|
||||
{file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=3.0.7)"]
|
||||
source = ["Cython (>=3.0.8)"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
|
@ -3851,16 +3911,6 @@ files = [
|
|||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
|
@ -3919,59 +3969,59 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "maxminddb"
|
||||
version = "2.5.1"
|
||||
version = "2.5.2"
|
||||
description = "Reader for the MaxMind DB format"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:62e93a8e99937bf4307eeece3ca37e1161325ebf9363c4ce195410fb5daf64a0"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea2e27a507b53dfbf2ba2ba85c98682a1ad2dac3f9941a7bffa5cb86150d0c47"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01b0341bd6bee431bb8c07c7ac0ed221250c7390b125c025b7d57578e78e8a3"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:607344b1079ea647629bf962dcea7580ec864faaad3f5aae650e2e8652121d89"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c2901daebd7c8a702302315e7a58cdc38e626406ad4a05b4d48634897d5f5a3"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-win32.whl", hash = "sha256:7805ae8c9de433c38939ada2e376706a9f6740239f61fd445927b88f5b42c267"},
|
||||
{file = "maxminddb-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:f1e5bd58b71f322dc6c16a95a129433b1bc229d4b714f870a61c2367425396ee"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0bbbd58b300aaddf985f763720bdebba9f7a73168ff9f57168117f630ad1c06"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a6751e2e89d62d53217870bcc2a8c887dc56ae370ba1b74e52e880761916e54"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ecb1be961f1969be047d07743093f0dcf2f6d4ec3a06a4555587f380a96f6e7"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1e091c2b44673c218ee2df23adbc0b6d04fd5c646cfcb6c6fe26fb849434812a"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09b295c401c104ae0e30f66c1a3f3c2aa4ba2cbe12a787576499356a5a4d6c1"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-win32.whl", hash = "sha256:3d52c693baf07bba897d109b0ecb067f21fd0cc0fb266d67db456e85b80d699e"},
|
||||
{file = "maxminddb-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:4c67621e842c415ce336ab019a9f087305dfcf24c095b68b8e9d27848f6f6d91"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17ea454f61631b9815d420d48d00663f8718fc7de30be53ffcec0f73989475eb"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef4d508c899ce0f37de731340759c68bfd1102a39a873675c71fae2c8d71ad97"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e5ca423b1e310f0327536f5ed1a2c6e08d83289a7f909e021590b0b477cae2"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a21abd85e10e5e0f60244b49c3db17e7e48befd4972e62a62833d91e2acbb49"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:85a302d79577efe5bc308647394ffdc535dd5f062644c41103604ccf24931a05"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-win32.whl", hash = "sha256:dd28c434fb44f825dde6a75df2c338d44645791b03480af66a4d993f93801e10"},
|
||||
{file = "maxminddb-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:b477852cf1741d9187b021e23723e64b063794bbf946a9b5b84cc222f3caf58a"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a1e1a19f9740f586362f47862d0095b54d50b9d465babcaa8a563746132fe5be"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d654895b546a47e85f2e071b98e377a60bb03cd643b9423017fa66fcd5adedce"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0702da59b9670a72761b65cb1a52bc3032d8f6799bdab641cb8350ad5740580b"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2e20a70c1545d6626dcd4ce2d7ecf3d566d978ea64cb37e7952f93baff66b812"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0cbd272db3202e948c9088e48dec62add071a47971d84ceb11d2cb2880f83e5a"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-win32.whl", hash = "sha256:fbd01fc7d7b5b2befe914e8cdb5ed3a1c5476e57b765197cceff8d897f33d012"},
|
||||
{file = "maxminddb-2.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:fe0af3ba9e1a78ed5f2ad32fc18d18b78ef233e7d0c627e1a77a525a7eb0c241"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d772be68cce812f7c4b15ae8c68e624c8b88ff83071e3903ca5b5f55e343c25"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e7b3ad87d5352ed3f496bd42bffbf9f896245278b0d8e76afa1382e42a7ae"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:892c11a8694394e97d3ac0f8d5974ea588c732d14e721f22095c58b4f584c144"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3ce1f42bdfce7b86cb5a56cba730fed611fb879d867e6024f0d520257bef6891"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6667948e7501a513caef90edda2d367865097239d4c2381eb3998e9905af7209"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-win32.whl", hash = "sha256:500d321bdefe4dcd351e4390a79b7786aab49b0536bedfa0788e5ffb0e91e421"},
|
||||
{file = "maxminddb-2.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:93f7055779caf7753810f1e2c6444af6d727393fd116ffa0767fbd54fb8c9bbf"},
|
||||
{file = "maxminddb-2.5.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8cee4315da7cdd3f2a18f1ab1418953a7a9eda65e63095b01f03c7d3645d633e"},
|
||||
{file = "maxminddb-2.5.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97eac5af102cede4b5f57cecb25e8f949fa4e4a8d812bed575539951c60ecaf"},
|
||||
{file = "maxminddb-2.5.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:526744b12075051fa20979090c111cc3a42a3b55e2714818270c7b84a41a8cfe"},
|
||||
{file = "maxminddb-2.5.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fad45cd2f2e3c5fbebacb8d172a60fb22443222e549bf740a0bc7eeb849e5ce7"},
|
||||
{file = "maxminddb-2.5.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b98ed5c34955c48e72d35daed713ba4a6833a8a6d1204e79d2c85e644049792"},
|
||||
{file = "maxminddb-2.5.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639aee8abd63a95baa12b94b6f3a842d51877d631879c7d08c98c68dc44a84c3"},
|
||||
{file = "maxminddb-2.5.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a7a73ab4bbc16b81983531c99fa102a0c7dae459db958c17fea48c981f5e764"},
|
||||
{file = "maxminddb-2.5.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:aae262da1940a67c3ba765c49e2308947ce68ff647f87630002c306433a98ca1"},
|
||||
{file = "maxminddb-2.5.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b223c53077a736c304b63cf5afceb928975fbd12ddae5afd6b71370bab7b4700"},
|
||||
{file = "maxminddb-2.5.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:969d0057ea5472e0b574c5293c4f3ecf49585362351c543e8ea55dc48b60f1eb"},
|
||||
{file = "maxminddb-2.5.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4d36cf3d390f02d2bdf53d9efefb92be7bd70e07a5a86cdb79020c48c2d81b7"},
|
||||
{file = "maxminddb-2.5.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:188173c07dce0692fd5660a6eb7ea8c126d7b3a4b61496c8a8ee9e8b10186ff5"},
|
||||
{file = "maxminddb-2.5.1.tar.gz", hash = "sha256:4807d374e645bd68334e4f487ba85a27189dbc1267a98e644aa686a7927e0559"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5682963a5817066db50f219c33aaa7eb969888211a289a444c42b5dfa0c0f78"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fe6bb1b5ea132fcd9fd7b16c80247f0ba667018d5f9f98cd645b297e3b02fbf"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a3ec4b161e872cc615b7a09ae9770049e9794e7b3832e3d78905a65c5049d"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29d63e7711e5f95c7c190010e57dca9e262aee8ac300aaf75c3f7ede0b5a5863"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:08a540ec3661f6ca40499c86028e96dca5780e9d471b485dc797859b0b22dd22"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-win32.whl", hash = "sha256:17fdb691c389a0e956410d5baef9ad082a0aa67dd6aa231d193499e71a104c19"},
|
||||
{file = "maxminddb-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:d71b48d3dff9150a44e949b28fa5e7251a7a6895a3a77e200ce08410f096f12f"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1409a045eb04cebb297221eab1020c4f05434d02c0961410f6996ef474482998"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d839c480e4b93bb37bb1cc2777d77e6b2127c006e60b56f748f10571d8b0e471"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bca70905515fe50684974a9afaa7db4a4e9fbfdebcb0c2cde9db8e048e0d8145"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:67f97cd0c6aac39a51294b04a1e922532125285c24b18a58e2a9c92c7691fa9f"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1a3fab6bea6cc59444e6bad2a4fbf91228f6f51dcb29d09ed091930a475bd8cb"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-win32.whl", hash = "sha256:a99e3125528ea31e807f80e8c5b65118dc5cc122d0a435f1691a3cc1df55840c"},
|
||||
{file = "maxminddb-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:b6adf63695fa5e3d2549f7c2c9d82c6d252edd5c6ba67074637d2cb944143673"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ed504ca9f3c42e8e71bdbe21f5b818139a1448ac15d7bb6ce12cf41e3b7e2067"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5053231228d7cbf57d98a741b3cbee9efa9e689348dbb56c414e5a4c7f6f1c"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7e8688342bab592647313cd2054779bcd35ad85933424ceae9f07e3a9779986"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:335ee3140b41d4e751c14f8fae297aa064c7d3f184c9fbb2790336123187c440"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b0203fa2731da45e5461f6e8a0768e85bba8e02137a1598b3fcadf7cbfe8e6f2"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-win32.whl", hash = "sha256:8b89129de70e1629f200df9dfda4e4f477c26b05c29e0836604a00209c9466d5"},
|
||||
{file = "maxminddb-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:099f4e27feec4bb9658034a3eb853e746721fc15709030bee4f2f889f4a34185"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19d8d1e9bbc5281fb4c8112d541d2bd350fd8b5ddfbb43a6951e46df7cd27b9d"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94183a78628cad257183a88ce12a3bb9ffbfe0544bd0c1aafc1f9dc55629dd1b"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17de49660372dcccaa23958eccdd1c2464f92f594d027045ad76788db14a5da4"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae05c4f87b1dd9a21d430c52451eef5f3bd5af609d093408db91fe0dc4d8d7d1"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cb718908b9dffa10e02361094158ae68ded5a82c750de89737437999a81bafe"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-win32.whl", hash = "sha256:e0faa0c4c458eb0eb2f267daa7b106baef72c3c7ebcbece00b9e974fc8321412"},
|
||||
{file = "maxminddb-2.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:bac5a29fdc5df9222f7baecbcc4a88b309a66a7d147b34160940c0850ee4b9c5"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c204f53ef7c1d77e9fb0dba415dbb56419f2b08ccaca66cd772e29b3a793c3e7"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae98508a200db6f7ae5985a53039aba8eef7ed71d34b0a0e9c9145c3e6139fc3"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e9198d25e252b27d4e9526d5fcd4b78341c23153363a94f1246de5afcd39f6d"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b85b008f8e2cf3abfabdc24041549c51c97ea9a8bc46eeeadac8cec7acf9fbf0"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6f50210506e9818162ef6706d3127efb0575dfe2cc98a7236ca2011f1cc3effe"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-win32.whl", hash = "sha256:2bba43d370a57785f5ef61c10d0b4bf8de58d431da3c4c2ed78bb2ff3d07edbf"},
|
||||
{file = "maxminddb-2.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:2e01b09480b97d2ebe6765618fb12a0f52caa17368d6cf1f42481d6740428de7"},
|
||||
{file = "maxminddb-2.5.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd47d13376eaee2e8d1a1fb55d3d6ccdcc995bc931699967f7d5670ec6a454a3"},
|
||||
{file = "maxminddb-2.5.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abd626efaba4f0bc867462337f846796da0bb97b82125dbdbc63067947e353b0"},
|
||||
{file = "maxminddb-2.5.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ddbe547d83a2e28e81d9f59fd9708d3044ffb2398ee0f8df2e2a2e9cdea6646"},
|
||||
{file = "maxminddb-2.5.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:22184fa2514c15f5b39e4e2522f4f73d00afcf5eb7102c473f9376f3c3a03b81"},
|
||||
{file = "maxminddb-2.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5cb6702fbcc5b209ac3cffacd9cf0a5155feabbeb6fdcf497038be7cb6e52da6"},
|
||||
{file = "maxminddb-2.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c3ebfc0af00445089629faffa4c5a1fcc42a1ca5d7dffc42bba314fde20c6d"},
|
||||
{file = "maxminddb-2.5.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:461dcf0a4f67aa1c9faea6d52c4060d39559bf68e99a514cf8c1e01af383f90b"},
|
||||
{file = "maxminddb-2.5.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e012e889639aab411f5483990188da51c968377f665dcb90584971dbf314d50a"},
|
||||
{file = "maxminddb-2.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:20596e452d03071db37a72c8ef9236126c04ed342864f68db0adf0d1bc9f642e"},
|
||||
{file = "maxminddb-2.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ec51b66774b102824c9a3dd4916356283f6a61db1868d4ebcb98bf26486718e"},
|
||||
{file = "maxminddb-2.5.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fda0dd512f345cc92492f96c61a0df47efc2e2064c15e8053ab2114b362d64d"},
|
||||
{file = "maxminddb-2.5.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:862fcfe226ebda29a537cdce678dc8dc71ca6540ad2483099f80c6a1ee4cdbdd"},
|
||||
{file = "maxminddb-2.5.2.tar.gz", hash = "sha256:b3c33e4fc7821ee6c9f40837116e16ab6175863d4a64eee024c5bec686690a87"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -4274,13 +4324,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "netaddr"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
description = "A network address manipulation library for Python"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "netaddr-0.10.0-py2.py3-none-any.whl", hash = "sha256:8752f96c8fc24162edbf5b73d3e464b5d88e62869917582daa37b2695b65afb4"},
|
||||
{file = "netaddr-0.10.0.tar.gz", hash = "sha256:4c30c54adf4ea4318b3c055ea3d8c7f6554a50aa2cd8aea4605a23caa0b0229e"},
|
||||
{file = "netaddr-0.10.1-py2.py3-none-any.whl", hash = "sha256:9822305b42ea1020d54fee322d43cee5622b044c07a1f0130b459bb467efcf88"},
|
||||
{file = "netaddr-0.10.1.tar.gz", hash = "sha256:f4da4222ca8c3f43c8e18a8263e5426c750a3a837fdfeccf74c68d0408eaa3bf"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
|
@ -4370,13 +4420,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.6.1"
|
||||
version = "1.9.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.7.1"
|
||||
files = [
|
||||
{file = "openai-1.6.1-py3-none-any.whl", hash = "sha256:bc9f774838d67ac29fb24cdeb2d58faf57de8b311085dcd1348f7aa02a96c7ee"},
|
||||
{file = "openai-1.6.1.tar.gz", hash = "sha256:d553ca9dbf9486b08e75b09e8671e4f638462aaadccfced632bf490fc3d75fa2"},
|
||||
{file = "openai-1.9.0-py3-none-any.whl", hash = "sha256:5774a0582ed82f6de92200ed5024e03e272b93e04e9d31caeda5fb80f63df50d"},
|
||||
{file = "openai-1.9.0.tar.gz", hash = "sha256:3e9947a544556c051fa138a4def5bd8b468364ec52803c6628532ab949ddce55"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -4499,13 +4549,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "oslo-config"
|
||||
version = "9.2.0"
|
||||
version = "9.3.0"
|
||||
description = "Oslo Configuration API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "oslo.config-9.2.0-py3-none-any.whl", hash = "sha256:b98e50b19161fc76f25905ff74043e239258a3ebe799a5f9070d285e3c039dee"},
|
||||
{file = "oslo.config-9.2.0.tar.gz", hash = "sha256:ffeb01ca65a603d5525905f1a88a3319be09ce2c6ac376c4312aaec283095878"},
|
||||
{file = "oslo.config-9.3.0-py3-none-any.whl", hash = "sha256:5642e75ab8070aee96563670b1c1ee3b6f3cac3c0302fe7fc78973cd4b4e3d29"},
|
||||
{file = "oslo.config-9.3.0.tar.gz", hash = "sha256:a4b1e526135d67c0e9b14d3ed299c6ec8a3887f92afcb26f4f3ea918504a3554"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -4547,13 +4597,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "oslo-serialization"
|
||||
version = "5.2.0"
|
||||
version = "5.3.0"
|
||||
description = "Oslo Serialization library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "oslo.serialization-5.2.0-py3-none-any.whl", hash = "sha256:c7ec759192a787c7e1a5e765920bb594752c75e6e0cd5a9a82c385a9088125e5"},
|
||||
{file = "oslo.serialization-5.2.0.tar.gz", hash = "sha256:9cf030d61a6cce1f47a62d4050f5e83e1bd1a1018ac671bb193aee07d15bdbc2"},
|
||||
{file = "oslo.serialization-5.3.0-py3-none-any.whl", hash = "sha256:0da7248d0e515b875ef9883e3631ff51f9a8d11e8576247f0ded890f3276c0bf"},
|
||||
{file = "oslo.serialization-5.3.0.tar.gz", hash = "sha256:228898f4f33b7deabc74289b32bbd302a659c39cf6dda9048510f930fc4f76ed"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -4570,13 +4620,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "oslo-utils"
|
||||
version = "6.3.0"
|
||||
version = "7.0.0"
|
||||
description = "Oslo Utility library"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "oslo.utils-6.3.0-py3-none-any.whl", hash = "sha256:6bac2e56650f502caae6c0e8ba6e5eda3d7a16743d115f8836cad54538dd667f"},
|
||||
{file = "oslo.utils-6.3.0.tar.gz", hash = "sha256:758d945b2bad5bea81abed80ad33ffea1d1d793348ac5eb5b3866ba745b11d55"},
|
||||
{file = "oslo.utils-7.0.0-py3-none-any.whl", hash = "sha256:dbb724041a2ea0c342d524c4d7c7f07c8bc5016f4762d38c6a41b2ef805b3a8e"},
|
||||
{file = "oslo.utils-7.0.0.tar.gz", hash = "sha256:5263c00980cfab74f6635ef61d0fc91e6bd4a8dd0e78a77897ed6e447c8c6731"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -4966,22 +5016,22 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "4.25.1"
|
||||
version = "4.25.2"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"},
|
||||
{file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"},
|
||||
{file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"},
|
||||
{file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"},
|
||||
{file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"},
|
||||
{file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"},
|
||||
{file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"},
|
||||
{file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"},
|
||||
{file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"},
|
||||
{file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"},
|
||||
{file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"},
|
||||
{file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"},
|
||||
{file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"},
|
||||
{file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"},
|
||||
{file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"},
|
||||
{file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"},
|
||||
{file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"},
|
||||
{file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"},
|
||||
{file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"},
|
||||
{file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"},
|
||||
{file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"},
|
||||
{file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
|
@ -5775,11 +5825,9 @@ files = [
|
|||
{file = "pymssql-2.2.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:049f2e3de919e8e02504780a21ebbf235e21ca8ed5c7538c5b6e705aa6c43d8c"},
|
||||
{file = "pymssql-2.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd86d8e3e346e34f3f03d12e333747b53a1daa74374a727f4714d5b82ee0dd5"},
|
||||
{file = "pymssql-2.2.8-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:508226a0df7cb6faeda9f8e84e85743690ca427d7b27af9a73d75fcf0c1eef6e"},
|
||||
{file = "pymssql-2.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:47859887adeaf184766b5e0bc845dd23611f3808f9521552063bb36eabc10092"},
|
||||
{file = "pymssql-2.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d873e553374d5b1c57fe1c43bb75e3bcc2920678db1ef26f6bfed396c7d21b30"},
|
||||
{file = "pymssql-2.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf31b8b76634c826a91f9999e15b7bfb0c051a0f53b319fd56481a67e5b903bb"},
|
||||
{file = "pymssql-2.2.8-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:821945c2214fe666fd456c61e09a29a00e7719c9e136c801bffb3a254e9c579b"},
|
||||
{file = "pymssql-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:cc85b609b4e60eac25fa38bbac1ff854fd2c2a276e0ca4a3614c6f97efb644bb"},
|
||||
{file = "pymssql-2.2.8-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ebe7f64d5278d807f14bea08951e02512bfbc6219fd4d4f15bb45ded885cf3d4"},
|
||||
{file = "pymssql-2.2.8-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:253af3d39fc0235627966817262d5c4c94ad09dcbea59664748063470048c29c"},
|
||||
{file = "pymssql-2.2.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c9d109df536dc5f7dd851a88d285a4c9cb12a9314b621625f4f5ab1197eb312"},
|
||||
|
@ -5795,13 +5843,11 @@ files = [
|
|||
{file = "pymssql-2.2.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3906993300650844ec140aa58772c0f5f3e9e9d5709c061334fd1551acdcf066"},
|
||||
{file = "pymssql-2.2.8-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7309c7352e4a87c9995c3183ebfe0ff4135e955bb759109637673c61c9f0ca8d"},
|
||||
{file = "pymssql-2.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9b8d603cc1ec7ae585c5a409a1d45e8da067970c79dd550d45c238ae0aa0f79f"},
|
||||
{file = "pymssql-2.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:293cb4d0339e221d877d6b19a1905082b658f0100a1e2ccc9dda10de58938901"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:895041edd002a2e91d8a4faf0906b6fbfef29d9164bc6beb398421f5927fa40e"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6b2d9c6d38a416c6f2db36ff1cd8e69f9a5387a46f9f4f612623192e0c9404b1"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63d6f25cf40fe6a03c49be2d4d337858362b8ab944d6684c268e4990807cf0c"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c83ad3ad20951f3a94894b354fa5fa9666dcd5ebb4a635dad507c7d1dd545833"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3933f7f082be74698eea835df51798dab9bc727d94d3d280bffc75ab9265f890"},
|
||||
{file = "pymssql-2.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:de313375b90b0f554058992f35c4a4beb3f6ec2f5912d8cd6afb649f95b03a9f"},
|
||||
{file = "pymssql-2.2.8.tar.gz", hash = "sha256:9baefbfbd07d0142756e2dfcaa804154361ac5806ab9381350aad4e780c3033e"},
|
||||
]
|
||||
|
||||
|
@ -6290,7 +6336,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
|
@ -6298,15 +6343,8 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
|
@ -6323,7 +6361,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
|
@ -6331,7 +6368,6 @@ files = [
|
|||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
|
@ -6595,13 +6631,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "service-identity"
|
||||
version = "23.1.0"
|
||||
version = "24.1.0"
|
||||
description = "Service identity verification for pyOpenSSL & cryptography."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "service_identity-23.1.0-py3-none-any.whl", hash = "sha256:87415a691d52fcad954a500cb81f424d0273f8e7e3ee7d766128f4575080f383"},
|
||||
{file = "service_identity-23.1.0.tar.gz", hash = "sha256:ecb33cd96307755041e978ab14f8b14e13b40f1fbd525a4dc78f46d2b986431d"},
|
||||
{file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
|
||||
{file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -6611,7 +6647,7 @@ pyasn1 = "*"
|
|||
pyasn1-modules = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pyopenssl", "service-identity[docs,idna,mypy,tests]"]
|
||||
dev = ["pyopenssl", "service-identity[idna,mypy,tests]"]
|
||||
docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"]
|
||||
idna = ["idna"]
|
||||
mypy = ["idna", "mypy", "types-pyopenssl"]
|
||||
|
@ -7051,13 +7087,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.0"
|
||||
version = "5.14.1"
|
||||
description = "Traitlets Python configuration system"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"},
|
||||
{file = "traitlets-5.14.0.tar.gz", hash = "sha256:fcdaa8ac49c04dfa0ed3ee3384ef6dfdb5d6f3741502be247279407679296772"},
|
||||
{file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"},
|
||||
{file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -7218,6 +7254,28 @@ type = "legacy"
|
|||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.2"
|
||||
description = "tzinfo object for the local timezone"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
|
||||
{file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
|
||||
|
||||
[package.source]
|
||||
type = "legacy"
|
||||
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||
reference = "tsinghua"
|
||||
|
||||
[[package]]
|
||||
name = "ua-parser"
|
||||
version = "0.18.0"
|
||||
|
@ -7371,13 +7429,13 @@ reference = "tsinghua"
|
|||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"},
|
||||
{file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"},
|
||||
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
|
||||
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
|
@ -7827,4 +7885,4 @@ reference = "tsinghua"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "5474eb8b2e55c714fc812ea637bf83fe40c5acaadebccc10072e30101ab9ba13"
|
||||
content-hash = "3f228326a11742303de60a3b1bb8f748b6efc5a3dae1aa200e8a5cc6c9df9c0a"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue